Repository: neo4j-contrib/neo4j-apoc-procedures
Pull request #24 (2016-05-06 09:48:24)

spatial - sort collection of paths by distance
Example : 

```
MATCH (a:City {name:'bruges'}), (b:City {name:'dresden'})
MATCH p=(a)-[*]->(b)
WITH collect(p) as paths
CALL apoc.spatial.sortPathsByDistance(paths) YIELD path, distance
RETURN path, distance
```


Patch:
diff --git a/docs/spatial.adoc b/docs/spatial.adoc
index 2913e432a..94ef354ac 100644
--- a/docs/spatial.adoc
+++ b/docs/spatial.adoc
@@ -1,7 +1,10 @@
 = Spatial Functions
 
 The spatial procedures are intended to enable geographic capabilities on your data.
-Currently there is only one procedure _geocode_ which will convert a textual address
+
+=== geocode
+
+The first procedure _geocode_ which will convert a textual address
 into a location containing _latitude_, _longitude_ and _description_. Despite being
 only a single function, together with the built-in functions _point_ and _distance_
 we can achieve quite powerful results.
@@ -44,6 +47,37 @@ ORDER BY distance
 LIMIT 100
 ----
 
+=== sortPathsByDistance
+
+The second procedure enables you to sort a given collection of paths by the sum of their distance based on lat/long properties
+on the nodes.
+
+Sample data :
+
+[source, cypher]
+----
+CREATE (bruges:City {name:"bruges", latitude: 51.2605829, longitude: 3.0817189})
+CREATE (brussels:City {name:"brussels", latitude: 50.854954, longitude: 4.3051786})
+CREATE (paris:City {name:"paris", latitude: 48.8588376, longitude: 2.2773455})
+CREATE (dresden:City {name:"dresden", latitude: 51.0767496, longitude: 13.6321595})
+MERGE (bruges)-[:NEXT]->(brussels)
+MERGE (brussels)-[:NEXT]->(dresden)
+MERGE (brussels)-[:NEXT]->(paris)
+MERGE (bruges)-[:NEXT]->(paris)
+MERGE (paris)-[:NEXT]->(dresden)
+----
+
+Finding paths and sort them by distance
+
+[source, cypher]
+----
+MATCH (a:City {name:'bruges'}), (b:City {name:'dresden'})
+MATCH p=(a)-[*]->(b)
+WITH collect(p) as paths
+CALL apoc.spatial.sortPathsByDistance(paths) YIELD path, distance
+RETURN path, distance
+----
+
 == Graph Refactoring
 
 In order not to have to repeatedly geocode the same thing in multiple queries, especially
diff --git a/docs/text.adoc b/docs/text.adoc
index 490718ebc..5daa5db59 100644
--- a/docs/text.adoc
+++ b/docs/text.adoc
@@ -35,5 +35,4 @@ CALL apoc.text.compareCleaned('Hello World!', '_hello-world_')
 ----
 UNWIND ['Hello World!', 'hello worlds'] as text
 CALL apoc.text.filterCleanMatches(text, 'hello_world')
-RETURN text
 ----
diff --git a/index.html b/index.html
index f0bd467c0..c01b114a2 100644
--- a/index.html
+++ b/index.html
@@ -487,14 +487,7 @@ body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-b
 </li>
 <li><a href="#_spatial_2">Spatial</a>
 <ul class="sectlevel2">
-<li><a href="#_spatial_functions">Spatial Functions</a>
-<ul class="sectlevel3">
-<li><a href="#_using_geocode_within_a_bigger_cypher_query">Using Geocode within a bigger Cypher query</a></li>
-<li><a href="#_calculating_distance_between_locations">Calculating distance between locations</a></li>
-<li><a href="#_graph_refactoring_2">Graph Refactoring</a></li>
-<li><a href="#_combined_space_and_time_search">Combined Space and Time search</a></li>
-</ul>
-</li>
+<li><a href="#_spatial_functions">Spatial Functions</a></li>
 </ul>
 </li>
 <li><a href="#_data_integration">Data Integration</a>
@@ -630,10 +623,6 @@ RETURN name AS undocumented</code></pre>
 </colgroup>
 <tbody>
 <tr>
