From commits-return-120491-archive-asf-public=cust-asf.ponee.io@lucene.apache.org Fri Jan 15 21:45:30 2021 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mxout1-he-de.apache.org (mxout1-he-de.apache.org [95.216.194.37]) by mx-eu-01.ponee.io (Postfix) with ESMTPS id B4CC01806D6 for ; Fri, 15 Jan 2021 22:45:30 +0100 (CET) Received: from mail.apache.org (mailroute1-lw-us.apache.org [207.244.88.153]) by mxout1-he-de.apache.org (ASF Mail Server at mxout1-he-de.apache.org) with SMTP id 1FF236697D for ; Fri, 15 Jan 2021 21:45:27 +0000 (UTC) Received: (qmail 32823 invoked by uid 500); 15 Jan 2021 21:45:17 -0000 Mailing-List: contact commits-help@lucene.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@lucene.apache.org Delivered-To: mailing list commits@lucene.apache.org Received: (qmail 32752 invoked by uid 99); 15 Jan 2021 21:45:17 -0000 Received: from ec2-52-202-80-70.compute-1.amazonaws.com (HELO gitbox.apache.org) (52.202.80.70) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 15 Jan 2021 21:45:17 +0000 Received: by gitbox.apache.org (ASF Mail Server at gitbox.apache.org, from userid 33) id 536E08E7AC; Fri, 15 Jan 2021 21:45:17 +0000 (UTC) Date: Fri, 15 Jan 2021 21:45:25 +0000 To: "commits@lucene.apache.org" Subject: [lucene-solr] 14/38: LUCENE-9641: Support for spatial relationships in LatLonPoint (#2155) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit From: ctargett@apache.org In-Reply-To: <161074711064.22108.13586738973927348560@gitbox.apache.org> References: <161074711064.22108.13586738973927348560@gitbox.apache.org> X-Git-Host: gitbox.apache.org X-Git-Repo: lucene-solr X-Git-Refname: refs/heads/jira/solr-13105-toMerge X-Git-Reftype: branch X-Git-Rev: f5a1c7ff06c29adb7effbcb217626e54dad84623 X-Git-NotificationType: diff X-Git-Multimail-Version: 1.5.dev Auto-Submitted: auto-generated Message-Id: <20210115214517.536E08E7AC@gitbox.apache.org> This is an automated email from the ASF dual-hosted git repository. ctargett pushed a commit to branch jira/solr-13105-toMerge in repository https://gitbox.apache.org/repos/asf/lucene-solr.git commit f5a1c7ff06c29adb7effbcb217626e54dad84623 Author: Ignacio Vera AuthorDate: Fri Jan 8 08:16:58 2021 +0100 LUCENE-9641: Support for spatial relationships in LatLonPoint (#2155) Equivalent to LatLonShape, LatLonPoint can be queried now using spatial relationships. --- lucene/CHANGES.txt | 4 +- .../lucene/document/LatLonDocValuesField.java | 37 ++- .../LatLonDocValuesPointInGeometryQuery.java | 166 ----------- .../lucene/document/LatLonDocValuesQuery.java | 272 ++++++++++++++++++ .../org/apache/lucene/document/LatLonPoint.java | 36 ++- .../document/LatLonPointInGeometryQuery.java | 300 -------------------- .../apache/lucene/document/LatLonPointQuery.java | 182 ++++++++++++ .../document/LatLonShapeBoundingBoxQuery.java | 274 +++++++++--------- .../apache/lucene/document/LatLonShapeQuery.java | 270 +++++++++--------- .../{ShapeQuery.java => SpatialQuery.java} | 314 +++++++++++---------- .../org/apache/lucene/document/XYShapeQuery.java | 268 ++++++++++-------- .../org/apache/lucene/geo/GeoEncodingUtils.java | 40 +-- .../src/java/org/apache/lucene/geo/Point2D.java | 14 +- .../document/BaseLatLonDocValueTestCase.java | 72 +++++ .../lucene/document/BaseLatLonPointTestCase.java | 139 +++++++++ .../lucene/document/BaseLatLonShapeTestCase.java | 283 +++---------------- .../lucene/document/BaseLatLonSpatialTestCase.java | 220 +++++++++++++++ ...ShapeTestCase.java => BaseSpatialTestCase.java} | 6 +- .../lucene/document/BaseXYShapeTestCase.java | 2 +- .../TestLatLonDocValuesMultiPointPointQueries.java | 99 +++++++ .../TestLatLonDocValuesPointPointQueries.java | 71 +++++ .../document/TestLatLonMultiPointPointQueries.java | 99 +++++++ .../document/TestLatLonPointPointQueries.java | 69 +++++ .../apache/lucene/document/TestLatLonShape.java | 9 + .../lucene/search/TestLatLonDocValuesQueries.java | 4 +- .../lucene/search/TestLatLonPointQueries.java | 3 +- 26 files changed, 1973 insertions(+), 1280 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index bb5bdb3..0bda4fc 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -237,7 +237,9 @@ 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) +* LUCENE-9552: New LatLonPoint query that accepts an array of LatLonGeometries. (Ignacio Vera) + +* LUCENE-9641: LatLonPoint query support for spatial relationships. (Ignacio Vera) * LUCENE-9553: New XYPoint query that accepts an array of XYGeometries. (Ignacio Vera) 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 8c80363..1a5dfa8 100644 --- a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java +++ b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java @@ -23,6 +23,7 @@ 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.Point; import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Rectangle; import org.apache.lucene.index.DocValuesType; @@ -209,7 +210,7 @@ public class LatLonDocValuesField extends Field { public static Query newSlowDistanceQuery( String field, double latitude, double longitude, double radiusMeters) { Circle circle = new Circle(latitude, longitude, radiusMeters); - return newSlowGeometryQuery(field, circle); + return newSlowGeometryQuery(field, ShapeField.QueryRelation.INTERSECTS, circle); } /** @@ -225,28 +226,42 @@ public class LatLonDocValuesField extends Field { * null polygon. */ public static Query newSlowPolygonQuery(String field, Polygon... polygons) { - return newSlowGeometryQuery(field, polygons); + return newSlowGeometryQuery(field, ShapeField.QueryRelation.INTERSECTS, 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 + * Create a query for matching one or more geometries against the provided {@link + * ShapeField.QueryRelation}. Line geometries are not supported for WITHIN relationship. 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...)}. + * ShapeField.QueryRelation, LatLonGeometry...)}. * * @param field field name. must not be null. + * @param queryRelation The relation the points needs to satisfy with the provided geometries, + * 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. + * @throws IllegalArgumentException if {@code field} is null, {@code queryRelation} 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) { + public static Query newSlowGeometryQuery( + String field, ShapeField.QueryRelation queryRelation, LatLonGeometry... latLonGeometries) { + if (queryRelation == ShapeField.QueryRelation.INTERSECTS + && 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); + if (queryRelation == ShapeField.QueryRelation.CONTAINS) { + for (LatLonGeometry geometry : latLonGeometries) { + if ((geometry instanceof Point) == false) { + return new MatchNoDocsQuery( + "Contains LatLonDocValuesField.newSlowGeometryQuery with non-point geometries"); + } + } + } + return new LatLonDocValuesQuery(field, queryRelation, latLonGeometries); } } diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesPointInGeometryQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesPointInGeometryQuery.java deleted file mode 100644 index 5bb1550..0000000 --- a/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesPointInGeometryQuery.java +++ /dev/null @@ -1,166 +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 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.Line; -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; - -/** Geometry query for {@link LatLonDocValuesField}. */ -public class LatLonDocValuesPointInGeometryQuery extends Query { - - private final String field; - private final LatLonGeometry[] geometries; - - LatLonDocValuesPointInGeometryQuery(String field, LatLonGeometry... geometries) { - if (field == null) { - throw new IllegalArgumentException("field must not be null"); - } - if (geometries == null) { - throw new IllegalArgumentException("geometries must not be null"); - } - if (geometries.length == 0) { - throw new IllegalArgumentException("geometries must not be empty"); - } - 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.geometries = geometries; - } - - @Override - public String toString(String field) { - StringBuilder sb = new StringBuilder(); - if (!this.field.equals(field)) { - sb.append(this.field); - sb.append(':'); - } - sb.append("geometries(").append(Arrays.toString(geometries)); - return sb.append(")").toString(); - } - - @Override - public boolean equals(Object obj) { - if (sameClassAs(obj) == false) { - return false; - } - LatLonDocValuesPointInGeometryQuery other = (LatLonDocValuesPointInGeometryQuery) obj; - return field.equals(other.field) && Arrays.equals(geometries, other.geometries); - } - - @Override - public int hashCode() { - int h = classHash(); - h = 31 * h + field.hashCode(); - h = 31 * h + Arrays.hashCode(geometries); - return h; - } - - @Override - public void visit(QueryVisitor visitor) { - if (visitor.acceptField(field)) { - visitor.visitLeaf(this); - } - } - - @Override - public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) - throws IOException { - final Component2D tree = LatLonGeometry.create(geometries); - - if (tree.getMinY() > tree.getMaxY()) { - // encodeLatitudeCeil may cause minY to be > maxY iff - // the delta between the longitude < the encoding resolution - return new ConstantScoreWeight(this, boost) { - @Override - public Scorer scorer(LeafReaderContext context) { - return null; - } - - @Override - public boolean isCacheable(LeafReaderContext ctx) { - return false; - } - }; - } - - final GeoEncodingUtils.Component2DPredicate component2DPredicate = - GeoEncodingUtils.createComponentPredicate(tree); - - return new ConstantScoreWeight(this, boost) { - - @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 (component2DPredicate.test(lat, lon)) { - return true; - } - } - return false; - } - - @Override - public float matchCost() { - return 1000f; // 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/LatLonDocValuesQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesQuery.java new file mode 100644 index 0000000..8e8120f --- /dev/null +++ b/lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesQuery.java @@ -0,0 +1,272 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.lucene.document; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.apache.lucene.geo.Component2D; +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.LatLonGeometry; +import org.apache.lucene.geo.Line; +import org.apache.lucene.geo.Point; +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; + +/** + * Finds all previously indexed geo points that comply the given {@link ShapeField.QueryRelation} + * with the specified array of {@link LatLonGeometry}. + * + *

The field must be indexed using {@link LatLonDocValuesField} added per document. + */ +class LatLonDocValuesQuery extends Query { + + private final String field; + private final LatLonGeometry[] geometries; + private final ShapeField.QueryRelation queryRelation; + private final Component2D component2D; + + LatLonDocValuesQuery( + String field, ShapeField.QueryRelation queryRelation, LatLonGeometry... geometries) { + if (field == null) { + throw new IllegalArgumentException("field must not be null"); + } + if (queryRelation == null) { + throw new IllegalArgumentException("queryRelation must not be null"); + } + if (queryRelation == ShapeField.QueryRelation.WITHIN) { + for (LatLonGeometry geometry : geometries) { + if (geometry instanceof Line) { + // TODO: line queries do not support within relations + throw new IllegalArgumentException( + "LatLonDocValuesPointQuery does not support " + + ShapeField.QueryRelation.WITHIN + + " queries with line geometries"); + } + } + } + if (queryRelation == ShapeField.QueryRelation.CONTAINS) { + for (LatLonGeometry geometry : geometries) { + if ((geometry instanceof Point) == false) { + throw new IllegalArgumentException( + "LatLonDocValuesPointQuery does not support " + + ShapeField.QueryRelation.CONTAINS + + " queries with non-points geometries"); + } + } + } + this.field = field; + this.geometries = geometries; + this.queryRelation = queryRelation; + this.component2D = LatLonGeometry.create(geometries); + } + + @Override + public String toString(String field) { + StringBuilder sb = new StringBuilder(); + if (!this.field.equals(field)) { + sb.append(this.field); + sb.append(':'); + } + sb.append(queryRelation).append(':'); + sb.append("geometries(").append(Arrays.toString(geometries)); + return sb.append(")").toString(); + } + + @Override + public boolean equals(Object obj) { + if (sameClassAs(obj) == false) { + return false; + } + LatLonDocValuesQuery other = (LatLonDocValuesQuery) obj; + return field.equals(other.field) + && queryRelation == other.queryRelation + && Arrays.equals(geometries, other.geometries); + } + + @Override + public int hashCode() { + int h = classHash(); + h = 31 * h + field.hashCode(); + h = 31 * h + queryRelation.hashCode(); + h = 31 * h + Arrays.hashCode(geometries); + return h; + } + + @Override + public void visit(QueryVisitor visitor) { + if (visitor.acceptField(field)) { + visitor.visitLeaf(this); + } + } + + @Override + public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) + throws IOException { + final GeoEncodingUtils.Component2DPredicate component2DPredicate = + queryRelation == ShapeField.QueryRelation.CONTAINS + ? null + : GeoEncodingUtils.createComponentPredicate(component2D); + return new ConstantScoreWeight(this, boost) { + + @Override + public Scorer scorer(LeafReaderContext context) throws IOException { + final SortedNumericDocValues values = context.reader().getSortedNumericDocValues(field); + if (values == null) { + return null; + } + final TwoPhaseIterator iterator; + switch (queryRelation) { + case INTERSECTS: + iterator = intersects(values, component2DPredicate); + break; + case WITHIN: + iterator = within(values, component2DPredicate); + break; + case DISJOINT: + iterator = disjoint(values, component2DPredicate); + break; + case CONTAINS: + iterator = contains(values, geometries); + break; + default: + throw new IllegalArgumentException( + "Invalid query relationship:[" + queryRelation + "]"); + } + return new ConstantScoreScorer(this, boost, scoreMode, iterator); + } + + @Override + public boolean isCacheable(LeafReaderContext ctx) { + return DocValues.isCacheable(ctx, field); + } + }; + } + + private TwoPhaseIterator intersects( + SortedNumericDocValues values, GeoEncodingUtils.Component2DPredicate component2DPredicate) { + return 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 (component2DPredicate.test(lat, lon)) { + return true; + } + } + return false; + } + + @Override + public float matchCost() { + return 1000f; // TODO: what should it be? + } + }; + } + + private TwoPhaseIterator within( + SortedNumericDocValues values, GeoEncodingUtils.Component2DPredicate component2DPredicate) { + return 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 (component2DPredicate.test(lat, lon) == false) { + return false; + } + } + return true; + } + + @Override + public float matchCost() { + return 1000f; // TODO: what should it be? + } + }; + } + + private TwoPhaseIterator disjoint( + SortedNumericDocValues values, GeoEncodingUtils.Component2DPredicate component2DPredicate) { + return 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 (component2DPredicate.test(lat, lon)) { + return false; + } + } + return true; + } + + @Override + public float matchCost() { + return 1000f; // TODO: what should it be? + } + }; + } + + private TwoPhaseIterator contains(SortedNumericDocValues values, LatLonGeometry[] geometries) { + final List component2Ds = new ArrayList<>(geometries.length); + for (int i = 0; i < geometries.length; i++) { + component2Ds.add(LatLonGeometry.create(geometries[i])); + } + return new TwoPhaseIterator(values) { + @Override + public boolean matches() throws IOException { + Component2D.WithinRelation answer = Component2D.WithinRelation.DISJOINT; + for (int i = 0, count = values.docValueCount(); i < count; ++i) { + final long value = values.nextValue(); + final double lat = GeoEncodingUtils.decodeLatitude((int) (value >>> 32)); + final double lon = GeoEncodingUtils.decodeLongitude((int) (value & 0xFFFFFFFF)); + for (Component2D component2D : component2Ds) { + Component2D.WithinRelation relation = component2D.withinPoint(lon, lat); + if (relation == Component2D.WithinRelation.NOTWITHIN) { + return false; + } else if (relation != Component2D.WithinRelation.DISJOINT) { + answer = relation; + } + } + } + return answer == Component2D.WithinRelation.CANDIDATE; + } + + @Override + public float matchCost() { + return 1000f; // TODO: what should it be? + } + }; + } +} 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 06de3cf..f2d5940 100644 --- a/lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java +++ b/lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java @@ -25,6 +25,7 @@ import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil; import org.apache.lucene.geo.Circle; import org.apache.lucene.geo.LatLonGeometry; +import org.apache.lucene.geo.Point; import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Rectangle; import org.apache.lucene.index.FieldInfo; @@ -292,21 +293,25 @@ public class LatLonPoint extends Field { * @see Polygon */ public static Query newPolygonQuery(String field, Polygon... polygons) { - return newGeometryQuery(field, polygons); + return newGeometryQuery(field, ShapeField.QueryRelation.INTERSECTS, polygons); } /** - * Create a query for matching one or more geometries. Line geometries are not supported. + * Create a query for matching one or more geometries against the provided {@link + * ShapeField.QueryRelation}. Line geometries are not supported for WITHIN relationship. * * @param field field name. must not be null. + * @param queryRelation The relation the points needs to satisfy with the provided geometries, + * 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. + * @throws IllegalArgumentException if {@code field} is null, {@code queryRelation} is null, + * {@code latLonGeometries} is null, empty or contain a null. * @see LatLonGeometry */ - public static Query newGeometryQuery(String field, LatLonGeometry... latLonGeometries) { - if (latLonGeometries.length == 1) { + public static Query newGeometryQuery( + String field, ShapeField.QueryRelation queryRelation, LatLonGeometry... latLonGeometries) { + if (queryRelation == ShapeField.QueryRelation.INTERSECTS && 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); @@ -316,7 +321,24 @@ public class LatLonPoint extends Field { return newDistanceQuery(field, circle.getLat(), circle.getLon(), circle.getRadius()); } } - return new LatLonPointInGeometryQuery(field, latLonGeometries); + if (queryRelation == ShapeField.QueryRelation.CONTAINS) { + return makeContainsGeometryQuery(field, latLonGeometries); + } + return new LatLonPointQuery(field, queryRelation, latLonGeometries); + } + + private static Query makeContainsGeometryQuery(String field, LatLonGeometry... latLonGeometries) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (LatLonGeometry geometry : latLonGeometries) { + if ((geometry instanceof Point) == false) { + return new MatchNoDocsQuery( + "Contains LatLonPoint.newGeometryQuery with non-point geometries"); + } + builder.add( + new LatLonPointQuery(field, ShapeField.QueryRelation.CONTAINS, geometry), + BooleanClause.Occur.MUST); + } + return new ConstantScoreQuery(builder.build()); } /** diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonPointInGeometryQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonPointInGeometryQuery.java deleted file mode 100644 index 66ad777..0000000 --- a/lucene/core/src/java/org/apache/lucene/document/LatLonPointInGeometryQuery.java +++ /dev/null @@ -1,300 +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 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; - -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.Line; -import org.apache.lucene.index.FieldInfo; -import org.apache.lucene.index.LeafReader; -import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.index.PointValues; -import org.apache.lucene.index.PointValues.IntersectVisitor; -import org.apache.lucene.index.PointValues.Relation; -import org.apache.lucene.search.ConstantScoreScorer; -import org.apache.lucene.search.ConstantScoreWeight; -import org.apache.lucene.search.DocIdSetIterator; -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.ScorerSupplier; -import org.apache.lucene.search.Weight; -import org.apache.lucene.util.DocIdSetBuilder; -import org.apache.lucene.util.NumericUtils; - -/** - * Finds all previously indexed points that fall within the specified geometries. - * - *

The field must be indexed with using {@link LatLonPoint} added per document. - * - * @lucene.experimental - */ -final class LatLonPointInGeometryQuery extends Query { - final String field; - final LatLonGeometry[] geometries; - - LatLonPointInGeometryQuery(String field, LatLonGeometry[] geometries) { - if (field == null) { - throw new IllegalArgumentException("field must not be null"); - } - if (geometries == null) { - throw new IllegalArgumentException("geometries must not be null"); - } - if (geometries.length == 0) { - throw new IllegalArgumentException("geometries must not be empty"); - } - 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.geometries = geometries.clone(); - } - - @Override - public void visit(QueryVisitor visitor) { - if (visitor.acceptField(field)) { - visitor.visitLeaf(this); - } - } - - private IntersectVisitor getIntersectVisitor( - DocIdSetBuilder result, - Component2D tree, - GeoEncodingUtils.Component2DPredicate component2DPredicate, - byte[] minLat, - byte[] maxLat, - byte[] minLon, - byte[] maxLon) { - return new IntersectVisitor() { - DocIdSetBuilder.BulkAdder adder; - - @Override - public void grow(int count) { - adder = result.grow(count); - } - - @Override - public void visit(int docID) { - adder.add(docID); - } - - @Override - public void visit(int docID, byte[] packedValue) { - if (component2DPredicate.test( - NumericUtils.sortableBytesToInt(packedValue, 0), - NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES))) { - visit(docID); - } - } - - @Override - public void visit(DocIdSetIterator iterator, byte[] packedValue) throws IOException { - if (component2DPredicate.test( - NumericUtils.sortableBytesToInt(packedValue, 0), - NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES))) { - int docID; - while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { - visit(docID); - } - } - } - - @Override - public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) { - if (Arrays.compareUnsigned(minPackedValue, 0, Integer.BYTES, maxLat, 0, Integer.BYTES) > 0 - || Arrays.compareUnsigned(maxPackedValue, 0, Integer.BYTES, minLat, 0, Integer.BYTES) - < 0 - || Arrays.compareUnsigned( - minPackedValue, - Integer.BYTES, - Integer.BYTES + Integer.BYTES, - maxLon, - 0, - Integer.BYTES) - > 0 - || Arrays.compareUnsigned( - maxPackedValue, - Integer.BYTES, - Integer.BYTES + Integer.BYTES, - minLon, - 0, - Integer.BYTES) - < 0) { - // outside of global bounding box range - return Relation.CELL_OUTSIDE_QUERY; - } - - double cellMinLat = decodeLatitude(minPackedValue, 0); - double cellMinLon = decodeLongitude(minPackedValue, Integer.BYTES); - double cellMaxLat = decodeLatitude(maxPackedValue, 0); - double cellMaxLon = decodeLongitude(maxPackedValue, Integer.BYTES); - - return tree.relate(cellMinLon, cellMaxLon, cellMinLat, cellMaxLat); - } - }; - } - - @Override - public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) - throws IOException { - final Component2D tree = LatLonGeometry.create(geometries); - if (tree.getMinY() > tree.getMaxY()) { - // encodeLatitudeCeil may cause minY to be > maxY iff - // the delta between the longitude < the encoding resolution - return new ConstantScoreWeight(this, boost) { - @Override - public Scorer scorer(LeafReaderContext context) { - return null; - } - - @Override - public boolean isCacheable(LeafReaderContext ctx) { - return false; - } - }; - } - 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]; - final byte maxLon[] = new byte[Integer.BYTES]; - NumericUtils.intToSortableBytes(encodeLatitude(tree.getMinY()), minLat, 0); - NumericUtils.intToSortableBytes(encodeLatitude(tree.getMaxY()), maxLat, 0); - NumericUtils.intToSortableBytes(encodeLongitude(tree.getMinX()), minLon, 0); - NumericUtils.intToSortableBytes(encodeLongitude(tree.getMaxX()), maxLon, 0); - - return new ConstantScoreWeight(this, boost) { - - @Override - public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException { - LeafReader reader = context.reader(); - PointValues values = reader.getPointValues(field); - if (values == null) { - // No docs in this segment had any points fields - return null; - } - FieldInfo fieldInfo = reader.getFieldInfos().fieldInfo(field); - if (fieldInfo == null) { - // No docs in this segment indexed this field at all - return null; - } - LatLonPoint.checkCompatible(fieldInfo); - final Weight weight = this; - - return new ScorerSupplier() { - - long cost = -1; - DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field); - final IntersectVisitor visitor = - getIntersectVisitor( - result, tree, component2DPredicate, minLat, maxLat, minLon, maxLon); - - @Override - public Scorer get(long leadCost) throws IOException { - values.intersect(visitor); - return new ConstantScoreScorer(weight, score(), scoreMode, result.build().iterator()); - } - - @Override - public long cost() { - if (cost == -1) { - // Computing the cost may be expensive, so only do it if necessary - cost = values.estimateDocCount(visitor); - assert cost >= 0; - } - return cost; - } - }; - } - - @Override - public Scorer scorer(LeafReaderContext context) throws IOException { - ScorerSupplier scorerSupplier = scorerSupplier(context); - if (scorerSupplier == null) { - return null; - } - return scorerSupplier.get(Long.MAX_VALUE); - } - - @Override - public boolean isCacheable(LeafReaderContext ctx) { - return true; - } - }; - } - - /** Returns the query field */ - public String getField() { - return field; - } - - /** Returns a copy of the internal geometry array */ - public LatLonGeometry[] getGeometries() { - return geometries.clone(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = classHash(); - result = prime * result + field.hashCode(); - result = prime * result + Arrays.hashCode(geometries); - return result; - } - - @Override - public boolean equals(Object other) { - return sameClassAs(other) && equalsTo(getClass().cast(other)); - } - - private boolean equalsTo(LatLonPointInGeometryQuery other) { - return field.equals(other.field) && Arrays.equals(geometries, other.geometries); - } - - @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(Arrays.toString(geometries)); - return sb.toString(); - } -} diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonPointQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonPointQuery.java new file mode 100644 index 0000000..83b3bf9 --- /dev/null +++ b/lucene/core/src/java/org/apache/lucene/document/LatLonPointQuery.java @@ -0,0 +1,182 @@ +/* + * 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 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; + +import java.util.Arrays; +import java.util.function.Function; +import java.util.function.Predicate; +import org.apache.lucene.document.ShapeField.QueryRelation; +import org.apache.lucene.geo.Component2D; +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.LatLonGeometry; +import org.apache.lucene.geo.Line; +import org.apache.lucene.geo.Point; +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.NumericUtils; + +/** + * Finds all previously indexed geo points that comply the given {@link QueryRelation} with the + * specified array of {@link LatLonGeometry}. + * + *

The field must be indexed using one or more {@link LatLonPoint} added per document. + */ +final class LatLonPointQuery extends SpatialQuery { + private final LatLonGeometry[] geometries; + private final Component2D component2D; + + /** + * Creates a query that matches all indexed shapes to the provided array of {@link LatLonGeometry} + */ + LatLonPointQuery(String field, QueryRelation queryRelation, LatLonGeometry... geometries) { + super(field, queryRelation); + if (queryRelation == QueryRelation.WITHIN) { + for (LatLonGeometry geometry : geometries) { + if (geometry instanceof Line) { + // TODO: line queries do not support within relations + throw new IllegalArgumentException( + "LatLonPointQuery does not support " + + QueryRelation.WITHIN + + " queries with line geometries"); + } + } + } + if (queryRelation == ShapeField.QueryRelation.CONTAINS) { + for (LatLonGeometry geometry : geometries) { + if ((geometry instanceof Point) == false) { + throw new IllegalArgumentException( + "LatLonPointQuery does not support " + + ShapeField.QueryRelation.CONTAINS + + " queries with non-points geometries"); + } + } + } + this.component2D = LatLonGeometry.create(geometries); + this.geometries = geometries.clone(); + } + + @Override + protected SpatialVisitor getSpatialVisitor() { + final GeoEncodingUtils.Component2DPredicate component2DPredicate = + GeoEncodingUtils.createComponentPredicate(component2D); + // 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]; + final byte[] maxLon = new byte[Integer.BYTES]; + NumericUtils.intToSortableBytes(encodeLatitude(component2D.getMinY()), minLat, 0); + NumericUtils.intToSortableBytes(encodeLatitude(component2D.getMaxY()), maxLat, 0); + NumericUtils.intToSortableBytes(encodeLongitude(component2D.getMinX()), minLon, 0); + NumericUtils.intToSortableBytes(encodeLongitude(component2D.getMaxX()), maxLon, 0); + + return new SpatialVisitor() { + @Override + protected Relation relate(byte[] minPackedValue, byte[] maxPackedValue) { + if (Arrays.compareUnsigned(minPackedValue, 0, Integer.BYTES, maxLat, 0, Integer.BYTES) > 0 + || Arrays.compareUnsigned(maxPackedValue, 0, Integer.BYTES, minLat, 0, Integer.BYTES) + < 0 + || Arrays.compareUnsigned( + minPackedValue, + Integer.BYTES, + Integer.BYTES + Integer.BYTES, + maxLon, + 0, + Integer.BYTES) + > 0 + || Arrays.compareUnsigned( + maxPackedValue, + Integer.BYTES, + Integer.BYTES + Integer.BYTES, + minLon, + 0, + Integer.BYTES) + < 0) { + // outside of global bounding box range + return Relation.CELL_OUTSIDE_QUERY; + } + + double cellMinLat = decodeLatitude(minPackedValue, 0); + double cellMinLon = decodeLongitude(minPackedValue, Integer.BYTES); + double cellMaxLat = decodeLatitude(maxPackedValue, 0); + double cellMaxLon = decodeLongitude(maxPackedValue, Integer.BYTES); + + return component2D.relate(cellMinLon, cellMaxLon, cellMinLat, cellMaxLat); + } + + @Override + protected Predicate intersects() { + return packedValue -> + component2DPredicate.test( + NumericUtils.sortableBytesToInt(packedValue, 0), + NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES)); + } + + @Override + protected Predicate within() { + return packedValue -> + component2DPredicate.test( + NumericUtils.sortableBytesToInt(packedValue, 0), + NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES)); + } + + @Override + protected Function contains() { + return packedValue -> + component2D.withinPoint( + GeoEncodingUtils.decodeLongitude( + NumericUtils.sortableBytesToInt(packedValue, Integer.BYTES)), + GeoEncodingUtils.decodeLatitude(NumericUtils.sortableBytesToInt(packedValue, 0))); + } + }; + } + + @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("["); + for (int i = 0; i < geometries.length; i++) { + sb.append(geometries[i].toString()); + sb.append(','); + } + sb.append(']'); + return sb.toString(); + } + + @Override + protected boolean equalsTo(Object o) { + return super.equalsTo(o) && Arrays.equals(geometries, ((LatLonPointQuery) o).geometries); + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + hash = 31 * hash + Arrays.hashCode(geometries); + return hash; + } +} 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 4dcdc47..0d6bae4 100644 --- a/lucene/core/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java +++ b/lucene/core/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java @@ -25,6 +25,8 @@ import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude; import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil; import java.util.Arrays; +import java.util.function.Function; +import java.util.function.Predicate; import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.GeoUtils; @@ -38,143 +40,161 @@ import org.apache.lucene.util.NumericUtils; *

The field must be indexed using {@link * org.apache.lucene.document.LatLonShape#createIndexableFields} added per document. */ -final class LatLonShapeBoundingBoxQuery extends ShapeQuery { +final class LatLonShapeBoundingBoxQuery extends SpatialQuery { private final Rectangle rectangle; - private final EncodedRectangle encodedRectangle; LatLonShapeBoundingBoxQuery(String field, QueryRelation queryRelation, Rectangle rectangle) { super(field, queryRelation); this.rectangle = rectangle; - this.encodedRectangle = - new EncodedRectangle( - rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon); - } - - @Override - protected Relation relateRangeBBoxToQuery( - int minXOffset, - int minYOffset, - byte[] minTriangle, - int maxXOffset, - int maxYOffset, - byte[] maxTriangle) { - if (queryRelation == QueryRelation.INTERSECTS || queryRelation == QueryRelation.DISJOINT) { - return encodedRectangle.intersectRangeBBox( - minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle); - } - return encodedRectangle.relateRangeBBox( - minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle); - } - - @Override - protected boolean queryIntersects(byte[] t, ShapeField.DecodedTriangle scratchTriangle) { - ShapeField.decodeTriangle(t, scratchTriangle); - - switch (scratchTriangle.type) { - case POINT: - { - return encodedRectangle.contains(scratchTriangle.aX, scratchTriangle.aY); - } - case LINE: - { - int aY = scratchTriangle.aY; - int aX = scratchTriangle.aX; - int bY = scratchTriangle.bY; - int bX = scratchTriangle.bX; - return encodedRectangle.intersectsLine(aX, aY, bX, bY); - } - case TRIANGLE: - { - int aY = scratchTriangle.aY; - int aX = scratchTriangle.aX; - int bY = scratchTriangle.bY; - int bX = scratchTriangle.bX; - int cY = scratchTriangle.cY; - int cX = scratchTriangle.cX; - return encodedRectangle.intersectsTriangle(aX, aY, bX, bY, cX, cY); - } - default: - throw new IllegalArgumentException( - "Unsupported triangle type :[" + scratchTriangle.type + "]"); - } } @Override - protected boolean queryContains(byte[] t, ShapeField.DecodedTriangle scratchTriangle) { - ShapeField.decodeTriangle(t, scratchTriangle); - - switch (scratchTriangle.type) { - case POINT: - { - return encodedRectangle.contains(scratchTriangle.aX, scratchTriangle.aY); - } - case LINE: - { - int aY = scratchTriangle.aY; - int aX = scratchTriangle.aX; - int bY = scratchTriangle.bY; - int bX = scratchTriangle.bX; - return encodedRectangle.containsLine(aX, aY, bX, bY); - } - case TRIANGLE: - { - int aY = scratchTriangle.aY; - int aX = scratchTriangle.aX; - int bY = scratchTriangle.bY; - int bX = scratchTriangle.bX; - int cY = scratchTriangle.cY; - int cX = scratchTriangle.cX; - return encodedRectangle.containsTriangle(aX, aY, bX, bY, cX, cY); - } - default: - throw new IllegalArgumentException( - "Unsupported triangle type :[" + scratchTriangle.type + "]"); - } - } - - @Override - protected Component2D.WithinRelation queryWithin( - byte[] t, ShapeField.DecodedTriangle scratchTriangle) { - if (encodedRectangle.crossesDateline()) { - throw new IllegalArgumentException( - "withinTriangle is not supported for rectangles crossing the date line"); - } - // decode indexed triangle - ShapeField.decodeTriangle(t, scratchTriangle); - - switch (scratchTriangle.type) { - case POINT: - { - return encodedRectangle.contains(scratchTriangle.aX, scratchTriangle.aY) - ? Component2D.WithinRelation.NOTWITHIN - : Component2D.WithinRelation.DISJOINT; - } - case LINE: - { - return encodedRectangle.withinLine( - scratchTriangle.aX, - scratchTriangle.aY, - scratchTriangle.ab, - scratchTriangle.bX, - scratchTriangle.bY); + protected SpatialVisitor getSpatialVisitor() { + final EncodedRectangle encodedRectangle = + new EncodedRectangle( + rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon); + return new SpatialVisitor() { + + @Override + protected Relation relate(byte[] minTriangle, byte[] maxTriangle) { + if (queryRelation == QueryRelation.INTERSECTS || queryRelation == QueryRelation.DISJOINT) { + return encodedRectangle.intersectRangeBBox( + ShapeField.BYTES, + 0, + minTriangle, + 3 * ShapeField.BYTES, + 2 * ShapeField.BYTES, + maxTriangle); } - case TRIANGLE: - { - return encodedRectangle.withinTriangle( - scratchTriangle.aX, - scratchTriangle.aY, - scratchTriangle.ab, - scratchTriangle.bX, - scratchTriangle.bY, - scratchTriangle.bc, - scratchTriangle.cX, - scratchTriangle.cY, - scratchTriangle.ca); + return encodedRectangle.relateRangeBBox( + ShapeField.BYTES, + 0, + minTriangle, + 3 * ShapeField.BYTES, + 2 * ShapeField.BYTES, + maxTriangle); + } + + @Override + protected Predicate intersects() { + final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); + return triangle -> { + ShapeField.decodeTriangle(triangle, scratchTriangle); + + switch (scratchTriangle.type) { + case POINT: + { + return encodedRectangle.contains(scratchTriangle.aX, scratchTriangle.aY); + } + case LINE: + { + int aY = scratchTriangle.aY; + int aX = scratchTriangle.aX; + int bY = scratchTriangle.bY; + int bX = scratchTriangle.bX; + return encodedRectangle.intersectsLine(aX, aY, bX, bY); + } + case TRIANGLE: + { + int aY = scratchTriangle.aY; + int aX = scratchTriangle.aX; + int bY = scratchTriangle.bY; + int bX = scratchTriangle.bX; + int cY = scratchTriangle.cY; + int cX = scratchTriangle.cX; + return encodedRectangle.intersectsTriangle(aX, aY, bX, bY, cX, cY); + } + default: + throw new IllegalArgumentException( + "Unsupported triangle type :[" + scratchTriangle.type + "]"); + } + }; + } + + @Override + protected Predicate within() { + final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); + return triangle -> { + ShapeField.decodeTriangle(triangle, scratchTriangle); + + switch (scratchTriangle.type) { + case POINT: + { + return encodedRectangle.contains(scratchTriangle.aX, scratchTriangle.aY); + } + case LINE: + { + int aY = scratchTriangle.aY; + int aX = scratchTriangle.aX; + int bY = scratchTriangle.bY; + int bX = scratchTriangle.bX; + return encodedRectangle.containsLine(aX, aY, bX, bY); + } + case TRIANGLE: + { + int aY = scratchTriangle.aY; + int aX = scratchTriangle.aX; + int bY = scratchTriangle.bY; + int bX = scratchTriangle.bX; + int cY = scratchTriangle.cY; + int cX = scratchTriangle.cX; + return encodedRectangle.containsTriangle(aX, aY, bX, bY, cX, cY); + } + default: + throw new IllegalArgumentException( + "Unsupported triangle type :[" + scratchTriangle.type + "]"); + } + }; + } + + @Override + protected Function contains() { + if (encodedRectangle.crossesDateline()) { + throw new IllegalArgumentException( + "withinTriangle is not supported for rectangles crossing the date line"); } - default: - throw new IllegalArgumentException( - "Unsupported triangle type :[" + scratchTriangle.type + "]"); - } + final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); + return triangle -> { + + // decode indexed triangle + ShapeField.decodeTriangle(triangle, scratchTriangle); + + switch (scratchTriangle.type) { + case POINT: + { + return encodedRectangle.contains(scratchTriangle.aX, scratchTriangle.aY) + ? Component2D.WithinRelation.NOTWITHIN + : Component2D.WithinRelation.DISJOINT; + } + case LINE: + { + return encodedRectangle.withinLine( + scratchTriangle.aX, + scratchTriangle.aY, + scratchTriangle.ab, + scratchTriangle.bX, + scratchTriangle.bY); + } + case TRIANGLE: + { + return encodedRectangle.withinTriangle( + scratchTriangle.aX, + scratchTriangle.aY, + scratchTriangle.ab, + scratchTriangle.bX, + scratchTriangle.bY, + scratchTriangle.bc, + scratchTriangle.cX, + scratchTriangle.cY, + scratchTriangle.ca); + } + default: + throw new IllegalArgumentException( + "Unsupported triangle type :[" + scratchTriangle.type + "]"); + } + }; + } + }; } @Override diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonShapeQuery.java b/lucene/core/src/java/org/apache/lucene/document/LatLonShapeQuery.java index 5b7ed54..af7c87b 100644 --- a/lucene/core/src/java/org/apache/lucene/document/LatLonShapeQuery.java +++ b/lucene/core/src/java/org/apache/lucene/document/LatLonShapeQuery.java @@ -17,6 +17,8 @@ package org.apache.lucene.document; import java.util.Arrays; +import java.util.function.Function; +import java.util.function.Predicate; import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.GeoEncodingUtils; @@ -26,12 +28,12 @@ import org.apache.lucene.index.PointValues.Relation; import org.apache.lucene.util.NumericUtils; /** - * Finds all previously indexed cartesian shapes that comply the given {@link QueryRelation} with - * the specified array of {@link LatLonGeometry}. + * Finds all previously indexed geo shapes that comply the given {@link QueryRelation} with the + * specified array of {@link LatLonGeometry}. * *

The field must be indexed using {@link LatLonShape#createIndexableFields} added per document. */ -final class LatLonShapeQuery extends ShapeQuery { +final class LatLonShapeQuery extends SpatialQuery { private final LatLonGeometry[] geometries; private final Component2D component2D; @@ -56,140 +58,150 @@ final class LatLonShapeQuery extends ShapeQuery { } @Override - protected Relation relateRangeBBoxToQuery( - int minXOffset, - int minYOffset, - byte[] minTriangle, - int maxXOffset, - int maxYOffset, - byte[] maxTriangle) { + protected SpatialVisitor getSpatialVisitor() { - 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)); + return new SpatialVisitor() { + @Override + protected Relation relate(byte[] minTriangle, byte[] maxTriangle) { + double minLat = + GeoEncodingUtils.decodeLatitude(NumericUtils.sortableBytesToInt(minTriangle, 0)); + double minLon = + GeoEncodingUtils.decodeLongitude( + NumericUtils.sortableBytesToInt(minTriangle, ShapeField.BYTES)); + double maxLat = + GeoEncodingUtils.decodeLatitude( + NumericUtils.sortableBytesToInt(maxTriangle, 2 * ShapeField.BYTES)); + double maxLon = + GeoEncodingUtils.decodeLongitude( + NumericUtils.sortableBytesToInt(maxTriangle, 3 * ShapeField.BYTES)); - // check internal node against query - return component2D.relate(minLon, maxLon, minLat, maxLat); - } + // check internal node against query + return component2D.relate(minLon, maxLon, minLat, maxLat); + } - @Override - protected boolean queryIntersects(byte[] t, ShapeField.DecodedTriangle scratchTriangle) { - ShapeField.decodeTriangle(t, scratchTriangle); + @Override + protected Predicate intersects() { + final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); + return triangle -> { + ShapeField.decodeTriangle(triangle, scratchTriangle); - switch (scratchTriangle.type) { - case POINT: - { - double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); - double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); - return component2D.contains(alon, alat); - } - case LINE: - { - double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); - double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); - double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY); - double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX); - return component2D.intersectsLine(alon, alat, blon, blat); - } - case TRIANGLE: - { - 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 component2D.intersectsTriangle(alon, alat, blon, blat, clon, clat); - } - default: - throw new IllegalArgumentException( - "Unsupported triangle type :[" + scratchTriangle.type + "]"); - } - } + switch (scratchTriangle.type) { + case POINT: + { + double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); + double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); + return component2D.contains(alon, alat); + } + case LINE: + { + double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); + double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); + double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY); + double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX); + return component2D.intersectsLine(alon, alat, blon, blat); + } + case TRIANGLE: + { + 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 component2D.intersectsTriangle(alon, alat, blon, blat, clon, clat); + } + default: + throw new IllegalArgumentException( + "Unsupported triangle type :[" + scratchTriangle.type + "]"); + } + }; + } - @Override - protected boolean queryContains(byte[] t, ShapeField.DecodedTriangle scratchTriangle) { - ShapeField.decodeTriangle(t, scratchTriangle); + @Override + protected Predicate within() { + final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); + return triangle -> { + ShapeField.decodeTriangle(triangle, scratchTriangle); - switch (scratchTriangle.type) { - case POINT: - { - double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); - double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); - return component2D.contains(alon, alat); - } - case LINE: - { - double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); - double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); - double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY); - double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX); - return component2D.containsLine(alon, alat, blon, blat); - } - case TRIANGLE: - { - 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 component2D.containsTriangle(alon, alat, blon, blat, clon, clat); - } - default: - throw new IllegalArgumentException( - "Unsupported triangle type :[" + scratchTriangle.type + "]"); - } - } + switch (scratchTriangle.type) { + case POINT: + { + double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); + double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); + return component2D.contains(alon, alat); + } + case LINE: + { + double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); + double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); + double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY); + double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX); + return component2D.containsLine(alon, alat, blon, blat); + } + case TRIANGLE: + { + 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 component2D.containsTriangle(alon, alat, blon, blat, clon, clat); + } + default: + throw new IllegalArgumentException( + "Unsupported triangle type :[" + scratchTriangle.type + "]"); + } + }; + } - @Override - protected Component2D.WithinRelation queryWithin( - byte[] t, ShapeField.DecodedTriangle scratchTriangle) { - ShapeField.decodeTriangle(t, scratchTriangle); + @Override + protected Function contains() { + final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); + return triangle -> { + ShapeField.decodeTriangle(triangle, scratchTriangle); - switch (scratchTriangle.type) { - case POINT: - { - double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); - double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); - return component2D.withinPoint(alon, alat); - } - case LINE: - { - double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); - double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); - double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY); - double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX); - return component2D.withinLine(alon, alat, scratchTriangle.ab, blon, blat); - } - case TRIANGLE: - { - 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 component2D.withinTriangle( - alon, - alat, - scratchTriangle.ab, - blon, - blat, - scratchTriangle.bc, - clon, - clat, - scratchTriangle.ca); - } - default: - throw new IllegalArgumentException( - "Unsupported triangle type :[" + scratchTriangle.type + "]"); - } + switch (scratchTriangle.type) { + case POINT: + { + double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); + double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); + return component2D.withinPoint(alon, alat); + } + case LINE: + { + double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle.aY); + double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle.aX); + double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle.bY); + double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle.bX); + return component2D.withinLine(alon, alat, scratchTriangle.ab, blon, blat); + } + case TRIANGLE: + { + 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 component2D.withinTriangle( + alon, + alat, + scratchTriangle.ab, + blon, + blat, + scratchTriangle.bc, + clon, + clat, + scratchTriangle.ca); + } + default: + throw new IllegalArgumentException( + "Unsupported triangle type :[" + scratchTriangle.type + "]"); + } + }; + } + }; } @Override diff --git a/lucene/core/src/java/org/apache/lucene/document/ShapeQuery.java b/lucene/core/src/java/org/apache/lucene/document/SpatialQuery.java similarity index 67% rename from lucene/core/src/java/org/apache/lucene/document/ShapeQuery.java rename to lucene/core/src/java/org/apache/lucene/document/SpatialQuery.java index b9052e9..17208d5 100644 --- a/lucene/core/src/java/org/apache/lucene/document/ShapeQuery.java +++ b/lucene/core/src/java/org/apache/lucene/document/SpatialQuery.java @@ -18,6 +18,9 @@ package org.apache.lucene.document; import java.io.IOException; import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.geo.Component2D; import org.apache.lucene.index.FieldInfo; @@ -42,29 +45,10 @@ import org.apache.lucene.util.DocIdSetBuilder; import org.apache.lucene.util.FixedBitSet; /** - * Base query class for all spatial geometries: {@link LatLonShape} and {@link XYShape}. - * - *

The field must be indexed using either {@link LatLonShape#createIndexableFields} or {@link - * XYShape#createIndexableFields} and the corresponding factory method must be used: - * - *

    - *
  • {@link LatLonShape#newBoxQuery newBoxQuery()} for matching geo shapes that have some {@link - * QueryRelation} with a bounding box. - *
  • {@link LatLonShape#newLineQuery newLineQuery()} for matching geo shapes that have some - * {@link QueryRelation} with a linestring. - *
  • {@link LatLonShape#newPolygonQuery newPolygonQuery()} for matching geo shapes that have - * some {@link QueryRelation} with a polygon. - *
  • {@link XYShape#newBoxQuery newBoxQuery()} for matching cartesian shapes that have some - * {@link QueryRelation} with a bounding box. - *
  • {@link XYShape#newLineQuery newLineQuery()} for matching cartesian shapes that have some - * {@link QueryRelation} with a linestring. - *
  • {@link XYShape#newPolygonQuery newPolygonQuery()} for matching cartesian shapes that have - * some {@link QueryRelation} with a polygon. - *
- * - *

+ * Base query class for all spatial geometries: {@link LatLonShape}, {@link LatLonPoint} and {@link + * XYShape}. In order to create a query, use the factory methods on those classes. */ -abstract class ShapeQuery extends Query { +abstract class SpatialQuery extends Query { /** field name */ final String field; /** @@ -74,72 +58,65 @@ abstract class ShapeQuery extends Query { */ final QueryRelation queryRelation; - protected ShapeQuery(String field, final QueryRelation queryType) { + protected SpatialQuery(String field, final QueryRelation queryRelation) { if (field == null) { throw new IllegalArgumentException("field must not be null"); } + if (queryRelation == null) { + throw new IllegalArgumentException("queryRelation must not be null"); + } this.field = field; - this.queryRelation = queryType; + this.queryRelation = queryRelation; } /** - * relates an internal node (bounding box of a range of triangles) to the target query Note: logic - * is specific to query type see {@link LatLonShapeBoundingBoxQuery#relateRangeToQuery} and {@link - * LatLonShapeQuery#relateRangeToQuery} + * returns the spatial visitor to be used for this query. Called before generating the query + * {@link Weight} */ - protected abstract Relation relateRangeBBoxToQuery( - int minXOffset, - int minYOffset, - byte[] minTriangle, - int maxXOffset, - int maxYOffset, - byte[] maxTriangle); - - /** returns true if the provided triangle matches the query */ - protected boolean queryMatches( - byte[] triangle, - ShapeField.DecodedTriangle scratchTriangle, - ShapeField.QueryRelation queryRelation) { - switch (queryRelation) { - case INTERSECTS: - return queryIntersects(triangle, scratchTriangle); - case WITHIN: - return queryContains(triangle, scratchTriangle); - case DISJOINT: - return queryIntersects(triangle, scratchTriangle) == false; - default: - throw new IllegalArgumentException("Unsupported query type :[" + queryRelation + "]"); + protected abstract SpatialVisitor getSpatialVisitor(); + + /** Visitor used for walking the BKD tree. */ + protected abstract static class SpatialVisitor { + /** relates a range of points (internal node) to the query */ + protected abstract Relation relate(byte[] minPackedValue, byte[] maxPackedValue); + + /** Gets a intersects predicate. Called when constructing a {@link Scorer} */ + protected abstract Predicate intersects(); + + /** Gets a within predicate. Called when constructing a {@link Scorer} */ + protected abstract Predicate within(); + + /** Gets a contains function. Called when constructing a {@link Scorer} */ + protected abstract Function contains(); + + private Predicate containsPredicate() { + final Function contains = contains(); + return bytes -> contains.apply(bytes) == Component2D.WithinRelation.CANDIDATE; + } + + private BiFunction getInnerFunction( + ShapeField.QueryRelation queryRelation) { + if (queryRelation == QueryRelation.DISJOINT) { + return (minPackedValue, maxPackedValue) -> + transposeRelation(relate(minPackedValue, maxPackedValue)); + } + return (minPackedValue, maxPackedValue) -> relate(minPackedValue, maxPackedValue); } - } - /** returns true if the provided triangle intersects the query */ - protected abstract boolean queryIntersects( - byte[] triangle, ShapeField.DecodedTriangle scratchTriangle); - - /** returns true if the provided triangle is within the query */ - protected abstract boolean queryContains( - byte[] triangle, ShapeField.DecodedTriangle scratchTriangle); - - /** Return the within relationship between the query and the indexed shape. */ - protected abstract Component2D.WithinRelation queryWithin( - byte[] triangle, ShapeField.DecodedTriangle scratchTriangle); - - /** relates a range of triangles (internal node) to the query */ - protected Relation relateRangeToQuery( - byte[] minTriangle, byte[] maxTriangle, QueryRelation queryRelation) { - // compute bounding box of internal node - final Relation r = - relateRangeBBoxToQuery( - ShapeField.BYTES, - 0, - minTriangle, - 3 * ShapeField.BYTES, - 2 * ShapeField.BYTES, - maxTriangle); - if (queryRelation == QueryRelation.DISJOINT) { - return transposeRelation(r); + private Predicate getLeafPredicate(ShapeField.QueryRelation queryRelation) { + switch (queryRelation) { + case INTERSECTS: + return intersects(); + case WITHIN: + return within(); + case DISJOINT: + return intersects().negate(); + case CONTAINS: + return containsPredicate(); + default: + throw new IllegalArgumentException("Unsupported query type :[" + queryRelation + "]"); + } } - return r; } @Override @@ -151,7 +128,8 @@ abstract class ShapeQuery extends Query { @Override public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) { - final ShapeQuery query = this; + final SpatialQuery query = this; + final SpatialVisitor spatialVisitor = getSpatialVisitor(); return new ConstantScoreWeight(query, boost) { @Override @@ -176,11 +154,11 @@ abstract class ShapeQuery extends Query { // No docs in this segment indexed this field at all return null; } - final Weight weight = this; final Relation rel = - relateRangeToQuery( - values.getMinPackedValue(), values.getMaxPackedValue(), queryRelation); + spatialVisitor + .getInnerFunction(queryRelation) + .apply(values.getMinPackedValue(), values.getMaxPackedValue()); if (rel == Relation.CELL_OUTSIDE_QUERY || (rel == Relation.CELL_INSIDE_QUERY && queryRelation == QueryRelation.CONTAINS)) { // no documents match the query @@ -202,13 +180,14 @@ abstract class ShapeQuery extends Query { } else { if (queryRelation != QueryRelation.INTERSECTS && queryRelation != QueryRelation.CONTAINS - && hasAnyHits(query, values) == false) { + && values.getDocCount() != values.size() + && hasAnyHits(spatialVisitor, queryRelation, values) == false) { // First we check if we have any hits so we are fast in the adversarial case where // the shape does not match any documents and we are in the dense case return null; } // walk the tree to get matching documents - return new RelationScorerSupplier(values, ShapeQuery.this) { + return new RelationScorerSupplier(values, spatialVisitor, queryRelation, field) { @Override public Scorer get(long leadCost) throws IOException { return getScorer(reader, weight, score(), scoreMode); @@ -249,15 +228,15 @@ abstract class ShapeQuery extends Query { /** class specific equals check */ protected boolean equalsTo(Object o) { - return Objects.equals(field, ((ShapeQuery) o).field) - && this.queryRelation == ((ShapeQuery) o).queryRelation; + return Objects.equals(field, ((SpatialQuery) o).field) + && this.queryRelation == ((SpatialQuery) o).queryRelation; } /** * transpose the relation; INSIDE becomes OUTSIDE, OUTSIDE becomes INSIDE, CROSSES remains * unchanged */ - private static Relation transposeRelation(Relation r) { + protected static Relation transposeRelation(Relation r) { if (r == Relation.CELL_INSIDE_QUERY) { return Relation.CELL_OUTSIDE_QUERY; } else if (r == Relation.CELL_OUTSIDE_QUERY) { @@ -271,36 +250,46 @@ abstract class ShapeQuery extends Query { */ private abstract static class RelationScorerSupplier extends ScorerSupplier { private final PointValues values; - private final ShapeQuery query; + private final SpatialVisitor spatialVisitor; + private final QueryRelation queryRelation; + private final String field; private long cost = -1; - RelationScorerSupplier(final PointValues values, final ShapeQuery query) { + RelationScorerSupplier( + final PointValues values, + SpatialVisitor spatialVisitor, + final QueryRelation queryRelation, + final String field) { this.values = values; - this.query = query; + this.spatialVisitor = spatialVisitor; + this.queryRelation = queryRelation; + this.field = field; } protected Scorer getScorer( final LeafReader reader, final Weight weight, final float boost, final ScoreMode scoreMode) throws IOException { - switch (query.getQueryRelation()) { + switch (queryRelation) { case INTERSECTS: return getSparseScorer(reader, weight, boost, scoreMode); - case WITHIN: - case DISJOINT: - return getDenseScorer(reader, weight, boost, scoreMode); case CONTAINS: return getContainsDenseScorer(reader, weight, boost, scoreMode); + case WITHIN: + case DISJOINT: + return values.getDocCount() == values.size() + ? getSparseScorer(reader, weight, boost, scoreMode) + : getDenseScorer(reader, weight, boost, scoreMode); default: - throw new IllegalArgumentException( - "Unsupported query type :[" + query.getQueryRelation() + "]"); + throw new IllegalArgumentException("Unsupported query type :[" + queryRelation + "]"); } } - /** Scorer used for INTERSECTS * */ + /** Scorer used for INTERSECTS and single value points */ private Scorer getSparseScorer( final LeafReader reader, final Weight weight, final float boost, final ScoreMode scoreMode) throws IOException { - if (values.getDocCount() == reader.maxDoc() + if (queryRelation == QueryRelation.DISJOINT + && values.getDocCount() == reader.maxDoc() && values.getDocCount() == values.size() && cost() > reader.maxDoc() / 2) { // If all docs have exactly one value and the cost is greater @@ -309,29 +298,27 @@ abstract class ShapeQuery extends Query { final FixedBitSet result = new FixedBitSet(reader.maxDoc()); result.set(0, reader.maxDoc()); final long[] cost = new long[] {reader.maxDoc()}; - values.intersect(getInverseDenseVisitor(query, result, cost)); + values.intersect(getInverseDenseVisitor(spatialVisitor, queryRelation, result, cost)); final DocIdSetIterator iterator = new BitSetIterator(result, cost[0]); return new ConstantScoreScorer(weight, boost, scoreMode, iterator); - } - if (values.getDocCount() < (values.size() >>> 2)) { + } else if (values.getDocCount() < (values.size() >>> 2)) { // we use a dense structure so we can skip already visited documents final FixedBitSet result = new FixedBitSet(reader.maxDoc()); final long[] cost = new long[] {0}; - values.intersect(getIntersectsDenseVisitor(query, result, cost)); + values.intersect(getIntersectsDenseVisitor(spatialVisitor, queryRelation, result, cost)); assert cost[0] > 0 || result.cardinality() == 0; final DocIdSetIterator iterator = cost[0] == 0 ? DocIdSetIterator.empty() : new BitSetIterator(result, cost[0]); return new ConstantScoreScorer(weight, boost, scoreMode, iterator); } else { - final DocIdSetBuilder docIdSetBuilder = - new DocIdSetBuilder(reader.maxDoc(), values, query.getField()); - values.intersect(getSparseVisitor(query, docIdSetBuilder)); + final DocIdSetBuilder docIdSetBuilder = new DocIdSetBuilder(reader.maxDoc(), values, field); + values.intersect(getSparseVisitor(spatialVisitor, queryRelation, docIdSetBuilder)); final DocIdSetIterator iterator = docIdSetBuilder.build().iterator(); return new ConstantScoreScorer(weight, boost, scoreMode, iterator); } } - /** Scorer used for WITHIN and DISJOINT * */ + /** Scorer used for WITHIN and DISJOINT */ private Scorer getDenseScorer( LeafReader reader, Weight weight, final float boost, ScoreMode scoreMode) throws IOException { @@ -343,17 +330,17 @@ abstract class ShapeQuery extends Query { // are potential matches result.set(0, reader.maxDoc()); // Remove false positives - values.intersect(getInverseDenseVisitor(query, result, cost)); + values.intersect(getInverseDenseVisitor(spatialVisitor, queryRelation, result, cost)); } else { cost = new long[] {0}; // Get potential documents. final FixedBitSet excluded = new FixedBitSet(reader.maxDoc()); - values.intersect(getDenseVisitor(query, result, excluded, cost)); + values.intersect(getDenseVisitor(spatialVisitor, queryRelation, result, excluded, cost)); result.andNot(excluded); // Remove false positives, we only care about the inner nodes as intersecting // leaf nodes have been already taken into account. Unfortunately this // process still reads the leaf nodes. - values.intersect(getShallowInverseDenseVisitor(query, result)); + values.intersect(getShallowInverseDenseVisitor(spatialVisitor, queryRelation, result)); } assert cost[0] > 0 || result.cardinality() == 0; final DocIdSetIterator iterator = @@ -368,7 +355,8 @@ abstract class ShapeQuery extends Query { final long[] cost = new long[] {0}; // Get potential documents. final FixedBitSet excluded = new FixedBitSet(reader.maxDoc()); - values.intersect(getContainsDenseVisitor(query, result, excluded, cost)); + values.intersect( + getContainsDenseVisitor(spatialVisitor, queryRelation, result, excluded, cost)); result.andNot(excluded); assert cost[0] > 0 || result.cardinality() == 0; final DocIdSetIterator iterator = @@ -380,7 +368,7 @@ abstract class ShapeQuery extends Query { public long cost() { if (cost == -1) { // Computing the cost may be expensive, so only do it if necessary - cost = values.estimateDocCount(getEstimateVisitor(query)); + cost = values.estimateDocCount(getEstimateVisitor(spatialVisitor, queryRelation)); assert cost >= 0; } return cost; @@ -388,7 +376,10 @@ abstract class ShapeQuery extends Query { } /** create a visitor for calculating point count estimates for the provided relation */ - private static IntersectVisitor getEstimateVisitor(final ShapeQuery query) { + private static IntersectVisitor getEstimateVisitor( + final SpatialVisitor spatialVisitor, QueryRelation queryRelation) { + final BiFunction innerFunction = + spatialVisitor.getInnerFunction(queryRelation); return new IntersectVisitor() { @Override public void visit(int docID) { @@ -402,7 +393,7 @@ abstract class ShapeQuery extends Query { @Override public Relation compare(byte[] minTriangle, byte[] maxTriangle) { - return query.relateRangeToQuery(minTriangle, maxTriangle, query.getQueryRelation()); + return innerFunction.apply(minTriangle, maxTriangle); } }; } @@ -412,9 +403,13 @@ abstract class ShapeQuery extends Query { * INTERSECT when the number of docs <= 4 * number of points ) */ private static IntersectVisitor getSparseVisitor( - final ShapeQuery query, final DocIdSetBuilder result) { + final SpatialVisitor spatialVisitor, + QueryRelation queryRelation, + final DocIdSetBuilder result) { + final BiFunction innerFunction = + spatialVisitor.getInnerFunction(queryRelation); + final Predicate leafPredicate = spatialVisitor.getLeafPredicate(queryRelation); return new IntersectVisitor() { - final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); DocIdSetBuilder.BulkAdder adder; @Override @@ -429,14 +424,14 @@ abstract class ShapeQuery extends Query { @Override public void visit(int docID, byte[] t) { - if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) { + if (leafPredicate.test(t)) { visit(docID); } } @Override public void visit(DocIdSetIterator iterator, byte[] t) throws IOException { - if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) { + if (leafPredicate.test(t)) { int docID; while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { visit(docID); @@ -446,16 +441,21 @@ abstract class ShapeQuery extends Query { @Override public Relation compare(byte[] minTriangle, byte[] maxTriangle) { - return query.relateRangeToQuery(minTriangle, maxTriangle, query.getQueryRelation()); + return innerFunction.apply(minTriangle, maxTriangle); } }; } - /** Scorer used for INTERSECTS when the number of points > 4 * number of docs * */ + /** Scorer used for INTERSECTS when the number of points > 4 * number of docs */ private static IntersectVisitor getIntersectsDenseVisitor( - final ShapeQuery query, final FixedBitSet result, final long[] cost) { + final SpatialVisitor spatialVisitor, + QueryRelation queryRelation, + final FixedBitSet result, + final long[] cost) { + final BiFunction innerFunction = + spatialVisitor.getInnerFunction(queryRelation); + final Predicate leafPredicate = spatialVisitor.getLeafPredicate(queryRelation); return new IntersectVisitor() { - final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); @Override public void visit(int docID) { @@ -466,7 +466,7 @@ abstract class ShapeQuery extends Query { @Override public void visit(int docID, byte[] t) { if (result.get(docID) == false) { - if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) { + if (leafPredicate.test(t)) { visit(docID); } } @@ -474,7 +474,7 @@ abstract class ShapeQuery extends Query { @Override public void visit(DocIdSetIterator iterator, byte[] t) throws IOException { - if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) { + if (leafPredicate.test(t)) { int docID; while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { visit(docID); @@ -484,7 +484,7 @@ abstract class ShapeQuery extends Query { @Override public Relation compare(byte[] minTriangle, byte[] maxTriangle) { - return query.relateRangeToQuery(minTriangle, maxTriangle, query.getQueryRelation()); + return innerFunction.apply(minTriangle, maxTriangle); } }; } @@ -494,13 +494,15 @@ abstract class ShapeQuery extends Query { * WITHIN & DISJOINT */ private static IntersectVisitor getDenseVisitor( - final ShapeQuery query, + final SpatialVisitor spatialVisitor, + final QueryRelation queryRelation, final FixedBitSet result, final FixedBitSet excluded, final long[] cost) { + final BiFunction innerFunction = + spatialVisitor.getInnerFunction(queryRelation); + final Predicate leafPredicate = spatialVisitor.getLeafPredicate(queryRelation); return new IntersectVisitor() { - final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); - @Override public void visit(int docID) { result.set(docID); @@ -510,7 +512,7 @@ abstract class ShapeQuery extends Query { @Override public void visit(int docID, byte[] t) { if (excluded.get(docID) == false) { - if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) { + if (leafPredicate.test(t)) { visit(docID); } else { excluded.set(docID); @@ -520,7 +522,7 @@ abstract class ShapeQuery extends Query { @Override public void visit(DocIdSetIterator iterator, byte[] t) throws IOException { - boolean matches = query.queryMatches(t, scratchTriangle, query.getQueryRelation()); + boolean matches = leafPredicate.test(t); int docID; while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { if (matches) { @@ -533,7 +535,7 @@ abstract class ShapeQuery extends Query { @Override public Relation compare(byte[] minTriangle, byte[] maxTriangle) { - return query.relateRangeToQuery(minTriangle, maxTriangle, query.getQueryRelation()); + return innerFunction.apply(minTriangle, maxTriangle); } }; } @@ -543,13 +545,15 @@ abstract class ShapeQuery extends Query { * CONTAINS */ private static IntersectVisitor getContainsDenseVisitor( - final ShapeQuery query, + final SpatialVisitor spatialVisitor, + final QueryRelation queryRelation, final FixedBitSet result, final FixedBitSet excluded, final long[] cost) { + final BiFunction innerFunction = + spatialVisitor.getInnerFunction(queryRelation); + final Function leafFunction = spatialVisitor.contains(); return new IntersectVisitor() { - final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); - @Override public void visit(int docID) { excluded.set(docID); @@ -558,7 +562,7 @@ abstract class ShapeQuery extends Query { @Override public void visit(int docID, byte[] t) { if (excluded.get(docID) == false) { - Component2D.WithinRelation within = query.queryWithin(t, scratchTriangle); + Component2D.WithinRelation within = leafFunction.apply(t); if (within == Component2D.WithinRelation.CANDIDATE) { cost[0]++; result.set(docID); @@ -570,7 +574,7 @@ abstract class ShapeQuery extends Query { @Override public void visit(DocIdSetIterator iterator, byte[] t) throws IOException { - Component2D.WithinRelation within = query.queryWithin(t, scratchTriangle); + Component2D.WithinRelation within = leafFunction.apply(t); int docID; while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { if (within == Component2D.WithinRelation.CANDIDATE) { @@ -584,7 +588,7 @@ abstract class ShapeQuery extends Query { @Override public Relation compare(byte[] minTriangle, byte[] maxTriangle) { - return query.relateRangeToQuery(minTriangle, maxTriangle, query.getQueryRelation()); + return innerFunction.apply(minTriangle, maxTriangle); } }; } @@ -594,9 +598,14 @@ abstract class ShapeQuery extends Query { * bitset; used with WITHIN & DISJOINT */ private static IntersectVisitor getInverseDenseVisitor( - final ShapeQuery query, final FixedBitSet result, final long[] cost) { + final SpatialVisitor spatialVisitor, + final QueryRelation queryRelation, + final FixedBitSet result, + final long[] cost) { + final BiFunction innerFunction = + spatialVisitor.getInnerFunction(queryRelation); + final Predicate leafPredicate = spatialVisitor.getLeafPredicate(queryRelation); return new IntersectVisitor() { - final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); @Override public void visit(int docID) { @@ -607,8 +616,7 @@ abstract class ShapeQuery extends Query { @Override public void visit(int docID, byte[] packedTriangle) { if (result.get(docID)) { - if (query.queryMatches(packedTriangle, scratchTriangle, query.getQueryRelation()) - == false) { + if (leafPredicate.test(packedTriangle) == false) { visit(docID); } } @@ -616,7 +624,7 @@ abstract class ShapeQuery extends Query { @Override public void visit(DocIdSetIterator iterator, byte[] t) throws IOException { - if (query.queryMatches(t, scratchTriangle, query.getQueryRelation()) == false) { + if (leafPredicate.test(t) == false) { int docID; while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { visit(docID); @@ -626,8 +634,7 @@ abstract class ShapeQuery extends Query { @Override public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) { - return transposeRelation( - query.relateRangeToQuery(minPackedValue, maxPackedValue, query.getQueryRelation())); + return transposeRelation(innerFunction.apply(minPackedValue, maxPackedValue)); } }; } @@ -637,7 +644,10 @@ abstract class ShapeQuery extends Query { * bitset; used with WITHIN & DISJOINT. This visitor only takes into account inner nodes */ private static IntersectVisitor getShallowInverseDenseVisitor( - final ShapeQuery query, final FixedBitSet result) { + final SpatialVisitor spatialVisitor, QueryRelation queryRelation, final FixedBitSet result) { + final BiFunction innerFunction = + spatialVisitor.getInnerFunction(queryRelation); + ; return new IntersectVisitor() { @Override @@ -657,8 +667,7 @@ abstract class ShapeQuery extends Query { @Override public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) { - return transposeRelation( - query.relateRangeToQuery(minPackedValue, maxPackedValue, query.getQueryRelation())); + return transposeRelation(innerFunction.apply(minPackedValue, maxPackedValue)); } }; } @@ -667,12 +676,15 @@ abstract class ShapeQuery extends Query { * Return true if the query matches at least one document. It creates a visitor that terminates as * soon as one or more docs are matched. */ - private static boolean hasAnyHits(final ShapeQuery query, final PointValues values) + private static boolean hasAnyHits( + final SpatialVisitor spatialVisitor, QueryRelation queryRelation, final PointValues values) throws IOException { try { + final BiFunction innerFunction = + spatialVisitor.getInnerFunction(queryRelation); + final Predicate leafPredicate = spatialVisitor.getLeafPredicate(queryRelation); values.intersect( new IntersectVisitor() { - final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); @Override public void visit(int docID) { @@ -681,23 +693,21 @@ abstract class ShapeQuery extends Query { @Override public void visit(int docID, byte[] t) { - if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) { + if (leafPredicate.test(t)) { throw new CollectionTerminatedException(); } } @Override public void visit(DocIdSetIterator iterator, byte[] t) { - if (query.queryMatches(t, scratchTriangle, query.getQueryRelation())) { + if (leafPredicate.test(t)) { throw new CollectionTerminatedException(); } } @Override public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) { - Relation rel = - query.relateRangeToQuery( - minPackedValue, maxPackedValue, query.getQueryRelation()); + Relation rel = innerFunction.apply(minPackedValue, maxPackedValue); if (rel == Relation.CELL_INSIDE_QUERY) { throw new CollectionTerminatedException(); } diff --git a/lucene/core/src/java/org/apache/lucene/document/XYShapeQuery.java b/lucene/core/src/java/org/apache/lucene/document/XYShapeQuery.java index e5cf429..9726d0b 100644 --- a/lucene/core/src/java/org/apache/lucene/document/XYShapeQuery.java +++ b/lucene/core/src/java/org/apache/lucene/document/XYShapeQuery.java @@ -19,6 +19,8 @@ package org.apache.lucene.document; import static org.apache.lucene.geo.XYEncodingUtils.decode; import java.util.Arrays; +import java.util.function.Function; +import java.util.function.Predicate; import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.XYEncodingUtils; @@ -32,7 +34,7 @@ import org.apache.lucene.util.NumericUtils; * *

The field must be indexed using {@link XYShape#createIndexableFields} added per document. */ -final class XYShapeQuery extends ShapeQuery { +final class XYShapeQuery extends SpatialQuery { final XYGeometry[] geometries; private final Component2D component2D; @@ -44,128 +46,148 @@ final class XYShapeQuery extends ShapeQuery { } @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 component2D.relate(minX, maxX, minY, maxY); - } - - @Override - protected boolean queryIntersects(byte[] t, ShapeField.DecodedTriangle scratchTriangle) { - ShapeField.decodeTriangle(t, scratchTriangle); - - switch (scratchTriangle.type) { - case POINT: - { - double y = decode(scratchTriangle.aY); - double x = decode(scratchTriangle.aX); - return component2D.contains(x, y); - } - case LINE: - { - double aY = decode(scratchTriangle.aY); - double aX = decode(scratchTriangle.aX); - double bY = decode(scratchTriangle.bY); - double bX = decode(scratchTriangle.bX); - return component2D.intersectsLine(aX, aY, bX, bY); - } - case TRIANGLE: - { - 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 component2D.intersectsTriangle(aX, aY, bX, bY, cX, cY); - } - default: - throw new IllegalArgumentException( - "Unsupported triangle type :[" + scratchTriangle.type + "]"); - } - } - - @Override - protected boolean queryContains(byte[] t, ShapeField.DecodedTriangle scratchTriangle) { - ShapeField.decodeTriangle(t, scratchTriangle); - - switch (scratchTriangle.type) { - case POINT: - { - double y = decode(scratchTriangle.aY); - double x = decode(scratchTriangle.aX); - return component2D.contains(x, y); - } - case LINE: - { - double aY = decode(scratchTriangle.aY); - double aX = decode(scratchTriangle.aX); - double bY = decode(scratchTriangle.bY); - double bX = decode(scratchTriangle.bX); - return component2D.containsLine(aX, aY, bX, bY); - } - case TRIANGLE: - { - 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 component2D.containsTriangle(aX, aY, bX, bY, cX, cY); - } - default: - throw new IllegalArgumentException( - "Unsupported triangle type :[" + scratchTriangle.type + "]"); - } - } - - @Override - protected Component2D.WithinRelation queryWithin( - byte[] t, ShapeField.DecodedTriangle scratchTriangle) { - ShapeField.decodeTriangle(t, scratchTriangle); - - switch (scratchTriangle.type) { - case POINT: - { - double y = decode(scratchTriangle.aY); - double x = decode(scratchTriangle.aX); - return component2D.withinPoint(x, y); - } - case LINE: - { - double aY = decode(scratchTriangle.aY); - double aX = decode(scratchTriangle.aX); - double bY = decode(scratchTriangle.bY); - double bX = decode(scratchTriangle.bX); - return component2D.withinLine(aX, aY, scratchTriangle.ab, bX, bY); - } - case TRIANGLE: - { - 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 component2D.withinTriangle( - aX, aY, scratchTriangle.ab, bX, bY, scratchTriangle.bc, cX, cY, scratchTriangle.ca); - } - default: - throw new IllegalArgumentException( - "Unsupported triangle type :[" + scratchTriangle.type + "]"); - } + protected SpatialVisitor getSpatialVisitor() { + return new SpatialVisitor() { + @Override + protected Relation relate(byte[] minTriangle, byte[] maxTriangle) { + + double minY = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(minTriangle, 0)); + double minX = + XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(minTriangle, ShapeField.BYTES)); + double maxY = + XYEncodingUtils.decode( + NumericUtils.sortableBytesToInt(maxTriangle, 2 * ShapeField.BYTES)); + double maxX = + XYEncodingUtils.decode( + NumericUtils.sortableBytesToInt(maxTriangle, 3 * ShapeField.BYTES)); + + // check internal node against query + return component2D.relate(minX, maxX, minY, maxY); + } + + @Override + protected Predicate intersects() { + final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); + return triangle -> { + ShapeField.decodeTriangle(triangle, scratchTriangle); + + switch (scratchTriangle.type) { + case POINT: + { + double y = decode(scratchTriangle.aY); + double x = decode(scratchTriangle.aX); + return component2D.contains(x, y); + } + case LINE: + { + double aY = decode(scratchTriangle.aY); + double aX = decode(scratchTriangle.aX); + double bY = decode(scratchTriangle.bY); + double bX = decode(scratchTriangle.bX); + return component2D.intersectsLine(aX, aY, bX, bY); + } + case TRIANGLE: + { + 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 component2D.intersectsTriangle(aX, aY, bX, bY, cX, cY); + } + default: + throw new IllegalArgumentException( + "Unsupported triangle type :[" + scratchTriangle.type + "]"); + } + }; + } + + @Override + protected Predicate within() { + final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); + return triangle -> { + ShapeField.decodeTriangle(triangle, scratchTriangle); + + switch (scratchTriangle.type) { + case POINT: + { + double y = decode(scratchTriangle.aY); + double x = decode(scratchTriangle.aX); + return component2D.contains(x, y); + } + case LINE: + { + double aY = decode(scratchTriangle.aY); + double aX = decode(scratchTriangle.aX); + double bY = decode(scratchTriangle.bY); + double bX = decode(scratchTriangle.bX); + return component2D.containsLine(aX, aY, bX, bY); + } + case TRIANGLE: + { + 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 component2D.containsTriangle(aX, aY, bX, bY, cX, cY); + } + default: + throw new IllegalArgumentException( + "Unsupported triangle type :[" + scratchTriangle.type + "]"); + } + }; + } + + @Override + protected Function contains() { + final ShapeField.DecodedTriangle scratchTriangle = new ShapeField.DecodedTriangle(); + return triangle -> { + ShapeField.decodeTriangle(triangle, scratchTriangle); + + switch (scratchTriangle.type) { + case POINT: + { + double y = decode(scratchTriangle.aY); + double x = decode(scratchTriangle.aX); + return component2D.withinPoint(x, y); + } + case LINE: + { + double aY = decode(scratchTriangle.aY); + double aX = decode(scratchTriangle.aX); + double bY = decode(scratchTriangle.bY); + double bX = decode(scratchTriangle.bX); + return component2D.withinLine(aX, aY, scratchTriangle.ab, bX, bY); + } + case TRIANGLE: + { + 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 component2D.withinTriangle( + aX, + aY, + scratchTriangle.ab, + bX, + bY, + scratchTriangle.bc, + cX, + cY, + scratchTriangle.ca); + } + default: + throw new IllegalArgumentException( + "Unsupported triangle type :[" + scratchTriangle.type + "]"); + } + }; + } + }; } @Override 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 e673c9e..d7ff62b 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/GeoEncodingUtils.java +++ b/lucene/core/src/java/org/apache/lucene/geo/GeoEncodingUtils.java @@ -178,7 +178,13 @@ public final class GeoEncodingUtils { box -> GeoUtils.relate( box.minLat, box.maxLat, box.minLon, box.maxLon, lat, lon, distanceSortKey, axisLat); - final Grid subBoxes = createSubBoxes(boundingBox, boxToRelation); + final Grid subBoxes = + createSubBoxes( + boundingBox.minLat, + boundingBox.maxLat, + boundingBox.minLon, + boundingBox.maxLon, + boxToRelation); return new DistancePredicate( subBoxes.latShift, @@ -200,11 +206,11 @@ public final class GeoEncodingUtils { * @lucene.internal */ 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); + final Grid subBoxes = + createSubBoxes( + tree.getMinY(), tree.getMaxY(), tree.getMinX(), tree.getMaxX(), boxToRelation); return new Component2DPredicate( subBoxes.latShift, @@ -218,13 +224,17 @@ public final class GeoEncodingUtils { } private static Grid createSubBoxes( - Rectangle boundingBox, Function boxToRelation) { - final int minLat = encodeLatitudeCeil(boundingBox.minLat); - final int maxLat = encodeLatitude(boundingBox.maxLat); - final int minLon = encodeLongitudeCeil(boundingBox.minLon); - final int maxLon = encodeLongitude(boundingBox.maxLon); - - if (maxLat < minLat || (boundingBox.crossesDateline() == false && maxLon < minLon)) { + double shapeMinLat, + double shapeMaxLat, + double shapeMinLon, + double shapeMaxLon, + Function boxToRelation) { + final int minLat = encodeLatitudeCeil(shapeMinLat); + final int maxLat = encodeLatitude(shapeMaxLat); + final int minLon = encodeLongitudeCeil(shapeMinLon); + final int maxLon = encodeLongitude(shapeMaxLon); + + if (maxLat < minLat || (shapeMaxLon >= shapeMinLon && maxLon < minLon)) { // the box cannot match any quantized point return new Grid(1, 1, 0, 0, 0, 0, new byte[0]); } @@ -243,7 +253,7 @@ public final class GeoEncodingUtils { { long minLon2 = (long) minLon - Integer.MIN_VALUE; long maxLon2 = (long) maxLon - Integer.MIN_VALUE; - if (boundingBox.crossesDateline()) { + if (shapeMaxLon < shapeMinLon) { // crosses dateline maxLon2 += 1L << 32; // wrap } lonShift = computeShift(minLon2, maxLon2); @@ -265,10 +275,8 @@ public final class GeoEncodingUtils { boxToRelation .apply( new Rectangle( - decodeLatitude(boxMinLat), - decodeLatitude(boxMaxLat), - decodeLongitude(boxMinLon), - decodeLongitude(boxMaxLon))) + decodeLatitude(boxMinLat), decodeLatitude(boxMaxLat), + decodeLongitude(boxMinLon), decodeLongitude(boxMaxLon))) .ordinal(); } } diff --git a/lucene/core/src/java/org/apache/lucene/geo/Point2D.java b/lucene/core/src/java/org/apache/lucene/geo/Point2D.java index 377b5b4..32b9f42 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/Point2D.java +++ b/lucene/core/src/java/org/apache/lucene/geo/Point2D.java @@ -167,9 +167,17 @@ final class Point2D implements Component2D { /** create a Point2D component tree from a LatLon point */ static Component2D create(Point point) { - return new Point2D( - GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(point.getLon())), - GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(point.getLat()))); + // Points behave as rectangles + double qLat = + point.getLat() == GeoUtils.MAX_LAT_INCL + ? point.getLat() + : GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitudeCeil(point.getLat())); + double qLon = + point.getLon() == GeoUtils.MAX_LON_INCL + ? point.getLon() + : GeoEncodingUtils.decodeLongitude( + GeoEncodingUtils.encodeLongitudeCeil(point.getLon())); + return new Point2D(qLon, qLat); } /** create a Point2D component tree from a XY point */ diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonDocValueTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonDocValueTestCase.java new file mode 100644 index 0000000..2e96c3a --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonDocValueTestCase.java @@ -0,0 +1,72 @@ +/* + * 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.document.ShapeField.QueryRelation; +import org.apache.lucene.geo.Circle; +import org.apache.lucene.geo.Line; +import org.apache.lucene.geo.Point; +import org.apache.lucene.geo.Polygon; +import org.apache.lucene.geo.Rectangle; +import org.apache.lucene.search.Query; + +/** + * Base test case for testing geospatial indexing and search functionality for {@link + * LatLonDocValuesField} * + */ +public abstract class BaseLatLonDocValueTestCase extends BaseLatLonSpatialTestCase { + + @Override + protected Query newRectQuery( + String field, + QueryRelation queryRelation, + double minLon, + double maxLon, + double minLat, + double maxLat) { + return LatLonDocValuesField.newSlowGeometryQuery( + field, queryRelation, new Rectangle(minLat, maxLat, minLon, maxLon)); + } + + @Override + protected Query newLineQuery(String field, QueryRelation queryRelation, Object... lines) { + return LatLonDocValuesField.newSlowGeometryQuery( + field, queryRelation, Arrays.stream(lines).toArray(Line[]::new)); + } + + @Override + protected Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons) { + return LatLonDocValuesField.newSlowGeometryQuery( + field, queryRelation, Arrays.stream(polygons).toArray(Polygon[]::new)); + } + + @Override + protected Query newDistanceQuery(String field, QueryRelation queryRelation, Object circle) { + return LatLonDocValuesField.newSlowGeometryQuery(field, queryRelation, (Circle) circle); + } + + @Override + protected Query newPointsQuery(String field, QueryRelation queryRelation, Object... points) { + Point[] pointsArray = new Point[points.length]; + for (int i = 0; i < points.length; i++) { + double[] point = (double[]) points[i]; + pointsArray[i] = new Point(point[0], point[1]); + } + return LatLonDocValuesField.newSlowGeometryQuery(field, queryRelation, pointsArray); + } +} diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonPointTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonPointTestCase.java new file mode 100644 index 0000000..d886979 --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonPointTestCase.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.lucene.document; + +import com.carrotsearch.randomizedtesting.generators.RandomPicks; +import java.util.Arrays; +import org.apache.lucene.document.ShapeField.QueryRelation; +import org.apache.lucene.geo.Circle; +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.Line; +import org.apache.lucene.geo.Point; +import org.apache.lucene.geo.Polygon; +import org.apache.lucene.geo.Rectangle; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.QueryUtils; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.IOUtils; + +/** + * Base test case for testing geospatial indexing and search functionality for {@link LatLonPoint} * + */ +public abstract class BaseLatLonPointTestCase extends BaseLatLonSpatialTestCase { + + @Override + protected Query newRectQuery( + String field, + QueryRelation queryRelation, + double minLon, + double maxLon, + double minLat, + double maxLat) { + return LatLonPoint.newGeometryQuery( + field, queryRelation, new Rectangle(minLat, maxLat, minLon, maxLon)); + } + + @Override + protected Query newLineQuery(String field, QueryRelation queryRelation, Object... lines) { + return LatLonPoint.newGeometryQuery( + field, queryRelation, Arrays.stream(lines).toArray(Line[]::new)); + } + + @Override + protected Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons) { + return LatLonPoint.newGeometryQuery( + field, queryRelation, Arrays.stream(polygons).toArray(Polygon[]::new)); + } + + @Override + protected Query newDistanceQuery(String field, QueryRelation queryRelation, Object circle) { + return LatLonPoint.newGeometryQuery(field, queryRelation, (Circle) circle); + } + + @Override + protected Query newPointsQuery(String field, QueryRelation queryRelation, Object... points) { + Point[] pointsArray = new Point[points.length]; + for (int i = 0; i < points.length; i++) { + double[] point = (double[]) points[i]; + pointsArray[i] = new Point(point[0], point[1]); + } + return LatLonPoint.newGeometryQuery(field, queryRelation, pointsArray); + } + + public void testBoundingBoxQueriesEquivalence() throws Exception { + int numShapes = atLeast(20); + + Directory dir = newDirectory(); + RandomIndexWriter w = new RandomIndexWriter(random(), dir); + + for (int i = 0; i < numShapes; i++) { + indexRandomShapes(w.w, nextShape()); + } + if (random().nextBoolean()) { + w.forceMerge(1); + } + + ///// search ////// + IndexReader reader = w.getReader(); + w.close(); + IndexSearcher searcher = newSearcher(reader); + + Rectangle box = GeoTestUtil.nextBox(); + + Query q1 = LatLonPoint.newBoxQuery(FIELD_NAME, box.minLat, box.maxLat, box.minLon, box.maxLon); + Query q2 = new LatLonPointQuery(FIELD_NAME, QueryRelation.INTERSECTS, box); + assertEquals(searcher.count(q1), searcher.count(q2)); + + IOUtils.close(w, reader, dir); + } + + public void testQueryEqualsAndHashcode() { + Polygon polygon = GeoTestUtil.nextPolygon(); + QueryRelation queryRelation = + RandomPicks.randomFrom( + random(), new QueryRelation[] {QueryRelation.INTERSECTS, QueryRelation.DISJOINT}); + String fieldName = "foo"; + Query q1 = newPolygonQuery(fieldName, queryRelation, polygon); + Query q2 = newPolygonQuery(fieldName, queryRelation, polygon); + QueryUtils.checkEqual(q1, q2); + // different field name + Query q3 = newPolygonQuery("bar", queryRelation, polygon); + QueryUtils.checkUnequal(q1, q3); + // different query relation + QueryRelation newQueryRelation = + RandomPicks.randomFrom( + random(), new QueryRelation[] {QueryRelation.INTERSECTS, QueryRelation.DISJOINT}); + Query q4 = newPolygonQuery(fieldName, newQueryRelation, polygon); + if (queryRelation == newQueryRelation) { + QueryUtils.checkEqual(q1, q4); + } else { + QueryUtils.checkUnequal(q1, q4); + } + // different shape + Polygon newPolygon = GeoTestUtil.nextPolygon(); + ; + Query q5 = newPolygonQuery(fieldName, queryRelation, newPolygon); + if (polygon.equals(newPolygon)) { + QueryUtils.checkEqual(q1, q5); + } else { + QueryUtils.checkUnequal(q1, q5); + } + } +} 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 0426ac0..36898a8 100644 --- a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java +++ b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java @@ -16,28 +16,14 @@ */ package org.apache.lucene.document; -import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude; -import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude; -import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; -import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil; -import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude; -import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil; -import static org.apache.lucene.geo.GeoTestUtil.nextLatitude; -import static org.apache.lucene.geo.GeoTestUtil.nextLongitude; - import com.carrotsearch.randomizedtesting.generators.RandomPicks; import java.util.Arrays; import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.geo.Circle; -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.Point; import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Rectangle; -import org.apache.lucene.geo.Tessellator; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.RandomIndexWriter; import org.apache.lucene.search.IndexSearcher; @@ -45,18 +31,12 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.QueryUtils; import org.apache.lucene.store.Directory; import org.apache.lucene.util.IOUtils; -import org.apache.lucene.util.TestUtil; - -/** Base test case for testing geospatial indexing and search functionality * */ -public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase { - - protected abstract ShapeType getShapeType(); - protected Object nextShape() { - return getShapeType().nextShape(); - } +/** + * Base test case for testing geospatial indexing and search functionality for {@link LatLonShape} * + */ +public abstract class BaseLatLonShapeTestCase extends BaseLatLonSpatialTestCase { - /** factory method to create a new bounding box query */ @Override protected Query newRectQuery( String field, @@ -68,14 +48,12 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase { return LatLonShape.newBoxQuery(field, queryRelation, minLat, maxLat, minLon, maxLon); } - /** factory method to create a new line query */ @Override protected Query newLineQuery(String field, QueryRelation queryRelation, Object... lines) { return LatLonShape.newLineQuery( field, queryRelation, Arrays.stream(lines).toArray(Line[]::new)); } - /** factory method to create a new polygon query */ @Override protected Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons) { return LatLonShape.newPolygonQuery( @@ -89,76 +67,56 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase { } @Override - protected Component2D toLine2D(Object... lines) { - return LatLonGeometry.create(Arrays.stream(lines).toArray(Line[]::new)); - } - - @Override - protected Component2D toPolygon2D(Object... polygons) { - return LatLonGeometry.create(Arrays.stream(polygons).toArray(Polygon[]::new)); - } - - @Override - protected Component2D toRectangle2D(double minX, double maxX, double minY, double maxY) { - return LatLonGeometry.create(new Rectangle(minY, maxY, minX, maxX)); - } - - @Override - protected Component2D toPoint2D(Object... points) { - double[][] p = Arrays.stream(points).toArray(double[][]::new); - org.apache.lucene.geo.Point[] pointArray = new org.apache.lucene.geo.Point[points.length]; - for (int i = 0; i < points.length; i++) { - pointArray[i] = new org.apache.lucene.geo.Point(p[i][0], p[i][1]); - } - return LatLonGeometry.create(pointArray); - } - - @Override protected Query newDistanceQuery(String field, QueryRelation queryRelation, Object circle) { return LatLonShape.newDistanceQuery(field, queryRelation, (Circle) circle); } - @Override - protected Component2D toCircle2D(Object circle) { - return LatLonGeometry.create((Circle) circle); - } - - @Override - protected Circle nextCircle() { - final double radiusMeters = - random().nextDouble() * GeoUtils.EARTH_MEAN_RADIUS_METERS * Math.PI / 2.0 + 1.0; - return new Circle(nextLatitude(), nextLongitude(), radiusMeters); - } + public void testBoundingBoxQueriesEquivalence() throws Exception { + int numShapes = atLeast(20); - @Override - public Rectangle randomQueryBox() { - return GeoTestUtil.nextBox(); - } + Directory dir = newDirectory(); + RandomIndexWriter w = new RandomIndexWriter(random(), dir); - @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(); + for (int i = 0; i < numShapes; i++) { + indexRandomShapes(w.w, nextShape()); + } + if (random().nextBoolean()) { + w.forceMerge(1); } - return points; - } - @Override - protected double rectMinX(Object rect) { - return ((Rectangle) rect).minLon; - } + ///// search ////// + IndexReader reader = w.getReader(); + w.close(); + IndexSearcher searcher = newSearcher(reader); - @Override - protected double rectMaxX(Object rect) { - return ((Rectangle) rect).maxLon; - } + Rectangle box = GeoTestUtil.nextBox(); - @Override - protected double rectMinY(Object rect) { - return ((Rectangle) rect).minLat; + Query q1 = + LatLonShape.newBoxQuery( + FIELD_NAME, QueryRelation.INTERSECTS, box.minLat, box.maxLat, box.minLon, box.maxLon); + Query q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.INTERSECTS, box); + assertEquals(searcher.count(q1), searcher.count(q2)); + q1 = + LatLonShape.newBoxQuery( + FIELD_NAME, QueryRelation.WITHIN, box.minLat, box.maxLat, box.minLon, box.maxLon); + q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.WITHIN, box); + assertEquals(searcher.count(q1), searcher.count(q2)); + q1 = + LatLonShape.newBoxQuery( + FIELD_NAME, QueryRelation.CONTAINS, box.minLat, box.maxLat, box.minLon, box.maxLon); + if (box.crossesDateline()) { + q2 = LatLonShape.newGeometryQuery(FIELD_NAME, QueryRelation.CONTAINS, box); + } else { + q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.CONTAINS, box); + } + assertEquals(searcher.count(q1), searcher.count(q2)); + q1 = + LatLonShape.newBoxQuery( + FIELD_NAME, QueryRelation.DISJOINT, box.minLat, box.maxLat, box.minLon, box.maxLon); + q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.DISJOINT, box); + assertEquals(searcher.count(q1), searcher.count(q2)); + + IOUtils.close(w, reader, dir); } public void testBoxQueryEqualsAndHashcode() { @@ -224,11 +182,6 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase { } } - /** factory method to create a new line query */ - protected Query newLineQuery(String field, QueryRelation queryRelation, Line... lines) { - return LatLonShape.newLineQuery(field, queryRelation, lines); - } - public void testLineQueryEqualsAndHashcode() { Line line = nextLine(); QueryRelation queryRelation = RandomPicks.randomFrom(random(), POINT_LINE_RELATIONS); @@ -257,59 +210,6 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase { } } - public void testBoundingBoxQueriesEquivalence() throws Exception { - int numShapes = atLeast(20); - - Directory dir = newDirectory(); - RandomIndexWriter w = new RandomIndexWriter(random(), dir); - - for (int i = 0; i < numShapes; i++) { - indexRandomShapes(w.w, nextShape()); - } - if (random().nextBoolean()) { - w.forceMerge(1); - } - - ///// search ////// - IndexReader reader = w.getReader(); - w.close(); - IndexSearcher searcher = newSearcher(reader); - - Rectangle box = GeoTestUtil.nextBox(); - - Query q1 = - LatLonShape.newBoxQuery( - FIELD_NAME, QueryRelation.INTERSECTS, box.minLat, box.maxLat, box.minLon, box.maxLon); - Query q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.INTERSECTS, box); - assertEquals(searcher.count(q1), searcher.count(q2)); - q1 = - LatLonShape.newBoxQuery( - FIELD_NAME, QueryRelation.WITHIN, box.minLat, box.maxLat, box.minLon, box.maxLon); - q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.WITHIN, box); - assertEquals(searcher.count(q1), searcher.count(q2)); - q1 = - LatLonShape.newBoxQuery( - FIELD_NAME, QueryRelation.CONTAINS, box.minLat, box.maxLat, box.minLon, box.maxLon); - if (box.crossesDateline()) { - q2 = LatLonShape.newGeometryQuery(FIELD_NAME, QueryRelation.CONTAINS, box); - } else { - q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.CONTAINS, box); - } - assertEquals(searcher.count(q1), searcher.count(q2)); - q1 = - LatLonShape.newBoxQuery( - FIELD_NAME, QueryRelation.DISJOINT, box.minLat, box.maxLat, box.minLon, box.maxLon); - q2 = new LatLonShapeQuery(FIELD_NAME, QueryRelation.DISJOINT, box); - assertEquals(searcher.count(q1), searcher.count(q2)); - - IOUtils.close(w, reader, dir); - } - - /** factory method to create a new polygon query */ - protected Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) { - return LatLonShape.newPolygonQuery(field, queryRelation, polygons); - } - public void testPolygonQueryEqualsAndHashcode() { Polygon polygon = GeoTestUtil.nextPolygon(); QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values()); @@ -338,99 +238,4 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase { QueryUtils.checkUnequal(q1, q5); } } - - @Override - protected double rectMaxY(Object rect) { - return ((Rectangle) rect).maxLat; - } - - @Override - protected boolean rectCrossesDateline(Object rect) { - return ((Rectangle) rect).crossesDateline(); - } - - @Override - public Line nextLine() { - return GeoTestUtil.nextLine(); - } - - @Override - protected Polygon nextPolygon() { - return GeoTestUtil.nextPolygon(); - } - - @Override - protected Encoder getEncoder() { - return new Encoder() { - @Override - double decodeX(int encoded) { - return decodeLongitude(encoded); - } - - @Override - double decodeY(int encoded) { - return decodeLatitude(encoded); - } - - @Override - double quantizeX(double raw) { - return decodeLongitude(encodeLongitude(raw)); - } - - @Override - double quantizeXCeil(double raw) { - return decodeLongitude(encodeLongitudeCeil(raw)); - } - - @Override - double quantizeY(double raw) { - return decodeLatitude(encodeLatitude(raw)); - } - - @Override - double quantizeYCeil(double raw) { - return decodeLatitude(encodeLatitudeCeil(raw)); - } - }; - } - - /** internal shape type for testing different shape types */ - protected enum ShapeType { - POINT() { - public Point nextShape() { - return GeoTestUtil.nextPoint(); - } - }, - LINE() { - public Line nextShape() { - return GeoTestUtil.nextLine(); - } - }, - POLYGON() { - public Polygon nextShape() { - while (true) { - Polygon p = GeoTestUtil.nextPolygon(); - try { - Tessellator.tessellate(p); - return p; - } catch (IllegalArgumentException e) { - // if we can't tessellate; then random polygon generator created a malformed shape - } - } - } - }, - MIXED() { - public Object nextShape() { - return RandomPicks.randomFrom(random(), subList).nextShape(); - } - }; - - static ShapeType[] subList; - - static { - subList = new ShapeType[] {POINT, LINE, POLYGON}; - } - - public abstract Object nextShape(); - } } diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonSpatialTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonSpatialTestCase.java new file mode 100644 index 0000000..518c9be --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonSpatialTestCase.java @@ -0,0 +1,220 @@ +/* + * 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 static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude; +import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude; +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil; +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude; +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil; +import static org.apache.lucene.geo.GeoTestUtil.nextLatitude; +import static org.apache.lucene.geo.GeoTestUtil.nextLongitude; + +import com.carrotsearch.randomizedtesting.generators.RandomPicks; +import java.util.Arrays; +import org.apache.lucene.document.ShapeField.QueryRelation; +import org.apache.lucene.geo.Circle; +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.Point; +import org.apache.lucene.geo.Polygon; +import org.apache.lucene.geo.Rectangle; +import org.apache.lucene.geo.Tessellator; +import org.apache.lucene.search.Query; +import org.apache.lucene.util.TestUtil; + +/** Base test case for testing geospatial indexing and search functionality * */ +public abstract class BaseLatLonSpatialTestCase extends BaseSpatialTestCase { + + protected abstract ShapeType getShapeType(); + + protected Object nextShape() { + return getShapeType().nextShape(); + } + + @Override + protected Component2D toLine2D(Object... lines) { + return LatLonGeometry.create(Arrays.stream(lines).toArray(Line[]::new)); + } + + @Override + protected Component2D toPolygon2D(Object... polygons) { + return LatLonGeometry.create(Arrays.stream(polygons).toArray(Polygon[]::new)); + } + + @Override + protected Component2D toRectangle2D(double minX, double maxX, double minY, double maxY) { + return LatLonGeometry.create(new Rectangle(minY, maxY, minX, maxX)); + } + + @Override + protected Component2D toPoint2D(Object... points) { + double[][] p = Arrays.stream(points).toArray(double[][]::new); + Point[] pointArray = new Point[points.length]; + for (int i = 0; i < points.length; i++) { + pointArray[i] = new Point(p[i][0], p[i][1]); + } + return LatLonGeometry.create(pointArray); + } + + @Override + protected Component2D toCircle2D(Object circle) { + return LatLonGeometry.create((Circle) circle); + } + + @Override + protected Circle nextCircle() { + final double radiusMeters = + random().nextDouble() * GeoUtils.EARTH_MEAN_RADIUS_METERS * Math.PI / 2.0 + 1.0; + return new Circle(nextLatitude(), nextLongitude(), radiusMeters); + } + + @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; + } + + @Override + protected double rectMaxX(Object rect) { + return ((Rectangle) rect).maxLon; + } + + @Override + protected double rectMinY(Object rect) { + return ((Rectangle) rect).minLat; + } + + /** factory method to create a new polygon query */ + protected Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) { + return LatLonShape.newPolygonQuery(field, queryRelation, polygons); + } + + @Override + protected double rectMaxY(Object rect) { + return ((Rectangle) rect).maxLat; + } + + @Override + protected boolean rectCrossesDateline(Object rect) { + return ((Rectangle) rect).crossesDateline(); + } + + @Override + public Line nextLine() { + return GeoTestUtil.nextLine(); + } + + @Override + protected Polygon nextPolygon() { + return GeoTestUtil.nextPolygon(); + } + + @Override + protected Encoder getEncoder() { + return new Encoder() { + @Override + double decodeX(int encoded) { + return decodeLongitude(encoded); + } + + @Override + double decodeY(int encoded) { + return decodeLatitude(encoded); + } + + @Override + double quantizeX(double raw) { + return decodeLongitude(encodeLongitude(raw)); + } + + @Override + double quantizeXCeil(double raw) { + return decodeLongitude(encodeLongitudeCeil(raw)); + } + + @Override + double quantizeY(double raw) { + return decodeLatitude(encodeLatitude(raw)); + } + + @Override + double quantizeYCeil(double raw) { + return decodeLatitude(encodeLatitudeCeil(raw)); + } + }; + } + + /** internal shape type for testing different shape types */ + protected enum ShapeType { + POINT() { + public Point nextShape() { + return GeoTestUtil.nextPoint(); + } + }, + LINE() { + public Line nextShape() { + return GeoTestUtil.nextLine(); + } + }, + POLYGON() { + public Polygon nextShape() { + while (true) { + Polygon p = GeoTestUtil.nextPolygon(); + try { + Tessellator.tessellate(p); + return p; + } catch (IllegalArgumentException e) { + // if we can't tessellate; then random polygon generator created a malformed shape + } + } + } + }, + MIXED() { + public Object nextShape() { + return RandomPicks.randomFrom(random(), subList).nextShape(); + } + }; + + static ShapeType[] subList; + + static { + subList = new ShapeType[] {POINT, LINE, POLYGON}; + } + + public abstract Object nextShape(); + } +} diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseSpatialTestCase.java similarity index 99% rename from lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java rename to lucene/core/src/test/org/apache/lucene/document/BaseSpatialTestCase.java index 90d11d1..2d2da74 100644 --- a/lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java +++ b/lucene/core/src/test/org/apache/lucene/document/BaseSpatialTestCase.java @@ -52,9 +52,9 @@ import org.apache.lucene.util.TestUtil; * Base test case for testing spherical and cartesian geometry indexing and search functionality * *

This class is implemented by {@link BaseXYShapeTestCase} for testing XY cartesian geometry and - * {@link BaseLatLonShapeTestCase} for testing Lat Lon geospatial geometry + * {@link BaseLatLonSpatialTestCase} for testing Lat Lon geospatial geometry */ -public abstract class BaseShapeTestCase extends LuceneTestCase { +public abstract class BaseSpatialTestCase extends LuceneTestCase { /** name of the LatLonShape indexed field */ protected static final String FIELD_NAME = "shape"; @@ -65,7 +65,7 @@ public abstract class BaseShapeTestCase extends LuceneTestCase { QueryRelation.INTERSECTS, QueryRelation.DISJOINT, QueryRelation.CONTAINS }; - public BaseShapeTestCase() { + public BaseSpatialTestCase() { ENCODER = getEncoder(); VALIDATOR = getValidator(); } diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java index 850a718..410c836 100644 --- a/lucene/core/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java +++ b/lucene/core/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java @@ -36,7 +36,7 @@ import org.apache.lucene.search.Query; import org.apache.lucene.util.TestUtil; /** Base test case for testing indexing and search functionality of cartesian geometry * */ -public abstract class BaseXYShapeTestCase extends BaseShapeTestCase { +public abstract class BaseXYShapeTestCase extends BaseSpatialTestCase { protected abstract ShapeType getShapeType(); protected Object nextShape() { diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonDocValuesMultiPointPointQueries.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonDocValuesMultiPointPointQueries.java new file mode 100644 index 0000000..e66a420 --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonDocValuesMultiPointPointQueries.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.lucene.document; + +import org.apache.lucene.document.ShapeField.QueryRelation; +import org.apache.lucene.geo.Component2D; +import org.apache.lucene.geo.Point; + +/** + * random bounding box, line, and polygon query tests for random indexed arrays of {@code latitude, + * longitude} points + */ +public class TestLatLonDocValuesMultiPointPointQueries extends BaseLatLonDocValueTestCase { + + @Override + protected ShapeType getShapeType() { + return ShapeType.POINT; + } + + @Override + protected Object nextShape() { + int n = random().nextInt(4) + 1; + Point[] points = new Point[n]; + for (int i = 0; i < n; i++) { + points[i] = (Point) ShapeType.POINT.nextShape(); + } + return points; + } + + @Override + protected Field[] createIndexableFields(String name, Object o) { + Point[] points = (Point[]) o; + Field[] fields = new Field[points.length]; + for (int i = 0; i < points.length; i++) { + fields[i] = new LatLonDocValuesField(FIELD_NAME, points[i].getLat(), points[i].getLon()); + } + return fields; + } + + @Override + public Validator getValidator() { + return new MultiPointValidator(ENCODER); + } + + protected class MultiPointValidator extends Validator { + TestLatLonPointShapeQueries.PointValidator POINTVALIDATOR; + + MultiPointValidator(Encoder encoder) { + super(encoder); + POINTVALIDATOR = new TestLatLonPointShapeQueries.PointValidator(encoder); + } + + @Override + public Validator setRelation(QueryRelation relation) { + super.setRelation(relation); + POINTVALIDATOR.queryRelation = relation; + return this; + } + + @Override + public boolean testComponentQuery(Component2D query, Object shape) { + Point[] points = (Point[]) shape; + for (Point p : points) { + boolean b = POINTVALIDATOR.testComponentQuery(query, p); + if (b == true && queryRelation == QueryRelation.INTERSECTS) { + return true; + } else if (b == true && queryRelation == QueryRelation.CONTAINS) { + return true; + } else if (b == false && queryRelation == QueryRelation.DISJOINT) { + return false; + } else if (b == false && queryRelation == QueryRelation.WITHIN) { + return false; + } + } + return queryRelation != QueryRelation.INTERSECTS && queryRelation != QueryRelation.CONTAINS; + } + } + + @Slow + @Nightly + @Override + public void testRandomBig() throws Exception { + doTestRandom(10000); + } +} diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonDocValuesPointPointQueries.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonDocValuesPointPointQueries.java new file mode 100644 index 0000000..1e8532b --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonDocValuesPointPointQueries.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.lucene.document; + +import org.apache.lucene.document.ShapeField.QueryRelation; +import org.apache.lucene.geo.Component2D; +import org.apache.lucene.geo.Point; + +/** + * random bounding box, line, and polygon query tests for random indexed arrays of {@code latitude, + * longitude} points + */ +public class TestLatLonDocValuesPointPointQueries extends BaseLatLonDocValueTestCase { + + @Override + protected ShapeType getShapeType() { + return ShapeType.POINT; + } + + @Override + protected Field[] createIndexableFields(String name, Object o) { + Point point = (Point) o; + Field[] fields = new Field[1]; + fields[0] = new LatLonDocValuesField(FIELD_NAME, point.getLat(), point.getLon()); + return fields; + } + + @Override + protected Validator getValidator() { + return new TestLatLonPointShapeQueries.PointValidator(this.ENCODER); + } + + protected static class PointValidator extends Validator { + protected PointValidator(Encoder encoder) { + super(encoder); + } + + @Override + public boolean testComponentQuery(Component2D query, Object shape) { + Point p = (Point) shape; + if (queryRelation == QueryRelation.CONTAINS) { + return testWithinQuery( + query, LatLonShape.createIndexableFields("dummy", p.getLat(), p.getLon())) + == Component2D.WithinRelation.CANDIDATE; + } + return testComponentQuery( + query, LatLonShape.createIndexableFields("dummy", p.getLat(), p.getLon())); + } + } + + @Slow + @Nightly + @Override + public void testRandomBig() throws Exception { + doTestRandom(10000); + } +} diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonMultiPointPointQueries.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonMultiPointPointQueries.java new file mode 100644 index 0000000..808fb9d --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonMultiPointPointQueries.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.lucene.document; + +import org.apache.lucene.document.ShapeField.QueryRelation; +import org.apache.lucene.geo.Component2D; +import org.apache.lucene.geo.Point; + +/** + * random bounding box, line, and polygon query tests for random indexed arrays of {@code latitude, + * longitude} points + */ +public class TestLatLonMultiPointPointQueries extends BaseLatLonPointTestCase { + + @Override + protected ShapeType getShapeType() { + return ShapeType.POINT; + } + + @Override + protected Object nextShape() { + int n = random().nextInt(4) + 1; + Point[] points = new Point[n]; + for (int i = 0; i < n; i++) { + points[i] = (Point) ShapeType.POINT.nextShape(); + } + return points; + } + + @Override + protected Field[] createIndexableFields(String name, Object o) { + Point[] points = (Point[]) o; + Field[] fields = new Field[points.length]; + for (int i = 0; i < points.length; i++) { + fields[i] = new LatLonPoint(FIELD_NAME, points[i].getLat(), points[i].getLon()); + } + return fields; + } + + @Override + public Validator getValidator() { + return new MultiPointValidator(ENCODER); + } + + protected class MultiPointValidator extends Validator { + TestLatLonPointShapeQueries.PointValidator POINTVALIDATOR; + + MultiPointValidator(Encoder encoder) { + super(encoder); + POINTVALIDATOR = new TestLatLonPointShapeQueries.PointValidator(encoder); + } + + @Override + public Validator setRelation(QueryRelation relation) { + super.setRelation(relation); + POINTVALIDATOR.queryRelation = relation; + return this; + } + + @Override + public boolean testComponentQuery(Component2D query, Object shape) { + Point[] points = (Point[]) shape; + for (Point p : points) { + boolean b = POINTVALIDATOR.testComponentQuery(query, p); + if (b == true && queryRelation == QueryRelation.INTERSECTS) { + return true; + } else if (b == true && queryRelation == QueryRelation.CONTAINS) { + return true; + } else if (b == false && queryRelation == QueryRelation.DISJOINT) { + return false; + } else if (b == false && queryRelation == QueryRelation.WITHIN) { + return false; + } + } + return queryRelation != QueryRelation.INTERSECTS && queryRelation != QueryRelation.CONTAINS; + } + } + + @Slow + @Nightly + @Override + public void testRandomBig() throws Exception { + doTestRandom(10000); + } +} diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonPointPointQueries.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonPointPointQueries.java new file mode 100644 index 0000000..752f9a0 --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonPointPointQueries.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.lucene.document; + +import org.apache.lucene.document.ShapeField.QueryRelation; +import org.apache.lucene.geo.Component2D; +import org.apache.lucene.geo.Point; + +/** + * random bounding box, line, and polygon query tests for random indexed arrays of {@code latitude, + * longitude} points + */ +public class TestLatLonPointPointQueries extends BaseLatLonPointTestCase { + + @Override + protected ShapeType getShapeType() { + return ShapeType.POINT; + } + + @Override + protected Validator getValidator() { + return new TestLatLonPointShapeQueries.PointValidator(this.ENCODER); + } + + @Override + protected Field[] createIndexableFields(String name, Object o) { + Point point = (Point) o; + return new Field[] {new LatLonPoint(FIELD_NAME, point.getLat(), point.getLon())}; + } + + protected static class PointValidator extends Validator { + protected PointValidator(Encoder encoder) { + super(encoder); + } + + @Override + public boolean testComponentQuery(Component2D query, Object shape) { + Point p = (Point) shape; + if (queryRelation == QueryRelation.CONTAINS) { + return testWithinQuery( + query, LatLonShape.createIndexableFields("dummy", p.getLat(), p.getLon())) + == Component2D.WithinRelation.CANDIDATE; + } + return testComponentQuery( + query, LatLonShape.createIndexableFields("dummy", p.getLat(), p.getLon())); + } + } + + @Slow + @Nightly + @Override + public void testRandomBig() throws Exception { + doTestRandom(10000); + } +} 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 70ddb7c..a916602 100644 --- a/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java +++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java @@ -456,6 +456,15 @@ public class TestLatLonShape extends LuceneTestCase { RandomIndexWriter writer = new RandomIndexWriter(random(), dir); Document document = new Document(); Point p = GeoTestUtil.nextPoint(); + double qLat = + p.getLat() == GeoUtils.MAX_LAT_INCL + ? p.getLat() + : GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitudeCeil(p.getLat())); + double qLon = + p.getLon() == GeoUtils.MAX_LON_INCL + ? p.getLon() + : GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitudeCeil(p.getLon())); + p = new Point(qLat, qLon); Field[] fields = LatLonShape.createIndexableFields(FIELDNAME, p.getLat(), p.getLon()); for (Field f : fields) { document.add(f); 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 f52c769..2d3e6b2 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestLatLonDocValuesQueries.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestLatLonDocValuesQueries.java @@ -18,6 +18,7 @@ package org.apache.lucene.search; import org.apache.lucene.document.Document; import org.apache.lucene.document.LatLonDocValuesField; +import org.apache.lucene.document.ShapeField; import org.apache.lucene.geo.BaseGeoPointTestCase; import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.LatLonGeometry; @@ -49,7 +50,8 @@ public class TestLatLonDocValuesQueries extends BaseGeoPointTestCase { @Override protected Query newGeometryQuery(String field, LatLonGeometry... geometry) { - return LatLonDocValuesField.newSlowGeometryQuery(field, geometry); + return LatLonDocValuesField.newSlowGeometryQuery( + field, ShapeField.QueryRelation.INTERSECTS, geometry); } @Override 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 9abbe64..c2cea11 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestLatLonPointQueries.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestLatLonPointQueries.java @@ -19,6 +19,7 @@ package org.apache.lucene.search; import java.io.IOException; import org.apache.lucene.document.Document; import org.apache.lucene.document.LatLonPoint; +import org.apache.lucene.document.ShapeField; import org.apache.lucene.geo.BaseGeoPointTestCase; import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.LatLonGeometry; @@ -55,7 +56,7 @@ public class TestLatLonPointQueries extends BaseGeoPointTestCase { @Override protected Query newGeometryQuery(String field, LatLonGeometry... geometry) { - return LatLonPoint.newGeometryQuery(field, geometry); + return LatLonPoint.newGeometryQuery(field, ShapeField.QueryRelation.INTERSECTS, geometry); } @Override