图数据库计算和查询结果的正确性,这个重要性当然是不言而喻的!
老夫之前也写文章讲过,今天再手书一篇,旨在向大家系统地介绍一下图数据库查询与计算到底如何进行正确性验证!!!
图数据库中的操作分为以下2类:
· 面向元数据的操作:即面向顶点、边或它们之上的属性字段的操作,具体可以分为增、删、改、查4类。
· 面向高维数据的操作:这也是本书关注的重点,例如面向全图或子图数据的查询结果返回多个顶点、边组合而成的高维数据结构,可能是多顶点的集合、点边构成的路径、子图(子网)甚至是全图遍历结果。
面向高维数据的查询有以下3类:
· K邻查询:即返回某顶点的全部K度(跳)邻居顶点集合。K邻查询可以有很多变种,包括按照某个特定方向、点边属性字段进行过滤。还有全图K邻查询,也被视作一种高计算复杂度的图算法。
· 路径查询:常见的有最短路径、模板路径、环路路径、组网查询、自动展开查询等。
· 图算法:图算法在本质上是面向元数据、K邻、路径等查询方式的组合。
无论以何种方式进行高维查询,图数据库中的操作无外乎遵循如下3种遍历模式:
· 广度优先:如K邻查询、最短路径等。·深度优先:如环路查询、组网查询、模板路径查询、图嵌入随机游走等。
· 深度优先与广度优先兼而有之:以最短路径方式遍历的模板路径或组网查询、带方向或条件过滤的模板K邻查询、定制化的图算法等。
注意:
面向元数据的图数据库操作和关系型数据库有相似的地方,正确性验证方法在此就不再赘述了。本文老夫着重介绍图数据库特有的高维数据查询正确性验证。我们以图数据库基准性能评测中常用的Twitter-2010数据集为例来说明如何进行图查询的正确性验证。
Twitter数据集(其中顶点数量为4200万,边数量为14.7亿,原始数据占24.6GB)的下载链接为:http://an.kaist.ac.kr/traces/WWW2010.html。
在开始验证前,以Twitter数据集为例,了解一下图数据模型的特征。
(1)有向图
由顶点(人)和边(关注关系)组成,其中关注关系为有向边。
Twitter源数据中有两列,对应每一行是由Tab键分隔的两个数字,例如12与13,代表两个用户的ID,表示两者间的有向的关注关系:12关注13。在图数据建模中应该构建为两条边,一条表示从12到13的正向边,另一条则是从13到12的反向边,缺一不可。后面的验证细节中很多正确性的问题都与此相关——没有构建反向边,查询结果不可避免会出错。
(2)简单图与多边图
如果一对顶点间存在超过(含)2条边,则其为多边图,否则为简单图。
在简单图中任意顶点间最多只有1条边,因此简单图也称作“单边图”。
Twitter数据实际上是一种特殊的多边图,当两个用户互相关注对方时,他们之间可以形成两条边。在金融场景中,如果用户账户为顶点,转账交易为边,那么两个账户之间可以存在多笔转账关系,即多条边。
很多系统只能支持简单的单边图,这样就会带来很多图上查询与计算的结果错误的问题。
(3)点、边属性
Twitter数据本身除了隐含的边的方向可以作为一种特殊的边属性外,并不存在其他点边属性。这个特征区别于金融行业中的交易流水图——无论是顶点还是边都可能存在多个属性,可以被用来对实体或关系进行精准的查询过滤、筛选、排序、聚合运算、下钻、归因分析等。不支持点边属性过滤的图数据库可以认为功能没有实现闭环,也不具备商业化价值。
我们以K邻查询为例来验证图数据库查询结果的正确性!!
首先,我们要明确K邻查询的定义,事实上K-Hop查询有两种含义,分别是:第K度(跳)邻居、从第1跳到第K跳的全部邻居。
其中第K跳邻居指的是全部距离原点最短路径距离为K的邻居数量。这两种含义的区别仅仅在于到底K-Hop的邻居是只包含当前步幅(跳、层)的邻居,还是包含前面所有层的邻居。无论是哪种定义,有两个要点直接影响“正确性”:
· K邻查询的正确实现方式默认应基于广度优先搜索。
· 结果集去重:即第K层的邻居集合中不会有重复的顶点,也不会有在其他层出现的邻居(已知的多款图数据库系统都存在数据结果没有去重的错误)。
有的图数据库(或图计算引擎)会用深度优先搜索(DFS)方式,通过穷举全部可能的深度为K跳的路径来试图找到全部路径和最终能抵达的终点。但是,DFS方式实现K邻查询有2个致命的缺点:
·效率低下:在体量稍大的图中,不可能遍历完全,例如Twitter数据集中常见的有超过百万邻居的顶点,如果以深度遍历,复杂度是天文数字级的(百万的11次方以上);
·结果大概率错误:即便是可以通过DFS完成遍历,也没有对结果进行分层,即无法判断某个邻居到底是位于第1跳还是第N跳。
我们先从验证1度(1跳)邻居开始,以Twitter数据集的顶点27960125为例。在源数据集中,如图1所示,返回了8行(对应图数据库中的8条边),但是它的1度邻居到底是几个呢?

正确答案是7个,注意图1中顶点27960125的一个邻居27498091出现了2次,它们之间存在两条相互的关系(对应源数据集中的2行)。但是作为去重后的1度邻居集合,只有7个。
在图2中,我们以无向图(即双向边遍历)的方式对顶点27960125进行1度邻居查询,得到全部邻居顶点为7个。

为了更精准地验证结果正确性,对K邻查询还可以按照边的方向来进行过滤,例如只查询顶点2796015的出边、入边或双向边(默认是查询双向边关联的全部邻居)。图3展示了如何通过图查询语言来完成相应的工作。注意,该顶点有6条出边对应的1跳邻居、2条入边对应的1跳邻居,其中有1个邻居27498091是重叠的。

如果参考美国图数据库厂家Tigergraph在Github网站上公开的性能测试结果数据文件,其K邻查询的结果存在明显的错误,例如图7-14中顶点27960125的1-Hop结果仅返回6个邻居。

Tigergraph的查询结果错误有3个可能,并且具有典型性。
·构图错误:只存储了单向边,没有存储反向边,无法进行反向边遍历;
·查询方式错误:只进行了单向查询,没有进行双向遍历查询;
·图查询代码实现错误:即没有对结果进行有效的去重,这个在多跳K-Hop查询中再继续分析。
其中,构图错误代表数据建模错误,这意味着业务逻辑不能在数据建模层面被准确反映。例如在反欺诈、反洗钱场景中,账户A收到了一笔来自账户B的转账,但是却因为没有存储一条从A至B的反向边而无法追踪该笔交易,这显然是不能容忍的。
查询方式和查询代码逻辑错误同样也会对结果造成严重影响——每一跳查询双向边,在多跳情况下查询复杂度指数级高于单向边查询,这也意味着Tigergraph如果正确地实现图数据建模、存储与查询,其性能会指数级降低,并且存储空间的占用也会成倍增加(存储正向和反向边的数据结构要比仅存储单向边复杂2倍以上),数据加载时间也会成倍增加。
如果我们继续追溯顶点27960125的2-Hop结果集,就会发现结果的错误更加隐蔽,例如Tigergraph的2-Hop实际上仅仅返回了沿出边遍历的第2度邻居结果(图5),并且没有对结果去重。其第2度邻居数1128中含有重复的顶点,按照只进行出边查询得到的去重结果应该是1127——但是2-Hop的正确结果应该是533108(图6),这两者间有473倍的差异,即47300%的误差!在2-Hop的结果中,就可以看到Tigergraph的查询结果同时存在以上所述的3种错误——构图错误、查询方式错误、结果未去重错误。


遗憾的是,类似的查询结果错误问题在今天的图数据库市场并不是个例,我们在Neo4j、ArangoDB等系统中也发现因底层算法实现或接口调用等问题而出现的错误。
更为遗憾的是,有多个厂家的“自研图数据库”实际上是对Neo4j社区版或ArangoDB的封装,姑且不论这么操作是否涉嫌违规商用,暴力封装几乎注定了它们的查询结果也是错误的。例如Neo4j默认并不对K邻查询结果集进行去重,而一旦开启去重,它的运行效率会指数级下降;而ArangoDB有一种最短路径查询模式,只返回一条路径,这种模式本身就是对最短路径的错误理解与实现。
图数据库配套的可视化工具可以帮助我们更直观、便捷地查询结果的正确性。
上面介绍了图上的基础查询K邻查询的正确性验证方法,以及可能出现的错误情形。还有很多其他图操作同样也涉及结果错误的问题,但是都能通过一些基础的方法来验证。下面再举2个有代表性的例子:最短路径和图算法。
最短路径可以看作K邻查询的一个自然延展,区别在于它需要返回的结果有2个特征:
·高维结果:最短路径需要返回多条由顶点、边按遍历顺序组合而成的路径;
·全部路径:任意两个顶点间可能存在多条最短路径,如果是转账网络、反洗钱网络、归因分析等查询,只计算一条路径显然是无法反映全貌的。
以Twitter数据集中的顶点12、13之间的最短路径为例,它们之间存在两条最短路径(图7),其中一条由12指向13,另一条由13指向12(图8),这个在源数据中也可以通过grep操作得到快速验证。在更复杂(更深度)的查询中,可以用类似的逻辑,通过层层的抽丝剥茧来验证结果的正确性。


下面以杰卡德相似度算法为例来说明如何验证图算法的正确性。以图9为例,计算A、B两个顶点间的相似度,计算公式如下:

在图9中,A、B节点的共同邻居数为2,全部邻居数为5,我们可以手工推算出这两个节点的杰卡德相似度为2/5=0.4。直接调用杰卡德相似度算法的结果也应该是0.4(40%)。如果用图查询语言来白盒化实现,代码逻辑如图10所示。

在Twitter数据集中,任意两个顶点间的杰卡德相似度计算的复杂度和被查询顶点的1度邻居的个数直接相关,以顶点12、13为例,它们都是典型的有百万邻居的“超级节点”,在这种情况下,手工验证结果的准确性并不现实。但是可以通过多组查询来校验结果是否正确,逻辑分为如下5步:
1)运行杰卡德相似度算法,如图11所示:

2)(验证方法一)通过多句查询计算杰卡德相似度,如图12所示:

3)(验证方法二)查询顶点12的1跳邻居个数(图13)。
4)(验证方法二)查询顶点13的1跳邻居个数(图13)。

5)(验证方法二)查询顶点12到13之间的全部深度为2的路径(图14),这一结果就是两个顶点之间的全部共有邻居。

6)(验证方法二)用以上第5步的结果除以(第3步结果+第4步结果–第5步结果)=0.15362638。如果以上两种验证方法结果均一致,则图算法计算结果正确。
最后要说的是,本文等于非常详细地介绍了如何在图数据库上进行查询正确性验证的方法,希望可以为聪明的朋友们以开阔思路,以达到举一反三、去伪存真的效果!!! 好了,洋洋洒洒有一大篇,就此打住。88!
· END ·
(文/Ricky - HPC高性能计算与存储专家、大数据专家、数据库专家及学者)