From 0e4abf7179d811a7b8bdfe4d4a2c29aff64a01c9 Mon Sep 17 00:00:00 2001 From: Mikhail Khludnev Date: Wed, 15 Jan 2020 12:19:16 +0300 Subject: [PATCH 1/2] SOLR-12490: reverting ref-guide-fix. --- solr/solr-ref-guide/src/common-query-parameters.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solr/solr-ref-guide/src/common-query-parameters.adoc b/solr/solr-ref-guide/src/common-query-parameters.adoc index 4d1949c00b1..d2915f4570b 100644 --- a/solr/solr-ref-guide/src/common-query-parameters.adoc +++ b/solr/solr-ref-guide/src/common-query-parameters.adoc @@ -102,7 +102,7 @@ fq=+popularity:[10 TO *] +section:0 ---- * The document sets from each filter query are cached independently. Thus, concerning the previous examples: use a single `fq` containing two mandatory clauses if those clauses appear together often, and use two separate `fq` parameters if they are relatively independent. (To learn about tuning cache sizes and making sure a filter cache actually exists, see <>.) -* It is also possible to use <> inside the `fq` to cache clauses individually and - among other things - to achieve union of cached filter queries. +* It is also possible to use <> inside the `fq` to cache clauses individually and - among other things - to achieve union of cached filter queries. * As with all parameters: special characters in an URL need to be properly escaped and encoded as hex values. Online tools are available to help you with URL-encoding. For example: http://meyerweb.com/eric/tools/dencoder/. From ff365a0abf23d89fd0eba3fbb707f0fc4f2316ce Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Wed, 15 Jan 2020 11:57:53 +0100 Subject: [PATCH 2/2] LUCENE-8903: Add LatLonShape point query (#762) --- lucene/CHANGES.txt | 3 +- .../apache/lucene/document/LatLonShape.java | 13 ++ .../document/LatLonShapePointQuery.java | 125 ++++++++++++++++ .../org/apache/lucene/document/XYShape.java | 13 ++ .../lucene/document/XYShapePointQuery.java | 127 +++++++++++++++++ .../java/org/apache/lucene/geo/Point2D.java | 134 ++++++++++++++++++ .../document/BaseLatLonShapeTestCase.java | 28 ++++ .../lucene/document/BaseShapeTestCase.java | 108 ++++++++++++++ .../lucene/document/BaseXYShapeTestCase.java | 23 +++ .../document/TestLatLonPointShapeQueries.java | 8 +- .../document/TestXYPointShapeQueries.java | 6 +- .../org/apache/lucene/geo/TestPoint2D.java | 80 +++++++++++ 12 files changed, 660 insertions(+), 8 deletions(-) create mode 100644 lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapePointQuery.java create mode 100644 lucene/sandbox/src/java/org/apache/lucene/document/XYShapePointQuery.java create mode 100644 lucene/sandbox/src/java/org/apache/lucene/geo/Point2D.java create mode 100644 lucene/sandbox/src/test/org/apache/lucene/geo/TestPoint2D.java diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 7a7122ba04a..910553f0ce4 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -93,7 +93,8 @@ API Changes New Features --------------------- -(No changes) + +* LUCENE-8903: Add LatLonShape point query. (Ignacio Vera) Improvements --------------------- diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java index 487afb8665f..8bcb49f2404 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java @@ -132,4 +132,17 @@ public class LatLonShape { } return new LatLonShapePolygonQuery(field, queryRelation, polygons); } + + /** create a query to find all indexed shapes that comply the {@link QueryRelation} with the provided point + **/ + public static Query newPointQuery(String field, QueryRelation queryRelation, double[]... points) { + if (queryRelation == QueryRelation.CONTAINS && points.length > 1) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (int i =0; i < points.length; i++) { + builder.add(newPointQuery(field, queryRelation, points[i]), BooleanClause.Occur.MUST); + } + return builder.build(); + } + return new LatLonShapePointQuery(field, queryRelation, points); + } } diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapePointQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapePointQuery.java new file mode 100644 index 00000000000..8c55ec6c9a2 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapePointQuery.java @@ -0,0 +1,125 @@ +/* + * 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.util.Arrays; + +import org.apache.lucene.geo.Component2D; +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.Point2D; +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.NumericUtils; + +/** + * Finds all previously indexed shapes that intersect the specified bounding box. + * + *

The field must be indexed using + * {@link LatLonShape#createIndexableFields} added per document. + * + * @lucene.experimental + **/ +final class LatLonShapePointQuery extends ShapeQuery { + final Component2D point2D; + final double[][] points; + + public LatLonShapePointQuery(String field, ShapeField.QueryRelation queryRelation, double[][] points) { + super(field, queryRelation); + this.points = points; + this.point2D = Point2D.create(points); + } + + @Override + protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle, + int maxXOffset, int maxYOffset, byte[] maxTriangle) { + double minLat = GeoEncodingUtils.decodeLatitude(NumericUtils.sortableBytesToInt(minTriangle, minYOffset)); + double minLon = GeoEncodingUtils.decodeLongitude(NumericUtils.sortableBytesToInt(minTriangle, minXOffset)); + double maxLat = GeoEncodingUtils.decodeLatitude(NumericUtils.sortableBytesToInt(maxTriangle, maxYOffset)); + double maxLon = GeoEncodingUtils.decodeLongitude(NumericUtils.sortableBytesToInt(maxTriangle, maxXOffset)); + + // check internal node against query + return point2D.relate(minLon, maxLon, minLat, maxLat); + } + + /** returns true if the query matches the encoded triangle */ + @Override + protected boolean queryMatches(byte[] t, ShapeField.DecodedTriangle scratchTriangle, ShapeField.QueryRelation queryRelation) { + ShapeField.decodeTriangle(t, scratchTriangle); + + double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); + double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); + double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY); + double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX); + double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle.cY); + double clon = GeoEncodingUtils.decodeLongitude(scratchTriangle.cX); + + switch (queryRelation) { + case INTERSECTS: + return point2D.relateTriangle(alon, alat, blon, blat, clon, clat) != Relation.CELL_OUTSIDE_QUERY; + case WITHIN: + return point2D.relateTriangle(alon, alat, blon, blat, clon, clat) == Relation.CELL_INSIDE_QUERY; + case DISJOINT: + return point2D.relateTriangle(alon, alat, blon, blat, clon, clat) == Relation.CELL_OUTSIDE_QUERY; + default: + throw new IllegalArgumentException("Unsupported query type :[" + queryRelation + "]"); + } + } + + @Override + protected Component2D.WithinRelation queryWithin(byte[] t, ShapeField.DecodedTriangle scratchTriangle) { + ShapeField.decodeTriangle(t, scratchTriangle); + + double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); + double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); + double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY); + double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX); + double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle.cY); + double clon = GeoEncodingUtils.decodeLongitude(scratchTriangle.cX); + + return point2D.withinTriangle(alon, alat, scratchTriangle.ab, blon, blat, scratchTriangle.bc, clon, clat, scratchTriangle.ca); + } + + @Override + public boolean equals(Object o) { + return sameClassAs(o) && equalsTo(getClass().cast(o)); + } + + @Override + protected boolean equalsTo(Object o) { + return super.equalsTo(o) && Arrays.equals(points, ((LatLonShapePointQuery)o).points); + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + hash = 31 * hash + Arrays.hashCode(points); + return hash; + } + + @Override + public String toString(String field) { + final StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()); + sb.append(':'); + if (this.field.equals(field) == false) { + sb.append(" field="); + sb.append(this.field); + sb.append(':'); + } + sb.append("lat = " + points[0][0] + " , lon = " + points[0][1]); + return sb.toString(); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/XYShape.java b/lucene/sandbox/src/java/org/apache/lucene/document/XYShape.java index a28922374bf..395c7f8dab8 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/XYShape.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/XYShape.java @@ -116,4 +116,17 @@ public class XYShape { } return new XYShapePolygonQuery(field, queryRelation, polygons); } + + /** create a query to find all indexed shapes that comply the {@link QueryRelation} with the provided point + **/ + public static Query newPointQuery(String field, QueryRelation queryRelation, float[]... points) { + if (queryRelation == QueryRelation.CONTAINS && points.length > 1) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (int i =0; i < points.length; i++) { + builder.add(newPointQuery(field, queryRelation, points[i]), BooleanClause.Occur.MUST); + } + return builder.build(); + } + return new XYShapePointQuery(field, queryRelation, points); + } } diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/XYShapePointQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/XYShapePointQuery.java new file mode 100644 index 00000000000..10bf296b350 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/XYShapePointQuery.java @@ -0,0 +1,127 @@ +/* + * 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.util.Arrays; + +import org.apache.lucene.geo.Component2D; +import org.apache.lucene.geo.Point2D; +import org.apache.lucene.geo.XYEncodingUtils; +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.NumericUtils; + +import static org.apache.lucene.geo.XYEncodingUtils.decode; + +/** + * Finds all previously indexed shapes that intersect the specified bounding box. + * + *

The field must be indexed using + * {@link XYShape#createIndexableFields} added per document. + * + * @lucene.experimental + **/ +final class XYShapePointQuery extends ShapeQuery { + final Component2D point2D; + final float[][] points; + + public XYShapePointQuery(String field, ShapeField.QueryRelation queryRelation, float[][] points) { + super(field, queryRelation); + this.points = points; + this.point2D = Point2D.create(points); + } + + @Override + protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle, + int maxXOffset, int maxYOffset, byte[] maxTriangle) { + double minY = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(minTriangle, minYOffset)); + double minX = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(minTriangle, minXOffset)); + double maxY = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(maxTriangle, maxYOffset)); + double maxX = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(maxTriangle, maxXOffset)); + + // check internal node against query + return point2D.relate(minX, maxX, minY, maxY); + } + + /** returns true if the query matches the encoded triangle */ + @Override + protected boolean queryMatches(byte[] t, ShapeField.DecodedTriangle scratchTriangle, ShapeField.QueryRelation queryRelation) { + ShapeField.decodeTriangle(t, scratchTriangle); + + double aY = decode(scratchTriangle.aY); + double aX = decode(scratchTriangle.aX); + double bY = decode(scratchTriangle.bY); + double bX = decode(scratchTriangle.bX); + double cY = decode(scratchTriangle.cY); + double cX = decode(scratchTriangle.cX); + + switch (queryRelation) { + case INTERSECTS: + return point2D.relateTriangle(aX, aY, bX, bY, cX, cY) != Relation.CELL_OUTSIDE_QUERY; + case WITHIN: + return point2D.relateTriangle(aX, aY, bX, bY, cX, cY) == Relation.CELL_INSIDE_QUERY; + case DISJOINT: + return point2D.relateTriangle(aX, aY, bX, bY, cX, cY) == Relation.CELL_OUTSIDE_QUERY; + default: + throw new IllegalArgumentException("Unsupported query type :[" + queryRelation + "]"); + } + } + + @Override + protected Component2D.WithinRelation queryWithin(byte[] t, ShapeField.DecodedTriangle scratchTriangle) { + ShapeField.decodeTriangle(t, scratchTriangle); + + double aY = decode(scratchTriangle.aY); + double aX = decode(scratchTriangle.aX); + double bY = decode(scratchTriangle.bY); + double bX = decode(scratchTriangle.bX); + double cY = decode(scratchTriangle.cY); + double cX = decode(scratchTriangle.cX); + + return point2D.withinTriangle(aX, aY, scratchTriangle.ab, bX, bY, scratchTriangle.bc, cX, cY, scratchTriangle.ca); + } + + @Override + public boolean equals(Object o) { + return sameClassAs(o) && equalsTo(getClass().cast(o)); + } + + @Override + protected boolean equalsTo(Object o) { + return super.equalsTo(o) && Arrays.equals(points, ((XYShapePointQuery)o).points); + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + hash = 31 * hash + Arrays.hashCode(points); + return hash; + } + + @Override + public String toString(String field) { + final StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()); + sb.append(':'); + if (this.field.equals(field) == false) { + sb.append(" field="); + sb.append(this.field); + sb.append(':'); + } + sb.append("lat = " + points[0][0] + " , lon = " + points[0][1]); + return sb.toString(); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/Point2D.java b/lucene/sandbox/src/java/org/apache/lucene/geo/Point2D.java new file mode 100644 index 00000000000..4547a22c727 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/Point2D.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.lucene.geo; + +import org.apache.lucene.index.PointValues; + +import static org.apache.lucene.geo.GeoUtils.orient; + +/** + * 2D point implementation containing geo spatial logic. + * + * @lucene.internal + */ +public class Point2D implements Component2D { + + final private double x; + final private double y; + + Point2D(double x, double y) { + this.x = x; + this.y = y; + } + + @Override + public double getMinX() { + return x; + } + + @Override + public double getMaxX() { + return x; + } + + @Override + public double getMinY() { + return y; + } + + @Override + public double getMaxY() { + return y; + } + + @Override + public boolean contains(double x, double y) { + return x == this.x && y == this.y; + } + + @Override + public PointValues.Relation relate(double minX, double maxX, double minY, double maxY) { + if (Component2D.containsPoint(x, y, minX, maxX, minY, maxY)) { + return PointValues.Relation.CELL_CROSSES_QUERY; + } + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + + @Override + public PointValues.Relation relateTriangle(double minX, double maxX, double minY, double maxY, + double ax, double ay, double bx, double by, double cx, double cy) { + if (Component2D.containsPoint(x, y, minX, maxX, minY, maxY) == false) { + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + if (ax == bx && bx == cx && ay == by && by == cy) { + return PointValues.Relation.CELL_INSIDE_QUERY; + } else if (ax == cx && ay == cy) { + // indexed "triangle" is a line: + if (orient(ax, ay, bx, by, x, y) == 0) { + return PointValues.Relation.CELL_INSIDE_QUERY; + } + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } else if (ax == bx && ay == by) { + // indexed "triangle" is a line: + if (orient(bx, by, cx, cy, x, y) == 0) { + return PointValues.Relation.CELL_INSIDE_QUERY; + } + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } else if (bx == cx && by == cy) { + // indexed "triangle" is a line: + if (orient(cx, cy, ax, ay, x, y) == 0) { + return PointValues.Relation.CELL_INSIDE_QUERY; + } + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } else if (Component2D.pointInTriangle(minX, maxX, minY, maxY, x, y, ax, ay, bx, by, cx, cy) == true) { + // indexed "triangle" is a triangle: + return PointValues.Relation.CELL_INSIDE_QUERY; + } + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + + @Override + public WithinRelation withinTriangle(double minX, double maxX, double minY, double maxY, + double aX, double aY, boolean ab, double bX, double bY, boolean bc, double cX, double cY, boolean ca) { + if (aX == bX && aY == bY && aX == cX && aY == cY) { + if (contains(aX, aY)) { + return WithinRelation.CANDIDATE; + } + } + return WithinRelation.DISJOINT; + } + + /** create a Point2D component tree from provided array of LatLon points. */ + public static Component2D create(double[]... points) { + Point2D components[] = new Point2D[points.length]; + for (int i = 0; i < components.length; ++i) { + components[i] = new Point2D(GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(points[i][1])) + , GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(points[i][0]))); + } + return ComponentTree.create(components); + } + + /** create a Point2D component tree from provided array of XY points. */ + public static Component2D create(float[]... xyPoints) { + Point2D components[] = new Point2D[xyPoints.length]; + for (int i = 0; i < components.length; ++i) { + components[i] = new Point2D(xyPoints[i][0], xyPoints[i][1]); + } + return ComponentTree.create(components); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java index d5f2ddd616f..6208b9ed973 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java @@ -24,11 +24,13 @@ import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line2D; +import org.apache.lucene.geo.Point2D; import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon2D; import org.apache.lucene.geo.Rectangle; import org.apache.lucene.search.Query; import org.apache.lucene.search.QueryUtils; +import org.apache.lucene.util.TestUtil; import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude; import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude; @@ -66,6 +68,11 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase { return LatLonShape.newPolygonQuery(field, queryRelation, Arrays.stream(polygons).toArray(Polygon[]::new)); } + @Override + protected Query newPointsQuery(String field, QueryRelation queryRelation, Object... points) { + return LatLonShape.newPointQuery(field, queryRelation, Arrays.stream(points).toArray(double[][]::new)); + } + @Override protected Component2D toLine2D(Object... lines) { return Line2D.create(Arrays.stream(lines).toArray(Line[]::new)); @@ -76,11 +83,27 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase { return Polygon2D.create(Arrays.stream(polygons).toArray(Polygon[]::new)); } + @Override + protected Component2D toPoint2D(Object... points) { + return Point2D.create(Arrays.stream(points).toArray(double[][]::new)); + } + @Override public Rectangle randomQueryBox() { return GeoTestUtil.nextBox(); } + @Override + protected Object[] nextPoints() { + int numPoints = TestUtil.nextInt(random(), 1, 20); + double[][] points = new double[numPoints][2]; + for (int i = 0; i < numPoints; i++) { + points[i][0] = nextLatitude(); + points[i][1] = nextLongitude(); + } + return points; + } + @Override protected double rectMinX(Object rect) { return ((Rectangle)rect).minLon; @@ -162,6 +185,11 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase { return LatLonShape.newPolygonQuery(field, queryRelation, polygons); } + /** factory method to create a new point query */ + protected Query newPointQuery(String field, QueryRelation queryRelation, double[]... points) { + return LatLonShape.newPointQuery(field, queryRelation, points); + } + public void testPolygonQueryEqualsAndHashcode() { Polygon polygon = GeoTestUtil.nextPolygon(); QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values()); diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/BaseShapeTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/document/BaseShapeTestCase.java index 606790d7d4c..20947b4c4c1 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/BaseShapeTestCase.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/BaseShapeTestCase.java @@ -161,6 +161,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase { protected abstract Object randomQueryBox(); + protected abstract Object[] nextPoints(); + protected abstract double rectMinX(Object rect); protected abstract double rectMaxX(Object rect); protected abstract double rectMinY(Object rect); @@ -189,10 +191,15 @@ public abstract class BaseShapeTestCase extends LuceneTestCase { /** factory method to create a new polygon query */ protected abstract Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons); + /** factory method to create a new polygon query */ + protected abstract Query newPointsQuery(String field, QueryRelation queryRelation, Object... points); + protected abstract Component2D toLine2D(Object... line); protected abstract Component2D toPolygon2D(Object... polygon); + protected abstract Component2D toPoint2D(Object... points); + private void verify(Object... shapes) throws Exception { IndexWriterConfig iwc = newIndexWriterConfig(); iwc.setMergeScheduler(new SerialMergeScheduler()); @@ -251,6 +258,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase { verifyRandomLineQueries(reader, shapes); // test random polygon queries verifyRandomPolygonQueries(reader, shapes); + // test random point queries + verifyRandomPointQueries(reader, shapes); } /** test random generated bounding boxes */ @@ -547,6 +556,105 @@ public abstract class BaseShapeTestCase extends LuceneTestCase { } } + /** test random generated point queries */ + protected void verifyRandomPointQueries(IndexReader reader, Object... shapes) throws Exception { + IndexSearcher s = newSearcher(reader); + + final int iters = scaledIterationCount(shapes.length); + + 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+1) + " of " + iters + " s=" + s); + } + + Object[] queryPoints = nextPoints(); + QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values()); + Component2D queryPoly2D; + Query query; + if (queryRelation == QueryRelation.CONTAINS) { + queryPoly2D = toPoint2D(queryPoints[0]); + query = newPointsQuery(FIELD_NAME, queryRelation, queryPoints[0]); + } else { + queryPoly2D = toPoint2D(queryPoints); + query = newPointsQuery(FIELD_NAME, queryRelation, queryPoints); + } + + if (VERBOSE) { + System.out.println(" query=" + query + ", relation=" + queryRelation); + } + + final FixedBitSet hits = new FixedBitSet(maxDoc); + s.search(query, new SimpleCollector() { + + private int docBase; + + @Override + public ScoreMode scoreMode() { + return ScoreMode.COMPLETE_NO_SCORES; + } + + @Override + protected void doSetNextReader(LeafReaderContext context) throws IOException { + docBase = context.docBase; + } + + @Override + public void collect(int doc) throws IOException { + hits.set(docBase+doc); + } + }); + + boolean fail = false; + NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id"); + for (int docID = 0; docID < maxDoc; ++docID) { + assertEquals(docID, docIDToID.nextDoc()); + int id = (int) docIDToID.longValue(); + boolean expected; + + if (liveDocs != null && liveDocs.get(docID) == false) { + // document is deleted + expected = false; + } else if (shapes[id] == null) { + expected = false; + } else { + expected = VALIDATOR.setRelation(queryRelation).testComponentQuery(queryPoly2D, shapes[id]); + } + + if (hits.get(docID) != expected) { + StringBuilder b = new StringBuilder(); + + if (expected) { + b.append("FAIL: id=" + id + " should match but did not\n"); + } else { + b.append("FAIL: id=" + id + " should not match but did\n"); + } + b.append(" relation=" + queryRelation + "\n"); + b.append(" query=" + query + " docID=" + docID + "\n"); + if (shapes[id] instanceof Object[]) { + b.append(" shape=" + Arrays.toString((Object[]) shapes[id]) + "\n"); + } else { + b.append(" shape=" + shapes[id] + "\n"); + } + b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false)); + b.append(" rect=Points(" + Arrays.toString(queryPoints) + ")\n"); + if (true) { + fail("wrong hit (first of possibly more):\n\n" + b); + } else { + System.out.println(b.toString()); + fail = true; + } + } + } + if (fail) { + fail("some hits were wrong"); + } + } + } + + protected abstract Validator getValidator(); protected static abstract class Encoder { diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java index a60356a0121..5f9035f7c6e 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java @@ -22,12 +22,14 @@ import com.carrotsearch.randomizedtesting.generators.RandomPicks; import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.Line2D; +import org.apache.lucene.geo.Point2D; import org.apache.lucene.geo.ShapeTestUtil; import org.apache.lucene.geo.XYLine; import org.apache.lucene.geo.XYPolygon; import org.apache.lucene.geo.XYPolygon2D; import org.apache.lucene.geo.XYRectangle; import org.apache.lucene.search.Query; +import org.apache.lucene.util.TestUtil; import static org.apache.lucene.geo.XYEncodingUtils.decode; import static org.apache.lucene.geo.XYEncodingUtils.encode; @@ -58,6 +60,16 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase { return XYShape.newPolygonQuery(field, queryRelation, Arrays.stream(polygons).toArray(XYPolygon[]::new)); } + @Override + protected Query newPointsQuery(String field, QueryRelation queryRelation, Object... points) { + return XYShape.newPointQuery(field, queryRelation, Arrays.stream(points).toArray(float[][]::new)); + } + + @Override + protected Component2D toPoint2D(Object... points) { + return Point2D.create(Arrays.stream(points).toArray(float[][]::new)); + } + @Override protected Component2D toLine2D(Object... lines) { return Line2D.create(Arrays.stream(lines).toArray(XYLine[]::new)); @@ -121,6 +133,17 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase { return ShapeTestUtil.nextPolygon(); } + @Override + protected Object[] nextPoints() { + int numPoints = TestUtil.nextInt(random(), 1, 20); + float[][] points = new float[numPoints][2]; + for (int i = 0; i < numPoints; i++) { + points[i][0] = (float) ShapeTestUtil.nextDouble(); + points[i][1] = (float) ShapeTestUtil.nextDouble(); + } + return points; + } + @Override protected Encoder getEncoder() { return new Encoder() { diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java index dd741d7a29d..313c13c2bd1 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java @@ -93,12 +93,12 @@ public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase { @Override public boolean testComponentQuery(Component2D query, Object shape) { - if (queryRelation == QueryRelation.CONTAINS) { - return false; - } - Point p = (Point) shape; + Point p = (Point) shape; double lat = encoder.quantizeY(p.lat); double lon = encoder.quantizeX(p.lon); + if (queryRelation == QueryRelation.CONTAINS) { + return query.withinTriangle(lon, lat, true, lon, lat, true, lon, lat, true) == Component2D.WithinRelation.CANDIDATE; + } // for consistency w/ the query we test the point as a triangle Relation r = query.relateTriangle(lon, lat, lon, lat, lon, lat); if (queryRelation == QueryRelation.WITHIN) { diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestXYPointShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYPointShapeQueries.java index b53e90a5868..eabb0332c3f 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestXYPointShapeQueries.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYPointShapeQueries.java @@ -82,12 +82,12 @@ public class TestXYPointShapeQueries extends BaseXYShapeTestCase { @Override public boolean testComponentQuery(Component2D query, Object shape) { - if (queryRelation == QueryRelation.CONTAINS) { - return false; - } Point p = (Point) shape; double lat = encoder.quantizeY(p.y); double lon = encoder.quantizeX(p.x); + if (queryRelation == QueryRelation.CONTAINS) { + return query.withinTriangle(lon, lat, true, lon, lat, true, lon, lat, true) == Component2D.WithinRelation.CANDIDATE; + } // for consistency w/ the query we test the point as a triangle Relation r = query.relateTriangle(lon, lat, lon, lat, lon, lat); if (queryRelation == QueryRelation.WITHIN) { diff --git a/lucene/sandbox/src/test/org/apache/lucene/geo/TestPoint2D.java b/lucene/sandbox/src/test/org/apache/lucene/geo/TestPoint2D.java new file mode 100644 index 00000000000..e6609ad31af --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/geo/TestPoint2D.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.lucene.geo; + +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.LuceneTestCase; + +public class TestPoint2D extends LuceneTestCase { + + public void testTriangleDisjoint() { + Component2D point2D = Point2D.create(new double[] {0, 0}); + double ax = 4; + double ay = 4; + double bx = 5; + double by = 5; + double cx = 5; + double cy = 4; + assertEquals(Relation.CELL_OUTSIDE_QUERY, point2D.relateTriangle(ax, ay, bx, by , cx, cy)); + } + + public void testTriangleIntersects() { + Component2D point2D = Point2D.create(new double[] {0, 0}); + double ax = 0.0; + double ay = 0.0; + double bx = 1; + double by = 0; + double cx = 0; + double cy = 1; + assertEquals(Relation.CELL_INSIDE_QUERY, point2D.relateTriangle(ax, ay, bx, by , cx, cy)); + } + + public void testTriangleContains() { + Component2D point2D = Point2D.create(new double[] {0, 0}); + double ax = 0.0; + double ay = 0.0; + double bx = 0; + double by = 0; + double cx = 0; + double cy = 0; + assertEquals(Relation.CELL_INSIDE_QUERY, point2D.relateTriangle(ax, ay, bx, by , cx, cy)); + } + + public void testRandomTriangles() { + Component2D point2D = Point2D.create(new double[] {GeoTestUtil.nextLatitude(), GeoTestUtil.nextLongitude()}); + + for (int i =0; i < 100; i++) { + double ax = GeoTestUtil.nextLongitude(); + double ay = GeoTestUtil.nextLatitude(); + double bx = GeoTestUtil.nextLongitude(); + double by = GeoTestUtil.nextLatitude(); + double cx = GeoTestUtil.nextLongitude(); + double cy = GeoTestUtil.nextLatitude(); + + double tMinX = StrictMath.min(StrictMath.min(ax, bx), cx); + double tMaxX = StrictMath.max(StrictMath.max(ax, bx), cx); + double tMinY = StrictMath.min(StrictMath.min(ay, by), cy); + double tMaxY = StrictMath.max(StrictMath.max(ay, by), cy); + + Relation r = point2D.relate(tMinX, tMaxX, tMinY, tMaxY); + if (r == Relation.CELL_OUTSIDE_QUERY) { + assertEquals(Relation.CELL_OUTSIDE_QUERY, point2D.relateTriangle(ax, ay, bx, by, cx, cy)); + } + } + } +}