apoc.path.expandConfig

This procedure is not considered safe to run from multiple threads. It is therefore not supported by the parallel runtime (introduced in Neo4j 5.13). For more information, see the Cypher Manual → Parallel runtime.

Details

Syntax

apoc.path.expandConfig(startNode, config) :: (path)

Description

Returns PATH values expanded from the start NODE with the given RELATIONSHIP types from min-depth to max-depth.

Input arguments

Name

Type

Description

startNode

ANY

The node to start the algorithm from. startNode can be of type STRING (elementId()), INTEGER (id()), NODE, or `LIST<STRING | INTEGER | NODE>.

config

MAP

{ minLevel = -1 :: INTEGER, maxLevel = -1 :: INTEGER, relationshipFilter :: STRING, labelFilter :: STRING, beginSequenceAtStart = true :: BOOLEAN, uniqueness = ""RELATIONSHIP_PATH"" :: STRING, bfs = true :: BOOLEAN, filterStartNode = false :: BOOLEAN, limit = -1 :: INTEGER, optional = false :: BOOLEAN, endNodes :: LIST<NODES>, terminatorNodes:: LIST<NODES>, allowlistNodes:: LIST<NODES>, denylistNodes:: LIST<NODES> }

Return arguments

Name

Type

Description

path

PATH

The expanded path.

Config parameters

This procedure support the following config parameters:

Config parameters
Name Type Default Description

minLevel

INTEGER

-1

the minimum number of hops in the traversal

maxLevel

INTEGER

-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, labelFilter, and relationshipFilter are ignored, as this takes priority.

See Specifying Sequences of node labels and relationship types.

beginSequenceAtStart

BOOLEAN

true

starts matching sequences of node labels and/or relationship types (defined in relationshipFilter, labelFilter, or sequences) one node away from the start node.

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 false

filterStartNode

BOOLEAN

false

whether the labelFilter and sequence apply to the start node of the expansion.

limit

INTEGER

-1

limit the number of paths returned. When using bfs:true, 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.

optional

BOOLEAN

false

is path expansion optional? If set to true, a null value is yielded whenever the expansion would normally eliminate rows due to no results.

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.

allowlistNodes

LIST<NODE>

null

Only these nodes are allowed in the expansion (though endNodes and terminatorNodes will also be allowed, if present).

denylistNodes

LIST<NODE>

null

None of the paths returned will include these nodes.

whitelistNodes (deprecated)

LIST<NODE>

null

See allowlistNodes.

blacklistNodes (deprecated)

LIST<NODE>

null

See denylistNodes.

Relationship Filters

The syntax for relationship filters is described below:

Syntax: [<]RELATIONSHIP_TYPE1[>]|[<]RELATIONSHIP_TYPE2[>]|…​

input type direction

LIKES>

LIKES

OUTGOING

<FOLLOWS

FOLLOWS

INCOMING

KNOWS

KNOWS

BOTH

>

any type

OUTGOING

<

any type

INCOMING

Label Filters

The syntax for label filters is described below:

Syntax: [+-/>]LABEL1|LABEL2|*|…​

input result

-Foe

denylist filter - No node in the path will have a label in the denylist.

+Friend

allowlist filter - All nodes in the path must have a label in the allowlist (exempting termination and end nodes, if using those filters). If no allowlist operator is present, all labels are considered allowlisted.

/Friend

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 allowlist. Termination filtering takes precedence over end node filtering.

>Friend

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 allowlist to be returned, but expansion beyond them is only allowed if the node has a label in the allowlist.

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 allowlisted

  • :SciFi is denylisted

  • :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:

Denylist filter -, termination filter /, end node filter >, allowlist filter +.

This means:

  • No denylisted label - will ever be present in the nodes of paths returned, even if the same label (or another label of a node with a denylisted 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 allowlist filter.

  • If a node is a termination node /, no further expansion beyond the node will occur.

  • The allowlist 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 allowlist applies to all nodes of the path.

  • If no allowlist operators are present in the labelFilter, this is treated as if all labels are allowlisted.

Uniqueness

Uniqueness of nodes and relationships guides the expansion and the returned results. The table below describes the available values:

value description

RELATIONSHIP_PATH

For each returned node there’s a (relationship wise) unique path from the start NODE to it. This is Cypher’s default expansion mode.

NODE_GLOBAL

A node cannot be traversed more than once. This is what the legacy traversal framework does.

NODE_LEVEL

Entities on the same level are guaranteed to be unique.

NODE_PATH

For each returned node there’s a unique path from the start NODE to it.

NODE_RECENT

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.

RELATIONSHIP_GLOBAL

A relationship cannot be traversed more than once, whereas nodes can.

RELATIONSHIP_LEVEL

Entities on the same level are guaranteed to be unique.

RELATIONSHIP_RECENT

Same as for NODE_RECENT, but for relationships.

NONE

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

labelFilter

Same syntax and filters, but uses commas (,) to separate the filters for each step in the sequence.

labelFilter:'Post|-Blocked,Reply,>Admin'

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 :Admin node in that position of the sequence will be returned.

relationship sequences only

relationshipFilter

Same syntax, but uses commas (,) to separate the filters for each relationship traversal in the sequence.

relationshipFilter:'NEXT>,<FROM,POSTED>|REPLIED>'

Expansion will first expand NEXT> from the start node, then <FROM, then either POSTED> or REPLIED>, then repeat if able.

sequences of both labels and relationships

sequence

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, labelFilter, and relationshipFilter are ignored, as this takes priority.

sequence:'Post|-Blocked, NEXT>, Reply, <FROM, >Admin, POSTED>|REPLIED>'

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

labelFilter

The start node is not considered part of the sequence. The sequence begins one node off from the start node.

beginSequenceAtStart:false, labelFilter:'Post|-Blocked,Reply,>Admin'

The next NODE values out from the start NODE begins the sequence (and must be a :Post node that isn’t :Blocked), and only paths ending with Admin nodes returned.

relationshipFilter

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.

beginSequenceAtStart:false, relationshipFilter:'FIRST>,NEXT>,<FROM,POSTED>|REPLIED>'

FIRST> will be traversed just from the start NODE to the node that will be the start of the repeating NEXT>,<FROM,POSTED>|REPLIED> sequence.

sequence

Combines the above two behaviors.

beginSequenceAtStart:false, sequence:'FIRST>, Post|-Blocked, NEXT>, Reply, <FROM, >Admin, POSTED>|REPLIED>'

Combines the behaviors above.

Sequence tips

Label filtering in sequences work together with the endNodes+terminatorNodes, though inclusion of a node must be unanimous.

If you need to limit the number of times a sequence repeats, this can be done with the maxLevel config param (multiply the number of iterations with the size of the nodes in the sequence).

Output parameters

Name Type

path

PATH

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:

apoc.path.expandConfig
Figure 1. 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.

The following returns the paths to people that Praveena KNOWS from 1 to 2 hops
MATCH (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;
Results
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.

The following returns paths containing only Engineering people that Praveena KNOWS from 1 to 2 hops
MATCH (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;
Results
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:

The following returns paths containing people that Alicia FOLLOWS or KNOWS from 1 to 3 hops
MATCH (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;
Results
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.

apoc.path.expandConfig.alicia
Figure 2. 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.

The following returns paths containing people that Alicia FOLLOWS or KNOWS from 1 to 3 hops, terminating as soon as a node with the Engineering label is reached
MATCH (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;
Results
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

The following returns paths containing people that Alicia FOLLOWS or KNOWS from 1 to 3 hops, where paths end with a node with the Engineering label
MATCH (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;
Results
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.

The following returns paths containing people that Alicia FOLLOWS or KNOWS from 1 to 3 hops, terminating as soon as Joe is reached
MATCH (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;
Results
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.

The following returns paths containing people that Alicia FOLLOWS or KNOWS from 1 to 3 hops, where paths end when they reach Joe
MATCH (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;
Results
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.

Allowlist Nodes and Denylist Nodes

Allowlist and denylist 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 allowlistNodes.

The following returns paths from Alicia following the FOLLOWS or KNOWS relationship types from 1 to 3 hops, only including paths that contain Mark, Joe, Zhen, and Praveena
MATCH (p:Person {name: "Alicia"})
MATCH (allowlist:Person)
WHERE allowlist.name IN ["Mark", "Joe", "Zhen", "Praveena"]
WITH p, collect(allowlist) AS allowlistNodes
CALL apoc.path.expandConfig(p, {
    relationshipFilter: "FOLLOWS>|KNOWS",
    minLevel: 1,
    maxLevel: 3,
    allowlistNodes: allowlistNodes
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
Results
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 allowlist, 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.

apoc.path.expandConfig.allowlist
Figure 3. Paths from Alicia to Mark, Joe, Zhen, and Praveena

A denylist 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 denylistNodes parameter.

The following returns paths containing people that Alicia FOLLOWS or KNOWS from 1 to 3 hops, excluding paths that include Joe
MATCH (p:Person {name: "Alicia"})
MATCH (joe:Person {name: "Joe"})
CALL apoc.path.expandConfig(p, {
    relationshipFilter: "FOLLOWS>|KNOWS",
    minLevel: 1,
    maxLevel: 3,
    denylistNodes: [joe]
})
YIELD path
RETURN path, length(path) AS hops
ORDER BY hops;
Results
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.

apoc.path.expandConfig.denylist
Figure 4. 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.

The following returns 10 paths containing people that Alicia FOLLOWS or KNOWS from 1 to 3 hops, using BFS
MATCH (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;
Results
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.

apoc.path.expandConfig.alicia.bfs
Figure 5. Paths from Alicia using Breadth First Search

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.

The following returns 10 paths containing people that Alicia FOLLOWS or KNOWS from 1 to 3 hops, using DFS
MATCH (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;
Results
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.

apoc.path.expandConfig.alicia.dfs
Figure 6. 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.

The following returns the nodes in paths starting from Joe and traversing the FOLLOWS relationship type from 1 to 3 hops
MATCH (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;
Results
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.

The following returns the nodes in paths starting from Joe and traversing the FOLLOWS relationship type from 1 to 3 hops, using the NODE_PATH strategy
MATCH (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;
Results
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.

The following returns the paths of 1 to 4 hops from Joe where the relationship types alternate between 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;
Results
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.

The following returns the paths of 2 to 4 hops from Joe where the relationship types alternate between 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;
Results
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.

The following returns the paths of 3 to 5 hops from Jake where the relationship types alternate between FOLLOWS and KNOWS, after first following KNOWS relationships from Jake
MATCH (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;
Results
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".

The following returns the paths of 1 to 4 hops from Praveena where the nodes alternate between having the 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;
Results
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.

The following returns the paths of 2 to 4 hops from Praveena where the nodes alternate between having the 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;
Results
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".

The following returns the paths of 1 to 4 hops from Praveena where the nodes alternate between having the Field label and not having the Field label
MATCH (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;
Results
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 → Not Field

  • 3 hops - Field → Not FieldField

  • 4 hops - Field → Not FieldField → Not Field

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:

The following returns nodes of paths of 1 to 4 hops from Praveena where the nodes alternate between having the Field label and not having the Field label
MATCH (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;
Results
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

The following returns nodes of paths of 2 to 4 hops from Praveena where the nodes alternate between having any label and the DevRel label
MATCH (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;
Results
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