【问题标题】:Given node A find all nodes in A's subgraph with linear time complexity in Neo4j给定节点 A 在 Neo4j 中找到 A 的子图中具有线性时间复杂度的所有节点
【发布时间】:2016-03-11 11:03:28
【问题描述】:

我正在评估 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 次数据库访问

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

性能更好的查询

为此,我编写了以下查询:

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

(这将搜索到深度 2。复制第 6-16 行以搜索到深度 4、6、8 等)

查询执行以下操作:

  1. 获取入口节点
  2. 将该节点添加到 nodes_found 集合和另一个 nodes_to_visit 集合中
  3. 对于 nodes_to_visit 中的每个节点 A,沿其边缘到达节点 B
  4. 从节点 B 只保留不在 nodes_found 中的节点作为节点 C
  5. nodes_to_visit 设置为节点 C,并将 nodes_found 设置为其先前的值加上节点 C
  6. 重复到所需的深度

这个查询应该几乎具有 BFS 的复杂性,除了一个复杂性。据我了解,每个中间 MATCH/WHERE 都需要匹配至少一个节点,否则 cypher 会返回空的节点,而忽略前面步骤中找到的节点。我通过将第 4 步更改为:

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

因为所有节点都有自反边,所以节点 A 将始终位于节点 B 的集合中,并且通过始终保留它,我确保查询的该部分将始终匹配至少一个节点。

这意味着这个查询有以下问题:

  • 我需要定义最大搜索深度
  • 增加深度并不是免费的,因为我每次都在探索 A 的边缘
  • 这个查询让人难以阅读,尤其是考虑到这实际上只是一个更大的查询的一部分,需要执行几次

这个查询的好处是我得到了更好的性能

  • 用于搜索深度 4:65 db 访问(与“哑”查询中的 3902 相比)
  • 用于搜索深度 6:97 db 访问(与“哑”查询中的 1714982 相比)

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

谢谢

【问题讨论】:

    标签: neo4j cypher


    【解决方案1】:

    [更新]

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

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

    如图所示,深度为 2 时,仅消耗 40 db 命中。

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

    注意 2:每个部分中的第三个 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;
    

    【讨论】:

    • 不幸的是,这也不适用于任意深度。如果您尝试运行它的深度大于子图中最长路径的长度,则此查询将不返回任何内容。您可以在我的测试数据上尝试深度 4 以了解我的意思。您可以执行与我在一次又一次地重新访问入口点节点时所做的相同的技巧,但这会导致查询的性能将随着搜索深度而不是随着图形的实际深度而缩放的相同问题。
    • 它适用于你的测试数据,深度为 2,这比你需要的要大(因为所有节点距离不超过 1 步),使用 console.neo4j.org 中的 2.3 版。但是,我将研究为什么它不返回任何深度为 4 的内容。
    • 这行得通!我没有意识到是 UNWIND 步骤中止了查询。我在这里遇到的问题是,在我的真实数据中,我事先不知道所需的搜索深度。我有一个理论上的最大值来限制查询,但绝大多数数据远不接近该最大值。因此,我需要一个查询,其中 DB 访问的数量仅取决于 BFS 树的深度,而这正是这样做的。谢谢
    【解决方案2】:

    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
    

    【讨论】:

    • 感谢您发布此信息。我目前没有在做这个,所以我现在不能测试它,但是一旦我回到这个,我会尝试你的解决方案并在这里更新。
    猜你喜欢
    • 1970-01-01
    • 2023-04-02
    • 1970-01-01
    • 2018-04-20
    • 1970-01-01
    • 1970-01-01
    • 2018-08-12
    • 1970-01-01
    • 2015-05-30
    相关资源
    最近更新 更多