关于数据库:图数据库|如何从零到一构建一个企业股权图谱系统

7次阅读

共计 12311 个字符,预计需要花费 31 分钟才能阅读完成。

本文首发于 Nebula Graph Community 公众号

咱们晓得无论是监管部门、企业还是集体,都有需要去针对一个企业、法人做一些背景考察,这些考察能够是法律诉讼、公开持股、企业任职等等多种多样的信息。这些背景信息能够辅助咱们做商业上的重要决策,躲避危险:比方依据公司的股权关系,理解是否存在利益冲突比方是否抉择与一家公司进行商业往来。

在满足这样的关系剖析需要的时候,咱们往往面临一些挑战,比方:

  1. 如何将这些数据的关联关系体现在零碎之中?使得它们能够被开掘、利用
  2. 多种异构数据、数据源之间的关系可能随着业务的倒退引申出更多的变动,在构造数据库中,这意味着 Schema 变更
  3. 剖析零碎须要尽可能实时获取须要的查问后果,这通常波及到多跳关系查问
  4. 领域专家是否疾速灵便、可视化获取分享信息

那么如何构建这样一个零碎解决以上挑战呢?

数据存在哪里?

前提:数据集筹备,为了更好的给大家演示解决这个问题,我写了一个轮子能随机生成股权结构相干的数据,生成的数据的例子在这里。

这里,咱们有法人、公司的数据,更有公司与子公司之间的关系,公司持有公司股份,法人任职公司,法人持有公司股份和法人之间亲密度的关系数据。

数据存在哪里?这是一个要害的问题,这里咱们剧透一下,答案是:图数据库。而后咱们再简略解释一下为什么这样一个股权图谱零碎跑在图数据库上是更好的。

在这样一个简略的数据模型之下,咱们能够很间接的在关系型数据库中这么建模:

而这么建模的问题在于:这种逻辑关联的形式使得无论数据的关联关系查问表白、存储、还是引入新的关联关系都不是很高效。

  • 查问表白不高效 是因为关系型数据库是面向表结构设计的,这决定了关系查问要写嵌套的 JOIN。

    • 这就是前边提到的 挑战 1:可能表白,然而比拟勉强,遇到略微简单的状况就变得很难。
  • 存储不高效 是因为表构造被设计的模式是面向数据记录,而非数据之间的关系:咱们尽管习惯了将数据中实体(比方法人)和实体关联(比方持有股权 hold_sharing_relationship)以另外一个表中的记录来表白、存储起来,这逻辑上齐全行得通,然而到了多跳、大量须要申请数据关系跳转的状况下,这样跨表 JOIN 的代价就成为了瓶颈。

    • 这就是前边提到的 挑战 3:无奈应答多条查问的性能须要。
  • 引入新的关联关系 代价大,还是前边提到的,表构造下,用新的表来表白持有股权 hold_sharing_relationship这个关联关系是可行的,然而这十分不灵便、而且低廉,它意味着咱们在引入这个关系的时候限定了终点起点的类型,比方股权持有的关系可能是法人 -> 公司,也可能是公司 -> 公司,随着业务的演进,咱们可能还须要引入政府 -> 公司的新关系,而这些变动都须要做有不小代价的工作:改变 Schema。

    • 这就是前边提到的 挑战 2:无奈应答业务上对数据关系上灵便多变的要求。

当一个通用零碎无奈满足不可漠视的具体需要的时候,一个新的零碎就会诞生,这就是图数据库,针对这样的场景,图数据库很天然地特地针对关联关系场景去设计整个数据库:

  • 面向关联关系表白的语义。(挑战 1)

    • 如下表,我列举了一个等价的一跳查问在表构造数据库与图数据库中,查问语句的区别。大家应该能够看出“找到所有持有和 p_100 独特持有公司股份的人”这样的查问表白能够在图数据库如何天然表白,这仅仅是一条查问的区别,如果是多跳的话,他们的复杂度辨别还会更显著一些。
