mirror of https://github.com/apache/lucene.git
LUCENE-8435: Add new LatLonShapePolygonQuery for querying indexed LatLonShape fields by arbitrary polygons
This commit is contained in:
parent
679b4aa71d
commit
18c2300fd6
|
@ -213,6 +213,8 @@ Changes in Runtime Behavior:
|
|||
|
||||
Improvements
|
||||
|
||||
* LUCENE-8435: Add new LatLonShapePolygonQuery for querying indexed LatLonShape fields by arbitrary polygons (Nick Knize)
|
||||
|
||||
* LUCENE-8367: Make per-dimension drill down optional for each facet dimension (Mike McCandless)
|
||||
|
||||
* LUCENE-8396: Add Points Based Shape Indexing and Search that decomposes shapes
|
||||
|
|
|
@ -202,6 +202,36 @@ public final class Polygon {
|
|||
return sb.toString();
|
||||
}
|
||||
|
||||
private String verticesToGeoJSON(final double[] lats, final double[] lons) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append('[');
|
||||
for (int i = 0; i < lats.length; i++) {
|
||||
sb.append("[")
|
||||
.append(lons[i])
|
||||
.append(", ")
|
||||
.append(lats[i])
|
||||
.append("]");
|
||||
if (i != lats.length - 1) {
|
||||
sb.append(", ");
|
||||
}
|
||||
}
|
||||
sb.append(']');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** prints polygons as geojson */
|
||||
public String toGeoJSON() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[");
|
||||
sb.append(verticesToGeoJSON(polyLats, polyLons));
|
||||
for (Polygon hole : holes) {
|
||||
sb.append(",");
|
||||
sb.append(verticesToGeoJSON(hole.polyLats, hole.polyLons));
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** Parses a standard GeoJSON polygon string. The type of the incoming GeoJSON object must be a Polygon or MultiPolygon, optionally
|
||||
* embedded under a "type: Feature". A Polygon will return as a length 1 array, while a MultiPolygon will be 1 or more in length.
|
||||
*
|
||||
|
|
|
@ -19,7 +19,6 @@ package org.apache.lucene.geo;
|
|||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.index.PointValues.Relation;
|
||||
import org.apache.lucene.util.ArrayUtil;
|
||||
|
||||
|
@ -123,7 +122,35 @@ public final class Polygon2D {
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/** Returns relation to the provided triangle */
|
||||
public Relation relateTriangle(double ax, double ay, double bx, double by, double cx, double cy) {
|
||||
// compute bounding box of triangle
|
||||
double minLat = StrictMath.min(StrictMath.min(ay, by), cy);
|
||||
double minLon = StrictMath.min(StrictMath.min(ax, bx), cx);
|
||||
double maxLat = StrictMath.max(StrictMath.max(ay, by), cy);
|
||||
double maxLon = StrictMath.max(StrictMath.max(ax, bx), cx);
|
||||
if (minLat <= maxY && minLon <= maxX) {
|
||||
Relation relation = componentRelateTriangle(ax, ay, bx, by, cx, cy);
|
||||
if (relation != Relation.CELL_OUTSIDE_QUERY) {
|
||||
return relation;
|
||||
}
|
||||
if (left != null) {
|
||||
relation = left.relateTriangle(ax, ay, bx, by, cx, cy);
|
||||
if (relation != Relation.CELL_OUTSIDE_QUERY) {
|
||||
return relation;
|
||||
}
|
||||
}
|
||||
if (right != null && ((splitX == false && maxLat >= this.minLat) || (splitX && maxLon >= this.minLon))) {
|
||||
relation = right.relateTriangle(ax, ay, bx, by, cx, cy);
|
||||
if (relation != Relation.CELL_OUTSIDE_QUERY) {
|
||||
return relation;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
|
||||
/** Returns relation to the provided rectangle */
|
||||
public Relation relate(double minLat, double maxLat, double minLon, double maxLon) {
|
||||
if (minLat <= maxY && minLon <= maxX) {
|
||||
|
@ -147,6 +174,42 @@ public final class Polygon2D {
|
|||
return Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
|
||||
private Relation componentRelateTriangle(double ax, double ay, double bx, double by, double cx, double cy) {
|
||||
// compute bounding box of triangle
|
||||
double minLat = StrictMath.min(StrictMath.min(ay, by), cy);
|
||||
double minLon = StrictMath.min(StrictMath.min(ax, bx), cx);
|
||||
double maxLat = StrictMath.max(StrictMath.max(ay, by), cy);
|
||||
double maxLon = StrictMath.max(StrictMath.max(ax, bx), cx);
|
||||
if (maxLon < this.minLon || minLon > this.maxLon || maxLat < this.minLat || minLat > this.maxLat) {
|
||||
return Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
// check any holes
|
||||
if (holes != null) {
|
||||
Relation holeRelation = holes.relateTriangle(ax, ay, bx, by, cx, cy);
|
||||
if (holeRelation == Relation.CELL_CROSSES_QUERY) {
|
||||
return Relation.CELL_CROSSES_QUERY;
|
||||
} else if (holeRelation == Relation.CELL_INSIDE_QUERY) {
|
||||
return Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
}
|
||||
// check each corner: if < 3 are present, its cheaper than crossesSlowly
|
||||
int numCorners = numberOfTriangleCorners(ax, ay, bx, by, cx, cy);
|
||||
if (numCorners == 3) {
|
||||
if (tree.crossesTriangle(ax, ay, bx, by, cx, cy)) {
|
||||
return Relation.CELL_CROSSES_QUERY;
|
||||
}
|
||||
return Relation.CELL_INSIDE_QUERY;
|
||||
} else if (numCorners > 0) {
|
||||
return Relation.CELL_CROSSES_QUERY;
|
||||
}
|
||||
|
||||
// we cross
|
||||
if (tree.crossesTriangle(ax, ay, bx, by, cx, cy)) {
|
||||
return Relation.CELL_CROSSES_QUERY;
|
||||
}
|
||||
return Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
|
||||
/** Returns relation to the provided rectangle for this component */
|
||||
private Relation componentRelate(double minLat, double maxLat, double minLon, double maxLon) {
|
||||
// if the bounding boxes are disjoint then the shape does not cross
|
||||
|
@ -184,7 +247,24 @@ public final class Polygon2D {
|
|||
|
||||
return Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
|
||||
|
||||
private int numberOfTriangleCorners(double ax, double ay, double bx, double by, double cx, double cy) {
|
||||
int containsCount = 0;
|
||||
if (componentContains(ay, ax)) {
|
||||
containsCount++;
|
||||
}
|
||||
if (componentContains(by, bx)) {
|
||||
containsCount++;
|
||||
}
|
||||
if (containsCount == 1) {
|
||||
return containsCount;
|
||||
}
|
||||
if (componentContains(cy, cx)) {
|
||||
containsCount++;
|
||||
}
|
||||
return containsCount;
|
||||
}
|
||||
|
||||
// returns 0, 4, or something in between
|
||||
private int numberOfCorners(double minLat, double maxLat, double minLon, double maxLon) {
|
||||
int containsCount = 0;
|
||||
|
@ -345,7 +425,66 @@ public final class Polygon2D {
|
|||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
/** Returns true if the triangle crosses any edge in this edge subtree */
|
||||
boolean crossesTriangle(double ax, double ay, double bx, double by, double cx, double cy) {
|
||||
// compute bounding box of triangle
|
||||
double minLat = StrictMath.min(StrictMath.min(ay, by), cy);
|
||||
double minLon = StrictMath.min(StrictMath.min(ax, bx), cx);
|
||||
double maxLat = StrictMath.max(StrictMath.max(ay, by), cy);
|
||||
double maxLon = StrictMath.max(StrictMath.max(ax, bx), cx);
|
||||
|
||||
if (minLat <= max) {
|
||||
double dy = lat1;
|
||||
double ey = lat2;
|
||||
double dx = lon1;
|
||||
double ex = lon2;
|
||||
|
||||
// optimization: see if the rectangle is outside of the "bounding box" of the polyline at all
|
||||
// if not, don't waste our time trying more complicated stuff
|
||||
boolean outside = (dy < minLat && ey < minLat) ||
|
||||
(dy > maxLat && ey > maxLat) ||
|
||||
(dx < minLon && ex < minLon) ||
|
||||
(dx > maxLon && ex > maxLon);
|
||||
|
||||
if (outside == false) {
|
||||
// does triangle's first edge intersect polyline?
|
||||
// ax, ay -> bx, by
|
||||
if (orient(dx, dy, ex, ey, ax, ay) * orient(dx, dy, ex, ey, bx, by) <= 0 &&
|
||||
orient(ax, ay, bx, by, dx, dy) * orient(ax, ay, bx, by, ex, ey) <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// does triangle's second edge intersect polyline?
|
||||
// bx, by -> cx, cy
|
||||
if (orient(dx, dy, ex, ey, bx, by) * orient(dx, dy, ex, ey, cx, cy) <= 0 &&
|
||||
orient(bx, by, cx, cy, dx, dy) * orient(bx, by, cx, cy, ex, ey) <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// does triangle's third edge intersect polyline?
|
||||
// cx, cy -> ax, ay
|
||||
if (orient(dx, dy, ex, ey, cx, cy) * orient(dx, dy, ex, ey, ax, ay) <= 0 &&
|
||||
orient(cx, cy, ax, ay, dx, dy) * orient(cx, cy, ax, ay, ex, ey) <= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (left != null) {
|
||||
if (left.crossesTriangle(ax, ay, bx, by, cx, cy)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (right != null && maxLat >= low) {
|
||||
if (right.crossesTriangle(ax, ay, bx, by, cx, cy)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Returns true if the box crosses any edge in this edge subtree */
|
||||
boolean crosses(double minLat, double maxLat, double minLon, double maxLon) {
|
||||
// we just have to cross one edge to answer the question, so we descend the tree and return when we do.
|
||||
|
|
|
@ -16,10 +16,13 @@
|
|||
*/
|
||||
package org.apache.lucene.geo;
|
||||
|
||||
import static org.apache.lucene.geo.GeoTestUtil.createRegularPolygon;
|
||||
import static org.apache.lucene.geo.GeoTestUtil.nextLatitude;
|
||||
import static org.apache.lucene.geo.GeoTestUtil.nextLongitude;
|
||||
import static org.apache.lucene.geo.GeoTestUtil.nextPointNear;
|
||||
import static org.apache.lucene.geo.GeoTestUtil.nextPolygon;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
|
||||
import org.apache.lucene.index.PointValues.Relation;
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
|
||||
|
@ -289,4 +292,44 @@ public class TestPolygon2D extends LuceneTestCase {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// targets the polygon directly
|
||||
public void testRelateTriangle() {
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
Polygon polygon = nextPolygon();
|
||||
Polygon2D impl = Polygon2D.create(polygon);
|
||||
|
||||
for (int j = 0; j < 100; j++) {
|
||||
double[] a = nextPointNear(polygon);
|
||||
double[] b = nextPointNear(polygon);
|
||||
double[] c = nextPointNear(polygon);
|
||||
|
||||
// if the point is within poly, then triangle should not intersect
|
||||
if (impl.contains(a[0], a[1]) || impl.contains(b[0], b[1]) || impl.contains(c[0], c[1])) {
|
||||
assertTrue(impl.relateTriangle(a[1], a[0], b[1], b[0], c[1], c[0]) != Relation.CELL_OUTSIDE_QUERY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test
|
||||
public void testRelateTriangleEdgeCases() {
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
// random radius between 1Km and 100Km
|
||||
int randomRadius = RandomNumbers.randomIntBetween(random(), 1000, 100000);
|
||||
// random number of vertices
|
||||
int numVertices = RandomNumbers.randomIntBetween(random(), 100, 1000);
|
||||
Polygon polygon = createRegularPolygon(0, 0, randomRadius, numVertices);
|
||||
Polygon2D impl = Polygon2D.create(polygon);
|
||||
|
||||
// create and test a simple tessellation
|
||||
for (int j = 1; j < numVertices; ++j) {
|
||||
double[] a = new double[] {0d, 0d}; // center of poly
|
||||
double[] b = new double[] {polygon.getPolyLat(j - 1), polygon.getPolyLon(j - 1)};
|
||||
// occassionally test pancake triangles
|
||||
double[] c = random().nextBoolean() ? new double[] {polygon.getPolyLat(j), polygon.getPolyLon(j)} : new double[] {a[0], a[1]};
|
||||
assertTrue(impl.relateTriangle(a[0], a[1], b[0], b[1], c[0], c[1]) != Relation.CELL_OUTSIDE_QUERY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,10 @@ public class LatLonShape {
|
|||
return new LatLonShapeBoundingBoxQuery(field, minLatitude, maxLatitude, minLongitude, maxLongitude);
|
||||
}
|
||||
|
||||
public static Query newPolygonQuery(String field, Polygon... polygons) {
|
||||
return new LatLonShapePolygonQuery(field, polygons);
|
||||
}
|
||||
|
||||
/** polygons are decomposed into tessellated triangles using {@link org.apache.lucene.geo.Tessellator}
|
||||
* these triangles are encoded and inserted as separate indexed POINT fields
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* 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 java.util.Objects;
|
||||
|
||||
import org.apache.lucene.geo.GeoEncodingUtils;
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.geo.Polygon2D;
|
||||
import org.apache.lucene.geo.Rectangle;
|
||||
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.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.FutureArrays;
|
||||
import org.apache.lucene.util.NumericUtils;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Finds all previously indexed shapes that intersect the specified arbitrary.
|
||||
*
|
||||
* <p>The field must be indexed using
|
||||
* {@link org.apache.lucene.document.LatLonShape#createIndexableFields(String, Polygon)} added per document.
|
||||
*
|
||||
* @lucene.experimental
|
||||
**/
|
||||
public class LatLonShapePolygonQuery extends Query {
|
||||
final String field;
|
||||
final Polygon[] polygons;
|
||||
|
||||
|
||||
public LatLonShapePolygonQuery(String field, Polygon... polygons) {
|
||||
if (field == null) {
|
||||
throw new IllegalArgumentException("field must not be null");
|
||||
}
|
||||
if (polygons == null) {
|
||||
throw new IllegalArgumentException("polygons must not be null");
|
||||
}
|
||||
if (polygons.length == 0) {
|
||||
throw new IllegalArgumentException("polygons must not be empty");
|
||||
}
|
||||
for (int i = 0; i < polygons.length; i++) {
|
||||
if (polygons[i] == null) {
|
||||
throw new IllegalArgumentException("polygon[" + i + "] must not be null");
|
||||
}
|
||||
}
|
||||
this.field = field;
|
||||
this.polygons = polygons.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
|
||||
final Rectangle box = Rectangle.fromPolygon(polygons);
|
||||
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(encodeLatitudeCeil(box.minLat), minLat, 0);
|
||||
NumericUtils.intToSortableBytes(encodeLatitude(box.maxLat), maxLat, 0);
|
||||
NumericUtils.intToSortableBytes(encodeLongitudeCeil(box.minLon), minLon, 0);
|
||||
NumericUtils.intToSortableBytes(encodeLongitude(box.maxLon), maxLon, 0);
|
||||
|
||||
final Polygon2D polygon = Polygon2D.create(polygons);
|
||||
|
||||
return new ConstantScoreWeight(this, boost) {
|
||||
|
||||
private Relation relateRangeToQuery(byte[] minTriangle, byte[] maxTriangle) {
|
||||
// compute bounding box
|
||||
int minXOfs = 0;
|
||||
int minYOfs = 0;
|
||||
int maxXOfs = 0;
|
||||
int maxYOfs = 0;
|
||||
for (int d = 1; d < 3; ++d) {
|
||||
// check minX
|
||||
int aOfs = (minXOfs * 2 * LatLonPoint.BYTES) + LatLonPoint.BYTES;
|
||||
int bOfs = (d * 2 * LatLonPoint.BYTES) + LatLonPoint.BYTES;
|
||||
if (FutureArrays.compareUnsigned(minTriangle, bOfs, bOfs + LatLonPoint.BYTES, minTriangle, aOfs, aOfs + LatLonPoint.BYTES) < 0) {
|
||||
minXOfs = d;
|
||||
}
|
||||
// check maxX
|
||||
aOfs = (maxXOfs * 2 * LatLonPoint.BYTES) + LatLonPoint.BYTES;
|
||||
if (FutureArrays.compareUnsigned(maxTriangle, bOfs, bOfs + LatLonPoint.BYTES, maxTriangle, aOfs, aOfs + LatLonPoint.BYTES) > 0) {
|
||||
maxXOfs = d;
|
||||
}
|
||||
// check minY
|
||||
aOfs = minYOfs * 2 * LatLonPoint.BYTES;
|
||||
bOfs = d * 2 * LatLonPoint.BYTES;
|
||||
if (FutureArrays.compareUnsigned(minTriangle, bOfs, bOfs + LatLonPoint.BYTES, minTriangle, aOfs, aOfs + LatLonPoint.BYTES) < 0) {
|
||||
minYOfs = d;
|
||||
}
|
||||
// check maxY
|
||||
aOfs = maxYOfs * 2 * LatLonPoint.BYTES;
|
||||
if (FutureArrays.compareUnsigned(maxTriangle, bOfs, bOfs + LatLonPoint.BYTES, maxTriangle, aOfs, aOfs + LatLonPoint.BYTES) > 0) {
|
||||
maxYOfs = d;
|
||||
}
|
||||
}
|
||||
minXOfs = (minXOfs * 2 * LatLonPoint.BYTES) + LatLonPoint.BYTES;
|
||||
maxXOfs = (maxXOfs * 2 * LatLonPoint.BYTES) + LatLonPoint.BYTES;
|
||||
minYOfs *= 2 * LatLonPoint.BYTES;
|
||||
maxYOfs *= 2 * LatLonPoint.BYTES;
|
||||
|
||||
double minLat = GeoEncodingUtils.decodeLatitude(minTriangle, minYOfs);
|
||||
double minLon = GeoEncodingUtils.decodeLongitude(minTriangle, minXOfs);
|
||||
double maxLat = GeoEncodingUtils.decodeLatitude(maxTriangle, maxYOfs);
|
||||
double maxLon = GeoEncodingUtils.decodeLongitude(maxTriangle, maxXOfs);
|
||||
|
||||
// check internal node against query
|
||||
return polygon.relate(minLat, maxLat, minLon, maxLon);
|
||||
}
|
||||
|
||||
private boolean queryCrossesTriangle(byte[] t) {
|
||||
double ay = GeoEncodingUtils.decodeLatitude(t, 0);
|
||||
double ax = GeoEncodingUtils.decodeLongitude(t, LatLonPoint.BYTES);
|
||||
double by = GeoEncodingUtils.decodeLatitude(t, 2 * LatLonPoint.BYTES);
|
||||
double bx = GeoEncodingUtils.decodeLongitude(t, 3 * LatLonPoint.BYTES);
|
||||
double cy = GeoEncodingUtils.decodeLatitude(t, 4 * LatLonPoint.BYTES);
|
||||
double cx = GeoEncodingUtils.decodeLongitude(t, 5 * LatLonPoint.BYTES);
|
||||
return polygon.relateTriangle(ax, ay, bx, by, cx, cy) != Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
|
||||
private IntersectVisitor getIntersectVisitor(DocIdSetBuilder result) {
|
||||
return new IntersectVisitor() {
|
||||
|
||||
DocIdSetBuilder.BulkAdder adder;
|
||||
|
||||
@Override
|
||||
public void grow(int count) {
|
||||
adder = result.grow(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(int docID) throws IOException {
|
||||
adder.add(docID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(int docID, byte[] t) throws IOException {
|
||||
if (queryCrossesTriangle(t)) {
|
||||
adder.add(docID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
|
||||
return relateRangeToQuery(minTriangle, maxTriangle);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
final Weight weight = this;
|
||||
return new ScorerSupplier() {
|
||||
final DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field);
|
||||
final PointValues.IntersectVisitor visitor = getIntersectVisitor(result);
|
||||
long cost = -1;
|
||||
|
||||
@Override
|
||||
public Scorer get(long leadCost) throws IOException {
|
||||
values.intersect(visitor);
|
||||
DocIdSetIterator iterator = result.build().iterator();
|
||||
return new ConstantScoreScorer(weight, score(), iterator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long cost() {
|
||||
if (cost == -1) {
|
||||
// Computing the cost may be expensive, so only do it if necessary
|
||||
cost = values.estimatePointCount(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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public String getField() {
|
||||
return field;
|
||||
}
|
||||
|
||||
@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("Polygon(" + polygons[0].toGeoJSON() + ")");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return sameClassAs(o) && equalsTo(getClass().cast(o));
|
||||
}
|
||||
|
||||
private boolean equalsTo(LatLonShapePolygonQuery o) {
|
||||
return Objects.equals(field, o.field) && Arrays.equals(polygons, o.polygons);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = classHash();
|
||||
hash = 31 * hash + field.hashCode();
|
||||
hash = 31 * hash + Arrays.hashCode(polygons);
|
||||
return hash;
|
||||
}
|
||||
}
|
|
@ -17,13 +17,16 @@
|
|||
package org.apache.lucene.document;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.lucene.geo.GeoTestUtil;
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.geo.Polygon2D;
|
||||
import org.apache.lucene.geo.Rectangle;
|
||||
import org.apache.lucene.geo.Tessellator;
|
||||
import org.apache.lucene.index.DirectoryReader;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
|
@ -53,7 +56,7 @@ import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
|
|||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
|
||||
|
||||
/** base Test case for {@link LatLonShape} indexing and search */
|
||||
public class TestLatLonShapeQueries extends LuceneTestCase {
|
||||
public class TestLatLonPolygonShapeQueries extends LuceneTestCase {
|
||||
protected static final String FIELD_NAME = "shape";
|
||||
|
||||
private Polygon quantizePolygon(Polygon polygon) {
|
||||
|
@ -93,6 +96,10 @@ public class TestLatLonShapeQueries extends LuceneTestCase {
|
|||
return LatLonShape.newBoxQuery(field, minLat, maxLat, minLon, maxLon);
|
||||
}
|
||||
|
||||
protected Query newPolygonQuery(String field, Polygon... polygons) {
|
||||
return LatLonShape.newPolygonQuery(field, polygons);
|
||||
}
|
||||
|
||||
public void testRandomTiny() throws Exception {
|
||||
// Make sure single-leaf-node case is OK:
|
||||
doTestRandom(10);
|
||||
|
@ -131,10 +138,20 @@ public class TestLatLonShapeQueries extends LuceneTestCase {
|
|||
}
|
||||
|
||||
private void verify(Polygon... polygons) throws Exception {
|
||||
verifyRandomBBoxes(polygons);
|
||||
ArrayList<Polygon2D> poly2d = new ArrayList<>();
|
||||
poly2d.ensureCapacity(polygons.length);
|
||||
// index random polygons; poly2d will contain the Polygon2D objects needed for verification
|
||||
IndexWriter w = indexRandomPolygons(poly2d, polygons);
|
||||
Directory dir = w.getDirectory();
|
||||
final IndexReader reader = DirectoryReader.open(w);
|
||||
// test random bbox queries
|
||||
verifyRandomBBoxQueries(reader, poly2d, polygons);
|
||||
// test random polygon queires
|
||||
verifyRandomPolygonQueries(reader, poly2d, polygons);
|
||||
IOUtils.close(w, reader, dir);
|
||||
}
|
||||
|
||||
protected void verifyRandomBBoxes(Polygon... polygons) throws Exception {
|
||||
protected IndexWriter indexRandomPolygons(List<Polygon2D> poly2d, Polygon... polygons) throws Exception {
|
||||
IndexWriterConfig iwc = newIndexWriterConfig();
|
||||
iwc.setMergeScheduler(new SerialMergeScheduler());
|
||||
int mbd = iwc.getMaxBufferedDocs();
|
||||
|
@ -150,7 +167,6 @@ public class TestLatLonShapeQueries extends LuceneTestCase {
|
|||
|
||||
Set<Integer> deleted = new HashSet<>();
|
||||
IndexWriter w = new IndexWriter(dir, iwc);
|
||||
Polygon2D[] poly2D = new Polygon2D[polygons.length];
|
||||
for (int id = 0; id < polygons.length; ++id) {
|
||||
Document doc = new Document();
|
||||
doc.add(newStringField("id", "" + id, Field.Store.NO));
|
||||
|
@ -168,9 +184,12 @@ public class TestLatLonShapeQueries extends LuceneTestCase {
|
|||
}
|
||||
// remove and skip the malformed shape
|
||||
polygons[id] = null;
|
||||
poly2d.add(id, null);
|
||||
continue;
|
||||
}
|
||||
poly2D[id] = Polygon2D.create(quantizePolygon(polygons[id]));
|
||||
poly2d.add(id, Polygon2D.create(quantizePolygon(polygons[id])));
|
||||
} else {
|
||||
poly2d.add(id, null);
|
||||
}
|
||||
w.addDocument(doc);
|
||||
if (id > 0 && random().nextInt(100) == 42) {
|
||||
|
@ -186,10 +205,12 @@ public class TestLatLonShapeQueries extends LuceneTestCase {
|
|||
if (random().nextBoolean()) {
|
||||
w.forceMerge(1);
|
||||
}
|
||||
final IndexReader r = DirectoryReader.open(w);
|
||||
w.close();
|
||||
|
||||
IndexSearcher s = newSearcher(r);
|
||||
return w;
|
||||
}
|
||||
|
||||
protected void verifyRandomBBoxQueries(IndexReader reader, List<Polygon2D> poly2d, Polygon... polygons) throws Exception {
|
||||
IndexSearcher s = newSearcher(reader);
|
||||
|
||||
final int iters = atLeast(75);
|
||||
|
||||
|
@ -231,7 +252,7 @@ public class TestLatLonShapeQueries extends LuceneTestCase {
|
|||
});
|
||||
|
||||
boolean fail = false;
|
||||
NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id");
|
||||
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
|
||||
for (int docID = 0; docID < maxDoc; ++docID) {
|
||||
assertEquals(docID, docIDToID.nextDoc());
|
||||
int id = (int) docIDToID.longValue();
|
||||
|
@ -243,7 +264,7 @@ public class TestLatLonShapeQueries extends LuceneTestCase {
|
|||
expected = false;
|
||||
} else {
|
||||
// check quantized poly against quantized query
|
||||
expected = poly2D[id].relate(quantizeLatCeil(rect.minLat), quantizeLat(rect.maxLat),
|
||||
expected = poly2d.get(id).relate(quantizeLatCeil(rect.minLat), quantizeLat(rect.maxLat),
|
||||
quantizeLonCeil(rect.minLon), quantizeLon(rect.maxLon)) != Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
|
||||
|
@ -271,6 +292,102 @@ public class TestLatLonShapeQueries extends LuceneTestCase {
|
|||
fail("some hits were wrong");
|
||||
}
|
||||
}
|
||||
IOUtils.close(r, dir);
|
||||
}
|
||||
|
||||
protected void verifyRandomPolygonQueries(IndexReader reader, List<Polygon2D> poly2d, Polygon... polygons) throws Exception {
|
||||
IndexSearcher s = newSearcher(reader);
|
||||
|
||||
final int iters = atLeast(75);
|
||||
|
||||
Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader());
|
||||
int maxDoc = s.getIndexReader().maxDoc();
|
||||
|
||||
for (int iter = 0; iter < iters; ++iter) {
|
||||
if (VERBOSE) {
|
||||
System.out.println("\nTEST: iter=" + (iter+1) + " of " + iters + " s=" + s);
|
||||
}
|
||||
|
||||
// Polygon
|
||||
Polygon queryPolygon = GeoTestUtil.nextPolygon();
|
||||
Polygon2D queryPoly2D = Polygon2D.create(queryPolygon);
|
||||
Query query = newPolygonQuery(FIELD_NAME, queryPolygon);
|
||||
|
||||
if (VERBOSE) {
|
||||
System.out.println(" query=" + query);
|
||||
}
|
||||
|
||||
final FixedBitSet hits = new FixedBitSet(maxDoc);
|
||||
s.search(query, new SimpleCollector() {
|
||||
|
||||
private int docBase;
|
||||
|
||||
@Override
|
||||
public ScoreMode scoreMode() {
|
||||
return ScoreMode.COMPLETE_NO_SCORES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSetNextReader(LeafReaderContext context) throws IOException {
|
||||
docBase = context.docBase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collect(int doc) throws IOException {
|
||||
hits.set(docBase+doc);
|
||||
}
|
||||
});
|
||||
|
||||
boolean fail = false;
|
||||
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
|
||||
for (int docID = 0; docID < maxDoc; ++docID) {
|
||||
assertEquals(docID, docIDToID.nextDoc());
|
||||
int id = (int) docIDToID.longValue();
|
||||
boolean expected;
|
||||
if (liveDocs != null && liveDocs.get(docID) == false) {
|
||||
// document is deleted
|
||||
expected = false;
|
||||
} else if (polygons[id] == null) {
|
||||
expected = false;
|
||||
} else {
|
||||
expected = false;
|
||||
try {
|
||||
// check poly (quantized the same way as indexed) against query polygon
|
||||
List<Tessellator.Triangle> tesselation = Tessellator.tessellate(quantizePolygon(polygons[id]));
|
||||
for (Tessellator.Triangle t : tesselation) {
|
||||
if (queryPoly2D.relateTriangle(t.getLon(0), t.getLat(0),
|
||||
t.getLon(1), t.getLat(1), t.getLon(2), t.getLat(2)) != Relation.CELL_OUTSIDE_QUERY) {
|
||||
expected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (hits.get(docID) != expected) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
|
||||
if (expected) {
|
||||
b.append("FAIL: id=" + id + " should match but did not\n");
|
||||
} else {
|
||||
b.append("FAIL: id=" + id + " should not match but did\n");
|
||||
}
|
||||
b.append(" query=" + query + " docID=" + docID + "\n");
|
||||
b.append(" polygon=" + quantizePolygon(polygons[id]).toGeoJSON() + "\n");
|
||||
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
|
||||
b.append(" queryPolygon=" + queryPolygon.toGeoJSON());
|
||||
if (true) {
|
||||
fail("wrong hit (first of possibly more):\n\n" + b);
|
||||
} else {
|
||||
System.out.println(b.toString());
|
||||
fail = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fail) {
|
||||
fail("some hits were wrong");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue