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 Show documentation
Show all versions of apoc Show documentation
A collection of useful 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.RelationshipResult;
import apoc.result.VirtualNode;
import apoc.result.VirtualPathResult;
import apoc.util.Util;
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.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.internal.helpers.collection.Iterables;
import org.neo4j.internal.helpers.collection.Pair;
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.RelationshipGroupCursor;
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 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;
import static org.neo4j.internal.kernel.api.security.AccessMode.Static.READ;
public class Nodes {
@Context
public GraphDatabaseService db;
@Context
public Transaction tx;
@Context
public KernelTransaction ktx;
@Context
public Pools pools;
@Procedure(mode = Mode.WRITE)
@Description("apoc.nodes.link([nodes],'REL_TYPE') - creates a linked list of nodes from first to last")
public void link(@Name("nodes") List nodes, @Name("type") String type) {
Iterator it = nodes.iterator();
if (it.hasNext()) {
RelationshipType relType = RelationshipType.withName(type);
Node node = it.next();
while (it.hasNext()) {
Node next = it.next();
node.createRelationshipTo(next, relType);
node = next;
}
}
}
@Procedure
@Description("apoc.nodes.get(node|nodes|id|[ids]) - quickly returns all nodes with these ids")
public Stream get(@Name("nodes") Object ids) {
return Util.nodeStream(tx, ids).map(NodeResult::new);
}
@Procedure(mode = Mode.WRITE)
@Description("apoc.nodes.delete(node|nodes|id|[ids]) - quickly delete all nodes with these 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(api,() -> batch.stream().peek( n -> {n.getRelationships().forEach(Relationship::delete);n.delete();}).count());
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
@Description("apoc.get.rels(rel|id|[ids]) - quickly returns all relationships with these ids")
public Stream rels(@Name("relationships") Object ids) {
return Util.relsStream(tx, ids).map(RelationshipResult::new);
}
@UserFunction("apoc.node.relationship.exists")
@Description("apoc.node.relationship.exists(node, rel-direction-pattern) - returns true when the node has the relationships of the pattern")
public boolean hasRelationship(@Name("node") Node node, @Name(value = "types", defaultValue = "") String types) {
if (types == null || types.isEmpty()) return node.hasRelationship();
long id = node.getId();
try ( NodeCursor nodeCursor = ktx.cursors().allocateNodeCursor()) {
ktx.dataRead().singleNode(id, nodeCursor);
nodeCursor.next();
TokenRead tokenRead = ktx.tokenRead();
for (Pair pair : parse(types)) {
int typeId = tokenRead.relationshipType(pair.first().name());
Direction direction = pair.other();
int count;
switch (direction) {
case INCOMING:
count = org.neo4j.internal.kernel.api.helpers.Nodes.countIncoming(nodeCursor, ktx.cursors(), typeId);
break;
case OUTGOING:
count = org.neo4j.internal.kernel.api.helpers.Nodes.countOutgoing(nodeCursor, ktx.cursors(), typeId);
break;
case BOTH:
count = org.neo4j.internal.kernel.api.helpers.Nodes.countAll(nodeCursor, ktx.cursors(), typeId);
break;
default:
throw new UnsupportedOperationException("invalid direction " + direction);
}
if (count > 0) {
return true;
}
}
}
return false;
}
@UserFunction("apoc.nodes.connected")
@Description("apoc.nodes.connected(start, end, rel-direction-pattern) - returns true when the node is connected to the other node, optimized for dense nodes")
public boolean connected(@Name("start") Node start, @Name("start") 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();
NodeCursor endNodeCursor = cursors.allocateNodeCursor()) {
dataRead.singleNode(startId, startNodeCursor);
if (!startNodeCursor.next()) {
throw new IllegalArgumentException("node with id " + startId + " does not exist.");
}
boolean startDense = startNodeCursor.isDense();
dataRead.singleNode(endId, endNodeCursor);
if (!endNodeCursor.next()) {
throw new IllegalArgumentException("node with id " + endId + " does not exist.");
}
boolean endDense = endNodeCursor.isDense();
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
@Description("apoc.nodes.collapse([nodes...],[{properties:'overwrite' or 'discard' or 'combine'}]) yield from, rel, to merge nodes onto first in list")
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()) {
start.allRelationships(relationship);
while (relationship.next()) {
if (relationship.neighbourNodeReference() ==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.first().name());
if (type == -1) continue;
if (pair.other() != Direction.INCOMING) {
result[outIdx][from++]= type;
}
if (pair.other() != 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;
}
static class Degree implements Comparable {
public final long node;
private final long group;
public final int degree;
public final long other;
public Degree(long node, long group, int degree, long other) {
this.node = node;
this.group = group;
this.degree = degree;
this.other = other;
}
@Override
public int compareTo(Degree o) {
return Integer.compare(degree, o.degree);
}
public boolean isConnected(Read read, RelationshipTraversalCursor relationship) {
read.relationships(node, group, relationship);
while (relationship.next()) {
if (relationship.neighbourNodeReference()==other) {
return true;
}
}
return false;
}
}
private boolean connectedDense(NodeCursor start, NodeCursor end, int[][] typedDirections) {
List degrees = new ArrayList<>(32);
Read read = ktx.dataRead();
// TODO: for 4.1 RelationshipGroupCursor, Read#relationshipGroups and Read#relationships will be removed
// Instead, relationships can be selected by type/direction criteria using the NodeCursor#relationships(RelationshipTraversalCursor,RelationshipSelection) method and degrees using NodeCursor#degrees/#degree
try (RelationshipGroupCursor relationshipGroup = ktx.cursors().allocateRelationshipGroupCursor()) {
addDegreesForNode(read, start, end, degrees, relationshipGroup, typedDirections);
addDegreesForNode(read, end, start, degrees, relationshipGroup, typedDirections);
}
Collections.sort(degrees);
try (RelationshipTraversalCursor relationship = ktx.cursors().allocateRelationshipTraversalCursor()) {
for (Degree degree : degrees) {
if (degree.isConnected(ktx.dataRead(), relationship)) return true;
}
return false;
}
}
private void addDegreesForNode(Read dataRead, NodeCursor node, NodeCursor other, List degrees, RelationshipGroupCursor relationshipGroup, int[][] typedDirections) {
long nodeId = node.nodeReference();
long otherId = other.nodeReference();
dataRead.relationshipGroups(nodeId, node.relationshipGroupReference(), relationshipGroup);
while (relationshipGroup.next()) {
int type = relationshipGroup.type();
if ((typedDirections==null) || (arrayContains(typedDirections[0], type))) {
addDegreeWithDirection(degrees, relationshipGroup.outgoingReference(), relationshipGroup.outgoingCount(), nodeId, otherId);
}
if ((typedDirections==null) || (arrayContains(typedDirections[1], type))) {
addDegreeWithDirection(degrees, relationshipGroup.incomingReference(), relationshipGroup.incomingCount(), nodeId, otherId);
}
}
}
private void addDegreeWithDirection(List degrees, long relationshipGroup, int degree, long nodeId, long otherId) {
if (degree > 0 ) {
degrees.add(new Degree(nodeId, relationshipGroup, degree, otherId));
}
}
@UserFunction("apoc.node.labels")
@Description("returns labels for (virtual) nodes")
public List labels(@Name("node") Node node) {
if (node == null) return null;
Iterator