给定节点A在Neo4j中查找A'子图中具有线性时间复杂度的所有节点

时间:2016-03-11 11:03:29

标签: neo4j cypher

我正在评估Neo4j在生产环境中使用的过程中,我在做一些我希望简单的事情时遇到了一些困难。我已设法解决它,但是以一种次优和相当复杂的方式,所以我在想是否有更简单的方法来完成同样的事情。

序言

Neo4j版本2.3.2

TL; DR

这是一个很长的解释,所以摘要如下:

给定节点A,我需要找到A子图中的所有节点,复杂度为O(number_of_vertices + number_of_edges)。

问题

对于我们的用例,我们有一个图表,就一种特定类型的关系而言,它被分解为每个不超过几十个节点的较小的断开连接的子图。我们要完成的是,给定一个节点的索引id发现子图中的所有节点(同时将图形视为无向)。另一个特点是我们的节点总是具有从每个节点回到自身的自反边缘。

算法上我们所需要的是广度优先搜索,其复杂度为O(num_of_vertices + num_of_edges)。由于我们的图形不密集,它们中的边缘数量与顶点数量大致呈线性关系,因此整体复杂度应与其中的顶点数量成线性关系。

测试图

为简单起见,我将此测试图设为完全连接的测试图。由于重点是比较密码查询,因此不会影响结果。

命令:

  • CREATE(:Label {id:1}),(:Label {id:2}),(:Label {id:3}),(:Label {id:4})
  • 比赛(a),(b)合并(a) - [:REL] - >(b)

简单查询

我尝试获得所需结果的第一个查询如下:

  • MATCH(a:Label {id:1}) - [:REL * 1 ..] - (b:Label)RETURN DISTINCT b

该查询从未终止。当我为关系模式添加上限并分析查询时,我得到以下内容:

  • 深度4:3902 db访问
  • 深度8:1714982 db accessses

因此,不是与顶点和边的数量成线性,而是看起来它找到了所有可能的路径,这些路径当然会随着深度而爆炸。

效果更佳的查询

为了实现这一点,我改为编写了以下查询:

https://gist.github.com/Dalamar42/1ec93cd74b01c145e7bd

(这将搜索深度2.重复第6-16行以搜索深度4,6,8等等)

查询执行以下操作:

  1. 获取入口节点
  2. 将该节点添加到 nodes_found 和另一个 nodes_to_visit
  3. 的集合中
  4. 对于 nodes_to_visit 中的每个节点A,其边缘跟随节点B
  5. 从节点B只保留不在 nodes_found 中的节点作为节点C
  6. nodes_to_visit 设置为节点C并将 nodes_found 设置为其先前的值加上节点C
  7. 重复所需深度
  8. 除了一个复杂性之外,这个查询几乎应该具有BFS的复杂性。根据我的理解,每个中间MATCH / WHERE需要匹配至少一个节点,否则cypher返回在前面的步骤中找到的空的忽略节点。我通过将第4步更改为:

    来解决这个问题

    "从节点B只保留节点A和不在 nodes_found 中的节点作为节点C"

    因为所有节点都具有自反边缘,所以节点A将始终位于节点B的集合中,并始终保持它,我确保查询的那一部分始终与至少一个节点匹配。

    这意味着此查询存在以下问题:

    • 我需要定义最大搜索深度
    • 由于我每次都在探索A的边缘,所以增加深度并不是免费的
    • 此查询对于某人来说是一种痛苦,特别是考虑到这实际上只是一个必须执行此操作的大型查询的一部分

    此查询的好处是我的表现更好

    • 搜索深度4:65 db访问(与" dumb"查询中的3902相比)
    • 搜索深度6:97 db访问次数(与" dumb"查询中的1714982相比)

    更好的解决方案? 有谁知道更好/更简单的解决方案?我错过了Cypher的一些明显特征吗?我无法通过文档找到任何内容。

    由于

2 个答案:

答案 0 :(得分:2)

[增订]

这是一个与您类似的更简单的方法,但它使用COALESCE函数来避免必须人为地将“入口节点”添加到每个“rim节点”集合中。 (通过“rim node”,我的意思是在最近的一场比赛中发现的一个以前未被发现的节点。)

该查询假定您已在:Label(id)上创建了一个索引,以加快第一个MATCH。顶部部分仅用于获取“入口节点”并初始化“res”(或结果)和“rim”集合。后续部分只是彼此的精确副本,可以重复以匹配所需的搜索深度。

深度为2,如此处所示,仅消耗了40分贝的命中率。

注1:给定测试数据,只需要深度为1。在这种情况下,只消耗了16个DB命中。

注意2:每个部分中的第3个WITH子句用于强制NULL成为空的rim集合。这是因为UNWIND将在要求展开空集合时中止查询。出于某种原因,将[NULL]而不是[]传递给COALLESCE()并不能正常工作。

MATCH (a:Label { id:1 })
USING INDEX a:Label(id)
WITH COLLECT(a) AS res
WITH res, res AS rim

UNWIND rim AS a
OPTIONAL MATCH (a)-[:REL]-(b:Label)
WHERE NOT b IN res
WITH res, COALESCE(COLLECT(DISTINCT b),[]) AS rim
WITH rim, res + rim AS res
WITH res, CASE rim WHEN [] THEN [NULL] ELSE rim END AS rim

UNWIND rim AS a
OPTIONAL MATCH (a)-[:REL]-(b:Label)
WHERE NOT b IN res
WITH res, COALESCE(COLLECT(DISTINCT b),[]) AS rim
WITH rim, res + rim AS res
WITH res, CASE rim WHEN [] THEN [NULL] ELSE rim END AS rim

RETURN res;

答案 1 :(得分:1)

Cyber​​sam的答案非常棒,表现相当不错。但是,对于APOC程序,有一种替代方案似乎在大型图表上执行得更快(针对完全互连的电影图表进行测试)。

APOC's path expander允许使用bfs方法,并且在使用NODE_GLOBAL唯一性时,节点只访问过一次。这也允许更简洁的查询。

MATCH (a:Label { id:1 })
USING INDEX a:Label(id)
CALL apoc.path.expandConfig(a,{relationshipFilter:'REL', bfs:true, uniqueness:"NODE_GLOBAL"}) 
  YIELD path
WITH a, LAST(NODES(path)) as b
RETURN b