You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by rm...@apache.org on 2016/03/12 01:29:54 UTC

lucene-solr git commit: LUCENE-7099: add newDistanceSort to sandbox LatLonPoint

Repository: lucene-solr
Updated Branches:
  refs/heads/master 684b22222 -> b3ee746a8


LUCENE-7099: add newDistanceSort to sandbox LatLonPoint


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/b3ee746a
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/b3ee746a
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/b3ee746a

Branch: refs/heads/master
Commit: b3ee746a8b1f394944c2bb9552e1ea6fd7afd83f
Parents: 684b222
Author: Robert Muir <rm...@apache.org>
Authored: Fri Mar 11 19:29:22 2016 -0500
Committer: Robert Muir <rm...@apache.org>
Committed: Fri Mar 11 19:29:22 2016 -0500

----------------------------------------------------------------------
 lucene/CHANGES.txt                              |   6 +-
 .../org/apache/lucene/document/LatLonPoint.java |  69 ++++++-
 .../document/LatLonPointDistanceComparator.java | 119 ++++++++++++
 .../lucene/document/LatLonPointSortField.java   | 108 +++++++++++
 .../apache/lucene/document/TestLatLonPoint.java |   3 +
 .../document/TestLatLonPointDistanceSort.java   | 190 +++++++++++++++++++
 6 files changed, 485 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b3ee746a/lucene/CHANGES.txt
----------------------------------------------------------------------
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index a07e69d..82d22fe 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -7,7 +7,11 @@ http://s.apache.org/luceneversions
 (No Changes)
 
 ======================= Lucene 6.1.0 =======================
-(No Changes)
+
+New Features
+
+* LUCENE-7099: Add LatLonPoint.newDistanceSort to the sandbox's
+  LatLonPoint. (Robert Muir)
 
 Optimizations
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b3ee746a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java
index fd3284b..e8c2f17 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java
@@ -18,14 +18,16 @@ package org.apache.lucene.document;
 
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.NumericUtils;
-
+import org.apache.lucene.index.DocValuesType;
 import org.apache.lucene.index.FieldInfo;
 import org.apache.lucene.index.PointValues;
 import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.ConstantScoreQuery;
