LUCENE-9552: Adds a LatLonPoint query that accepts an array of LatLonGeometries (#1940)

This commit is contained in:
Ignacio Vera 2020-11-03 08:51:28 +01:00 committed by GitHub
parent 6a7131ee24
commit 8bfbed8d4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 379 additions and 525 deletions

View File

@ -212,6 +212,8 @@ New Features
* LUCENE-9572: TypeAsSynonymFilter has been enhanced support ignoring some types, and to allow * LUCENE-9572: TypeAsSynonymFilter has been enhanced support ignoring some types, and to allow
the generated synonyms to copy some or all flags from the original token (Gus Heck). the generated synonyms to copy some or all flags from the original token (Gus Heck).
* LUCENE-9552: New LatLonPoint query that accepts an array of LatLonGeometries. (Ignacio Vera)
Improvements Improvements
--------------------- ---------------------

View File

@ -1,148 +0,0 @@
/*
* 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.geo.GeoEncodingUtils;
import org.apache.lucene.geo.GeoUtils;
import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.SortedNumericDocValues;
import org.apache.lucene.search.ConstantScoreScorer;
import org.apache.lucene.search.ConstantScoreWeight;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryVisitor;
import org.apache.lucene.search.ScoreMode;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.TwoPhaseIterator;
import org.apache.lucene.search.Weight;
/** Distance query for {@link LatLonDocValuesField}. */
final class LatLonDocValuesDistanceQuery extends Query {
private final String field;
private final double latitude, longitude;
private final double radiusMeters;
LatLonDocValuesDistanceQuery(String field, double latitude, double longitude, double radiusMeters) {
if (Double.isFinite(radiusMeters) == false || radiusMeters < 0) {
throw new IllegalArgumentException("radiusMeters: '" + radiusMeters + "' is invalid");
}
GeoUtils.checkLatitude(latitude);
GeoUtils.checkLongitude(longitude);
if (field == null) {
throw new IllegalArgumentException("field must not be null");
}
this.field = field;
this.latitude = latitude;
this.longitude = longitude;
this.radiusMeters = radiusMeters;
}
@Override
public void visit(QueryVisitor visitor) {
if (visitor.acceptField(field)) {
visitor.visitLeaf(this);
}
}
@Override
public String toString(String field) {
StringBuilder sb = new StringBuilder();
if (!this.field.equals(field)) {
sb.append(this.field);
sb.append(':');
}
sb.append(latitude);
sb.append(",");
sb.append(longitude);
sb.append(" +/- ");
sb.append(radiusMeters);
sb.append(" meters");
return sb.toString();
}
@Override
public boolean equals(Object obj) {
if (sameClassAs(obj) == false) {
return false;
}
LatLonDocValuesDistanceQuery other = (LatLonDocValuesDistanceQuery) obj;
return field.equals(other.field) &&
Double.doubleToLongBits(latitude) == Double.doubleToLongBits(other.latitude) &&
Double.doubleToLongBits(longitude) == Double.doubleToLongBits(other.longitude) &&
Double.doubleToLongBits(radiusMeters) == Double.doubleToLongBits(other.radiusMeters);
}
@Override
public int hashCode() {
int h = classHash();
h = 31 * h + field.hashCode();
h = 31 * h + Double.hashCode(latitude);
h = 31 * h + Double.hashCode(longitude);
h = 31 * h + Double.hashCode(radiusMeters);
return h;
}
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
return new ConstantScoreWeight(this, boost) {
private final GeoEncodingUtils.DistancePredicate distancePredicate = GeoEncodingUtils.createDistancePredicate(latitude, longitude, radiusMeters);
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
final SortedNumericDocValues values = context.reader().getSortedNumericDocValues(field);
if (values == null) {
return null;
}
final TwoPhaseIterator iterator = new TwoPhaseIterator(values) {
@Override
public boolean matches() throws IOException {
for (int i = 0, count = values.docValueCount(); i < count; ++i) {
final long value = values.nextValue();
final int lat = (int) (value >>> 32);
final int lon = (int) (value & 0xFFFFFFFF);
if (distancePredicate.test(lat, lon)) {
return true;
}
}
return false;
}
@Override
public float matchCost() {
return 100f; // TODO: what should it be?
}
};
return new ConstantScoreScorer(this, boost, scoreMode, iterator);
}
@Override
public boolean isCacheable(LeafReaderContext ctx) {
return DocValues.isCacheable(ctx, field);
}
};
}
}

View File

@ -21,7 +21,10 @@ import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude; import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
import org.apache.lucene.geo.Circle;
import org.apache.lucene.geo.LatLonGeometry;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Rectangle;
import org.apache.lucene.index.DocValuesType; import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.search.FieldDoc; import org.apache.lucene.search.FieldDoc;
@ -177,7 +180,8 @@ public class LatLonDocValuesField extends Field {
* @throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or radius is invalid. * @throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or radius is invalid.
*/ */
public static Query newSlowDistanceQuery(String field, double latitude, double longitude, double radiusMeters) { public static Query newSlowDistanceQuery(String field, double latitude, double longitude, double radiusMeters) {
return new LatLonDocValuesDistanceQuery(field, latitude, longitude, radiusMeters); Circle circle = new Circle(latitude, longitude, radiusMeters);
return newSlowGeometryQuery(field, circle);
} }
/** /**
@ -192,6 +196,26 @@ public class LatLonDocValuesField extends Field {
* @throws IllegalArgumentException if {@code field} is null or polygons is empty or contain a null polygon. * @throws IllegalArgumentException if {@code field} is null or polygons is empty or contain a null polygon.
*/ */
public static Query newSlowPolygonQuery(String field, Polygon... polygons) { public static Query newSlowPolygonQuery(String field, Polygon... polygons) {
return new LatLonDocValuesPointInPolygonQuery(field, polygons); return newSlowGeometryQuery(field, polygons);
}
/**
* Create a query for matching points within the supplied geometries. Line geometries are not supported.
* This query is usually slow as it does not use an index structure and needs
* to verify documents one-by-one in order to know whether they match. It is
* best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
* {@link LatLonPoint#newGeometryQuery(String, LatLonGeometry...)}.
* @param field field name. must not be null.
* @param latLonGeometries array of LatLonGeometries. must not be null or empty.
* @return query matching points within the given polygons.
* @throws IllegalArgumentException if {@code field} is null, {@code latLonGeometries} is null, empty or contain a null or line geometry.
*/
public static Query newSlowGeometryQuery(String field, LatLonGeometry... latLonGeometries) {
if (latLonGeometries.length == 1 && latLonGeometries[0] instanceof Rectangle) {
LatLonGeometry geometry = latLonGeometries[0];
Rectangle rect = (Rectangle) geometry;
return newSlowBoxQuery(field, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
}
return new LatLonDocValuesPointInGeometryQuery(field, latLonGeometries);
} }
} }

View File

@ -17,13 +17,10 @@
package org.apache.lucene.document; package org.apache.lucene.document;
import java.io.IOException;
import java.util.Arrays;
import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.Component2D;
import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.geo.LatLonGeometry; import org.apache.lucene.geo.LatLonGeometry;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Line;
import org.apache.lucene.index.DocValues; import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.SortedNumericDocValues; import org.apache.lucene.index.SortedNumericDocValues;
@ -37,30 +34,36 @@ import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.TwoPhaseIterator; import org.apache.lucene.search.TwoPhaseIterator;
import org.apache.lucene.search.Weight; import org.apache.lucene.search.Weight;
/** Polygon query for {@link LatLonDocValuesField}. */ import java.io.IOException;
public class LatLonDocValuesPointInPolygonQuery extends Query { import java.util.Arrays;
/** Geometry query for {@link LatLonDocValuesField}. */
public class LatLonDocValuesPointInGeometryQuery extends Query {
private final String field; private final String field;
private final Polygon[] polygons; private final LatLonGeometry[] geometries;
LatLonDocValuesPointInPolygonQuery(String field, Polygon... polygons) { LatLonDocValuesPointInGeometryQuery(String field, LatLonGeometry... geometries) {
if (field == null) { if (field == null) {
throw new IllegalArgumentException("field must not be null"); throw new IllegalArgumentException("field must not be null");
} }
if (polygons == null) { if (geometries == null) {
throw new IllegalArgumentException("polygons must not be null"); throw new IllegalArgumentException("geometries must not be null");
} }
if (polygons.length == 0) { if (geometries.length == 0) {
throw new IllegalArgumentException("polygons must not be empty"); throw new IllegalArgumentException("geometries must not be empty");
} }
for (int i = 0; i < polygons.length; i++) { for (int i = 0; i < geometries.length; i++) {
if (polygons[i] == null) { if (geometries[i] == null) {
throw new IllegalArgumentException("polygon[" + i + "] must not be null"); throw new IllegalArgumentException("geometries[" + i + "] must not be null");
}
if (geometries[i] instanceof Line) {
throw new IllegalArgumentException("LatLonDocValuesPointInGeometryQuery does not support queries with line geometries");
} }
} }
this.field = field; this.field = field;
this.polygons = polygons; this.geometries = geometries;
} }
@Override @Override
@ -70,7 +73,7 @@ public class LatLonDocValuesPointInPolygonQuery extends Query {
sb.append(this.field); sb.append(this.field);
sb.append(':'); sb.append(':');
} }
sb.append("polygons(").append(Arrays.toString(polygons)); sb.append("geometries(").append(Arrays.toString(geometries));
return sb.append(")").toString(); return sb.append(")").toString();
} }
@ -79,16 +82,16 @@ public class LatLonDocValuesPointInPolygonQuery extends Query {
if (sameClassAs(obj) == false) { if (sameClassAs(obj) == false) {
return false; return false;
} }
LatLonDocValuesPointInPolygonQuery other = (LatLonDocValuesPointInPolygonQuery) obj; LatLonDocValuesPointInGeometryQuery other = (LatLonDocValuesPointInGeometryQuery) obj;
return field.equals(other.field) && return field.equals(other.field) &&
Arrays.equals(polygons, other.polygons); Arrays.equals(geometries, other.geometries);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int h = classHash(); int h = classHash();
h = 31 * h + field.hashCode(); h = 31 * h + field.hashCode();
h = 31 * h + Arrays.hashCode(polygons); h = 31 * h + Arrays.hashCode(geometries);
return h; return h;
} }
@ -104,8 +107,8 @@ public class LatLonDocValuesPointInPolygonQuery extends Query {
return new ConstantScoreWeight(this, boost) { return new ConstantScoreWeight(this, boost) {
final Component2D tree = LatLonGeometry.create(polygons); final Component2D tree = LatLonGeometry.create(geometries);
final GeoEncodingUtils.PolygonPredicate polygonPredicate = GeoEncodingUtils.createComponentPredicate(tree); final GeoEncodingUtils.Component2DPredicate component2DPredicate = GeoEncodingUtils.createComponentPredicate(tree);
@Override @Override
public Scorer scorer(LeafReaderContext context) throws IOException { public Scorer scorer(LeafReaderContext context) throws IOException {
@ -122,7 +125,7 @@ public class LatLonDocValuesPointInPolygonQuery extends Query {
final long value = values.nextValue(); final long value = values.nextValue();
final int lat = (int) (value >>> 32); final int lat = (int) (value >>> 32);
final int lon = (int) (value & 0xFFFFFFFF); final int lon = (int) (value & 0xFFFFFFFF);
if (polygonPredicate.test(lat, lon)) { if (component2DPredicate.test(lat, lon)) {
return true; return true;
} }
} }

View File

@ -16,7 +16,10 @@
*/ */
package org.apache.lucene.document; package org.apache.lucene.document;
import org.apache.lucene.geo.Circle;
import org.apache.lucene.geo.LatLonGeometry;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Rectangle;
import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.PointValues; import org.apache.lucene.index.PointValues;
import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanClause;
@ -49,6 +52,7 @@ import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
* <li>{@link #newBoxQuery newBoxQuery()} for matching points within a bounding box. * <li>{@link #newBoxQuery newBoxQuery()} for matching points within a bounding box.
* <li>{@link #newDistanceQuery newDistanceQuery()} for matching points within a specified distance. * <li>{@link #newDistanceQuery newDistanceQuery()} for matching points within a specified distance.
* <li>{@link #newPolygonQuery newPolygonQuery()} for matching points within an arbitrary polygon. * <li>{@link #newPolygonQuery newPolygonQuery()} for matching points within an arbitrary polygon.
* <li>{@link #newGeometryQuery newGeometryQuery()} for matching points within an arbitrary geometry collection.
* </ul> * </ul>
* <p> * <p>
* If you also need per-document operations such as sort by distance, add a separate {@link LatLonDocValuesField} instance. * If you also need per-document operations such as sort by distance, add a separate {@link LatLonDocValuesField} instance.
@ -251,7 +255,29 @@ public class LatLonPoint extends Field {
* @see Polygon * @see Polygon
*/ */
public static Query newPolygonQuery(String field, Polygon... polygons) { public static Query newPolygonQuery(String field, Polygon... polygons) {
return new LatLonPointInPolygonQuery(field, polygons); return newGeometryQuery(field, polygons);
}
/**
* Create a query for matching one or more geometries. Line geometries are not supported.
* @param field field name. must not be null.
* @param latLonGeometries array of LatLonGeometries. must not be null or empty.
* @return query matching points within at least one geometry.
* @throws IllegalArgumentException if {@code field} is null, {@code latLonGeometries} is null, empty or contain a null or line geometry.
* @see LatLonGeometry
*/
public static Query newGeometryQuery(String field, LatLonGeometry... latLonGeometries) {
if (latLonGeometries.length == 1) {
if (latLonGeometries[0] instanceof Rectangle) {
final Rectangle rect = (Rectangle) latLonGeometries[0];
return newBoxQuery(field, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
}
if (latLonGeometries[0] instanceof Circle) {
final Circle circle = (Circle) latLonGeometries[0];
return newDistanceQuery(field, circle.getLat(), circle.getLon(), circle.getRadius());
}
}
return new LatLonPointInGeometryQuery(field, latLonGeometries);
} }
/** /**

View File

@ -16,13 +16,10 @@
*/ */
package org.apache.lucene.document; package org.apache.lucene.document;
import java.io.IOException;
import java.util.Arrays;
import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.Component2D;
import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.geo.LatLonGeometry; import org.apache.lucene.geo.LatLonGeometry;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Line;
import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.LeafReaderContext;
@ -42,39 +39,44 @@ import org.apache.lucene.search.Weight;
import org.apache.lucene.util.DocIdSetBuilder; import org.apache.lucene.util.DocIdSetBuilder;
import org.apache.lucene.util.NumericUtils; import org.apache.lucene.util.NumericUtils;
import java.io.IOException;
import java.util.Arrays;
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude; import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude; import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude; import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
/** Finds all previously indexed points that fall within the specified polygons. /** Finds all previously indexed points that fall within the specified geometries.
* *
* <p>The field must be indexed with using {@link org.apache.lucene.document.LatLonPoint} added per document. * <p>The field must be indexed with using {@link LatLonPoint} added per document.
* *
* @lucene.experimental */ * @lucene.experimental */
final class LatLonPointInPolygonQuery extends Query { final class LatLonPointInGeometryQuery extends Query {
final String field; final String field;
final Polygon[] polygons; final LatLonGeometry[] geometries;
LatLonPointInPolygonQuery(String field, Polygon[] polygons) { LatLonPointInGeometryQuery(String field, LatLonGeometry[] geometries) {
if (field == null) { if (field == null) {
throw new IllegalArgumentException("field must not be null"); throw new IllegalArgumentException("field must not be null");
} }
if (polygons == null) { if (geometries == null) {
throw new IllegalArgumentException("polygons must not be null"); throw new IllegalArgumentException("geometries must not be null");
} }
if (polygons.length == 0) { if (geometries.length == 0) {
throw new IllegalArgumentException("polygons must not be empty"); throw new IllegalArgumentException("geometries must not be empty");
} }
for (int i = 0; i < polygons.length; i++) { for (int i = 0; i < geometries.length; i++) {
if (polygons[i] == null) { if (geometries[i] == null) {
throw new IllegalArgumentException("polygon[" + i + "] must not be null"); throw new IllegalArgumentException("geometries[" + i + "] must not be null");
}
if (geometries[i] instanceof Line) {
throw new IllegalArgumentException("LatLonPointInGeometryQuery does not support queries with line geometries");
} }
} }
this.field = field; this.field = field;
this.polygons = polygons.clone(); this.geometries = geometries.clone();
// TODO: we could also compute the maximal inner bounding box, to make relations faster to compute?
} }
@Override @Override
@ -84,7 +86,7 @@ final class LatLonPointInPolygonQuery extends Query {
} }
} }
private IntersectVisitor getIntersectVisitor(DocIdSetBuilder result, Component2D tree, GeoEncodingUtils.PolygonPredicate polygonPredicate, private IntersectVisitor getIntersectVisitor(DocIdSetBuilder result, Component2D tree, GeoEncodingUtils.Component2DPredicate component2DPredicate,
byte[] minLat, byte[] maxLat, byte[] minLon, byte[] maxLon) { byte[] minLat, byte[] maxLat, byte[] minLon, byte[] maxLon) {
return new IntersectVisitor() { return new IntersectVisitor() {
DocIdSetBuilder.BulkAdder adder; DocIdSetBuilder.BulkAdder adder;
@ -101,7 +103,7 @@ final class LatLonPointInPolygonQuery extends Query {
@Override @Override
public void visit(int docID, byte[] packedValue) { public void visit(int docID, byte[] packedValue) {
if (polygonPredicate.test(NumericUtils.sortableBytesToInt(packedValue, 0), if (component2DPredicate.test(NumericUtils.sortableBytesToInt(packedValue, 0),
NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES))) { NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES))) {
visit(docID); visit(docID);
} }
@ -109,7 +111,7 @@ final class LatLonPointInPolygonQuery extends Query {
@Override @Override
public void visit(DocIdSetIterator iterator, byte[] packedValue) throws IOException { public void visit(DocIdSetIterator iterator, byte[] packedValue) throws IOException {
if (polygonPredicate.test(NumericUtils.sortableBytesToInt(packedValue, 0), if (component2DPredicate.test(NumericUtils.sortableBytesToInt(packedValue, 0),
NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES))) { NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES))) {
int docID; int docID;
while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
@ -141,9 +143,9 @@ final class LatLonPointInPolygonQuery extends Query {
@Override @Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException { public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
final Component2D tree = LatLonGeometry.create(polygons); final Component2D tree = LatLonGeometry.create(geometries);
final GeoEncodingUtils.PolygonPredicate polygonPredicate = GeoEncodingUtils.createComponentPredicate(tree); final GeoEncodingUtils.Component2DPredicate component2DPredicate = GeoEncodingUtils.createComponentPredicate(tree);
// bounding box over all polygons, this can speed up tree intersection/cheaply improve approximation for complex multi-polygons // bounding box over all geometries, this can speed up tree intersection/cheaply improve approximation for complex multi-geometries
final byte minLat[] = new byte[Integer.BYTES]; final byte minLat[] = new byte[Integer.BYTES];
final byte maxLat[] = new byte[Integer.BYTES]; final byte maxLat[] = new byte[Integer.BYTES];
final byte minLon[] = new byte[Integer.BYTES]; final byte minLon[] = new byte[Integer.BYTES];
@ -175,7 +177,7 @@ final class LatLonPointInPolygonQuery extends Query {
long cost = -1; long cost = -1;
DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field); DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field);
final IntersectVisitor visitor = getIntersectVisitor(result, tree, polygonPredicate, minLat, maxLat, minLon, maxLon); final IntersectVisitor visitor = getIntersectVisitor(result, tree, component2DPredicate, minLat, maxLat, minLon, maxLon);
@Override @Override
public Scorer get(long leadCost) throws IOException { public Scorer get(long leadCost) throws IOException {
@ -217,9 +219,9 @@ final class LatLonPointInPolygonQuery extends Query {
return field; return field;
} }
/** Returns a copy of the internal polygon array */ /** Returns a copy of the internal geometry array */
public Polygon[] getPolygons() { public LatLonGeometry[] getGeometries() {
return polygons.clone(); return geometries.clone();
} }
@Override @Override
@ -227,7 +229,7 @@ final class LatLonPointInPolygonQuery extends Query {
final int prime = 31; final int prime = 31;
int result = classHash(); int result = classHash();
result = prime * result + field.hashCode(); result = prime * result + field.hashCode();
result = prime * result + Arrays.hashCode(polygons); result = prime * result + Arrays.hashCode(geometries);
return result; return result;
} }
@ -237,9 +239,9 @@ final class LatLonPointInPolygonQuery extends Query {
equalsTo(getClass().cast(other)); equalsTo(getClass().cast(other));
} }
private boolean equalsTo(LatLonPointInPolygonQuery other) { private boolean equalsTo(LatLonPointInGeometryQuery other) {
return field.equals(other.field) && return field.equals(other.field) &&
Arrays.equals(polygons, other.polygons); Arrays.equals(geometries, other.geometries);
} }
@Override @Override
@ -252,7 +254,7 @@ final class LatLonPointInPolygonQuery extends Query {
sb.append(this.field); sb.append(this.field);
sb.append(':'); sb.append(':');
} }
sb.append(Arrays.toString(polygons)); sb.append(Arrays.toString(geometries));
return sb.toString(); return sb.toString();
} }
} }

View File

@ -178,37 +178,23 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
EncodedRectangle(double minLat, double maxLat, double minLon, double maxLon) { EncodedRectangle(double minLat, double maxLat, double minLon, double maxLon) {
this.bbox = new byte[4 * BYTES]; this.bbox = new byte[4 * BYTES];
int minXenc = encodeLongitudeCeil(minLon); if (minLon == 180.0 && minLon > maxLon) {
int maxXenc = encodeLongitude(maxLon); minLon = -180;
int minYenc = encodeLatitudeCeil(minLat);
int maxYenc = encodeLatitude(maxLat);
if (minYenc > maxYenc) {
minYenc = maxYenc;
} }
this.minY = minYenc; this.minX = encodeLongitudeCeil(minLon);
this.maxY = maxYenc; this.maxX = encodeLongitude(maxLon);
this.minY = encodeLatitudeCeil(minLat);
if (minLon > maxLon == true) { this.maxY = encodeLatitude(maxLat);
this.crossesDateline = true; this.crossesDateline = minLon > maxLon;
if (this.crossesDateline) {
// crossing dateline is split into east/west boxes // crossing dateline is split into east/west boxes
this.west = new byte[4 * BYTES]; this.west = new byte[4 * BYTES];
this.minX = minXenc;
this.maxX = maxXenc;
encode(MIN_LON_ENCODED, this.maxX, this.minY, this.maxY, this.west); encode(MIN_LON_ENCODED, this.maxX, this.minY, this.maxY, this.west);
encode(this.minX, MAX_LON_ENCODED, this.minY, this.maxY, this.bbox); encode(this.minX, MAX_LON_ENCODED, this.minY, this.maxY, this.bbox);
} else { } else {
this.crossesDateline = false;
// encodeLongitudeCeil may cause minX to be > maxX iff
// the delta between the longitude < the encoding resolution
if (minXenc > maxXenc) {
minXenc = maxXenc;
}
this.west = null; this.west = null;
this.minX = minXenc;
this.maxX = maxXenc;
encode(this.minX, this.maxX, this.minY, this.maxY, bbox); encode(this.minX, this.maxX, this.minY, this.maxY, bbox);
} }
} }
/** /**

View File

@ -35,9 +35,6 @@ public final class Circle extends LatLonGeometry {
private final double lon; private final double lon;
/** radius in meters */ /** radius in meters */
private final double radiusMeters; private final double radiusMeters;
/** Max radius allowed, half of the earth mean radius.*/
public static double MAX_RADIUS = GeoUtils.EARTH_MEAN_RADIUS_METERS / 2.0;
/** /**
* Creates a new circle from the supplied latitude/longitude center and a radius in meters.. * Creates a new circle from the supplied latitude/longitude center and a radius in meters..
@ -45,11 +42,8 @@ public final class Circle extends LatLonGeometry {
public Circle(double lat, double lon, double radiusMeters) { public Circle(double lat, double lon, double radiusMeters) {
GeoUtils.checkLatitude(lat); GeoUtils.checkLatitude(lat);
GeoUtils.checkLongitude(lon); GeoUtils.checkLongitude(lon);
if (radiusMeters <= 0) { if (Double.isFinite(radiusMeters) == false || radiusMeters < 0) {
throw new IllegalArgumentException("radius must be bigger than 0, got " + radiusMeters); throw new IllegalArgumentException("radiusMeters: '" + radiusMeters + "' is invalid");
}
if (radiusMeters < MAX_RADIUS == false) {
throw new IllegalArgumentException("radius must be lower than " + MAX_RADIUS + ", got " + radiusMeters);
} }
this.lat = lat; this.lat = lat;
this.lon = lon; this.lon = lon;

View File

@ -178,16 +178,16 @@ public final class GeoEncodingUtils {
lat, lon, distanceSortKey); lat, lon, distanceSortKey);
} }
/** Create a predicate that checks whether points are within a polygon. /** Create a predicate that checks whether points are within a geometry.
* It works the same way as {@link #createDistancePredicate}. * It works the same way as {@link #createDistancePredicate}.
* @lucene.internal */ * @lucene.internal */
public static PolygonPredicate createComponentPredicate(Component2D tree) { public static Component2DPredicate createComponentPredicate(Component2D tree) {
final Rectangle boundingBox = new Rectangle(tree.getMinY(), tree.getMaxY(), tree.getMinX(), tree.getMaxX()); final Rectangle boundingBox = new Rectangle(tree.getMinY(), tree.getMaxY(), tree.getMinX(), tree.getMaxX());
final Function<Rectangle, Relation> boxToRelation = box -> tree.relate( final Function<Rectangle, Relation> boxToRelation = box -> tree.relate(
box.minLon, box.maxLon, box.minLat, box.maxLat); box.minLon, box.maxLon, box.minLat, box.maxLat);
final Grid subBoxes = createSubBoxes(boundingBox, boxToRelation); final Grid subBoxes = createSubBoxes(boundingBox, boxToRelation);
return new PolygonPredicate( return new Component2DPredicate(
subBoxes.latShift, subBoxes.lonShift, subBoxes.latShift, subBoxes.lonShift,
subBoxes.latBase, subBoxes.lonBase, subBoxes.latBase, subBoxes.lonBase,
subBoxes.maxLatDelta, subBoxes.maxLonDelta, subBoxes.maxLatDelta, subBoxes.maxLonDelta,
@ -342,12 +342,12 @@ public final class GeoEncodingUtils {
} }
} }
/** A predicate that checks whether a given point is within a polygon. */ /** A predicate that checks whether a given point is within a component2D geometry. */
public static class PolygonPredicate extends Grid { public static class Component2DPredicate extends Grid {
private final Component2D tree; private final Component2D tree;
private PolygonPredicate( private Component2DPredicate(
int latShift, int lonShift, int latShift, int lonShift,
int latBase, int lonBase, int latBase, int lonBase,
int maxLatDelta, int maxLonDelta, int maxLatDelta, int maxLonDelta,

View File

@ -232,28 +232,25 @@ final class Rectangle2D implements Component2D {
/** create a component2D from the provided LatLon rectangle */ /** create a component2D from the provided LatLon rectangle */
static Component2D create(Rectangle rectangle) { static Component2D create(Rectangle rectangle) {
// behavior of LatLonPoint.newBoxQuery()
double minLongitude = rectangle.minLon;
boolean crossesDateline = rectangle.minLon > rectangle.maxLon;
if (minLongitude == 180.0 && crossesDateline) {
minLongitude = -180;
crossesDateline = false;
}
// need to quantize! // need to quantize!
double qMinLat = decodeLatitude(encodeLatitudeCeil(rectangle.minLat)); double qMinLat = decodeLatitude(encodeLatitudeCeil(rectangle.minLat));
double qMaxLat = decodeLatitude(encodeLatitude(rectangle.maxLat)); double qMaxLat = decodeLatitude(encodeLatitude(rectangle.maxLat));
double qMinLon = decodeLongitude(encodeLongitudeCeil(rectangle.minLon)); double qMinLon = decodeLongitude(encodeLongitudeCeil(minLongitude));
double qMaxLon = decodeLongitude(encodeLongitude(rectangle.maxLon)); double qMaxLon = decodeLongitude(encodeLongitude(rectangle.maxLon));
if (qMinLat > qMaxLat) { if (crossesDateline) {
// encodeLatitudeCeil may cause minY to be > maxY iff
// the delta between the longitude < the encoding resolution
qMinLat = qMaxLat;
}
if (rectangle.minLon > rectangle.maxLon) {
// for rectangles that cross the dateline we need to create two components // for rectangles that cross the dateline we need to create two components
Component2D[] components = new Component2D[2]; Component2D[] components = new Component2D[2];
components[0] = new Rectangle2D(MIN_LON_INCL_QUANTIZE, qMaxLon, qMinLat, qMaxLat); components[0] = new Rectangle2D(MIN_LON_INCL_QUANTIZE, qMaxLon, qMinLat, qMaxLat);
components[1] = new Rectangle2D(qMinLon, MAX_LON_INCL_QUANTIZE, qMinLat, qMaxLat); components[1] = new Rectangle2D(qMinLon, MAX_LON_INCL_QUANTIZE, qMinLat, qMaxLat);
return ComponentTree.create(components); return ComponentTree.create(components);
} else { } else {
// encodeLongitudeCeil may cause minX to be > maxX iff
// the delta between the longitude < the encoding resolution
if (qMinLon > qMaxLon) {
qMinLon = qMaxLon;
}
return new Rectangle2D(qMinLon, qMaxLon, qMinLat, qMaxLat); return new Rectangle2D(qMinLon, qMaxLon, qMinLat, qMaxLat);
} }
} }

View File

@ -22,6 +22,7 @@ import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.Component2D;
import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.GeoTestUtil;
import org.apache.lucene.geo.GeoUtils;
import org.apache.lucene.geo.LatLonGeometry; import org.apache.lucene.geo.LatLonGeometry;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
@ -109,7 +110,8 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
@Override @Override
protected Circle nextCircle() { protected Circle nextCircle() {
return new Circle(nextLatitude(), nextLongitude(), random().nextDouble() * Circle.MAX_RADIUS); final double radiusMeters = random().nextDouble() * GeoUtils.EARTH_MEAN_RADIUS_METERS * Math.PI / 2.0 + 1.0;
return new Circle(nextLatitude(), nextLongitude(), radiusMeters);
} }
@Override @Override

View File

@ -326,34 +326,23 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
assertEquals(docID, docIDToID.nextDoc()); assertEquals(docID, docIDToID.nextDoc());
int id = (int) docIDToID.longValue(); int id = (int) docIDToID.longValue();
boolean expected; boolean expected;
double qMinLon = ENCODER.quantizeXCeil(rectMinX(rect)); double minLon = rectMinX(rect);
double qMaxLon = ENCODER.quantizeX(rectMaxX(rect)); double maxLon = rectMaxX(rect);
double qMinLat = ENCODER.quantizeYCeil(rectMinY(rect)); double minLat = rectMinY(rect);
double qMaxLat = ENCODER.quantizeY(rectMaxY(rect)); double maxLat = rectMaxY(rect);
if (liveDocs != null && liveDocs.get(docID) == false) { if (liveDocs != null && liveDocs.get(docID) == false) {
// document is deleted // document is deleted
expected = false; expected = false;
} else if (shapes[id] == null) { } else if (shapes[id] == null) {
expected = false; expected = false;
} else { } else {
if (qMinLat > qMaxLat) {
qMinLat = ENCODER.quantizeY(rectMaxY(rect));
}
if (queryRelation == QueryRelation.CONTAINS && rectCrossesDateline(rect)) { if (queryRelation == QueryRelation.CONTAINS && rectCrossesDateline(rect)) {
//For contains we need to call the validator for each section. It is only expected // For contains we need to call the validator for each section.
//if both sides are contained. // It is only expected if both sides are contained.
expected = VALIDATOR.setRelation(queryRelation).testBBoxQuery(qMinLat, qMaxLat, qMinLon, GeoUtils.MAX_LON_INCL, shapes[id]); expected = VALIDATOR.setRelation(queryRelation).testBBoxQuery(minLat, maxLat, minLon, GeoUtils.MAX_LON_INCL, shapes[id]) &&
if (expected) { VALIDATOR.setRelation(queryRelation).testBBoxQuery(minLat, maxLat, GeoUtils.MIN_LON_INCL, maxLon, shapes[id]);
expected = VALIDATOR.setRelation(queryRelation).testBBoxQuery(qMinLat, qMaxLat, GeoUtils.MIN_LON_INCL, qMaxLon, shapes[id]);
}
} else { } else {
// check quantized poly against quantized query expected = VALIDATOR.setRelation(queryRelation).testBBoxQuery(minLat, maxLat, minLon, maxLon, shapes[id]);
if (qMinLon > qMaxLon && rectCrossesDateline(rect) == false) {
// if the quantization creates a false dateline crossing (because of encodeCeil):
// then do not use encodeCeil
qMinLon = ENCODER.quantizeX(rectMinX(rect));
}
expected = VALIDATOR.setRelation(queryRelation).testBBoxQuery(qMinLat, qMaxLat, qMinLon, qMaxLon, shapes[id]);
} }
} }
@ -373,7 +362,7 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
b.append(" shape=" + shapes[id] + "\n"); b.append(" shape=" + shapes[id] + "\n");
} }
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false)); b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
b.append(" rect=Rectangle(lat=" + ENCODER.quantizeYCeil(rectMinY(rect)) + " TO " + ENCODER.quantizeY(rectMaxY(rect)) + " lon=" + qMinLon + " TO " + ENCODER.quantizeX(rectMaxX(rect)) + ")\n"); b.append(" rect=Rectangle(lat=" + ENCODER.quantizeYCeil(rectMinY(rect)) + " TO " + ENCODER.quantizeY(rectMaxY(rect)) + " lon=" + minLon + " TO " + ENCODER.quantizeX(rectMaxX(rect)) + ")\n");
if (true) { if (true) {
fail("wrong hit (first of possibly more):\n\n" + b); fail("wrong hit (first of possibly more):\n\n" + b);
} else { } else {

View File

@ -20,7 +20,9 @@ import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.Component2D;
import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.GeoTestUtil;
import org.apache.lucene.geo.LatLonGeometry;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Rectangle;
/** random bounding box, line, and polygon query tests for random generated {@code latitude, longitude} points */ /** random bounding box, line, and polygon query tests for random generated {@code latitude, longitude} points */
public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase { public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase {
@ -73,21 +75,8 @@ public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase {
@Override @Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
if (queryRelation == QueryRelation.CONTAINS) { Component2D rectangle2D = LatLonGeometry.create(new Rectangle(minLat, maxLat, minLon, maxLon));
return false; return testComponentQuery(rectangle2D, shape);
}
Point p = (Point)shape;
double lat = encoder.quantizeY(p.lat);
double lon = encoder.quantizeX(p.lon);
boolean isDisjoint = lat < minLat || lat > maxLat;
isDisjoint = isDisjoint || ((minLon > maxLon)
? lon < minLon && lon > maxLon
: lon < minLon || lon > maxLon);
if (queryRelation == QueryRelation.DISJOINT) {
return isDisjoint;
}
return isDisjoint == false;
} }
@Override @Override

View File

@ -22,6 +22,7 @@ import org.apache.lucene.geo.Circle;
import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.Component2D;
import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.GeoTestUtil;
import org.apache.lucene.geo.GeoUtils;
import org.apache.lucene.geo.LatLonGeometry; import org.apache.lucene.geo.LatLonGeometry;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
@ -433,7 +434,7 @@ public class TestLatLonShape extends LuceneTestCase {
IndexSearcher s = newSearcher(r); IndexSearcher s = newSearcher(r);
// search by same point // search by same point
Query q = LatLonShape.newBoxQuery(FIELDNAME, QueryRelation.INTERSECTS, p.lat, p.lat, p.lon, p.lon); Query q = LatLonShape.newPointQuery(FIELDNAME, QueryRelation.INTERSECTS, new double[] {p.lat, p.lon});
assertEquals(1, s.count(q)); assertEquals(1, s.count(q));
IOUtils.close(r, dir); IOUtils.close(r, dir);
} }
@ -777,10 +778,7 @@ public class TestLatLonShape extends LuceneTestCase {
double lat = GeoTestUtil.nextLatitude(); double lat = GeoTestUtil.nextLatitude();
double lon = GeoTestUtil.nextLongitude(); double lon = GeoTestUtil.nextLongitude();
double radiusMeters = random().nextDouble() * Circle.MAX_RADIUS; final double radiusMeters = random().nextDouble() * GeoUtils.EARTH_MEAN_RADIUS_METERS * Math.PI / 2.0 + 1.0;
while (radiusMeters == 0 || radiusMeters == Circle.MAX_RADIUS) {
radiusMeters = random().nextDouble() * Circle.MAX_RADIUS;
}
Circle circle = new Circle(lat, lon, radiusMeters); Circle circle = new Circle(lat, lon, radiusMeters);
Component2D circle2D = LatLonGeometry.create(circle); Component2D circle2D = LatLonGeometry.create(circle);
int expected; int expected;

View File

@ -41,15 +41,15 @@ public class TestCircle extends LuceneTestCase {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new Circle(43.5, 45.23, -1000); new Circle(43.5, 45.23, -1000);
}); });
assertTrue(expected.getMessage().contains("radius must be bigger than 0, got -1000.0")); assertTrue(expected.getMessage().contains("radiusMeters: '-1000.0' is invalid"));
} }
/** radius must be lower than 3185504.3857 */ /** radius must cannot be infinite */
public void testInfiniteRadius() { public void testInfiniteRadius() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new Circle(43.5, 45.23, Double.POSITIVE_INFINITY); new Circle(43.5, 45.23, Double.POSITIVE_INFINITY);
}); });
assertTrue(expected.getMessage().contains("radius must be lower than 3185504.3857, got Infinity")); assertTrue(expected.getMessage().contains("radiusMeters: 'Infinity' is invalid"));
} }
/** equals and hashcode */ /** equals and hashcode */

View File

@ -20,15 +20,11 @@ import org.apache.lucene.document.Document;
import org.apache.lucene.document.LatLonDocValuesField; import org.apache.lucene.document.LatLonDocValuesField;
import org.apache.lucene.geo.BaseGeoPointTestCase; import org.apache.lucene.geo.BaseGeoPointTestCase;
import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.geo.LatLonGeometry;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
public class TestLatLonDocValuesQueries extends BaseGeoPointTestCase { public class TestLatLonDocValuesQueries extends BaseGeoPointTestCase {
@Override
protected boolean supportsPolygons() {
return true;
}
@Override @Override
protected void addPointToDoc(String field, Document doc, double lat, double lon) { protected void addPointToDoc(String field, Document doc, double lat, double lon) {
doc.add(new LatLonDocValuesField(field, lat, lon)); doc.add(new LatLonDocValuesField(field, lat, lon));
@ -49,6 +45,11 @@ public class TestLatLonDocValuesQueries extends BaseGeoPointTestCase {
return LatLonDocValuesField.newSlowPolygonQuery(field, polygons); return LatLonDocValuesField.newSlowPolygonQuery(field, polygons);
} }
@Override
protected Query newGeometryQuery(String field, LatLonGeometry... geometry) {
return LatLonDocValuesField.newSlowGeometryQuery(field, geometry);
}
@Override @Override
protected double quantizeLat(double latRaw) { protected double quantizeLat(double latRaw) {
return GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(latRaw)); return GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(latRaw));

View File

@ -22,6 +22,7 @@ import org.apache.lucene.document.Document;
import org.apache.lucene.document.LatLonPoint; import org.apache.lucene.document.LatLonPoint;
import org.apache.lucene.geo.BaseGeoPointTestCase; import org.apache.lucene.geo.BaseGeoPointTestCase;
import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.geo.LatLonGeometry;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexReader;
@ -51,6 +52,11 @@ public class TestLatLonPointQueries extends BaseGeoPointTestCase {
return LatLonPoint.newPolygonQuery(field, polygons); return LatLonPoint.newPolygonQuery(field, polygons);
} }
@Override
protected Query newGeometryQuery(String field, LatLonGeometry... geometry) {
return LatLonPoint.newGeometryQuery(field, geometry);
}
@Override @Override
protected double quantizeLat(double latRaw) { protected double quantizeLat(double latRaw) {
return GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(latRaw)); return GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(latRaw));

View File

@ -24,6 +24,7 @@ import java.util.BitSet;
import java.util.HashSet; import java.util.HashSet;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
import org.apache.lucene.analysis.MockAnalyzer; import org.apache.lucene.analysis.MockAnalyzer;
import org.apache.lucene.codecs.Codec; import org.apache.lucene.codecs.Codec;
@ -97,13 +98,33 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
return org.apache.lucene.geo.GeoTestUtil.nextBox(); return org.apache.lucene.geo.GeoTestUtil.nextBox();
} }
protected Circle nextCircle() {
return org.apache.lucene.geo.GeoTestUtil.nextCircle();
}
protected Polygon nextPolygon() { protected Polygon nextPolygon() {
return org.apache.lucene.geo.GeoTestUtil.nextPolygon(); return org.apache.lucene.geo.GeoTestUtil.nextPolygon();
} }
/** Whether this impl supports polygons. */ protected LatLonGeometry[] nextGeometry() {
protected boolean supportsPolygons() { final int length = random().nextInt(4) + 1;
return true; final LatLonGeometry[] geometries = new LatLonGeometry[length];
for (int i = 0; i < length; i++) {
final LatLonGeometry geometry;
switch (random().nextInt(3)) {
case 0:
geometry = nextBox();
break;
case 1:
geometry = nextCircle();
break;
default:
geometry = nextPolygon();
break;
}
geometries[i] = geometry;
}
return geometries;
} }
/** Valid values that should not cause exception */ /** Valid values that should not cause exception */
@ -291,7 +312,6 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
/** test we can search for a polygon */ /** test we can search for a polygon */
public void testPolygonBasics() throws Exception { public void testPolygonBasics() throws Exception {
assumeTrue("Impl does not support polygons", supportsPolygons());
Directory dir = newDirectory(); Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir); RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
@ -314,7 +334,6 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
/** test we can search for a polygon with a hole (but still includes the doc) */ /** test we can search for a polygon with a hole (but still includes the doc) */
public void testPolygonHole() throws Exception { public void testPolygonHole() throws Exception {
assumeTrue("Impl does not support polygons", supportsPolygons());
Directory dir = newDirectory(); Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir); RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
@ -339,7 +358,6 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
/** test we can search for a polygon with a hole (that excludes the doc) */ /** test we can search for a polygon with a hole (that excludes the doc) */
public void testPolygonHoleExcludes() throws Exception { public void testPolygonHoleExcludes() throws Exception {
assumeTrue("Impl does not support polygons", supportsPolygons());
Directory dir = newDirectory(); Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir); RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
@ -364,7 +382,6 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
/** test we can search for a multi-polygon */ /** test we can search for a multi-polygon */
public void testMultiPolygonBasics() throws Exception { public void testMultiPolygonBasics() throws Exception {
assumeTrue("Impl does not support polygons", supportsPolygons());
Directory dir = newDirectory(); Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir); RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
@ -389,7 +406,6 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
/** null field name not allowed */ /** null field name not allowed */
public void testPolygonNullField() { public void testPolygonNullField() {
assumeTrue("Impl does not support polygons", supportsPolygons());
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
newPolygonQuery(null, new Polygon( newPolygonQuery(null, new Polygon(
new double[] { 18, 18, 19, 19, 18 }, new double[] { 18, 18, 19, 19, 18 },
@ -583,26 +599,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
Query query = newRectQuery(FIELD_NAME, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon); Query query = newRectQuery(FIELD_NAME, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
final FixedBitSet hits = new FixedBitSet(r.maxDoc()); final FixedBitSet hits = searchIndex(s, query, r.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) {
hits.set(docBase+doc);
}
});
boolean fail = false; boolean fail = false;
@ -743,6 +740,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
protected abstract Query newPolygonQuery(String field, Polygon... polygon); protected abstract Query newPolygonQuery(String field, Polygon... polygon);
protected abstract Query newGeometryQuery(String field, LatLonGeometry... geometry);
static final boolean rectContainsPoint(Rectangle rect, double pointLat, double pointLon) { static final boolean rectContainsPoint(Rectangle rect, double pointLat, double pointLon) {
assert Double.isNaN(pointLat) == false; assert Double.isNaN(pointLat) == false;
@ -773,9 +772,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
} }
verifyRandomRectangles(lats, lons); verifyRandomRectangles(lats, lons);
verifyRandomDistances(lats, lons); verifyRandomDistances(lats, lons);
if (supportsPolygons()) { verifyRandomPolygons(lats, lons);
verifyRandomPolygons(lats, lons); verifyRandomGeometries(lats, lons);
}
} }
protected void verifyRandomRectangles(double[] lats, double[] lons) throws Exception { protected void verifyRandomRectangles(double[] lats, double[] lons) throws Exception {
@ -797,27 +795,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
Set<Integer> deleted = new HashSet<>(); Set<Integer> deleted = new HashSet<>();
// RandomIndexWriter is too slow here: // RandomIndexWriter is too slow here:
IndexWriter w = new IndexWriter(dir, iwc); IndexWriter w = new IndexWriter(dir, iwc);
for(int id=0;id<lats.length;id++) { indexPoints(lats, lons, deleted, w);
Document doc = new Document();
doc.add(newStringField("id", ""+id, Field.Store.NO));
doc.add(new NumericDocValuesField("id", id));
if (Double.isNaN(lats[id]) == false) {
addPointToDoc(FIELD_NAME, doc, lats[id], lons[id]);
}
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);
}
final IndexReader r = DirectoryReader.open(w); final IndexReader r = DirectoryReader.open(w);
w.close(); w.close();
@ -842,26 +821,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
System.out.println(" query=" + query); System.out.println(" query=" + query);
} }
final FixedBitSet hits = new FixedBitSet(maxDoc); final FixedBitSet hits = searchIndex(s, query, 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) {
hits.set(docBase+doc);
}
});
boolean fail = false; boolean fail = false;
NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id"); NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id");
@ -879,24 +839,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
} }
if (hits.get(docID) != expected) { if (hits.get(docID) != expected) {
StringBuilder b = new StringBuilder(); buildError(docID, expected, id, lats, lons, query, liveDocs, (b) -> b.append(" rect=").append(rect));
b.append("docID=(").append(docID).append(")\n"); fail = true;
if (expected) {
b.append("FAIL: id=").append(id).append(" should match but did not\n");
} else {
b.append("FAIL: id=").append(id).append(" should not match but did\n");
}
b.append(" box=").append(rect).append("\n");
b.append(" query=").append(query).append(" docID=").append(docID).append("\n");
b.append(" lat=").append(lats[id]).append(" lon=").append(lons[id]).append("\n");
b.append(" deleted?=").append(liveDocs != null && liveDocs.get(docID) == false);
if (true) {
fail("wrong hit (first of possibly more):\n\n" + b);
} else {
System.out.println(b.toString());
fail = true;
}
} }
} }
if (fail) { if (fail) {
@ -926,27 +870,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
Set<Integer> deleted = new HashSet<>(); Set<Integer> deleted = new HashSet<>();
// RandomIndexWriter is too slow here: // RandomIndexWriter is too slow here:
IndexWriter w = new IndexWriter(dir, iwc); IndexWriter w = new IndexWriter(dir, iwc);
for(int id=0;id<lats.length;id++) { indexPoints(lats, lons, deleted, w);
Document doc = new Document();
doc.add(newStringField("id", ""+id, Field.Store.NO));
doc.add(new NumericDocValuesField("id", id));
if (Double.isNaN(lats[id]) == false) {
addPointToDoc(FIELD_NAME, doc, lats[id], lons[id]);
}
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);
}
final IndexReader r = DirectoryReader.open(w); final IndexReader r = DirectoryReader.open(w);
w.close(); w.close();
@ -981,26 +906,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
System.out.println(" query=" + query); System.out.println(" query=" + query);
} }
final FixedBitSet hits = new FixedBitSet(maxDoc); final FixedBitSet hits = searchIndex(s, query, 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) {
hits.set(docBase+doc);
}
});
boolean fail = false; boolean fail = false;
NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id"); NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id");
@ -1018,26 +924,14 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
} }
if (hits.get(docID) != expected) { if (hits.get(docID) != expected) {
StringBuilder b = new StringBuilder(); Consumer<StringBuilder> explain = (b) -> {
if (Double.isNaN(lats[id]) == false) {
if (expected) { double distanceMeters = SloppyMath.haversinMeters(centerLat, centerLon, lats[id], lons[id]);
b.append("FAIL: id=").append(id).append(" should match but did not\n"); b.append(" centerLat=").append(centerLat).append(" centerLon=").append(centerLon).append(" distanceMeters=").append(distanceMeters).append(" vs radiusMeters=").append(radiusMeters);
} else { }
b.append("FAIL: id=").append(id).append(" should not match but did\n"); };
} buildError(docID, expected, id, lats, lons, query, liveDocs, explain);
b.append(" query=").append(query).append(" docID=").append(docID).append("\n"); fail = true;
b.append(" lat=").append(lats[id]).append(" lon=").append(lons[id]).append("\n");
b.append(" deleted?=").append(liveDocs != null && liveDocs.get(docID) == false);
if (Double.isNaN(lats[id]) == false) {
double distanceMeters = SloppyMath.haversinMeters(centerLat, centerLon, lats[id], lons[id]);
b.append(" centerLat=").append(centerLat).append(" centerLon=").append(centerLon).append(" distanceMeters=").append(distanceMeters).append(" vs radiusMeters=").append(radiusMeters);
}
if (true) {
fail("wrong hit (first of possibly more):\n\n" + b);
} else {
System.out.println(b.toString());
fail = true;
}
} }
} }
if (fail) { if (fail) {
@ -1067,27 +961,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
Set<Integer> deleted = new HashSet<>(); Set<Integer> deleted = new HashSet<>();
// RandomIndexWriter is too slow here: // RandomIndexWriter is too slow here:
IndexWriter w = new IndexWriter(dir, iwc); IndexWriter w = new IndexWriter(dir, iwc);
for(int id=0;id<lats.length;id++) { indexPoints(lats, lons, deleted, w);
Document doc = new Document();
doc.add(newStringField("id", ""+id, Field.Store.NO));
doc.add(new NumericDocValuesField("id", id));
if (Double.isNaN(lats[id]) == false) {
addPointToDoc(FIELD_NAME, doc, lats[id], lons[id]);
}
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);
}
final IndexReader r = DirectoryReader.open(w); final IndexReader r = DirectoryReader.open(w);
w.close(); w.close();
@ -1113,26 +988,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
System.out.println(" query=" + query); System.out.println(" query=" + query);
} }
final FixedBitSet hits = new FixedBitSet(maxDoc); final FixedBitSet hits = searchIndex(s, query, 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) {
hits.set(docBase+doc);
}
});
boolean fail = false; boolean fail = false;
NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id"); NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id");
@ -1150,23 +1006,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
} }
if (hits.get(docID) != expected) { if (hits.get(docID) != expected) {
StringBuilder b = new StringBuilder(); buildError(docID, expected, id, lats, lons, query, liveDocs, (b) -> b.append(" polygon=").append(polygon));
fail = true;
if (expected) {
b.append("FAIL: id=").append(id).append(" should match but did not\n");
} else {
b.append("FAIL: id=").append(id).append(" should not match but did\n");
}
b.append(" query=").append(query).append(" docID=").append(docID).append("\n");
b.append(" lat=").append(lats[id]).append(" lon=").append(lons[id]).append("\n");
b.append(" deleted?=").append(liveDocs != null && liveDocs.get(docID) == false);
b.append(" polygon=").append(polygon);
if (true) {
fail("wrong hit (first of possibly more):\n\n" + b);
} else {
System.out.println(b.toString());
fail = true;
}
} }
} }
if (fail) { if (fail) {
@ -1177,6 +1018,152 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
IOUtils.close(r, dir); IOUtils.close(r, dir);
} }
protected void verifyRandomGeometries(double[] lats, double[] lons) throws Exception {
IndexWriterConfig iwc = newIndexWriterConfig();
// Else seeds may not reproduce:
iwc.setMergeScheduler(new SerialMergeScheduler());
// Else we can get O(N^2) merging:
int mbd = iwc.getMaxBufferedDocs();
if (mbd != -1 && mbd < lats.length/100) {
iwc.setMaxBufferedDocs(lats.length/100);
}
Directory dir;
if (lats.length > 100000) {
dir = newFSDirectory(createTempDir(getClass().getSimpleName()));
} else {
dir = newDirectory();
}
Set<Integer> deleted = new HashSet<>();
// RandomIndexWriter is too slow here:
IndexWriter w = new IndexWriter(dir, iwc);
indexPoints(lats, lons, deleted, w);
final IndexReader r = DirectoryReader.open(w);
w.close();
// We can't wrap with "exotic" readers because points needs to work:
IndexSearcher s = newSearcher(r);
final int iters = atLeast(75);
Bits liveDocs = MultiBits.getLiveDocs(s.getIndexReader());
int maxDoc = s.getIndexReader().maxDoc();
for (int iter=0;iter<iters;iter++) {
if (VERBOSE) {
System.out.println("\nTEST: iter=" + iter + " s=" + s);
}
// Polygon
LatLonGeometry[] geometries = nextGeometry();
Query query = newGeometryQuery(FIELD_NAME, geometries);
if (VERBOSE) {
System.out.println(" query=" + query);
}
final FixedBitSet hits = searchIndex(s, query, maxDoc);
Component2D component2D = LatLonGeometry.create(geometries);
boolean fail = false;
NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "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 (Double.isNaN(lats[id])) {
expected = false;
} else {
expected = component2D.contains(quantizeLon(lons[id]), quantizeLat(lats[id]));
}
if (hits.get(docID) != expected) {
buildError(docID, expected, id, lats, lons, query, liveDocs, (b) -> b.append(" geometry=").append(Arrays.toString(geometries)));
fail = true;
}
}
if (fail) {
fail("some hits were wrong");
}
}
IOUtils.close(r, dir);
}
private void indexPoints(double[] lats, double[] lons, Set<Integer> deleted, IndexWriter w) throws IOException {
for(int id=0;id<lats.length;id++) {
Document doc = new Document();
doc.add(newStringField("id", ""+id, Field.Store.NO));
doc.add(new NumericDocValuesField("id", id));
if (Double.isNaN(lats[id]) == false) {
addPointToDoc(FIELD_NAME, doc, lats[id], lons[id]);
}
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);
}
}
private FixedBitSet searchIndex(IndexSearcher s, Query query, int maxDoc) throws IOException {
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) {
docBase = context.docBase;
}
@Override
public void collect(int doc) {
hits.set(docBase+doc);
}
});
return hits;
}
private void buildError(int docID, boolean expected, int id, double[] lats, double[] lons, Query query,
Bits liveDocs, Consumer<StringBuilder> explain) {
StringBuilder b = new StringBuilder();
if (expected) {
b.append("FAIL: id=").append(id).append(" should match but did not\n");
} else {
b.append("FAIL: id=").append(id).append(" should not match but did\n");
}
b.append(" query=").append(query).append(" docID=").append(docID).append("\n");
b.append(" lat=").append(lats[id]).append(" lon=").append(lons[id]).append("\n");
b.append(" deleted?=").append(liveDocs != null && liveDocs.get(docID) == false);
explain.accept(b);
if (true) {
fail("wrong hit (first of possibly more):\n\n" + b);
} else {
System.out.println(b.toString());
}
}
public void testRectBoundariesAreInclusive() throws Exception { public void testRectBoundariesAreInclusive() throws Exception {
Rectangle rect; Rectangle rect;
// TODO: why this dateline leniency??? // TODO: why this dateline leniency???
@ -1383,12 +1370,10 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
lons[3] = rect.maxLon; lons[3] = rect.maxLon;
lats[4] = rect.minLat; lats[4] = rect.minLat;
lons[4] = rect.minLon; lons[4] = rect.minLon;
if (supportsPolygons()) { q1 = newPolygonQuery("field", new Polygon(lats, lons));
q1 = newPolygonQuery("field", new Polygon(lats, lons)); q2 = newPolygonQuery("field", new Polygon(lats, lons));
q2 = newPolygonQuery("field", new Polygon(lats, lons)); assertEquals(q1, q2);
assertEquals(q1, q2); assertFalse(q1.equals(newPolygonQuery("field2", new Polygon(lats, lons))));
assertFalse(q1.equals(newPolygonQuery("field2", new Polygon(lats, lons))));
}
} }
/** return topdocs over a small set of points in field "point" */ /** return topdocs over a small set of points in field "point" */
@ -1477,7 +1462,6 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
} }
public void testSmallSetPoly() throws Exception { public void testSmallSetPoly() throws Exception {
assumeTrue("Impl does not support polygons", supportsPolygons());
TopDocs td = searchSmallSet(newPolygonQuery("point", TopDocs td = searchSmallSet(newPolygonQuery("point",
new Polygon( new Polygon(
new double[]{33.073130, 32.9942669, 32.938386, 33.0374494, new double[]{33.073130, 32.9942669, 32.938386, 33.0374494,
@ -1489,7 +1473,6 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
} }
public void testSmallSetPolyWholeMap() throws Exception { public void testSmallSetPolyWholeMap() throws Exception {
assumeTrue("Impl does not support polygons", supportsPolygons());
TopDocs td = searchSmallSet(newPolygonQuery("point", TopDocs td = searchSmallSet(newPolygonQuery("point",
new Polygon( new Polygon(
new double[] {GeoUtils.MIN_LAT_INCL, GeoUtils.MAX_LAT_INCL, GeoUtils.MAX_LAT_INCL, GeoUtils.MIN_LAT_INCL, GeoUtils.MIN_LAT_INCL}, new double[] {GeoUtils.MIN_LAT_INCL, GeoUtils.MAX_LAT_INCL, GeoUtils.MAX_LAT_INCL, GeoUtils.MIN_LAT_INCL, GeoUtils.MIN_LAT_INCL},

View File

@ -394,7 +394,7 @@ public class GeoTestUtil {
public static Circle nextCircle() { public static Circle nextCircle() {
double lat = nextLatitude(); double lat = nextLatitude();
double lon = nextLongitude(); double lon = nextLongitude();
double radiusMeters = random().nextDouble() * Circle.MAX_RADIUS; double radiusMeters = random().nextDouble() * GeoUtils.EARTH_MEAN_RADIUS_METERS * Math.PI / 2.0 + 1.0;
return new Circle(lat, lon, radiusMeters); return new Circle(lat, lon, radiusMeters);
} }