apoc.nodes.Nodes Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of apoc-core Show documentation
Show all versions of apoc-core Show documentation
Core package for Neo4j Procedures
package apoc.nodes;
import apoc.Pools;
import apoc.create.Create;
import apoc.refactor.util.PropertiesManager;
import apoc.refactor.util.RefactorConfig;
import apoc.result.LongResult;
import apoc.result.NodeResult;
import apoc.result.PathResult;
import apoc.result.RelationshipResult;
import apoc.result.VirtualNode;
import apoc.result.VirtualPath;
import apoc.result.VirtualPathResult;
import apoc.util.collection.Iterables;
import apoc.util.Util;
import org.apache.commons.lang3.tuple.Pair;
import org.neo4j.graphalgo.BasicEvaluationContext;
import org.neo4j.graphalgo.GraphAlgoFactory;
import org.neo4j.graphalgo.PathFinder;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Entity;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.PathExpander;
import org.neo4j.graphdb.PathExpanderBuilder;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.internal.kernel.api.CursorFactory;
import org.neo4j.internal.kernel.api.NodeCursor;
import org.neo4j.internal.kernel.api.Read;
import org.neo4j.internal.kernel.api.RelationshipTraversalCursor;
import org.neo4j.internal.kernel.api.TokenRead;
import org.neo4j.kernel.api.KernelTransaction;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Mode;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;
import org.neo4j.procedure.UserFunction;
import org.neo4j.storageengine.api.RelationshipSelection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static apoc.path.RelationshipTypeAndDirections.format;
import static apoc.path.RelationshipTypeAndDirections.parse;
import static apoc.refactor.util.RefactorUtil.copyProperties;
import static apoc.util.Util.map;
public class Nodes {
@Context
public GraphDatabaseService db;
@Context
public Transaction tx;
@Context
public KernelTransaction ktx;
@Context
public Pools pools;
@Procedure("apoc.nodes.cycles")
@Description("Detects all path cycles in the given node list.\n" +
"This procedure can be limited on relationships as well.")
public Stream cycles(@Name("nodes") List nodes, @Name(value = "config",defaultValue = "{}") Map config) {
NodesConfig conf = new NodesConfig(config);
final List types = conf.getRelTypes();
Stream paths = nodes.stream().flatMap(start -> {
boolean allRels = types.isEmpty();
final RelationshipType[] relTypes = types.stream().map(RelationshipType::withName).toArray(RelationshipType[]::new);
final Iterable relationships = allRels
? start.getRelationships(Direction.OUTGOING)
: start.getRelationships(Direction.OUTGOING, relTypes);
PathExpanderBuilder expanderBuilder;
if (allRels) {
expanderBuilder = PathExpanderBuilder.allTypes(Direction.OUTGOING);
} else {
expanderBuilder = PathExpanderBuilder.empty();
for (RelationshipType relType: relTypes) {
expanderBuilder = expanderBuilder.add(relType, Direction.OUTGOING);
}
}
final PathExpander pathExpander = expanderBuilder.build();
PathFinder finder = GraphAlgoFactory.shortestPath(
new BasicEvaluationContext(tx, db),
pathExpander,
conf.getMaxDepth());
Map> dups = new HashMap<>();
return Iterables.stream(relationships)
// to prevent duplicated (start and end nodes with double-rels)
.filter(relationship -> {
final List nodeDups = dups.computeIfAbsent(relationship.getStartNodeId(), (key) -> new ArrayList<>());
if (nodeDups.contains(relationship.getEndNodeId())) {
return false;
}
nodeDups.add(relationship.getEndNodeId());
return true;
})
.flatMap(relationship -> {
final Path path = finder.findSinglePath(relationship.getEndNode(), start);
if (path == null) return Stream.empty();
VirtualPath virtualPath = new VirtualPath(start);
virtualPath.addRel(relationship);
for (Relationship relPath : path.relationships()) {
virtualPath.addRel(relPath);
}
return Stream.of(virtualPath);
});
});
return paths.map(PathResult::new);
}
@Procedure(name = "apoc.nodes.link", mode = Mode.WRITE)
@Description("Creates a linked list of the given nodes connected by the given relationship type.")
public void link(@Name("nodes") List nodes, @Name("type") String type, @Name(value = "config",defaultValue = "{}") Map config) {
RefactorConfig conf = new RefactorConfig(config);
Iterator it = nodes.iterator();
if (it.hasNext()) {
RelationshipType relType = RelationshipType.withName(type);
Node node = it.next();
while (it.hasNext()) {
Node next = it.next();
final boolean createRelationship = !conf.isAvoidDuplicates() || (conf.isAvoidDuplicates() && !connected(node, next, type));
if (createRelationship) {
node.createRelationshipTo(next, relType);
}
node = next;
}
}
}
@Procedure("apoc.nodes.get")
@Description("Returns all nodes with the given ids.")
public Stream get(@Name("nodes") Object ids) {
return Util.nodeStream(tx, ids).map(NodeResult::new);
}
@Procedure(name = "apoc.nodes.delete", mode = Mode.WRITE)
@Description("Deletes all nodes with the given ids.")
public Stream delete(@Name("nodes") Object ids, @Name("batchSize") long batchSize) {
Iterator it = Util.nodeStream(tx, ids).iterator();
long count = 0;
while (it.hasNext()) {
final List batch = Util.take(it, (int)batchSize);
count += Util.inTx(db, pools, (txInThread) -> {txInThread.execute("FOREACH (n in $nodes | DETACH DELETE n)",map("nodes",batch)).close();return batch.size();});
}
return Stream.of(new LongResult(count));
}
@Procedure("apoc.nodes.rels")
@Description("Returns all relationships with the given ids.")
public Stream rels(@Name("rels") Object ids) {
return Util.relsStream(tx, ids).map(RelationshipResult::new);
}
@UserFunction("apoc.node.relationship.exists")
@Description("Returns a boolean based on whether the given node has a relationship (or whether the given node has a relationship of the given type and direction).")
public boolean hasRelationship(@Name("node") Node node, @Name(value = "relTypes", defaultValue = "") String types) {
if (types == null || types.isEmpty()) return node.hasRelationship();
long id = node.getId();
try ( NodeCursor nodeCursor = ktx.cursors().allocateNodeCursor(ktx.cursorContext())) {
ktx.dataRead().singleNode(id, nodeCursor);
nodeCursor.next();
TokenRead tokenRead = ktx.tokenRead();
for (Pair pair : parse(types)) {
int typeId = tokenRead.relationshipType(pair.getLeft().name());
Direction direction = pair.getRight();
int count;
switch (direction) {
case INCOMING:
count = org.neo4j.internal.kernel.api.helpers.Nodes.countIncoming(nodeCursor, typeId);
break;
case OUTGOING:
count = org.neo4j.internal.kernel.api.helpers.Nodes.countOutgoing(nodeCursor, typeId);
break;
case BOTH:
count = org.neo4j.internal.kernel.api.helpers.Nodes.countAll(nodeCursor, typeId);
break;
default:
throw new UnsupportedOperationException("invalid direction " + direction);
}
if (count > 0) {
return true;
}
}
}
return false;
}
@UserFunction("apoc.nodes.connected")
@Description("Returns true when a given node is directly connected to another given node.\n" +
"This function is optimized for dense nodes.")
public boolean connected(@Name("startNode") Node start, @Name("endNode") Node end, @Name(value = "types", defaultValue = "") String types) {
if (start == null || end == null) return false;
if (start.equals(end)) return true;
long startId = start.getId();
long endId = end.getId();
List> pairs = (types == null || types.isEmpty()) ? null : parse(types);
Read dataRead = ktx.dataRead();
TokenRead tokenRead = ktx.tokenRead();
CursorFactory cursors = ktx.cursors();
try (NodeCursor startNodeCursor = cursors.allocateNodeCursor(ktx.cursorContext());
NodeCursor endNodeCursor = cursors.allocateNodeCursor(ktx.cursorContext())) {
dataRead.singleNode(startId, startNodeCursor);
if (!startNodeCursor.next()) {
throw new IllegalArgumentException("node with id " + startId + " does not exist.");
}
// boolean startDense = startNodeCursor.supportsFastDegreeLookup();
dataRead.singleNode(endId, endNodeCursor);
if (!endNodeCursor.next()) {
throw new IllegalArgumentException("node with id " + endId + " does not exist.");
}
// boolean endDense = endNodeCursor.supportsFastDegreeLookup();
return connected(startNodeCursor, endId, typedDirections(tokenRead, pairs, true));
// if (!startDense) return connected(startNodeCursor, endId, typedDirections(tokenRead, pairs, true));
// if (!endDense) return connected(endNodeCursor, startId, typedDirections(tokenRead, pairs, false));
// return connectedDense(startNodeCursor, endNodeCursor, typedDirections(tokenRead, pairs, true));
}
}
@Procedure("apoc.nodes.collapse")
@Description("Merges nodes together in the given list.\n" +
"The nodes are then combined to become one node, with all labels of the previous nodes attached to it, and all relationships pointing to it.")
public Stream collapse(@Name("nodes") List nodes, @Name(value = "config", defaultValue = "{}") Map config) {
if (nodes == null || nodes.isEmpty()) return Stream.empty();
if (nodes.size() == 1) return Stream.of(new VirtualPathResult(nodes.get(0), null, null));
Set nodeSet = new LinkedHashSet<>(nodes);
RefactorConfig conf = new RefactorConfig(config);
VirtualNode first = createVirtualNode(nodeSet, conf);
if (first.getRelationships().iterator().hasNext()) {
return StreamSupport.stream(first.getRelationships().spliterator(), false)
.map(relationship -> new VirtualPathResult(relationship.getStartNode(), relationship, relationship.getEndNode()));
} else {
return Stream.of(new VirtualPathResult(first, null, null));
}
}
private VirtualNode createVirtualNode(Set nodes, RefactorConfig conf) {
Create create = new Create();
Node first = nodes.iterator().next();
List labels = Util.labelStrings(first);
if (conf.isCollapsedLabel()) {
labels.add("Collapsed");
}
VirtualNode virtualNode = (VirtualNode) create.vNodeFunction(labels, first.getAllProperties());
createVirtualRelationships(nodes, virtualNode, first, conf);
nodes.stream().skip(1).forEach(node -> {
virtualNode.addLabels(node.getLabels());
PropertiesManager.mergeProperties(node.getAllProperties(), virtualNode, conf);
createVirtualRelationships(nodes, virtualNode, node, conf);
});
if (conf.isCountMerge()) {
virtualNode.setProperty("count", nodes.size());
}
return virtualNode;
}
private void createVirtualRelationships(Set nodes, VirtualNode virtualNode, Node node, RefactorConfig refactorConfig) {
node.getRelationships().forEach(relationship -> {
Node startNode = relationship.getStartNode();
Node endNode = relationship.getEndNode();
if (nodes.contains(startNode) && nodes.contains(endNode)) {
if (refactorConfig.isSelfRel()) {
createOrMergeVirtualRelationship(virtualNode, refactorConfig, relationship, virtualNode, Direction.OUTGOING);
}
} else {
if (startNode.getId() == node.getId()) {
createOrMergeVirtualRelationship(virtualNode, refactorConfig, relationship, endNode, Direction.OUTGOING);
} else {
createOrMergeVirtualRelationship(virtualNode, refactorConfig, relationship, startNode, Direction.INCOMING);
}
}
});
}
private void createOrMergeVirtualRelationship(VirtualNode virtualNode, RefactorConfig refactorConfig, Relationship source, Node node, Direction direction) {
Iterable rels = virtualNode.getRelationships(direction, source.getType());
Optional first = StreamSupport.stream(rels.spliterator(), false).filter(relationship -> relationship.getOtherNode(virtualNode).equals(node)).findFirst();
if (refactorConfig.isMergeVirtualRels() && first.isPresent()) {
mergeRelationship(source, first.get(), refactorConfig);
} else {
if (direction==Direction.OUTGOING)
copyProperties(source, virtualNode.createRelationshipTo(node, source.getType()));
if (direction==Direction.INCOMING)
copyProperties(source, virtualNode.createRelationshipFrom(node, source.getType()));
}
}
private void mergeRelationship(Relationship source, Relationship target, RefactorConfig refactorConfig) {
if (refactorConfig.isCountMerge()) {
target.setProperty("count", (Integer) target.getProperty("count", 0) + 1);
}
PropertiesManager.mergeProperties(source.getAllProperties(), target, refactorConfig);
}
/**
* TODO: be more efficient, in
* @param start
* @param end
* @param typedDirections
* @return
*/
private boolean connected(NodeCursor start, long end, int[][] typedDirections) {
try (RelationshipTraversalCursor relationship = ktx.cursors().allocateRelationshipTraversalCursor(ktx.cursorContext())) {
start.relationships(relationship, RelationshipSelection.selection(Direction.BOTH));
while (relationship.next()) {
if (relationship.otherNodeReference() ==end) {
if (typedDirections==null) {
return true;
} else {
int direction = relationship.targetNodeReference() == end ? 0 : 1 ;
int[] types = typedDirections[direction];
if (arrayContains(types, relationship.type())) return true;
}
}
}
}
return false;
}
private boolean arrayContains(int[] array, int element) {
for (int i=0; i> pairs, boolean outgoing) {
if (pairs==null) return null;
int from=0;int to=0;
int[][] result = new int[2][pairs.size()];
int outIdx = Direction.OUTGOING.ordinal();
int inIdx = Direction.INCOMING.ordinal();
for (Pair pair : pairs) {
int type = ops.relationshipType(pair.getLeft().name());
if (type == -1) continue;
if (pair.getRight() != Direction.INCOMING) {
result[outIdx][from++]= type;
}
if (pair.getRight() != Direction.OUTGOING) {
result[inIdx][to++]= type;
}
}
result[outIdx] = Arrays.copyOf(result[outIdx], from);
result[inIdx] = Arrays.copyOf(result[inIdx], to);
if (!outgoing) {
int[] tmp = result[outIdx];
result[outIdx] = result[inIdx];
result[inIdx] = tmp;
}
return result;
}
@UserFunction("apoc.node.labels")
@Description("Returns the labels for the given virtual node.")
public List labels(@Name("node") Node node) {
if (node == null) return null;
Iterator