xây dựng Customer Relationship Management sử dụng Graph API và REST

Bài trước, ta đã tìm hiểu nhũng khái niệm cơ bản và cách cài đặt xây dựng 1 mối quan hệ đơn giản thông qua Neo4j - Graph database. Bài này ta sẽ đi sâu hơn để giải quyết những vấn đề phức tạp hơn bằng việc xây dựng 1 hệ thống CRM (Customer Relationship Management).

Trước khi bắt đầu ta cần hiểu sơ qua về CRM. CRM (Customer Relationship Management) tạm dịch là quản lý mối quan hệ khách hàng. Đơn giản có thể hiểu CRM là tập hợp các công tác quản lý, chăm sóc và xây dựng mối quan hệ giữa các khách hàng và doanh nghiệp.

Ý tưởng

Chúng ta sẽ sử dụng biểu đồ Sticks and Bubbles (mũi tên và hình tròn) để mô tả các mối quan hệ. các ô tròn sẽ là các đối tượng còn mũi tên biểu thị mối quan hệ giữa các đối tượng đó. Ví dụ của chúng ta là xây dựng quan hệ giữa territory manager và account manager

Image

chúng ta có thể thấy với mỗi đối tượng (node) lại có nhiều attribute (label, title, name) . 2 đối tượng này có mối quan hệ Managers với nhau và ta có thể thấy Linda là manager của Jeff

Neo4j Cypher Query

Với ngôn ngữ Cyphẻ, để tạo các node và attribute như trên thì ta thực hiện như sau:

CREATE (:Person {name:"Linda Barnes", title:"Territory Manager"} );
CREATE (:Person {name:"Jeff Dudley", title:"Account Manager"} );

trong đó các node sẽ được khai báo viết hoa kèm theo dấu 2 chấm đằng trước. Còn các attribute thì được gần như khai báo giống như khai báo hash trong ruby, đặc biêtj là neo4j không giới hạn số attribute chứ ko không cưng nhắc như Mysql hay PostgreSQL.

Tiếp đó để biểu thị mối quán hệ giữa các node ta thực hiện như sau

MATCH ( a:Person {name:"Linda Barnes"} ), ( b:Person {name:"Jeff Dudley"} )
CREATE (a)-[:Manages]->(b);

để biểu thị rõ ràng hướng của quan hệ giữa 2 node, người ta sử dụng ký hiệu >, thực tế là có thể tối giản được ký hiệu này

Sử dụng Neo4j với ruby

Bài trước ta đã thiết lập và cài đặt được Neo4j và khởi động server neo4j. Để thực hiện ví dụ này, ta cũng khởi động server neo4j

neo4j start

Sau đó ta truy cập màn hình quản lý của neo4j

 localhost:7474

Để sử dụng rest API ta cần cài một số thư viện thông qua gem

 gem install rest-client
gem install json

Như bài trước, với mục đích training nên ta sẽ xuyên suốt trong 1 class. Ta tạo 1 file rgraph.rb và khai báo class RGraph

require 'json'
require 'rest_client'

class RGraph

  def initialize
    @url = 'http://localhost:7474/db/data/cypher'
  end
end

đường dẫn /db/data/cypher là mặc định cho tất cả các API sử dụng ngôn ngữ Cypher

Bây giờ ta sẽ thực hiện hàm khởi tạo node

def create_node(label,attr={})
  query = ''  # khai báo biến query dạng string
  attributes = '' # biến lưu tên các attribute
  if attr.size == 0
    # nếu ko có attribute thì sẽ khởi tạo 1 node
    query += "CREATE (:#{label});"
  else
    # Create the attribute clause portion of the query
    attributes += '{ '
    attr.each do |key,value|
      attributes += "#{key.to_s}: '#{value}',"
    end
    attributes.chomp!(',') # xoá dấu phẩy cuối
    attributes += ' }'
    query += "CREATE (:#{label} " + attributes + ');'
  end
  c = {
      "query" => "#{query}",
      "params" => {}
  }
  RestClient.post @url, c.to_json, :content_type => :json, :accept => :json
end

Một chú ý quan trọng là khi khởi tạo node thì label là bắt buộc còn các attribute là optional tuy nhiên thì hiếm khi khởi tạo 1 node mà ko đi kèm theo các thuộc tính vì các attribute cung cấp các thông tin của node.

Bây giờ ta sẽ tạo quan hệ giữa 2 node. Ta dùng MATCH để kết nối các node và sử dụng CREATE để thực hiện câu lệnh

def create_directed_relationship (from_node, to_node, rel_type)
  query = ''
  attributes = ''
  query += "MATCH ( a:#{from_node[:type]} "
  from_node.each do |key,value|
    next if key == :type # nếu attribute là `type` thì bỏ qua
    attributes += "#{key.to_s}: '#{value}',"
  end
  attributes.chomp!(',') # bỏ dấu phẩy cuối
  query += "{ #{attributes} }),"
  attributes = '' # Reset attribut để thực hiện câu lệnh match tiếp theo
  query += " ( b:#{to_node[:type]} "
  to_node.each do |key,value|
    next if key == :type
    attributes += "#{key.to_s}: '#{value}',"
  end
  attributes.chomp!(',')
  query += "{ #{attributes} }) "
  # node a và node b đã được khai báo , giờ ta sẽ thưc hiện nối chúng lại
  query += "CREATE (a)-[:#{rel_type}]->(b);"
  c = {
      "query" => "#{query}",
      "params" => {}
  }
  RestClient.post @url, c.to_json, :content_type => :json, :accept => :json
end
  • from_nodeto_node là các hash để xác định node nào là node nguồn, node nào là node đích. Các thông tin của các node sẽ được lưu vào MATCH và sau đó add vào biến query.

CRM Database

Trên đây ta đã thực hiện các bước cơ bản để khởi tạo node và thiết lập quan hệ giữa các node. Tiếp theo ta sẽ hoàn thành khai báo các node và quan hệ theo biểu đồ 1 hệ thống CRM đơn giản sau:

Image

Cấu trúc của CRM này sẽ cho phép các nhà quản lý lãnh thổ (trong ví dụ của ta là Linda) thực hiện mọi quyền. Ví dụ, Linda có thể đặt câu hỏi, "Trong tất cả các công ty trong lãnh thổ của tôi, là tất cả những người quản lý khách hàng của chúng tôi đã không liên lạc nào, và ai là những nhà quản lý tài khoản có trách nhiệm liên quan?"

Với sơ đồ như trên ta có thể tóm tắt lại như sau:

  • Hệ thống có một quản lý cấp cao nhất gọi là tổng giams đốc và ngtuwowif này sẽ quản lý 3 account managers.
  • Mỗi account manager sẽ quản lý 1 công ty.
  • Mỗi công ty sẽ có các nhân viên và các manger quản lý các nhân viên đó
  • Các manager sẽ có các cuộc gặp gỡ với khách hàng

Chúng ta sẽ mô tả dữ liệu dưới dạng key hash của ruby và ta cũng sẽ chỉ mô tả 1 phần ví dụ dử liệu bên sơ đồ trên bằng cách khai báo biến @data trong hàm initialize.

