diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index eef4de49469..d049e55645b 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -212,6 +212,8 @@ New Features * 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). +* LUCENE-9552: New LatLonPoint query that accepts an array of LatLonGeometries. (Ignacio Vera) + Improvements --------------------- diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesDistanceQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesDistanceQuery.java deleted file mode 100644 index 5ea137b15a2..00000000000 --- a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesDistanceQuery.java +++ /dev/null @@ -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); - } - - }; - } - -} diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java index 07f1bb8b73a..e263f78b850 100644 --- a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java +++ b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java @@ -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.encodeLongitude; +import org.apache.lucene.geo.Circle; +import org.apache.lucene.geo.LatLonGeometry; import org.apache.lucene.geo.Polygon; +import org.apache.lucene.geo.Rectangle; import org.apache.lucene.index.DocValuesType; import org.apache.lucene.index.FieldInfo; 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. */ 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. */ 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); } } diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesPointInPolygonQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesPointInGeometryQuery.java similarity index 72% rename from lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesPointInPolygonQuery.java rename to lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesPointInGeometryQuery.java index abf1c883884..3d6f25b9c04 100644 --- a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesPointInPolygonQuery.java +++ b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesPointInGeometryQuery.java @@ -17,13 +17,10 @@ package org.apache.lucene.document; -import java.io.IOException; -import java.util.Arrays; - import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.GeoEncodingUtils; 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.LeafReaderContext; 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.Weight; -/** Polygon query for {@link LatLonDocValuesField}. */ -public class LatLonDocValuesPointInPolygonQuery extends Query { +import java.io.IOException; +import java.util.Arrays; + +/** Geometry query for {@link LatLonDocValuesField}. */ +public class LatLonDocValuesPointInGeometryQuery extends Query { 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) { throw new IllegalArgumentException("field must not be null"); } - if (polygons == null) { - throw new IllegalArgumentException("polygons must not be null"); + if (geometries == null) { + throw new IllegalArgumentException("geometries must not be null"); } - if (polygons.length == 0) { - throw new IllegalArgumentException("polygons must not be empty"); + if (geometries.length == 0) { + throw new IllegalArgumentException("geometries must not be empty"); } - for (int i = 0; i < polygons.length; i++) { - if (polygons[i] == null) { - throw new IllegalArgumentException("polygon[" + i + "] must not be null"); + for (int i = 0; i < geometries.length; i++) { + if (geometries[i] == 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.polygons = polygons; + this.geometries = geometries; } @Override @@ -70,7 +73,7 @@ public class LatLonDocValuesPointInPolygonQuery extends Query { sb.append(this.field); sb.append(':'); } - sb.append("polygons(").append(Arrays.toString(polygons)); + sb.append("geometries(").append(Arrays.toString(geometries)); return sb.append(")").toString(); } @@ -79,16 +82,16 @@ public class LatLonDocValuesPointInPolygonQuery extends Query { if (sameClassAs(obj) == false) { return false; } - LatLonDocValuesPointInPolygonQuery other = (LatLonDocValuesPointInPolygonQuery) obj; + LatLonDocValuesPointInGeometryQuery other = (LatLonDocValuesPointInGeometryQuery) obj; return field.equals(other.field) && - Arrays.equals(polygons, other.polygons); + Arrays.equals(geometries, other.geometries); } @Override public int hashCode() { int h = classHash(); h = 31 * h + field.hashCode(); - h = 31 * h + Arrays.hashCode(polygons); + h = 31 * h + Arrays.hashCode(geometries); return h; } @@ -104,8 +107,8 @@ public class LatLonDocValuesPointInPolygonQuery extends Query { return new ConstantScoreWeight(this, boost) { - final Component2D tree = LatLonGeometry.create(polygons); - final GeoEncodingUtils.PolygonPredicate polygonPredicate = GeoEncodingUtils.createComponentPredicate(tree); + final Component2D tree = LatLonGeometry.create(geometries); + final GeoEncodingUtils.Component2DPredicate component2DPredicate = GeoEncodingUtils.createComponentPredicate(tree); @Override public Scorer scorer(LeafReaderContext context) throws IOException { @@ -122,7 +125,7 @@ public class LatLonDocValuesPointInPolygonQuery extends Query { final long value = values.nextValue(); final int lat = (int) (value >>> 32); final int lon = (int) (value & 0xFFFFFFFF); - if (polygonPredicate.test(lat, lon)) { + if (component2DPredicate.test(lat, lon)) { return true; } } diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java b/lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java index 22f20c994c1..7dfb5a006e3 100644 --- a/lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java +++ b/lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java @@ -16,7 +16,10 @@ */ 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.Rectangle; import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.PointValues; import org.apache.lucene.search.BooleanClause; @@ -49,6 +52,7 @@ import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil; *
  • {@link #newBoxQuery newBoxQuery()} for matching points within a bounding box. *
  • {@link #newDistanceQuery newDistanceQuery()} for matching points within a specified distance. *
  • {@link #newPolygonQuery newPolygonQuery()} for matching points within an arbitrary polygon. + *
  • {@link #newGeometryQuery newGeometryQuery()} for matching points within an arbitrary geometry collection. * *

    * 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 */ 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); } /** diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonPointInGeometryQuery.java similarity index 80% rename from lucene/core/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java rename to lucene/core/src/java/org/apache/lucene/document/LatLonPointInGeometryQuery.java index 4285e096577..adeba14e5d4 100644 --- a/lucene/core/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java +++ b/lucene/core/src/java/org/apache/lucene/document/LatLonPointInGeometryQuery.java @@ -16,13 +16,10 @@ */ package org.apache.lucene.document; -import java.io.IOException; -import java.util.Arrays; - import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.GeoEncodingUtils; 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.LeafReader; 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.NumericUtils; +import java.io.IOException; +import java.util.Arrays; + 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; -/** Finds all previously indexed points that fall within the specified polygons. +/** Finds all previously indexed points that fall within the specified geometries. * - *

    The field must be indexed with using {@link org.apache.lucene.document.LatLonPoint} added per document. + *

    The field must be indexed with using {@link LatLonPoint} added per document. * * @lucene.experimental */ -final class LatLonPointInPolygonQuery extends Query { +final class LatLonPointInGeometryQuery extends Query { final String field; - final Polygon[] polygons; + final LatLonGeometry[] geometries; - LatLonPointInPolygonQuery(String field, Polygon[] polygons) { + LatLonPointInGeometryQuery(String field, LatLonGeometry[] geometries) { if (field == null) { throw new IllegalArgumentException("field must not be null"); } - if (polygons == null) { - throw new IllegalArgumentException("polygons must not be null"); + if (geometries == null) { + throw new IllegalArgumentException("geometries must not be null"); } - if (polygons.length == 0) { - throw new IllegalArgumentException("polygons must not be empty"); + if (geometries.length == 0) { + throw new IllegalArgumentException("geometries must not be empty"); } - for (int i = 0; i < polygons.length; i++) { - if (polygons[i] == null) { - throw new IllegalArgumentException("polygon[" + i + "] must not be null"); + for (int i = 0; i < geometries.length; i++) { + if (geometries[i] == 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.polygons = polygons.clone(); - // TODO: we could also compute the maximal inner bounding box, to make relations faster to compute? + this.geometries = geometries.clone(); } @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) { return new IntersectVisitor() { DocIdSetBuilder.BulkAdder adder; @@ -101,7 +103,7 @@ final class LatLonPointInPolygonQuery extends Query { @Override 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))) { visit(docID); } @@ -109,7 +111,7 @@ final class LatLonPointInPolygonQuery extends Query { @Override 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))) { int docID; while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { @@ -141,9 +143,9 @@ final class LatLonPointInPolygonQuery extends Query { @Override public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException { - final Component2D tree = LatLonGeometry.create(polygons); - final GeoEncodingUtils.PolygonPredicate polygonPredicate = GeoEncodingUtils.createComponentPredicate(tree); - // bounding box over all polygons, this can speed up tree intersection/cheaply improve approximation for complex multi-polygons + final Component2D tree = LatLonGeometry.create(geometries); + final GeoEncodingUtils.Component2DPredicate component2DPredicate = GeoEncodingUtils.createComponentPredicate(tree); + // 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 maxLat[] = new byte[Integer.BYTES]; final byte minLon[] = new byte[Integer.BYTES]; @@ -175,7 +177,7 @@ final class LatLonPointInPolygonQuery extends Query { long cost = -1; 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 public Scorer get(long leadCost) throws IOException { @@ -217,9 +219,9 @@ final class LatLonPointInPolygonQuery extends Query { return field; } - /** Returns a copy of the internal polygon array */ - public Polygon[] getPolygons() { - return polygons.clone(); + /** Returns a copy of the internal geometry array */ + public LatLonGeometry[] getGeometries() { + return geometries.clone(); } @Override @@ -227,7 +229,7 @@ final class LatLonPointInPolygonQuery extends Query { final int prime = 31; int result = classHash(); result = prime * result + field.hashCode(); - result = prime * result + Arrays.hashCode(polygons); + result = prime * result + Arrays.hashCode(geometries); return result; } @@ -237,9 +239,9 @@ final class LatLonPointInPolygonQuery extends Query { equalsTo(getClass().cast(other)); } - private boolean equalsTo(LatLonPointInPolygonQuery other) { + private boolean equalsTo(LatLonPointInGeometryQuery other) { return field.equals(other.field) && - Arrays.equals(polygons, other.polygons); + Arrays.equals(geometries, other.geometries); } @Override @@ -252,7 +254,7 @@ final class LatLonPointInPolygonQuery extends Query { sb.append(this.field); sb.append(':'); } - sb.append(Arrays.toString(polygons)); + sb.append(Arrays.toString(geometries)); return sb.toString(); } } diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java index 465cf511637..d3ad3e253bc 100644 --- a/lucene/core/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java +++ b/lucene/core/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java @@ -178,37 +178,23 @@ final class LatLonShapeBoundingBoxQuery extends ShapeQuery { EncodedRectangle(double minLat, double maxLat, double minLon, double maxLon) { this.bbox = new byte[4 * BYTES]; - int minXenc = encodeLongitudeCeil(minLon); - int maxXenc = encodeLongitude(maxLon); - int minYenc = encodeLatitudeCeil(minLat); - int maxYenc = encodeLatitude(maxLat); - if (minYenc > maxYenc) { - minYenc = maxYenc; + if (minLon == 180.0 && minLon > maxLon) { + minLon = -180; } - this.minY = minYenc; - this.maxY = maxYenc; - - if (minLon > maxLon == true) { - this.crossesDateline = true; + this.minX = encodeLongitudeCeil(minLon); + this.maxX = encodeLongitude(maxLon); + this.minY = encodeLatitudeCeil(minLat); + this.maxY = encodeLatitude(maxLat); + this.crossesDateline = minLon > maxLon; + if (this.crossesDateline) { // crossing dateline is split into east/west boxes 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(this.minX, MAX_LON_ENCODED, this.minY, this.maxY, this.bbox); } 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.minX = minXenc; - this.maxX = maxXenc; encode(this.minX, this.maxX, this.minY, this.maxY, bbox); } - } /** diff --git a/lucene/core/src/java/org/apache/lucene/geo/Circle.java b/lucene/core/src/java/org/apache/lucene/geo/Circle.java index 4bb63c845b4..30121c20e75 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/Circle.java +++ b/lucene/core/src/java/org/apache/lucene/geo/Circle.java @@ -35,9 +35,6 @@ public final class Circle extends LatLonGeometry { private final double lon; /** radius in meters */ 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.. @@ -45,11 +42,8 @@ public final class Circle extends LatLonGeometry { public Circle(double lat, double lon, double radiusMeters) { GeoUtils.checkLatitude(lat); GeoUtils.checkLongitude(lon); - if (radiusMeters <= 0) { - throw new IllegalArgumentException("radius must be bigger than 0, got " + radiusMeters); - } - if (radiusMeters < MAX_RADIUS == false) { - throw new IllegalArgumentException("radius must be lower than " + MAX_RADIUS + ", got " + radiusMeters); + if (Double.isFinite(radiusMeters) == false || radiusMeters < 0) { + throw new IllegalArgumentException("radiusMeters: '" + radiusMeters + "' is invalid"); } this.lat = lat; this.lon = lon; diff --git a/lucene/core/src/java/org/apache/lucene/geo/GeoEncodingUtils.java b/lucene/core/src/java/org/apache/lucene/geo/GeoEncodingUtils.java index 5c7078d061a..da638805276 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/GeoEncodingUtils.java +++ b/lucene/core/src/java/org/apache/lucene/geo/GeoEncodingUtils.java @@ -178,16 +178,16 @@ public final class GeoEncodingUtils { 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}. * @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 Function boxToRelation = box -> tree.relate( box.minLon, box.maxLon, box.minLat, box.maxLat); final Grid subBoxes = createSubBoxes(boundingBox, boxToRelation); - return new PolygonPredicate( + return new Component2DPredicate( subBoxes.latShift, subBoxes.lonShift, subBoxes.latBase, subBoxes.lonBase, subBoxes.maxLatDelta, subBoxes.maxLonDelta, @@ -342,12 +342,12 @@ public final class GeoEncodingUtils { } } - /** A predicate that checks whether a given point is within a polygon. */ - public static class PolygonPredicate extends Grid { + /** A predicate that checks whether a given point is within a component2D geometry. */ + public static class Component2DPredicate extends Grid { private final Component2D tree; - private PolygonPredicate( + private Component2DPredicate( int latShift, int lonShift, int latBase, int lonBase, int maxLatDelta, int maxLonDelta, diff --git a/lucene/core/src/java/org/apache/lucene/geo/Rectangle2D.java b/lucene/core/src/java/org/apache/lucene/geo/Rectangle2D.java index b1beb5d4fdd..37b5f032991 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/Rectangle2D.java +++ b/lucene/core/src/java/org/apache/lucene/geo/Rectangle2D.java @@ -232,28 +232,25 @@ final class Rectangle2D implements Component2D { /** create a component2D from the provided LatLon 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! double qMinLat = decodeLatitude(encodeLatitudeCeil(rectangle.minLat)); double qMaxLat = decodeLatitude(encodeLatitude(rectangle.maxLat)); - double qMinLon = decodeLongitude(encodeLongitudeCeil(rectangle.minLon)); + double qMinLon = decodeLongitude(encodeLongitudeCeil(minLongitude)); double qMaxLon = decodeLongitude(encodeLongitude(rectangle.maxLon)); - if (qMinLat > qMaxLat) { - // encodeLatitudeCeil may cause minY to be > maxY iff - // the delta between the longitude < the encoding resolution - qMinLat = qMaxLat; - } - if (rectangle.minLon > rectangle.maxLon) { + if (crossesDateline) { // for rectangles that cross the dateline we need to create two components Component2D[] components = new Component2D[2]; components[0] = new Rectangle2D(MIN_LON_INCL_QUANTIZE, qMaxLon, qMinLat, qMaxLat); components[1] = new Rectangle2D(qMinLon, MAX_LON_INCL_QUANTIZE, qMinLat, qMaxLat); return ComponentTree.create(components); } 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); } } diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java index 785faecab6a..831c01645db 100644 --- a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java +++ b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java @@ -22,6 +22,7 @@ import com.carrotsearch.randomizedtesting.generators.RandomPicks; import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.GeoUtils; import org.apache.lucene.geo.LatLonGeometry; import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Polygon; @@ -109,7 +110,8 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase { @Override 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 diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java index ada90f7376a..3374ea25963 100644 --- a/lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java +++ b/lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java @@ -326,34 +326,23 @@ public abstract class BaseShapeTestCase extends LuceneTestCase { assertEquals(docID, docIDToID.nextDoc()); int id = (int) docIDToID.longValue(); boolean expected; - double qMinLon = ENCODER.quantizeXCeil(rectMinX(rect)); - double qMaxLon = ENCODER.quantizeX(rectMaxX(rect)); - double qMinLat = ENCODER.quantizeYCeil(rectMinY(rect)); - double qMaxLat = ENCODER.quantizeY(rectMaxY(rect)); + double minLon = rectMinX(rect); + double maxLon = rectMaxX(rect); + double minLat = rectMinY(rect); + double maxLat = rectMaxY(rect); if (liveDocs != null && liveDocs.get(docID) == false) { // document is deleted expected = false; } else if (shapes[id] == null) { expected = false; } else { - if (qMinLat > qMaxLat) { - qMinLat = ENCODER.quantizeY(rectMaxY(rect)); - } if (queryRelation == QueryRelation.CONTAINS && rectCrossesDateline(rect)) { - //For contains we need to call the validator for each section. It is only expected - //if both sides are contained. - expected = VALIDATOR.setRelation(queryRelation).testBBoxQuery(qMinLat, qMaxLat, qMinLon, GeoUtils.MAX_LON_INCL, shapes[id]); - if (expected) { - expected = VALIDATOR.setRelation(queryRelation).testBBoxQuery(qMinLat, qMaxLat, GeoUtils.MIN_LON_INCL, qMaxLon, shapes[id]); - } + // For contains we need to call the validator for each section. + // It is only expected if both sides are contained. + expected = VALIDATOR.setRelation(queryRelation).testBBoxQuery(minLat, maxLat, minLon, GeoUtils.MAX_LON_INCL, shapes[id]) && + VALIDATOR.setRelation(queryRelation).testBBoxQuery(minLat, maxLat, GeoUtils.MIN_LON_INCL, maxLon, shapes[id]); } else { - // check quantized poly against quantized query - 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]); + expected = VALIDATOR.setRelation(queryRelation).testBBoxQuery(minLat, maxLat, minLon, maxLon, shapes[id]); } } @@ -373,7 +362,7 @@ public abstract class BaseShapeTestCase extends LuceneTestCase { b.append(" shape=" + shapes[id] + "\n"); } 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) { fail("wrong hit (first of possibly more):\n\n" + b); } else { diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java index 11acf62c07b..9ada38916b1 100644 --- a/lucene/core/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java +++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java @@ -20,7 +20,9 @@ import com.carrotsearch.randomizedtesting.generators.RandomNumbers; import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.LatLonGeometry; 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 */ public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase { @@ -73,21 +75,8 @@ public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase { @Override public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { - if (queryRelation == QueryRelation.CONTAINS) { - return false; - } - 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; + Component2D rectangle2D = LatLonGeometry.create(new Rectangle(minLat, maxLat, minLon, maxLon)); + return testComponentQuery(rectangle2D, shape); } @Override diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java index e6443586e25..24d7784ab79 100644 --- a/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java +++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java @@ -22,6 +22,7 @@ import org.apache.lucene.geo.Circle; import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.GeoUtils; import org.apache.lucene.geo.LatLonGeometry; import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Polygon; @@ -433,7 +434,7 @@ public class TestLatLonShape extends LuceneTestCase { IndexSearcher s = newSearcher(r); // 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)); IOUtils.close(r, dir); } @@ -777,10 +778,7 @@ public class TestLatLonShape extends LuceneTestCase { double lat = GeoTestUtil.nextLatitude(); double lon = GeoTestUtil.nextLongitude(); - double radiusMeters = random().nextDouble() * Circle.MAX_RADIUS; - while (radiusMeters == 0 || radiusMeters == Circle.MAX_RADIUS) { - radiusMeters = random().nextDouble() * Circle.MAX_RADIUS; - } + final double radiusMeters = random().nextDouble() * GeoUtils.EARTH_MEAN_RADIUS_METERS * Math.PI / 2.0 + 1.0; Circle circle = new Circle(lat, lon, radiusMeters); Component2D circle2D = LatLonGeometry.create(circle); int expected; diff --git a/lucene/core/src/test/org/apache/lucene/geo/TestCircle.java b/lucene/core/src/test/org/apache/lucene/geo/TestCircle.java index 8005d728e8f..37f545b2772 100644 --- a/lucene/core/src/test/org/apache/lucene/geo/TestCircle.java +++ b/lucene/core/src/test/org/apache/lucene/geo/TestCircle.java @@ -41,15 +41,15 @@ public class TestCircle extends LuceneTestCase { IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { 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() { IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { 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 */ diff --git a/lucene/core/src/test/org/apache/lucene/search/TestLatLonDocValuesQueries.java b/lucene/core/src/test/org/apache/lucene/search/TestLatLonDocValuesQueries.java index af113e1adc9..ebb5debda97 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestLatLonDocValuesQueries.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestLatLonDocValuesQueries.java @@ -20,15 +20,11 @@ import org.apache.lucene.document.Document; import org.apache.lucene.document.LatLonDocValuesField; import org.apache.lucene.geo.BaseGeoPointTestCase; import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.LatLonGeometry; import org.apache.lucene.geo.Polygon; public class TestLatLonDocValuesQueries extends BaseGeoPointTestCase { - @Override - protected boolean supportsPolygons() { - return true; - } - @Override protected void addPointToDoc(String field, Document doc, double lat, double lon) { doc.add(new LatLonDocValuesField(field, lat, lon)); @@ -49,6 +45,11 @@ public class TestLatLonDocValuesQueries extends BaseGeoPointTestCase { return LatLonDocValuesField.newSlowPolygonQuery(field, polygons); } + @Override + protected Query newGeometryQuery(String field, LatLonGeometry... geometry) { + return LatLonDocValuesField.newSlowGeometryQuery(field, geometry); + } + @Override protected double quantizeLat(double latRaw) { return GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(latRaw)); diff --git a/lucene/core/src/test/org/apache/lucene/search/TestLatLonPointQueries.java b/lucene/core/src/test/org/apache/lucene/search/TestLatLonPointQueries.java index d3fce00a010..265367fe197 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestLatLonPointQueries.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestLatLonPointQueries.java @@ -22,6 +22,7 @@ import org.apache.lucene.document.Document; import org.apache.lucene.document.LatLonPoint; import org.apache.lucene.geo.BaseGeoPointTestCase; import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.LatLonGeometry; import org.apache.lucene.geo.Polygon; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; @@ -51,6 +52,11 @@ public class TestLatLonPointQueries extends BaseGeoPointTestCase { return LatLonPoint.newPolygonQuery(field, polygons); } + @Override + protected Query newGeometryQuery(String field, LatLonGeometry... geometry) { + return LatLonPoint.newGeometryQuery(field, geometry); + } + @Override protected double quantizeLat(double latRaw) { return GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(latRaw)); diff --git a/lucene/test-framework/src/java/org/apache/lucene/geo/BaseGeoPointTestCase.java b/lucene/test-framework/src/java/org/apache/lucene/geo/BaseGeoPointTestCase.java index c080db19b08..a4b47f0cc05 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/geo/BaseGeoPointTestCase.java +++ b/lucene/test-framework/src/java/org/apache/lucene/geo/BaseGeoPointTestCase.java @@ -24,6 +24,7 @@ import java.util.BitSet; import java.util.HashSet; import java.util.Locale; import java.util.Set; +import java.util.function.Consumer; import org.apache.lucene.analysis.MockAnalyzer; import org.apache.lucene.codecs.Codec; @@ -96,14 +97,34 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { protected Rectangle nextBox() { return org.apache.lucene.geo.GeoTestUtil.nextBox(); } + + protected Circle nextCircle() { + return org.apache.lucene.geo.GeoTestUtil.nextCircle(); + } protected Polygon nextPolygon() { return org.apache.lucene.geo.GeoTestUtil.nextPolygon(); } - /** Whether this impl supports polygons. */ - protected boolean supportsPolygons() { - return true; + protected LatLonGeometry[] nextGeometry() { + final int length = random().nextInt(4) + 1; + 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 */ @@ -291,7 +312,6 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { /** test we can search for a polygon */ public void testPolygonBasics() throws Exception { - assumeTrue("Impl does not support polygons", supportsPolygons()); Directory dir = newDirectory(); 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) */ public void testPolygonHole() throws Exception { - assumeTrue("Impl does not support polygons", supportsPolygons()); Directory dir = newDirectory(); 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) */ public void testPolygonHoleExcludes() throws Exception { - assumeTrue("Impl does not support polygons", supportsPolygons()); Directory dir = newDirectory(); RandomIndexWriter writer = new RandomIndexWriter(random(), dir); @@ -364,7 +382,6 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { /** test we can search for a multi-polygon */ public void testMultiPolygonBasics() throws Exception { - assumeTrue("Impl does not support polygons", supportsPolygons()); Directory dir = newDirectory(); RandomIndexWriter writer = new RandomIndexWriter(random(), dir); @@ -389,7 +406,6 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { /** null field name not allowed */ public void testPolygonNullField() { - assumeTrue("Impl does not support polygons", supportsPolygons()); IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { newPolygonQuery(null, new Polygon( new double[] { 18, 18, 19, 19, 18 }, @@ -583,27 +599,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { Query query = newRectQuery(FIELD_NAME, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon); - final FixedBitSet hits = new FixedBitSet(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); - } - }); - + final FixedBitSet hits = searchIndex(s, query, r.maxDoc()); + boolean fail = false; for(int docID=0;docID deleted = new HashSet<>(); // RandomIndexWriter is too slow here: IndexWriter w = new IndexWriter(dir, iwc); - for(int id=0;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); - } + indexPoints(lats, lons, deleted, w); + final IndexReader r = DirectoryReader.open(w); w.close(); @@ -842,26 +821,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { 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) { - hits.set(docBase+doc); - } - }); + final FixedBitSet hits = searchIndex(s, query, maxDoc); boolean fail = false; NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id"); @@ -879,24 +839,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { } if (hits.get(docID) != expected) { - StringBuilder b = new StringBuilder(); - b.append("docID=(").append(docID).append(")\n"); - - 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; - } + buildError(docID, expected, id, lats, lons, query, liveDocs, (b) -> b.append(" rect=").append(rect)); + fail = true; } } if (fail) { @@ -926,27 +870,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { Set deleted = new HashSet<>(); // RandomIndexWriter is too slow here: IndexWriter w = new IndexWriter(dir, iwc); - for(int id=0;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); - } + indexPoints(lats, lons, deleted, w); + final IndexReader r = DirectoryReader.open(w); w.close(); @@ -981,27 +906,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { 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) { - hits.set(docBase+doc); - } - }); - + final FixedBitSet hits = searchIndex(s, query, maxDoc); + boolean fail = false; NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id"); for(int docID=0;docID explain = (b) -> { + 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); + } + }; + buildError(docID, expected, id, lats, lons, query, liveDocs, explain); + fail = true; } } if (fail) { @@ -1067,27 +961,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { Set deleted = new HashSet<>(); // RandomIndexWriter is too slow here: IndexWriter w = new IndexWriter(dir, iwc); - for(int id=0;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); - } + indexPoints(lats, lons, deleted, w); + final IndexReader r = DirectoryReader.open(w); w.close(); @@ -1113,26 +988,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { 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) { - hits.set(docBase+doc); - } - }); + final FixedBitSet hits = searchIndex(s, query, maxDoc); boolean fail = false; NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id"); @@ -1150,23 +1006,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { } if (hits.get(docID) != expected) { - 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); - 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; - } + buildError(docID, expected, id, lats, lons, query, liveDocs, (b) -> b.append(" polygon=").append(polygon)); + fail = true; } } if (fail) { @@ -1176,6 +1017,152 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { 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 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 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 deleted, IndexWriter w) throws IOException { + for(int id=0;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 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 { Rectangle rect; @@ -1350,7 +1337,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { dir.close(); } - public void testEquals() throws Exception { + public void testEquals() throws Exception { Query q1, q2; Rectangle rect = nextBox(); @@ -1383,12 +1370,10 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { lons[3] = rect.maxLon; lats[4] = rect.minLat; lons[4] = rect.minLon; - if (supportsPolygons()) { - q1 = newPolygonQuery("field", new Polygon(lats, lons)); - q2 = newPolygonQuery("field", new Polygon(lats, lons)); - assertEquals(q1, q2); - assertFalse(q1.equals(newPolygonQuery("field2", new Polygon(lats, lons)))); - } + q1 = newPolygonQuery("field", new Polygon(lats, lons)); + q2 = newPolygonQuery("field", new Polygon(lats, lons)); + assertEquals(q1, q2); + assertFalse(q1.equals(newPolygonQuery("field2", new Polygon(lats, lons)))); } /** 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 { - assumeTrue("Impl does not support polygons", supportsPolygons()); TopDocs td = searchSmallSet(newPolygonQuery("point", new Polygon( 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 { - assumeTrue("Impl does not support polygons", supportsPolygons()); TopDocs td = searchSmallSet(newPolygonQuery("point", new Polygon( new double[] {GeoUtils.MIN_LAT_INCL, GeoUtils.MAX_LAT_INCL, GeoUtils.MAX_LAT_INCL, GeoUtils.MIN_LAT_INCL, GeoUtils.MIN_LAT_INCL}, diff --git a/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java b/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java index 39bfe97d5f8..cbcc4c1f1e4 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java +++ b/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java @@ -394,7 +394,7 @@ public class GeoTestUtil { public static Circle nextCircle() { double lat = nextLatitude(); 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); }