xây dựng Customer Relationship Management sử dụng Graph API và REST
Bài đăng này đã không được cập nhật trong 8 năm
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
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_node
vàto_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:
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_node
và create_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 titleAccount Manager
và làm việc tạiOurCompany, 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 companytc
(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.
All rights reserved