LUCENE-8447: Add DISJOINT and WITHIN support to LatLonShape queries

This commit is contained in:
Nicholas Knize 2018-08-06 16:31:21 -05:00
parent 9b418a4593
commit cbaedb470c
10 changed files with 671 additions and 628 deletions

View File

@ -225,6 +225,8 @@ Changes in Runtime Behavior:
Improvements Improvements
* LUCENE-8447: Add DISJOINT and WITHIN support to LatLonShape queries. (Nick Knize)
* LUCENE-8440: Add support for indexing and searching Line and Point shapes using LatLonShape encoding (Nick Knize) * LUCENE-8440: Add support for indexing and searching Line and Point shapes using LatLonShape encoding (Nick Knize)
* LUCENE-8435: Add new LatLonShapePolygonQuery for querying indexed LatLonShape fields by arbitrary polygons (Nick Knize) * LUCENE-8435: Add new LatLonShapePolygonQuery for querying indexed LatLonShape fields by arbitrary polygons (Nick Knize)

View File

@ -131,12 +131,12 @@ public class LatLonShape {
* note: does not currently support dateline crossing boxes * note: does not currently support dateline crossing boxes
* todo split dateline crossing boxes into two queries like {@link LatLonPoint#newBoxQuery} * todo split dateline crossing boxes into two queries like {@link LatLonPoint#newBoxQuery}
**/ **/
public static Query newBoxQuery(String field, double minLatitude, double maxLatitude, double minLongitude, double maxLongitude) { public static Query newBoxQuery(String field, QueryRelation queryRelation, double minLatitude, double maxLatitude, double minLongitude, double maxLongitude) {
return new LatLonShapeBoundingBoxQuery(field, minLatitude, maxLatitude, minLongitude, maxLongitude); return new LatLonShapeBoundingBoxQuery(field, queryRelation, minLatitude, maxLatitude, minLongitude, maxLongitude);
} }
public static Query newPolygonQuery(String field, Polygon... polygons) { public static Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) {
return new LatLonShapePolygonQuery(field, polygons); return new LatLonShapePolygonQuery(field, queryRelation, polygons);
} }
/** polygons are decomposed into tessellated triangles using {@link org.apache.lucene.geo.Tessellator} /** polygons are decomposed into tessellated triangles using {@link org.apache.lucene.geo.Tessellator}
@ -167,4 +167,9 @@ public class LatLonShape {
NumericUtils.intToSortableBytes(cX, bytes, BYTES * 5); NumericUtils.intToSortableBytes(cX, bytes, BYTES * 5);
} }
} }
/** Query Relation Types **/
public enum QueryRelation {
INTERSECTS, WITHIN, DISJOINT
}
} }

View File

@ -16,29 +16,11 @@
*/ */
package org.apache.lucene.document; package org.apache.lucene.document;
import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Tessellator; import org.apache.lucene.geo.Tessellator;
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.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.BitSetIterator;
import org.apache.lucene.util.DocIdSetBuilder;
import org.apache.lucene.util.FixedBitSet;
import org.apache.lucene.util.FutureArrays; import org.apache.lucene.util.FutureArrays;
import org.apache.lucene.util.NumericUtils; import org.apache.lucene.util.NumericUtils;
@ -58,19 +40,18 @@ import static org.apache.lucene.geo.GeoUtils.orient;
* *
* @lucene.experimental * @lucene.experimental
**/ **/
class LatLonShapeBoundingBoxQuery extends Query { final class LatLonShapeBoundingBoxQuery extends LatLonShapeQuery {
final String field;
final byte[] bbox; final byte[] bbox;
final int minX; final int minX;
final int maxX; final int maxX;
final int minY; final int minY;
final int maxY; final int maxY;
public LatLonShapeBoundingBoxQuery(String field, double minLat, double maxLat, double minLon, double maxLon) { public LatLonShapeBoundingBoxQuery(String field, LatLonShape.QueryRelation queryRelation, double minLat, double maxLat, double minLon, double maxLon) {
super(field, queryRelation);
if (minLon > maxLon) { if (minLon > maxLon) {
throw new IllegalArgumentException("dateline crossing bounding box queries are not supported for [" + field + "]"); throw new IllegalArgumentException("dateline crossing bounding box queries are not supported for [" + field + "]");
} }
this.field = field;
this.bbox = new byte[4 * LatLonPoint.BYTES]; this.bbox = new byte[4 * LatLonPoint.BYTES];
this.minX = encodeLongitudeCeil(minLon); this.minX = encodeLongitudeCeil(minLon);
this.maxX = encodeLongitude(maxLon); this.maxX = encodeLongitude(maxLon);
@ -83,301 +64,129 @@ class LatLonShapeBoundingBoxQuery extends Query {
} }
@Override @Override
public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException { protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle,
return new ConstantScoreWeight(this, boost) { int maxXOffset, int maxYOffset, byte[] maxTriangle) {
// check bounding box (DISJOINT)
if (FutureArrays.compareUnsigned(minTriangle, minXOffset, minXOffset + LatLonPoint.BYTES, bbox, 3 * LatLonPoint.BYTES, 4 * LatLonPoint.BYTES) > 0 ||
FutureArrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + LatLonPoint.BYTES, bbox, LatLonPoint.BYTES, 2 * LatLonPoint.BYTES) < 0 ||
FutureArrays.compareUnsigned(minTriangle, minYOffset, minYOffset + LatLonPoint.BYTES, bbox, 2 * LatLonPoint.BYTES, 3 * LatLonPoint.BYTES) > 0 ||
FutureArrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + LatLonPoint.BYTES, bbox, 0, LatLonPoint.BYTES) < 0) {
return Relation.CELL_OUTSIDE_QUERY;
}
private boolean edgeIntersectsQuery(double ax, double ay, double bx, double by) { if (FutureArrays.compareUnsigned(minTriangle, minXOffset, minXOffset + LatLonPoint.BYTES, bbox, LatLonPoint.BYTES, 2 * LatLonPoint.BYTES) > 0 &&
// top FutureArrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + LatLonPoint.BYTES, bbox, 3 * LatLonPoint.BYTES, 4 * LatLonPoint.BYTES) < 0 &&
if (orient(ax, ay, bx, by, minX, maxY) * orient(ax, ay, bx, by, maxX, maxY) <= 0 && FutureArrays.compareUnsigned(minTriangle, minYOffset, minYOffset + LatLonPoint.BYTES, bbox, 0, LatLonPoint.BYTES) > 0 &&
orient(minX, maxY, maxX, maxY, ax, ay) * orient(minX, maxY, maxX, maxY, bx, by) <= 0) { FutureArrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + LatLonPoint.BYTES, bbox, 2 * LatLonPoint.BYTES, 2 * LatLonPoint.BYTES) < 0) {
return true; return Relation.CELL_INSIDE_QUERY;
} }
return Relation.CELL_CROSSES_QUERY;
// right
if (orient(ax, ay, bx, by, maxX, maxY) * orient(ax, ay, bx, by, maxX, minY) <= 0 &&
orient(maxX, maxY, maxX, minY, ax, ay) * orient(maxX, maxY, maxX, minY, bx, by) <= 0) {
return true;
}
// bottom
if (orient(ax, ay, bx, by, maxX, minY) * orient(ax, ay, bx, by, minX, minY) <= 0 &&
orient(maxX, minY, minX, minY, ax, ay) * orient(maxX, minY, minX, minY, bx, by) <= 0) {
return true;
}
// left
if (orient(ax, ay, bx, by, minX, minY) * orient(ax, ay, bx, by, minX, maxY) <= 0 &&
orient(minX, minY, minX, maxY, ax, ay) * orient(minX, minY, minX, maxY, bx, by) <= 0) {
return true;
}
return false;
}
private boolean queryContains(byte[] t, int point) {
final int yIdx = 2 * LatLonPoint.BYTES * point;
final int xIdx = yIdx + LatLonPoint.BYTES;
if (FutureArrays.compareUnsigned(t, yIdx, xIdx, bbox, 0, LatLonPoint.BYTES) < 0 || //minY
FutureArrays.compareUnsigned(t, yIdx, xIdx, bbox, 2 * LatLonPoint.BYTES, 3 * LatLonPoint.BYTES) > 0 || //maxY
FutureArrays.compareUnsigned(t, xIdx, xIdx + LatLonPoint.BYTES, bbox, LatLonPoint.BYTES, 2 * LatLonPoint.BYTES) < 0 || // minX
FutureArrays.compareUnsigned(t, xIdx, xIdx + LatLonPoint.BYTES, bbox, 3 * LatLonPoint.BYTES, bbox.length) > 0) {
return false;
}
return true;
}
private boolean queryIntersects(int ax, int ay, int bx, int by, int cx, int cy) {
// check each edge of the triangle against the query
if (edgeIntersectsQuery(ax, ay, bx, by) ||
edgeIntersectsQuery(bx, by, cx, cy) ||
edgeIntersectsQuery(cx, cy, ax, ay)) {
return true;
}
return false;
}
private boolean queryCrossesTriangle(byte[] t) {
// 1. query contains any triangle points
if (queryContains(t, 0) || queryContains(t, 1) || queryContains(t, 2)) {
return true;
}
int aY = NumericUtils.sortableBytesToInt(t, 0);
int aX = NumericUtils.sortableBytesToInt(t, LatLonPoint.BYTES);
int bY = NumericUtils.sortableBytesToInt(t, 2 * LatLonPoint.BYTES);
int bX = NumericUtils.sortableBytesToInt(t, 3 * LatLonPoint.BYTES);
int cY = NumericUtils.sortableBytesToInt(t, 4 * LatLonPoint.BYTES);
int cX = NumericUtils.sortableBytesToInt(t, 5 * LatLonPoint.BYTES);
int tMinX = StrictMath.min(StrictMath.min(aX, bX), cX);
int tMaxX = StrictMath.max(StrictMath.max(aX, bX), cX);
int tMinY = StrictMath.min(StrictMath.min(aY, bY), cY);
int tMaxY = StrictMath.max(StrictMath.max(aY, bY), cY);
// 2. check bounding boxes are disjoint
if (tMaxX < minX || tMinX > maxX || tMinY > maxY || tMaxY < minY) {
return false;
}
// 3. check triangle contains any query points
if (Tessellator.pointInTriangle(minX, minY, aX, aY, bX, bY, cX, cY)) {
return true;
} else if (Tessellator.pointInTriangle(maxX, minY, aX, aY, bX, bY, cX, cY)) {
return true;
} else if (Tessellator.pointInTriangle(maxX, maxY, aX, aY, bX, bY, cX, cY)) {
return true;
} else if (Tessellator.pointInTriangle(minX, maxY, aX, aY, bX, bY, cX, cY)) {
return true;
}
// 4. last ditch effort: check crossings
if (queryIntersects(aX, aY, bX, bY, cX, cY)) {
return true;
}
return false;
}
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;
// check bounding box (DISJOINT)
if (FutureArrays.compareUnsigned(minTriangle, minXOfs, minXOfs + LatLonPoint.BYTES, bbox, 3 * LatLonPoint.BYTES, 4 * LatLonPoint.BYTES) > 0 ||
FutureArrays.compareUnsigned(maxTriangle, maxXOfs, maxXOfs + LatLonPoint.BYTES, bbox, LatLonPoint.BYTES, 2 * LatLonPoint.BYTES) < 0 ||
FutureArrays.compareUnsigned(minTriangle, minYOfs, minYOfs + LatLonPoint.BYTES, bbox, 2 * LatLonPoint.BYTES, 3 * LatLonPoint.BYTES) > 0 ||
FutureArrays.compareUnsigned(maxTriangle, maxYOfs, maxYOfs + LatLonPoint.BYTES, bbox, 0, LatLonPoint.BYTES) < 0) {
return Relation.CELL_OUTSIDE_QUERY;
}
if (FutureArrays.compareUnsigned(minTriangle, minXOfs, minXOfs + LatLonPoint.BYTES, bbox, LatLonPoint.BYTES, 2 * LatLonPoint.BYTES) > 0 &&
FutureArrays.compareUnsigned(maxTriangle, maxXOfs, maxXOfs + LatLonPoint.BYTES, bbox, 3 * LatLonPoint.BYTES, 4 * LatLonPoint.BYTES) < 0 &&
FutureArrays.compareUnsigned(minTriangle, minYOfs, minYOfs + LatLonPoint.BYTES, bbox, 0, LatLonPoint.BYTES) > 0 &&
FutureArrays.compareUnsigned(maxTriangle, maxYOfs, maxYOfs + LatLonPoint.BYTES, bbox, 2 * LatLonPoint.BYTES, 2 * LatLonPoint.BYTES) < 0) {
return Relation.CELL_INSIDE_QUERY;
}
return Relation.CELL_CROSSES_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);
}
};
}
/**
* Create a visitor that clears documents that do NOT match the bounding box query.
*/
private IntersectVisitor getInverseIntersectVisitor(FixedBitSet result, int[] cost) {
return new IntersectVisitor() {
@Override
public void visit(int docID) {
result.clear(docID);
cost[0]--;
}
@Override
public void visit(int docID, byte[] packedTriangle) {
if (queryCrossesTriangle(packedTriangle) == false) {
result.clear(docID);
cost[0]--;
}
}
@Override
public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
Relation r = relateRangeToQuery(minPackedValue, maxPackedValue);
if (r == Relation.CELL_OUTSIDE_QUERY) {
return Relation.CELL_INSIDE_QUERY;
} else if (r == Relation.CELL_INSIDE_QUERY) {
return Relation.CELL_OUTSIDE_QUERY;
}
return r;
}
};
}
@Override
public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
LeafReader reader = context.reader();
PointValues values = reader.getPointValues(field);
if (values == null) {
return null;
}
boolean allDocsMatch = true;
if (values.getDocCount() != reader.maxDoc() ||
relateRangeToQuery(values.getMinPackedValue(), values.getMaxPackedValue()) != Relation.CELL_INSIDE_QUERY) {
allDocsMatch = false;
}
final Weight weight = this;
if (allDocsMatch) {
return new ScorerSupplier() {
@Override
public Scorer get(long leadCost) throws IOException {
return new ConstantScoreScorer(weight, score(),
DocIdSetIterator.all(reader.maxDoc()));
}
@Override
public long cost() {
return reader.maxDoc();
}
};
} else {
return new ScorerSupplier() {
final DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field);
final IntersectVisitor visitor = getIntersectVisitor(result);
long cost = -1;
@Override
public Scorer get(long leadCost) throws IOException {
if (values.getDocCount() == reader.maxDoc()
&& values.getDocCount() == values.size()
&& cost() > reader.maxDoc() / 2) {
// If all docs have exactly one value and the cost is greater
// than half the leaf size then maybe we can make things faster
// by computing the set of documents that do NOT match the query
final FixedBitSet result = new FixedBitSet(reader.maxDoc());
result.set(0, reader.maxDoc());
int[] cost = new int[]{reader.maxDoc()};
values.intersect(getInverseIntersectVisitor(result, cost));
final DocIdSetIterator iterator = new BitSetIterator(result, cost[0]);
return new ConstantScoreScorer(weight, score(), iterator);
}
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() { /** returns true if the query matches the encoded triangle */
return field; @Override
protected boolean queryMatches(byte[] t) {
if (queryRelation == LatLonShape.QueryRelation.WITHIN) {
return queryContains(t, 0) && queryContains(t, 1) && queryContains(t, 2);
}
return queryIntersects(t);
}
/** returns true if the query intersects the encoded triangle */
protected boolean queryIntersects(byte[] t) {
// 1. query contains any triangle points
if (queryContains(t, 0) || queryContains(t, 1) || queryContains(t, 2)) {
return true;
}
int aY = NumericUtils.sortableBytesToInt(t, 0);
int aX = NumericUtils.sortableBytesToInt(t, LatLonPoint.BYTES);
int bY = NumericUtils.sortableBytesToInt(t, 2 * LatLonPoint.BYTES);
int bX = NumericUtils.sortableBytesToInt(t, 3 * LatLonPoint.BYTES);
int cY = NumericUtils.sortableBytesToInt(t, 4 * LatLonPoint.BYTES);
int cX = NumericUtils.sortableBytesToInt(t, 5 * LatLonPoint.BYTES);
int tMinX = StrictMath.min(StrictMath.min(aX, bX), cX);
int tMaxX = StrictMath.max(StrictMath.max(aX, bX), cX);
int tMinY = StrictMath.min(StrictMath.min(aY, bY), cY);
int tMaxY = StrictMath.max(StrictMath.max(aY, bY), cY);
// 2. check bounding boxes are disjoint
if (tMaxX < minX || tMinX > maxX || tMinY > maxY || tMaxY < minY) {
return false;
}
// 3. check triangle contains any query points
if (Tessellator.pointInTriangle(minX, minY, aX, aY, bX, bY, cX, cY)) {
return true;
} else if (Tessellator.pointInTriangle(maxX, minY, aX, aY, bX, bY, cX, cY)) {
return true;
} else if (Tessellator.pointInTriangle(maxX, maxY, aX, aY, bX, bY, cX, cY)) {
return true;
} else if (Tessellator.pointInTriangle(minX, maxY, aX, aY, bX, bY, cX, cY)) {
return true;
}
// 4. last ditch effort: check crossings
if (queryIntersects(aX, aY, bX, bY, cX, cY)) {
return true;
}
return false;
}
/** returns true if the edge (defined by (ax, ay) (bx, by)) intersects the query */
private boolean edgeIntersectsQuery(double ax, double ay, double bx, double by) {
// top
if (orient(ax, ay, bx, by, minX, maxY) * orient(ax, ay, bx, by, maxX, maxY) <= 0 &&
orient(minX, maxY, maxX, maxY, ax, ay) * orient(minX, maxY, maxX, maxY, bx, by) <= 0) {
return true;
}
// right
if (orient(ax, ay, bx, by, maxX, maxY) * orient(ax, ay, bx, by, maxX, minY) <= 0 &&
orient(maxX, maxY, maxX, minY, ax, ay) * orient(maxX, maxY, maxX, minY, bx, by) <= 0) {
return true;
}
// bottom
if (orient(ax, ay, bx, by, maxX, minY) * orient(ax, ay, bx, by, minX, minY) <= 0 &&
orient(maxX, minY, minX, minY, ax, ay) * orient(maxX, minY, minX, minY, bx, by) <= 0) {
return true;
}
// left
if (orient(ax, ay, bx, by, minX, minY) * orient(ax, ay, bx, by, minX, maxY) <= 0 &&
orient(minX, minY, minX, maxY, ax, ay) * orient(minX, minY, minX, maxY, bx, by) <= 0) {
return true;
}
return false;
}
/** returns true if the query contains the triangle vertex */
private boolean queryContains(byte[] t, int point) {
final int yIdx = 2 * LatLonPoint.BYTES * point;
final int xIdx = yIdx + LatLonPoint.BYTES;
if (FutureArrays.compareUnsigned(t, yIdx, xIdx, bbox, 0, LatLonPoint.BYTES) < 0 || //minY
FutureArrays.compareUnsigned(t, yIdx, xIdx, bbox, 2 * LatLonPoint.BYTES, 3 * LatLonPoint.BYTES) > 0 || //maxY
FutureArrays.compareUnsigned(t, xIdx, xIdx + LatLonPoint.BYTES, bbox, LatLonPoint.BYTES, 2 * LatLonPoint.BYTES) < 0 || // minX
FutureArrays.compareUnsigned(t, xIdx, xIdx + LatLonPoint.BYTES, bbox, 3 * LatLonPoint.BYTES, bbox.length) > 0) {
return false;
}
return true;
}
/** returns true if the query intersects the provided triangle (in encoded space) */
private boolean queryIntersects(int ax, int ay, int bx, int by, int cx, int cy) {
// check each edge of the triangle against the query
if (edgeIntersectsQuery(ax, ay, bx, by) ||
edgeIntersectsQuery(bx, by, cx, cy) ||
edgeIntersectsQuery(cx, cy, ax, ay)) {
return true;
}
return false;
} }
@Override @Override
@ -385,15 +194,14 @@ class LatLonShapeBoundingBoxQuery extends Query {
return sameClassAs(o) && equalsTo(getClass().cast(o)); return sameClassAs(o) && equalsTo(getClass().cast(o));
} }
private boolean equalsTo(LatLonShapeBoundingBoxQuery o) { @Override
return Objects.equals(field, o.field) && protected boolean equalsTo(Object o) {
Arrays.equals(bbox, o.bbox); return super.equalsTo(o) && Arrays.equals(bbox, ((LatLonShapeBoundingBoxQuery)o).bbox);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int hash = classHash(); int hash = super.hashCode();
hash = 31 * hash + field.hashCode();
hash = 31 * hash + Arrays.hashCode(bbox); hash = 31 * hash + Arrays.hashCode(bbox);
return hash; return hash;
} }

View File

@ -16,39 +16,13 @@
*/ */
package org.apache.lucene.document; package org.apache.lucene.document;
import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
import org.apache.lucene.document.LatLonShape.QueryRelation;
import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Polygon2D; 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.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.BitSetIterator;
import org.apache.lucene.util.DocIdSetBuilder;
import org.apache.lucene.util.FixedBitSet;
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. * Finds all previously indexed shapes that intersect the specified arbitrary.
@ -58,15 +32,15 @@ import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
* *
* @lucene.experimental * @lucene.experimental
**/ **/
public class LatLonShapePolygonQuery extends Query { final class LatLonShapePolygonQuery extends LatLonShapeQuery {
final String field;
final Polygon[] polygons; final Polygon[] polygons;
final private Polygon2D poly2D;
/**
public LatLonShapePolygonQuery(String field, Polygon... polygons) { * Creates a query that matches all indexed shapes to the provided polygons
if (field == null) { */
throw new IllegalArgumentException("field must not be null"); public LatLonShapePolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) {
} super(field, queryRelation);
if (polygons == null) { if (polygons == null) {
throw new IllegalArgumentException("polygons must not be null"); throw new IllegalArgumentException("polygons must not be null");
} }
@ -76,236 +50,40 @@ public class LatLonShapePolygonQuery extends Query {
for (int i = 0; i < polygons.length; i++) { for (int i = 0; i < polygons.length; i++) {
if (polygons[i] == null) { if (polygons[i] == null) {
throw new IllegalArgumentException("polygon[" + i + "] must not be null"); throw new IllegalArgumentException("polygon[" + i + "] must not be null");
} else if (polygons[i].minLon > polygons[i].maxLon) {
throw new IllegalArgumentException("LatLonShapePolygonQuery does not currently support querying across dateline.");
} }
} }
this.field = field;
this.polygons = polygons.clone(); this.polygons = polygons.clone();
this.poly2D = Polygon2D.create(polygons);
} }
@Override @Override
public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException { protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle,
final Rectangle box = Rectangle.fromPolygon(polygons); int maxXOffset, int maxYOffset, byte[] maxTriangle) {
final byte minLat[] = new byte[Integer.BYTES]; double minLat = GeoEncodingUtils.decodeLatitude(minTriangle, minYOffset);
final byte maxLat[] = new byte[Integer.BYTES]; double minLon = GeoEncodingUtils.decodeLongitude(minTriangle, minXOffset);
final byte minLon[] = new byte[Integer.BYTES]; double maxLat = GeoEncodingUtils.decodeLatitude(maxTriangle, maxYOffset);
final byte maxLon[] = new byte[Integer.BYTES]; double maxLon = GeoEncodingUtils.decodeLongitude(maxTriangle, maxXOffset);
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); // check internal node against query
return poly2D.relate(minLat, maxLat, minLon, maxLon);
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);
}
};
}
/**
* Create a visitor that clears documents that do NOT match the polygon query.
*/
private IntersectVisitor getInverseIntersectVisitor(FixedBitSet result, int[] cost) {
return new IntersectVisitor() {
@Override
public void visit(int docID) {
result.clear(docID);
cost[0]--;
}
@Override
public void visit(int docID, byte[] packedTriangle) {
if (queryCrossesTriangle(packedTriangle) == false) {
result.clear(docID);
cost[0]--;
}
}
@Override
public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
Relation r = relateRangeToQuery(minPackedValue, maxPackedValue);
if (r == Relation.CELL_OUTSIDE_QUERY) {
return Relation.CELL_INSIDE_QUERY;
} else if (r == Relation.CELL_INSIDE_QUERY) {
return Relation.CELL_OUTSIDE_QUERY;
}
return r;
}
};
}
@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;
}
boolean allDocsMatch = true;
if (values.getDocCount() != reader.maxDoc() ||
relateRangeToQuery(values.getMinPackedValue(), values.getMaxPackedValue()) != Relation.CELL_INSIDE_QUERY) {
allDocsMatch = false;
}
final Weight weight = this;
if (allDocsMatch) {
return new ScorerSupplier() {
@Override
public Scorer get(long leadCost) throws IOException {
return new ConstantScoreScorer(weight, score(),
DocIdSetIterator.all(reader.maxDoc()));
}
@Override
public long cost() {
return reader.maxDoc();
}
};
} else {
return new ScorerSupplier() {
final DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field);
final IntersectVisitor visitor = getIntersectVisitor(result);
long cost = -1;
@Override
public Scorer get(long leadCost) throws IOException {
if (values.getDocCount() == reader.maxDoc()
&& values.getDocCount() == values.size()
&& cost() > reader.maxDoc() / 2) {
// If all docs have exactly one value and the cost is greater
// than half the leaf size then maybe we can make things faster
// by computing the set of documents that do NOT match the query
final FixedBitSet result = new FixedBitSet(reader.maxDoc());
result.set(0, reader.maxDoc());
int[] cost = new int[]{reader.maxDoc()};
values.intersect(getInverseIntersectVisitor(result, cost));
final DocIdSetIterator iterator = new BitSetIterator(result, cost[0]);
return new ConstantScoreScorer(weight, score(), iterator);
}
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() { @Override
return field; protected boolean queryMatches(byte[] triangle) {
double ay = GeoEncodingUtils.decodeLatitude(triangle, 0);
double ax = GeoEncodingUtils.decodeLongitude(triangle, LatLonPoint.BYTES);
double by = GeoEncodingUtils.decodeLatitude(triangle, 2 * LatLonPoint.BYTES);
double bx = GeoEncodingUtils.decodeLongitude(triangle, 3 * LatLonPoint.BYTES);
double cy = GeoEncodingUtils.decodeLatitude(triangle, 4 * LatLonPoint.BYTES);
double cx = GeoEncodingUtils.decodeLongitude(triangle, 5 * LatLonPoint.BYTES);
if (queryRelation == QueryRelation.WITHIN) {
return poly2D.relateTriangle(ax, ay, bx, by, cx, cy) == Relation.CELL_INSIDE_QUERY;
}
// INTERSECTS
return poly2D.relateTriangle(ax, ay, bx, by, cx, cy) != Relation.CELL_OUTSIDE_QUERY;
} }
@Override @Override
@ -323,18 +101,13 @@ public class LatLonShapePolygonQuery extends Query {
} }
@Override @Override
public boolean equals(Object o) { protected boolean equalsTo(Object o) {
return sameClassAs(o) && equalsTo(getClass().cast(o)); return super.equalsTo(o) && Arrays.equals(polygons, ((LatLonShapePolygonQuery)o).polygons);
}
private boolean equalsTo(LatLonShapePolygonQuery o) {
return Objects.equals(field, o.field) && Arrays.equals(polygons, o.polygons);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int hash = classHash(); int hash = super.hashCode();
hash = 31 * hash + field.hashCode();
hash = 31 * hash + Arrays.hashCode(polygons); hash = 31 * hash + Arrays.hashCode(polygons);
return hash; return hash;
} }

View File

@ -0,0 +1,394 @@
/*
* 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.Objects;
import org.apache.lucene.document.LatLonShape.QueryRelation;
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.BitSetIterator;
import org.apache.lucene.util.DocIdSetBuilder;
import org.apache.lucene.util.FixedBitSet;
import org.apache.lucene.util.FutureArrays;
/**
* Base LatLonShape Query class providing common query logic for
* {@link LatLonShapeBoundingBoxQuery} and {@link LatLonShapePolygonQuery}
*
* Note: this class implements the majority of the INTERSECTS, WITHIN, DISJOINT relation logic
*
* @lucene.experimental
**/
abstract class LatLonShapeQuery extends Query {
/** field name */
final String field;
/** query relation
* disjoint: {@code CELL_OUTSIDE_QUERY}
* intersects: {@code CELL_CROSSES_QUERY},
* within: {@code CELL_WITHIN_QUERY} */
final LatLonShape.QueryRelation queryRelation;
protected LatLonShapeQuery(String field, final QueryRelation queryType) {
if (field == null) {
throw new IllegalArgumentException("field must not be null");
}
this.field = field;
this.queryRelation = queryType;
}
/**
* 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 LatLonShapePolygonQuery#relateRangeToQuery}
*/
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 abstract boolean queryMatches(byte[] triangle);
/** relates a range of triangles (internal node) to the query */
protected Relation relateRangeToQuery(byte[] minTriangle, byte[] maxTriangle) {
// compute bounding box of internal node
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;
Relation r = relateRangeBBoxToQuery(minXOfs, minYOfs, minTriangle, maxXOfs, maxYOfs, maxTriangle);
if (queryRelation == QueryRelation.DISJOINT) {
return transposeRelation(r);
}
return r;
}
@Override
public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
return new ConstantScoreWeight(this, boost) {
/** create a visitor that adds documents that match the query using a sparse bitset. (Used by INTERSECT) */
protected IntersectVisitor getSparseIntersectVisitor(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 (queryMatches(t)) {
adder.add(docID);
}
}
@Override
public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
return relateRangeToQuery(minTriangle, maxTriangle);
}
};
}
/** create a visitor that adds documents that match the query using a dense bitset. (Used by WITHIN, DISJOINT) */
protected IntersectVisitor getDenseIntersectVisitor(FixedBitSet intersect, FixedBitSet disjoint) {
return new IntersectVisitor() {
@Override
public void visit(int docID) throws IOException {
if (queryRelation == QueryRelation.DISJOINT) {
// if DISJOINT query set the doc in the disjoint bitset
disjoint.set(docID);
} else {
// for INTERSECT, and WITHIN queries we set the intersect bitset
intersect.set(docID);
}
}
@Override
public void visit(int docID, byte[] t) throws IOException {
if (queryMatches(t)) {
intersect.set(docID);
} else {
disjoint.set(docID);
}
}
@Override
public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
return relateRangeToQuery(minTriangle, maxTriangle);
}
};
}
/** get a scorer supplier for INTERSECT queries */
protected ScorerSupplier getIntersectScorerSupplier(LeafReader reader, PointValues values, Weight weight) throws IOException {
DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field);
IntersectVisitor visitor = getSparseIntersectVisitor(result);
return new RelationScorerSupplier(values, visitor) {
@Override
public Scorer get(long leadCost) throws IOException {
return getIntersectsScorer(LatLonShapeQuery.this, reader, weight, result, score());
}
};
}
/** get a scorer supplier for all other queries (DISJOINT, WITHIN) */
protected ScorerSupplier getScorerSupplier(LeafReader reader, PointValues values, Weight weight) throws IOException {
if (queryRelation == QueryRelation.INTERSECTS) {
return getIntersectScorerSupplier(reader, values, weight);
}
FixedBitSet intersect = new FixedBitSet(reader.maxDoc());
FixedBitSet disjoint = new FixedBitSet(reader.maxDoc());
IntersectVisitor visitor = getDenseIntersectVisitor(intersect, disjoint);
return new RelationScorerSupplier(values, visitor) {
@Override
public Scorer get(long leadCost) throws IOException {
return getScorer(LatLonShapeQuery.this, weight, intersect, disjoint, score());
}
};
}
@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;
}
boolean allDocsMatch = true;
if (values.getDocCount() != reader.maxDoc() ||
relateRangeToQuery(values.getMinPackedValue(), values.getMaxPackedValue()) != Relation.CELL_INSIDE_QUERY) {
allDocsMatch = false;
}
final Weight weight = this;
if (allDocsMatch) {
return new ScorerSupplier() {
@Override
public Scorer get(long leadCost) throws IOException {
return new ConstantScoreScorer(weight, score(),
DocIdSetIterator.all(reader.maxDoc()));
}
@Override
public long cost() {
return reader.maxDoc();
}
};
} else {
return getScorerSupplier(reader, values, weight);
}
}
@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 field name */
public String getField() {
return field;
}
/** returns the query relation */
public QueryRelation getQueryRelation() {
return queryRelation;
}
@Override
public int hashCode() {
int hash = classHash();
hash = 31 * hash + field.hashCode();
hash = 31 * hash + queryRelation.hashCode();
return hash;
}
@Override
public boolean equals(Object o) {
return sameClassAs(o) && equalsTo(o);
}
protected boolean equalsTo(Object o) {
return Objects.equals(field, ((LatLonShapeQuery)o).field) && this.queryRelation == ((LatLonShapeQuery)o).queryRelation;
}
/** transpose the relation; INSIDE becomes OUTSIDE, OUTSIDE becomes INSIDE, CROSSES remains unchanged */
private static Relation transposeRelation(Relation r) {
if (r == Relation.CELL_INSIDE_QUERY) {
return Relation.CELL_OUTSIDE_QUERY;
} else if (r == Relation.CELL_OUTSIDE_QUERY) {
return Relation.CELL_INSIDE_QUERY;
}
return Relation.CELL_CROSSES_QUERY;
}
/** utility class for implementing constant score logic specifig to INTERSECT, WITHIN, and DISJOINT */
protected static abstract class RelationScorerSupplier extends ScorerSupplier {
PointValues values;
IntersectVisitor visitor;
long cost = -1;
RelationScorerSupplier(PointValues values, IntersectVisitor visitor) {
this.values = values;
this.visitor = visitor;
}
/** create a visitor that clears documents that do NOT match the polygon query; used with INTERSECTS */
private IntersectVisitor getInverseIntersectVisitor(LatLonShapeQuery query, FixedBitSet result, int[] cost) {
return new IntersectVisitor() {
@Override
public void visit(int docID) {
result.clear(docID);
cost[0]--;
}
@Override
public void visit(int docID, byte[] packedTriangle) {
if (query.queryMatches(packedTriangle) == false) {
result.clear(docID);
cost[0]--;
}
}
@Override
public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
return transposeRelation(query.relateRangeToQuery(minPackedValue, maxPackedValue));
}
};
}
/** returns a Scorer for INTERSECT queries that uses a sparse bitset */
protected Scorer getIntersectsScorer(LatLonShapeQuery query, LeafReader reader, Weight weight,
DocIdSetBuilder docIdSetBuilder, final float boost) throws IOException {
if (values.getDocCount() == reader.maxDoc()
&& values.getDocCount() == values.size()
&& cost() > reader.maxDoc() / 2) {
// If all docs have exactly one value and the cost is greater
// than half the leaf size then maybe we can make things faster
// by computing the set of documents that do NOT match the query
final FixedBitSet result = new FixedBitSet(reader.maxDoc());
result.set(0, reader.maxDoc());
int[] cost = new int[]{reader.maxDoc()};
values.intersect(getInverseIntersectVisitor(query, result, cost));
final DocIdSetIterator iterator = new BitSetIterator(result, cost[0]);
return new ConstantScoreScorer(weight, boost, iterator);
}
values.intersect(visitor);
DocIdSetIterator iterator = docIdSetBuilder.build().iterator();
return new ConstantScoreScorer(weight, boost, iterator);
}
/** returns a Scorer for all other (non INTERSECT) queries */
protected Scorer getScorer(LatLonShapeQuery query, Weight weight,
FixedBitSet intersect, FixedBitSet disjoint, final float boost) throws IOException {
values.intersect(visitor);
DocIdSetIterator iterator;
if (query.queryRelation == QueryRelation.DISJOINT) {
disjoint.andNot(intersect);
iterator = new BitSetIterator(disjoint, cost());
} else if (query.queryRelation == QueryRelation.WITHIN) {
intersect.andNot(disjoint);
iterator = new BitSetIterator(intersect, cost());
} else {
iterator = new BitSetIterator(intersect, cost());
}
return new ConstantScoreScorer(weight, boost, 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;
}
}
}

View File

@ -22,6 +22,7 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
import com.carrotsearch.randomizedtesting.generators.RandomPicks; import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import org.apache.lucene.document.LatLonShape.QueryRelation;
import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.GeoTestUtil;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
@ -94,6 +95,16 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
return new Polygon(lats, lons); return new Polygon(lats, lons);
} }
protected Line quantizeLine(Line line) {
double[] lats = new double[line.numPoints()];
double[] lons = new double[line.numPoints()];
for (int i = 0; i < lats.length; ++i) {
lats[i] = quantizeLat(line.getLat(i));
lons[i] = quantizeLon(line.getLon(i));
}
return new Line(lats, lons);
}
protected abstract Field[] createIndexableFields(String field, Object shape); protected abstract Field[] createIndexableFields(String field, Object shape);
private void addShapeToDoc(String field, Document doc, Object shape) { private void addShapeToDoc(String field, Document doc, Object shape) {
@ -103,12 +114,12 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
} }
} }
protected Query newRectQuery(String field, double minLat, double maxLat, double minLon, double maxLon) { protected Query newRectQuery(String field, QueryRelation queryRelation, double minLat, double maxLat, double minLon, double maxLon) {
return LatLonShape.newBoxQuery(field, minLat, maxLat, minLon, maxLon); return LatLonShape.newBoxQuery(field, queryRelation, minLat, maxLat, minLon, maxLon);
} }
protected Query newPolygonQuery(String field, Polygon... polygons) { protected Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) {
return LatLonShape.newPolygonQuery(field, polygons); return LatLonShape.newPolygonQuery(field, queryRelation, polygons);
} }
// A particularly tricky adversary for BKD tree: // A particularly tricky adversary for BKD tree:
@ -240,7 +251,8 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
break; break;
} }
} }
Query query = newRectQuery(FIELD_NAME, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon); QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
Query query = newRectQuery(FIELD_NAME, queryRelation, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
if (VERBOSE) { if (VERBOSE) {
System.out.println(" query=" + query); System.out.println(" query=" + query);
@ -280,7 +292,7 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
expected = false; expected = false;
} else { } else {
// check quantized poly against quantized query // check quantized poly against quantized query
expected = getValidator().testBBoxQuery(quantizeLatCeil(rect.minLat), quantizeLat(rect.maxLat), expected = getValidator(queryRelation).testBBoxQuery(quantizeLatCeil(rect.minLat), quantizeLat(rect.maxLat),
quantizeLonCeil(rect.minLon), quantizeLon(rect.maxLon), shapes[id]); quantizeLonCeil(rect.minLon), quantizeLon(rect.maxLon), shapes[id]);
} }
@ -292,10 +304,11 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
} else { } else {
b.append("FAIL: id=" + id + " should not match but did\n"); b.append("FAIL: id=" + id + " should not match but did\n");
} }
b.append(" relation=" + queryRelation + "\n");
b.append(" query=" + query + " docID=" + docID + "\n"); b.append(" query=" + query + " docID=" + docID + "\n");
b.append(" shape=" + shapes[id] + "\n"); b.append(" shape=" + shapes[id] + "\n");
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false)); b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
b.append(" rect=Rectangle(" + quantizeLatCeil(rect.minLat) + " TO " + quantizeLat(rect.maxLat) + " lon=" + quantizeLonCeil(rect.minLon) + " TO " + quantizeLon(rect.maxLon) + ")"); b.append(" rect=Rectangle(" + quantizeLatCeil(rect.minLat) + " TO " + quantizeLat(rect.maxLat) + " lon=" + quantizeLonCeil(rect.minLon) + " TO " + quantizeLon(rect.maxLon) + ")\n");
if (true) { if (true) {
fail("wrong hit (first of possibly more):\n\n" + b); fail("wrong hit (first of possibly more):\n\n" + b);
} else { } else {
@ -326,7 +339,8 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
// Polygon // Polygon
Polygon queryPolygon = GeoTestUtil.nextPolygon(); Polygon queryPolygon = GeoTestUtil.nextPolygon();
Polygon2D queryPoly2D = Polygon2D.create(queryPolygon); Polygon2D queryPoly2D = Polygon2D.create(queryPolygon);
Query query = newPolygonQuery(FIELD_NAME, queryPolygon); QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
Query query = newPolygonQuery(FIELD_NAME, queryRelation, queryPolygon);
if (VERBOSE) { if (VERBOSE) {
System.out.println(" query=" + query); System.out.println(" query=" + query);
@ -365,7 +379,7 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
} else if (shapes[id] == null) { } else if (shapes[id] == null) {
expected = false; expected = false;
} else { } else {
expected = getValidator().testPolygonQuery(queryPoly2D, shapes[id]); expected = getValidator(queryRelation).testPolygonQuery(queryPoly2D, shapes[id]);
} }
if (hits.get(docID) != expected) { if (hits.get(docID) != expected) {
@ -376,6 +390,7 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
} else { } else {
b.append("FAIL: id=" + id + " should not match but did\n"); b.append("FAIL: id=" + id + " should not match but did\n");
} }
b.append(" relation=" + queryRelation + "\n");
b.append(" query=" + query + " docID=" + docID + "\n"); b.append(" query=" + query + " docID=" + docID + "\n");
b.append(" shape=" + shapes[id] + "\n"); b.append(" shape=" + shapes[id] + "\n");
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false)); b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
@ -394,7 +409,7 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
} }
} }
protected abstract Validator getValidator(); protected abstract Validator getValidator(QueryRelation relation);
/** internal point class for testing point shapes */ /** internal point class for testing point shapes */
protected static class Point { protected static class Point {
@ -466,8 +481,13 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
} }
} }
protected interface Validator { protected abstract class Validator {
boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape); protected QueryRelation queryRelation = QueryRelation.INTERSECTS;
boolean testPolygonQuery(Polygon2D poly2d, Object shape); public abstract boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape);
public abstract boolean testPolygonQuery(Polygon2D poly2d, Object shape);
public void setRelation(QueryRelation relation) {
this.queryRelation = relation;
}
} }
} }

View File

@ -16,16 +16,12 @@
*/ */
package org.apache.lucene.document; package org.apache.lucene.document;
import org.apache.lucene.document.LatLonShape.QueryRelation;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Polygon2D; import org.apache.lucene.geo.Polygon2D;
import org.apache.lucene.index.PointValues.Relation; import org.apache.lucene.index.PointValues.Relation;
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;
/** random bounding box and polygon query tests for random generated {@link Line} types */ /** random bounding box and polygon query tests for random generated {@link Line} types */
public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase { public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase {
@ -42,17 +38,25 @@ public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
protected Validator getValidator() { protected Validator getValidator(QueryRelation queryRelation) {
VALIDATOR.setRelation(queryRelation);
return VALIDATOR; return VALIDATOR;
} }
protected class LineValidator implements Validator { protected class LineValidator extends Validator {
@Override @Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
Line l = (Line)shape;
if (queryRelation == QueryRelation.WITHIN) {
// within: bounding box of shape should be within query box
return minLat <= quantizeLat(l.minLat) && maxLat >= quantizeLat(l.maxLat)
&& minLon <= quantizeLon(l.minLon) && maxLon >= quantizeLon(l.maxLon);
}
// to keep it simple we convert the bbox into a polygon and use poly2d // to keep it simple we convert the bbox into a polygon and use poly2d
Polygon2D p = Polygon2D.create(new Polygon[] {new Polygon(new double[] {minLat, minLat, maxLat, maxLat, minLat}, Polygon2D p = Polygon2D.create(new Polygon[] {new Polygon(new double[] {minLat, minLat, maxLat, maxLat, minLat},
new double[] {minLon, maxLon, maxLon, minLon, minLon})}); new double[] {minLon, maxLon, maxLon, minLon, minLon})});
return testLine(p, (Line)shape); return testLine(p, l);
} }
@Override @Override
@ -62,11 +66,12 @@ public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase {
private boolean testLine(Polygon2D queryPoly, Line line) { private boolean testLine(Polygon2D queryPoly, Line line) {
double ax, ay, bx, by, temp; double ax, ay, bx, by, temp;
Relation r;
for (int i = 0, j = 1; j < line.numPoints(); ++i, ++j) { for (int i = 0, j = 1; j < line.numPoints(); ++i, ++j) {
ay = decodeLatitude(encodeLatitude(line.getLat(i))); ay = quantizeLat(line.getLat(i));
ax = decodeLongitude(encodeLongitude(line.getLon(i))); ax = quantizeLon(line.getLon(i));
by = decodeLatitude(encodeLatitude(line.getLat(j))); by = quantizeLat(line.getLat(j));
bx = decodeLongitude(encodeLongitude(line.getLon(j))); bx = quantizeLon(line.getLon(j));
if (ay > by) { if (ay > by) {
temp = ay; temp = ay;
ay = by; ay = by;
@ -84,11 +89,16 @@ public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase {
bx = temp; bx = temp;
} }
} }
if (queryPoly.relateTriangle(ax, ay, bx, by, ax, ay) != Relation.CELL_OUTSIDE_QUERY) { r = queryPoly.relateTriangle(ax, ay, bx, by, ax, ay);
return true; if (queryRelation == QueryRelation.DISJOINT) {
if (r != Relation.CELL_OUTSIDE_QUERY) return false;
} else if (queryRelation == QueryRelation.WITHIN) {
if (r != Relation.CELL_INSIDE_QUERY) return false;
} else {
if (r != Relation.CELL_OUTSIDE_QUERY) return true;
} }
} }
return false; return queryRelation == QueryRelation.INTERSECTS ? false : true;
} }
} }
} }

View File

@ -16,6 +16,7 @@
*/ */
package org.apache.lucene.document; package org.apache.lucene.document;
import org.apache.lucene.document.LatLonShape.QueryRelation;
import org.apache.lucene.geo.Polygon2D; import org.apache.lucene.geo.Polygon2D;
import org.apache.lucene.index.PointValues.Relation; import org.apache.lucene.index.PointValues.Relation;
@ -41,17 +42,22 @@ public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
protected Validator getValidator() { protected Validator getValidator(QueryRelation relation) {
VALIDATOR.setRelation(relation);
return VALIDATOR; return VALIDATOR;
} }
protected class PointValidator implements Validator { protected class PointValidator extends Validator {
@Override @Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
Point p = (Point)shape; Point p = (Point)shape;
double lat = decodeLatitude(encodeLatitude(p.lat)); double lat = decodeLatitude(encodeLatitude(p.lat));
double lon = decodeLongitude(encodeLongitude(p.lon)); double lon = decodeLongitude(encodeLongitude(p.lon));
return (lat < minLat || lat > maxLat || lon < minLon || lon > maxLon) == false; boolean isDisjoint = lat < minLat || lat > maxLat || lon < minLon || lon > maxLon;
if (queryRelation == QueryRelation.DISJOINT) {
return isDisjoint;
}
return isDisjoint == false;
} }
@Override @Override
@ -60,7 +66,13 @@ public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase {
double lat = decodeLatitude(encodeLatitude(p.lat)); double lat = decodeLatitude(encodeLatitude(p.lat));
double lon = decodeLongitude(encodeLongitude(p.lon)); double lon = decodeLongitude(encodeLongitude(p.lon));
// for consistency w/ the query we test the point as a triangle // for consistency w/ the query we test the point as a triangle
return poly2d.relateTriangle(lon, lat, lon, lat, lon, lat) != Relation.CELL_OUTSIDE_QUERY; Relation r = poly2d.relateTriangle(lon, lat, lon, lat, lon, lat);
if (queryRelation == QueryRelation.WITHIN) {
return r == Relation.CELL_INSIDE_QUERY;
} else if (queryRelation == QueryRelation.DISJOINT) {
return r == Relation.CELL_OUTSIDE_QUERY;
}
return r != Relation.CELL_OUTSIDE_QUERY;
} }
} }

View File

@ -18,11 +18,13 @@ package org.apache.lucene.document;
import java.util.List; import java.util.List;
import org.apache.lucene.document.LatLonShape.QueryRelation;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Polygon2D; import org.apache.lucene.geo.Polygon2D;
import org.apache.lucene.geo.Tessellator; import org.apache.lucene.geo.Tessellator;
import org.apache.lucene.index.PointValues.Relation; import org.apache.lucene.index.PointValues.Relation;
/** random bounding box and polygon query tests for random indexed {@link Polygon} types */
public class TestLatLonPolygonShapeQueries extends BaseLatLonShapeTestCase { public class TestLatLonPolygonShapeQueries extends BaseLatLonShapeTestCase {
protected final PolygonValidator VALIDATOR = new PolygonValidator(); protected final PolygonValidator VALIDATOR = new PolygonValidator();
@ -53,30 +55,46 @@ public class TestLatLonPolygonShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
protected Validator getValidator() { protected Validator getValidator(QueryRelation relation) {
VALIDATOR.setRelation(relation);
return VALIDATOR; return VALIDATOR;
} }
protected class PolygonValidator implements Validator { protected class PolygonValidator extends Validator {
@Override @Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
Polygon2D poly = Polygon2D.create(quantizePolygon((Polygon)shape)); Polygon p = (Polygon)shape;
return poly.relate(minLat, maxLat, minLon, maxLon) != Relation.CELL_OUTSIDE_QUERY; if (queryRelation == QueryRelation.WITHIN) {
// within: bounding box of shape should be within query box
return minLat <= quantizeLat(p.minLat) && maxLat >= quantizeLat(p.maxLat)
&& minLon <= quantizeLon(p.minLon) && maxLon >= quantizeLon(p.maxLon);
}
Polygon2D poly = Polygon2D.create(quantizePolygon(p));
Relation r = poly.relate(minLat, maxLat, minLon, maxLon);
if (queryRelation == QueryRelation.DISJOINT) {
return r == Relation.CELL_OUTSIDE_QUERY;
}
return r != Relation.CELL_OUTSIDE_QUERY;
} }
@Override @Override
public boolean testPolygonQuery(Polygon2D query, Object shape) { public boolean testPolygonQuery(Polygon2D query, Object shape) {
List<Tessellator.Triangle> tessellation = Tessellator.tessellate((Polygon) shape); List<Tessellator.Triangle> tessellation = Tessellator.tessellate((Polygon) shape);
for (Tessellator.Triangle t : tessellation) { for (Tessellator.Triangle t : tessellation) {
// we quantize the triangle for consistency with the index // we quantize the triangle for consistency with the index
if (query.relateTriangle(quantizeLon(t.getLon(0)), quantizeLat(t.getLat(0)), Relation r = query.relateTriangle(quantizeLon(t.getLon(0)), quantizeLat(t.getLat(0)),
quantizeLon(t.getLon(1)), quantizeLat(t.getLat(1)), quantizeLon(t.getLon(1)), quantizeLat(t.getLat(1)),
quantizeLon(t.getLon(2)), quantizeLat(t.getLat(2))) != Relation.CELL_OUTSIDE_QUERY) { quantizeLon(t.getLon(2)), quantizeLat(t.getLat(2)));
return true; if (queryRelation == QueryRelation.DISJOINT) {
if (r != Relation.CELL_OUTSIDE_QUERY) return false;
} else if (queryRelation == QueryRelation.WITHIN) {
if (r != Relation.CELL_INSIDE_QUERY) return false;
} else {
if (r != Relation.CELL_OUTSIDE_QUERY) return true;
} }
} }
return false; return queryRelation == QueryRelation.INTERSECTS ? false : true;
} }
} }
} }

View File

@ -17,6 +17,7 @@
package org.apache.lucene.document; package org.apache.lucene.document;
import com.carrotsearch.randomizedtesting.generators.RandomNumbers; import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
import org.apache.lucene.document.LatLonShape.QueryRelation;
import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.GeoTestUtil;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
@ -52,7 +53,7 @@ public class TestLatLonShape extends LuceneTestCase {
} }
protected Query newRectQuery(String field, double minLat, double maxLat, double minLon, double maxLon) { protected Query newRectQuery(String field, double minLat, double maxLat, double minLon, double maxLon) {
return LatLonShape.newBoxQuery(field, minLat, maxLat, minLon, maxLon); return LatLonShape.newBoxQuery(field, QueryRelation.INTERSECTS, minLat, maxLat, minLon, maxLon);
} }
@Ignore @Ignore