org.codelibs.elasticsearch.index.query.GeoBoundingBoxQueryBuilder Maven / Gradle / Ivy
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.codelibs.elasticsearch.index.query;
import org.apache.lucene.geo.Rectangle;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query;
import org.codelibs.elasticsearch.ElasticsearchParseException;
import org.codelibs.elasticsearch.common.Numbers;
import org.codelibs.elasticsearch.common.ParseField;
import org.codelibs.elasticsearch.common.ParsingException;
import org.codelibs.elasticsearch.common.geo.GeoHashUtils;
import org.codelibs.elasticsearch.common.geo.GeoPoint;
import org.codelibs.elasticsearch.common.geo.GeoUtils;
import org.codelibs.elasticsearch.common.io.stream.StreamInput;
import org.codelibs.elasticsearch.common.io.stream.StreamOutput;
import org.codelibs.elasticsearch.common.xcontent.XContentBuilder;
import org.codelibs.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
/**
* Creates a Lucene query that will filter for all documents that lie within the specified
* bounding box.
*
* This query can only operate on fields of type geo_point that have latitude and longitude
* enabled.
* */
public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder {
public static final String NAME = "geo_bounding_box";
public static final ParseField QUERY_NAME_FIELD = new ParseField(NAME, "geo_bbox");
/** Default type for executing this query (memory as of this writing). */
public static final GeoExecType DEFAULT_TYPE = GeoExecType.MEMORY;
/**
* The default value for ignore_unmapped.
*/
public static final boolean DEFAULT_IGNORE_UNMAPPED = false;
private static final ParseField TYPE_FIELD = new ParseField("type");
private static final ParseField VALIDATION_METHOD_FIELD = new ParseField("validation_method");
private static final ParseField COERCE_FIELD =new ParseField("coerce", "normalize")
.withAllDeprecated("validation_method");
private static final ParseField IGNORE_MALFORMED_FIELD = new ParseField("ignore_malformed")
.withAllDeprecated("validation_method");
private static final ParseField FIELD_FIELD = new ParseField("field");
private static final ParseField TOP_FIELD = new ParseField("top");
private static final ParseField BOTTOM_FIELD = new ParseField("bottom");
private static final ParseField LEFT_FIELD = new ParseField("left");
private static final ParseField RIGHT_FIELD = new ParseField("right");
private static final ParseField TOP_LEFT_FIELD = new ParseField("top_left");
private static final ParseField BOTTOM_RIGHT_FIELD = new ParseField("bottom_right");
private static final ParseField TOP_RIGHT_FIELD = new ParseField("top_right");
private static final ParseField BOTTOM_LEFT_FIELD = new ParseField("bottom_left");
private static final ParseField IGNORE_UNMAPPED_FIELD = new ParseField("ignore_unmapped");
/** Name of field holding geo coordinates to compute the bounding box on.*/
private final String fieldName;
/** Top left corner coordinates of bounding box. */
private GeoPoint topLeft = new GeoPoint(Double.NaN, Double.NaN);
/** Bottom right corner coordinates of bounding box.*/
private GeoPoint bottomRight = new GeoPoint(Double.NaN, Double.NaN);
/** How to deal with incorrect coordinates.*/
private GeoValidationMethod validationMethod = GeoValidationMethod.DEFAULT;
/** How the query should be run. */
private GeoExecType type = DEFAULT_TYPE;
private boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED;
/**
* Create new bounding box query.
* @param fieldName name of index field containing geo coordinates to operate on.
* */
public GeoBoundingBoxQueryBuilder(String fieldName) {
if (fieldName == null) {
throw new IllegalArgumentException("Field name must not be empty.");
}
this.fieldName = fieldName;
}
/**
* Read from a stream.
*/
public GeoBoundingBoxQueryBuilder(StreamInput in) throws IOException {
super(in);
fieldName = in.readString();
topLeft = in.readGeoPoint();
bottomRight = in.readGeoPoint();
type = GeoExecType.readFromStream(in);
validationMethod = GeoValidationMethod.readFromStream(in);
ignoreUnmapped = in.readBoolean();
}
@Override
protected void doWriteTo(StreamOutput out) throws IOException {
out.writeString(fieldName);
out.writeGeoPoint(topLeft);
out.writeGeoPoint(bottomRight);
type.writeTo(out);
validationMethod.writeTo(out);
out.writeBoolean(ignoreUnmapped);
}
/**
* Adds top left point.
* @param top The top latitude
* @param left The left longitude
* @param bottom The bottom latitude
* @param right The right longitude
*/
public GeoBoundingBoxQueryBuilder setCorners(double top, double left, double bottom, double right) {
if (GeoValidationMethod.isIgnoreMalformed(validationMethod) == false) {
if (Numbers.isValidDouble(top) == false) {
throw new IllegalArgumentException("top latitude is invalid: " + top);
}
if (Numbers.isValidDouble(left) == false) {
throw new IllegalArgumentException("left longitude is invalid: " + left);
}
if (Numbers.isValidDouble(bottom) == false) {
throw new IllegalArgumentException("bottom latitude is invalid: " + bottom);
}
if (Numbers.isValidDouble(right) == false) {
throw new IllegalArgumentException("right longitude is invalid: " + right);
}
// all corners are valid after above checks - make sure they are in the right relation
if (top < bottom) {
throw new IllegalArgumentException("top is below bottom corner: " +
top + " vs. " + bottom);
} else if (top == bottom) {
throw new IllegalArgumentException("top cannot be the same as bottom: " +
top + " == " + bottom);
} else if (left == right) {
throw new IllegalArgumentException("left cannot be the same as right: " +
left + " == " + right);
}
// we do not check longitudes as the query generation code can deal with flipped left/right values
}
topLeft.reset(top, left);
bottomRight.reset(bottom, right);
return this;
}
/**
* Adds points.
* @param topLeft topLeft point to add.
* @param bottomRight bottomRight point to add.
* */
public GeoBoundingBoxQueryBuilder setCorners(GeoPoint topLeft, GeoPoint bottomRight) {
return setCorners(topLeft.getLat(), topLeft.getLon(), bottomRight.getLat(), bottomRight.getLon());
}
/**
* Adds points from a single geohash.
* @param geohash The geohash for computing the bounding box.
*/
public GeoBoundingBoxQueryBuilder setCorners(final String geohash) {
// get the bounding box of the geohash and set topLeft and bottomRight
Rectangle ghBBox = GeoHashUtils.bbox(geohash);
return setCorners(new GeoPoint(ghBBox.maxLat, ghBBox.minLon), new GeoPoint(ghBBox.minLat, ghBBox.maxLon));
}
/**
* Adds points.
* @param topLeft topLeft point to add as geohash.
* @param bottomRight bottomRight point to add as geohash.
* */
public GeoBoundingBoxQueryBuilder setCorners(String topLeft, String bottomRight) {
return setCorners(GeoPoint.fromGeohash(topLeft), GeoPoint.fromGeohash(bottomRight));
}
/** Returns the top left corner of the bounding box. */
public GeoPoint topLeft() {
return topLeft;
}
/** Returns the bottom right corner of the bounding box. */
public GeoPoint bottomRight() {
return bottomRight;
}
/**
* Adds corners in OGC standard bbox/ envelop format.
*
* @param bottomLeft bottom left corner of bounding box.
* @param topRight top right corner of bounding box.
*/
public GeoBoundingBoxQueryBuilder setCornersOGC(GeoPoint bottomLeft, GeoPoint topRight) {
return setCorners(topRight.getLat(), bottomLeft.getLon(), bottomLeft.getLat(), topRight.getLon());
}
/**
* Adds corners in OGC standard bbox/ envelop format.
*
* @param bottomLeft bottom left corner geohash.
* @param topRight top right corner geohash.
*/
public GeoBoundingBoxQueryBuilder setCornersOGC(String bottomLeft, String topRight) {
return setCornersOGC(GeoPoint.fromGeohash(bottomLeft), GeoPoint.fromGeohash(topRight));
}
/**
* Specify whether or not to ignore validation errors of bounding boxes.
* Can only be set if coerce set to false, otherwise calling this
* method has no effect.
**/
public GeoBoundingBoxQueryBuilder setValidationMethod(GeoValidationMethod method) {
this.validationMethod = method;
return this;
}
/**
* Returns geo coordinate validation method to use.
* */
public GeoValidationMethod getValidationMethod() {
return this.validationMethod;
}
/**
* Sets the type of executing of the geo bounding box. Can be either `memory` or `indexed`. Defaults
* to `memory`.
*/
public GeoBoundingBoxQueryBuilder type(GeoExecType type) {
if (type == null) {
throw new IllegalArgumentException("Type is not allowed to be null.");
}
this.type = type;
return this;
}
/**
* For BWC: Parse type from type name.
* */
public GeoBoundingBoxQueryBuilder type(String type) {
this.type = GeoExecType.fromString(type);
return this;
}
/** Returns the execution type of the geo bounding box.*/
public GeoExecType type() {
return type;
}
/** Returns the name of the field to base the bounding box computation on. */
public String fieldName() {
return this.fieldName;
}
/**
* Sets whether the query builder should ignore unmapped fields (and run a
* {MatchNoDocsQuery} in place of this query) or throw an exception if
* the field is unmapped.
*/
public GeoBoundingBoxQueryBuilder ignoreUnmapped(boolean ignoreUnmapped) {
this.ignoreUnmapped = ignoreUnmapped;
return this;
}
/**
* Gets whether the query builder will ignore unmapped fields (and run a
* {MatchNoDocsQuery} in place of this query) or throw an exception if
* the field is unmapped.
*/
public boolean ignoreUnmapped() {
return ignoreUnmapped;
}
QueryValidationException checkLatLon(boolean indexCreatedBeforeV2_0) {
// validation was not available prior to 2.x, so to support bwc percolation queries we only ignore_malformed on 2.x created indexes
if (GeoValidationMethod.isIgnoreMalformed(validationMethod) || indexCreatedBeforeV2_0) {
return null;
}
QueryValidationException validationException = null;
// For everything post 2.0 validate latitude and longitude unless validation was explicitly turned off
if (GeoUtils.isValidLatitude(topLeft.getLat()) == false) {
validationException = addValidationError("top latitude is invalid: " + topLeft.getLat(),
validationException);
}
if (GeoUtils.isValidLongitude(topLeft.getLon()) == false) {
validationException = addValidationError("left longitude is invalid: " + topLeft.getLon(),
validationException);
}
if (GeoUtils.isValidLatitude(bottomRight.getLat()) == false) {
validationException = addValidationError("bottom latitude is invalid: " + bottomRight.getLat(),
validationException);
}
if (GeoUtils.isValidLongitude(bottomRight.getLon()) == false) {
validationException = addValidationError("right longitude is invalid: " + bottomRight.getLon(),
validationException);
}
return validationException;
}
@Override
public Query doToQuery(QueryShardContext context) {
throw new UnsupportedOperationException("querybuilders does not support this operation.");
}
@Override
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(NAME);
builder.startObject(fieldName);
builder.array(TOP_LEFT_FIELD.getPreferredName(), topLeft.getLon(), topLeft.getLat());
builder.array(BOTTOM_RIGHT_FIELD.getPreferredName(), bottomRight.getLon(), bottomRight.getLat());
builder.endObject();
builder.field(VALIDATION_METHOD_FIELD.getPreferredName(), validationMethod);
builder.field(TYPE_FIELD.getPreferredName(), type);
builder.field(IGNORE_UNMAPPED_FIELD.getPreferredName(), ignoreUnmapped);
printBoostAndQueryName(builder);
builder.endObject();
}
public static Optional fromXContent(QueryParseContext parseContext) throws IOException {
XContentParser parser = parseContext.parser();
String fieldName = null;
double top = Double.NaN;
double bottom = Double.NaN;
double left = Double.NaN;
double right = Double.NaN;
float boost = AbstractQueryBuilder.DEFAULT_BOOST;
String queryName = null;
String currentFieldName = null;
XContentParser.Token token;
boolean coerce = GeoValidationMethod.DEFAULT_LENIENT_PARSING;
boolean ignoreMalformed = GeoValidationMethod.DEFAULT_LENIENT_PARSING;
GeoValidationMethod validationMethod = null;
boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED;
GeoPoint sparse = new GeoPoint();
String type = "memory";
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token == XContentParser.Token.START_OBJECT) {
fieldName = currentFieldName;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
token = parser.nextToken();
if (parseContext.isDeprecatedSetting(currentFieldName)) {
// skip
} else if (FIELD_FIELD.match(currentFieldName)) {
fieldName = parser.text();
} else if (TOP_FIELD.match(currentFieldName)) {
top = parser.doubleValue();
} else if (BOTTOM_FIELD.match(currentFieldName)) {
bottom = parser.doubleValue();
} else if (LEFT_FIELD.match(currentFieldName)) {
left = parser.doubleValue();
} else if (RIGHT_FIELD.match(currentFieldName)) {
right = parser.doubleValue();
} else {
if (TOP_LEFT_FIELD.match(currentFieldName)) {
GeoUtils.parseGeoPoint(parser, sparse);
top = sparse.getLat();
left = sparse.getLon();
} else if (BOTTOM_RIGHT_FIELD.match(currentFieldName)) {
GeoUtils.parseGeoPoint(parser, sparse);
bottom = sparse.getLat();
right = sparse.getLon();
} else if (TOP_RIGHT_FIELD.match(currentFieldName)) {
GeoUtils.parseGeoPoint(parser, sparse);
top = sparse.getLat();
right = sparse.getLon();
} else if (BOTTOM_LEFT_FIELD.match(currentFieldName)) {
GeoUtils.parseGeoPoint(parser, sparse);
bottom = sparse.getLat();
left = sparse.getLon();
} else {
throw new ElasticsearchParseException("failed to parse [{}] query. unexpected field [{}]",
QUERY_NAME_FIELD.getPreferredName(), currentFieldName);
}
}
} else {
throw new ElasticsearchParseException("failed to parse [{}] query. field name expected but [{}] found",
QUERY_NAME_FIELD.getPreferredName(), token);
}
}
} else if (token.isValue()) {
if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName)) {
queryName = parser.text();
} else if (AbstractQueryBuilder.BOOST_FIELD.match(currentFieldName)) {
boost = parser.floatValue();
} else if (COERCE_FIELD.match(currentFieldName)) {
coerce = parser.booleanValue();
if (coerce) {
ignoreMalformed = true;
}
} else if (VALIDATION_METHOD_FIELD.match(currentFieldName)) {
validationMethod = GeoValidationMethod.fromString(parser.text());
} else if (IGNORE_UNMAPPED_FIELD.match(currentFieldName)) {
ignoreUnmapped = parser.booleanValue();
} else if (TYPE_FIELD.match(currentFieldName)) {
type = parser.text();
} else if (IGNORE_MALFORMED_FIELD.match(currentFieldName)) {
ignoreMalformed = parser.booleanValue();
} else {
throw new ParsingException(parser.getTokenLocation(), "failed to parse [{}] query. unexpected field [{}]",
QUERY_NAME_FIELD.getPreferredName(), currentFieldName);
}
}
}
final GeoPoint topLeft = sparse.reset(top, left); //just keep the object
final GeoPoint bottomRight = new GeoPoint(bottom, right);
GeoBoundingBoxQueryBuilder builder = new GeoBoundingBoxQueryBuilder(fieldName);
builder.setCorners(topLeft, bottomRight);
builder.queryName(queryName);
builder.boost(boost);
builder.type(GeoExecType.fromString(type));
builder.ignoreUnmapped(ignoreUnmapped);
if (validationMethod != null) {
// ignore deprecated coerce/ignoreMalformed settings if validationMethod is set
builder.setValidationMethod(validationMethod);
} else {
builder.setValidationMethod(GeoValidationMethod.infer(coerce, ignoreMalformed));
}
return Optional.of(builder);
}
@Override
protected boolean doEquals(GeoBoundingBoxQueryBuilder other) {
return Objects.equals(topLeft, other.topLeft) &&
Objects.equals(bottomRight, other.bottomRight) &&
Objects.equals(type, other.type) &&
Objects.equals(validationMethod, other.validationMethod) &&
Objects.equals(fieldName, other.fieldName) &&
Objects.equals(ignoreUnmapped, other.ignoreUnmapped);
}
@Override
protected int doHashCode() {
return Objects.hash(topLeft, bottomRight, type, validationMethod, fieldName, ignoreUnmapped);
}
@Override
public String getWriteableName() {
return NAME;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy