mirror of https://github.com/apache/lucene.git
LUCENE-8440: Add support for indexing and searching Line and Point shapes using LatLonShape encoding
This commit is contained in:
parent
b5ed6350a0
commit
a0e33a9bc8
lucene
CHANGES.txt
core/src/java/org/apache/lucene/geo
sandbox/src
java/org/apache/lucene
test/org/apache/lucene/document
|
@ -213,6 +213,8 @@ Changes in Runtime Behavior:
|
|||
|
||||
Improvements
|
||||
|
||||
* LUCENE-8440: Add support for indexing and searching Line and Point shapes using LatLonShape encoding (Nick Knize)
|
||||
|
||||
* LUCENE-8435: Add new LatLonShapePolygonQuery for querying indexed LatLonShape fields by arbitrary polygons (Nick Knize)
|
||||
|
||||
* LUCENE-8367: Make per-dimension drill down optional for each facet dimension (Mike McCandless)
|
||||
|
|
|
@ -202,7 +202,7 @@ public final class Polygon {
|
|||
return sb.toString();
|
||||
}
|
||||
|
||||
private String verticesToGeoJSON(final double[] lats, final double[] lons) {
|
||||
public static String verticesToGeoJSON(final double[] lats, final double[] lons) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append('[');
|
||||
for (int i = 0; i < lats.length; i++) {
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.apache.lucene.document;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.lucene.geo.Line;
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.geo.Tessellator;
|
||||
import org.apache.lucene.geo.Tessellator.Triangle;
|
||||
|
@ -27,6 +28,9 @@ import org.apache.lucene.search.Query;
|
|||
import org.apache.lucene.util.BytesRef;
|
||||
import org.apache.lucene.util.NumericUtils;
|
||||
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
|
||||
|
||||
/**
|
||||
* An indexed shape utility class.
|
||||
* <p>
|
||||
|
@ -62,16 +66,67 @@ public class LatLonShape {
|
|||
private LatLonShape() {
|
||||
}
|
||||
|
||||
/** the lionshare of the indexing is done by the tessellator */
|
||||
/** create indexable fields for polygon geometry */
|
||||
public static Field[] createIndexableFields(String fieldName, Polygon polygon) {
|
||||
// the lionshare of the indexing is done by the tessellator
|
||||
List<Triangle> tessellation = Tessellator.tessellate(polygon);
|
||||
List<LatLonTriangle> fields = new ArrayList<>();
|
||||
for (int i = 0; i < tessellation.size(); ++i) {
|
||||
fields.add(new LatLonTriangle(fieldName, tessellation.get(i)));
|
||||
for (Triangle t : tessellation) {
|
||||
fields.add(new LatLonTriangle(fieldName, t.getEncodedX(0), t.getEncodedY(0),
|
||||
t.getEncodedX(1), t.getEncodedY(1), t.getEncodedX(2), t.getEncodedY(2)));
|
||||
}
|
||||
return fields.toArray(new Field[fields.size()]);
|
||||
}
|
||||
|
||||
/** create indexable fields for line geometry */
|
||||
public static Field[] createIndexableFields(String fieldName, Line line) {
|
||||
int numPoints = line.numPoints();
|
||||
List<LatLonTriangle> fields = new ArrayList<>(numPoints - 1);
|
||||
|
||||
// encode the line vertices
|
||||
int[] encodedLats = new int[numPoints];
|
||||
int[] encodedLons = new int[numPoints];
|
||||
for (int i = 0; i < numPoints; ++i) {
|
||||
encodedLats[i] = encodeLatitude(line.getLat(i));
|
||||
encodedLons[i] = encodeLongitude(line.getLon(i));
|
||||
}
|
||||
|
||||
// create "flat" triangles
|
||||
int aLat, bLat, aLon, bLon, temp;
|
||||
for (int i = 0, j = 1; j < numPoints; ++i, ++j) {
|
||||
aLat = encodedLats[i];
|
||||
aLon = encodedLons[i];
|
||||
bLat = encodedLats[j];
|
||||
bLon = encodedLons[j];
|
||||
if (aLat > bLat) {
|
||||
temp = aLat;
|
||||
aLat = bLat;
|
||||
bLat = temp;
|
||||
temp = aLon;
|
||||
aLon = bLon;
|
||||
bLon = temp;
|
||||
} else if (aLat == bLat) {
|
||||
if (aLon > bLon) {
|
||||
temp = aLat;
|
||||
aLat = bLat;
|
||||
bLat = temp;
|
||||
temp = aLon;
|
||||
aLon = bLon;
|
||||
bLon = temp;
|
||||
}
|
||||
}
|
||||
fields.add(new LatLonTriangle(fieldName, aLon, aLat, bLon, bLat, aLon, aLat));
|
||||
}
|
||||
return fields.toArray(new Field[fields.size()]);
|
||||
}
|
||||
|
||||
/** create indexable fields for point geometry */
|
||||
public static Field[] createIndexableFields(String fieldName, double lat, double lon) {
|
||||
final int encodedLat = encodeLatitude(lat);
|
||||
final int encodedLon = encodeLongitude(lon);
|
||||
return new Field[] {new LatLonTriangle(fieldName, encodedLon, encodedLat, encodedLon, encodedLat, encodedLon, encodedLat)};
|
||||
}
|
||||
|
||||
/** create a query to find all polygons that intersect a defined bounding box
|
||||
* note: does not currently support dateline crossing boxes
|
||||
* todo split dateline crossing boxes into two queries like {@link LatLonPoint#newBoxQuery}
|
||||
|
@ -89,11 +144,9 @@ public class LatLonShape {
|
|||
*/
|
||||
private static class LatLonTriangle extends Field {
|
||||
|
||||
public LatLonTriangle(String name, Triangle t) {
|
||||
LatLonTriangle(String name, int ax, int ay, int bx, int by, int cx, int cy) {
|
||||
super(name, TYPE);
|
||||
setTriangleValue(t.getEncodedX(0), t.getEncodedY(0),
|
||||
t.getEncodedX(1), t.getEncodedY(1),
|
||||
t.getEncodedX(2), t.getEncodedY(2));
|
||||
setTriangleValue(ax, ay, bx, by, cx, cy);
|
||||
}
|
||||
|
||||
public void setTriangleValue(int aX, int aY, int bX, int bY, int cX, int cY) {
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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.geo;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Represents a line on the earth's surface. You can construct the Line directly with {@code double[]}
|
||||
* coordinates.
|
||||
* <p>
|
||||
* NOTES:
|
||||
* <ol>
|
||||
* <li>All latitude/longitude values must be in decimal degrees.
|
||||
* <li>For more advanced GeoSpatial indexing and query operations see the {@code spatial-extras} module
|
||||
* </ol>
|
||||
* @lucene.experimental
|
||||
*/
|
||||
public class Line {
|
||||
/** array of latitude coordinates */
|
||||
private final double[] lats;
|
||||
/** array of longitude coordinates */
|
||||
private final double[] lons;
|
||||
|
||||
/** minimum latitude of this line's bounding box */
|
||||
public final double minLat;
|
||||
/** maximum latitude of this line's bounding box */
|
||||
public final double maxLat;
|
||||
/** minimum longitude of this line's bounding box */
|
||||
public final double minLon;
|
||||
/** maximum longitude of this line's bounding box */
|
||||
public final double maxLon;
|
||||
|
||||
/**
|
||||
* Creates a new Line from the supplied latitude/longitude array.
|
||||
*/
|
||||
public Line(double[] lats, double[] lons) {
|
||||
if (lats == null) {
|
||||
throw new IllegalArgumentException("lats must not be null");
|
||||
}
|
||||
if (lons == null) {
|
||||
throw new IllegalArgumentException("lons must not be null");
|
||||
}
|
||||
if (lats.length != lons.length) {
|
||||
throw new IllegalArgumentException("lats and lons must be equal length");
|
||||
}
|
||||
if (lats.length < 2) {
|
||||
throw new IllegalArgumentException("at least 2 line points required");
|
||||
}
|
||||
|
||||
// compute bounding box
|
||||
double minLat = lats[0];
|
||||
double minLon = lons[0];
|
||||
double maxLat = lats[0];
|
||||
double maxLon = lons[0];
|
||||
for (int i = 0; i < lats.length; ++i) {
|
||||
GeoUtils.checkLatitude(lats[i]);
|
||||
GeoUtils.checkLongitude(lons[i]);
|
||||
minLat = Math.min(lats[i], minLat);
|
||||
minLon = Math.min(lons[i], minLon);
|
||||
maxLat = Math.max(lats[i], maxLat);
|
||||
maxLon = Math.max(lons[i], maxLon);
|
||||
}
|
||||
|
||||
this.lats = lats.clone();
|
||||
this.lons = lons.clone();
|
||||
this.minLat = minLat;
|
||||
this.maxLat = maxLat;
|
||||
this.minLon = minLon;
|
||||
this.maxLon = maxLon;
|
||||
}
|
||||
|
||||
/** returns the number of vertex points */
|
||||
public int numPoints() {
|
||||
return lats.length;
|
||||
}
|
||||
|
||||
/** Returns latitude value at given index */
|
||||
public double getLat(int vertex) {
|
||||
return lats[vertex];
|
||||
}
|
||||
|
||||
/** Returns longitude value at given index */
|
||||
public double getLon(int vertex) {
|
||||
return lons[vertex];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Line)) return false;
|
||||
Line line = (Line) o;
|
||||
return Arrays.equals(lats, line.lats) && Arrays.equals(lons, line.lons);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Arrays.hashCode(lats);
|
||||
result = 31 * result + Arrays.hashCode(lons);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("LINE(");
|
||||
for (int i = 0; i < lats.length; i++) {
|
||||
sb.append("[")
|
||||
.append(lats[i])
|
||||
.append(", ")
|
||||
.append(lons[i])
|
||||
.append("]");
|
||||
}
|
||||
sb.append(')');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** prints polygons as geojson */
|
||||
public String toGeoJSON() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[");
|
||||
sb.append(Polygon.verticesToGeoJSON(lats, lons));
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,458 @@
|
|||
/*
|
||||
* 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.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
|
||||
import org.apache.lucene.geo.GeoTestUtil;
|
||||
import org.apache.lucene.geo.Line;
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.geo.Polygon2D;
|
||||
import org.apache.lucene.geo.Rectangle;
|
||||
import org.apache.lucene.index.DirectoryReader;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.index.IndexWriterConfig;
|
||||
import org.apache.lucene.index.LeafReaderContext;
|
||||
import org.apache.lucene.index.MultiDocValues;
|
||||
import org.apache.lucene.index.MultiFields;
|
||||
import org.apache.lucene.index.NumericDocValues;
|
||||
import org.apache.lucene.index.SerialMergeScheduler;
|
||||
import org.apache.lucene.index.Term;
|
||||
import org.apache.lucene.search.IndexSearcher;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.search.ScoreMode;
|
||||
import org.apache.lucene.search.SimpleCollector;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.apache.lucene.util.Bits;
|
||||
import org.apache.lucene.util.FixedBitSet;
|
||||
import org.apache.lucene.util.IOUtils;
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
|
||||
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomBoolean;
|
||||
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomInt;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
|
||||
import static org.apache.lucene.geo.GeoTestUtil.nextLatitude;
|
||||
import static org.apache.lucene.geo.GeoTestUtil.nextLongitude;
|
||||
|
||||
public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
|
||||
|
||||
protected static final String FIELD_NAME = "shape";
|
||||
|
||||
protected abstract ShapeType getShapeType();
|
||||
|
||||
protected Object nextShape() {
|
||||
return getShapeType().nextShape();
|
||||
}
|
||||
|
||||
protected double quantizeLat(double rawLat) {
|
||||
return decodeLatitude(encodeLatitude(rawLat));
|
||||
}
|
||||
|
||||
protected double quantizeLatCeil(double rawLat) {
|
||||
return decodeLatitude(encodeLatitudeCeil(rawLat));
|
||||
}
|
||||
|
||||
protected double quantizeLon(double rawLon) {
|
||||
return decodeLongitude(encodeLongitude(rawLon));
|
||||
}
|
||||
|
||||
protected double quantizeLonCeil(double rawLon) {
|
||||
return decodeLongitude(encodeLongitudeCeil(rawLon));
|
||||
}
|
||||
|
||||
protected Polygon quantizePolygon(Polygon polygon) {
|
||||
double[] lats = new double[polygon.numPoints()];
|
||||
double[] lons = new double[polygon.numPoints()];
|
||||
for (int i = 0; i < lats.length; ++i) {
|
||||
lats[i] = quantizeLat(polygon.getPolyLat(i));
|
||||
lons[i] = quantizeLon(polygon.getPolyLon(i));
|
||||
}
|
||||
return new Polygon(lats, lons);
|
||||
}
|
||||
|
||||
protected abstract Field[] createIndexableFields(String field, Object shape);
|
||||
|
||||
private void addShapeToDoc(String field, Document doc, Object shape) {
|
||||
Field[] fields = createIndexableFields(field, shape);
|
||||
for (Field f : fields) {
|
||||
doc.add(f);
|
||||
}
|
||||
}
|
||||
|
||||
protected Query newRectQuery(String field, double minLat, double maxLat, double minLon, double maxLon) {
|
||||
return LatLonShape.newBoxQuery(field, minLat, maxLat, minLon, maxLon);
|
||||
}
|
||||
|
||||
protected Query newPolygonQuery(String field, Polygon... polygons) {
|
||||
return LatLonShape.newPolygonQuery(field, polygons);
|
||||
}
|
||||
|
||||
public void testRandomTiny() throws Exception {
|
||||
// Make sure single-leaf-node case is OK:
|
||||
doTestRandom(10);
|
||||
}
|
||||
|
||||
public void testRandomMedium() throws Exception {
|
||||
doTestRandom(10000);
|
||||
}
|
||||
|
||||
@Nightly
|
||||
public void testRandomBig() throws Exception {
|
||||
doTestRandom(50000);
|
||||
}
|
||||
|
||||
private void doTestRandom(int count) throws Exception {
|
||||
int numShapes = atLeast(count);
|
||||
ShapeType type = getShapeType();
|
||||
|
||||
if (VERBOSE) {
|
||||
System.out.println("TEST: number of " + type.name() + " shapes=" + numShapes);
|
||||
}
|
||||
|
||||
Object[] shapes = new Object[numShapes];
|
||||
for (int id = 0; id < numShapes; ++id) {
|
||||
int x = randomInt(20);
|
||||
if (x == 17) {
|
||||
shapes[id] = null;
|
||||
if (VERBOSE) {
|
||||
System.out.println(" id=" + id + " is missing");
|
||||
}
|
||||
} else {
|
||||
// create a new shape
|
||||
shapes[id] = nextShape();
|
||||
}
|
||||
}
|
||||
verify(shapes);
|
||||
}
|
||||
|
||||
private void verify(Object... shapes) throws Exception {
|
||||
IndexWriterConfig iwc = newIndexWriterConfig();
|
||||
iwc.setMergeScheduler(new SerialMergeScheduler());
|
||||
int mbd = iwc.getMaxBufferedDocs();
|
||||
if (mbd != -1 && mbd < shapes.length / 100) {
|
||||
iwc.setMaxBufferedDocs(shapes.length / 100);
|
||||
}
|
||||
Directory dir;
|
||||
if (shapes.length > 1000) {
|
||||
dir = newFSDirectory(createTempDir(getClass().getSimpleName()));
|
||||
} else {
|
||||
dir = newDirectory();
|
||||
}
|
||||
IndexWriter w = new IndexWriter(dir, iwc);
|
||||
|
||||
// index random polygons
|
||||
indexRandomShapes(w, shapes);
|
||||
|
||||
// query testing
|
||||
final IndexReader reader = DirectoryReader.open(w);
|
||||
|
||||
// test random bbox queries
|
||||
verifyRandomBBoxQueries(reader, shapes);
|
||||
// test random polygon queires
|
||||
verifyRandomPolygonQueries(reader, shapes);
|
||||
|
||||
IOUtils.close(w, reader, dir);
|
||||
}
|
||||
|
||||
protected void indexRandomShapes(IndexWriter w, Object... shapes) throws Exception {
|
||||
Set<Integer> deleted = new HashSet<>();
|
||||
for (int id = 0; id < shapes.length; ++id) {
|
||||
Document doc = new Document();
|
||||
doc.add(newStringField("id", "" + id, Field.Store.NO));
|
||||
doc.add(new NumericDocValuesField("id", id));
|
||||
if (shapes[id] != null) {
|
||||
addShapeToDoc(FIELD_NAME, doc, shapes[id]);
|
||||
}
|
||||
w.addDocument(doc);
|
||||
if (id > 0 && randomInt(100) == 42) {
|
||||
int idToDelete = randomInt(id);
|
||||
w.deleteDocuments(new Term("id", ""+idToDelete));
|
||||
deleted.add(idToDelete);
|
||||
if (VERBOSE) {
|
||||
System.out.println(" delete id=" + idToDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (randomBoolean()) {
|
||||
w.forceMerge(1);
|
||||
}
|
||||
}
|
||||
|
||||
protected void verifyRandomBBoxQueries(IndexReader reader, Object... shapes) throws Exception {
|
||||
IndexSearcher s = newSearcher(reader);
|
||||
|
||||
final int iters = atLeast(75);
|
||||
|
||||
Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader());
|
||||
int maxDoc = s.getIndexReader().maxDoc();
|
||||
|
||||
for (int iter = 0; iter < iters; ++iter) {
|
||||
if (VERBOSE) {
|
||||
System.out.println("\nTEST: iter=" + (iter+1) + " of " + iters + " s=" + s);
|
||||
}
|
||||
|
||||
// BBox
|
||||
Rectangle rect;
|
||||
// quantizing the bbox may end up w/ bounding boxes crossing dateline...
|
||||
// todo add support for bounding boxes crossing dateline
|
||||
while (true) {
|
||||
rect = GeoTestUtil.nextBoxNotCrossingDateline();
|
||||
if (decodeLongitude(encodeLongitudeCeil(rect.minLon)) <= decodeLongitude(encodeLongitude(rect.maxLon)) &&
|
||||
decodeLatitude(encodeLatitudeCeil(rect.minLat)) <= decodeLatitude(encodeLatitude(rect.maxLat))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Query query = newRectQuery(FIELD_NAME, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
|
||||
|
||||
if (VERBOSE) {
|
||||
System.out.println(" query=" + query);
|
||||
}
|
||||
|
||||
final FixedBitSet hits = new FixedBitSet(maxDoc);
|
||||
s.search(query, new SimpleCollector() {
|
||||
|
||||
private int docBase;
|
||||
|
||||
@Override
|
||||
public ScoreMode scoreMode() {
|
||||
return ScoreMode.COMPLETE_NO_SCORES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSetNextReader(LeafReaderContext context) throws IOException {
|
||||
docBase = context.docBase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collect(int doc) throws IOException {
|
||||
hits.set(docBase+doc);
|
||||
}
|
||||
});
|
||||
|
||||
boolean fail = false;
|
||||
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
|
||||
for (int docID = 0; docID < maxDoc; ++docID) {
|
||||
assertEquals(docID, docIDToID.nextDoc());
|
||||
int id = (int) docIDToID.longValue();
|
||||
boolean expected;
|
||||
if (liveDocs != null && liveDocs.get(docID) == false) {
|
||||
// document is deleted
|
||||
expected = false;
|
||||
} else if (shapes[id] == null) {
|
||||
expected = false;
|
||||
} else {
|
||||
// check quantized poly against quantized query
|
||||
expected = getValidator().testBBoxQuery(quantizeLatCeil(rect.minLat), quantizeLat(rect.maxLat),
|
||||
quantizeLonCeil(rect.minLon), quantizeLon(rect.maxLon), shapes[id]);
|
||||
}
|
||||
|
||||
if (hits.get(docID) != expected) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
|
||||
if (expected) {
|
||||
b.append("FAIL: id=" + id + " should match but did not\n");
|
||||
} else {
|
||||
b.append("FAIL: id=" + id + " should not match but did\n");
|
||||
}
|
||||
b.append(" query=" + query + " docID=" + docID + "\n");
|
||||
b.append(" shape=" + shapes[id] + "\n");
|
||||
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
|
||||
b.append(" rect=Rectangle(" + quantizeLatCeil(rect.minLat) + " TO " + quantizeLat(rect.maxLat) + " lon=" + quantizeLonCeil(rect.minLon) + " TO " + quantizeLon(rect.maxLon) + ")");
|
||||
if (true) {
|
||||
fail("wrong hit (first of possibly more):\n\n" + b);
|
||||
} else {
|
||||
System.out.println(b.toString());
|
||||
fail = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fail) {
|
||||
fail("some hits were wrong");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void verifyRandomPolygonQueries(IndexReader reader, Object... shapes) throws Exception {
|
||||
IndexSearcher s = newSearcher(reader);
|
||||
|
||||
final int iters = atLeast(75);
|
||||
|
||||
Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader());
|
||||
int maxDoc = s.getIndexReader().maxDoc();
|
||||
|
||||
for (int iter = 0; iter < iters; ++iter) {
|
||||
if (VERBOSE) {
|
||||
System.out.println("\nTEST: iter=" + (iter + 1) + " of " + iters + " s=" + s);
|
||||
}
|
||||
|
||||
// Polygon
|
||||
Polygon queryPolygon = GeoTestUtil.nextPolygon();
|
||||
Polygon2D queryPoly2D = Polygon2D.create(queryPolygon);
|
||||
Query query = newPolygonQuery(FIELD_NAME, queryPolygon);
|
||||
|
||||
if (VERBOSE) {
|
||||
System.out.println(" query=" + query);
|
||||
}
|
||||
|
||||
final FixedBitSet hits = new FixedBitSet(maxDoc);
|
||||
s.search(query, new SimpleCollector() {
|
||||
|
||||
private int docBase;
|
||||
|
||||
@Override
|
||||
public ScoreMode scoreMode() {
|
||||
return ScoreMode.COMPLETE_NO_SCORES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSetNextReader(LeafReaderContext context) throws IOException {
|
||||
docBase = context.docBase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collect(int doc) throws IOException {
|
||||
hits.set(docBase+doc);
|
||||
}
|
||||
});
|
||||
|
||||
boolean fail = false;
|
||||
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
|
||||
for (int docID = 0; docID < maxDoc; ++docID) {
|
||||
assertEquals(docID, docIDToID.nextDoc());
|
||||
int id = (int) docIDToID.longValue();
|
||||
boolean expected;
|
||||
if (liveDocs != null && liveDocs.get(docID) == false) {
|
||||
// document is deleted
|
||||
expected = false;
|
||||
} else if (shapes[id] == null) {
|
||||
expected = false;
|
||||
} else {
|
||||
expected = getValidator().testPolygonQuery(queryPoly2D, shapes[id]);
|
||||
}
|
||||
|
||||
if (hits.get(docID) != expected) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
|
||||
if (expected) {
|
||||
b.append("FAIL: id=" + id + " should match but did not\n");
|
||||
} else {
|
||||
b.append("FAIL: id=" + id + " should not match but did\n");
|
||||
}
|
||||
b.append(" query=" + query + " docID=" + docID + "\n");
|
||||
b.append(" shape=" + shapes[id] + "\n");
|
||||
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
|
||||
b.append(" queryPolygon=" + queryPolygon.toGeoJSON());
|
||||
if (true) {
|
||||
fail("wrong hit (first of possibly more):\n\n" + b);
|
||||
} else {
|
||||
System.out.println(b.toString());
|
||||
fail = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fail) {
|
||||
fail("some hits were wrong");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Validator getValidator();
|
||||
|
||||
/** internal point class for testing point shapes */
|
||||
protected static class Point {
|
||||
double lat;
|
||||
double lon;
|
||||
|
||||
public Point(double lat, double lon) {
|
||||
this.lat = lat;
|
||||
this.lon = lon;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("POINT(");
|
||||
sb.append(lon);
|
||||
sb.append(',');
|
||||
sb.append(lat);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/** internal shape type for testing different shape types */
|
||||
protected enum ShapeType {
|
||||
POINT() {
|
||||
public Point nextShape() {
|
||||
return new Point(nextLatitude(), nextLongitude());
|
||||
}
|
||||
},
|
||||
LINE() {
|
||||
public Line nextShape() {
|
||||
Polygon p = GeoTestUtil.nextPolygon();
|
||||
double[] lats = new double[p.numPoints() - 1];
|
||||
double[] lons = new double[lats.length];
|
||||
for (int i = 0; i < lats.length; ++i) {
|
||||
lats[i] = p.getPolyLat(i);
|
||||
lons[i] = p.getPolyLon(i);
|
||||
}
|
||||
return new Line(lats, lons);
|
||||
}
|
||||
},
|
||||
POLYGON() {
|
||||
public Polygon nextShape() {
|
||||
return GeoTestUtil.nextPolygon();
|
||||
}
|
||||
},
|
||||
MIXED() {
|
||||
public Object nextShape() {
|
||||
return RandomPicks.randomFrom(random(), subList).nextShape();
|
||||
}
|
||||
};
|
||||
|
||||
static ShapeType[] subList;
|
||||
static {
|
||||
subList = new ShapeType[] {POINT, LINE, POLYGON};
|
||||
}
|
||||
|
||||
public abstract Object nextShape();
|
||||
|
||||
static ShapeType fromObject(Object shape) {
|
||||
if (shape instanceof Point) {
|
||||
return POINT;
|
||||
} else if (shape instanceof Line) {
|
||||
return LINE;
|
||||
} else if (shape instanceof Polygon) {
|
||||
return POLYGON;
|
||||
}
|
||||
throw new IllegalArgumentException("invalid shape type from " + shape.toString());
|
||||
}
|
||||
}
|
||||
|
||||
protected interface Validator {
|
||||
boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape);
|
||||
boolean testPolygonQuery(Polygon2D poly2d, Object shape);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 org.apache.lucene.geo.Line;
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.geo.Polygon2D;
|
||||
import org.apache.lucene.index.PointValues.Relation;
|
||||
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
|
||||
|
||||
/** random bounding box and polygon query tests for random generated {@link Line} types */
|
||||
public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase {
|
||||
|
||||
protected final LineValidator VALIDATOR = new LineValidator();
|
||||
|
||||
@Override
|
||||
protected ShapeType getShapeType() {
|
||||
return ShapeType.LINE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Field[] createIndexableFields(String field, Object line) {
|
||||
return LatLonShape.createIndexableFields(field, (Line)line);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Validator getValidator() {
|
||||
return VALIDATOR;
|
||||
}
|
||||
|
||||
protected class LineValidator implements Validator {
|
||||
@Override
|
||||
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
|
||||
// to keep it simple we convert the bbox into a polygon and use poly2d
|
||||
Polygon2D p = Polygon2D.create(new Polygon[] {new Polygon(new double[] {minLat, minLat, maxLat, maxLat, minLat},
|
||||
new double[] {minLon, maxLon, maxLon, minLon, minLon})});
|
||||
return testLine(p, (Line)shape);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean testPolygonQuery(Polygon2D poly2d, Object shape) {
|
||||
return testLine(poly2d, (Line) shape);
|
||||
}
|
||||
|
||||
private boolean testLine(Polygon2D queryPoly, Line line) {
|
||||
double ax, ay, bx, by, temp;
|
||||
for (int i = 0, j = 1; j < line.numPoints(); ++i, ++j) {
|
||||
ay = decodeLatitude(encodeLatitude(line.getLat(i)));
|
||||
ax = decodeLongitude(encodeLongitude(line.getLon(i)));
|
||||
by = decodeLatitude(encodeLatitude(line.getLat(j)));
|
||||
bx = decodeLongitude(encodeLongitude(line.getLon(j)));
|
||||
if (ay > by) {
|
||||
temp = ay;
|
||||
ay = by;
|
||||
by = temp;
|
||||
temp = ax;
|
||||
ax = bx;
|
||||
bx = temp;
|
||||
} else if (ay == by) {
|
||||
if (ax > bx) {
|
||||
temp = ay;
|
||||
ay = by;
|
||||
by = temp;
|
||||
temp = ax;
|
||||
ax = bx;
|
||||
bx = temp;
|
||||
}
|
||||
}
|
||||
if (queryPoly.relateTriangle(ax, ay, bx, by, ax, ay) != Relation.CELL_OUTSIDE_QUERY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 org.apache.lucene.geo.Polygon2D;
|
||||
import org.apache.lucene.index.PointValues.Relation;
|
||||
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
|
||||
|
||||
/** random bounding box and polygon query tests for random generated {@code latitude, longitude} points */
|
||||
public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase {
|
||||
|
||||
protected final PointValidator VALIDATOR = new PointValidator();
|
||||
|
||||
@Override
|
||||
protected ShapeType getShapeType() {
|
||||
return ShapeType.POINT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Field[] createIndexableFields(String field, Object point) {
|
||||
Point p = (Point)point;
|
||||
return LatLonShape.createIndexableFields(field, p.lat, p.lon);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Validator getValidator() {
|
||||
return VALIDATOR;
|
||||
}
|
||||
|
||||
protected class PointValidator implements Validator {
|
||||
@Override
|
||||
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
|
||||
Point p = (Point)shape;
|
||||
double lat = decodeLatitude(encodeLatitude(p.lat));
|
||||
double lon = decodeLongitude(encodeLongitude(p.lon));
|
||||
return (lat < minLat || lat > maxLat || lon < minLon || lon > maxLon) == false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean testPolygonQuery(Polygon2D poly2d, Object shape) {
|
||||
Point p = (Point) shape;
|
||||
double lat = decodeLatitude(encodeLatitude(p.lat));
|
||||
double lon = decodeLongitude(encodeLongitude(p.lon));
|
||||
// for consistency w/ the query we test the point as a triangle
|
||||
return poly2d.relateTriangle(lon, lat, lon, lat, lon, lat) != Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,378 +16,67 @@
|
|||
*/
|
||||
package org.apache.lucene.document;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.lucene.geo.GeoTestUtil;
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.geo.Polygon2D;
|
||||
import org.apache.lucene.geo.Rectangle;
|
||||
import org.apache.lucene.geo.Tessellator;
|
||||
import org.apache.lucene.index.DirectoryReader;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.index.IndexWriterConfig;
|
||||
import org.apache.lucene.index.LeafReaderContext;
|
||||
import org.apache.lucene.index.MultiDocValues;
|
||||
import org.apache.lucene.index.MultiFields;
|
||||
import org.apache.lucene.index.NumericDocValues;
|
||||
import org.apache.lucene.index.PointValues.Relation;
|
||||
import org.apache.lucene.index.SerialMergeScheduler;
|
||||
import org.apache.lucene.index.Term;
|
||||
import org.apache.lucene.search.IndexSearcher;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.search.ScoreMode;
|
||||
import org.apache.lucene.search.SimpleCollector;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.apache.lucene.util.Bits;
|
||||
import org.apache.lucene.util.FixedBitSet;
|
||||
import org.apache.lucene.util.IOUtils;
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
|
||||
public class TestLatLonPolygonShapeQueries extends BaseLatLonShapeTestCase {
|
||||
|
||||
/** base Test case for {@link LatLonShape} indexing and search */
|
||||
public class TestLatLonPolygonShapeQueries extends LuceneTestCase {
|
||||
protected static final String FIELD_NAME = "shape";
|
||||
protected final PolygonValidator VALIDATOR = new PolygonValidator();
|
||||
|
||||
private Polygon quantizePolygon(Polygon polygon) {
|
||||
double[] lats = new double[polygon.numPoints()];
|
||||
double[] lons = new double[polygon.numPoints()];
|
||||
for (int i = 0; i < lats.length; ++i) {
|
||||
lats[i] = quantizeLat(polygon.getPolyLat(i));
|
||||
lons[i] = quantizeLon(polygon.getPolyLon(i));
|
||||
}
|
||||
return new Polygon(lats, lons);
|
||||
@Override
|
||||
protected ShapeType getShapeType() {
|
||||
return ShapeType.POLYGON;
|
||||
}
|
||||
|
||||
protected double quantizeLat(double rawLat) {
|
||||
return decodeLatitude(encodeLatitude(rawLat));
|
||||
}
|
||||
|
||||
protected double quantizeLatCeil(double rawLat) {
|
||||
return decodeLatitude(encodeLatitudeCeil(rawLat));
|
||||
}
|
||||
|
||||
protected double quantizeLon(double rawLon) {
|
||||
return decodeLongitude(encodeLongitude(rawLon));
|
||||
}
|
||||
|
||||
protected double quantizeLonCeil(double rawLon) {
|
||||
return decodeLongitude(encodeLongitudeCeil(rawLon));
|
||||
}
|
||||
|
||||
protected void addPolygonsToDoc(String field, Document doc, Polygon polygon) {
|
||||
Field[] fields = LatLonShape.createIndexableFields(field, polygon);
|
||||
for (Field f : fields) {
|
||||
doc.add(f);
|
||||
}
|
||||
}
|
||||
|
||||
protected Query newRectQuery(String field, double minLat, double maxLat, double minLon, double maxLon) {
|
||||
return LatLonShape.newBoxQuery(field, minLat, maxLat, minLon, maxLon);
|
||||
}
|
||||
|
||||
protected Query newPolygonQuery(String field, Polygon... polygons) {
|
||||
return LatLonShape.newPolygonQuery(field, polygons);
|
||||
}
|
||||
|
||||
public void testRandomTiny() throws Exception {
|
||||
// Make sure single-leaf-node case is OK:
|
||||
doTestRandom(10);
|
||||
}
|
||||
|
||||
public void testRandomMedium() throws Exception {
|
||||
doTestRandom(10000);
|
||||
}
|
||||
|
||||
@Nightly
|
||||
public void testRandomBig() throws Exception {
|
||||
doTestRandom(50000);
|
||||
}
|
||||
|
||||
private void doTestRandom(int count) throws Exception {
|
||||
int numPolygons = atLeast(count);
|
||||
|
||||
if (VERBOSE) {
|
||||
System.out.println("TEST: numPolygons=" + numPolygons);
|
||||
}
|
||||
|
||||
Polygon[] polygons = new Polygon[numPolygons];
|
||||
for (int id = 0; id < numPolygons; ++id) {
|
||||
int x = random().nextInt(20);
|
||||
if (x == 17) {
|
||||
polygons[id] = null;
|
||||
if (VERBOSE) {
|
||||
System.out.println(" id=" + id + " is missing");
|
||||
}
|
||||
} else {
|
||||
// create a polygon that does not cross the dateline
|
||||
polygons[id] = GeoTestUtil.nextPolygon();
|
||||
}
|
||||
}
|
||||
verify(polygons);
|
||||
}
|
||||
|
||||
private void verify(Polygon... polygons) throws Exception {
|
||||
ArrayList<Polygon2D> poly2d = new ArrayList<>();
|
||||
poly2d.ensureCapacity(polygons.length);
|
||||
// index random polygons; poly2d will contain the Polygon2D objects needed for verification
|
||||
IndexWriter w = indexRandomPolygons(poly2d, polygons);
|
||||
Directory dir = w.getDirectory();
|
||||
final IndexReader reader = DirectoryReader.open(w);
|
||||
// test random bbox queries
|
||||
verifyRandomBBoxQueries(reader, poly2d, polygons);
|
||||
// test random polygon queires
|
||||
verifyRandomPolygonQueries(reader, poly2d, polygons);
|
||||
IOUtils.close(w, reader, dir);
|
||||
}
|
||||
|
||||
protected IndexWriter indexRandomPolygons(List<Polygon2D> poly2d, Polygon... polygons) throws Exception {
|
||||
IndexWriterConfig iwc = newIndexWriterConfig();
|
||||
iwc.setMergeScheduler(new SerialMergeScheduler());
|
||||
int mbd = iwc.getMaxBufferedDocs();
|
||||
if (mbd != -1 && mbd < polygons.length / 100) {
|
||||
iwc.setMaxBufferedDocs(polygons.length / 100);
|
||||
}
|
||||
Directory dir;
|
||||
if (polygons.length > 1000) {
|
||||
dir = newFSDirectory(createTempDir(getClass().getSimpleName()));
|
||||
} else {
|
||||
dir = newDirectory();
|
||||
}
|
||||
|
||||
Set<Integer> deleted = new HashSet<>();
|
||||
IndexWriter w = new IndexWriter(dir, iwc);
|
||||
for (int id = 0; id < polygons.length; ++id) {
|
||||
Document doc = new Document();
|
||||
doc.add(newStringField("id", "" + id, Field.Store.NO));
|
||||
doc.add(new NumericDocValuesField("id", id));
|
||||
if (polygons[id] != null) {
|
||||
try {
|
||||
addPolygonsToDoc(FIELD_NAME, doc, polygons[id]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// GeoTestUtil will occassionally create invalid polygons
|
||||
// invalid polygons will not tessellate
|
||||
// we skip those polygons that will not tessellate, relying on the TestTessellator class
|
||||
// to ensure the Tessellator correctly identified a malformed shape and its not a bug
|
||||
if (VERBOSE) {
|
||||
System.out.println(" id=" + id + " could not tessellate. Malformed shape " + polygons[id] + " detected");
|
||||
}
|
||||
// remove and skip the malformed shape
|
||||
polygons[id] = null;
|
||||
poly2d.add(id, null);
|
||||
continue;
|
||||
}
|
||||
poly2d.add(id, Polygon2D.create(quantizePolygon(polygons[id])));
|
||||
} else {
|
||||
poly2d.add(id, null);
|
||||
}
|
||||
w.addDocument(doc);
|
||||
if (id > 0 && random().nextInt(100) == 42) {
|
||||
int idToDelete = random().nextInt(id);
|
||||
w.deleteDocuments(new Term("id", ""+idToDelete));
|
||||
deleted.add(idToDelete);
|
||||
if (VERBOSE) {
|
||||
System.out.println(" delete id=" + idToDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (random().nextBoolean()) {
|
||||
w.forceMerge(1);
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
protected void verifyRandomBBoxQueries(IndexReader reader, List<Polygon2D> poly2d, Polygon... polygons) throws Exception {
|
||||
IndexSearcher s = newSearcher(reader);
|
||||
|
||||
final int iters = atLeast(75);
|
||||
|
||||
Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader());
|
||||
int maxDoc = s.getIndexReader().maxDoc();
|
||||
|
||||
for (int iter = 0; iter < iters; ++iter) {
|
||||
if (VERBOSE) {
|
||||
System.out.println("\nTEST: iter=" + (iter+1) + " of " + iters + " s=" + s);
|
||||
}
|
||||
|
||||
// BBox
|
||||
Rectangle rect = GeoTestUtil.nextBoxNotCrossingDateline();
|
||||
Query query = newRectQuery(FIELD_NAME, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
|
||||
|
||||
if (VERBOSE) {
|
||||
System.out.println(" query=" + query);
|
||||
}
|
||||
|
||||
final FixedBitSet hits = new FixedBitSet(maxDoc);
|
||||
s.search(query, new SimpleCollector() {
|
||||
|
||||
private int docBase;
|
||||
|
||||
@Override
|
||||
public ScoreMode scoreMode() {
|
||||
return ScoreMode.COMPLETE_NO_SCORES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSetNextReader(LeafReaderContext context) throws IOException {
|
||||
docBase = context.docBase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collect(int doc) throws IOException {
|
||||
hits.set(docBase+doc);
|
||||
}
|
||||
});
|
||||
|
||||
boolean fail = false;
|
||||
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
|
||||
for (int docID = 0; docID < maxDoc; ++docID) {
|
||||
assertEquals(docID, docIDToID.nextDoc());
|
||||
int id = (int) docIDToID.longValue();
|
||||
boolean expected;
|
||||
if (liveDocs != null && liveDocs.get(docID) == false) {
|
||||
// document is deleted
|
||||
expected = false;
|
||||
} else if (polygons[id] == null) {
|
||||
expected = false;
|
||||
} else {
|
||||
// check quantized poly against quantized query
|
||||
expected = poly2d.get(id).relate(quantizeLatCeil(rect.minLat), quantizeLat(rect.maxLat),
|
||||
quantizeLonCeil(rect.minLon), quantizeLon(rect.maxLon)) != Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
|
||||
if (hits.get(docID) != expected) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
|
||||
if (expected) {
|
||||
b.append("FAIL: id=" + id + " should match but did not\n");
|
||||
} else {
|
||||
b.append("FAIL: id=" + id + " should not match but did\n");
|
||||
}
|
||||
b.append(" query=" + query + " docID=" + docID + "\n");
|
||||
b.append(" polygon=" + quantizePolygon(polygons[id]) + "\n");
|
||||
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
|
||||
b.append(" rect=Rectangle(" + quantizeLatCeil(rect.minLat) + " TO " + quantizeLat(rect.maxLat) + " lon=" + quantizeLonCeil(rect.minLon) + " TO " + quantizeLon(rect.maxLon) + ")");
|
||||
if (true) {
|
||||
fail("wrong hit (first of possibly more):\n\n" + b);
|
||||
} else {
|
||||
System.out.println(b.toString());
|
||||
fail = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fail) {
|
||||
fail("some hits were wrong");
|
||||
@Override
|
||||
protected Polygon nextShape() {
|
||||
Polygon p;
|
||||
while (true) {
|
||||
// if we can't tessellate; then random polygon generator created a malformed shape
|
||||
p = (Polygon)getShapeType().nextShape();
|
||||
try {
|
||||
Tessellator.tessellate(p);
|
||||
return p;
|
||||
} catch (IllegalArgumentException e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void verifyRandomPolygonQueries(IndexReader reader, List<Polygon2D> poly2d, Polygon... polygons) throws Exception {
|
||||
IndexSearcher s = newSearcher(reader);
|
||||
@Override
|
||||
protected Field[] createIndexableFields(String field, Object polygon) {
|
||||
return LatLonShape.createIndexableFields(field, (Polygon)polygon);
|
||||
}
|
||||
|
||||
final int iters = atLeast(75);
|
||||
@Override
|
||||
protected Validator getValidator() {
|
||||
return VALIDATOR;
|
||||
}
|
||||
|
||||
Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader());
|
||||
int maxDoc = s.getIndexReader().maxDoc();
|
||||
protected class PolygonValidator implements Validator {
|
||||
@Override
|
||||
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
|
||||
Polygon2D poly = Polygon2D.create(quantizePolygon((Polygon)shape));
|
||||
return poly.relate(minLat, maxLat, minLon, maxLon) != Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
|
||||
for (int iter = 0; iter < iters; ++iter) {
|
||||
if (VERBOSE) {
|
||||
System.out.println("\nTEST: iter=" + (iter+1) + " of " + iters + " s=" + s);
|
||||
}
|
||||
@Override
|
||||
public boolean testPolygonQuery(Polygon2D query, Object shape) {
|
||||
|
||||
// Polygon
|
||||
Polygon queryPolygon = GeoTestUtil.nextPolygon();
|
||||
Polygon2D queryPoly2D = Polygon2D.create(queryPolygon);
|
||||
Query query = newPolygonQuery(FIELD_NAME, queryPolygon);
|
||||
|
||||
if (VERBOSE) {
|
||||
System.out.println(" query=" + query);
|
||||
}
|
||||
|
||||
final FixedBitSet hits = new FixedBitSet(maxDoc);
|
||||
s.search(query, new SimpleCollector() {
|
||||
|
||||
private int docBase;
|
||||
|
||||
@Override
|
||||
public ScoreMode scoreMode() {
|
||||
return ScoreMode.COMPLETE_NO_SCORES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSetNextReader(LeafReaderContext context) throws IOException {
|
||||
docBase = context.docBase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collect(int doc) throws IOException {
|
||||
hits.set(docBase+doc);
|
||||
}
|
||||
});
|
||||
|
||||
boolean fail = false;
|
||||
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
|
||||
for (int docID = 0; docID < maxDoc; ++docID) {
|
||||
assertEquals(docID, docIDToID.nextDoc());
|
||||
int id = (int) docIDToID.longValue();
|
||||
boolean expected;
|
||||
if (liveDocs != null && liveDocs.get(docID) == false) {
|
||||
// document is deleted
|
||||
expected = false;
|
||||
} else if (polygons[id] == null) {
|
||||
expected = false;
|
||||
} else {
|
||||
expected = false;
|
||||
try {
|
||||
// check poly (quantized the same way as indexed) against query polygon
|
||||
List<Tessellator.Triangle> tesselation = Tessellator.tessellate(quantizePolygon(polygons[id]));
|
||||
for (Tessellator.Triangle t : tesselation) {
|
||||
if (queryPoly2D.relateTriangle(t.getLon(0), t.getLat(0),
|
||||
t.getLon(1), t.getLat(1), t.getLon(2), t.getLat(2)) != Relation.CELL_OUTSIDE_QUERY) {
|
||||
expected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (hits.get(docID) != expected) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
|
||||
if (expected) {
|
||||
b.append("FAIL: id=" + id + " should match but did not\n");
|
||||
} else {
|
||||
b.append("FAIL: id=" + id + " should not match but did\n");
|
||||
}
|
||||
b.append(" query=" + query + " docID=" + docID + "\n");
|
||||
b.append(" polygon=" + quantizePolygon(polygons[id]).toGeoJSON() + "\n");
|
||||
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
|
||||
b.append(" queryPolygon=" + queryPolygon.toGeoJSON());
|
||||
if (true) {
|
||||
fail("wrong hit (first of possibly more):\n\n" + b);
|
||||
} else {
|
||||
System.out.println(b.toString());
|
||||
fail = true;
|
||||
}
|
||||
List<Tessellator.Triangle> tessellation = Tessellator.tessellate((Polygon) shape);
|
||||
for (Tessellator.Triangle t : tessellation) {
|
||||
// we quantize the triangle for consistency with the index
|
||||
if (query.relateTriangle(quantizeLon(t.getLon(0)), quantizeLat(t.getLat(0)),
|
||||
quantizeLon(t.getLon(1)), quantizeLat(t.getLat(1)),
|
||||
quantizeLon(t.getLon(2)), quantizeLat(t.getLat(2))) != Relation.CELL_OUTSIDE_QUERY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (fail) {
|
||||
fail("some hits were wrong");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.apache.lucene.document;
|
|||
|
||||
import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
|
||||
import org.apache.lucene.geo.GeoTestUtil;
|
||||
import org.apache.lucene.geo.Line;
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.index.DirectoryReader;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
|
@ -43,6 +44,13 @@ public class TestLatLonShape extends LuceneTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
protected void addLineToDoc(String field, Document doc, Line line) {
|
||||
Field[] fields = LatLonShape.createIndexableFields(field, line);
|
||||
for (Field f : fields) {
|
||||
doc.add(f);
|
||||
}
|
||||
}
|
||||
|
||||
protected Query newRectQuery(String field, double minLat, double maxLat, double minLon, double maxLon) {
|
||||
return LatLonShape.newBoxQuery(field, minLat, maxLat, minLon, maxLon);
|
||||
}
|
||||
|
@ -81,19 +89,36 @@ public class TestLatLonShape extends LuceneTestCase {
|
|||
Directory dir = newDirectory();
|
||||
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
|
||||
|
||||
// add a random polygon
|
||||
// add a random polygon document
|
||||
Polygon p = GeoTestUtil.createRegularPolygon(0, 90, atLeast(1000000), numVertices);
|
||||
Document document = new Document();
|
||||
addPolygonsToDoc(FIELDNAME, document, p);
|
||||
writer.addDocument(document);
|
||||
|
||||
// add a line document
|
||||
document = new Document();
|
||||
// add a line string
|
||||
double lats[] = new double[p.numPoints() - 1];
|
||||
double lons[] = new double[p.numPoints() - 1];
|
||||
for (int i = 0; i < lats.length; ++i) {
|
||||
lats[i] = p.getPolyLat(i);
|
||||
lons[i] = p.getPolyLon(i);
|
||||
}
|
||||
Line l = new Line(lats, lons);
|
||||
addLineToDoc(FIELDNAME, document, l);
|
||||
writer.addDocument(document);
|
||||
|
||||
////// search /////
|
||||
// search an intersecting bbox
|
||||
IndexReader reader = writer.getReader();
|
||||
writer.close();
|
||||
IndexSearcher searcher = newSearcher(reader);
|
||||
Query q = newRectQuery(FIELDNAME, -1d, 1d, p.minLon, p.maxLon);
|
||||
assertEquals(1, searcher.count(q));
|
||||
double minLat = Math.min(lats[0], lats[1]);
|
||||
double minLon = Math.min(lons[0], lons[1]);
|
||||
double maxLat = Math.max(lats[0], lats[1]);
|
||||
double maxLon = Math.max(lons[0], lons[1]);
|
||||
Query q = newRectQuery(FIELDNAME, minLat, maxLat, minLon, maxLon);
|
||||
assertEquals(2, searcher.count(q));
|
||||
|
||||
// search a disjoint bbox
|
||||
q = newRectQuery(FIELDNAME, p.minLat-1d, p.minLat+1, p.minLon-1d, p.minLon+1d);
|
||||
|
|
Loading…
Reference in New Issue