@data = {
    nodes: [
        {
            label: 'Person',
            title: 'Territory Manager',
            name: 'Linda Barnes'
        },
        {
            label: 'Person',
            title: 'Account Manager',
            name: 'Jeff Dudley',
        },
        # ...
        {
            label: 'Company',
            name: 'OurCompany, Inc.'
        },
        {
            label: 'Company',
            name: 'Acme, Inc.'
        },
        {
            label: 'Company',
            name: 'Wiley, Inc.'
        },
        {
            label: 'Company',
            name: 'Coyote, Ltd.'
        },
    ],
    relationships: [
        {
            type: 'MANAGES',
            source: 'Linda Barnes',
            destination: ['Jeff Dudley', 'Mike Wells', 'Vanessa Jones']
        },
        {
            type: 'MANAGES',
            source: 'Jesse Hoover',
            destination: ['Ralph Green', 'Patricia McDonald']
        },
        # ...
        {
            type: 'WORKS_FOR',
            destination: 'OurCompany, Inc.',
            source: ['Linda Barnes', 'Jeff Dudley', 'Mike Wells', 'Vanessa Jones']
        },
        {
            type: 'WORKS_FOR',
            destination: 'Acme, Inc.',
            source: ['Jesse Hoover', 'Ralph Green', 'Sheila Foxworthy', 'Janet Huxley-Smith',
                     'Tim Reynolds', 'Zachary Meyer', 'Milton Stacey', 'Steve Nauman', 'Patricia McDonald']
        },
        # ...
        {
            type: 'ACCOUNT_MANAGES',
            source: 'Jeff Dudley',
            destination: 'Acme, Inc.'
        },
        {
            type: 'ACCOUNT_MANAGES',
            source: 'Mike Wells',
            destination: 'Wiley, Inc.'
        },
        {
            type: 'ACCOUNT_MANAGES',
            source: 'Vanessa Jones',
            destination: 'Coyote, Ltd.'
        },
        {
            type: 'HAS_MET_WITH',
            source: 'Jeff Dudley',
            destination: ['Tim Reynolds', 'Zachary Meyer', 'Janet Huxley-Smith', 'Patricia McDonald']
        },
        {
            type: 'HAS_MET_WITH',
            source: 'Mike Wells',
            destination: ['Francine Gonzalez', 'Tsunomi Ito', 'Frank Cutler']
        },
        {
            type: 'HAS_MET_WITH',
            source: 'Vanessa Jones',
            destination: 'Tracey Stankowski'
        }
    ]

Tương tự như trên ta sẽ viết 2 hàm giống 2 hàm create_nodecreate_directed_relationship ở trên nhưng ta sẽ viết dứoi dạng số nhiều vì số lươnhj node à attribute của chúng ta khá nhiều.

def create_nodes
  # Scan file, find each node and create it in Neo4j
  @data.each do |key,value|
    if key == :nodes
      @data[key].each do |node| # lặp các key trong @data
        next unless node.has_key?(:label) # bỏ qua các node ko có label
        label = node[:label]
        attr = Hash.new
        node.each do |k,v|
          next if k == :label # ta sẽ ko tạo attribute khi key = "label"
          attr[k] = v
        end
        create_node(label,attr)
      end
    end
  end
end

Ta thấy có 3 vòng lặp. Vòng lăp chính sẽ lấy dữ liệu từ các node . Vòng lặp thứ 2 sẽ lọc bỏ các node không có label. Vòng lặp cuối sẽ tổng các dữ liệu trong các node get được từ vòng lặp thứ 2 rồi gọi hàm create_node ở trên. Nó sẽ push tất cả dữ liệu lên Neo4j database.

Tiếp đó ta viết function create_directed_relationships giống hàm bên trên. Ta cũng lặp data để lấy các key và thiết lập relation rồi gọi hàm create_directed_relationship bên trêntrên

def create_directed_relationships
  # Scan file, look for relationships and their respective nodes
  @data.each do |key,value|
    if key == :relationships
      @data[key].each do |relationship| # Cycle through each relationship
        next unless relationship.has_key?(:type) &&
            relationship.has_key?(:source) &&
            relationship.has_key?(:destination)
        rel_type = relationship[:type]
        case rel_type
          # Handle the different types of cases
          when 'MANAGES', 'ACCOUNT_MANAGES', 'HAS_MET_WITH'
            # in all cases, we have one :Person source and one or more destinations
            from_node = {type: 'Person', name: relationship[:source]}
            to_node = (rel_type == 'ACCOUNT_MANAGES') ? {type: 'Company'} : {type: 'Person'}
            if relationship[:destination].class == Array
              # multiple destinations
              relationship[:destination].each do |dest|
                to_node[:name] = dest
                create_directed_relationship(from_node,to_node,rel_type)
              end
            else
              to_node[:name] = relationship[:destination]
              create_directed_relationship(from_node,to_node,rel_type)
            end
          when 'WORKS_FOR'
            # one destination, one or more sources
            to_node = {type: 'Company', name: relationship[:destination]}
            from_node = {type: 'Person'}
            rel_type = 'WORKS_FOR'
            if relationship[:source].class == Array
              # multiple sources
              relationship[:source].each do |src|
                from_node[:name] = src
                create_directed_relationship(from_node,to_node,rel_type)
              end
            else
              from_node[:name] = relationship[:source]
            end
        end
      end
    end
  end
end

Cuối cùng để hoàn thiện database ta cần khởi tạo class và gọi 2 hàm create node và relationship

rGraph = RGraph.new
rGraph.create_nodes
rGraph.create_directed_relationships

Cơ sở dữ liệu này sẽ tạo các quan hệ giữa người bán hàng với khách hàng thông qua HAS_MET_WITH. Tổng giám đóc dựa vào đó có thể biết được những người quản lý nào không gặp khachs hàng của công ty và từ đó truy ra trách nhiệm ...

ta sẽ viết tắt các vị trí Account Manager làm viêc tại công ty "OurCompany" là am, manager của các target account là tm và các target account company là tc.

Sử dụng ngôn ngữ Cypher để tạo các liên kết

MATCH (am:Person), (tm:Person), (tc:Company)
WHERE (am {title:"Account Manager"})-[:WORKS_FOR]->(:Company {name:"OurCompany, Inc."})
AND (am)-[:ACCOUNT_MANAGES]->(tc)
AND (tm)-[:WORKS_FOR]->(tc)
AND (tm)-[:MANAGES]->()
AND NOT (am)-[:HAS_MET_WITH]->(tm)
return am.name,tm.name,tc.name;
  • Account manager am được xác định qua title Account Manager và làm việc tại OurCompany, Inc.
(am {title:"Account Manager"})-[:WORKS_FOR]->(:Company {name:"OurCompany, Inc."})
  • target_company tc được xác định thông qua ACCOUT_MANAGER:
(am)-[:ACCOUNT_MANAGES]->(tc)
  • target account company được xác định thông qua action WORKS_FOR với target company tc
(tm)-[:WORKS_FOR]->(tc)

và để xác định những account_manager chưa gặp target_manager, ta dùng action HAS_MET_WITH

NOT (am)-[:HAS_MET_WITH]->(tm)

và biểu thị thông qua ruby functyion:

def find_managers_not_met
  query =  'MATCH (am:Person), (tm:Person), (tc:Company)'
  query += 'WHERE (am {title:"Account Manager"})-[:WORKS_FOR]->(:Company {name:"OurCompany, Inc."})'
  query += 'AND (am)-[:ACCOUNT_MANAGES]->(tc)'
  query += 'AND (tm)-[:WORKS_FOR]->(tc)'
  query += 'AND (tm)-[:MANAGES]->()'
  query += 'AND NOT (am)-[:HAS_MET_WITH]->(tm)'
  query += 'return am.name,tm.name,tc.name;'
  c = {
      "query" => "#{query}",
      "params" => {}
  }
  response = RestClient.post @url, c.to_json, :content_type => :json, :accept => :json
  puts JSON.parse(response)
end

Ta thu được kết quả:

{
  "columns"=>["am.name", "tm.name", "tc.name"],
  "data"=>[
    ["Jeff Dudley", "Jesse Hoover", "Acme, Inc."],
    ["Jeff Dudley", "Ralph Green", "Acme, Inc."],
    ["Mike Wells", "Mary Galloway", "Wiley, Inc."],
    ["Vanessa Jones", "George Quincy", "Coyote, Ltd."]
  ]
}

Chú thích đầu ra của data như sau: Jeff Dudley chưa gặp quản lý của công ty Acme, Inc. - Jesse Hoover.

Kết luận

Graph database được sử dụng rộng rãi để biểu thị các quan hệ dữ liệu dưới dạng node. Loạ DB này rất hữu hiệu với những trường hợp dữ liệu có quan hệ phức tạp mà ta lại cần truy vấn với tốc độ cao. Bài này cũng giúp ta làm quen với ngôn ngữ Cypher và ta cũng có thể dùng ngôn ngữ Cypher để query trực tiếp khi gọi Rest API.