知识库

对多个节点执行模式否定

某些用例需要匹配到未连接到其他任何节点集的节点。我们将讨论此类查询的错误和正确方法。

对于我们的示例,我们将使用一个食谱图,其中主要结构如下

(:Recipe)-[:INCLUDES]->(:Ingredient)

用例是,提供要排除的配料名称列表,以匹配不包含任何这些配料的食谱。

错误方法:在测试排除时将节点留在它们自己的行上

一种常见的错误方法不使用集合,而是错误地假设被否定的模式将变量值视为集合。

MATCH (excluded:Ingredient)
WHERE excluded.name in $excludedIngredients
MATCH (r:Recipe)
WHERE NOT (r)-[:INCLUDES]->(excluded)
RETURN r

对于单个排除的配料,上面的查询将正常工作。

但是,当 $excludedIngredients 包含多个条目时,此查询可能会无法生成正确的结果。

原因是每个被排除的配料都在自己的记录/行上,并且将单独测试,这意味着包含被排除配料中的一部分但并非全部的某些食谱将无法正确过滤。

例如,假设参数如下: {excludedIngredients:['eggs', 'walnuts']}

假设正在评估 Chocolate Cake :Recipe,它包含鸡蛋但不包含核桃。

由于每个被排除的配料都在自己的记录上,如果我们在评估时检查已构建的记录,它可能看起来像这样

excluded.name r.name

eggs

Chocolate Cake

walnuts

Chocolate Cake

已构建的记录是每个包含它的食谱中的每个单一配料。它们不会自动排列成集合(这需要使用 collect()

每个记录将被独立评估: WHERE NOT (r)-[:INCLUDES]→(excluded)

将评估第一条记录,由于 Chocolate Cake 包含鸡蛋,因此将消除该记录。

将评估第二条记录,由于 Chocolate Cake 不包含核桃,因此将保留该记录。

Chocolate Cake :Recipe 将作为结果返回,这显然是错误的。

正确方法:收集要排除的节点,并在集合上使用 WHERE NONE() 来驱动排除

正确方法是使用集合成员资格来驱动排除。实际上,有很多类似的正确查询是从这种方法开始的,一些查询效率不同,具体取决于实际的图

MATCH (excluded:Ingredient)
WHERE excluded.name in $excludedIngredients
WITH collect(excluded) as excluded
MATCH (r:Recipe)-[:INCLUDES]->(i)
WITH excluded, r, collect(i) as ingredients
WHERE NONE (i in ingredients where i in excluded)
RETURN r

我们可以使用模式理解一次性扩展和收集所有内容来做同样的事情。

MATCH (excluded:Ingredient)
WHERE excluded.name in $excludedIngredients
WITH collect(excluded) as excluded
MATCH (r:Recipe)
WITH excluded, r, [(r)-[:INCLUDES]->(i) | i] as ingredients
WHERE NONE (i in ingredients where i in excluded)
RETURN r

或者,我们可以避免为每个食谱收集配料,而是使用 NONE() 来排除关系具有任何被排除配料的模式。

MATCH (excluded:Ingredient)
WHERE excluded.name in $excludedIngredients
WITH collect(excluded) as excluded
MATCH (r:Recipe)
WHERE NONE(i in excluded WHERE (r)-[:INCLUDES]->(i))
RETURN r

另一种正确方法:使用 OPTIONAL MATCH 匹配被排除集合的成员,并排除非空值

另一种方法不使用 WHERE NONE(),而是使用从 :Recipe 开始的 OPTIONAL MATCH,但只匹配到要排除的节点。

如果没有与任何被排除节点匹配,新引入的变量 i 将为空,我们可以使用另一个 WHERE 子句来过滤,以仅获取那些行。

MATCH (excluded:Ingredient)
WHERE excluded.name in $excludedIngredients
WITH collect(excluded) as excluded
MATCH (r:Recipe)
OPTIONAL MATCH (r)-[:INCLUDES]->(i)
WHERE i in excluded
WITH r
WHERE i IS NULL
RETURN r