表构造数据库 图数据库(属性图)
  • 将关联关系存储为物理连贯,从而使得跳转查问代价最小。(挑战 3、2)

    • 图数据之中,从点拓展(找到一个或者多个关系的另一头)进来的代价是十分小的,这因为图数据库是一个专有的零碎,得益于它次要关怀“图”构造的设计,查找确定的实体(比方和一个法人 A)所有关联(可能是任职、亲戚、持有、等等关系)其余所有实体(公司、法人)这个查找的代价是 O(1) 的,因为它们在图数据库的数据机构里是真的链接在一起的。
    • 大家能够从下表的定量参考数据一窥图数据库在这种查问下的劣势,这种劣势在多跳高并发状况下的区别是“能”与”不能“作为线上零碎的区别,是“实时”与“离线”的区别。
    • 在面向关联关系的数据建模和数据结构之下,引入新的实体、关联关系的代价要小很多,还是前边提到的例子:
      在 Nebula Graph 图数据中引入一个新的“政府机构”类型的实体,并减少政府机构 -> 公司的“持有股份”的关联关系相比于在非图模型的数据库中的代价小很多。
表构造数据库 图数据库(属性图)
4 跳查问时延 1544 秒 4 跳查问时延 1.36 秒
  • 建模合乎直觉;图数据库有面向数据连贯的数据可视化能力(挑战 4)

    • 大家在下表第二列中能够比照咱们本文中进行的股权剖析数据在两种数据库之中的建模的区别,尤其是在关怀关联关系的场景下,咱们能够感触到属性图的模型建设是很合乎人类大脑直觉的,而这和大脑之中神经元的构造可能也有一些关系。
    • 图数据库中内置的可视化工具提供了个别用户便捷了解数据关系的能力,也给领域专家用户提供了表白申请简单数据关系的直观接口。
表构造数据库 图数据库(属性图)

表构造数据库与图数据库的总体比拟:

<!—

GO FROM "p_100" OVER hold_share YIELD dst(edge) AS corp_with_share |\
GO FROM $-.corp_with_share OVER hold_share REVERSELY YIELD properties(vertex).name;
SELECT a.id, a.name, c.name
FROM person a
JOIN hold_share b ON a.id=b.person_id
JOIN corp c ON c.id=b.corp_id
WHERE c.name IN (SELECT c.name
FROM person a
JOIN hold_share b ON a.id=b.person_id
JOIN corp c ON c.id=b.corp_id
WHERE a.id = 'p_100')

–>

表构造数据库 图数据库(属性图)
查问
建模
性能 4 跳查问时延 1544 秒 4 跳查问时延 1.36 秒

综上,在本教程里,咱们将利用图数据库来进行数据存储。

图数据建模

后面在探讨数据存在哪里的时候,咱们曾经揭示了在图数据库中建模的形式:实质上,在这张图中,将会有两种实体:

  • 公司

四种关系:

  • 作为亲人 –>
  • 作为角色 –> 公司
  • 或者 公司 持有股份 –> 公司
  • 公司 作为子机构 –> 公司

这外面,实体与关系自身都能够蕴含更多的信息,这些信息在图数据库里就是实体、关系本身的属性。如下图示意:

  • 的属性包含 nameage
  • 公司 的属性包含 namelocation
  • 持有股份 这个关系有属性 share(份额)
  • 任职 这个关系有属性 rolelevel

数据入库

本教程中,咱们应用的图数据库叫做 Nebula Graph(星云图数据库),它是一个以 Apache 2.0 许可证开源的分布式图数据库。

Nebula Graph in Github: https://github.com/vesoft-inc…

在向 Nebula Graph 导入数据的时候,对于如何抉择工具,请参考这篇文档和这个视频。

这里,因为数据格式是 csv 文件并且利用单机的客户端资源就足够了,咱们能够抉择应用 nebula-importer 来实现这个工作。

提醒:在导入数据之前,请先部署一个 Nebula Graph 集群,最简便的部署形式是应用 nebula-up 这个小工具,只须要一行命令就能在 Linux 机器上同时启动一个 Nebula Graph 外围和可视化图摸索工具 Nebula Graph Studio。如果你更违心用 Docker 部署,请参考这个文档。

本文假如咱们应用 Nebula-UP 来部署:

curl -fsSL nebula-up.siwei.io/install.sh | bash

这里的数据是生成器生成的,你能够按需生成任意规模随机数据集,或者抉择一份生成好了的数据在这里

有了这些数据,咱们能够开始导入了。

$ pip install Faker==2.0.5 pydbgen==1.0.5
$ python3 data_generator.py
$ ls -l data
total 1688
-rw-r--r--  1 weyl  staff   23941 Jul 14 13:28 corp.csv
-rw-r--r--  1 weyl  staff    1277 Jul 14 13:26 corp_rel.csv
-rw-r--r--  1 weyl  staff    3048 Jul 14 13:26 corp_share.csv
-rw-r--r--  1 weyl  staff  211661 Jul 14 13:26 person.csv
-rw-r--r--  1 weyl  staff  179770 Jul 14 13:26 person_corp_role.csv
-rw-r--r--  1 weyl  staff  322965 Jul 14 13:26 person_corp_share.csv
-rw-r--r--  1 weyl  staff   17689 Jul 14 13:26 person_rel.csv

导入工具 nebula-importer 是一个 golang 的二进制文件,应用形式就是将导入的 Nebula Graph 连贯信息、数据源中字段的含意的信息写进 YAML 格局的配置文件里,而后通过命令行调用它。能够参考文档或者它的 GitHub 仓库里的例子。

这里我曾经写好了筹备好了一份 nebula-importer 的配置文件,在数据生成器同一个 repo 之下的这里。

最初,只须要执行如下命令就能够开始数据导入了:

留神,在写本文的时候,nebula 的新版本是 2.6.1,这里对应的 nebula-importer 是 v2.6.0,如果您呈现导入谬误可能是版本不匹配,能够相应调整下边命令中的版本号。

git clone https://github.com/wey-gu/nebula-shareholding-example
cp -r data_sample /tmp/data
cp nebula-importer.yaml /tmp/data/
docker run --rm -ti \
    --network=nebula-docker-compose_nebula-net \
    -v /tmp/data:/root \
    vesoft/nebula-importer:v2.6.0 \
    --config /root/nebula-importer.yaml

你晓得吗?TL;DR

实际上,这份 importer 的配置里帮咱们做了 Nebula Graph 之中的图建模的操作,它们的指令在下边,咱们不须要手动去执行了。

CREATE SPACE IF NOT EXISTS shareholding(partition_num=5, replica_factor=1, vid_type=FIXED_STRING(10));
USE shareholding;
CREATE TAG person(name string);
CREATE TAG corp(name string);
CREATE TAG INDEX person_name on person(name(20));
CREATE TAG INDEX corp_name on corp(name(20));
CREATE EDGE role_as(role string);
CREATE EDGE is_branch_of();
CREATE EDGE hold_share(share float);
CREATE EDGE reletive_with(degree int);

图库中查问数据

Tips: 你晓得吗,你也能够无需部署装置,通过 Nebula-Playground 之中,找到股权穿透来在线拜访同一份数据集。

咱们能够借助 Nebula Graph Studio 来拜访数据,拜访咱们部署 Nebula-UP 的服务器地址的 7001 端口就能够了:

假如服务器地址为 192.168.8.127,则有:

  • Nebula Studio 地址:192.168.8.127:7001
  • Nebula Graph 地址:192.168.8.127:9669
  • 默认用户名:root
  • 默认明码:nebula

拜访 Nebula Studio:

抉择图空间: Shareholding

之后,咱们就能够在里边摸索比方一个公司的三跳以内的股权穿透,具体的操作能够参考:股权穿透在线 Playground 的介绍:

构建一个图谱零碎

这部分的代码开源在 GitHub 上:

https://github.com/wey-gu/neb…

本我的项目的 Demo 也在 PyCon China 2021 上的演讲中有过展现:视频地址

在此基础之上,咱们能够构建一个提供给终端用户来应用的股权查问零碎了,咱们曾经有了图数据库作为这个图谱的存储引擎,实践上,如果业务容许,咱们能够间接应用或者封装 Nebula Graph Studio 来提供服务,这齐全是可行也是合规的,不过,有一些状况下,咱们须要本人去实现界面、或者咱们须要封装出一个 API 给上游(多端)提供图谱查问的性能。

为此,我为大家写了一个简略的实例我的项目,提供这样的服务,他的架构也很间接:

  • 前端承受用户要查问的穿透法人、公司,按需发申请给后端,并用 D3.js 将返回后果渲染为关系图
  • 后端承受前端的 API 申请,将申请转换为 Graph DB 的查问,并返回前端期待的后果
  ┌───────────────┬───────────────┐
  │               │  Frontend     │
  │               │               │
  │    ┌──────────▼──────────┐    │
  │    │ Vue.JS              │    │
  │    │ D3.JS               │    │
  │    └──────────┬──────────┘    │
  │               │  Backend      │
  │    ┌──────────┴──────────┐    │
  │    │ Flask               │    │
  │    │ Nebula-Python       │    │
  │    └──────────┬──────────┘    │
  │               │  Graph Query  │
  │    ┌──────────▼──────────┐    │
  │    │ Graph Database      │    │
  │    └─────────────────────┘    │
  │                               │
  └───────────────────────────────┘

后端服务 –> 图数据库

具体的数据格式剖析大家能够参考这里

查问语句

咱们假如用户申请的实体是 c_132,那么申请 1 到 3 步的关系穿透的语法是:

MATCH p=(v)-[e:hold_share|:is_branch_of|:reletive_with|:role_as*1..3]-(v2) \
WHERE id(v) IN ["c_132"] RETURN p LIMIT 100

这里边 ()包裹的是图之中的点,而[] 包裹的则是点之间的关系:边,所以:

(v)-[e:hold_share|:is_branch_of|:reletive_with|:role_as*1..3]-(v2) 之中的:

(v)-[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]-(v2)应该比拟好了解,意思是从 vv2 做拓展。

当初咱们介绍两头 [] 包裹的局部,这里,它的语义是:经由四种类型的边(:之后的是边的类型,|代表或者)通过可变的跳数:*1..3(一跳到三跳)。

所以,简略来说整顿看开,咱们的拓展的门路是:从点 v 开始,经由四种关系一到三跳拓展到点v2,返回整个拓展门路 p,限度 100 个门路后果,其中 vc_132

Nebula Python Client/ SDK

咱们曾经晓得了查问语句的语法,那么就只须要在后端程序里依据申请、通过图数据库的客户端来收回查问申请,并解决返回构造就好了。在明天的例子中,我抉择应用 Python 来实现后端的逻辑,所以我用了 Nebula-python 这个库,它是 Nebula 的 Python Client。

你晓得么?截至到当初,Nebula 在 GitHub 上有 Java,GO,Python,C++,Spark,Flink,Rust(未 GA),NodeJS(未 GA)的客户端反对,更多的语言的客户端也会缓缓被公布哦。

下边是一个 Python Client 执行一个查问并返回后果的例子,值得注意的是,在我实现这个代码的时候,Nebula Python 尚未反对返回 JSON(通过session.execute_json())后果,如果你要实现本人的代码,我十分举荐试试 JSON 哈,就能够不必从对象中一点点取数据了,不过借助 iPython/IDLE 这种 REPL,疾速理解返回对象的构造也没有那么麻烦。

$ python3 -m pip install nebula2-python==2.5.0 # 留神这里我援用旧的记录,它是 2.5.0,$ ipython
In [1]: from nebula2.gclient.net import ConnectionPool
In [2]: from nebula2.Config import Config
In [3]: config = Config()
   ...: config.max_connection_pool_size = 10
   ...: # init connection pool
   ...: connection_pool = ConnectionPool()
   ...: # if the given servers are ok, return true, else return false
   ...: ok = connection_pool.init([('192.168.8.137', 9669)], config)
   ...: session = connection_pool.get_session('root', 'nebula')
[2021-10-13 13:44:24,242]:Get connection to ('192.168.8.137', 9669)

In [4]: resp = session.execute("use shareholding")
In [5]: query = '''
   ...: MATCH p=(v)-[e:hold_share|:is_branch_of|:reletive_with|:role_as*1..3]-(v2) \
   ...: WHERE id(v) IN ["c_132"] RETURN p LIMIT 100
   ...: '''
In [6]: resp = session.execute(query) # Note: after nebula graph 2.6.0, we could use execute_json as well

In [7]: resp.col_size()
Out[7]: 1

In [9]: resp.row_size()
Out[10]: 100

咱们往下剖析看看,咱们晓得这个申请实质上后果是门路,它有一个 .nodes() 办法和 .relationships()办法来取得门路上的点和边:

In [11]: p=resp.row_values(22)[0].as_path()

In [12]: p.nodes()
Out[12]:
[("c_132" :corp{name: "Chambers LLC"}),
 ("p_4000" :person{name: "Colton Bailey"})]

In [13]: p.relationships()
Out[13]: [("p_4000")-[:role_as@0{role: "Editorial assistant"}]->("c_132")]

对于边来说有这些办法 .edge_name(), .properties(), .start_vertex_id(), .end_vertex_id(),这里 edge_name 是取得边的类型。

In [14]: rel=p.relationships()[0]

In [15]: rel
Out[15]: ("p_4000")-[:role_as@0{role: "Editorial assistant"}]->("c_132")

In [16]: rel.edge_name()
Out[16]: 'role_as'

In [17]: rel.properties()
Out[17]: {'role': "Editorial assistant"}

In [18]: rel.start_vertex_id()
Out[18]: "p_4000"

In [19]: rel.end_vertex_id()
Out[19]: "c_132"

对于点来说,能够用到这些办法 .tags(), properties, get_id(),这里边 tags 是取得点的类型,它在 Nebula 里叫标签tag

这些概念能够在文档里取得更具体的解释。

In [20]: node=p.nodes()[0]

In [21]: node.tags()
Out[21]: ['corp']

In [22]: node.properties('corp')
Out[22]: {'name': "Chambers LLC"}

In [23]: node.get_id()
Out[23]: "c_132"

前端渲染点边为图

具体的剖析大家也能够参考这里

为了不便实现,咱们采纳了 Vue.js 和 vue-network-d3(D3 的 Vue Binding)。

通过 vue-network-d3 的形象,能看进去喂给他这样的数据,就能够把点边信息渲染成很难看的图

nodes: [{"id": "c_132", "name": "Chambers LLC", "tag": "corp"},
        {"id": "p_4000", "name": "Colton Bailey", "tag": "person"}],
relationships: [{"source": "p_4000", "target": "c_132", "properties": { "role": "Editorial assistant"}, "edge": "role_as"}]

前端 <– 后端

详细信息能够参考这里

咱们从 D3 的初步钻研上能够晓得,后端只须要返回如下的 JSON 格局数据就好了

Nodes:

[{"id": "c_132", "name": "Chambers LLC", "tag": "corp"},
 {"id": "p_4000", "name": "Colton Bailey", "tag": "person"}]

Relationships:

[{"source": "p_4000", "target": "c_132", "properties": { "role": "Editorial assistant"}, "edge": "role_as"},
 {"source": "p_1039", "target": "c_132", "properties": { "share": "3.0"}, "edge": "hold_share"}]

于是,,联合前边咱们用 iPython 剖析 Python 返回后果看,这个逻辑大略是:

def make_graph_response(resp) -> dict:
    nodes, relationships = list(), list()
    for row_index in range(resp.row_size()):
        path = resp.row_values(row_index)[0].as_path()
        _nodes = [
            {"id": node.get_id(), "tag": node.tags()[0],
                "name": node.properties(node.tags()[0]).get("name", "")
                }
                for node in path.nodes()]
        nodes.extend(_nodes)
        _relationships = [
            {"source": rel.start_vertex_id(),
                "target": rel.end_vertex_id(),
                "properties": rel.properties(),
                "edge": rel.edge_name()}
                for rel in path.relationships()]
        relationships.extend(_relationships)
    return {"nodes": nodes, "relationships": relationships}

前端到后端的通信是 HTTP,所以咱们能够借助 Flask,把这个函数封装成一个 RESTful API:

前端程序通过 HTTP POST 到 /api

参考这里

from flask import Flask, jsonify, request



app = Flask(__name__)


@app.route("/")
def root():
    return "Hey There?"


@app.route("/api", methods=["POST"])
def api():
    request_data = request.get_json()
    entity = request_data.get("entity", "")
    if entity:
        resp = query_shareholding(entity)
        data = make_graph_response(resp)
    else:
        data = dict() # tbd
    return jsonify(data)


def parse_nebula_graphd_endpoint():
    ng_endpoints_str = os.environ.get('NG_ENDPOINTS', '127.0.0.1:9669,').split(",")
    ng_endpoints = []
    for endpoint in ng_endpoints_str:
        if endpoint:
            parts = endpoint.split(":")  # we dont consider IPv6 now
            ng_endpoints.append((parts[0], int(parts[1])))
    return ng_endpoints

def query_shareholding(entity):
    query_string = (
        f"USE shareholding;"
        f"MATCH p=(v)-[e:hold_share|:is_branch_of|:reletive_with|:role_as*1..3]-(v2)"
        f"WHERE id(v) IN ['{ entity}'] RETURN p LIMIT 100"
    )
    session = connection_pool.get_session('root', 'nebula')
    resp = session.execute(query_string)
    return resp

这个申请的后果则是前边前端期待的 JSON,像这样:

curl --header "Content-Type: application/json" \
     --request POST \
     --data '{"entity":"c_132"}' \
     http://192.168.10.14:5000/api | jq

{
  "nodes": [
    {
      "id": "c_132",
      "name": "\"Chambers LLC\"","tag":"corp"
    },
    {
      "id": "c_245",
      "name": "\"Thompson-King\"","tag":"corp"
    },
    {
      "id": "c_132",
      "name": "\"Chambers LLC\"","tag":"corp"
    },
...
    }
  ],
  "relationships": [
    {
      "edge": "hold_share",
      "properties": "{'share': 0.0}",
      "source": "c_245",
      "target": "c_132"
    {
      "edge": "hold_share",
      "properties": "{'share': 9.0}",
      "source": "p_1767",
      "target": "c_132"
    },
    {
      "edge": "hold_share",
      "properties": "{'share': 11.0}",
      "source": "p_1997",
      "target": "c_132"
    },
...
    },
    {
      "edge": "reletive_with",
      "properties": "{'degree': 51}",
      "source": "p_7283",
      "target": "p_4723"
    }
  ]
}

放到一起

我的项目的代码都在 GitHub 上,最初其实只有一两百行的代码,把所有货色拼起来之后的代码是:

├── README.md         # You could find Design Logs here
├── corp-rel-backend
│   └── app.py        # Flask App to handle Requst and calls GDB
├── corp-rel-frontend
│   └── src
│       ├── App.vue
│       └── main.js   # Vue App to call Flask App and Renders Graph
└── requirements.txt

最终成果

咱们做进去了一个简陋然而足够具备参考性的小零碎,它承受一个用户输出的实体的 ID,再回车之后:

  • 前端程序把申请发给后端
  • 后端拼接 Nebula Graph 的查问语句,通过 Nebula Python 客户端申请 Nebula Graph
  • Nebula Graph 承受申请做出穿透查问,返回构造给后端
  • 后端将后果构建成前端 D3 承受的格局,传给前端
  • 前端接管到图构造的数据,渲染股权穿透的数据如下:

<video width=”800″ controls>
<source src=”https://siwei.io/corp-rel-graph/demo.mov” type=”video/mp4″>
</video>

总结

当初,咱们晓得得益于图数据库的设计,在它上边构建一个不便的股权剖析零碎十分天然、高效,咱们或者利用图数据库的图摸索可视化能力、或者本人搭建,能够为用户提供十分高效、直观的多跳股权穿透剖析。

如果你想理解更多对于分布式图数据库的常识,欢送关注 Nebula Graph 这个开源我的项目,它曾经被国内很多团队、公司认可选为图时代数据技术存储层的利器,大家能够拜访这里,或者这里,理解更多相干的分享和文章。

将来,我会给大家分享更多图数据库相干的文章、视频和开源示例我的项目思路分享和教程,欢送大家关注我的网站: siwei.io。


Nebula 社区首届征文活动进行中!🔗 奖品丰富,全场景笼罩:撸码机械键盘⌨️、手机无线充🔋、衰弱小助手智能手环⌚️,更有数据库设计、常识图谱实际书籍📚 等你来领,还有 Nebula 粗劣周边送不停~🎁

欢送对 Nebula 有趣味、喜钻研的小伙伴来书写本人和 Nebula 乏味的故事呀~

交换图数据库技术?退出 Nebula 交换群请先填写下你的 Nebula 名片,Nebula 小助手会拉你进群~~

正文完
 0