+import org.apache.lucene.search.FieldDoc;
 import org.apache.lucene.search.PointRangeQuery;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.search.SortField;
 import org.apache.lucene.spatial.util.GeoUtils;
 
 /** 
@@ -35,10 +37,11 @@ import org.apache.lucene.spatial.util.GeoUtils;
  * efficient.  Multiple values for the same field in one document
  * is allowed. 
  * <p>
- * This field defines static factory methods for creating common queries:
+ * This field defines static factory methods for common operations:
  * <ul>
  *   <li>{@link #newBoxQuery newBoxQuery()} for matching points within a bounding box.
  *   <li>{@link #newDistanceQuery newDistanceQuery()} for matching points within a specified distance.
+ *   <li>{@link #newDistanceSort newDistanceSort()} for ordering documents by distance from a specified location. 
  *   <li>{@link #newPolygonQuery newPolygonQuery()} for matching points within an arbitrary polygon.
  * </ul>
  * <p>
@@ -50,6 +53,8 @@ import org.apache.lucene.spatial.util.GeoUtils;
 // to the field is not actually what gets indexed. Float would be 1E-5 error vs 1E-7, but it might be
 // a better tradeoff? then it would be completely transparent to the user and lucene would be "lossless".
 public class LatLonPoint extends Field {
+  private long currentValue;
+
   /**
    * Type for an indexed LatLonPoint
    * <p>
@@ -58,6 +63,7 @@ public class LatLonPoint extends Field {
   public static final FieldType TYPE = new FieldType();
   static {
     TYPE.setDimensions(2, Integer.BYTES);
+    TYPE.setDocValuesType(DocValuesType.SORTED_NUMERIC);
     TYPE.freeze();
   }
   
@@ -69,9 +75,12 @@ public class LatLonPoint extends Field {
    */
   public void setLocationValue(double latitude, double longitude) {
     byte[] bytes = new byte[8];
-    NumericUtils.intToSortableBytes(encodeLatitude(latitude), bytes, 0);
-    NumericUtils.intToSortableBytes(encodeLongitude(longitude), bytes, Integer.BYTES);
+    int latitudeEncoded = encodeLatitude(latitude);
+    int longitudeEncoded = encodeLongitude(longitude);
+    NumericUtils.intToSortableBytes(latitudeEncoded, bytes, 0);
+    NumericUtils.intToSortableBytes(longitudeEncoded, bytes, Integer.BYTES);
     fieldsData = new BytesRef(bytes);
+    currentValue = (((long)latitudeEncoded) << 32) | (longitudeEncoded & 0xFFFFFFFFL);
   }
 
   /** 
@@ -98,15 +107,25 @@ public class LatLonPoint extends Field {
     result.append(name);
     result.append(':');
 
-    BytesRef bytes = (BytesRef) fieldsData;
-    result.append(decodeLatitude(BytesRef.deepCopyOf(bytes).bytes, 0));
+    result.append(decodeLatitude((int)(currentValue >> 32)));
     result.append(',');
-    result.append(decodeLongitude(BytesRef.deepCopyOf(bytes).bytes, Integer.BYTES));
+    result.append(decodeLongitude((int)(currentValue & 0xFFFFFFFF)));
 
     result.append('>');
     return result.toString();
   }
 
+  /**
+   * Returns a 64-bit long, where the upper 32 bits are the encoded latitude,
+   * and the lower 32 bits are the encoded longitude.
+   * @see #decodeLatitude(int)
+   * @see #decodeLongitude(int)
+   */
+  @Override
+  public Number numericValue() {
+    return currentValue;
+  }
+
   // public helper methods (e.g. for queries)
 
   /** 
@@ -197,16 +216,22 @@ public class LatLonPoint extends Field {
 
   /** helper: checks a fieldinfo and throws exception if its definitely not a LatLonPoint */
   static void checkCompatible(FieldInfo fieldInfo) {
-    if (fieldInfo.getPointDimensionCount() != TYPE.pointDimensionCount()) {
+    // point/dv properties could be "unset", if you e.g. used only StoredField with this same name in the segment.
+    if (fieldInfo.getPointDimensionCount() != 0 && fieldInfo.getPointDimensionCount() != TYPE.pointDimensionCount()) {
       throw new IllegalArgumentException("field=\"" + fieldInfo.name + "\" was indexed with numDims=" + fieldInfo.getPointDimensionCount() + 
                                          " but this point type has numDims=" + TYPE.pointDimensionCount() + 
                                          ", is the field really a LatLonPoint?");
     }
-    if (fieldInfo.getPointNumBytes() != TYPE.pointNumBytes()) {
+    if (fieldInfo.getPointNumBytes() != 0 && fieldInfo.getPointNumBytes() != TYPE.pointNumBytes()) {
       throw new IllegalArgumentException("field=\"" + fieldInfo.name + "\" was indexed with bytesPerDim=" + fieldInfo.getPointNumBytes() + 
                                          " but this point type has bytesPerDim=" + TYPE.pointNumBytes() + 
                                          ", is the field really a LatLonPoint?");
     }
+    if (fieldInfo.getDocValuesType() != DocValuesType.NONE && fieldInfo.getDocValuesType() != TYPE.docValuesType()) {
+      throw new IllegalArgumentException("field=\"" + fieldInfo.name + "\" was indexed with docValuesType=" + fieldInfo.getDocValuesType() + 
+                                         " but this point type has docValuesType=" + TYPE.docValuesType() + 
+                                         ", is the field really a LatLonPoint?");
+    }
   }
 
   // static methods for generating queries
@@ -298,4 +323,30 @@ public class LatLonPoint extends Field {
   public static Query newPolygonQuery(String field, double[] polyLats, double[] polyLons) {
     return new LatLonPointInPolygonQuery(field, polyLats, polyLons);
   }
+
+  /**
+   * Creates a SortField for sorting by distance from a location.
+   * <p>
+   * This sort orders documents by ascending distance from the location. The value returned in {@link FieldDoc} for
+   * the hits contains a Double instance with the distance in meters.
+   * <p>
+   * If a document is missing the field, then by default it is treated as having {@link Double#POSITIVE_INFINITY} distance
+   * (missing last). You can change this by calling {@link SortField#setMissingValue(Object)} on the returned SortField 
+   * to a different Double value.
+   * <p>
+   * If a document contains multiple values for the field, the <i>closest</i> distance to the location is used.
+   * <p>
+   * <b>NOTE</b>: distance sorting might be expensive for many documents. Consider restricting the document
+   * set with a {@link #newBoxQuery box}, {@link #newDistanceQuery radius} radius, or {@link #newPolygonQuery polygon}
+   * query for better performance
+   * 
+   * @param field field name. cannot be null.
+   * @param latitude latitude at the center: must be within standard +/-90 coordinate bounds.
+   * @param longitude longitude at the center: must be within standard +/-180 coordinate bounds.
+   * @return SortField ordering documents by distance
+   * @throws IllegalArgumentException if {@code field} is null or location has invalid coordinates.
+   */
+  public static SortField newDistanceSort(String field, double latitude, double longitude) {
+    return new LatLonPointSortField(field, latitude, longitude);
+  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b3ee746a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointDistanceComparator.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointDistanceComparator.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointDistanceComparator.java
new file mode 100644
index 0000000..a5d8534
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointDistanceComparator.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.lucene.document;
+
+import java.io.IOException;
+
+import org.apache.lucene.index.DocValues;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.SortedNumericDocValues;
+import org.apache.lucene.search.FieldComparator;
+import org.apache.lucene.search.LeafFieldComparator;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.spatial.util.GeoDistanceUtils;
+
+/** Compares docs by distance from an origin */
+class LatLonPointDistanceComparator extends FieldComparator<Double> implements LeafFieldComparator {
+  final String field;
+  final double latitude;
+  final double longitude;
+  final double missingValue;
+
+  final double[] values;
+  double bottom;
+  double topValue;
+  SortedNumericDocValues currentDocs;
+  
+  public LatLonPointDistanceComparator(String field, double latitude, double longitude, int numHits, double missingValue) {
+    this.field = field;
+    this.latitude = latitude;
+    this.longitude = longitude;
+    this.values = new double[numHits];
+    this.missingValue = missingValue;
+  }
+  
+  @Override
+  public void setScorer(Scorer scorer) {}
+
+  @Override
+  public int compare(int slot1, int slot2) {
+    return Double.compare(values[slot1], values[slot2]);
+  }
+  
+  @Override
+  public void setBottom(int slot) {
+    bottom = values[slot];
+  }
+  
+  @Override
+  public void setTopValue(Double value) {
+    topValue = value.doubleValue();
+  }
+  
+  @Override
+  public int compareBottom(int doc) throws IOException {
+    return Double.compare(bottom, distance(doc));
+  }
+  
+  @Override
+  public void copy(int slot, int doc) throws IOException {
+    values[slot] = distance(doc);
+  }
+  
+  @Override
+  public LeafFieldComparator getLeafComparator(LeafReaderContext context) throws IOException {
+    LeafReader reader = context.reader();
+    FieldInfo info = reader.getFieldInfos().fieldInfo(field);
+    if (info != null) {
+      LatLonPoint.checkCompatible(info);
+    }
+    currentDocs = DocValues.getSortedNumeric(reader, field);
+    return this;
+  }
+  
+  @Override
+  public Double value(int slot) {
+    return Double.valueOf(values[slot]);
+  }
+  
+  @Override
+  public int compareTop(int doc) throws IOException {
+    return Double.compare(topValue, distance(doc));
+  }
+  
+  // TODO: optimize for single-valued case?
+  // TODO: do all kinds of other optimizations!
+  double distance(int doc) {
+    currentDocs.setDocument(doc);
+
+    int numValues = currentDocs.count();
+    if (numValues == 0) {
+      return missingValue;
+    }
+
+    double minValue = Double.POSITIVE_INFINITY;
+    for (int i = 0; i < numValues; i++) {
+      long encoded = currentDocs.valueAt(i);
+      double docLatitude = LatLonPoint.decodeLatitude((int)(encoded >> 32));
+      double docLongitude = LatLonPoint.decodeLongitude((int)(encoded & 0xFFFFFFFF));
+      minValue = Math.min(minValue, GeoDistanceUtils.haversin(latitude, longitude, docLatitude, docLongitude));
+    }
+    return minValue;
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b3ee746a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointSortField.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointSortField.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointSortField.java
new file mode 100644
index 0000000..9d3928e
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointSortField.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.lucene.document;
+
+import java.io.IOException;
+
+import org.apache.lucene.search.FieldComparator;
+import org.apache.lucene.search.SortField;
+import org.apache.lucene.spatial.util.GeoUtils;
+
+/**
+ * Sorts by distance from an origin location.
+ */
+final class LatLonPointSortField extends SortField {
+  final double latitude;
+  final double longitude;
+
+  LatLonPointSortField(String field, double latitude, double longitude) {
+    super(field, SortField.Type.CUSTOM);
+    if (field == null) {
+      throw new IllegalArgumentException("field cannot be null");
+    }
+    if (GeoUtils.isValidLat(latitude) == false) {
+      throw new IllegalArgumentException("latitude: '" + latitude + "' is invalid");
+    }
+    if (GeoUtils.isValidLon(longitude) == false) {
+      throw new IllegalArgumentException("longitude: '" + longitude + "' is invalid");
+    }
+    this.latitude = latitude;
+    this.longitude = longitude;
+    setMissingValue(Double.POSITIVE_INFINITY);
+  }
+  
+  @Override
+  public FieldComparator<?> getComparator(int numHits, int sortPos) throws IOException {
+    return new LatLonPointDistanceComparator(getField(), latitude, longitude, numHits, getMissingValue());
+  }
+
+  @Override
+  public Double getMissingValue() {
+    return (Double) super.getMissingValue();
+  }
+
+  @Override
+  public void setMissingValue(Object missingValue) {
+    if (missingValue == null) {
+      throw new IllegalArgumentException("Missing value cannot be null");
+    }
+    if (missingValue.getClass() != Double.class)
+      throw new IllegalArgumentException("Missing value can only be of type java.lang.Double, but got " + missingValue.getClass());
+    this.missingValue = missingValue;
+  }
+  
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = super.hashCode();
+    long temp;
+    temp = Double.doubleToLongBits(latitude);
+    result = prime * result + (int) (temp ^ (temp >>> 32));
+    temp = Double.doubleToLongBits(longitude);
+    result = prime * result + (int) (temp ^ (temp >>> 32));
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) return true;
+    if (!super.equals(obj)) return false;
+    if (getClass() != obj.getClass()) return false;
+    LatLonPointSortField other = (LatLonPointSortField) obj;
+    if (Double.doubleToLongBits(latitude) != Double.doubleToLongBits(other.latitude)) return false;
+    if (Double.doubleToLongBits(longitude) != Double.doubleToLongBits(other.longitude)) return false;
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder();
+    builder.append("<distance:");
+    builder.append('"');
+    builder.append(getField());
+    builder.append('"');
+    builder.append(" latitude=");
+    builder.append(latitude);
+    builder.append(" longitude=");
+    builder.append(longitude);
+    if (Double.POSITIVE_INFINITY != getMissingValue()) {
+      builder.append(" missingValue=" + getMissingValue());
+    }
+    builder.append('>');
+    return builder.toString();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b3ee746a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPoint.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPoint.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPoint.java
index 61c6754..519478f 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPoint.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPoint.java
@@ -56,6 +56,9 @@ public class TestLatLonPoint extends LuceneTestCase {
     
     // distance query does not quantize inputs
     assertEquals("field:18.0,19.0 +/- 25.0 meters", LatLonPoint.newDistanceQuery("field", 18, 19, 25).toString());
+    
+    // sort field
+    assertEquals("<distance:\"field\" latitude=18.0 longitude=19.0>", LatLonPoint.newDistanceSort("field", 18.0, 19.0).toString());
   }
   
   /** Valid values that should not cause exception */

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b3ee746a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointDistanceSort.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointDistanceSort.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointDistanceSort.java
new file mode 100644
index 0000000..a0ee83f
--- /dev/null
+++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointDistanceSort.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.lucene.document;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.search.FieldDoc;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.search.TopFieldDocs;
+import org.apache.lucene.spatial.util.GeoDistanceUtils;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.lucene.util.TestUtil;
+
+/** Simple tests for {@link LatLonPoint#newDistanceSort} */
+public class TestLatLonPointDistanceSort extends LuceneTestCase {
+
+  /** Add three points and sort by distance */
+  public void testDistanceSort() throws Exception {
+    Directory dir = newDirectory();
+    RandomIndexWriter iw = new RandomIndexWriter(random(), dir);
+    
+    // add some docs
+    Document doc = new Document();
+    doc.add(new LatLonPoint("location", 40.759011, -73.9844722));
+    iw.addDocument(doc);
+    
+    doc = new Document();
+    doc.add(new LatLonPoint("location", 40.718266, -74.007819));
+    iw.addDocument(doc);
+    
+    doc = new Document();
+    doc.add(new LatLonPoint("location", 40.7051157, -74.0088305));
+    iw.addDocument(doc);
+    
+    IndexReader reader = iw.getReader();
+    IndexSearcher searcher = new IndexSearcher(reader);
+    iw.close();
+
+    Sort sort = new Sort(LatLonPoint.newDistanceSort("location", 40.7143528, -74.0059731));
+    TopFieldDocs td = searcher.search(new MatchAllDocsQuery(), 3, sort);
+    
+    FieldDoc d = (FieldDoc) td.scoreDocs[0];
+    assertEquals(462.61748421408186D, (Double)d.fields[0], 0.0D);
+    
+    d = (FieldDoc) td.scoreDocs[1];
+    assertEquals(1056.1630445911035D, (Double)d.fields[0], 0.0D);
+    
+    d = (FieldDoc) td.scoreDocs[2];
+    assertEquals(5291.798081404466D, (Double)d.fields[0], 0.0D);
+    
+    reader.close();
+    dir.close();
+  }
+  
+  /** Run a few iterations with just 10 docs, hopefully easy to debug */
+  public void testRandom() throws Exception {
+    for (int iters = 0; iters < 100; iters++) {
+      doRandomTest(10, 100);
+    }
+  }
+  
+  /** Runs with thousands of docs */
+  @Nightly
+  public void testRandomHuge() throws Exception {
+    for (int iters = 0; iters < 10; iters++) {
+      doRandomTest(2000, 100);
+    }
+  }
+  
+  // result class used for testing. holds an id+distance.
+  // we sort these with Arrays.sort and compare with lucene's results
+  static class Result implements Comparable<Result> {
+    int id;
+    double distance;
+    
+    Result(int id, double distance) {
+      this.id = id;
+      this.distance = distance;
+    }
+
+    @Override
+    public int compareTo(Result o) {
+      int cmp = Double.compare(distance, o.distance);
+      if (cmp == 0) {
+        return Integer.compare(id, o.id);
+      }
+      return cmp;
+    }
+
+    @Override
+    public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      long temp;
+      temp = Double.doubleToLongBits(distance);
+      result = prime * result + (int) (temp ^ (temp >>> 32));
+      result = prime * result + id;
+      return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) return true;
+      if (obj == null) return false;
+      if (getClass() != obj.getClass()) return false;
+      Result other = (Result) obj;
+      if (Double.doubleToLongBits(distance) != Double.doubleToLongBits(other.distance)) return false;
+      if (id != other.id) return false;
+      return true;
+    }
+  }
+  
+  private void doRandomTest(int numDocs, int numQueries) throws IOException {
+    Directory dir = newDirectory();    
+    RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
+
+    for (int i = 0; i < numDocs; i++) {
+      double latRaw = -90 + 180.0 * random().nextDouble();
+      double lonRaw = -180 + 360.0 * random().nextDouble();
+      // pre-normalize up front, so we can just use quantized value for testing and do simple exact comparisons
+      double lat = LatLonPoint.decodeLatitude(LatLonPoint.encodeLatitude(latRaw));
+      double lon = LatLonPoint.decodeLongitude(LatLonPoint.encodeLongitude(lonRaw));
+      Document doc = new Document();
+      doc.add(new StoredField("id", i));
+      doc.add(new NumericDocValuesField("id", i));
+      doc.add(new LatLonPoint("field", lat, lon));
+      doc.add(new StoredField("lat", lat));
+      doc.add(new StoredField("lon", lon));
+      writer.addDocument(doc);
+    }
+    IndexReader reader = writer.getReader();
+    IndexSearcher searcher = new IndexSearcher(reader);
+
+    for (int i = 0; i < numQueries; i++) {
+      double lat = -90 + 180.0 * random().nextDouble();
+      double lon = -180 + 360.0 * random().nextDouble();
+
+      Result expected[] = new Result[reader.maxDoc()];
+      
+      for (int doc = 0; doc < reader.maxDoc(); doc++) {
+        Document targetDoc = reader.document(doc);
+        double docLatitude = targetDoc.getField("lat").numericValue().doubleValue();
+        double docLongitude = targetDoc.getField("lon").numericValue().doubleValue();
+        double distance = GeoDistanceUtils.haversin(lat, lon, docLatitude, docLongitude);
+        int id = targetDoc.getField("id").numericValue().intValue();
+        expected[doc] = new Result(id, distance);
+      }
+      
+      Arrays.sort(expected);
+      
+      // randomize the topN a bit
+      int topN = TestUtil.nextInt(random(), 1, reader.maxDoc());
+      // sort by distance, then ID
+      Sort sort = new Sort(LatLonPoint.newDistanceSort("field", lat, lon), 
+                           new SortField("id", SortField.Type.INT));
+
+      TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), topN, sort);
+      for (int resultNumber = 0; resultNumber < topN; resultNumber++) {
+        FieldDoc fieldDoc = (FieldDoc) topDocs.scoreDocs[resultNumber];
+        Result actual = new Result((Integer) fieldDoc.fields[1], (Double) fieldDoc.fields[0]);
+        assertEquals(expected[resultNumber], actual);
+      }
+    }
+    reader.close();
+    writer.close();
+    dir.close();
+  }
+}