-<td class="tableblock halign-left valign-top"><p class="tableblock"><code>apoc.index.addAllNode('index-name',{label1:['prop1',&#8230;&#8203;],&#8230;&#8203;})</code></p></td>
-<td class="tableblock halign-left valign-top"><p class="tableblock">add all nodes to this full text index with the given fields, additionally populates a 'search' index field with all of them in one place</p></td>
-</tr>
-<tr>
 <td class="tableblock halign-left valign-top"><p class="tableblock"><code>apoc.index.addNode(node,['prop1',&#8230;&#8203;])</code></p></td>
 <td class="tableblock halign-left valign-top"><p class="tableblock">add node to an index for each label it has</p></td>
 </tr>
@@ -645,18 +634,6 @@ RETURN name AS undocumented</code></pre>
 <td class="tableblock halign-left valign-top"><p class="tableblock"><code>apoc.index.addRelationship(rel,['prop1',&#8230;&#8203;])</code></p></td>
 <td class="tableblock halign-left valign-top"><p class="tableblock">add relationship to an index for its type</p></td>
 </tr>
-</tbody>
-</table>
-<table class="tableblock frame-all grid-all spread">
-<colgroup>
-<col style="width: 16%;">
-<col style="width: 83%;">
-</colgroup>
-<tbody>
-<tr>
-<td class="tableblock halign-left valign-top"><p class="tableblock"><code>apoc.index.search('name', 'query') YIELD node, weight</code></p></td>
-<td class="tableblock halign-left valign-top"><p class="tableblock">search for nodes in the given full text index matching the given lucene query</p></td>
-</tr>
 <tr>
 <td class="tableblock halign-left valign-top"><p class="tableblock"><code>apoc.index.nodes('Label','prop:value*') YIELD node, weight</code></p></td>
 <td class="tableblock halign-left valign-top"><p class="tableblock">lucene query on node index with the given label name</p></td>
@@ -1248,22 +1225,6 @@ RETURN *;</code></pre>
 <td class="tableblock halign-left valign-top"><p class="tableblock"><code>CALL apoc.text.phoneticDelta(text1, text2) yield phonetic1, phonetic2, delta</code></p></td>
 <td class="tableblock halign-left valign-top"><p class="tableblock">Compute the US_ENGLISH soundex character difference between two given strings</p></td>
 </tr>
-<tr>
-<td class="tableblock halign-left valign-top"><p class="tableblock"><code>CALL apoc.text.join(['text1','text2',&#8230;&#8203;], delimiter) YIELD value</code></p></td>
-<td class="tableblock halign-left valign-top"><p class="tableblock">join the given strings with the given delimiter.</p></td>
-</tr>
-<tr>
-<td class="tableblock halign-left valign-top"><p class="tableblock"><code>CAL apoc.text.clean(text) YIELD value</code></p></td>
-<td class="tableblock halign-left valign-top"><p class="tableblock">strip the given string of everything except alpha numeric characters and convert it to lower case.</p></td>
-</tr>
-<tr>
-<td class="tableblock halign-left valign-top"><p class="tableblock"><code>CALL apoc.text.compareCleaned(text1, text2) YIELD value</code></p></td>
-<td class="tableblock halign-left valign-top"><p class="tableblock">compare the given strings stripped of everything except alpha numeric characters converted to lower case.</p></td>
-</tr>
-<tr>
-<td class="tableblock halign-left valign-top"><p class="tableblock"><code>CALL apoc.text.filterCleanMatches(text1, text2) YIELD value</code></p></td>
-<td class="tableblock halign-left valign-top"><p class="tableblock">filter out non-matches of the given strings stripped of everything except alpha numeric characters converted to lower case.</p></td>
-</tr>
 </tbody>
 </table>
 </div>
@@ -1467,18 +1428,6 @@ RETURN *;</code></pre>
 <td class="tableblock halign-left valign-top"><p class="tableblock"><code>CALL apoc.algo.betweenness(['TYPE',&#8230;&#8203;],nodes,BOTH) YIELD node, centrality</code></p></td>
 <td class="tableblock halign-left valign-top"><p class="tableblock">calculate betweenness centrality for given nodes`</p></td>
 </tr>
-<tr>
-<td class="tableblock halign-left valign-top"><p class="tableblock"><code>CALL apoc.algo.pageRank(nodes) YIELD node, score</code></p></td>
-<td class="tableblock halign-left valign-top"><p class="tableblock">calculates page rank for given nodes with 20 iterations</p></td>
-</tr>
-<tr>
-<td class="tableblock halign-left valign-top"><p class="tableblock"><code>CALL apoc.algo.pageRankWithIterations(iterations, nodes) YIELD node, score</code></p></td>
-<td class="tableblock halign-left valign-top"><p class="tableblock">calculates page rank for given nodes and iterations</p></td>
-</tr>
-<tr>
-<td class="tableblock halign-left valign-top"><p class="tableblock"><code>CALL apoc.algo.community(node,partitionKey,type,direction,weightKey)</code></p></td>
-<td class="tableblock halign-left valign-top"><p class="tableblock">community detection using label propagation</p></td>
-</tr>
 </tbody>
 </table>
 <div class="paragraph">
@@ -2055,131 +2004,7 @@ ORDER BY score DESC</code></pre>
 <div class="sectionbody">
 <div class="sect2">
 <h3 id="_spatial_functions">Spatial Functions</h3>
-<div class="paragraph">
-<p>The spatial procedures are intended to enable geographic capabilities on your data.
-Currently there is only one procedure <em>geocode</em> which will convert a textual address
-into a location containing <em>latitude</em>, <em>longitude</em> and <em>description</em>. Despite being
-only a single function, together with the built-in functions <em>point</em> and <em>distance</em>
-we can achieve quite powerful results.</p>
-</div>
-<div class="paragraph">
-<p>First, how can we use the procedure:</p>
-</div>
-<div class="listingblock">
-<div class="content">
-<pre class="highlight"><code class="language-cypher" data-lang="cypher">CALL apoc.spatial.geocode('21 rue Paul Bellamy 44000 NANTES FRANCE') YIELD location
-RETURN location.latitude, location.longitude // will return 47.2221667, -1.5566624</code></pre>
-</div>
-</div>
-<div class="sect3">
-<h4 id="_using_geocode_within_a_bigger_cypher_query">Using Geocode within a bigger Cypher query</h4>
-<div class="paragraph">
-<p>A more complex, or useful, example which geocodes addresses found in properties of nodes:</p>
-</div>
-<div class="listingblock">
-<div class="content">
-<pre class="highlight"><code class="language-cypher" data-lang="cypher">MATCH (a:Place)
-WHERE exists(a.address)
-CALL apoc.spatial.geocode(a.address) YIELD location
-RETURN location.latitude AS latitude, location.longitude AS longitude, location.description AS description</code></pre>
-</div>
-</div>
-</div>
-<div class="sect3">
-<h4 id="_calculating_distance_between_locations">Calculating distance between locations</h4>
-<div class="paragraph">
-<p>If we wish to calculate the distance between addresses, we need to use the point() function to convert
-latitude and longitude to Cyper Point types, and then use the distance() function to calculate the distance:</p>
-</div>
-<div class="listingblock">
-<div class="content">
-<pre class="highlight"><code class="language-cypher" data-lang="cypher">WITH point({latitude: 48.8582532, longitude: 2.294287}) AS eiffel
-MATCH (a:Place)
-WHERE exists(a.address)
-CALL apoc.spatial.geocode(a.address) YIELD location
-WITH location, distance(point(location), eiffel) AS distance
-WHERE distance &lt; 5000
-RETURN location.description AS description, distance
-ORDER BY distance
-LIMIT 100</code></pre>
-</div>
-</div>
-</div>
-<div class="sect3">
-<h4 id="_graph_refactoring_2">Graph Refactoring</h4>
-<div class="paragraph">
-<p>In order not to have to repeatedly geocode the same thing in multiple queries, especially
-if the database will be used by many people, it might be a good idea to persist the results
-in the database so that subsequent calls can use the saved results.</p>
-</div>
-<div class="paragraph">
-<p>Geocode and persist the result</p>
-</div>
-<div class="listingblock">
-<div class="content">
-<pre class="highlight"><code class="language-cypher" data-lang="cypher">MATCH (a:Place)
-WHERE exists(a.address) AND NOT exists(a.latitude)
-WITH a LIMIT 1000
-CALL apoc.spatial.geocode(a.address) YIELD location
-SET a.latitude = location.latitude
-SET a.longitude = location.longitude</code></pre>
-</div>
-</div>
-<div class="paragraph">
-<p>Note that the above command only geocodes the first 1000 ‘Place’ nodes that have not already been geocoded.
-This query can be run multiple times until all places are geocoded. Why would we want to do this?
-Two good reasons:</p>
-</div>
-<div class="ulist">
-<ul>
-<li>
-<p>The geocoding service is a public service that can throttle or blacklist sites that hit the service too heavily, so controlling how much we do is useful.</p>
-</li>
-<li>
-<p>The transaction is updating the database, and it is wise not to update the database with too many things in the same transaction, to avoid using up too much memory. This trick will keep the memory usage very low.</p>
-</li>
-</ul>
-</div>
-<div class="paragraph">
-<p>Now make use of the results in distance queries</p>
-</div>
-<div class="listingblock">
-<div class="content">
-<pre class="highlight"><code class="language-cypher" data-lang="cypher">WITH point({latitude: 48.8582532, longitude: 2.294287}) AS eiffel
-MATCH (a:Place)
-WHERE exists(a.latitude) AND exists(a.longitude)
-WITH a, distance(point(a), eiffel) AS distance
-WHERE distance &lt; 5000
-RETURN a.name, distance
-ORDER BY distance
-LIMIT 100</code></pre>
-</div>
-</div>
-</div>
-<div class="sect3">
-<h4 id="_combined_space_and_time_search">Combined Space and Time search</h4>
-<div class="paragraph">
-<p>Combining spatial and date-time procedures can allow for more complex queries:</p>
-</div>
-<div class="listingblock">
-<div class="content">
-<pre class="highlight"><code class="language-cypher" data-lang="cypher">CALL apoc.date.parse('2016-06-01 00:00:00','h') YIELD value AS due_date
-WITH due_date,
-point({latitude: 48.8582532, longitude: 2.294287}) AS eiffel
-MATCH (e:Event)
-WHERE exists(e.address) AND exists(e.datetime)
-CALL apoc.spatial.geocode(e.address) YIELD value AS location
-CALL apoc.date.parse(e.datetime,'h') YIELD value AS hours
-WITH e, location,
-distance(point(location), eiffel) AS distance,
-            (due_date - hours)/24.0 AS days_before_due
-WHERE distance &lt; 5000 AND days_before_due &lt; 14 AND hours &lt; due_date
-RETURN e.name AS event, e.datetime AS date,
-location.description AS description, distance
-ORDER BY distance</code></pre>
-</div>
-</div>
-</div>
+
 </div>
 </div>
 </div>
@@ -2404,7 +2229,7 @@ SET u:Processed RETURN count(*)
 </div>
 <div id="footer">
 <div id="footer-text">
-Last updated 2016-05-06 09:01:52 CEST
+Last updated 2016-05-05 18:33:22 CEST
 </div>
 </div>
 </body>
diff --git a/readme.adoc b/readme.adoc
index 88e3d3147..77847bcf1 100644
--- a/readme.adoc
+++ b/readme.adoc
@@ -68,15 +68,9 @@ Procedures to add to and query manual indexes
 
 [cols="1m,5"]
 |===
-| apoc.index.addAllNode('index-name',{label1:['prop1',...],...}) | add all nodes to this full text index with the given fields, additionally populates a 'search' index field with all of them in one place
 | apoc.index.addNode(node,['prop1',...]) | add node to an index for each label it has
 | apoc.index.addNodeByLabel(node,'Label',['prop1',...]) | add node to an index for the given label
 | apoc.index.addRelationship(rel,['prop1',...]) | add relationship to an index for its type
-|===
-
-[cols="1m,5"]
-|===
-| apoc.index.search('name', 'query') YIELD node, weight | search for nodes in the given full text index matching the given lucene query
 | apoc.index.nodes('Label','prop:value*') YIELD node, weight | lucene query on node index with the given label name
 | apoc.index.relationships('TYPE','prop:value*') YIELD rel, weight | lucene query on relationship index with the given type name
 | apoc.index.between(node1,'TYPE',node2,'prop:value*') YIELD rel, weight | lucene query on relationship index with the given type name bound by either or both sides (each node parameter can be null)
@@ -269,6 +263,7 @@ TODO:
 [cols="1m,5"]
 |===
 | CALL apoc.spatial.geocode('address') YIELD location, latitude, longitude, description, osmData | look up geographic location of location from openstreetmap geocoding service
+| CALL apoc.spatial.sortPathsByDistance(Collection<Path>) YIELD path, distance | sort a given collection of paths by geographic distance based on lat/long properties on the path nodes
 |===
 
 === Helpers
@@ -316,10 +311,6 @@ TODO:
 |===
 | CALL apoc.text.phonetic(value) yield value | Compute the US_ENGLISH phonetic soundex encoding of all words of the text value which can be a single string or a list of strings
 | CALL apoc.text.phoneticDelta(text1, text2) yield phonetic1, phonetic2, delta | Compute the US_ENGLISH soundex character difference between two given strings
-| CALL apoc.text.join(['text1','text2',...], delimiter) YIELD value | join the given strings with the given delimiter.
-| CAL apoc.text.clean(text) YIELD value | strip the given string of everything except alpha numeric characters and convert it to lower case.
-| CALL apoc.text.compareCleaned(text1, text2) YIELD value | compare the given strings stripped of everything except alpha numeric characters converted to lower case.
-| CALL apoc.text.filterCleanMatches(text1, text2) YIELD value | filter out non-matches of the given strings stripped of everything except alpha numeric characters converted to lower case.
 |===
 
 === Date/time Support
@@ -396,9 +387,6 @@ Provides a wrapper around GraphAlgoFactory.
 | CALL apoc.algo.dijkstraWithDefaultWeight(startNode, endNode, relAndDirections, costProperty, defaultCost) | run dijkstra with a relationship property as cost function. If the relationship property does not exist, use the specified default value instead.
 | CALL apoc.algo.closeness(['TYPE',...],nodes, INCOMING) YIELD node, centrality | calculate closeness centrality for given nodes
 | CALL apoc.algo.betweenness(['TYPE',...],nodes,BOTH) YIELD node, centrality | calculate betweenness centrality for given nodes`
-| CALL apoc.algo.pageRank(nodes) YIELD node, score | calculates page rank for given nodes with 20 iterations
-| CALL apoc.algo.pageRankWithIterations(iterations, nodes) YIELD node, score | calculates page rank for given nodes and iterations
-| CALL apoc.algo.community(node,partitionKey,type,direction,weightKey) | community detection using label propagation
 |===
 
 
diff --git a/src/main/java/apoc/index/FreeTextQueryParser.java b/src/main/java/apoc/index/FreeTextQueryParser.java
deleted file mode 100644
index 7d1b4ad70..000000000
--- a/src/main/java/apoc/index/FreeTextQueryParser.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package apoc.index;
-
-
-import org.apache.lucene.queryparser.classic.ParseException;
-import org.apache.lucene.queryparser.classic.QueryParser;
-import org.apache.lucene.search.Query;
-
-import static apoc.index.FreeTextSearch.KEY;
-import static org.apache.lucene.search.NumericRangeQuery.newDoubleRange;
-
-class FreeTextQueryParser extends QueryParser {
-    static Query parseFreeTextQuery(String query) throws ParseException {
-        return PARSER.get().parse(query);
-    }
-
-    private static final ThreadLocal<QueryParser> PARSER = new ThreadLocal<QueryParser>() {
-        @Override
-        protected QueryParser initialValue() {
-            return new FreeTextQueryParser();
-        }
-    };
-
-    private FreeTextQueryParser() {
-        super(KEY, FreeTextSearch.analyzer());
-    }
-
-    @Override
-    protected Query newRangeQuery(String field, String part1, String part2, boolean startInclusive, boolean endInclusive) {
-        if (!KEY.equals(field)) {
-            Double num1 = parseNumber(part1), num2 = parseNumber(part2);
-            if ((num1 != null || num2 != null) && !(num1 == null && part1 != null) && !(num2 == null && part2 != null)) {
-                return newDoubleRange(field, num1, num2, startInclusive, endInclusive);
-            }
-        }
-        return super.newRangeQuery(field, part1, part2, startInclusive, endInclusive);
-    }
-
-    private static Double parseNumber(String number) {
-        try {
-            return number == null ? null : Double.parseDouble(number);
-        } catch (NumberFormatException e) {
-            return null;
-        }
-    }
-}
diff --git a/src/main/java/apoc/index/FreeTextSearch.java b/src/main/java/apoc/index/FreeTextSearch.java
deleted file mode 100644
index 5458d4c93..000000000
--- a/src/main/java/apoc/index/FreeTextSearch.java
+++ /dev/null
@@ -1,303 +0,0 @@
-package apoc.index;
-
-import apoc.Description;
-import apoc.result.WeightedNodeResult;
-import org.apache.lucene.analysis.Analyzer;
-import org.apache.lucene.queryparser.classic.ParseException;
-import org.neo4j.graphdb.*;
-import org.neo4j.graphdb.index.Index;
-import org.neo4j.graphdb.index.IndexHits;
-import org.neo4j.index.impl.lucene.legacy.LuceneDataSource;
-import org.neo4j.index.impl.lucene.legacy.LuceneIndexImplementation;
-import org.neo4j.kernel.impl.util.JobScheduler;
-import org.neo4j.kernel.internal.GraphDatabaseAPI;
-import org.neo4j.logging.Log;
-import org.neo4j.procedure.Context;
-import org.neo4j.procedure.Name;
-import org.neo4j.procedure.PerformsWrites;
-import org.neo4j.procedure.Procedure;
-
-import java.util.*;
-import java.util.concurrent.Executor;
-import java.util.function.Consumer;
-import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
-
-import static apoc.index.FreeTextQueryParser.parseFreeTextQuery;
-import static apoc.util.AsyncStream.async;
-import static java.util.Arrays.asList;
-import static java.util.Collections.*;
-import static java.util.stream.Collectors.toMap;
-import static org.neo4j.graphdb.Label.label;
-import static org.neo4j.helpers.collection.Iterables.single;
-
-public class FreeTextSearch {
-    /**
-     * Create (or recreate) a free text search index.
-     * <p>
-     * This will populate the index with all currently matching data. Updates will not be reflected in the index.
-     * In order to get updates into the index, the index has to be rebuilt.
-     *
-     * @param index     The name of the index to create.
-     * @param structure The labels of nodes to index, and the properties to index for each label.
-     * @return a stream containing a single element that describes the created index.
-     */
-    @Procedure
-    @PerformsWrites
-    @Description("apoc.index.addAllNodes('name',{label1:['prop1',...],...}) YIELD type, name, config - create a free text search index")
-    public Stream<FulltextIndex.IndexInfo> addAllNodes(@Name("index") String index, @Name("structure") Map<String, List<String>> structure) {
-        switch (structure.size()) {
-            case 0:
-                throw new IllegalArgumentException("No structure given.");
-            case 1:
-                return create(new Indexer.Single(index, db, log, structure));
-            default:
-                return create(new Indexer(index, db, log, structure));
-        }
-    }
-
-    /**
-     * Search in the specified index for nodes matching the the given value.
-     * <p>
-     * Any indexed property is searched.
-     *
-     * @param index The name of the index to search in.
-     * @param query The query specifying what to search for.
-     * @return a stream of all matching nodes.
-     */
-    @Procedure
-    @PerformsWrites
-    @Description("apoc.index.search('name', 'query') YIELD node, weight - search for nodes in the free text index matching the given query")
-    public Stream<WeightedNodeResult> search(@Name("index") String index, @Name("query") String query) throws ParseException {
-        if (!db.index().existsForNodes(index)) {
-            return Stream.empty();
-        }
-        return result(db.index().forNodes(index).query(parseFreeTextQuery(query)));
-    }
-
-    @Context
-    public GraphDatabaseAPI db;
-
-    @Context
-    public Log log;
-
-    // BEGIN: implementation
-
-    static Stream<WeightedNodeResult> result(IndexHits<Node> hits) {
-        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new Iterator<WeightedNodeResult>() {
-            @Override
-            public boolean hasNext() {
-                return hits.hasNext();
-            }
-
-            @Override
-            public WeightedNodeResult next() {
-                Node node = hits.next();
-                float weight = hits.currentScore();
-                return new WeightedNodeResult(node, weight);
-            }
-        }, 0), false);
-    }
-
-    private static final Map<String, String> CONFIG = LuceneIndexImplementation.FULLTEXT_CONFIG;
-    static final String KEY = "search";
-    private static final JobScheduler.Group GROUP = new JobScheduler.Group(
-            FreeTextSearch.class.getSimpleName(), JobScheduler.SchedulingStrategy.POOLED);
-
-    private Stream<FulltextIndex.IndexInfo> create(Indexer indexer) {
-        return async(executor(), "Creating index '" + indexer.name + "'", indexer);
-    }
-
-    private Executor executor() {
-        return db.getDependencyResolver().resolveDependency(JobScheduler.class).executor(GROUP);
-    }
-
-    static Analyzer analyzer() {
-        return LuceneDataSource.LOWER_CASE_WHITESPACE_ANALYZER;
-    }
-
-    private static class Indexer implements Consumer<Consumer<FulltextIndex.IndexInfo>> {
-        private final String name;
-        final GraphDatabaseService db;
-        private final Log log;
-        final Set<String> labels = new HashSet<>();
-        final Map<Set<String/*labels*/>, Properties> properties = new HashMap<>();
-
-        Indexer(String name, GraphDatabaseService db, Log log, Map<String, List<String>> structure) {
-            this.name = name;
-            this.db = db;
-            this.log = log;
-            for (Map.Entry<String, List<String>> entry : structure.entrySet()) {
-                labels.add(entry.getKey());
-                properties.put(singleton(entry.getKey()),
-                        new Properties(entry.getKey(), new HashSet<>(entry.getValue())));
-            }
-        }
-
-        ResourceIterator<Node> nodes() {
-            return db.getAllNodes().iterator();
-        }
-
-        Properties properties(Node node) {
-            Set<String> labels = labels(node);
-            if (labels == null) {
-                return null;
-            }
-            Properties properties = this.properties.get(labels);
-            if (properties == null) {
-                properties = new Properties(labels.stream().map(label -> this.properties.get(singleton(label))));
-                this.properties.put(labels, properties);
-            }
-            return properties;
-        }
-
-        private Set<String> labels(Node node) {
-            Set<String> labels = null;
-            for (Label label : node.getLabels()) {
-                String name = label.name();
-                if (this.labels.contains(name)) {
-                    if (labels == null) {
-                        labels = singleton(name);
-                        continue;
-                    } else if (labels.size() == 1) {
-                        labels = new HashSet<>(labels);
-                    }
-                    labels.add(name);
-                }
-            }
-            return labels;
-        }
-
-        @Override
-        public final void accept(Consumer<FulltextIndex.IndexInfo> result) {
-            Transaction tx = db.beginTx();
-            try {
-                int batch = 0;
-                if (db.index().existsForNodes(name)) {
-                    Index<Node> old = db.index().forNodes(name);
-                    Map<String, String> config = db.index().getConfiguration(old);
-                    log.info("Dropping existing index '%s', with config: %s", name, config);
-                    old.delete();
-                }
-
-                Index<Node> index = db.index().forNodes(name, configuration());
-
-                try (ResourceIterator<Node> nodes = nodes()) {
-                    for (int count = 0; nodes.hasNext(); ) {
-                        Node node = nodes.next();
-
-                        Properties properties = properties(node);
-                        if (properties != null) {
-                            properties.index(node, index);
-                            count++;
-                        }
-
-                        if (count == 50_000) {
-                            tx.success();
-                            tx.close();
-                            tx = db.beginTx();
-                            count = 0;
-                            log.info("Committing batch %s in creation of index '%s'", ++batch, name);
-                        }
-                    }
-                }
-
-                log.info("Committing batch %s in creation of index '%s' (last batch)", ++batch, name);
-
-                result.accept(new FulltextIndex.IndexInfo(FulltextIndex.NODE, name, CONFIG));
-
-                tx.success();
-            } finally {
-                tx.close();
-            }
-        }
-
-        private Map<String, String> configuration() {
-            Map<String, String> config = new HashMap<>(CONFIG);
-            config.put("labels", escape(labels));
-            for (String label : labels) {
-                Properties properties = this.properties.get(singleton(label));
-                config.put("keysForLabel:" + escape(label), escape(asList(properties.keys)));
-            }
-            return config;
-        }
-
-        private static String escape(Collection<String> keys) {
-            StringBuilder result = new StringBuilder();
-            for (String key : keys) {
-                if (result.length() > 0) {
-                    result.append(":");
-                }
-                result.append(escape(key));
-            }
-            return result.toString();
-        }
-
-        private static String escape(String key) {
-            return key.replace("$", "$$").replace(":", "$");
-        }
-
-        private static class Single extends Indexer {
-            private final Label label;
-            private final Properties single;
-
-            Single(String name, GraphDatabaseService db, Log log, Map<String, List<String>> structure) {
-                super(name, db, log, structure);
-                this.label = label(single(this.labels));
-                this.single = single(this.properties.values());
-            }
-
-            @Override
-            ResourceIterator<Node> nodes() {
-                return db.findNodes(label);
-            }
-
-            @Override
-            Properties properties(Node node) {
-                return node.hasLabel(label) ? single : null;
-            }
-        }
-    }
-
-    private static class Properties {
-        private final String[] keys;
-        private final Map<String/*key*/, List<String/*fields*/>> fields;
-
-        Properties(String label, Collection<String> properties) {
-            this.keys = properties.toArray(new String[properties.size()]);
-            this.fields = properties.stream().collect(toMap(key -> key, key -> singletonList(label + "." + key)));
-        }
-
-        Properties(Stream<Properties> properties) {
-            Set<String> keys = new HashSet<>();
-            this.fields = new HashMap<>();
-            properties.forEach(props -> {
-                addAll(keys, props.keys);
-                for (Map.Entry<String, List<String>> entry : props.fields.entrySet()) {
-                    fields.compute(entry.getKey(), (k, v) -> {
-                        if (v == null) {
-                            return entry.getValue();
-                        } else {
-                            List<String> value = new ArrayList<>(v.size() + entry.getValue().size());
-                            value.addAll(v);
-                            value.addAll(entry.getValue());
-                            return value;
-                        }
-                    });
-                }
-            });
-            this.keys = keys.toArray(new String[keys.size()]);
-        }
-
-        <T extends PropertyContainer> void index(T entity, Index<T> index) {
-            for (Map.Entry<String, Object> property : entity.getProperties(keys).entrySet()) {
-                Object value = property.getValue();
-                index.add(entity, KEY, value.toString());
-                for (String field : fields.get(property.getKey())) {
-                    index.add(entity, field, value
-                    );
-                }
-            }
-        }
-    }
-}
diff --git a/src/main/java/apoc/result/DistancePathResult.java b/src/main/java/apoc/result/DistancePathResult.java
new file mode 100644
index 000000000..756ad1e69
--- /dev/null
+++ b/src/main/java/apoc/result/DistancePathResult.java
@@ -0,0 +1,20 @@
+package apoc.result;
+
+import org.neo4j.graphdb.Path;
+
+public class DistancePathResult implements Comparable<DistancePathResult> {
+
+    public final Path path;
+
+    public final double distance;
+
+    public DistancePathResult(Path path, double distance) {
+        this.path = path;
+        this.distance = distance;
+    }
+
+    @Override
+    public int compareTo(DistancePathResult o) {
+        return o.distance < this.distance ? 1 : -1;
+    }
+}
diff --git a/src/main/java/apoc/spatial/Distance.java b/src/main/java/apoc/spatial/Distance.java
new file mode 100644
index 000000000..7aa06fbcc
--- /dev/null
+++ b/src/main/java/apoc/spatial/Distance.java
@@ -0,0 +1,85 @@
+package apoc.spatial;
+
+import apoc.Description;
+import apoc.result.DistancePathResult;
+import org.neo4j.graphdb.GraphDatabaseService;
+import org.neo4j.graphdb.Node;
+import org.neo4j.graphdb.Path;
+import org.neo4j.procedure.Context;
+import org.neo4j.procedure.Name;
+import org.neo4j.procedure.Procedure;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+public class Distance {
+
+    private static final String LATITUDE = "latitude";
+    private static final String LONGITUDE = "longitude";
+
+    @Context
+    public GraphDatabaseService db;
+
+    @Procedure
+    @Description("apoc.spatial.sortPathsByDistance(List<Path>) sort the given paths based on the geo informations (lat/long) in ascending order")
+    public Stream<DistancePathResult> sortByDistance(@Name("paths")List<Path> paths) {
+        return paths.size() > 0 ? sortPaths(paths).stream() : Stream.empty();
+    }
+
+    public SortedSet<DistancePathResult> sortPaths(List<Path> paths) {
+        SortedSet<DistancePathResult> result = new TreeSet<DistancePathResult>();
+        for (int i = 0; i <= paths.size()-1; ++i) {
+            double d = getPathDistance(paths.get(i));
+            result.add(new DistancePathResult(paths.get(i), d));
+        }
+
+        return result;
+    }
+
+    public double getPathDistance(Path path) {
+        double distance = 0;
+        List<Node> nodes = new ArrayList<>();
+        for (Node node : path.nodes()) {
+            checkNodeHasGeo(node);
+            nodes.add(node);
+        }
+
+        for (int i = 1; i <= nodes.size()-1; ++i) {
+            Node prev = nodes.get(i-1);
+            Node curr = nodes.get(i);
+            distance += getDistance(
+                    (double) prev.getProperty(LATITUDE),
+                    (double) prev.getProperty(LONGITUDE),
+                    (double) curr.getProperty(LATITUDE),
+                    (double) curr.getProperty(LONGITUDE)
+            );
+        }
+
+        return distance;
+    }
+
+    public double getDistance(double lat1, double lon1, double lat2, double lon2) {
+        double theta = lon1 - lon2;
+        double dist = Math.sin(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(deg2rad(theta));
+        dist = Math.acos(dist);
+        dist = rad2deg(dist);
+        dist = dist * 60 * 1.1515;
+
+        return dist * 1.609344;
+    }
+
+    private double deg2rad(double deg) {
+        return (deg * Math.PI / 180.0);
+    }
+
+    private double rad2deg(double rad) {
+        return (rad * 180.0 / Math.PI);
+    }
+
+    private void checkNodeHasGeo(Node node) {
+        if (!node.hasProperty(LATITUDE) || !node.hasProperty(LONGITUDE)) {
+            throw new IllegalArgumentException(String.format("Node with id %s has invalid geo properties", node.getId()));
+        }
+    }
+
+}
diff --git a/src/main/java/apoc/text/Strings.java b/src/main/java/apoc/text/Strings.java
index 7494a3af5..e59535676 100644
--- a/src/main/java/apoc/text/Strings.java
+++ b/src/main/java/apoc/text/Strings.java
@@ -2,7 +2,6 @@ package apoc.text;
 
 import apoc.Description;
 import apoc.result.BooleanResult;
-import apoc.result.Empty;
 import apoc.result.StringResult;
 import org.neo4j.procedure.Name;
 import org.neo4j.procedure.Procedure;
@@ -52,11 +51,12 @@ public class Strings {
     }
 
     @Procedure
-    @Description("apoc.text.filterCleanMatches(text1, text2) - filter out non-matches of the given strings stripped of everything except alpha numeric characters converted to lower case.")
-    public Stream<Empty> filterCleanMatches(final @Name("text1") String text1, final @Name("text2") String text2) {
+    @Description("apoc.text.filterCleanMatches(text1, text2) YIELD value - filter out non-matches of the given strings stripped of everything except alpha numeric characters converted to lower case.")
+    public Stream<StringResult> filterCleanMatches(final @Name("text1") String text1, final @Name("text2") String text2) {
 
-        boolean matched = text1 != null && text2 != null && removeNonWordCharacters(text1).equals(removeNonWordCharacters(text2));
-        return Empty.stream(matched);
+        return (text1 != null && text2 != null && removeNonWordCharacters(text1).equals(removeNonWordCharacters(text2))) ?
+                Stream.of(new StringResult(text1)) :
+                Stream.empty();
     }
 
     private static Pattern cleanPattern = Pattern.compile("[^A-Za-z0-9]+");
diff --git a/src/main/java/apoc/util/AsyncStream.java b/src/main/java/apoc/util/AsyncStream.java
deleted file mode 100644
index aec32c781..000000000
--- a/src/main/java/apoc/util/AsyncStream.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package apoc.util;
-
-import java.util.Iterator;
-import java.util.concurrent.*;
-import java.util.function.Consumer;
-import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
-
-import static java.util.Spliterators.spliteratorUnknownSize;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-
-public final class AsyncStream<T> implements Consumer<T>, Iterator<T> {
-    public static <T> Stream<T> async(Executor executor, String taskName, Consumer<Consumer<T>> body) {
-        AsyncStream<T> sink = new AsyncStream<>(16);
-        executor.execute(() -> {
-            Thread thread = Thread.currentThread();
-            String name = thread.getName();
-            thread.setName(taskName);
-            try {
-                body.accept(sink);
-            } finally {
-                thread.setName(name);
-                sink.done();
-            }
-        });
-        return StreamSupport.stream(spliteratorUnknownSize(sink, 0), false);
-    }
-
-    private final BlockingQueue<T> queue;
-    private final CountDownLatch done = new CountDownLatch(1);
-
-    private AsyncStream(int capacity) {
-        this.queue = new ArrayBlockingQueue<>(capacity);
-    }
-
-    @Override
-    public boolean hasNext() {
-        if (queue.isEmpty()) {
-            try {
-                for (long timeout = 10; !done.await(timeout, MILLISECONDS); timeout = timeout > 1000 ? timeout : timeout * 2) {
-                    if (!queue.isEmpty()) {
-                        return true;
-                    }
-                }
-                return !queue.isEmpty();
-            } catch (InterruptedException e) {
-                throw new IllegalStateException(e);
-            }
-        } else {
-            return true;
-        }
-    }
-
-    @Override
-    public T next() {
-        return queue.remove();
-    }
-
-    @Override
-    public void accept(T t) {
-        try {
-            queue.put(t);
-        } catch (InterruptedException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    private void done() {
-        done.countDown();
-    }
-}


Test patch:
diff --git a/src/test/java/apoc/algo/PageRankTest.java b/src/test/java/apoc/algo/PageRankTest.java
index f85ff301f..8870ac18d 100644
--- a/src/test/java/apoc/algo/PageRankTest.java
+++ b/src/test/java/apoc/algo/PageRankTest.java
@@ -88,6 +88,7 @@ public class PageRankTest
         Map<String,Object> row = result.next();
         assertFalse( result.hasNext() );
         assertEquals( PageRankAlgoTest.EXPECTED, (double) row.get( "score" ), 0.1D );
+        System.out.println( row );
 
     }
 
diff --git a/src/test/java/apoc/index/FreeTextQueryParserTest.java b/src/test/java/apoc/index/FreeTextQueryParserTest.java
deleted file mode 100644
index 1925498d2..000000000
--- a/src/test/java/apoc/index/FreeTextQueryParserTest.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package apoc.index;
-
-import org.apache.lucene.queryparser.classic.ParseException;
-import org.apache.lucene.queryparser.classic.QueryParser;
-import org.apache.lucene.search.NumericRangeQuery;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.TermRangeQuery;
-import org.apache.lucene.util.BytesRef;
-import org.hamcrest.CoreMatchers;
-import org.junit.Test;
-
-import static apoc.index.FreeTextQueryParser.parseFreeTextQuery;
-import static org.apache.lucene.search.NumericRangeQuery.newDoubleRange;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
-
-public class FreeTextQueryParserTest {
-    @Test
-    public void shouldParseNumericRange() throws Exception {
-        assertNumRange("num:[10 TO 15]", 10.0, 15.0);
-        assertNumRange("num:[10 TO *]", 10.0, null);
-        assertNumRange("num:[* TO 20]", null, 20.0);
-        assertTextRange("num:[* TO *]", null, null);
-        assertTextRange("[10 TO 15]", "10", "15");
-        assertTextRange("num:[1 TO two]", "1", "two");
-        assertTextRange("num:[one TO 2]", "one", "2");
-    }
-
-    private static void assertNumRange(String query, Double lo, Double hi) throws ParseException {
-        Query parsed = parseFreeTextQuery(query);
-        assertThat(parsed, CoreMatchers.instanceOf(NumericRangeQuery.class));
-        assertEquals(newDoubleRange(((NumericRangeQuery) parsed).getField(), lo, hi, true, true), parsed);
-    }
-
-    private static void assertTextRange(String query, String lo, String hi) throws ParseException {
-        Query parsed = parseFreeTextQuery(query);
-        assertThat(parsed, CoreMatchers.instanceOf(TermRangeQuery.class));
-        assertEquals(new TermRangeQuery(((TermRangeQuery) parsed).getField(),
-                lo == null ? null : new BytesRef(lo),
-                hi == null ? null : new BytesRef(hi), true, true), parsed);
-    }
-}
diff --git a/src/test/java/apoc/index/FreeTextSearchTest.java b/src/test/java/apoc/index/FreeTextSearchTest.java
deleted file mode 100644
index 4db5d9f4c..000000000
--- a/src/test/java/apoc/index/FreeTextSearchTest.java
+++ /dev/null
@@ -1,240 +0,0 @@
-package apoc.index;
-
-import apoc.util.TestUtil;
-import org.apache.lucene.index.Term;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.TermQuery;
-import org.hamcrest.Description;
-import org.hamcrest.Matcher;
-import org.hamcrest.TypeSafeDiagnosingMatcher;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.neo4j.graphdb.*;
-import org.neo4j.graphdb.index.Index;
-import org.neo4j.test.TestGraphDatabaseFactory;
-
-import java.util.Iterator;
-
-import static apoc.index.FreeTextSearch.KEY;
-import static org.hamcrest.CoreMatchers.*;
-import static org.junit.Assert.*;
-import static org.neo4j.graphdb.Label.label;
-import static org.neo4j.helpers.collection.MapUtil.map;
-
-
-public class FreeTextSearchTest {
-    private GraphDatabaseService db;
-
-    @Before
-    public void setUp() throws Exception {
-        db = new TestGraphDatabaseFactory().newImpermanentDatabase();
-        TestUtil.registerProcedure(db, FreeTextSearch.class);
-    }
-
-    @After
-    public void tearDown() {
-        db.shutdown();
-        db = null;
-    }
-
-    @Test
-    public void shouldCreateIndexWithSingleLabel() throws Exception {
-        // given
-        execute("CREATE (:Person{name:'George Goldman', nick:'GeeGee'}), (:Person{name:'Cyrus Jones', age:103})");
-
-        // when
-        execute("CALL apoc.index.addAllNodes('people', {Person:['name','nick']})");
-
-        // then
-        assertSingleNode("people", termQuery("GeeGee"), hasProperty("name", "George Goldman"));
-        assertSingleNode("people", termQuery("George"), hasProperty("name", "George Goldman"));
-        assertSingleNode("people", termQuery("Goldman"), hasProperty("name", "George Goldman"));
-        assertSingleNode("people", termQuery("Cyrus"), hasProperty("name", "Cyrus Jones"));
-        assertSingleNode("people", termQuery("Jones"), hasProperty("name", "Cyrus Jones"));
-    }
-
-    @Test
-    public void shouldRefuseToCreateIndexWithNoStructure() throws Exception {
-        // when
-        try {
-            execute("CALL apoc.index.addAllNodes('empty', {})");
-
-            fail("expected exception");
-        }
-        // then
-        catch (QueryExecutionException e) {
-            Throwable cause = e.getCause();
-            for (Throwable next = cause.getCause(); next != null; ) {
-                cause = next;
-                next = cause.getCause();
-            }
-            assertThat(cause, instanceOf(IllegalArgumentException.class));
-            assertEquals("No structure given.", cause.getMessage());
-        }
-    }
-
-    @Test
-    public void shouldCreateFreeTextIndexWithMultipleLabels() throws Exception {
-        // given
-        execute("CREATE (:Person{name:'Long John Silver', nick:'Cook'}), (:City{name:'London'})");
-
-        // when
-        execute("CALL apoc.index.addAllNodes('stuff', {Person:['name','nick'], City:['name']})");
-
-        // then
-        assertSingleNode("stuff", termQuery("Long"), hasProperty("name", "Long John Silver"), hasLabel("Person"));
-        assertSingleNode("stuff", termQuery("John"), hasProperty("name", "Long John Silver"), hasLabel("Person"));
-        assertSingleNode("stuff", termQuery("Silver"), hasProperty("name", "Long John Silver"), hasLabel("Person"));
-        assertSingleNode("stuff", termQuery("London"), hasProperty("name", "London"), hasLabel("City"));
-    }
-
-    @Test
-    public void shouldQueryFreeTextIndex() throws Exception {
-        // given
-        execute("CREATE (:Person{name:'George Goldman', nick:'GeeGee'}), (:Person{name:'Cyrus Jones', age:103})");
-        execute("CALL apoc.index.addAllNodes('people', {Person:['name','nick']})");
-
-        // then
-        assertSingle(ftsNodes("people", "GeeGee"), hasProperty("name", "George Goldman"));
-        assertSingle(ftsNodes("people", "George"), hasProperty("name", "George Goldman"));
-        assertSingle(ftsNodes("people", "Goldman"), hasProperty("name", "George Goldman"));
-        assertSingle(ftsNodes("people", "Cyrus"), hasProperty("name", "Cyrus Jones"));
-        assertSingle(ftsNodes("people", "Jones"), hasProperty("name", "Cyrus Jones"));
-    }
-
-    @Test
-    public void shouldEnableSearchingBySpecificField() throws Exception {
-        // given
-        execute("CREATE (:Person{name:'Johnny'}), (:Product{name:'Johnny'})");
-        execute("CALL apoc.index.addAllNodes('stuff', {Person:['name'],Product:['name']})");
-
-        // then
-        assertSingle(ftsNodes("stuff", "Person.name:Johnny"), hasLabel("Person"), not(hasLabel("Product")));
-        assertSingle(ftsNodes("stuff", "Product.name:Johnny"), hasLabel("Product"), not(hasLabel("Person")));
-    }
-
-    @Test
-    public void shouldIndexNodesWithMultipleLabels() throws Exception {
-        // given
-        execute("CREATE (:Foo:Bar:Baz{name:'thing'})");
-        execute("CALL apoc.index.addAllNodes('stuff', {Foo:['name'], Baz:['name'], Axe:['name']})");
-
-        // then
-        assertSingle(ftsNodes("stuff", "Foo.name:thing"));
-        assertSingle(ftsNodes("stuff", "Baz.name:thing"));
-        assertNone(ftsNodes("stuff", "Bar.name:thing"));
-        assertNone(ftsNodes("stuff", "Axe.name:thing"));
-        assertSingle(ftsNodes("stuff", "thing"),
-                hasLabel("Foo"), hasLabel("Bar"), hasLabel("Baz"), hasProperty("name", "thing"));
-    }
-
-    @Test
-    public void shouldIndexingInBatchesWork() {
-        // given
-        // create 90k nodes - this force 2 batches during indexing
-        execute("UNWIND range(1,90000) as x CREATE (:Person{name:'person'+x})");
-        execute("CALL apoc.index.addAllNodes('people', {Person:['name']})");
-
-        // then
-        assertSingle(ftsNodes("people", "person89999"), hasProperty("name", "person89999"));
-    }
-
-    private ResourceIterator<Node> ftsNodes(String index, String value) {
-        return db.execute("CALL apoc.index.search({index}, {value}) YIELD node RETURN node",
-                map("index", index, "value", value)).columnAs("node");
-    }
-
-    @SafeVarargs
-    private final void assertSingleNode(String index, Object query, Matcher<? super Node>... matchers) {
-        try (Transaction tx = db.beginTx()) {
-            assertSingle(nodeIndex(index).query(query), matchers);
-            tx.success();
-        }
-    }
-
-    private static void assertNone(Iterator<?> iter) {
-        try {
-            assertFalse("should not contain any values", iter.hasNext());
-        } finally {
-            if (iter instanceof ResourceIterator<?>) {
-                ((ResourceIterator<?>) iter).close();
-            }
-        }
-    }
-
-    @SafeVarargs
-    private static <T extends PropertyContainer> void assertSingle(Iterator<T> iter, Matcher<? super T>... matchers) {
-        try {
-            assertTrue("should contain at least one value", iter.hasNext());
-            assertThat(iter.next(), allOf(matchers));
-            assertFalse("should contain at most one value", iter.hasNext());
-        } finally {
-            if (iter instanceof ResourceIterator<?>) {
-                ((ResourceIterator<?>) iter).close();
-            }
-        }
-    }
-
-    private static Query termQuery(String value) {
-        return new TermQuery(new Term(KEY, value.toLowerCase()));
-    }
-
-    private static Matcher<Node> hasLabel(String label) {
-        return new TypeSafeDiagnosingMatcher<Node>() {
-            @Override
-            protected boolean matchesSafely(Node item, Description mismatchDescription) {
-                if (item.hasLabel(label(label))) {
-                    return true;
-                }
-                mismatchDescription.appendText("missing label ").appendValue(label);
-                return false;
-            }
-
-            @Override
-            public void describeTo(Description description) {
-                description.appendText("Node with label ").appendValue(label);
-            }
-        };
-    }
-
-    private static Matcher<? super PropertyContainer> hasProperty(String key, Object value) {
-        return new TypeSafeDiagnosingMatcher<PropertyContainer>() {
-            @Override
-            protected boolean matchesSafely(PropertyContainer item, Description mismatchDescription) {
-                Object property = item.getProperty(key, null);
-                if (property == null) {
-                    mismatchDescription.appendText("property ").appendValue(key).appendText(" not present");
-                    return false;
-                }
-                if (value instanceof Matcher<?>) {
-                    Matcher<?> matcher = (Matcher<?>) value;
-                    if (!matcher.matches(property)) {
-                        matcher.describeMismatch(property, mismatchDescription);
-                        return false;
-                    }
-                    return true;
-                }
-                if (!property.equals(value)) {
-                    mismatchDescription.appendText("property ").appendValue(key).appendText("has value").appendValue(property);
-                    return false;
-                }
-                return true;
-            }
-
-            @Override
-            public void describeTo(Description description) {
-                description.appendText("entity with property ").appendValue(key).appendText("=").appendValue(value);
-            }
-        };
-    }
-
-    private Index<Node> nodeIndex(String name) {
-        assertTrue(db.index().existsForNodes(name));
-        return db.index().forNodes(name);
-    }
-
-    private void execute(String query) {
-        db.execute(query).close();
-    }
-}
diff --git a/src/test/java/apoc/spatial/DistanceTest.java b/src/test/java/apoc/spatial/DistanceTest.java
new file mode 100644
index 000000000..c6b1f8705
--- /dev/null
+++ b/src/test/java/apoc/spatial/DistanceTest.java
@@ -0,0 +1,122 @@
+package apoc.spatial;
+
+import apoc.result.DistancePathResult;
+import apoc.util.TestUtil;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.neo4j.graphdb.*;
+import org.neo4j.test.TestGraphDatabaseFactory;
+
+import java.util.*;
+
+import static org.junit.Assert.*;
+import static apoc.util.TestUtil.testCall;
+
+public class DistanceTest {
+
+    private GraphDatabaseService db;
+
+    private static final String LABEL = "Point";
+    private static final String LAT = "latitude";
+    private static final String LONG = "longitude";
+    private static final String NAME = "name";
+    private static final String RELATES = "RELATES";
+
+    @Before
+    public void setUp() throws Exception {
+        db = new TestGraphDatabaseFactory().newImpermanentDatabase();
+        TestUtil.registerProcedure(db, Distance.class);
+    }
+
+    @After
+    public void tearDown() {
+        db.shutdown();
+    }
+
+    /* This test fails due to a bug in neo with Path objects returned in scala
+    @Test
+    public void testCalculateShortestDistanceAndReturnPath() {
+        createPoints();
+        int i = 0;
+        testCall(db, "MATCH (a:Point {name: \"bruges\"}), (b:Point {name: \"dresden\"}) " +
+                "MATCH p=(a)-[*]->(b) " +
+                "WITH collect(p) as paths " +
+                "CALL apoc.spatial.shortestDistancePath(paths) YIELD path RETURN path", (row) -> {
+            PathImpl path = (PathImpl) row.get("path");
+            List<Node> nodes = new ArrayList<>();
+            for (Node node : path.nodes()) {
+                nodes.add(node);
+            }
+            assertEquals(3, nodes.size());
+            assertEquals("brussels", nodes.get(1).getProperty(NAME));
+
+        });
+    }
+    */
+
+    @Test
+    public void testSortingPathsOnDistance() {
+        Distance distanceProc = new Distance();
+        distanceProc.db = db;
+        createPoints();
+        int i = 0;
+        try (Transaction tx = db.beginTx()) {
+            List<Path> paths = new ArrayList<>();
+            Result result = db.execute("MATCH (a:Point {name:'bruges'}), (b:Point {name:'dresden'}) " +
+            "MATCH p=(a)-[*]->(b) RETURN p");
+            while (result.hasNext()) {
+                Map<String, Object> record = result.next();
+                ++i;
+                paths.add((Path) record.get("p"));
+            }
+            int z = 0;
+            SortedSet<DistancePathResult> sorted = distanceProc.sortPaths(paths);
+            double lastDistance = 0.0;
+            Iterator<DistancePathResult> it = sorted.iterator();
+            while (it.hasNext()) {
+                ++z;
+                DistancePathResult d = it.next();
+                assertTrue(d.distance > lastDistance);
+                lastDistance = d.distance;
+            }
+            assertEquals(3, z);
+            tx.success();
+        }
+        assertEquals(3, i);
+    }
+
+    private void createPoints() {
+        try (Transaction tx = db.beginTx()) {
+            Node bruges = db.createNode(Label.label(LABEL));
+            bruges.setProperty(NAME, "bruges");
+            bruges.setProperty(LAT, 51.2605829);
+            bruges.setProperty(LONG, 3.0817189);
+
+            Node brussels = db.createNode(Label.label(LABEL));
+            brussels.setProperty(NAME, "brussels");
+            brussels.setProperty(LAT, 50.854954);
+            brussels.setProperty(LONG, 4.3051786);
+
+            Node paris = db.createNode(Label.label(LABEL));
+            paris.setProperty(NAME, "paris");
+            paris.setProperty(LAT, 48.8588376);
+            paris.setProperty(LONG, 2.2773455);
+
+            Node dresden = db.createNode(Label.label(LABEL));
+            dresden.setProperty(NAME, "dresden");
+            dresden.setProperty(LAT, 51.0767496);
+            dresden.setProperty(LONG, 13.6321595);
+
+            bruges.createRelationshipTo(brussels, RelationshipType.withName(RELATES));
+            brussels.createRelationshipTo(dresden, RelationshipType.withName(RELATES));
+            brussels.createRelationshipTo(paris, RelationshipType.withName(RELATES));
+            bruges.createRelationshipTo(paris, RelationshipType.withName(RELATES));
+            paris.createRelationshipTo(dresden, RelationshipType.withName(RELATES));
+
+            tx.success();
+        }
+    }
+
+
+}
diff --git a/src/test/java/apoc/text/StringsTest.java b/src/test/java/apoc/text/StringsTest.java
index 2fef8bfa9..790a8ccfd 100644
--- a/src/test/java/apoc/text/StringsTest.java
+++ b/src/test/java/apoc/text/StringsTest.java
@@ -139,11 +139,11 @@ public class StringsTest {
         String stringToFind = " neo4j-<30";
         List<String> strings = asList(string1, string2, null);
         String query = "UNWIND {strings} as s " +
-                "CALL apoc.text.filterCleanMatches(s, {expected}) " +
-                "RETURN s";
+                "CALL apoc.text.filterCleanMatches(s, {expected}) YIELD value " +
+                "RETURN value";
         testCall(db, query,
                 map("strings", strings,"expected",stringToFind),
-                row -> assertEquals(string1, row.get("s")));
+                row -> assertEquals(string1, row.get("value")));
     }
 
     @Test
@@ -194,7 +194,7 @@ public class StringsTest {
     public void testDocFilterCleanMatches() throws Exception {
         testCall(db,
                 "UNWIND ['Hello World!', 'hello worlds'] as text " +
-                        "CALL apoc.text.filterCleanMatches(text, 'hello_world') RETURN text",
-                row -> assertEquals("Hello World!", row.get("text")));
+                        "CALL apoc.text.filterCleanMatches(text, 'hello_world') YIELD value RETURN value",
+                row -> assertEquals("Hello World!", row.get("value")));
     }
 }