ArangoDB Graphs 系列——03 图遍历

1,024 阅读4分钟

上一篇中,我们介绍了ArangoDB中图遍历的基础语法,并编写了最简单的遍历深度为1的语句,这一篇将在图遍历主题上更进一步,介绍ArangoDB中图遍历的各种选项。

遍历选项

用户可以利用这些选项,来更精确地控制遍历行为

深度/广度优先搜索

一旦遍历深度>=2时,就涉及到了该如何遍历,即这里指的遍历选项,是用深度优先DFS(Depth First Search)呢? 还是用广度优先 BFS(Breadth First Search)?

  • DFS(默认):从起点依据遍历深度一条路走到黑,然后再换一条路径
  • BFS:先遍历深度较低的全部节点,然后逐步增加深度,像是地毯式搜索

截屏2022-04-13 下午8.10.22.png

图: DFS与BFS

如果遍历的其他选项都一样,则这两种搜索方式会返回同样数量的路径,区别仅在于边和顶点被访问到的次序

如果BFS跟filters和 limits 搭配使用,在未达最大遍历深度之前就停止遍历,则可以显著提升遍历效率。

请看下图的例子,我们要从S开始,进行1..10深度的遍历,找到满足条件的某个节点,此处假设要找F顶点。如果使用DFS,则遍历可能会从A开始,然后一路沿着D,G往深走10跳到达最深处,然后是E,H这条路径;但使用BFS时,如果你限制只取一个匹配,在深度为2时找到F顶点后,就不会再去搜索更深的顶点。

FOR v IN 1..10 OUTBOUND 'verts/S' edges 
    OPTIONS {bfs: true}
    FILTER v._key == 'F'
    LIMIT 1
    RETURN v

截屏2022-04-13 下午8.26.02.png

Uniqueness 选项

图上的某两个顶点之间,很多时候并不是仅有一条路径可达,前文提到过ArangoDB支持各种类型、各种体量的图,有时候图中会出现环,比如下图中 S->B->C->S就是一个环。

  • 默认情况,为了避免遍历时陷入一个环而无法到达最大深度,当一条边已经被经过而又再次出现时,遍历会停止。
  • 除非另有配置,否则允许一条路径上出现重复的顶点。

截屏2022-04-14 上午11.00.27.png 图1:从S出发的遍历

来看一个例子:

# 为了助于理解,此查询中的两个选项都是默认的,但我们依旧明确写出来
# 使用了路径变量p, 将每条路径上的顶点,用其._key形成一个字符串,比如 S->A->D->E

FOR v, e, p IN 1..5 OUTBOUND 'verts/S' edges 
    OPTIONS {
        uniqueVertices: 'none',
        uniqueEdges: 'path' 
    }
    RETURN CONCAT_SEPARATOR('->', p.vertices[*]._key)

上述查询共找到10条路径:

  1. S->A
  2. S->B
  3. S->A->D
  4. S->B->D
  5. S->B->C
  6. S->A->D->E
  7. S->B->D->E
  8. S->B->C->S
  9. S->B->C->S->A
  10. S->B->C->S->A->D

其中 S->B->C->S路径中开始和终止都是S节点,这是合理的,因为 uniqueVertices 选项没有做规定。

S->B->C->S->B->C没出现在查询结果中,因为选项规定一条路径中不能出现重复的边,如果选项是 uniqueEdges: 'none', 则这条带重复边的路径,恰好其深度为5,符合我们的查询要求,就可以出现在结果中。 如果查询要求的最大深度更高,则整个遍历的结果可能会由于有个环而产生很多路径结果。

如果想防止某个顶点在遍历中被访问多次,有两种可行方式:

  • uniqueVertices: 'path': 保证单个路径中不存在重复顶点
  • uniqueVertices: 'global':更严苛,整次遍历中,每个可达的顶点仅被遍历一次

此处需要 bfs:true,不支持dfs,因为dfs的结果有不确定性(多次运行结果不一致),根本原因是并没有一个规则告诉遍历器,当面前同时有几条路时,先走哪条,后走哪条。例如前面的图1,有多条路可从S->E,由于没有明确顺序,遍历器会随机选一条路. 这就会导致多次运行的结果可能不一致。

动手实践

机场航班数据集中,我们想查询从某个机场出发,所有可直达的机场

# 从洛杉矶LAX机场可直达的所有机场, 
# 方法1,我本机运行时间约1.1ms

FOR airport IN OUTBOUND 'airports/LAX' flights
    OPTIONS {bfs: true, uniqueVertices: 'global'}
    RETURN airport


# 方法2, 我本机运行时间约为 45ms, 二者结果一样,效率差别很大

FOR airport IN OUTBOUND 'airports/LAX' flights
    OPTIONS {bfs: true}
    RETURN DISTINCT airport

上述两种方法的效率差别很大,原因是 RETURN DISTINCT 只有在遍历将所有的顶点返回之后(因为两个机场之间有很多航班可以通航,所以有巨大的中间结果),才会去重;而 uniqueVertices: 'global' 是个遍历选项,它会指引遍历器在遇到重复顶点时,马上忽略。