解析 Nebula Graph 子图设计及实践

本文首发于 Nebula Graph 公众号 NebulaGraphCommunity,Follow 看大厂图数据库技术实践。git

解析 Nebula Graph 子图设计及实践

前言

在先前的 Query Engine 源码解析中,咱们介绍了 2.0 中 Query Engine 和 1.0 的主要变化和大致的结构:github

架构变化

你们能够大概了解到用户经过客户端发送一条查询语句,Query Engine 是如何解析语句、把语句构建为抽象语法树,在抽象语法树进行校验、生成执行计划的过程。本文会经过 2.0 中新增的子图算法模块继续讲解 Query Engine 背后所作的内容,并着重介绍执行计划生成的过程,以便增强你对源码更好地理解。算法

子图的定义

子图是指节点集合和边集合分别是某一图的节点集的子集和边集的子集的图。直观地理解,就是从用户指定的起点开始出发沿着指定的边一步步拓展,直到达到用户所设定的步数为止,而后返回在拓展过程当中遇到的全部点集和边集。shell

子图的语法​

GET SUBGRAPH [<step_count> STEPS] FROM {<vid>, <vid>...} [IN <edge_type>, <edge_type>...]
[OUT <edge_type>, <edge_type>...] [BOTH <edge_type>, <edge_type>...]
复制代码
  • step_count:指定从起始点开始的跳数,返回从 0 到 step_count 跳的子图。必须是非负整数。默认值为 1
  • vid:指定起始点 ID
  • edge_type:指定边类型。能够用 INOUTBOTH 来指定起始点上该边类型的方向。默认为 BOTH

子图的实现

当 Query Engine 接收到 GET SUBGRAPH 命令后,Parser 模块(由 flex 和 bison 实现)会根据已经写好的规则(parser.yyget_subgraph_sentence 规则)把所须要的内容从查询语句中提取出来,生成一个抽象语法树,以下所示:数据库

解析 Nebula Graph 子图设计及实践

而后进入 Validate 阶段,此时对生成的抽象语法树进行校验,目的是为了验证用户的输入是否合法(参考 Query Engine 的文章),当校验经过后,会把语法树中的内容提取出来,生成一个执行计划。 ​ 那么这个执行计划是如何生成的呢?对同一功能不一样的数据库厂商可能会生成不一样的执行计划,可是原理都是相同的。那就是要看自身的算子有哪些和查询层和存储层是如何进行交互的。由于咱们的每一条查询语句到最后都是要从存储层取数据的。在 Nebula Graph 中 Query Engine 和存储层是经过 RPC 方式(fbthrift)进行交互的(接口定义在 common 仓中的 interface 目录下)。这里有两个很是关键的接口 getNeighbors 和 getProps 须要了解一下。markdown

getNeighbors 其中 fbthrift 的定义格式以下:架构

struct GetNeighborsRequest {    
    1: common.GraphSpaceID                      space_id,
    2: list<binary>                             column_names,
    3: map<common.PartitionID, list<common.Row>>
        (cpp.template = "std::unordered_map")   parts,
    4: TraverseSpec                             traverse_spec
}
复制代码

该结构中每一个变量的详细定义能够参考 github.com/vesoft-inc/…,里面有详细的注释。 ​oop

其主要功能就是 Query Engine 根据定义好的结构传入起始点和要拓展的边类型信息,而后存储层会找到起始点,而后把该点的属性和以该点的出边的边属性找出来组装成一个表格返回给 Query Engine,其中返回的表格的格式参考 github.com/vesoft-inc/… 中 GetNeighborsResponse 的定义,而后在 Query Engine 中咱们就能够经过这个表格提取到咱们想要的内容。 ​ 例如在 basketba l l 数据集中,当起始点为 Tim Duncan、Manu Ginobili 沿着 like 边双向拓展。想要得到 $^.[player.name](http://player.name/)like._dst$$.[player.name](http://player.name/)like.likeness 这四个属性。其返回的数据大体以下所示:fetch

数据图

表格1flex

由于是双向拓展第四列的 + like 表明出边,第五列的 - like 表明入边。

在 Nebula Graph 的存储层中边是和起始点在一块儿存放的,因此经过 getNeighbor 接口就能够得到起点和出边的全部属性信息,可是若是想要在拓展过程当中拿到目的点的属性信息则须要使用 getProps 接口,固然若是我只想经过 fetch 语句拿到某个点或者边的属性也须要调用这个接口。你能够自行了解 github.com/vesoft-inc/… 下 getPropRequest 的定义,加深理解。

执行计划

有了上面的接口定义咱们就能够开始执行计划了,首先须要的算子有 start、getNeighbor、subgraph、loop、datacollect。

  • start 算子:至关于执行计划中叶子节点,不作任何事情。目的是告诉调度器,以后没有能够依赖的算子,或者能够理解为递归算法中的终止条件。
  • loop 算子:至关于 C 语言中的 while 语法,该算子有三个成员 depend、condition 和 loopBody,depend 在多语句和 PIPE 中会使用当前暂且不表,condition 至关于终止条件。loopBody 至关于 while 中的循环体。
  • subgraph 算子:负责把 getNeighbor 算子结果中的 _dst(目的点)属性提出来而后过滤掉已经访问过的目的点(避免重复从存储层拿数据),而后把它们看成 getNeighbor 算子下一次拓展时的输入。
  • datacollect 算子:负责在最后把拓展过程当中得到的点和边属性收集起来组装为 vertex 和 edge 类型。

其中各个算子的详细信息,可参考源码 github.com/vesoft-inc/… 。 下面经过图1 举例,咱们是如何构建子图的

构建子图 图1

拓展一步的状况

当从 A 点开始沿着 like 边只获取一步的全部点和边的信息,则很容易。只须要 getNeighbor 和 dataCollect 这两个算子就能够了。执行计划以下图所示 :

拓展一步的状况

拓展多步的状况

一步场景实际上是多步的场景的特殊状况。因此能够将一步的场景合入到多步场景中。当从 A 点开始,沿着 like 边拓展三步的话,根据现有的算子,能够在 getNeighbor 拓展后把目的点提取出来,而后将这些目的点看成起点从新调用 getNeighbor 接口,这个循环两次就能够了(loop 算子的终止条件设置为当前步数),所以执行计划以下图所示 :

拓展多步的状况

输入和输出

通常状况下,每一个算子的输入就是所依赖算子的输出,这时候根据执行计划的依赖关系就能够直观地肯定每一个算子的输入和输出。可是在某些状况下,好比:子图,在多步场景中每一次 getNeighbor 算子的输入都应该是上一次拓展边的目的点,也就是 subgraph 算子的输出,所以 subgraph 算子的输出应该就是 getNeighbor 算子的输入。这时就和上图的执行计划依赖不一致,这时就须要自行设置每一个算子的输入和输出。在 Query Engine 2.0 中咱们已经介绍了每一个算子的输入和输出是存放在哈希表中的,其中 value 是 vector 类型。以下表 ResultMap 所示:

ResultMap

  • 起始点存放在 ResultMap["StartVid"] 中
  • getNeighbor 算子的输入是 ResultMap["StartVid"], 输出存放在 ResultMap["GN_1"]
  • subgraph 算子的输入是 ResultMap["GN_1"], 输出存放在 ResultMap["StartVid"]
  • loop 算子不产生数据,看成逻辑循环使用,所以不须要设置输入输出
  • dataCollect 算子的输入是 ResultMap["GN_1"], 输出存放在 ResultMap["DATACOLLECT_2"]

这时 getNeighbor 算子会把每一次的结果放在 ResultMap["GN_1"] 中的 vector 中的末尾,而后 subgraph 算子从 ResultMap["GN_1"] 中的 vector 中的末尾取值,通过计算再把下一次要拓展的起始点存放在 ResultMap["StartVid"] 中。

当拓展第一步后,ResultMap 的结果以下: ​ ResultMap

为了方便显示,GetNeighbor 的结果只写了 _dst 的属性,实际上会带上边上全部的属性和起始点的全部属性,相似于表格 1。

subgraph 算子接收"GN_1"的输入,提取 _dst 属性,而后将结果放入"StartVid"中。当拓展第二步后,ResultMap 的结果以下:

ResultMap

当拓展第三步后,ResultMap 的结果以下:

ResultMap

最后 dataCollect 算子从 ResultMap["GN_1"] 中取出拓展过程当中遇到的全部点集和边集,组装成最终的结果返回给用户。

实例

下面执行一个子图的实例看看在 Nebula Graph 中执行计划的具体结构,打开 nebula-console, 切换 space 到 basketball, 输入 EXPLAIN format="dot" GET SUBGRAPH 2 STEPS FROM 'Tim Duncan' IN like, serve,这时候 nebula-console 会生成 dot 格式的数据,而后打开 Graphviz Online 这个网站,将生成的 dot 数据粘贴上去,就能够看到以下结构:

dot 结果

其中 Start_0 算子是 loop 算子中 depend 的依赖,因为没有多语句或 PIPE 语句,所以不作任何处理。 ​ 以上为本次子图的讲解,若是你在使用子图或者其余 Nebula 过程当中遇到问题,欢迎来论坛和咱们交流:discuss.nebula-graph.com.cn/

想要和其余大厂交流图数据库技术吗?NUC 2021 大会等你来交流:NUC 2021 报名传送门