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
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package apoc.nodes;
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;
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.Util;
import apoc.util.collection.Iterables;
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.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
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.kernel.impl.coreapi.InternalTransaction;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Mode;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.NotThreadSafe;
import org.neo4j.procedure.Procedure;
import org.neo4j.procedure.UserFunction;
import org.neo4j.storageengine.api.RelationshipSelection;
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 `LIST`.\n"
+ "This procedure can be limited on `RELATIONSHIP` values 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.getStartNode().getElementId(), (key) -> new ArrayList<>());
if (nodeDups.contains(relationship.getEndNode().getElementId())) {
return false;
}
nodeDups.add(relationship.getEndNode().getElementId());
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 `NODE` values 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 `NODE` values with the given ids.")
public Stream get(@Name("nodes") Object ids) {
return Util.nodeStream((InternalTransaction) tx, ids).map(NodeResult::new);
}
@Procedure(name = "apoc.nodes.delete", mode = Mode.WRITE)
@Description("Deletes all `NODE` values with the given ids.")
public Stream delete(@Name("nodes") Object ids, @Name("batchSize") long batchSize) {
Iterator it = Util.nodeStream((InternalTransaction) 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 `RELATIONSHIP` values with the given ids.")
public Stream rels(@Name("rels") Object ids) {
return Util.relsStream((InternalTransaction) tx, ids).map(RelationshipResult::new);
}
@UserFunction("apoc.node.relationship.exists")
@Description(
"Returns a `BOOLEAN` based on whether the given `NODE` has a connecting `RELATIONSHIP` (or whether the given `NODE` has a connecting `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 = ((InternalTransaction) tx).elementIdMapper().nodeId(node.getElementId());
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 -> org.neo4j.internal.kernel.api.helpers.Nodes.countIncoming(
nodeCursor, typeId);
case OUTGOING -> org.neo4j.internal.kernel.api.helpers.Nodes.countOutgoing(
nodeCursor, typeId);
case BOTH -> org.neo4j.internal.kernel.api.helpers.Nodes.countAll(nodeCursor, typeId);
};
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 = ((InternalTransaction) tx).elementIdMapper().nodeId(start.getElementId());
long endId = ((InternalTransaction) tx).elementIdMapper().nodeId(end.getElementId());
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.");
}
dataRead.singleNode(endId, endNodeCursor);
if (!endNodeCursor.next()) {
throw new IllegalArgumentException("node with id " + endId + " does not exist.");
}
return connected(startNodeCursor, endId, typedDirections(tokenRead, pairs));
}
}
@Procedure("apoc.nodes.collapse")
@Description(
"Merges `NODE` values together in the given `LIST`.\n"
+ "The `NODE` values are then combined to become one `NODE`, with all labels of the previous `NODE` values attached to it, and all `RELATIONSHIP` values 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 (Objects.equals(startNode.getElementId(), node.getElementId())) {
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 j : array) {
if (j == element) {
return true;
}
}
return false;
}
/**
* @param ops
* @param pairs
* @return a int[][] where the first index is 0 for outgoing, 1 for incoming. second array contains rel type ids
*/
private int[][] typedDirections(TokenRead ops, List> pairs) {
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);
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