
org.opensearch.index.query.AbstractGeometryQueryBuilder Maven / Gradle / Ivy
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/*
* 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.
*/
/*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/
package org.opensearch.index.query;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query;
import org.opensearch.Version;
import org.opensearch.action.get.GetRequest;
import org.opensearch.action.get.GetResponse;
import org.opensearch.client.Client;
import org.opensearch.common.SetOnce;
import org.opensearch.common.geo.GeoJson;
import org.opensearch.common.geo.GeometryIO;
import org.opensearch.common.geo.GeometryParser;
import org.opensearch.common.geo.ShapeRelation;
import org.opensearch.common.geo.builders.ShapeBuilder;
import org.opensearch.common.xcontent.LoggingDeprecationHandler;
import org.opensearch.common.xcontent.XContentHelper;
import org.opensearch.core.ParseField;
import org.opensearch.core.action.ActionListener;
import org.opensearch.core.common.ParsingException;
import org.opensearch.core.common.io.stream.StreamInput;
import org.opensearch.core.common.io.stream.StreamOutput;
import org.opensearch.core.xcontent.NamedXContentRegistry;
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.core.xcontent.XContentParser;
import org.opensearch.geometry.Geometry;
import org.opensearch.index.mapper.MappedFieldType;
import org.opensearch.index.mapper.MapperService;
import java.io.IOException;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Base {@link QueryBuilder} that builds a Geometry Query
*
* @opensearch.internal
*/
public abstract class AbstractGeometryQueryBuilder> extends AbstractQueryBuilder
implements
WithFieldName {
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;
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_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 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);
}
/**
* 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);
}
/**
* 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);
}
protected AbstractGeometryQueryBuilder(String fieldName, Geometry shape, String indexedShapeId) {
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.supplier = null;
}
protected AbstractGeometryQueryBuilder(String fieldName, Supplier supplier, String indexedShapeId) {
if (fieldName == null) {
throw new IllegalArgumentException("fieldName is required");
}
if (supplier == null && indexedShapeId == null) {
throw new IllegalArgumentException("either shape or indexedShapeId is required");
}
this.fieldName = fieldName;
this.shape = null;
this.supplier = supplier;
this.indexedShapeId = indexedShapeId;
}
/**
* Read from a stream.
*/
protected AbstractGeometryQueryBuilder(StreamInput in) throws IOException {
super(in);
fieldName = in.readString();
if (in.readBoolean()) {
shape = GeometryIO.readGeometry(in);
indexedShapeId = null;
} else {
shape = null;
indexedShapeId = in.readOptionalString();
if (in.getVersion().before(Version.V_2_0_0)) {
String type = in.readOptionalString();
assert MapperService.SINGLE_MAPPING_NAME.equals(type) : "Expected type [_doc], got [" + type + "]";
}
indexedShapeIndex = in.readOptionalString();
indexedShapePath = in.readOptionalString();
indexedShapeRouting = in.readOptionalString();
}
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);
if (out.getVersion().before(Version.V_2_0_0)) {
out.writeOptionalString(MapperService.SINGLE_MAPPING_NAME);
}
out.writeOptionalString(indexedShapeIndex);
out.writeOptionalString(indexedShapePath);
out.writeOptionalString(indexedShapeRouting);
}
relation.writeTo(out);
out.writeBoolean(ignoreUnmapped);
}
/**
* @return the name of the field that will be queried
*/
@Override
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;
}
/**
* 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;
}
/** builds the appropriate lucene shape query */
protected abstract Query buildShapeQuery(QueryShardContext context, MappedFieldType fieldType);
/** 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 */
protected abstract AbstractGeometryQueryBuilder newShapeQueryBuilder(
String fieldName,
Supplier shapeSupplier,
String indexedShapeId
);
@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 type for 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() + "] not found");
}
if (response.isSourceEmpty()) {
throw new IllegalArgumentException("Shape with ID [" + getRequest.id() + "] 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 (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(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,
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 = new GetRequest(indexedShapeIndex, 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).relation(relation);
}
return this;
}
/**
* local class that encapsulates xcontent parsed shape parameters
*
* @opensearch.internal
*/
protected abstract static class ParsedGeometryQueryParams {
public String fieldName;
public ShapeRelation relation;
public ShapeBuilder shape;
public String id = 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 ParsedGeometryQueryParams parsedParamsFromXContent(XContentParser parser, ParsedGeometryQueryParams 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_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;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy