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 ...
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 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.