apoc.path.expandConfig
Procedure APOC Core
apoc.path.expandConfig(startNode <id>|Node|list, {minLevel,maxLevel,uniqueness,relationshipFilter,labelFilter,uniqueness:'RELATIONSHIP_PATH',bfs:true, filterStartNode:false, limit:-1, optional:false, endNodes:[], terminatorNodes:[], sequence, beginSequenceAtStart:true}) yield path - expand from start node following the given relationships from min to max-level adhering to the label filters.
Config parameters
This procedure support the following config parameters:
name | type | default | description |
---|---|---|---|
minLevel |
Long |
-1 |
the minimum number of hops in the traversal |
maxLevel |
Long |
-1 |
the maximum number of hops in the traversal |
relationshipFilter |
String |
null |
the relationship types and directions to traverse. See Relationship Filters. |
labelFilter |
String |
null |
the node labels to traverse. See Label Filters. |
sequence |
String |
null |
comma-separated alternating label and relationship filters, for each step in a repeating sequence.
If present, See Specifying Sequences of node labels and relationship types. |
beginSequenceAtStart |
Boolean |
true |
starts matching sequences of node labels and/or relationship types (defined in |
uniqueness |
String |
RELATIONSHIP_PATH |
the strategy to use when expanding relationships in a traversal. See Uniqueness. |
bfs |
Boolean |
true |
use Breadth First Search when traversing. Uses Depth First Search if set to |
filterStartNode |
Boolean |
false |
whether the |
limit |
Long |
-1 |
limit the number of paths returned. When using |
optional |
Boolean |
false |
is path expansion optional?
If set to |
endNodes |
List<Node> |
null |
only these nodes can end returned paths, and expansion will continue past these nodes, if possible. |
terminatorNodes |
List<Node> |
null |
Only these nodes can end returned paths, and expansion won’t continue past these nodes. |
whitelistNodes |
List<Node> |
null |
Only these nodes are allowed in the expansion (though endNodes and terminatorNodes will also be allowed, if present). |
blacklistNodes |
List<Node> |
null |
None of the paths returned will include these nodes. |
Relationship Filters
The syntax for relationship filters is described below:
Syntax: [<]RELATIONSHIP_TYPE1[>]|[<]RELATIONSHIP_TYPE2[>]|…
input | type | direction |
---|---|---|
|
|
OUTGOING |
|
|
INCOMING |
|
|
BOTH |
|
|
OUTGOING |
|
|
INCOMING |
Label Filters
The syntax for label filters is described below:
Syntax: [+-/>]LABEL1|LABEL2|*|…
input | result |
---|---|
|
blacklist filter - No node in the path will have a label in the blacklist. |
|
whitelist filter - All nodes in the path must have a label in the whitelist (exempting termination and end nodes, if using those filters). If no whitelist operator is present, all labels are considered whitelisted. |
|
termination filter - Only return paths up to a node of the given labels, and stop further expansion beyond it. Termination nodes do not have to respect the whitelist. Termination filtering takes precedence over end node filtering. |
|
end node filter - Only return paths up to a node of the given labels, but continue expansion to match on end nodes beyond it. End nodes do not have to respect the whitelist to be returned, but expansion beyond them is only allowed if the node has a label in the whitelist. |
Label filter operator precedence and behavior
Multiple label filter operators are allowed at the same time. Take the following example:
labelFilter:'+Person|Movie|-SciFi|>Western|/Romance'
If we work through this label filter, we can see that:
-
:Person
and:Movie
labels are whitelisted -
:SciFi
is blacklisted -
:Western
is an end node label -
:Romance
is as a termination label.
The precedence of operator evaluation isn’t dependent upon their location in the labelFilter but is fixed:
Blacklist filter -
, termination filter /
, end node filter >
, whitelist filter +
.
This means:
-
No blacklisted label
-
will ever be present in the nodes of paths returned, even if the same label (or another label of a node with a blacklisted label) is included in another filter list. -
If the termination filter
/
or end node filter>
is used, then only paths up to nodes with those labels will be returned as results. These end nodes are exempt from the whitelist filter. -
If a node is a termination node
/
, no further expansion beyond the node will occur. -
The whitelist only applies to nodes up to but not including end nodes from the termination or end node filters. If no end node or termination node operators are present, then the whitelist applies to all nodes of the path.
-
If no whitelist operators are present in the labelFilter, this is treated as if all labels are whitelisted.
Uniqueness
Uniqueness of nodes and relationships guides the expansion and the returned results. The table below describes the available values:
value | description |
---|---|
|
For each returned node there’s a (relationship wise) unique path from the start node to it. This is Cypher’s default expansion mode. |
|
A node cannot be traversed more than once. This is what the legacy traversal framework does. |
|
Entities on the same level are guaranteed to be unique. |
|
For each returned node there’s a unique path from the start node to it. |
|
This is like NODE_GLOBAL, but only guarantees uniqueness among the most recent visited nodes, with a configurable count. Traversing a huge graph is quite memory intensive in that it keeps track of all the nodes it has visited. For huge graphs a traverser can hog all the memory in the JVM, causing OutOfMemoryError. Together with this Uniqueness you can supply a count, which is the number of most recent visited nodes. This can cause a node to be visited more than once, but scales infinitely. |
|
A relationship cannot be traversed more than once, whereas nodes can. |
|
Entities on the same level are guaranteed to be unique. |
|
Same as for NODE_RECENT, but for relationships. |
|
No restriction (the user will have to manage it) |
Specifying Sequences of node labels and relationship types
Path expander procedures can expand on repeating sequences of labels, relationship types, or both. Sequences can be defined as follows:
-
If only using label sequences, use the
labelFilter
, but use commas to separate the filtering for each step in the repeating sequence. -
If only using relationship sequences, use the
relationshipFilter
, but use commas to separate the filtering for each step of the repeating sequence. -
If using sequences of both relationships and labels, use the
sequence
parameter.
Usage | config param | description | syntax | explanation |
---|---|---|---|---|
label sequences only |
|
Same syntax and filters, but uses commas ( |
|
Start node must be a :Post node that isn’t :Blocked, next node must be a :Reply, and the next must be an :Admin, then repeat if able. Only paths ending with the |
relationship sequences only |
|
Same syntax, but uses commas ( |
|
Expansion will first expand |
sequences of both labels and relationships |
|
A string of comma-separated alternating label and relationship filters, for each step in a repeating sequence. The sequence should begin with a label filter, and end with a relationship filter. If present, |
|
Combines the behaviors above. |
There are some uses cases where the sequence does not begin at the start node, but at one node distant.
The config parameter beginSequenceAtStart
toggles this behavior.
Its default value is true
.
If set to false
, this changes the expected values for labelFilter
, relationshipFilter
, and sequence
as noted below:
sequence | altered behavior | example | explanation |
---|---|---|---|
|
The start node is not considered part of the sequence. The sequence begins one node off from the start node. |
|
The next node(s) out from the start node begins the sequence (and must be a :Post node that isn’t :Blocked), and only paths ending with |
|
The first relationship filter in the sequence string will not be considered part of the repeating sequence, and will only be used for the first relationship from the start node to the node that will be the actual start of the sequence. |
|
|
|
Combines the above two behaviors. |
|
Combines the behaviors above. |
Sequence tips
Label filtering in sequences work together with the If you need to limit the number of times a sequence repeats, this can be done with the |
Usage Examples
The examples in this section are based on the following sample graph:
MERGE (mark:Person:DevRel {name: "Mark"})
MERGE (lju:Person:DevRel {name: "Lju"})
MERGE (praveena:Person:Engineering {name: "Praveena"})
MERGE (zhen:Person:Engineering {name: "Zhen"})
MERGE (martin:Person:Engineering {name: "Martin"})
MERGE (joe:Person:Field {name: "Joe"})
MERGE (stefan:Person:Field {name: "Stefan"})
MERGE (alicia:Person:Product {name: "Alicia"})
MERGE (jake:Person:Product {name: "Jake"})
MERGE (john:Person:Product {name: "John"})
MERGE (jonny:Person:Sales {name: "Jonny"})
MERGE (anthony:Person:Sales {name: "Anthony"})
MERGE (rik:Person:Sales {name: "Rik"})
MERGE (zhen)-[:KNOWS]-(stefan)
MERGE (zhen)-[:KNOWS]-(lju)
MERGE (zhen)-[:KNOWS]-(praveena)
MERGE (zhen)-[:KNOWS]-(martin)
MERGE (mark)-[:KNOWS]-(jake)
MERGE (alicia)-[:KNOWS]-(jake)
MERGE (jonny)-[:KNOWS]-(anthony)
MERGE (john)-[:KNOWS]-(rik)
MERGE (alicia)-[:FOLLOWS]->(joe)
MERGE (joe)-[:FOLLOWS]->(mark)
MERGE (joe)-[:FOLLOWS]->(praveena)
MERGE (joe)-[:FOLLOWS]->(zhen)
MERGE (mark)-[:FOLLOWS]->(stefan)
MERGE (stefan)-[:FOLLOWS]->(joe)
MERGE (praveena)-[:FOLLOWS]->(joe)
MERGE (lju)-[:FOLLOWS]->(jake)
MERGE (alicia)-[:FOLLOWS]->(jonny)
MERGE (zhen)-[:FOLLOWS]->(john)
MERGE (anthony)-[:FOLLOWS]->(joe)
The Neo4j Browser visualization below shows the sample graph:
The KNOWS
relationship type is considered to be bidirectional, where if Zhen knows Stefan, we can imply that Stefan knows Zhen.
When using the KNOWS
relationship we will ignore the direction.
The FOLLOWS
relationship has a direction, so we will specify a direction when we use it.
Relationship Type and Node Label filters
Let’s start by expanding paths from the Praveena node.
We only want to consider the KNOWS
relationship type, so we’ll specify that as the relationshipFilter
parameter.
KNOWS
from 1 to 2 hopsMATCH (p:Person {name: "Praveena"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "KNOWS",
minLevel: 1,
maxLevel: 2
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"}) |
1 |
(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Martin"}) |
2 |
(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:DevRel {name: "Lju"}) |
2 |
(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Field {name: "Stefan"}) |
2 |
Praveena only has a direct KNOWS
relationship to Zhen, but Zhen has KNOWS
relationships to 3 other people, which means they’re 2 hops away from Praveena.
We can also provide a node label filter to restrict the nodes that are returned.
If we want to only return paths where every node has the Engineering
label, we’ll provide the value +Engineering
to the labelFilter
parameter.
Engineering
people that Praveena KNOWS
from 1 to 2 hopsMATCH (p:Person {name: "Praveena"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "KNOWS",
labelFilter: "+Engineering",
minLevel: 1,
maxLevel: 2
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"}) |
1 |
(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Martin"}) |
2 |
We lose the paths that ended with Lju and Stefan because neither of those nodes had the Engineering
label.
We can specify multiple relationship types.
The following query starts from the Alicia node, and then expands the FOLLOWS
and KNOWS
relationships:
FOLLOWS
or KNOWS
from 1 to 3 hopsMATCH (p:Person {name: "Alicia"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>|KNOWS",
minLevel: 1,
maxLevel: 3
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
1 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Sales {name: "Jonny"}) |
1 |
(:Person:Product {name: "Alicia"})-[:KNOWS]→(:Person:Product {name: "Jake"}) |
1 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Sales {name: "Jonny"})-[:KNOWS]→(:Person:Sales {name: "Anthony"}) |
2 |
(:Person:Product {name: "Alicia"})-[:KNOWS]→(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:DevRel {name: "Mark"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:FOLLOWS]→(:Person:Product {name: "John"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Martin"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Praveena"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:DevRel {name: "Lju"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Field {name: "Stefan"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"})-[:KNOWS]→(:Person:Product {name: "Jake"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Sales {name: "Jonny"})-[:KNOWS]→(:Person:Sales {name: "Anthony"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
3 |
(:Person:Product {name: "Alicia"})-[:KNOWS]→(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"}) |
3 |
This query returns 19 paths, Alicia is very well connected!
We can see a Neo4j Browser visualization of the returned paths in Paths from Alicia.
We can also specify traversal termination criteria using label filters.
If we wanted to terminate a traversal as soon as the traversal encounters a node containing the Engineering
label, we can use the /Engineering
node filter.
FOLLOWS
or KNOWS
from 1 to 3 hops, terminating as soon as a node with the Engineering
label is reachedMATCH (p:Person {name: "Alicia"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>|KNOWS",
labelFilter: "/Engineering",
minLevel: 1,
maxLevel: 3
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"}) |
2 |
We’re now down to only two paths.
But this query doesn’t capture all of the paths from Alicia that end in a node with the Engineering
label.
We can use the >Engineering
node filter to define a traversal that:
-
only returns paths that terminate at nodes with the
Engineering
label -
continues expansion to end nodes after that, looking for more paths that end with the
Engineering
label
FOLLOWS
or KNOWS
from 1 to 3 hops, where paths end with a node with the Engineering
labelMATCH (p:Person {name: "Alicia"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>|KNOWS",
labelFilter: ">Engineering",
minLevel: 1,
maxLevel: 3
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Martin"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Praveena"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"}) |
3 |
Our query now also returns paths going through Praveena and Zhen, one going to Martin, and other others going back to Zhen and Praveena!
Terminator Nodes and End Nodes
As well as specifying terminator and end labels for traversals, we can also specify terminator and end nodes.
Let’s build on the previous query that found people that Alicia KNOWS
or FOLLOWS
.
We want any returned paths to stop as soon as the Joe node is encountered, which we can do by passing the Joe node to the terminatorNodes
parameter.
FOLLOWS
or KNOWS
from 1 to 3 hops, terminating as soon as Joe is reachedMATCH (p:Person {name: "Alicia"})
MATCH (joe:Person {name: "Joe"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>|KNOWS",
minLevel: 1,
maxLevel: 3,
terminatorNodes: [joe]
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
1 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Sales {name: "Jonny"})-[:KNOWS]→(:Person:Sales {name: "Anthony"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
3 |
Alicia FOLLOWS
Joe, but there’s also another path that goes via Jonny and Anthony.
The terminator nodes approach doesn’t necessarily find all the paths that exist between Alicia and Joe.
There might be other paths that go through the Joe node twice.
We can find these paths by passing the Joe node to the endNodes
parameter.
If we use this parameter, all returned paths will end at the Joe node, but expansion will continue past this node to try and find other paths that end at Joe.
FOLLOWS
or KNOWS
from 1 to 3 hops, where paths end when they reach JoeMATCH (p:Person {name: "Alicia"})
MATCH (joe:Person {name: "Joe"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>|KNOWS",
minLevel: 1,
maxLevel: 3,
endNodes: [joe]
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
1 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Sales {name: "Jonny"})-[:KNOWS]→(:Person:Sales {name: "Anthony"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
3 |
We’ve got the two paths we got with the terminator nodes approach, from Alicia to Joe, and from Alicia to Jonny to Jonny to Joe. But we’ve also got an extra path that goes from Alicia to Joe to Praveena to Joe.
Whitelist Nodes and Blacklist Nodes
Whitelist and blacklist nodes can also be specified.
Let’s build on the previous query that found people that Alicia KNOWS
or FOLLOWS
.
We want any returned paths to only include the nodes Mark, Joe, Zhen, and Praveena, which we can do by passing these nodes to the parameter whitelistNodes
.
FOLLOWS
or KNOWS
relationship types from 1 to 3 hops, only including paths that contain Mark, Joe, Zhen, and PraveenaMATCH (p:Person {name: "Alicia"})
MATCH (whitelist:Person)
WHERE whitelist.name IN ["Mark", "Joe", "Zhen", "Praveena"]
WITH p, collect(whitelist) AS whitelistNodes
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>|KNOWS",
minLevel: 1,
maxLevel: 3,
whitelistNodes: whitelistNodes
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
1 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Praveena"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"}) |
3 |
Out of the white list, the only person with a direct connection to Alicia is Joe, so all paths go through him. We then go from Joe to the others, and then between each other for the paths of 3 hops.
We can see a Neo4j Browser visualization of the returned paths in Paths from Alicia to Mark, Joe, Zhen, and Praveena.
A blacklist is used to exclude nodes from the returned paths.
If we want to exclude paths that contain Joe, we can do this by passing the Joe node to the blacklistNodes
parameter.
FOLLOWS
or KNOWS
from 1 to 3 hops, excluding paths that include JoeMATCH (p:Person {name: "Alicia"})
MATCH (joe:Person {name: "Joe"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>|KNOWS",
minLevel: 1,
maxLevel: 3,
blacklistNodes: [joe]
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Sales {name: "Jonny"}) |
1 |
(:Person:Product {name: "Alicia"})-[:KNOWS]→(:Person:Product {name: "Jake"}) |
1 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Sales {name: "Jonny"})-[:KNOWS]→(:Person:Sales {name: "Anthony"}) |
2 |
(:Person:Product {name: "Alicia"})-[:KNOWS]→(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:DevRel {name: "Mark"}) |
2 |
(:Person:Product {name: "Alicia"})-[:KNOWS]→(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"}) |
3 |
This returns a very small set of paths since Joe was a very pivotal node in connecting Alicia to the rest of the graph.
We can see a Neo4j Browser visualization of the returned paths in Paths from Alicia that don’t include Joe.
Breadth First Search and Depth First Search
We can control whether the traversal uses the Breadth First Search (BFS), by specifying bfs: true
, or Depth First Search algorithm (DFS), by specifying bfs: false
.
This is often combined with the limit
parameter to find the nearest nodes based on the chosen algorithm.
FOLLOWS
or KNOWS
from 1 to 3 hops, using BFSMATCH (p:Person {name: "Alicia"})
MATCH (joe:Person {name: "Joe"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>|KNOWS",
minLevel: 1,
maxLevel: 5,
bfs: true,
limit: 10
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
1 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Sales {name: "Jonny"}) |
1 |
(:Person:Product {name: "Alicia"})-[:KNOWS]→(:Person:Product {name: "Jake"}) |
1 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Sales {name: "Jonny"})-[:KNOWS]→(:Person:Sales {name: "Anthony"}) |
2 |
(:Person:Product {name: "Alicia"})-[:KNOWS]→(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:DevRel {name: "Mark"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:FOLLOWS]→(:Person:Product {name: "John"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Martin"}) |
3 |
From these results we can see that paths are completely expanded at each level before going onto the next one. For example, we first expand from:
-
Alicia
→
Joe -
Alicia
→
Jonny -
Alicia
→
Jake
Before then following relationships from those nodes. And once it’s expanded everything at level 2, it will then explore level 3.
If we use the Depth First Search algorithm, the traversal will go as far as it can (up to the maxLevel
of hops) down a particular path, before going back up and exploring other ones.
FOLLOWS
or KNOWS
from 1 to 3 hops, using DFSMATCH (p:Person {name: "Alicia"})
MATCH (joe:Person {name: "Joe"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>|KNOWS",
minLevel: 1,
maxLevel: 3,
bfs: false,
limit: 10
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
1 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"}) |
2 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:FOLLOWS]→(:Person:Product {name: "John"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Martin"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Praveena"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:DevRel {name: "Lju"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Field {name: "Stefan"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
3 |
(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"}) |
3 |
Now we have a different set of paths returned. We don’t even see the paths from Alicia to Jonny or Alicia to Jake because our limit of 10 paths is completely taken up with paths going through Joe.
We can see a Neo4j Browser visualization of the returned paths in Paths from Alicia using Depth First Search.
Uniqueness
We can specify the uniqueness strategy to be used by the traversal through the uniqueness
parameter.
See Uniqueness for a list of valid strategies.
The default value is RELATIONSHIP_PATH
.
In this section we’re going to write queries that start from Joe and traverse the FOLLOWS
relationship.
FOLLOWS
relationship type from 1 to 3 hopsMATCH (p:Person {name: "Joe"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>",
minLevel: 1,
maxLevel: 3,
uniqueness: "RELATIONSHIP_PATH" // default
})
YIELD path
RETURN [node in nodes(path) | node.name] AS nodes, length(path) AS hops
ORDER BY hops;
nodes | hops |
---|---|
["Joe", "Zhen"] |
1 |
["Joe", "Praveena"] |
1 |
["Joe", "Mark"] |
1 |
["Joe", "Zhen", "John"] |
2 |
["Joe", "Praveena", "Joe"] |
2 |
["Joe", "Mark", "Stefan"] |
2 |
["Joe", "Praveena", "Joe", "Zhen"] |
3 |
["Joe", "Praveena", "Joe", "Mark"] |
3 |
["Joe", "Mark", "Stefan", "Joe"] |
3 |
Several of the paths returned contain the Joe node twice.
If we want to ensure that the nodes in a path are unique, we can use the NODE_PATH
strategy.
FOLLOWS
relationship type from 1 to 3 hops, using the NODE_PATH
strategyMATCH (p:Person {name: "Joe"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>",
minLevel: 1,
maxLevel: 3,
uniqueness: "NODE_PATH"
})
YIELD path
RETURN [node in nodes(path) | node.name] AS nodes, length(path) AS hops
ORDER BY hops;
nodes | hops |
---|---|
["Joe", "Zhen"] |
1 |
["Joe", "Praveena"] |
1 |
["Joe", "Mark"] |
1 |
["Joe", "Zhen", "John"] |
2 |
["Joe", "Mark", "Stefan"] |
2 |
The paths returned now have unique lists of nodes.
Sequences of relationship types
Sequences of relationship types can be specified by comma separating the values passed to relationshipFilter
.
For example, if we want to start from the Joe node and traverse a sequence of the FOLLOWS
relationship in the outgoing direction and the KNOWS
relationship in either direction, we can specify the relationship filter FOLLOWS>,KNOWS
.
FOLLOWS
and KNOWS
MATCH (p:Person {name: "Joe"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>,KNOWS",
beginSequenceAtStart: true,
minLevel: 1,
maxLevel: 4
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"}) |
1 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"}) |
1 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"}) |
1 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Martin"}) |
2 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Praveena"}) |
2 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:DevRel {name: "Lju"}) |
2 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Field {name: "Stefan"}) |
2 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"}) |
2 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"})-[:KNOWS]→(:Person:Product {name: "Jake"}) |
2 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
3 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:DevRel {name: "Lju"})-[:FOLLOWS]→(:Person:Product {name: "Jake"}) |
3 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Field {name: "Stefan"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
3 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"})-[:FOLLOWS]→(:Person:Product {name: "John"}) |
3 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:DevRel {name: "Lju"})-[:FOLLOWS]→(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:DevRel {name: "Mark"}) |
4 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:DevRel {name: "Lju"})-[:FOLLOWS]→(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:Product {name: "Alicia"}) |
4 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"})-[:FOLLOWS]→(:Person:Product {name: "John"})-[:KNOWS]→(:Person:Sales {name: "Rik"}) |
4 |
The minLevel
and maxLevel
values refer to the number of relationships in the path.
Using a minLevel
of 1 means that paths one hop from Joe with the FOLLOWS
relationship type will be returned.
If we want to ensure that the relationship type sequence defined in this relationshipFilter
is matched at least once, we need to use a minLevel
of 2
since there are two relationship types in the filter.
FOLLOWS
and KNOWS
MATCH (p:Person {name: "Joe"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "FOLLOWS>,KNOWS",
beginSequenceAtStart: true,
minLevel: 2,
maxLevel: 4
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Martin"}) |
2 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Praveena"}) |
2 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:DevRel {name: "Lju"}) |
2 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Field {name: "Stefan"}) |
2 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"}) |
2 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"})-[:KNOWS]→(:Person:Product {name: "Jake"}) |
2 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
3 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:DevRel {name: "Lju"})-[:FOLLOWS]→(:Person:Product {name: "Jake"}) |
3 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Field {name: "Stefan"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
3 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"})-[:FOLLOWS]→(:Person:Product {name: "John"}) |
3 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:DevRel {name: "Lju"})-[:FOLLOWS]→(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:DevRel {name: "Mark"}) |
4 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:DevRel {name: "Lju"})-[:FOLLOWS]→(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:Product {name: "Alicia"}) |
4 |
(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"})-[:FOLLOWS]→(:Person:Product {name: "John"})-[:KNOWS]→(:Person:Sales {name: "Rik"}) |
4 |
This config can also be used in combination with beginSequenceAtStart: false
, which means that the sequence will start one hop away from the starting node.
If we use this config, it means that the first relationship type defined in relationshipFilter
will only apply to the starting node.
FOLLOWS
and KNOWS
, after first following KNOWS
relationships from JakeMATCH (p:Person {name: "Jake"})
CALL apoc.path.expandConfig(p, {
relationshipFilter: "KNOWS,FOLLOWS>,KNOWS",
beginSequenceAtStart: false,
minLevel: 3,
maxLevel: 7
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"}) |
3 |
(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Sales {name: "Jonny"})-[:KNOWS]→(:Person:Sales {name: "Anthony"}) |
3 |
(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"})-[:FOLLOWS]→(:Person:Product {name: "John"}) |
4 |
(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:Product {name: "Alicia"})-[:FOLLOWS]→(:Person:Sales {name: "Jonny"})-[:KNOWS]→(:Person:Sales {name: "Anthony"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
4 |
(:Person:Product {name: "Jake"})←[:KNOWS]-(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"})-[:FOLLOWS]→(:Person:Product {name: "John"})-[:KNOWS]→(:Person:Sales {name: "Rik"}) |
5 |
Sequences of node labels
Sequences of node labels can be specified by comma separating values passed to labelFilter
.
This is usually used in combination with beginSequenceAtStart: false
, which means that sequences will start one hop away from the starting node.
For example, if we start from the Praveena node and want to return the paths that contain alternating Field
and DevRel
nodes, we can specify a label filter of "+Field,+DevRel"
.
Field
and DevRel
labels.MATCH (p:Person {name: "Praveena"})
CALL apoc.path.expandConfig(p, {
labelFilter: "+Field,+DevRel",
beginSequenceAtStart: false,
minLevel: 1,
maxLevel: 4
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"}) |
1 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"}) |
1 |
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"}) |
2 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"}) |
2 |
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"}) |
3 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"}) |
3 |
The minLevel
and maxLevel
values refer to the number of relationships in the path.
Using a minLevel
of 1 means that paths where the node one hop from Praveena has the Field
label will be returned.
If we want to ensure that the label sequence defined in this labelFilter
is matched at least once, we need to use a minLevel
of 2
.
Field
and DevRel
labels.MATCH (p:Person {name: "Praveena"})
CALL apoc.path.expandConfig(p, {
labelFilter: "+Field,+DevRel",
beginSequenceAtStart: false,
minLevel: 2,
maxLevel: 4
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"}) |
2 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"}) |
2 |
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"}) |
3 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"}) |
3 |
The paths that only contain a relationship from Praveena to Joe have now been filtered out.
But what if we don’t want to specify multiple labels exist, but instead want to find paths where a node doesn’t have a label?
To find paths that contain alternating Field
and not Field
nodes, we can specify a label filter of "+Field,-Field"
.
Field
label and not having the Field
labelMATCH (p:Person {name: "Praveena"})
CALL apoc.path.expandConfig(p, {
labelFilter: "+Field,-Field",
beginSequenceAtStart: false,
minLevel: 2,
maxLevel: 4
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
path | hops |
---|---|
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})←[:FOLLOWS]-(:Person:Sales {name: "Anthony"}) |
2 |
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"}) |
2 |
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})←[:FOLLOWS]-(:Person:Product {name: "Alicia"}) |
2 |
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"}) |
2 |
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})←[:FOLLOWS]-(:Person:Engineering {name: "Praveena"}) |
2 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})←[:FOLLOWS]-(:Person:Sales {name: "Anthony"}) |
2 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"}) |
2 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})←[:FOLLOWS]-(:Person:Product {name: "Alicia"}) |
2 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Praveena"}) |
2 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"}) |
2 |
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Field {name: "Stefan"}) |
3 |
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"}) |
3 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Field {name: "Stefan"}) |
3 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"}) |
3 |
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Field {name: "Stefan"})←[:FOLLOWS]-(:Person:DevRel {name: "Mark"}) |
4 |
(:Person:Engineering {name: "Praveena"})←[:FOLLOWS]-(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"}) |
4 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:Engineering {name: "Zhen"})-[:KNOWS]→(:Person:Field {name: "Stefan"})←[:FOLLOWS]-(:Person:DevRel {name: "Mark"}) |
4 |
(:Person:Engineering {name: "Praveena"})-[:FOLLOWS]→(:Person:Field {name: "Joe"})-[:FOLLOWS]→(:Person:DevRel {name: "Mark"})-[:FOLLOWS]→(:Person:Field {name: "Stefan"})←[:KNOWS]-(:Person:Engineering {name: "Zhen"}) |
4 |
We’ve got a lot more paths, with path lengths between 2 and 4 hops. These paths have the following labels:
-
2 hops -
Field
→ NotField
-
3 hops -
Field
→ NotField
→Field
-
4 hops -
Field
→ NotField
→Field
→ NotField
These paths are a bit difficult to read, so we can simplify the output by using the nodes
function to just return the nodes.
We’ll also filter the results so that we only return paths that match the complete +Field,-Field
label filter.
We can do this by only returning paths of even length:
Field
label and not having the Field
labelMATCH (p:Person {name: "Praveena"})
CALL apoc.path.expandConfig(p, {
labelFilter: "+Field,-Field",
beginSequenceAtStart: false,
minLevel: 2,
maxLevel: 4
})
YIELD path
WHERE length(path) % 2 = 0
// Remove the Praveena node from the returned path
RETURN nodes(path)[1..] AS nodes, length(path) AS hops
ORDER BY hops;
nodes | hops |
---|---|
[(:Person:Field {name: "Joe"}), (:Person:Sales {name: "Anthony"})] |
2 |
[(:Person:Field {name: "Joe"}), (:Person:Engineering {name: "Zhen"})] |
2 |
[(:Person:Field {name: "Joe"}), (:Person:Product {name: "Alicia"})] |
2 |
[(:Person:Field {name: "Joe"}), (:Person:DevRel {name: "Mark"})] |
2 |
[(:Person:Field {name: "Joe"}), (:Person:Engineering {name: "Praveena"})] |
2 |
[(:Person:Field {name: "Joe"}), (:Person:Sales {name: "Anthony"})] |
2 |
[(:Person:Field {name: "Joe"}), (:Person:Engineering {name: "Zhen"})] |
2 |
[(:Person:Field {name: "Joe"}), (:Person:Product {name: "Alicia"})] |
2 |
[(:Person:Field {name: "Joe"}), (:Person:Engineering {name: "Praveena"})] |
2 |
[(:Person:Field {name: "Joe"}), (:Person:DevRel {name: "Mark"})] |
2 |
[(:Person:Field {name: "Joe"}), (:Person:Engineering {name: "Zhen"}), (:Person:Field {name: "Stefan"}), (:Person:DevRel {name: "Mark"})] |
4 |
[(:Person:Field {name: "Joe"}), (:Person:DevRel {name: "Mark"}), (:Person:Field {name: "Stefan"}), (:Person:Engineering {name: "Zhen"})] |
4 |
[(:Person:Field {name: "Joe"}), (:Person:Engineering {name: "Zhen"}), (:Person:Field {name: "Stefan"}), (:Person:DevRel {name: "Mark"})] |
4 |
[(:Person:Field {name: "Joe"}), (:Person:DevRel {name: "Mark"}), (:Person:Field {name: "Stefan"}), (:Person:Engineering {name: "Zhen"})] |
4 |
The *
character can be used as a wildcard in a node sequence to indicate that any label can appear in that position.
If we want to match a sequence of nodes with any label followed by one with the DevRel
label, we can specify the label filter *,+DevRel
DevRel
labelMATCH (p:Person {name: "Praveena"})
CALL apoc.path.expandConfig(p, {
labelFilter: "*,+DevRel",
beginSequenceAtStart: false,
minLevel: 2,
maxLevel: 4
})
YIELD path
WHERE length(path) % 2 = 0
// Remove the Praveena node from the returned path
RETURN nodes(path)[1..] AS nodes, length(path) AS hops
ORDER BY hops;
nodes | hops |
---|---|
[(:Person:Field {name: "Joe"}), (:Person:DevRel {name: "Mark"})] |
2 |
[(:Person:Field {name: "Joe"}), (:Person:DevRel {name: "Mark"})] |
2 |
[(:Person:Engineering {name: "Zhen"}), (:Person:DevRel {name: "Lju"})] |
2 |
[(:Person:Field {name: "Joe"}), (:Person:DevRel {name: "Mark"}), (:Person:Product {name: "Jake"}), (:Person:DevRel {name: "Lju"})] |
4 |
[(:Person:Field {name: "Joe"}), (:Person:DevRel {name: "Mark"}), (:Person:Product {name: "Jake"}), (:Person:DevRel {name: "Lju"})] |
4 |
[(:Person:Engineering {name: "Zhen"}), (:Person:DevRel {name: "Lju"}), (:Person:Product {name: "Jake"}), (:Person:DevRel {name: "Mark"})] |
4 |