Skip to content

Commit

Permalink
Added whitelistNodes and blacklistNodes, to finish up node-specific f…
Browse files Browse the repository at this point in the history
…iltering for path expanders. (#796)
  • Loading branch information
InverseFalcon authored and jexp committed Apr 27, 2018
1 parent 030dbf5 commit 4c5a85a
Show file tree
Hide file tree
Showing 7 changed files with 604 additions and 221 deletions.
18 changes: 12 additions & 6 deletions docs/expand.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -282,15 +282,21 @@ For huge graphs a traverser can hog all the memory in the JVM, causing OutOfMemo
| NONE | No restriction (the user will have to manage it)
|===

.endNodes and terminatorNodes
.Node filters

As of the February 2018 APOC releases, if the end nodes of the expansion are known ahead of time (such as when testing reachability), then these nodes can be passed in as `endNodes` or `terminatorNodes`.
While label filters use labels to allow whitelisting, blacklisting, and restrictions on which kind of nodes can end or terminate expansion,
you can also filter based upon actual nodes.

This restricts the returned paths (or nodes) to only these nodes (or nodes with the given ids, if an integer list is passed).
Each of these config parameter accepts a list of nodes, or a list of node ids.

For `endNodes`, expansion continues past end nodes.

For `terminatorNodes`, expansion down a path stops when a terminator node is reached.
[opts=header,cols="m,a,a"]
|===
| config parameter | description | added in
| endNodes | Only these nodes can end returned paths, and expansion will continue past these nodes, if possible. | Winter 2018 APOC releases.
| terminatorNodes | Only these nodes can end returned paths, and expansion won't continue past these nodes. | Winter 2018 APOC releases.
| whitelistNodes | Only these nodes are allowed in the expansion (though endNodes and terminatorNodes will also be allowed, if present). | Spring 2018 APOC releases.
| blacklistNodes | None of the paths returned will include these nodes. | Spring 2018 APOC releases.
|===

.General Examples

Expand Down
22 changes: 12 additions & 10 deletions docs/overview.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1156,19 +1156,21 @@ For huge graphs a traverser can hog all the memory in the JVM, causing OutOfMemo
|===


=== End nodes and terminator nodes
== Node Filters

As of the January 2018 APOC releases, you can optionally use `endNodes` and `terminatorNodes` params in the config param map when the end nodes of the expansion are known.
While label filters use labels to allow whitelisting, blacklisting, and restrictions on which kind of nodes can end or terminate expansion,
you can also filter based upon actual nodes.

When `endNodes` are present, only these end nodes must be at the end of the expanded paths.
Expansion continues beyond end nodes.
This behavior is similar to the end node filter `>` in the label filters.
Each of these config parameter accepts a list of nodes, or a list of node ids.

Nodes given as `terminatorNodes` behave just like `endNodes` (they must be at the end of expanded paths), but stops traversal beyond the terminator nodes.
This behavior is similar to the termination filter `/` in the label filters.

`endNodes` and/or `terminatorNodes` do not conflict with each other (an end node will be returned even if not present in the terminator nodes, and vice versa),
and they can freely be used along with the labelFilter, but a node can only be included by unanimous agreement from endNodes+terminatoNodes and the labelFilter.
[opts=header,cols="m,a,a"]
|===
| config parameter | description | added in
| endNodes | Only these nodes can end returned paths, and expansion will continue past these nodes, if possible. | Winter 2018 APOC releases.
| terminatorNodes | Only these nodes can end returned paths, and expansion won't continue past these nodes. | Winter 2018 APOC releases.
| whitelistNodes | Only these nodes are allowed in the expansion (though endNodes and terminatorNodes will also be allowed, if present). | Spring 2018 APOC releases.
| blacklistNodes | None of the paths returned will include these nodes. | Spring 2018 APOC releases.
|===

== Parallel Node Search

Expand Down
82 changes: 82 additions & 0 deletions src/main/java/apoc/path/LabelSequenceEvaluator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package apoc.path;

import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.traversal.Evaluation;
import org.neo4j.graphdb.traversal.Evaluator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.neo4j.graphdb.traversal.Evaluation.EXCLUDE_AND_CONTINUE;
import static org.neo4j.graphdb.traversal.Evaluation.INCLUDE_AND_CONTINUE;

// when no commas present, acts as a pathwide label filter
public class LabelSequenceEvaluator implements Evaluator {
private List<LabelMatcherGroup> sequenceMatchers;

private Evaluation whitelistAllowedEvaluation;
private boolean endNodesOnly;
private boolean filterStartNode;
private boolean beginSequenceAtStart;
private long minLevel = -1;

public LabelSequenceEvaluator(String labelSequence, boolean filterStartNode, boolean beginSequenceAtStart, int minLevel) {
List<String> labelSequenceList;

// parse sequence
if (labelSequence != null && !labelSequence.isEmpty()) {
labelSequenceList = Arrays.asList(labelSequence.split(","));
} else {
labelSequenceList = Collections.emptyList();
}

initialize(labelSequenceList, filterStartNode, beginSequenceAtStart, minLevel);
}

public LabelSequenceEvaluator(List<String> labelSequenceList, boolean filterStartNode, boolean beginSequenceAtStart, int minLevel) {
initialize(labelSequenceList, filterStartNode, beginSequenceAtStart, minLevel);
}

private void initialize(List<String> labelSequenceList, boolean filterStartNode, boolean beginSequenceAtStart, int minLevel) {
this.filterStartNode = filterStartNode;
this.beginSequenceAtStart = beginSequenceAtStart;
this.minLevel = minLevel;
sequenceMatchers = new ArrayList<>(labelSequenceList.size());

for (String labelFilterString : labelSequenceList) {
LabelMatcherGroup matcherGroup = new LabelMatcherGroup().addLabels(labelFilterString.trim());
sequenceMatchers.add(matcherGroup);
endNodesOnly = endNodesOnly || matcherGroup.isEndNodesOnly();
}

// if true for one matcher, need to set true for all matchers
if (endNodesOnly) {
for (LabelMatcherGroup group : sequenceMatchers) {
group.setEndNodesOnly(endNodesOnly);
}
}

whitelistAllowedEvaluation = endNodesOnly ? EXCLUDE_AND_CONTINUE : INCLUDE_AND_CONTINUE;
}

@Override
public Evaluation evaluate(Path path) {
int depth = path.length();
Node node = path.endNode();
boolean belowMinLevel = depth < minLevel;

// if start node shouldn't be filtered, exclude/include based on if using termination/endnode filter or not
// minLevel evaluator will separately enforce exclusion if we're below minLevel
if (depth == 0 && (!filterStartNode || !beginSequenceAtStart)) {
return whitelistAllowedEvaluation;
}

// the user may want the sequence to begin at the start node (default), or the sequence may only apply from the next node on
LabelMatcherGroup matcherGroup = sequenceMatchers.get((beginSequenceAtStart ? depth : depth - 1) % sequenceMatchers.size());

return matcherGroup.evaluate(node, belowMinLevel);
}
}
104 changes: 104 additions & 0 deletions src/main/java/apoc/path/NodeEvaluators.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package apoc.path;

import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.traversal.Evaluation;
import org.neo4j.graphdb.traversal.Evaluator;
import org.neo4j.graphdb.traversal.Evaluators;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* Static factory methods for obtaining node evaluators
*/
public final class NodeEvaluators {
// non-instantiable
private NodeEvaluators() {};

/**
* Returns an evaluator which handles end nodes and terminator nodes
* Returns null if both lists are empty
*/
public static Evaluator endAndTerminatorNodeEvaluator(List<Node> endNodes, List<Node> terminatorNodes) {
Evaluator endNodeEvaluator = null;
Evaluator terminatorNodeEvaluator = null;

if (!endNodes.isEmpty()) {
Node[] nodes = endNodes.toArray(new Node[endNodes.size()]);
endNodeEvaluator = Evaluators.includeWhereEndNodeIs(nodes);
}

if (!terminatorNodes.isEmpty()) {
Node[] nodes = terminatorNodes.toArray(new Node[terminatorNodes.size()]);
terminatorNodeEvaluator = Evaluators.pruneWhereEndNodeIs(nodes);
}

if (endNodeEvaluator != null || terminatorNodeEvaluator != null) {
return new EndAndTerminatorNodeEvaluator(endNodeEvaluator, terminatorNodeEvaluator);
}

return null;
}

public static Evaluator whitelistNodeEvaluator(List<Node> whitelistNodes) {
return new WhitelistNodeEvaluator(whitelistNodes);
}

public static Evaluator blacklistNodeEvaluator(List<Node> blacklistNodes) {
return new BlacklistNodeEvaluator(blacklistNodes);
}

// The evaluators from pruneWhereEndNodeIs and includeWhereEndNodeIs interfere with each other, this makes them play nice
private static class EndAndTerminatorNodeEvaluator implements Evaluator {
private Evaluator endNodeEvaluator;
private Evaluator terminatorNodeEvaluator;

public EndAndTerminatorNodeEvaluator(Evaluator endNodeEvaluator, Evaluator terminatorNodeEvaluator) {
this.endNodeEvaluator = endNodeEvaluator;
this.terminatorNodeEvaluator = terminatorNodeEvaluator;
}

@Override
public Evaluation evaluate(Path path) {
// at least one has to give a thumbs up to include
boolean includes = evalIncludes(endNodeEvaluator, path) || evalIncludes(terminatorNodeEvaluator, path);
// prune = terminatorNodeEvaluator != null && !terminatorNodeEvaluator.evaluate(path).continues()
// negate this to get continues result
boolean continues = terminatorNodeEvaluator == null || terminatorNodeEvaluator.evaluate(path).continues();

return Evaluation.of(includes, continues);
}

private boolean evalIncludes(Evaluator eval, Path path) {
return eval != null && eval.evaluate(path).includes();
}
}

private static class BlacklistNodeEvaluator implements Evaluator {
private Set<Node> blacklistSet;

public BlacklistNodeEvaluator(List<Node> blacklistNodes) {
blacklistSet = new HashSet<>(blacklistNodes);
}

@Override
public Evaluation evaluate(Path path) {
return blacklistSet.contains(path.endNode()) ? Evaluation.EXCLUDE_AND_PRUNE : Evaluation.INCLUDE_AND_CONTINUE;
}
}

private static class WhitelistNodeEvaluator implements Evaluator {
private Set<Node> whitelistSet;

public WhitelistNodeEvaluator(List<Node> whitelistNodes) {
whitelistSet = new HashSet<>(whitelistNodes);
}

@Override
public Evaluation evaluate(Path path) {
return whitelistSet.contains(path.endNode()) ? Evaluation.INCLUDE_AND_CONTINUE : Evaluation.EXCLUDE_AND_PRUNE;
}
}
}
Loading

0 comments on commit 4c5a85a

Please sign in to comment.