org.apache.lucene.spatial.bbox.BBoxStrategy Maven / Gradle / Ivy
Show all versions of lucene-spatial-extras Show documentation
/*
* 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.spatial.bbox;
import org.apache.lucene.document.DoubleDocValuesField;
import org.apache.lucene.document.DoublePoint;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.LegacyDoubleField;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.Term;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.LegacyNumericRangeQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.spatial.SpatialStrategy;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.spatial.query.UnsupportedSpatialOperation;
import org.apache.lucene.spatial.util.DistanceToShapeValueSource;
import org.apache.lucene.util.BytesRefBuilder;
import org.apache.lucene.util.LegacyNumericUtils;
import org.apache.lucene.util.NumericUtils;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.Shape;
/**
* A SpatialStrategy for indexing and searching Rectangles by storing its
* coordinates in numeric fields. It supports all {@link SpatialOperation}s and
* has a custom overlap relevancy. It is based on GeoPortal's SpatialClauseAdapter.
*
* Characteristics:
*
*
* - Only indexes Rectangles; just one per field value. Other shapes can be provided
* and the bounding box will be used.
* - Can query only by a Rectangle. Providing other shapes is an error.
* - Supports most {@link SpatialOperation}s but not Overlaps.
* - Uses the DocValues API for any sorting / relevancy.
*
*
* Implementation:
*
* This uses 4 double fields for minX, maxX, minY, maxY
* and a boolean to mark a dateline cross. Depending on the particular {@link
* SpatialOperation}s, there are a variety of range queries on {@link DoublePoint}s to be
* done.
* The {@link #makeOverlapRatioValueSource(org.locationtech.spatial4j.shape.Rectangle, double)}
* works by calculating the query bbox overlap percentage against the indexed
* shape overlap percentage. The indexed shape's coordinates are retrieved from
* {@link org.apache.lucene.index.LeafReader#getNumericDocValues}.
*
* @lucene.experimental
*/
public class BBoxStrategy extends SpatialStrategy {
// note: we use a FieldType to articulate the options we want on the field. We don't use it as-is with a Field, we
// create more than one Field.
/**
* pointValues, docValues, and nothing else.
*/
public static FieldType DEFAULT_FIELDTYPE;
@Deprecated
public static FieldType LEGACY_FIELDTYPE;
static {
// Default: pointValues + docValues
FieldType type = new FieldType();
type.setDimensions(1, Double.BYTES);//pointValues (assume Double)
type.setDocValuesType(DocValuesType.NUMERIC);//docValues
type.setStored(false);
type.freeze();
DEFAULT_FIELDTYPE = type;
// Legacy default: legacyNumerics + docValues
type = new FieldType();
type.setIndexOptions(IndexOptions.DOCS);
type.setNumericType(FieldType.LegacyNumericType.DOUBLE);
type.setNumericPrecisionStep(8);// same as solr default
type.setDocValuesType(DocValuesType.NUMERIC);//docValues
type.setStored(false);
type.freeze();
LEGACY_FIELDTYPE = type;
}
public static final String SUFFIX_MINX = "__minX";
public static final String SUFFIX_MAXX = "__maxX";
public static final String SUFFIX_MINY = "__minY";
public static final String SUFFIX_MAXY = "__maxY";
public static final String SUFFIX_XDL = "__xdl";
/*
* The Bounding Box gets stored as four fields for x/y min/max and a flag
* that says if the box crosses the dateline (xdl).
*/
final String field_bbox;
final String field_minX;
final String field_minY;
final String field_maxX;
final String field_maxY;
final String field_xdl; // crosses dateline
private final FieldType optionsFieldType;//from constructor; aggregate field type used to express all options
private final int fieldsLen;
private final boolean hasStored;
private final boolean hasDocVals;
private final boolean hasPointVals;
// equiv to "hasLegacyNumerics":
private final FieldType legacyNumericFieldType; // not stored; holds precision step.
private final FieldType xdlFieldType;
/**
* Creates a new {@link BBoxStrategy} instance that uses {@link DoublePoint} and {@link DoublePoint#newRangeQuery}
*/
public static BBoxStrategy newInstance(SpatialContext ctx, String fieldNamePrefix) {
return new BBoxStrategy(ctx, fieldNamePrefix, DEFAULT_FIELDTYPE);
}
/**
* Creates a new {@link BBoxStrategy} instance that uses {@link LegacyDoubleField} for backwards compatibility
* @deprecated LegacyNumerics will be removed
*/
@Deprecated
public static BBoxStrategy newLegacyInstance(SpatialContext ctx, String fieldNamePrefix) {
return new BBoxStrategy(ctx, fieldNamePrefix, LEGACY_FIELDTYPE);
}
/**
* Creates this strategy.
* {@code fieldType} is used to customize the indexing options of the 4 number fields, and to a lesser degree the XDL
* field too. Search requires pointValues (or legacy numerics), and relevancy requires docValues. If these features
* aren't needed then disable them.
*/
public BBoxStrategy(SpatialContext ctx, String fieldNamePrefix, FieldType fieldType) {
super(ctx, fieldNamePrefix);
field_bbox = fieldNamePrefix;
field_minX = fieldNamePrefix + SUFFIX_MINX;
field_maxX = fieldNamePrefix + SUFFIX_MAXX;
field_minY = fieldNamePrefix + SUFFIX_MINY;
field_maxY = fieldNamePrefix + SUFFIX_MAXY;
field_xdl = fieldNamePrefix + SUFFIX_XDL;
fieldType.freeze();
this.optionsFieldType = fieldType;
int numQuads = 0;
if ((this.hasStored = fieldType.stored())) {
numQuads++;
}
if ((this.hasDocVals = fieldType.docValuesType() != DocValuesType.NONE)) {
numQuads++;
}
if ((this.hasPointVals = fieldType.pointDimensionCount() > 0)) {
numQuads++;
}
if (fieldType.indexOptions() != IndexOptions.NONE && fieldType.numericType() != null) {
if (hasPointVals) {
throw new IllegalArgumentException("pointValues and LegacyNumericType are mutually exclusive");
}
if (fieldType.numericType() != FieldType.LegacyNumericType.DOUBLE) {
throw new IllegalArgumentException(getClass() + " does not support " + fieldType.numericType());
}
numQuads++;
legacyNumericFieldType = new FieldType(LegacyDoubleField.TYPE_NOT_STORED);
legacyNumericFieldType.setNumericPrecisionStep(fieldType.numericPrecisionStep());
legacyNumericFieldType.freeze();
} else {
legacyNumericFieldType = null;
}
if (hasPointVals || legacyNumericFieldType != null) { // if we have an index...
xdlFieldType = new FieldType(StringField.TYPE_NOT_STORED);
xdlFieldType.setIndexOptions(IndexOptions.DOCS);
xdlFieldType.freeze();
} else {
xdlFieldType = null;
}
this.fieldsLen = numQuads * 4 + (xdlFieldType != null ? 1 : 0);
}
/** Returns a field type representing the set of field options. This is identical to what was passed into the
* constructor. It's frozen. */
public FieldType getFieldType() {
return optionsFieldType;
}
//---------------------------------
// Indexing
//---------------------------------
@Override
public Field[] createIndexableFields(Shape shape) {
return createIndexableFields(shape.getBoundingBox());
}
private Field[] createIndexableFields(Rectangle bbox) {
Field[] fields = new Field[fieldsLen];
int idx = -1;
if (hasStored) {
fields[++idx] = new StoredField(field_minX, bbox.getMinX());
fields[++idx] = new StoredField(field_minY, bbox.getMinY());
fields[++idx] = new StoredField(field_maxX, bbox.getMaxX());
fields[++idx] = new StoredField(field_maxY, bbox.getMaxY());
}
if (hasDocVals) {
fields[++idx] = new DoubleDocValuesField(field_minX, bbox.getMinX());
fields[++idx] = new DoubleDocValuesField(field_minY, bbox.getMinY());
fields[++idx] = new DoubleDocValuesField(field_maxX, bbox.getMaxX());
fields[++idx] = new DoubleDocValuesField(field_maxY, bbox.getMaxY());
}
if (hasPointVals) {
fields[++idx] = new DoublePoint(field_minX, bbox.getMinX());
fields[++idx] = new DoublePoint(field_minY, bbox.getMinY());
fields[++idx] = new DoublePoint(field_maxX, bbox.getMaxX());
fields[++idx] = new DoublePoint(field_maxY, bbox.getMaxY());
}
if (legacyNumericFieldType != null) {
fields[++idx] = new LegacyDoubleField(field_minX, bbox.getMinX(), legacyNumericFieldType);
fields[++idx] = new LegacyDoubleField(field_minY, bbox.getMinY(), legacyNumericFieldType);
fields[++idx] = new LegacyDoubleField(field_maxX, bbox.getMaxX(), legacyNumericFieldType);
fields[++idx] = new LegacyDoubleField(field_maxY, bbox.getMaxY(), legacyNumericFieldType);
}
if (xdlFieldType != null) {
fields[++idx] = new Field(field_xdl, bbox.getCrossesDateLine()?"T":"F", xdlFieldType);
}
assert idx == fields.length - 1;
return fields;
}
//---------------------------------
// Value Source / Relevancy
//---------------------------------
/**
* Provides access to each rectangle per document as a ValueSource in which
* {@link org.apache.lucene.queries.function.FunctionValues#objectVal(int)} returns a {@link
* Shape}.
*/ //TODO raise to SpatialStrategy
public ValueSource makeShapeValueSource() {
return new BBoxValueSource(this);
}
@Override
public ValueSource makeDistanceValueSource(Point queryPoint, double multiplier) {
//TODO if makeShapeValueSource gets lifted to the top; this could become a generic impl.
return new DistanceToShapeValueSource(makeShapeValueSource(), queryPoint, multiplier, ctx);
}
/** Returns a similarity based on {@link BBoxOverlapRatioValueSource}. This is just a
* convenience method. */
public ValueSource makeOverlapRatioValueSource(Rectangle queryBox, double queryTargetProportion) {
return new BBoxOverlapRatioValueSource(
makeShapeValueSource(), ctx.isGeo(), queryBox, queryTargetProportion, 0.0);
}
//---------------------------------
// Query Building
//---------------------------------
// Utility on SpatialStrategy?
// public Query makeQueryWithValueSource(SpatialArgs args, ValueSource valueSource) {
// return new CustomScoreQuery(makeQuery(args), new FunctionQuery(valueSource));
//or...
// return new BooleanQuery.Builder()
// .add(new FunctionQuery(valueSource), BooleanClause.Occur.MUST)//matches everything and provides score
// .add(filterQuery, BooleanClause.Occur.FILTER)//filters (score isn't used)
// .build();
// }
@Override
public Query makeQuery(SpatialArgs args) {
Shape shape = args.getShape();
if (!(shape instanceof Rectangle))
throw new UnsupportedOperationException("Can only query by Rectangle, not " + shape);
Rectangle bbox = (Rectangle) shape;
Query spatial;
// Useful for understanding Relations:
// http://edndoc.esri.com/arcsde/9.1/general_topics/understand_spatial_relations.htm
SpatialOperation op = args.getOperation();
if( op == SpatialOperation.BBoxIntersects ) spatial = makeIntersects(bbox);
else if( op == SpatialOperation.BBoxWithin ) spatial = makeWithin(bbox);
else if( op == SpatialOperation.Contains ) spatial = makeContains(bbox);
else if( op == SpatialOperation.Intersects ) spatial = makeIntersects(bbox);
else if( op == SpatialOperation.IsEqualTo ) spatial = makeEquals(bbox);
else if( op == SpatialOperation.IsDisjointTo ) spatial = makeDisjoint(bbox);
else if( op == SpatialOperation.IsWithin ) spatial = makeWithin(bbox);
else { //no Overlaps support yet
throw new UnsupportedSpatialOperation(op);
}
return new ConstantScoreQuery(spatial);
}
/**
* Constructs a query to retrieve documents that fully contain the input envelope.
*
* @return the spatial query
*/
Query makeContains(Rectangle bbox) {
// general case
// docMinX <= queryExtent.getMinX() AND docMinY <= queryExtent.getMinY() AND docMaxX >= queryExtent.getMaxX() AND docMaxY >= queryExtent.getMaxY()
// Y conditions
// docMinY <= queryExtent.getMinY() AND docMaxY >= queryExtent.getMaxY()
Query qMinY = this.makeNumericRangeQuery(field_minY, null, bbox.getMinY(), false, true);
Query qMaxY = this.makeNumericRangeQuery(field_maxY, bbox.getMaxY(), null, true, false);
Query yConditions = this.makeQuery(BooleanClause.Occur.MUST, qMinY, qMaxY);
// X conditions
Query xConditions;
// queries that do not cross the date line
if (!bbox.getCrossesDateLine()) {
// X Conditions for documents that do not cross the date line,
// documents that contain the min X and max X of the query envelope,
// docMinX <= queryExtent.getMinX() AND docMaxX >= queryExtent.getMaxX()
Query qMinX = this.makeNumericRangeQuery(field_minX, null, bbox.getMinX(), false, true);
Query qMaxX = this.makeNumericRangeQuery(field_maxX, bbox.getMaxX(), null, true, false);
Query qMinMax = this.makeQuery(BooleanClause.Occur.MUST, qMinX, qMaxX);
Query qNonXDL = this.makeXDL(false, qMinMax);
if (!ctx.isGeo()) {
xConditions = qNonXDL;
} else {
// X Conditions for documents that cross the date line,
// the left portion of the document contains the min X of the query
// OR the right portion of the document contains the max X of the query,
// docMinXLeft <= queryExtent.getMinX() OR docMaxXRight >= queryExtent.getMaxX()
Query qXDLLeft = this.makeNumericRangeQuery(field_minX, null, bbox.getMinX(), false, true);
Query qXDLRight = this.makeNumericRangeQuery(field_maxX, bbox.getMaxX(), null, true, false);
Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.SHOULD, qXDLLeft, qXDLRight);
Query qXDL = this.makeXDL(true, qXDLLeftRight);
Query qEdgeDL = null;
if (bbox.getMinX() == bbox.getMaxX() && Math.abs(bbox.getMinX()) == 180) {
double edge = bbox.getMinX() * -1;//opposite dateline edge
qEdgeDL = makeQuery(BooleanClause.Occur.SHOULD,
makeNumberTermQuery(field_minX, edge), makeNumberTermQuery(field_maxX, edge));
}
// apply the non-XDL and XDL conditions
xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL, qEdgeDL);
}
} else {
// queries that cross the date line
// No need to search for documents that do not cross the date line
// X Conditions for documents that cross the date line,
// the left portion of the document contains the min X of the query
// AND the right portion of the document contains the max X of the query,
// docMinXLeft <= queryExtent.getMinX() AND docMaxXRight >= queryExtent.getMaxX()
Query qXDLLeft = this.makeNumericRangeQuery(field_minX, null, bbox.getMinX(), false, true);
Query qXDLRight = this.makeNumericRangeQuery(field_maxX, bbox.getMaxX(), null, true, false);
Query qXDLLeftRight = this.makeXDL(true, this.makeQuery(BooleanClause.Occur.MUST, qXDLLeft, qXDLRight));
Query qWorld = makeQuery(BooleanClause.Occur.MUST,
makeNumberTermQuery(field_minX, -180), makeNumberTermQuery(field_maxX, 180));
xConditions = makeQuery(BooleanClause.Occur.SHOULD, qXDLLeftRight, qWorld);
}
// both X and Y conditions must occur
return this.makeQuery(BooleanClause.Occur.MUST, xConditions, yConditions);
}
/**
* Constructs a query to retrieve documents that are disjoint to the input envelope.
*
* @return the spatial query
*/
Query makeDisjoint(Rectangle bbox) {
// general case
// docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX() OR docMinY > queryExtent.getMaxY() OR docMaxY < queryExtent.getMinY()
// Y conditions
// docMinY > queryExtent.getMaxY() OR docMaxY < queryExtent.getMinY()
Query qMinY = this.makeNumericRangeQuery(field_minY, bbox.getMaxY(), null, false, false);
Query qMaxY = this.makeNumericRangeQuery(field_maxY, null, bbox.getMinY(), false, false);
Query yConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qMinY, qMaxY);
// X conditions
Query xConditions;
// queries that do not cross the date line
if (!bbox.getCrossesDateLine()) {
// X Conditions for documents that do not cross the date line,
// docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX()
Query qMinX = this.makeNumericRangeQuery(field_minX, bbox.getMaxX(), null, false, false);
if (bbox.getMinX() == -180.0 && ctx.isGeo()) {//touches dateline; -180 == 180
BooleanQuery.Builder bq = new BooleanQuery.Builder();
bq.add(qMinX, BooleanClause.Occur.MUST);
bq.add(makeNumberTermQuery(field_maxX, 180.0), BooleanClause.Occur.MUST_NOT);
qMinX = bq.build();
}
Query qMaxX = this.makeNumericRangeQuery(field_maxX, null, bbox.getMinX(), false, false);
if (bbox.getMaxX() == 180.0 && ctx.isGeo()) {//touches dateline; -180 == 180
BooleanQuery.Builder bq = new BooleanQuery.Builder();
bq.add(qMaxX, BooleanClause.Occur.MUST);
bq.add(makeNumberTermQuery(field_minX, -180.0), BooleanClause.Occur.MUST_NOT);
qMaxX = bq.build();
}
Query qMinMax = this.makeQuery(BooleanClause.Occur.SHOULD, qMinX, qMaxX);
Query qNonXDL = this.makeXDL(false, qMinMax);
if (!ctx.isGeo()) {
xConditions = qNonXDL;
} else {
// X Conditions for documents that cross the date line,
// both the left and right portions of the document must be disjoint to the query
// (docMinXLeft > queryExtent.getMaxX() OR docMaxXLeft < queryExtent.getMinX()) AND
// (docMinXRight > queryExtent.getMaxX() OR docMaxXRight < queryExtent.getMinX())
// where: docMaxXLeft = 180.0, docMinXRight = -180.0
// (docMaxXLeft < queryExtent.getMinX()) equates to (180.0 < queryExtent.getMinX()) and is ignored
// (docMinXRight > queryExtent.getMaxX()) equates to (-180.0 > queryExtent.getMaxX()) and is ignored
Query qMinXLeft = this.makeNumericRangeQuery(field_minX, bbox.getMaxX(), null, false, false);
Query qMaxXRight = this.makeNumericRangeQuery(field_maxX, null, bbox.getMinX(), false, false);
Query qLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qMinXLeft, qMaxXRight);
Query qXDL = this.makeXDL(true, qLeftRight);
// apply the non-XDL and XDL conditions
xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
}
// queries that cross the date line
} else {
// X Conditions for documents that do not cross the date line,
// the document must be disjoint to both the left and right query portions
// (docMinX > queryExtent.getMaxX()Left OR docMaxX < queryExtent.getMinX()) AND (docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX()Left)
// where: queryExtent.getMaxX()Left = 180.0, queryExtent.getMinX()Left = -180.0
Query qMinXLeft = this.makeNumericRangeQuery(field_minX, 180.0, null, false, false);
Query qMaxXLeft = this.makeNumericRangeQuery(field_maxX, null, bbox.getMinX(), false, false);
Query qMinXRight = this.makeNumericRangeQuery(field_minX, bbox.getMaxX(), null, false, false);
Query qMaxXRight = this.makeNumericRangeQuery(field_maxX, null, -180.0, false, false);
Query qLeft = this.makeQuery(BooleanClause.Occur.SHOULD, qMinXLeft, qMaxXLeft);
Query qRight = this.makeQuery(BooleanClause.Occur.SHOULD, qMinXRight, qMaxXRight);
Query qLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qLeft, qRight);
// No need to search for documents that do not cross the date line
xConditions = this.makeXDL(false, qLeftRight);
}
// either X or Y conditions should occur
return this.makeQuery(BooleanClause.Occur.SHOULD, xConditions, yConditions);
}
/**
* Constructs a query to retrieve documents that equal the input envelope.
*
* @return the spatial query
*/
Query makeEquals(Rectangle bbox) {
// docMinX = queryExtent.getMinX() AND docMinY = queryExtent.getMinY() AND docMaxX = queryExtent.getMaxX() AND docMaxY = queryExtent.getMaxY()
Query qMinX = makeNumberTermQuery(field_minX, bbox.getMinX());
Query qMinY = makeNumberTermQuery(field_minY, bbox.getMinY());
Query qMaxX = makeNumberTermQuery(field_maxX, bbox.getMaxX());
Query qMaxY = makeNumberTermQuery(field_maxY, bbox.getMaxY());
return makeQuery(BooleanClause.Occur.MUST, qMinX, qMinY, qMaxX, qMaxY);
}
/**
* Constructs a query to retrieve documents that intersect the input envelope.
*
* @return the spatial query
*/
Query makeIntersects(Rectangle bbox) {
// the original intersects query does not work for envelopes that cross the date line,
// switch to a NOT Disjoint query
// MUST_NOT causes a problem when it's the only clause type within a BooleanQuery,
// to get around it we add all documents as a SHOULD
// there must be an envelope, it must not be disjoint
Query qHasEnv;
if (ctx.isGeo()) {
Query qIsNonXDL = this.makeXDL(false);
Query qIsXDL = ctx.isGeo() ? this.makeXDL(true) : null;
qHasEnv = this.makeQuery(BooleanClause.Occur.SHOULD, qIsNonXDL, qIsXDL);
} else {
qHasEnv = this.makeXDL(false);
}
BooleanQuery.Builder qNotDisjoint = new BooleanQuery.Builder();
qNotDisjoint.add(qHasEnv, BooleanClause.Occur.MUST);
Query qDisjoint = makeDisjoint(bbox);
qNotDisjoint.add(qDisjoint, BooleanClause.Occur.MUST_NOT);
//Query qDisjoint = makeDisjoint();
//BooleanQuery qNotDisjoint = new BooleanQuery();
//qNotDisjoint.add(new MatchAllDocsQuery(),BooleanClause.Occur.SHOULD);
//qNotDisjoint.add(qDisjoint,BooleanClause.Occur.MUST_NOT);
return qNotDisjoint.build();
}
/**
* Makes a boolean query based upon a collection of queries and a logical operator.
*
* @param occur the logical operator
* @param queries the query collection
* @return the query
*/
BooleanQuery makeQuery(BooleanClause.Occur occur, Query... queries) {
BooleanQuery.Builder bq = new BooleanQuery.Builder();
for (Query query : queries) {
if (query != null)
bq.add(query, occur);
}
return bq.build();
}
/**
* Constructs a query to retrieve documents are fully within the input envelope.
*
* @return the spatial query
*/
Query makeWithin(Rectangle bbox) {
// general case
// docMinX >= queryExtent.getMinX() AND docMinY >= queryExtent.getMinY() AND docMaxX <= queryExtent.getMaxX() AND docMaxY <= queryExtent.getMaxY()
// Y conditions
// docMinY >= queryExtent.getMinY() AND docMaxY <= queryExtent.getMaxY()
Query qMinY = this.makeNumericRangeQuery(field_minY, bbox.getMinY(), null, true, false);
Query qMaxY = this.makeNumericRangeQuery(field_maxY, null, bbox.getMaxY(), false, true);
Query yConditions = this.makeQuery(BooleanClause.Occur.MUST, qMinY, qMaxY);
// X conditions
Query xConditions;
if (ctx.isGeo() && bbox.getMinX() == -180.0 && bbox.getMaxX() == 180.0) {
//if query world-wraps, only the y condition matters
return yConditions;
} else if (!bbox.getCrossesDateLine()) {
// queries that do not cross the date line
// docMinX >= queryExtent.getMinX() AND docMaxX <= queryExtent.getMaxX()
Query qMinX = this.makeNumericRangeQuery(field_minX, bbox.getMinX(), null, true, false);
Query qMaxX = this.makeNumericRangeQuery(field_maxX, null, bbox.getMaxX(), false, true);
Query qMinMax = this.makeQuery(BooleanClause.Occur.MUST, qMinX, qMaxX);
double edge = 0;//none, otherwise opposite dateline of query
if (bbox.getMinX() == -180.0)
edge = 180;
else if (bbox.getMaxX() == 180.0)
edge = -180;
if (edge != 0 && ctx.isGeo()) {
Query edgeQ = makeQuery(BooleanClause.Occur.MUST,
makeNumberTermQuery(field_minX, edge), makeNumberTermQuery(field_maxX, edge));
qMinMax = makeQuery(BooleanClause.Occur.SHOULD, qMinMax, edgeQ);
}
xConditions = this.makeXDL(false, qMinMax);
// queries that cross the date line
} else {
// X Conditions for documents that do not cross the date line
// the document should be within the left portion of the query
// docMinX >= queryExtent.getMinX() AND docMaxX <= 180.0
Query qMinXLeft = this.makeNumericRangeQuery(field_minX, bbox.getMinX(), null, true, false);
Query qMaxXLeft = this.makeNumericRangeQuery(field_maxX, null, 180.0, false, true);
Query qLeft = this.makeQuery(BooleanClause.Occur.MUST, qMinXLeft, qMaxXLeft);
// the document should be within the right portion of the query
// docMinX >= -180.0 AND docMaxX <= queryExtent.getMaxX()
Query qMinXRight = this.makeNumericRangeQuery(field_minX, -180.0, null, true, false);
Query qMaxXRight = this.makeNumericRangeQuery(field_maxX, null, bbox.getMaxX(), false, true);
Query qRight = this.makeQuery(BooleanClause.Occur.MUST, qMinXRight, qMaxXRight);
// either left or right conditions should occur,
// apply the left and right conditions to documents that do not cross the date line
Query qLeftRight = this.makeQuery(BooleanClause.Occur.SHOULD, qLeft, qRight);
Query qNonXDL = this.makeXDL(false, qLeftRight);
// X Conditions for documents that cross the date line,
// the left portion of the document must be within the left portion of the query,
// AND the right portion of the document must be within the right portion of the query
// docMinXLeft >= queryExtent.getMinX() AND docMaxXLeft <= 180.0
// AND docMinXRight >= -180.0 AND docMaxXRight <= queryExtent.getMaxX()
Query qXDLLeft = this.makeNumericRangeQuery(field_minX, bbox.getMinX(), null, true, false);
Query qXDLRight = this.makeNumericRangeQuery(field_maxX, null, bbox.getMaxX(), false, true);
Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qXDLLeft, qXDLRight);
Query qXDL = this.makeXDL(true, qXDLLeftRight);
// apply the non-XDL and XDL conditions
xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
}
// both X and Y conditions must occur
return this.makeQuery(BooleanClause.Occur.MUST, xConditions, yConditions);
}
/**
* Constructs a query to retrieve documents that do or do not cross the date line.
*
* @param crossedDateLine true for documents that cross the date line
* @return the query
*/
private Query makeXDL(boolean crossedDateLine) {
// The 'T' and 'F' values match solr fields
return new TermQuery(new Term(field_xdl, crossedDateLine ? "T" : "F"));
}
/**
* Constructs a query to retrieve documents that do or do not cross the date line
* and match the supplied spatial query.
*
* @param crossedDateLine true for documents that cross the date line
* @param query the spatial query
* @return the query
*/
private Query makeXDL(boolean crossedDateLine, Query query) {
if (!ctx.isGeo()) {
assert !crossedDateLine;
return query;
}
BooleanQuery.Builder bq = new BooleanQuery.Builder();
bq.add(this.makeXDL(crossedDateLine), BooleanClause.Occur.MUST);
bq.add(query, BooleanClause.Occur.MUST);
return bq.build();
}
private Query makeNumberTermQuery(String field, double number) {
if (hasPointVals) {
return DoublePoint.newExactQuery(field, number);
} else if (legacyNumericFieldType != null) {
BytesRefBuilder bytes = new BytesRefBuilder();
LegacyNumericUtils.longToPrefixCoded(NumericUtils.doubleToSortableLong(number), 0, bytes);
return new TermQuery(new Term(field, bytes.get()));
}
throw new UnsupportedOperationException("An index is required for this operation.");
}
/**
* Returns a numeric range query based on FieldType
* {@link LegacyNumericRangeQuery} is used for indexes created using {@code FieldType.LegacyNumericType}
* {@link DoublePoint#newRangeQuery} is used for indexes created using {@link DoublePoint} fields
*
* @param fieldname field name. must not be null
.
* @param min minimum value of the range.
* @param max maximum value of the range.
* @param minInclusive include the minimum value if true
.
* @param maxInclusive include the maximum value if true
*/
private Query makeNumericRangeQuery(String fieldname, Double min, Double max, boolean minInclusive, boolean maxInclusive) {
if (hasPointVals) {
if (min == null) {
min = Double.NEGATIVE_INFINITY;
}
if (max == null) {
max = Double.POSITIVE_INFINITY;
}
if (minInclusive == false) {
min = Math.nextUp(min);
}
if (maxInclusive == false) {
max = Math.nextDown(max);
}
return DoublePoint.newRangeQuery(fieldname, min, max);
} else if (legacyNumericFieldType != null) {// todo remove legacy numeric support in 7.0
return LegacyNumericRangeQuery.newDoubleRange(fieldname, legacyNumericFieldType.numericPrecisionStep(), min, max, minInclusive, maxInclusive);
}
throw new UnsupportedOperationException("An index is required for this operation.");
}
}