From 133f848e9f514a8c87b9cfa090c934efa4550a51 Mon Sep 17 00:00:00 2001 From: Nick Knize Date: Wed, 24 Jul 2019 15:41:31 -0500 Subject: [PATCH] [Geo] Refactor GeoShapeQueryBuilder to derive from AbstractGeometryQueryBuilder (#44780) Refactors GeoShapeQueryBuilder to derive from a new AbstractGeometryQueryBuilder that provides common parsing and build logic for spatial geometries. This will allow development of custom geometry queries by extending AbstractGeometryQueryBuilder preventing duplication of common spatial query logic. --- .../query/AbstractGeometryQueryBuilder.java | 640 +++++++++++++ .../index/query/GeoShapeQueryBuilder.java | 905 +++++------------- 2 files changed, 899 insertions(+), 646 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/index/query/AbstractGeometryQueryBuilder.java diff --git a/server/src/main/java/org/elasticsearch/index/query/AbstractGeometryQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/AbstractGeometryQueryBuilder.java new file mode 100644 index 0000000000000..53e353db92289 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/AbstractGeometryQueryBuilder.java @@ -0,0 +1,640 @@ +/* + * 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.elasticsearch.index.query; + +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.geo.GeoJson; +import org.elasticsearch.common.geo.GeometryIO; +import org.elasticsearch.common.geo.GeometryParser; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.geo.geometry.Geometry; +import org.elasticsearch.index.mapper.MappedFieldType; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Base {@link QueryBuilder} that builds a Geometry Query + */ +public abstract class AbstractGeometryQueryBuilder> extends AbstractQueryBuilder { + + static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Types are deprecated in [geo_shape] queries. " + + "The type should no longer be specified in the [indexed_shape] section."; + + public static final String DEFAULT_SHAPE_INDEX_NAME = "shapes"; + public static final String DEFAULT_SHAPE_FIELD_NAME = "shape"; + public static final ShapeRelation DEFAULT_SHAPE_RELATION = ShapeRelation.INTERSECTS; + + /** registry of content types this query can be used with */ + protected final List validContentTypes = new ArrayList<>(validContentTypes()); + + /** The default value for ignore_unmapped. */ + public static final boolean DEFAULT_IGNORE_UNMAPPED = false; + + protected static final ParseField SHAPE_FIELD = new ParseField("shape"); + protected static final ParseField RELATION_FIELD = new ParseField("relation"); + protected static final ParseField INDEXED_SHAPE_FIELD = new ParseField("indexed_shape"); + protected static final ParseField SHAPE_ID_FIELD = new ParseField("id"); + protected static final ParseField SHAPE_TYPE_FIELD = new ParseField("type"); + protected static final ParseField SHAPE_INDEX_FIELD = new ParseField("index"); + protected static final ParseField SHAPE_PATH_FIELD = new ParseField("path"); + protected static final ParseField SHAPE_ROUTING_FIELD = new ParseField("routing"); + protected static final ParseField IGNORE_UNMAPPED_FIELD = new ParseField("ignore_unmapped"); + + protected final String fieldName; + protected final Supplier supplier; + + protected final String indexedShapeId; + protected final String indexedShapeType; + + protected Geometry shape; + protected String indexedShapeIndex = DEFAULT_SHAPE_INDEX_NAME; + protected String indexedShapePath = DEFAULT_SHAPE_FIELD_NAME; + protected String indexedShapeRouting; + + protected ShapeRelation relation = DEFAULT_SHAPE_RELATION; + + protected boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED; + + /** + * Creates a new ShapeQueryBuilder whose Query will be against the given + * field name using the given Shape + * + * @param fieldName + * Name of the field that will be queried + * @param shape + * Shape used in the Query + * @deprecated use {@link #AbstractGeometryQueryBuilder(String, Geometry)} instead + */ + @Deprecated + protected AbstractGeometryQueryBuilder(String fieldName, ShapeBuilder shape) { + this(fieldName, shape == null ? null : shape.buildGeometry(), null, null); + } + + /** + * Creates a new AbstractGeometryQueryBuilder whose Query will be against the given + * field name using the given Shape + * + * @param fieldName + * Name of the field that will be queried + * @param shape + * Shape used in the Query + */ + public AbstractGeometryQueryBuilder(String fieldName, Geometry shape) { + this(fieldName, shape, null, null); + } + + /** + * Creates a new ShapeQueryBuilder whose Query will be against the given + * field name and will use the Shape found with the given ID + * + * @param fieldName + * Name of the field that will be filtered + * @param indexedShapeId + * ID of the indexed Shape that will be used in the Query + */ + protected AbstractGeometryQueryBuilder(String fieldName, String indexedShapeId) { + this(fieldName, (Geometry) null, indexedShapeId, null); + } + + /** + * Creates a new AbstractGeometryQueryBuilder whose Query will be against the given + * field name and will use the Shape found with the given ID in the given + * type + * + * @param fieldName + * Name of the field that will be filtered + * @param indexedShapeId + * ID of the indexed Shape that will be used in the Query + * @param indexedShapeType + * Index type of the indexed Shapes + * @deprecated use {@link #AbstractGeometryQueryBuilder(String, String)} instead + */ + @Deprecated + protected AbstractGeometryQueryBuilder(String fieldName, String indexedShapeId, String indexedShapeType) { + this(fieldName, (Geometry) null, indexedShapeId, indexedShapeType); + } + + protected AbstractGeometryQueryBuilder(String fieldName, Geometry shape, String indexedShapeId, @Nullable String indexedShapeType) { + if (fieldName == null) { + throw new IllegalArgumentException("fieldName is required"); + } + if (shape == null && indexedShapeId == null) { + throw new IllegalArgumentException("either shape or indexedShapeId is required"); + } + this.fieldName = fieldName; + this.shape = shape; + this.indexedShapeId = indexedShapeId; + this.indexedShapeType = indexedShapeType; + this.supplier = null; + } + + protected AbstractGeometryQueryBuilder(String fieldName, Supplier supplier, String indexedShapeId, + @Nullable String indexedShapeType) { + this.fieldName = fieldName; + this.shape = null; + this.supplier = supplier; + this.indexedShapeId = indexedShapeId; + this.indexedShapeType = indexedShapeType; + } + + /** + * Read from a stream. + */ + protected AbstractGeometryQueryBuilder(StreamInput in) throws IOException { + super(in); + fieldName = in.readString(); + if (in.readBoolean()) { + shape = GeometryIO.readGeometry(in); + indexedShapeId = null; + indexedShapeType = null; + } else { + shape = null; + indexedShapeId = in.readOptionalString(); + indexedShapeType = in.readOptionalString(); + indexedShapeIndex = in.readOptionalString(); + indexedShapePath = in.readOptionalString(); + if (in.getVersion().onOrAfter(Version.V_6_4_0)) { + indexedShapeRouting = in.readOptionalString(); + } else { + indexedShapeRouting = null; + } + } + relation = ShapeRelation.readFromStream(in); + ignoreUnmapped = in.readBoolean(); + supplier = null; + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + if (supplier != null) { + throw new IllegalStateException("supplier must be null, can't serialize suppliers, missing a rewriteAndFetch?"); + } + out.writeString(fieldName); + boolean hasShape = shape != null; + out.writeBoolean(hasShape); + if (hasShape) { + GeometryIO.writeGeometry(out, shape); + } else { + out.writeOptionalString(indexedShapeId); + out.writeOptionalString(indexedShapeType); + out.writeOptionalString(indexedShapeIndex); + out.writeOptionalString(indexedShapePath); + if (out.getVersion().onOrAfter(Version.V_6_4_0)) { + out.writeOptionalString(indexedShapeRouting); + } else if (indexedShapeRouting != null) { + throw new IllegalStateException("indexed shape routing cannot be serialized to older nodes"); + } + } + relation.writeTo(out); + out.writeBoolean(ignoreUnmapped); + } + + /** + * @return the name of the field that will be queried + */ + public String fieldName() { + return fieldName; + } + + /** + * Sets the shapeBuilder for the query shape. + * + * @param geometry the geometry + * @return this + */ + public QB shape(Geometry geometry) { + if (geometry == null) { + throw new IllegalArgumentException("No geometry defined"); + } + this.shape = geometry; + return (QB)this; + } + + /** + * @return the shape used in the Query + */ + public Geometry shape() { + return shape; + } + + /** + * @return the ID of the indexed Shape that will be used in the Query + */ + public String indexedShapeId() { + return indexedShapeId; + } + + /** + * @return the document type of the indexed Shape that will be used in the + * Query + * + * @deprecated Types are in the process of being removed. + */ + @Deprecated + public String indexedShapeType() { + return indexedShapeType; + } + + /** + * Sets the name of the index where the indexed Shape can be found + * + * @param indexedShapeIndex Name of the index where the indexed Shape is + * @return this + */ + public QB indexedShapeIndex(String indexedShapeIndex) { + this.indexedShapeIndex = indexedShapeIndex; + return (QB)this; + } + + /** + * @return the index name for the indexed Shape that will be used in the + * Query + */ + public String indexedShapeIndex() { + return indexedShapeIndex; + } + + /** + * Sets the path of the field in the indexed Shape document that has the Shape itself + * + * @param indexedShapePath Path of the field where the Shape itself is defined + * @return this + */ + public QB indexedShapePath(String indexedShapePath) { + this.indexedShapePath = indexedShapePath; + return (QB)this; + } + + /** + * @return the path of the indexed Shape that will be used in the Query + */ + public String indexedShapePath() { + return indexedShapePath; + } + + /** + * Sets the optional routing to the indexed Shape that will be used in the query + * + * @param indexedShapeRouting indexed shape routing + * @return this + */ + public QB indexedShapeRouting(String indexedShapeRouting) { + this.indexedShapeRouting = indexedShapeRouting; + return (QB)this; + } + + + /** + * @return the optional routing to the indexed Shape that will be used in the + * Query + */ + public String indexedShapeRouting() { + return indexedShapeRouting; + } + + /** + * Sets the relation of query shape and indexed shape. + * + * @param relation relation of the shapes + * @return this + */ + public QB relation(ShapeRelation relation) { + if (relation == null) { + throw new IllegalArgumentException("No Shape Relation defined"); + } + this.relation = relation; + return (QB)this; + } + + /** + * @return the relation of query shape and indexed shape to use in the Query + */ + public ShapeRelation relation() { + return relation; + } + + /** + * Sets whether the query builder should ignore unmapped fields (and run a + * {@link MatchNoDocsQuery} in place of this query) or throw an exception if + * the field is unmapped. + */ + public AbstractGeometryQueryBuilder ignoreUnmapped(boolean ignoreUnmapped) { + this.ignoreUnmapped = ignoreUnmapped; + return this; + } + + /** + * Gets whether the query builder will ignore unmapped fields (and run a + * {@link MatchNoDocsQuery} in place of this query) or throw an exception if + * the field is unmapped. + */ + public boolean ignoreUnmapped() { + return ignoreUnmapped; + } + + /** list of content types this shape query is compatible with */ + protected abstract List validContentTypes(); + /** builds the appropriate lucene shape query */ + protected abstract Query buildShapeQuery(QueryShardContext context, MappedFieldType fieldType); + /** returns expected content type for this query */ + protected abstract String queryFieldType(); + /** writes the xcontent specific to this shape query */ + protected abstract void doShapeQueryXContent(XContentBuilder builder, Params params) throws IOException; + /** creates a new ShapeQueryBuilder from the provided field name and shape builder */ + protected abstract AbstractGeometryQueryBuilder newShapeQueryBuilder(String fieldName, Geometry shape); + /** creates a new ShapeQueryBuilder from the provided field name, supplier, indexed shape id, and indexed shape type */ + protected abstract AbstractGeometryQueryBuilder newShapeQueryBuilder(String fieldName, Supplier shapeSupplier, + String indexedShapeId, String indexedShapeType); + + /** returns true if the provided field type is valid for this query */ + protected boolean isValidContentType(String typeName) { + return validContentTypes.contains(typeName); + } + + @Override + protected Query doToQuery(QueryShardContext context) { + if (shape == null || supplier != null) { + throw new UnsupportedOperationException("query must be rewritten first"); + } + final MappedFieldType fieldType = context.fieldMapper(fieldName); + if (fieldType == null) { + if (ignoreUnmapped) { + return new MatchNoDocsQuery(); + } else { + throw new QueryShardException(context, "failed to find " + queryFieldType() + " field [" + fieldName + "]"); + } + } + + return buildShapeQuery(context, fieldType); + } + + /** + * Fetches the Shape with the given ID in the given type and index. + * + * @param getRequest + * GetRequest containing index, type and id + * @param path + * Name or path of the field in the Shape Document where the + * Shape itself is located + */ + private void fetch(Client client, GetRequest getRequest, String path, ActionListener listener) { + getRequest.preference("_local"); + client.get(getRequest, new ActionListener(){ + + @Override + public void onResponse(GetResponse response) { + try { + if (!response.isExists()) { + throw new IllegalArgumentException("Shape with ID [" + getRequest.id() + "] in type [" + getRequest.type() + + "] not found"); + } + if (response.isSourceEmpty()) { + throw new IllegalArgumentException("Shape with ID [" + getRequest.id() + "] in type [" + getRequest.type() + + "] source disabled"); + } + + String[] pathElements = path.split("\\."); + int currentPathSlot = 0; + + // It is safe to use EMPTY here because this never uses namedObject + try (XContentParser parser = XContentHelper + .createParser(NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, response.getSourceAsBytesRef())) { + XContentParser.Token currentToken; + while ((currentToken = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (currentToken == XContentParser.Token.FIELD_NAME) { + if (pathElements[currentPathSlot].equals(parser.currentName())) { + parser.nextToken(); + if (++currentPathSlot == pathElements.length) { + listener.onResponse(new GeometryParser(true, true, true).parse(parser)); + return; + } + } else { + parser.nextToken(); + parser.skipChildren(); + } + } + } + throw new IllegalStateException("Shape with name [" + getRequest.id() + "] found but missing " + path + " field"); + } + } catch (Exception e) { + onFailure(e); + } + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }); + } + + @Override + protected void doXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(getWriteableName()); + + builder.startObject(fieldName); + + if (shape != null) { + builder.field(SHAPE_FIELD.getPreferredName()); + GeoJson.toXContent(shape, builder, params); + } else { + builder.startObject(INDEXED_SHAPE_FIELD.getPreferredName()) + .field(SHAPE_ID_FIELD.getPreferredName(), indexedShapeId); + if (indexedShapeType != null) { + builder.field(SHAPE_TYPE_FIELD.getPreferredName(), indexedShapeType); + } + if (indexedShapeIndex != null) { + builder.field(SHAPE_INDEX_FIELD.getPreferredName(), indexedShapeIndex); + } + if (indexedShapePath != null) { + builder.field(SHAPE_PATH_FIELD.getPreferredName(), indexedShapePath); + } + if (indexedShapeRouting != null) { + builder.field(SHAPE_ROUTING_FIELD.getPreferredName(), indexedShapeRouting); + } + builder.endObject(); + } + + if(relation != null) { + builder.field(RELATION_FIELD.getPreferredName(), relation.getRelationName()); + } + + doShapeQueryXContent(builder, params); + builder.endObject(); + builder.field(IGNORE_UNMAPPED_FIELD.getPreferredName(), ignoreUnmapped); + + printBoostAndQueryName(builder); + + builder.endObject(); + } + + @Override + protected boolean doEquals(AbstractGeometryQueryBuilder other) { + return Objects.equals(fieldName, other.fieldName) + && Objects.equals(indexedShapeId, other.indexedShapeId) + && Objects.equals(indexedShapeIndex, other.indexedShapeIndex) + && Objects.equals(indexedShapePath, other.indexedShapePath) + && Objects.equals(indexedShapeType, other.indexedShapeType) + && Objects.equals(indexedShapeRouting, other.indexedShapeRouting) + && Objects.equals(relation, other.relation) + && Objects.equals(shape, other.shape) + && Objects.equals(supplier, other.supplier) + && Objects.equals(ignoreUnmapped, other.ignoreUnmapped); + } + + @Override + protected int doHashCode() { + return Objects.hash(fieldName, indexedShapeId, indexedShapeIndex, + indexedShapePath, indexedShapeType, indexedShapeRouting, relation, shape, ignoreUnmapped, supplier); + } + + @Override + protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { + if (supplier != null) { + return supplier.get() == null ? this : newShapeQueryBuilder(this.fieldName, supplier.get()).relation(relation); + } else if (this.shape == null) { + SetOnce supplier = new SetOnce<>(); + queryRewriteContext.registerAsyncAction((client, listener) -> { + GetRequest getRequest; + if (indexedShapeType == null) { + getRequest = new GetRequest(indexedShapeIndex, indexedShapeId); + } else { + getRequest = new GetRequest(indexedShapeIndex, indexedShapeType, indexedShapeId); + } + getRequest.routing(indexedShapeRouting); + fetch(client, getRequest, indexedShapePath, ActionListener.wrap(builder-> { + supplier.set(builder); + listener.onResponse(null); + }, listener::onFailure)); + }); + return newShapeQueryBuilder(this.fieldName, supplier::get, this.indexedShapeId, this.indexedShapeType).relation(relation); + } + return this; + } + + /** local class that encapsulates xcontent parsed shape parameters */ + protected abstract static class ParsedShapeQueryParams { + public String fieldName; + public ShapeRelation relation; + public ShapeBuilder shape; + + public String id = null; + public String type = null; + public String index = null; + public String shapePath = null; + public String shapeRouting = null; + + public float boost; + public String queryName; + public boolean ignoreUnmapped; + + protected abstract boolean parseXContentField(XContentParser parser) throws IOException; + } + + public static ParsedShapeQueryParams parsedParamsFromXContent(XContentParser parser, ParsedShapeQueryParams params) + throws IOException { + String fieldName = null; + XContentParser.Token token; + String currentFieldName = null; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + if (fieldName != null) { + throw new ParsingException(parser.getTokenLocation(), "point specified twice. [" + currentFieldName + "]"); + } + fieldName = currentFieldName; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + token = parser.nextToken(); + if (RELATION_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + params.relation = ShapeRelation.getRelationByName(parser.text()); + if (params.relation == null) { + throw new ParsingException(parser.getTokenLocation(), "Unknown shape operation [" + parser.text() + " ]"); + } + } else if (params.parseXContentField(parser)) { + continue; + } else if (INDEXED_SHAPE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (SHAPE_ID_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + params.id = parser.text(); + } else if (SHAPE_TYPE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + params.type = parser.text(); + } else if (SHAPE_INDEX_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + params.index = parser.text(); + } else if (SHAPE_PATH_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + params.shapePath = parser.text(); + } else if (SHAPE_ROUTING_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + params.shapeRouting = parser.text(); + } + } else { + throw new ParsingException(parser.getTokenLocation(), "unknown token [" + token + + "] after [" + currentFieldName + "]"); + } + } + } else { + throw new ParsingException(parser.getTokenLocation(), "query does not support [" + currentFieldName + "]"); + } + } + } + } else if (token.isValue()) { + if (AbstractQueryBuilder.BOOST_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + params.boost = parser.floatValue(); + } else if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + params.queryName = parser.text(); + } else if (IGNORE_UNMAPPED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + params.ignoreUnmapped = parser.booleanValue(); + } else { + throw new ParsingException(parser.getTokenLocation(), "query does not support [" + currentFieldName + "]"); + } + } + } + params.fieldName = fieldName; + return params; + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java index ddc1f622f51e2..dc015f0c6d866 100644 --- a/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java @@ -32,20 +32,11 @@ import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy; import org.apache.lucene.spatial.query.SpatialArgs; import org.apache.lucene.spatial.query.SpatialOperation; -import org.apache.lucene.util.SetOnce; -import org.elasticsearch.Version; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.get.GetRequest; -import org.elasticsearch.action.get.GetResponse; -import org.elasticsearch.client.Client; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; -import org.elasticsearch.common.geo.GeoJson; import org.elasticsearch.common.geo.GeoShapeType; -import org.elasticsearch.common.geo.GeometryIO; import org.elasticsearch.common.geo.GeometryIndexer; -import org.elasticsearch.common.geo.GeometryParser; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.SpatialStrategy; import org.elasticsearch.common.geo.builders.EnvelopeBuilder; @@ -61,10 +52,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.logging.DeprecationLogger; -import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; -import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.geo.geometry.Circle; import org.elasticsearch.geo.geometry.Geometry; @@ -77,6 +65,7 @@ import org.elasticsearch.geo.geometry.Point; import org.elasticsearch.geo.geometry.Rectangle; import org.elasticsearch.index.mapper.BaseGeoShapeFieldMapper; +import org.elasticsearch.index.mapper.GeoShapeFieldMapper; import org.elasticsearch.index.mapper.LegacyGeoShapeFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.locationtech.jts.geom.Coordinate; @@ -84,6 +73,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.Supplier; @@ -91,54 +81,17 @@ import static org.elasticsearch.index.mapper.GeoShapeFieldMapper.toLucenePolygon; /** - * {@link QueryBuilder} that builds a GeoShape Query + * Derived {@link AbstractGeometryQueryBuilder} that builds a lat, lon GeoShape Query */ -public class GeoShapeQueryBuilder extends AbstractQueryBuilder { +public class GeoShapeQueryBuilder extends AbstractGeometryQueryBuilder { public static final String NAME = "geo_shape"; private static final DeprecationLogger deprecationLogger = new DeprecationLogger( LogManager.getLogger(GeoShapeQueryBuilder.class)); - static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Types are deprecated in [geo_shape] queries. " + - "The type should no longer be specified in the [indexed_shape] section."; - public static final String DEFAULT_SHAPE_INDEX_NAME = "shapes"; - public static final String DEFAULT_SHAPE_FIELD_NAME = "shape"; - public static final ShapeRelation DEFAULT_SHAPE_RELATION = ShapeRelation.INTERSECTS; - - /** - * The default value for ignore_unmapped. - */ - public static final boolean DEFAULT_IGNORE_UNMAPPED = false; - - private static final ParseField SHAPE_FIELD = new ParseField("shape"); - private static final ParseField STRATEGY_FIELD = new ParseField("strategy"); - private static final ParseField RELATION_FIELD = new ParseField("relation"); - private static final ParseField INDEXED_SHAPE_FIELD = new ParseField("indexed_shape"); - private static final ParseField SHAPE_ID_FIELD = new ParseField("id"); - private static final ParseField SHAPE_TYPE_FIELD = new ParseField("type"); - private static final ParseField SHAPE_INDEX_FIELD = new ParseField("index"); - private static final ParseField SHAPE_PATH_FIELD = new ParseField("path"); - private static final ParseField SHAPE_ROUTING_FIELD = new ParseField("routing"); - private static final ParseField IGNORE_UNMAPPED_FIELD = new ParseField("ignore_unmapped"); - - private final String fieldName; - - private final Geometry shape; - private final Supplier supplier; + protected static final ParseField STRATEGY_FIELD = new ParseField("strategy"); private SpatialStrategy strategy; - private final String indexedShapeId; - private final String indexedShapeType; - - - private String indexedShapeIndex = DEFAULT_SHAPE_INDEX_NAME; - private String indexedShapePath = DEFAULT_SHAPE_FIELD_NAME; - private String indexedShapeRouting; - - private ShapeRelation relation = DEFAULT_SHAPE_RELATION; - - private boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED; - /** * Creates a new GeoShapeQueryBuilder whose Query will be against the given * field name using the given Shape @@ -147,12 +100,9 @@ public class GeoShapeQueryBuilder extends AbstractQueryBuilder shapeSupplier, String indexedShapeId, + @Nullable String indexedShapeType) { + super(fieldName, shapeSupplier, indexedShapeId, indexedShapeType); } /** @@ -197,116 +141,36 @@ public GeoShapeQueryBuilder(String fieldName, String indexedShapeId) { */ @Deprecated public GeoShapeQueryBuilder(String fieldName, String indexedShapeId, String indexedShapeType) { - this(fieldName, (Geometry) null, indexedShapeId, indexedShapeType); - } - - private GeoShapeQueryBuilder(String fieldName, Geometry shape, String indexedShapeId, @Nullable String indexedShapeType) { - if (fieldName == null) { - throw new IllegalArgumentException("fieldName is required"); - } - if (shape == null && indexedShapeId == null) { - throw new IllegalArgumentException("either shape or indexedShapeId is required"); - } - this.fieldName = fieldName; - this.shape = shape; - this.indexedShapeId = indexedShapeId; - this.indexedShapeType = indexedShapeType; - this.supplier = null; - } - - private GeoShapeQueryBuilder(String fieldName, Supplier supplier, String indexedShapeId, - @Nullable String indexedShapeType) { - this.fieldName = fieldName; - this.shape = null; - this.supplier = supplier; - this.indexedShapeId = indexedShapeId; - this.indexedShapeType = indexedShapeType; + super(fieldName, indexedShapeId, indexedShapeType); } /** - * Read from a stream. + * Creates a new GeoShapeQueryBuilder whose Query will be against the given + * field name and will use the Shape found with the given ID + * + * @param fieldName + * Name of the field that will be filtered + * @param indexedShapeId + * ID of the indexed Shape that will be used in the Query */ + public GeoShapeQueryBuilder(String fieldName, String indexedShapeId) { + super(fieldName, indexedShapeId); + } + public GeoShapeQueryBuilder(StreamInput in) throws IOException { super(in); - fieldName = in.readString(); - if (in.readBoolean()) { - shape = GeometryIO.readGeometry(in); - indexedShapeId = null; - indexedShapeType = null; - } else { - shape = null; - indexedShapeId = in.readOptionalString(); - indexedShapeType = in.readOptionalString(); - indexedShapeIndex = in.readOptionalString(); - indexedShapePath = in.readOptionalString(); - if (in.getVersion().onOrAfter(Version.V_6_4_0)) { - indexedShapeRouting = in.readOptionalString(); - } else { - indexedShapeRouting = null; - } - } - relation = ShapeRelation.readFromStream(in); strategy = in.readOptionalWriteable(SpatialStrategy::readFromStream); - ignoreUnmapped = in.readBoolean(); - supplier = null; } @Override protected void doWriteTo(StreamOutput out) throws IOException { - if (supplier != null) { - throw new IllegalStateException("supplier must be null, can't serialize suppliers, missing a rewriteAndFetch?"); - } - out.writeString(fieldName); - boolean hasShape = shape != null; - out.writeBoolean(hasShape); - if (hasShape) { - GeometryIO.writeGeometry(out, shape);; - } else { - out.writeOptionalString(indexedShapeId); - out.writeOptionalString(indexedShapeType); - out.writeOptionalString(indexedShapeIndex); - out.writeOptionalString(indexedShapePath); - if (out.getVersion().onOrAfter(Version.V_6_4_0)) { - out.writeOptionalString(indexedShapeRouting); - } else if (indexedShapeRouting != null) { - throw new IllegalStateException("indexed shape routing cannot be serialized to older nodes"); - } - } - relation.writeTo(out); + super.doWriteTo(out); out.writeOptionalWriteable(strategy); - out.writeBoolean(ignoreUnmapped); - } - - /** - * @return the name of the field that will be queried - */ - public String fieldName() { - return fieldName; } - /** - * @return the shape used in the Query - */ - public Geometry shape() { - return shape; - } - - /** - * @return the ID of the indexed Shape that will be used in the Query - */ - public String indexedShapeId() { - return indexedShapeId; - } - - /** - * @return the document type of the indexed Shape that will be used in the - * Query - * - * @deprecated Types are in the process of being removed. - */ - @Deprecated - public String indexedShapeType() { - return indexedShapeType; + @Override + public String getWriteableName() { + return NAME; } /** @@ -321,12 +185,11 @@ public String indexedShapeType() { public GeoShapeQueryBuilder strategy(SpatialStrategy strategy) { if (strategy != null && strategy == SpatialStrategy.TERM && relation != ShapeRelation.INTERSECTS) { throw new IllegalArgumentException("strategy [" + strategy.getStrategyName() + "] only supports relation [" - + ShapeRelation.INTERSECTS.getRelationName() + "] found relation [" + relation.getRelationName() + "]"); + + ShapeRelation.INTERSECTS.getRelationName() + "] found relation [" + relation.getRelationName() + "]"); } this.strategy = strategy; return this; } - /** * @return The spatial strategy to use for building the geo shape Query */ @@ -334,122 +197,39 @@ public SpatialStrategy strategy() { return strategy; } - /** - * Sets the name of the index where the indexed Shape can be found - * - * @param indexedShapeIndex Name of the index where the indexed Shape is - * @return this - */ - public GeoShapeQueryBuilder indexedShapeIndex(String indexedShapeIndex) { - this.indexedShapeIndex = indexedShapeIndex; - return this; - } - - /** - * @return the index name for the indexed Shape that will be used in the - * Query - */ - public String indexedShapeIndex() { - return indexedShapeIndex; - } - - /** - * Sets the path of the field in the indexed Shape document that has the Shape itself - * - * @param indexedShapePath Path of the field where the Shape itself is defined - * @return this - */ - public GeoShapeQueryBuilder indexedShapePath(String indexedShapePath) { - this.indexedShapePath = indexedShapePath; - return this; - } - - /** - * @return the path of the indexed Shape that will be used in the Query - */ - public String indexedShapePath() { - return indexedShapePath; - } - - /** - * Sets the optional routing to the indexed Shape that will be used in the query - * - * @param indexedShapeRouting indexed shape routing - * @return this - */ - public GeoShapeQueryBuilder indexedShapeRouting(String indexedShapeRouting) { - this.indexedShapeRouting = indexedShapeRouting; - return this; + @Override + protected List validContentTypes() { + return Arrays.asList(BaseGeoShapeFieldMapper.CONTENT_TYPE); } - - /** - * @return the optional routing to the indexed Shape that will be used in the - * Query - */ - public String indexedShapeRouting() { - return indexedShapeRouting; + @Override + public String queryFieldType() { + return BaseGeoShapeFieldMapper.CONTENT_TYPE; } - /** - * Sets the relation of query shape and indexed shape. - * - * @param relation relation of the shapes - * @return this - */ - public GeoShapeQueryBuilder relation(ShapeRelation relation) { - if (relation == null) { - throw new IllegalArgumentException("No Shape Relation defined"); - } - if (SpatialStrategy.TERM.equals(strategy) && relation != ShapeRelation.INTERSECTS) { - throw new IllegalArgumentException("current strategy [" + strategy.getStrategyName() + "] only supports relation [" - + ShapeRelation.INTERSECTS.getRelationName() + "] found relation [" + relation.getRelationName() + "]"); + @Override + public void doShapeQueryXContent(XContentBuilder builder, Params params) throws IOException { + if (strategy != null) { + builder.field(STRATEGY_FIELD.getPreferredName(), strategy.getStrategyName()); } - this.relation = relation; - return this; - } - - /** - * @return the relation of query shape and indexed shape to use in the Query - */ - public ShapeRelation relation() { - return relation; } - /** - * Sets whether the query builder should ignore unmapped fields (and run a - * {@link MatchNoDocsQuery} in place of this query) or throw an exception if - * the field is unmapped. - */ - public GeoShapeQueryBuilder ignoreUnmapped(boolean ignoreUnmapped) { - this.ignoreUnmapped = ignoreUnmapped; - return this; + @Override + protected GeoShapeQueryBuilder newShapeQueryBuilder(String fieldName, Geometry shape) { + return new GeoShapeQueryBuilder(fieldName, shape); } - /** - * Gets whether the query builder will ignore unmapped fields (and run a - * {@link MatchNoDocsQuery} in place of this query) or throw an exception if - * the field is unmapped. - */ - public boolean ignoreUnmapped() { - return ignoreUnmapped; + @Override + protected GeoShapeQueryBuilder newShapeQueryBuilder(String fieldName, Supplier shapeSupplier, + String indexedShapeId, String indexedShapeType) { + return new GeoShapeQueryBuilder(fieldName, shapeSupplier, indexedShapeId, indexedShapeType); } @Override - protected Query doToQuery(QueryShardContext context) { - if (shape == null || supplier != null) { - throw new UnsupportedOperationException("query must be rewritten first"); - } - final MappedFieldType fieldType = context.fieldMapper(fieldName); - if (fieldType == null) { - if (ignoreUnmapped) { - return new MatchNoDocsQuery(); - } else { - throw new QueryShardException(context, "failed to find geo_shape field [" + fieldName + "]"); - } - } else if (fieldType.typeName().equals(BaseGeoShapeFieldMapper.CONTENT_TYPE) == false) { + public Query buildShapeQuery(QueryShardContext context, MappedFieldType fieldType) { + if (fieldType.typeName().equals(BaseGeoShapeFieldMapper.CONTENT_TYPE) == false) { throw new QueryShardException(context, - "Field [" + fieldName + "] is not of type [geo_shape] but of type [" + fieldType.typeName() + "]"); + "Field [" + fieldName + "] is not of type [" + queryFieldType() + "] but of type [" + fieldType.typeName() + "]"); } final BaseGeoShapeFieldMapper.BaseGeoShapeFieldType ft = (BaseGeoShapeFieldMapper.BaseGeoShapeFieldType) fieldType; @@ -480,13 +260,41 @@ protected Query doToQuery(QueryShardContext context) { return query; } + public static SpatialArgs getArgs(Geometry shape, ShapeRelation relation) { + switch (relation) { + case DISJOINT: + return new SpatialArgs(SpatialOperation.IsDisjointTo, buildS4J(shape)); + case INTERSECTS: + return new SpatialArgs(SpatialOperation.Intersects, buildS4J(shape)); + case WITHIN: + return new SpatialArgs(SpatialOperation.IsWithin, buildS4J(shape)); + case CONTAINS: + return new SpatialArgs(SpatialOperation.Contains, buildS4J(shape)); + default: + throw new IllegalArgumentException("invalid relation [" + relation + "]"); + } + } + + /** + * Builds JTS shape from a geometry + * + * This method is needed to handle legacy indices and will be removed when we no longer need to build JTS shapes + */ + private static Shape buildS4J(Geometry geometry) { + return geometryToShapeBuilder(geometry).buildS4J(); + } + private Query getVectorQuery(QueryShardContext context, Geometry queryShape) { // CONTAINS queries are not yet supported by VECTOR strategy if (relation == ShapeRelation.CONTAINS) { throw new QueryShardException(context, ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "]"); } + // wrap geoQuery as a ConstantScoreQuery + return getVectorQueryFromShape(context, queryShape); + } + protected Query getVectorQueryFromShape(QueryShardContext context, Geometry queryShape) { // TODO: Move this to QueryShardContext GeometryIndexer geometryIndexer = new GeometryIndexer(true); @@ -495,463 +303,268 @@ private Query getVectorQuery(QueryShardContext context, Geometry queryShape) { if (processedShape == null) { return new MatchNoDocsQuery(); } + return queryShape.visit(new ShapeVisitor(context)); + } - return processedShape.visit(new GeometryVisitor() { + public static ShapeBuilder geometryToShapeBuilder(Geometry geometry) { + ShapeBuilder shapeBuilder = geometry.visit(new GeometryVisitor, RuntimeException>() { @Override - public Query visit(Circle circle) { - throw new QueryShardException(context, "Field [" + fieldName + "] found and unknown shape Circle"); + public ShapeBuilder visit(Circle circle) { + throw new UnsupportedOperationException("circle is not supported"); } @Override - public Query visit(GeometryCollection collection) { - BooleanQuery.Builder bqb = new BooleanQuery.Builder(); - visit(bqb, collection); - return bqb.build(); - } - - private void visit(BooleanQuery.Builder bqb, GeometryCollection collection) { - for (Geometry shape : collection) { - if (shape instanceof MultiPoint) { - // Flatten multipoints - visit(bqb, (GeometryCollection) shape); - } else { - bqb.add(shape.visit(this), BooleanClause.Occur.SHOULD); - } + public ShapeBuilder visit(GeometryCollection collection) { + GeometryCollectionBuilder shapes = new GeometryCollectionBuilder(); + for (Geometry geometry : collection) { + shapes.shape(geometry.visit(this)); } + return shapes; } @Override - public Query visit(org.elasticsearch.geo.geometry.Line line) { - return LatLonShape.newLineQuery(fieldName(), relation.getLuceneRelation(), new Line(line.getLats(), line.getLons())); - } - - @Override - public Query visit(LinearRing ring) { - throw new QueryShardException(context, "Field [" + fieldName + "] found and unsupported shape LinearRing"); - } - - @Override - public Query visit(MultiLine multiLine) { - Line[] lines = new Line[multiLine.size()]; - for (int i=0; i visit(org.elasticsearch.geo.geometry.Line line) { + List coordinates = new ArrayList<>(); + for (int i = 0; i < line.length(); i++) { + coordinates.add(new Coordinate(line.getLon(i), line.getLat(i), line.getAlt(i))); } - return LatLonShape.newLineQuery(fieldName(), relation.getLuceneRelation(), lines); + return new LineStringBuilder(coordinates); } @Override - public Query visit(MultiPoint multiPoint) { - throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + GeoShapeType.MULTIPOINT + - " queries"); + public ShapeBuilder visit(LinearRing ring) { + throw new UnsupportedOperationException("circle is not supported"); } @Override - public Query visit(MultiPolygon multiPolygon) { - Polygon[] polygons = new Polygon[multiPolygon.size()]; - for (int i=0; i visit(MultiLine multiLine) { + MultiLineStringBuilder lines = new MultiLineStringBuilder(); + for (int i = 0; i < multiLine.size(); i++) { + lines.linestring((LineStringBuilder) visit(multiLine.get(i))); } - return LatLonShape.newPolygonQuery(fieldName(), relation.getLuceneRelation(), polygons); + return lines; } @Override - public Query visit(Point point) { - return LatLonShape.newBoxQuery(fieldName, relation.getLuceneRelation(), - point.getLat(), point.getLat(), point.getLon(), point.getLon()); + public ShapeBuilder visit(MultiPoint multiPoint) { + List coordinates = new ArrayList<>(); + for (int i = 0; i < multiPoint.size(); i++) { + Point p = multiPoint.get(i); + coordinates.add(new Coordinate(p.getLon(), p.getLat(), p.getAlt())); + } + return new MultiPointBuilder(coordinates); } @Override - public Query visit(org.elasticsearch.geo.geometry.Polygon polygon) { - return LatLonShape.newPolygonQuery(fieldName(), relation.getLuceneRelation(), toLucenePolygon(polygon)); + public ShapeBuilder visit(MultiPolygon multiPolygon) { + MultiPolygonBuilder polygons = new MultiPolygonBuilder(); + for (int i = 0; i < multiPolygon.size(); i++) { + polygons.polygon((PolygonBuilder) visit(multiPolygon.get(i))); + } + return polygons; } @Override - public Query visit(org.elasticsearch.geo.geometry.Rectangle r) { - return LatLonShape.newBoxQuery(fieldName(), relation.getLuceneRelation(), - r.getMinLat(), r.getMaxLat(), r.getMinLon(), r.getMaxLon()); + public ShapeBuilder visit(Point point) { + return new PointBuilder(point.getLon(), point.getLat()); } - }); - } - - /** - * Fetches the Shape with the given ID in the given type and index. - * - * @param getRequest - * GetRequest containing index, type and id - * @param path - * Name or path of the field in the Shape Document where the - * Shape itself is located - */ - private void fetch(Client client, GetRequest getRequest, String path, ActionListener listener) { - getRequest.preference("_local"); - client.get(getRequest, new ActionListener(){ @Override - public void onResponse(GetResponse response) { - try { - if (!response.isExists()) { - throw new IllegalArgumentException("Shape with ID [" + getRequest.id() + "] in type [" + getRequest.type() - + "] not found"); - } - if (response.isSourceEmpty()) { - throw new IllegalArgumentException("Shape with ID [" + getRequest.id() + "] in type [" + getRequest.type() + - "] source disabled"); - } - - String[] pathElements = path.split("\\."); - int currentPathSlot = 0; - - // It is safe to use EMPTY here because this never uses namedObject - try (XContentParser parser = XContentHelper - .createParser(NamedXContentRegistry.EMPTY, - LoggingDeprecationHandler.INSTANCE, response.getSourceAsBytesRef())) { - XContentParser.Token currentToken; - while ((currentToken = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (currentToken == XContentParser.Token.FIELD_NAME) { - if (pathElements[currentPathSlot].equals(parser.currentName())) { - parser.nextToken(); - if (++currentPathSlot == pathElements.length) { - listener.onResponse(new GeometryParser(true, true, true).parse(parser)); - return; - } - } else { - parser.nextToken(); - parser.skipChildren(); - } - } - } - throw new IllegalStateException("Shape with name [" + getRequest.id() + "] found but missing " + path + " field"); - } - } catch (Exception e) { - onFailure(e); + public ShapeBuilder visit(org.elasticsearch.geo.geometry.Polygon polygon) { + PolygonBuilder polygonBuilder = + new PolygonBuilder((LineStringBuilder) visit((org.elasticsearch.geo.geometry.Line) polygon.getPolygon()), + ShapeBuilder.Orientation.RIGHT, false); + for (int i = 0; i < polygon.getNumberOfHoles(); i++) { + polygonBuilder.hole((LineStringBuilder) visit((org.elasticsearch.geo.geometry.Line) polygon.getHole(i))); } + return polygonBuilder; } @Override - public void onFailure(Exception e) { - listener.onFailure(e); + public ShapeBuilder visit(Rectangle rectangle) { + return new EnvelopeBuilder(new Coordinate(rectangle.getMinLon(), rectangle.getMaxLat()), + new Coordinate(rectangle.getMaxLon(), rectangle.getMinLat())); } }); - - } - - public static SpatialArgs getArgs(Geometry shape, ShapeRelation relation) { - switch (relation) { - case DISJOINT: - return new SpatialArgs(SpatialOperation.IsDisjointTo, buildS4J(shape)); - case INTERSECTS: - return new SpatialArgs(SpatialOperation.Intersects, buildS4J(shape)); - case WITHIN: - return new SpatialArgs(SpatialOperation.IsWithin, buildS4J(shape)); - case CONTAINS: - return new SpatialArgs(SpatialOperation.Contains, buildS4J(shape)); - default: - throw new IllegalArgumentException("invalid relation [" + relation + "]"); - } + return shapeBuilder; } - @Override - protected void doXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(NAME); - - builder.startObject(fieldName); + private class ShapeVisitor implements GeometryVisitor { + QueryShardContext context; + MappedFieldType fieldType; - if (strategy != null) { - builder.field(STRATEGY_FIELD.getPreferredName(), strategy.getStrategyName()); + ShapeVisitor(QueryShardContext context) { + this.context = context; + this.fieldType = context.fieldMapper(fieldName); } - if (shape != null) { - builder.field(SHAPE_FIELD.getPreferredName()); - GeoJson.toXContent(shape, builder,params); - } else { - builder.startObject(INDEXED_SHAPE_FIELD.getPreferredName()) - .field(SHAPE_ID_FIELD.getPreferredName(), indexedShapeId); - if (indexedShapeType != null) { - builder.field(SHAPE_TYPE_FIELD.getPreferredName(), indexedShapeType); - } - if (indexedShapeIndex != null) { - builder.field(SHAPE_INDEX_FIELD.getPreferredName(), indexedShapeIndex); - } - if (indexedShapePath != null) { - builder.field(SHAPE_PATH_FIELD.getPreferredName(), indexedShapePath); - } - if (indexedShapeRouting != null) { - builder.field(SHAPE_ROUTING_FIELD.getPreferredName(), indexedShapeRouting); - } - builder.endObject(); + @Override + public Query visit(Circle circle) { + throw new QueryShardException(context, "Field [" + fieldName + "] found and unknown shape Circle"); } - if(relation != null) { - builder.field(RELATION_FIELD.getPreferredName(), relation.getRelationName()); + @Override + public Query visit(GeometryCollection collection) { + BooleanQuery.Builder bqb = new BooleanQuery.Builder(); + visit(bqb, collection); + return bqb.build(); } - builder.endObject(); - builder.field(IGNORE_UNMAPPED_FIELD.getPreferredName(), ignoreUnmapped); - - printBoostAndQueryName(builder); - - builder.endObject(); - } - - public static GeoShapeQueryBuilder fromXContent(XContentParser parser) throws IOException { - String fieldName = null; - ShapeRelation shapeRelation = null; - SpatialStrategy strategy = null; - ShapeBuilder shape = null; - - String id = null; - String type = null; - String index = null; - String shapePath = null; - String shapeRouting = null; - - XContentParser.Token token; - String currentFieldName = null; - float boost = AbstractQueryBuilder.DEFAULT_BOOST; - String queryName = null; - boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED; - - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else if (token == XContentParser.Token.START_OBJECT) { - if (fieldName != null) { - throw new ParsingException(parser.getTokenLocation(), "[" + - GeoShapeQueryBuilder.NAME + "] point specified twice. [" + currentFieldName + "]"); - } - fieldName = currentFieldName; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - token = parser.nextToken(); - if (SHAPE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - shape = ShapeParser.parse(parser); - } else if (STRATEGY_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - String strategyName = parser.text(); - strategy = SpatialStrategy.fromString(strategyName); - if (strategy == null) { - throw new ParsingException(parser.getTokenLocation(), "Unknown strategy [" + strategyName + " ]"); - } - } else if (RELATION_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - shapeRelation = ShapeRelation.getRelationByName(parser.text()); - if (shapeRelation == null) { - throw new ParsingException(parser.getTokenLocation(), "Unknown shape operation [" + parser.text() + " ]"); - } - } else if (INDEXED_SHAPE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else if (token.isValue()) { - if (SHAPE_ID_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - id = parser.text(); - } else if (SHAPE_TYPE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - type = parser.text(); - } else if (SHAPE_INDEX_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - index = parser.text(); - } else if (SHAPE_PATH_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - shapePath = parser.text(); - } else if (SHAPE_ROUTING_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - shapeRouting = parser.text(); - } - } else { - throw new ParsingException(parser.getTokenLocation(), "[" + GeoShapeQueryBuilder.NAME + - "] unknown token [" + token + "] after [" + currentFieldName + "]"); - } - } - } else { - throw new ParsingException(parser.getTokenLocation(), "[" + GeoShapeQueryBuilder.NAME + - "] query does not support [" + currentFieldName + "]"); - } - } - } - } else if (token.isValue()) { - if (AbstractQueryBuilder.BOOST_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - boost = parser.floatValue(); - } else if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - queryName = parser.text(); - } else if (IGNORE_UNMAPPED_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { - ignoreUnmapped = parser.booleanValue(); + private void visit(BooleanQuery.Builder bqb, GeometryCollection collection) { + for (Geometry shape : collection) { + if (shape instanceof MultiPoint) { + // Flatten multipoints + visit(bqb, (GeometryCollection) shape); } else { - throw new ParsingException(parser.getTokenLocation(), "[" + GeoShapeQueryBuilder.NAME + - "] query does not support [" + currentFieldName + "]"); + bqb.add(shape.visit(this), BooleanClause.Occur.SHOULD); } } } - GeoShapeQueryBuilder builder; - if (type != null) { - deprecationLogger.deprecatedAndMaybeLog( - "geo_share_query_with_types", TYPES_DEPRECATION_MESSAGE); + + @Override + public Query visit(org.elasticsearch.geo.geometry.Line line) { + validateIsGeoShapeFieldType(); + return LatLonShape.newLineQuery(fieldName(), relation.getLuceneRelation(), new Line(line.getLats(), line.getLons())); } - if (shape != null) { - builder = new GeoShapeQueryBuilder(fieldName, shape); - } else { - builder = new GeoShapeQueryBuilder(fieldName, id, type); + @Override + public Query visit(LinearRing ring) { + throw new QueryShardException(context, "Field [" + fieldName + "] found and unsupported shape LinearRing"); } - if (index != null) { - builder.indexedShapeIndex(index); + + @Override + public Query visit(MultiLine multiLine) { + validateIsGeoShapeFieldType(); + Line[] lines = new Line[multiLine.size()]; + for (int i=0; i supplier = new SetOnce<>(); - queryRewriteContext.registerAsyncAction((client, listener) -> { - GetRequest getRequest; - if (indexedShapeType == null) { - getRequest = new GetRequest(indexedShapeIndex, indexedShapeId); + private static class ParsedGeoShapeQueryParams extends ParsedShapeQueryParams { + SpatialStrategy strategy; + + @Override + protected boolean parseXContentField(XContentParser parser) throws IOException { + SpatialStrategy strategy; + if (SHAPE_FIELD.match(parser.currentName(), parser.getDeprecationHandler())) { + this.shape = ShapeParser.parse(parser); + return true; + } else if (STRATEGY_FIELD.match(parser.currentName(), parser.getDeprecationHandler())) { + String strategyName = parser.text(); + strategy = SpatialStrategy.fromString(strategyName); + if (strategy == null) { + throw new ParsingException(parser.getTokenLocation(), "Unknown strategy [" + strategyName + " ]"); } else { - getRequest = new GetRequest(indexedShapeIndex, indexedShapeType, indexedShapeId); + this.strategy = strategy; } - getRequest.routing(indexedShapeRouting); - fetch(client, getRequest, indexedShapePath, ActionListener.wrap(builder-> { - supplier.set(builder); - listener.onResponse(null); - }, listener::onFailure)); - }); - return new GeoShapeQueryBuilder(this.fieldName, supplier::get, this.indexedShapeId, this.indexedShapeType).relation(relation) - .strategy(strategy); + return true; + } + return false; } - return this; } - /** - * Builds JTS shape from a geometry - * - * This method is needed to handle legacy indices and will be removed when we no longer need to build JTS shapes - */ - private static Shape buildS4J(Geometry geometry) { - return geometryToShapeBuilder(geometry).buildS4J(); - } - - public static ShapeBuilder geometryToShapeBuilder(Geometry geometry) { - ShapeBuilder shapeBuilder = geometry.visit(new GeometryVisitor, RuntimeException>() { - @Override - public ShapeBuilder visit(Circle circle) { - throw new UnsupportedOperationException("circle is not supported"); - } + public static GeoShapeQueryBuilder fromXContent(XContentParser parser) throws IOException { + ParsedGeoShapeQueryParams pgsqp = + (ParsedGeoShapeQueryParams) AbstractGeometryQueryBuilder.parsedParamsFromXContent(parser, new ParsedGeoShapeQueryParams()); - @Override - public ShapeBuilder visit(GeometryCollection collection) { - GeometryCollectionBuilder shapes = new GeometryCollectionBuilder(); - for (Geometry geometry : collection) { - shapes.shape(geometry.visit(this)); - } - return shapes; - } + GeoShapeQueryBuilder builder; + if (pgsqp.type != null) { + deprecationLogger.deprecatedAndMaybeLog("geo_share_query_with_types", TYPES_DEPRECATION_MESSAGE); + } - @Override - public ShapeBuilder visit(org.elasticsearch.geo.geometry.Line line) { - List coordinates = new ArrayList<>(); - for (int i = 0; i < line.length(); i++) { - coordinates.add(new Coordinate(line.getLon(i), line.getLat(i), line.getAlt(i))); - } - return new LineStringBuilder(coordinates); - } + if (pgsqp.shape != null) { + builder = new GeoShapeQueryBuilder(pgsqp.fieldName, pgsqp.shape); + } else { + builder = new GeoShapeQueryBuilder(pgsqp.fieldName, pgsqp.id, pgsqp.type); + } - @Override - public ShapeBuilder visit(LinearRing ring) { - throw new UnsupportedOperationException("circle is not supported"); - } + if (pgsqp.index != null) { + builder.indexedShapeIndex(pgsqp.index); + } - @Override - public ShapeBuilder visit(MultiLine multiLine) { - MultiLineStringBuilder lines = new MultiLineStringBuilder(); - for (int i = 0; i < multiLine.size(); i++) { - lines.linestring((LineStringBuilder) visit(multiLine.get(i))); - } - return lines; - } + if (pgsqp.shapePath != null) { + builder.indexedShapePath(pgsqp.shapePath); + } - @Override - public ShapeBuilder visit(MultiPoint multiPoint) { - List coordinates = new ArrayList<>(); - for (int i = 0; i < multiPoint.size(); i++) { - Point p = multiPoint.get(i); - coordinates.add(new Coordinate(p.getLon(), p.getLat(), p.getAlt())); - } - return new MultiPointBuilder(coordinates); - } + if (pgsqp.shapeRouting != null) { + builder.indexedShapeRouting(pgsqp.shapeRouting); + } - @Override - public ShapeBuilder visit(MultiPolygon multiPolygon) { - MultiPolygonBuilder polygons = new MultiPolygonBuilder(); - for (int i = 0; i < multiPolygon.size(); i++) { - polygons.polygon((PolygonBuilder) visit(multiPolygon.get(i))); - } - return polygons; - } + if (pgsqp.relation != null) { + builder.relation(pgsqp.relation); + } - @Override - public ShapeBuilder visit(Point point) { - return new PointBuilder(point.getLon(), point.getLat()); - } + if (pgsqp.strategy != null) { + builder.strategy(pgsqp.strategy); + } - @Override - public ShapeBuilder visit(org.elasticsearch.geo.geometry.Polygon polygon) { - PolygonBuilder polygonBuilder = - new PolygonBuilder((LineStringBuilder) visit((org.elasticsearch.geo.geometry.Line) polygon.getPolygon()), - ShapeBuilder.Orientation.RIGHT, false); - for (int i = 0; i < polygon.getNumberOfHoles(); i++) { - polygonBuilder.hole((LineStringBuilder) visit((org.elasticsearch.geo.geometry.Line) polygon.getHole(i))); - } - return polygonBuilder; - } + if (pgsqp.queryName != null) { + builder.queryName(pgsqp.queryName); + } - @Override - public ShapeBuilder visit(Rectangle rectangle) { - return new EnvelopeBuilder(new Coordinate(rectangle.getMinLon(), rectangle.getMaxLat()), - new Coordinate(rectangle.getMaxLon(), rectangle.getMinLat())); - } - }); - return shapeBuilder; + builder.boost(pgsqp.boost); + builder.ignoreUnmapped(pgsqp.ignoreUnmapped); + return builder; } }