From 7795891db41c5391a51c7b71148d96530800d8f2 Mon Sep 17 00:00:00 2001 From: InverseFalcon Date: Sun, 19 Mar 2017 03:40:31 -0700 Subject: [PATCH] Add end node operator to path expander label filter (#327) * Add end node operator to path expander label filter * Updated end node operator in the code and tests --- docs/expand.adoc | 23 +++++++------ docs/overview.adoc | 7 ++-- src/main/java/apoc/path/PathExplorer.java | 5 +-- src/test/java/apoc/path/ExpandPathTest.java | 38 +++++++++++++++++++++ 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/docs/expand.adoc b/docs/expand.adoc index 04cd39b1db..a37a37c9fc 100644 --- a/docs/expand.adoc +++ b/docs/expand.adoc @@ -18,7 +18,7 @@ Expand from start node following the given relationships from min to max-level a ---- CALL apoc.path.expand(startNode |Node, relationshipFilter, labelFilter, minLevel, maxLevel ) -CALL apoc.path.expand(startNode |Node|list, 'TYPE|TYPE_OUT>||Node|list, 'TYPE|TYPE_OUT>|EndNodeLabel', minLevel, maxLevel ) yield path ---- ==== Relationship Filter @@ -35,9 +35,9 @@ Syntax: `[<]RELATIONSHIP_TYPE1[>]|[<]RELATIONSHIP_TYPE2[>]|...` ==== Label Filter -Syntax: `[+-/]LABEL1|LABEL2|...` +Syntax: `[+-/>]LABEL1|LABEL2|...` -Only one type of operation allowed in the label filter (`+` or `-` or `/`, never more than one). +Only one type of operation allowed in the label filter (`+` or `-` or `/` or `>`, never more than one). With APOC 3.2.x.x, label filters will no longer apply to starting nodes of the expansion by default. @@ -47,6 +47,7 @@ With APOC 3.2.x.x, label filters will no longer apply to starting nodes of the e | +Friend | Friend | whitelist filter - include label, all nodes in the path must have a label in the whitelist | -Foe | Foe | blacklist filter - exclude label, no node in the path will have a label in the blacklist | /Friend | Friend | termination filter - only return paths to :Friend nodes, and stop further traversal along a path after reaching a :Friend +| >Friend | Friend | end node filter - only return paths to :Friend nodes, but continue traversal along a path after reaching a :Friend |=== @@ -71,7 +72,7 @@ call apoc.path.expand(p,"ACTED_IN>|PRODUCED<|FOLLOWS<","+Movie|Person",1,2) yiel return p, pp ---- -.Termination label filter example +.Termination and end node label filter example We will first set a `:Western` label on some nodes. @@ -82,7 +83,7 @@ where p.name in ['Clint Eastwood', 'Gene Hackman'] set p:Western ---- -Now expand from 'Keanu Reeves' to all `:Western` nodes. +Now expand from 'Keanu Reeves' to all `:Western` nodes with a termination filter: [source,cypher] ---- @@ -92,7 +93,9 @@ return path ---- The one returned path only matches up to 'Gene Hackman'. -While there is a path from 'Keanu Reeves' to 'Clint Eastwood' through 'Gene Hackman', no further expansion is permitted through a node in the termination filter list. +While there is a path from 'Keanu Reeves' to 'Clint Eastwood' through 'Gene Hackman', no further expansion is permitted through a node in the termination filter. + +If you didn't want to stop expansion on reaching 'Gene Hackman', and wanted 'Clint Eastwood' returned as well, use the end node filter instead (`>`). == Expand with Config @@ -108,7 +111,7 @@ Takes an additional map parameter, `config`, to provide configuration options: {minLevel: -1|number, maxLevel: -1|number, relationshipFilter: '[<]RELATIONSHIP_TYPE1[>]|[<]RELATIONSHIP_TYPE2[>]|...', - labelFilter: '[+-]LABEL1|LABEL2|...', + labelFilter: '[+-/>]LABEL1|LABEL2|...', uniqueness: RELATIONSHIP_PATH|NONE|NODE_GLOBAL|NODE_LEVEL|NODE_PATH|NODE_RECENT| RELATIONSHIP_GLOBAL|RELATIONSHIP_LEVEL|RELATIONSHIP_RECENT, bfs: true|false, @@ -132,11 +135,9 @@ Use `filterStartNode = false` when you want your label filter to only apply to a .Limit -Only when using the termination label filter `/`, you can use the `limit` config parameter to limit the number of paths returned. - -When using `bfs:true` (which is the default for all expand procedures), this has the effect of returning paths to the `n` nearest nodes with labels in the termination filter, where `n` is the limit given. +Only when using the termination label filter `/` or end node filter `>`, you can use the `limit` config parameter to limit the number of paths returned. -Remember that when using the termination filter, further expansion is stopped when a termination node is reached, so other termination nodes beyond them may not be reachable or returned if there is no alternate path to them. +When using `bfs:true` (which is the default for all expand procedures), this has the effect of returning paths to the `n` nearest nodes with labels in the termination or end node filter, where `n` is the limit given. The default limit value, `-1`, means no limit. diff --git a/docs/overview.adoc b/docs/overview.adoc index 004b351fe2..588a5761b0 100644 --- a/docs/overview.adoc +++ b/docs/overview.adoc @@ -866,7 +866,7 @@ examples (thanks @keesvegter) -The apoc.path.expand procedure makes it possible to do variable length path traversals where you can specify the direction of the relationship per relationship type and a list of Label names which act as a "whitelist" or a "blacklist". The procedure will return a list of Paths in a variable name called "path". +The apoc.path.expand procedure makes it possible to do variable length path traversals where you can specify the direction of the relationship per relationship type and a list of Label names which act as a "whitelist" or a "blacklist" or define end nodes for the expansion. The procedure will return a list of Paths in a variable name called "path". [cols="1m,5"] |=== @@ -897,9 +897,9 @@ Syntax: `[<]RELATIONSHIP_TYPE1[>]|[<]RELATIONSHIP_TYPE2[>]|...` === Label Filter -Syntax: `[+-/]LABEL1|LABEL2|...` +Syntax: `[+-/>]LABEL1|LABEL2|...` -Only one type of operation allowed in the label filter (`+` or `-` or `/`, never more than one). +Only one type of operation allowed in the label filter (`+` or `-` or `/` or `>`, never more than one). With APOC 3.2.x.x, label filters will no longer apply to starting nodes of the expansion by default. @@ -909,6 +909,7 @@ With APOC 3.2.x.x, label filters will no longer apply to starting nodes of the e | +Friend | Friend | whitelist filter - include label, all nodes in the path must have a label in the whitelist | -Foe | Foe | blacklist filter - exclude label, no node in the path will have a label in the blacklist | /Friend | Friend | termination filter - only return paths to :Friend nodes, and stop further traversal along a path after reaching a :Friend +| >Friend | Friend | end node filter - only return paths to :Friend nodes, but continue traversal along a path after reaching a :Friend |=== === Uniqueness diff --git a/src/main/java/apoc/path/PathExplorer.java b/src/main/java/apoc/path/PathExplorer.java index dcdfa7b1a2..fa581bae72 100644 --- a/src/main/java/apoc/path/PathExplorer.java +++ b/src/main/java/apoc/path/PathExplorer.java @@ -201,7 +201,7 @@ public LabelEvaluator(String labelFilter, boolean filterStartNode, long limit) { @Override public Evaluation evaluate(Path path) { if (path.length() == 0 && !filterStartNode) { - if (operator == '/') { + if (operator == '/' || operator == '>') { return EXCLUDE_AND_CONTINUE; } else { return INCLUDE_AND_CONTINUE; @@ -217,11 +217,12 @@ public Evaluation evaluate(Path path) { case '-': result = labelExists(check) ? EXCLUDE_AND_PRUNE : INCLUDE_AND_CONTINUE; break; + case '>': case '/': if (limit != -1 && resultCount >= limit) { result = EXCLUDE_AND_PRUNE; } else if (labelExists(check)) { - result = INCLUDE_AND_PRUNE; + result = operator == '/' ? INCLUDE_AND_PRUNE : INCLUDE_AND_CONTINUE; resultCount++; } else { result = EXCLUDE_AND_CONTINUE; diff --git a/src/test/java/apoc/path/ExpandPathTest.java b/src/test/java/apoc/path/ExpandPathTest.java index 73c446dbcb..31a37f01e9 100644 --- a/src/test/java/apoc/path/ExpandPathTest.java +++ b/src/test/java/apoc/path/ExpandPathTest.java @@ -1,6 +1,7 @@ package apoc.path; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import apoc.util.Util; import org.junit.*; @@ -12,6 +13,7 @@ import apoc.util.TestUtil; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -102,4 +104,40 @@ public void testExplorePathWithLimitReturnsLimitedResults() { assertEquals("Clint Eastwood", path.endNode().getProperty("name")); }); } + + @Test + public void testExplorePathWithEndNodeLabel() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}) " + + "CALL apoc.path.expandConfig(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', labelFilter:'>Western', uniqueness: 'NODE_GLOBAL'}) yield path " + + "return path", + result -> { + List> maps = Iterators.asList(result); + assertEquals(2, maps.size()); + Path path = (Path) maps.get(0).get("path"); + assertEquals("Gene Hackman", path.endNode().getProperty("name"));; + path = (Path) maps.get(1).get("path"); + assertEquals("Clint Eastwood", path.endNode().getProperty("name")); + }); + } + + @Test + public void testExplorePathWithEndNodeLabelAndLimit() { + db.execute("MATCH (c:Person) WHERE c.name in ['Clint Eastwood', 'Gene Hackman', 'Christian Bale'] SET c:Western"); + + TestUtil.testResult(db, + "MATCH (k:Person {name:'Keanu Reeves'}) " + + "CALL apoc.path.expandConfig(k, {relationshipFilter:'ACTED_IN|PRODUCED|DIRECTED', labelFilter:'>Western', uniqueness: 'NODE_GLOBAL', limit:2}) yield path " + + "return path", + result -> { + List> maps = Iterators.asList(result); + assertEquals(2, maps.size()); + Path path = (Path) maps.get(0).get("path"); + assertEquals("Gene Hackman", path.endNode().getProperty("name"));; + path = (Path) maps.get(1).get("path"); + assertEquals("Clint Eastwood", path.endNode().getProperty("name")); + }); + } }