package org.neo4j.graphalgo.shortestpath; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.neo4j.commons.iterator.CollectionWrapper; import org.neo4j.commons.iterator.IteratorUtil; import org.neo4j.commons.iterator.NestingIterator; import org.neo4j.commons.iterator.PrefetchingIterator; import org.neo4j.graphalgo.PathImpl; import org.neo4j.graphalgo.PathImpl.Builder; import org.neo4j.graphdb.Direction; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Path; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.RelationshipExpander; import org.neo4j.graphdb.Transaction; /** * Find (all or one) simple shortest path(s) between two nodes. It starts * from both ends and goes one relationship at the time, alternating side * between each traversal. It does so to minimize the traversal overhead * if one side has a very large amount of relationships, but the other one * very few. It performs well however the graph is proportioned. * * Relationships are traversed in the specified directions from the start node, * but in the reverse direction ( {@link Direction#reverse()} ) from the * end node. This doesn't affect {@link Direction#BOTH}. */ public class ShortestPathsFinder implements PathFinder { private final GraphDatabaseService graphDb; private final int maxDepth; private final RelationshipExpander relExpander; public ShortestPathsFinder( GraphDatabaseService graphDb, int maxDepth, RelationshipExpander relExpander ) { this.graphDb = graphDb; this.maxDepth = maxDepth; this.relExpander = relExpander; } public Collection<Path> findPaths( Node start, Node end ) { return internalPaths( start, end, false ); } public Path findSinglePath( Node start, Node end ) { Collection<Path> paths = internalPaths( start, end, true ); return IteratorUtil.singleValueOrNull( paths.iterator() ); } public Collection<Path> findPathsFromScetch( Node... someNodesAlongTheWay ) { return internalPathsFromScetch( false, someNodesAlongTheWay ); } public Path findPathFromScetch( Node... someNodesAlongTheWay ) { Collection<Path> paths = internalPathsFromScetch( true, someNodesAlongTheWay ); return IteratorUtil.singleValueOrNull( paths.iterator() ); } private Collection<Path> internalPathsFromScetch( boolean stopAsap, Node... someNodesAlongTheWay ) { List<PathThread> threads = new ArrayList<PathThread>(); for ( Node[] startAndEnd : splitNodesIntoPairs( someNodesAlongTheWay ) ) { PathThread thread = new PathThread( startAndEnd[0], startAndEnd[1], stopAsap ); threads.add( thread ); thread.start(); } boolean allFound = true; for ( PathThread thread : threads ) { try { thread.join(); if ( thread.result == null || thread.result.isEmpty() ) { allFound = false; } } catch ( InterruptedException e ) { Thread.interrupted(); // TODO } } if ( !allFound ) { return Collections.emptyList(); } Collection<Path> paths = null; for ( PathThread thread : threads ) { paths = appendPaths( paths, thread.result ); } return paths; } private Collection<Path> appendPaths( Collection<Path> paths, Collection<Path> step ) { if ( paths == null ) { return step; } Collection<Path> result = new ArrayList<Path>(); for ( Path startPath : paths ) { for ( Path pathStep : step ) { result.add( merge( startPath, pathStep ) ); } } return result; } private Path merge( Path start, Path end ) { PathImpl.Builder builder = new PathImpl.Builder( start.getStartNode() ); for ( Relationship rel : start.relationships() ) { builder = builder.push( rel ); } for ( Relationship rel : end.relationships() ) { builder = builder.push( rel ); } return builder.build(); } private Collection<Node[]> splitNodesIntoPairs( Node[] someNodesAlongTheWay ) { Node start = someNodesAlongTheWay[0]; Node end = someNodesAlongTheWay[1]; Collection<Node[]> result = new ArrayList<Node[]>(); for ( int i = 2; end != null; i++ ) { result.add( new Node[] { start, end } ); start = end; end = i < someNodesAlongTheWay.length ? someNodesAlongTheWay[i] : null; } return result; } private Collection<Path> internalPaths( Node start, Node end, boolean stopAsap ) { if ( start.equals( end ) ) { return Arrays.asList( PathImpl.singular( start ) ); } Map<Integer, Collection<Hit>> hits = new HashMap<Integer, Collection<Hit>>(); Collection<Long> sharedVisitedRels = new HashSet<Long>(); ValueHolder<Integer> sharedFrozenDepth = new ValueHolder<Integer>( null ); ValueHolder<Boolean> sharedStop = new ValueHolder<Boolean>( false ); ValueHolder<Integer> sharedCurrentDepth = new ValueHolder<Integer>( 0 ); final DirectionData startData = new DirectionData( start, sharedVisitedRels, sharedFrozenDepth, sharedStop, sharedCurrentDepth, stopAsap, false ); final DirectionData endData = new DirectionData( end, sharedVisitedRels, sharedFrozenDepth, sharedStop, sharedCurrentDepth, stopAsap, true ); while ( startData.hasNext() || endData.hasNext() ) { goOneStep( startData, endData, hits, stopAsap, startData ); goOneStep( endData, startData, hits, stopAsap, startData ); } return least( hits, start, end ); } private Collection<Path> least( Map<Integer, Collection<Hit>> hits, Node start, Node end ) { if ( hits.size() == 0 ) { return Collections.emptyList(); } // FIXME eehhh... loop through from zero, are you kiddin' me? for ( int i = 0; true; i++ ) { Collection<Hit> depthHits = hits.get( i ); if ( depthHits != null ) { return hitsToPaths( depthHits, start, end ); } } } private Collection<Path> hitsToPaths( Collection<Hit> depthHits, Node start, Node end ) { Collection<Path> paths = new ArrayList<Path>(); for ( Hit hit : depthHits ) { Collection<LinkedList<Relationship>> startPaths = getPaths( hit, hit.start ); Collection<LinkedList<Relationship>> endPaths = getPaths( hit, hit.end ); for ( LinkedList<Relationship> startPath : startPaths ) { PathImpl.Builder startBuilder = toBuilder( start, startPath ); for ( LinkedList<Relationship> endPath : endPaths ) { PathImpl.Builder endBuilder = toBuilder( end, endPath ); Path path = startBuilder.build( endBuilder ); paths.add( path ); } } } return paths; } private static class PathData { private final LinkedList<Relationship> rels; private final Node node; PathData( Node node, LinkedList<Relationship> rels ) { this.rels = rels; this.node = node; } @Override public String toString() { return node + ":" + rels; } } private Collection<LinkedList<Relationship>> getPaths( Hit hit, DirectionData data ) { LevelData levelData = data.visitedNodes.get( hit.connectingNode ); if ( levelData.depth == 0 ) { Collection<LinkedList<Relationship>> result = new ArrayList<LinkedList<Relationship>>(); result.add( new LinkedList<Relationship>() ); return result; } Collection<PathData> set = new ArrayList<PathData>(); for ( Long rel : levelData.relsToHere ) { set.add( new PathData( hit.connectingNode, new LinkedList<Relationship>( Arrays.asList( graphDb.getRelationshipById( rel ) ) ) ) ); } for ( int i = 0; i < levelData.depth - 1; i++ ) { // One level Collection<PathData> nextSet = new ArrayList<PathData>(); for ( PathData entry : set ) { // One path... int counter = 0; Node otherNode = entry.rels.getFirst().getOtherNode( entry.node ); LevelData otherLevelData = data.visitedNodes.get( otherNode ); for ( Long rel : otherLevelData.relsToHere ) { // ...may split into several paths LinkedList<Relationship> rels = counter++ == 0 ? entry.rels : new LinkedList<Relationship>( entry.rels ); rels.addFirst( graphDb.getRelationshipById( rel ) ); nextSet.add( new PathData( otherNode, rels ) ); } } set = nextSet; } return new CollectionWrapper<LinkedList<Relationship>, PathData>( set ) { @Override protected PathData objectToUnderlyingObject( LinkedList<Relationship> list ) { throw new UnsupportedOperationException(); } @Override protected LinkedList<Relationship> underlyingObjectToObject( PathData object ) { return object.rels; } }; } private Builder toBuilder( Node startNode, LinkedList<Relationship> rels ) { PathImpl.Builder builder = new PathImpl.Builder( startNode ); for ( Relationship rel : rels ) { builder = builder.push( rel ); } return builder; } // Few long-lived instances private static class Hit { private final DirectionData start; private final DirectionData end; private final Node connectingNode; Hit( DirectionData start, DirectionData end, Node connectingNode ) { this.start = start; this.end = end; this.connectingNode = connectingNode; } @Override public int hashCode() { return connectingNode.hashCode(); } @Override public boolean equals( Object obj ) { Hit o = (Hit) obj; return connectingNode.equals( o.connectingNode ); } } private void goOneStep( DirectionData directionData, DirectionData otherSide, Map<Integer, Collection<Hit>> hits, boolean stopAsEarlyAsPossible, DirectionData startSide ) { if ( !directionData.hasNext() ) { return; } Node nextNode = directionData.next(); LevelData otherSideHit = otherSide.visitedNodes.get( nextNode ); if ( otherSideHit != null ) { // This is a hit int depth = directionData.currentDepth + otherSideHit.depth; if ( directionData.sharedFrozenDepth.value == null ) { directionData.sharedFrozenDepth.value = depth; } if ( depth <= directionData.sharedFrozenDepth.value ) { directionData.haveFoundSomething = true; if ( depth < directionData.sharedFrozenDepth.value ) { directionData.sharedFrozenDepth.value = depth; // TODO Is it really ok to just stop the other side here? // I'm basing that decision on that it was the other side // which found the deeper paths (correct assumption?) otherSide.stop = true; if ( stopAsEarlyAsPossible ) { // we can stop here because we won't get a less deep path than this. directionData.sharedStop.value = true; } } // Add it to the list of hits Collection<Hit> depthHits = hits.get( depth ); if ( depthHits == null ) { depthHits = new HashSet<Hit>(); hits.put( depth, depthHits ); } DirectionData startSideData = directionData == startSide ? directionData : otherSide; DirectionData endSideData = directionData == startSide ? otherSide : directionData; depthHits.add( new Hit( startSideData, endSideData, nextNode ) ); } } } // Two long-lived instances protected class DirectionData extends PrefetchingIterator<Node> { private int currentDepth; private Iterator<Relationship> nextRelationships; private final Collection<Node> nextNodes = new ArrayList<Node>(); private Map<Node, LevelData> visitedNodes = new HashMap<Node, LevelData>(); private Node lastParentTraverserNode; private final ValueHolder<Integer> sharedFrozenDepth; private final ValueHolder<Boolean> sharedStop; private final ValueHolder<Integer> sharedCurrentDepth; private boolean haveFoundSomething; private boolean stop; private final boolean stopAsap; private final RelationshipExpander expander; DirectionData( Node startNode, Collection<Long> sharedVisitedRels, ValueHolder<Integer> sharedFrozenDepth, ValueHolder<Boolean> sharedStop, ValueHolder<Integer> sharedCurrentDepth, boolean stopAsap, boolean reversed ) { this.visitedNodes.put( startNode, new LevelData( null, 0 ) ); this.nextNodes.add( startNode ); this.sharedFrozenDepth = sharedFrozenDepth; this.sharedStop = sharedStop; this.sharedCurrentDepth = sharedCurrentDepth; this.stopAsap = stopAsap; this.expander = reversed ? relExpander.reversed() : relExpander; prepareNextLevel(); } private void prepareNextLevel() { Collection<Node> nodesToIterate = new ArrayList<Node>( filterNextLevelNodes( this.nextNodes ) ); this.nextNodes.clear(); this.nextRelationships = new NestingIterator<Relationship, Node>( nodesToIterate.iterator() ) { @Override protected Iterator<Relationship> createNestedIterator( Node node ) { lastParentTraverserNode = node; return expander.expand( node ).iterator(); } }; this.currentDepth++; this.sharedCurrentDepth.value++; } @Override protected Node fetchNextOrNull() { while ( true ) { Relationship nextRel = fetchNextRelOrNull(); if ( nextRel == null ) { return null; } Node result = nextRel.getOtherNode( this.lastParentTraverserNode ); LevelData levelData = this.visitedNodes.get( result ); boolean createdLevelData = false; if ( levelData == null ) { levelData = new LevelData( nextRel, this.currentDepth ); this.visitedNodes.put( result, levelData ); createdLevelData = true; } if ( this.currentDepth < levelData.depth ) { throw new RuntimeException( "This shouldn't happen... I think" ); } else if ( !this.stopAsap && this.currentDepth == levelData.depth && !createdLevelData ) { levelData.addRel( nextRel ); } // Have we visited this node before? In that case don't add it // as next node to traverse if ( !createdLevelData ) { continue; } this.nextNodes.add( result ); return result; } } private boolean canGoDeeper() { return this.sharedFrozenDepth.value == null && this.sharedCurrentDepth.value < maxDepth; } private Relationship fetchNextRelOrNull() { boolean stopped = this.stop || this.sharedStop.value; boolean hasComeTooFarEmptyHanded = this.sharedFrozenDepth.value != null && this.sharedCurrentDepth.value > this.sharedFrozenDepth.value && !this.haveFoundSomething; if ( stopped || hasComeTooFarEmptyHanded ) { return null; } if ( !this.nextRelationships.hasNext() ) { if ( canGoDeeper() ) { prepareNextLevel(); } } return this.nextRelationships.hasNext() ? this.nextRelationships.next() : null; } } protected Collection<Node> filterNextLevelNodes( Collection<Node> nextNodes ) { return nextNodes; } // Few long-lived instances private class ValueHolder<T> { private T value; ValueHolder( T initialValue ) { this.value = initialValue; } } // Many long-lived instances private class LevelData { private Long[] relsToHere; private int depth; LevelData( Relationship relToHere, int depth ) { if ( relToHere != null ) { addRel( relToHere ); } this.depth = depth; } void addRel( Relationship rel ) { Long[] newRels = null; if ( relsToHere == null ) { newRels = new Long[1]; } else { newRels = new Long[relsToHere.length+1]; System.arraycopy( relsToHere, 0, newRels, 0, relsToHere.length ); } newRels[newRels.length-1] = rel.getId(); relsToHere = newRels; } } // Few long-lived instances private class PathThread extends Thread { private final Node start; private final Node end; private final boolean stopAsap; private Collection<Path> result; PathThread( Node start, Node end, boolean stopAsap ) { this.start = start; this.end = end; this.stopAsap = stopAsap; } @Override public void run() { Transaction tx = graphDb.beginTx(); try { result = internalPaths( start, end, stopAsap ); tx.success(); } finally { tx.finish(); } } } }