From f603f06250ab72b229bdf45476a73c9eeb21ac46 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 26 Jul 2019 12:14:05 -0400 Subject: [PATCH] Geo: refactor geo mapper and query builder (#44884) Refactors out the indexing and query generation logic out of the mapper and query builder into a separate unit-testable classes. --- .../common/geo/parsers/GeoJsonParser.java | 12 +- .../common/geo/parsers/GeoWKTParser.java | 12 +- .../common/geo/parsers/ShapeParser.java | 4 +- ....java => AbstractGeometryFieldMapper.java} | 132 ++++++-- .../index/mapper/GeoShapeFieldMapper.java | 72 ++--- .../mapper/GeoShapeIndexer.java} | 55 ++-- .../mapper/LegacyGeoShapeFieldMapper.java | 68 ++-- .../index/mapper/LegacyGeoShapeIndexer.java | 35 +++ .../index/query/GeoShapeQueryBuilder.java | 294 +----------------- .../query/LegacyGeoShapeQueryProcessor.java | 197 ++++++++++++ .../query/VectorGeoShapeQueryProcessor.java | 171 ++++++++++ .../elasticsearch/indices/IndicesModule.java | 5 +- .../common/geo/BaseGeoParsingTestCase.java | 3 +- .../common/geo/GeoJsonShapeParserTests.java | 3 +- .../common/geo/GeoWKTShapeParserTests.java | 3 +- .../common/geo/GeometryIOTests.java | 2 +- .../common/geo/GeometryIndexerTests.java | 7 +- .../common/geo/ShapeBuilderTests.java | 3 +- .../index/mapper/ExternalMapper.java | 8 +- .../mapper/GeoShapeFieldMapperTests.java | 3 +- 20 files changed, 648 insertions(+), 441 deletions(-) rename server/src/main/java/org/elasticsearch/index/mapper/{BaseGeoShapeFieldMapper.java => AbstractGeometryFieldMapper.java} (73%) rename server/src/main/java/org/elasticsearch/{common/geo/GeometryIndexer.java => index/mapper/GeoShapeIndexer.java} (96%) create mode 100644 server/src/main/java/org/elasticsearch/index/mapper/LegacyGeoShapeIndexer.java create mode 100644 server/src/main/java/org/elasticsearch/index/query/LegacyGeoShapeQueryProcessor.java create mode 100644 server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java diff --git a/server/src/main/java/org/elasticsearch/common/geo/parsers/GeoJsonParser.java b/server/src/main/java/org/elasticsearch/common/geo/parsers/GeoJsonParser.java index b008786ed9211..8ab6a44f26e8e 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/parsers/GeoJsonParser.java +++ b/server/src/main/java/org/elasticsearch/common/geo/parsers/GeoJsonParser.java @@ -29,7 +29,7 @@ import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentSubParser; -import org.elasticsearch.index.mapper.BaseGeoShapeFieldMapper; +import org.elasticsearch.index.mapper.AbstractGeometryFieldMapper; import org.locationtech.jts.geom.Coordinate; import java.io.IOException; @@ -42,7 +42,7 @@ * complies with geojson specification: https://tools.ietf.org/html/rfc7946 */ abstract class GeoJsonParser { - protected static ShapeBuilder parse(XContentParser parser, BaseGeoShapeFieldMapper shapeMapper) + protected static ShapeBuilder parse(XContentParser parser, AbstractGeometryFieldMapper shapeMapper) throws IOException { GeoShapeType shapeType = null; DistanceUnit.Distance radius = null; @@ -50,13 +50,13 @@ protected static ShapeBuilder parse(XContentParser parser, BaseGeoShapeFieldMapp GeometryCollectionBuilder geometryCollections = null; Orientation orientation = (shapeMapper == null) - ? BaseGeoShapeFieldMapper.Defaults.ORIENTATION.value() + ? AbstractGeometryFieldMapper.Defaults.ORIENTATION.value() : shapeMapper.orientation(); Explicit coerce = (shapeMapper == null) - ? BaseGeoShapeFieldMapper.Defaults.COERCE + ? AbstractGeometryFieldMapper.Defaults.COERCE : shapeMapper.coerce(); Explicit ignoreZValue = (shapeMapper == null) - ? BaseGeoShapeFieldMapper.Defaults.IGNORE_Z_VALUE + ? AbstractGeometryFieldMapper.Defaults.IGNORE_Z_VALUE : shapeMapper.ignoreZValue(); String malformedException = null; @@ -208,7 +208,7 @@ private static Coordinate parseCoordinate(XContentParser parser, boolean ignoreZ * @return Geometry[] geometries of the GeometryCollection * @throws IOException Thrown if an error occurs while reading from the XContentParser */ - static GeometryCollectionBuilder parseGeometries(XContentParser parser, BaseGeoShapeFieldMapper mapper) throws + static GeometryCollectionBuilder parseGeometries(XContentParser parser, AbstractGeometryFieldMapper mapper) throws IOException { if (parser.currentToken() != XContentParser.Token.START_ARRAY) { throw new ElasticsearchParseException("geometries must be an array of geojson objects"); diff --git a/server/src/main/java/org/elasticsearch/common/geo/parsers/GeoWKTParser.java b/server/src/main/java/org/elasticsearch/common/geo/parsers/GeoWKTParser.java index 2cffa417246fd..69c84a52a2db4 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/parsers/GeoWKTParser.java +++ b/server/src/main/java/org/elasticsearch/common/geo/parsers/GeoWKTParser.java @@ -34,7 +34,7 @@ import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.mapper.BaseGeoShapeFieldMapper; +import org.elasticsearch.index.mapper.AbstractGeometryFieldMapper; import org.locationtech.jts.geom.Coordinate; import java.io.IOException; @@ -63,7 +63,7 @@ public class GeoWKTParser { // no instance private GeoWKTParser() {} - public static ShapeBuilder parse(XContentParser parser, final BaseGeoShapeFieldMapper shapeMapper) + public static ShapeBuilder parse(XContentParser parser, final AbstractGeometryFieldMapper shapeMapper) throws IOException, ElasticsearchParseException { return parseExpectedType(parser, null, shapeMapper); } @@ -75,12 +75,12 @@ public static ShapeBuilder parseExpectedType(XContentParser parser, final GeoSha /** throws an exception if the parsed geometry type does not match the expected shape type */ public static ShapeBuilder parseExpectedType(XContentParser parser, final GeoShapeType shapeType, - final BaseGeoShapeFieldMapper shapeMapper) + final AbstractGeometryFieldMapper shapeMapper) throws IOException, ElasticsearchParseException { try (StringReader reader = new StringReader(parser.text())) { - Explicit ignoreZValue = (shapeMapper == null) ? BaseGeoShapeFieldMapper.Defaults.IGNORE_Z_VALUE : + Explicit ignoreZValue = (shapeMapper == null) ? AbstractGeometryFieldMapper.Defaults.IGNORE_Z_VALUE : shapeMapper.ignoreZValue(); - Explicit coerce = (shapeMapper == null) ? BaseGeoShapeFieldMapper.Defaults.COERCE : shapeMapper.coerce(); + Explicit coerce = (shapeMapper == null) ? AbstractGeometryFieldMapper.Defaults.COERCE : shapeMapper.coerce(); // setup the tokenizer; configured to read words w/o numbers StreamTokenizer tokenizer = new StreamTokenizer(reader); tokenizer.resetSyntax(); @@ -258,7 +258,7 @@ private static PolygonBuilder parsePolygon(StreamTokenizer stream, final boolean return null; } PolygonBuilder builder = new PolygonBuilder(parseLinearRing(stream, ignoreZValue, coerce), - BaseGeoShapeFieldMapper.Defaults.ORIENTATION.value()); + AbstractGeometryFieldMapper.Defaults.ORIENTATION.value()); while (nextCloserOrComma(stream).equals(COMMA)) { builder.hole(parseLinearRing(stream, ignoreZValue, coerce)); } diff --git a/server/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java b/server/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java index 4a976d19b2347..f15b2f2777f91 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java +++ b/server/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java @@ -26,7 +26,7 @@ import org.elasticsearch.common.xcontent.XContent; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.support.MapXContentParser; -import org.elasticsearch.index.mapper.BaseGeoShapeFieldMapper; +import org.elasticsearch.index.mapper.AbstractGeometryFieldMapper; import java.io.IOException; import java.util.Collections; @@ -50,7 +50,7 @@ public interface ShapeParser { * if the parsers current token has been null * @throws IOException if the input could not be read */ - static ShapeBuilder parse(XContentParser parser, BaseGeoShapeFieldMapper shapeMapper) throws IOException { + static ShapeBuilder parse(XContentParser parser, AbstractGeometryFieldMapper shapeMapper) throws IOException { if (parser.currentToken() == XContentParser.Token.VALUE_NULL) { return null; } if (parser.currentToken() == XContentParser.Token.START_OBJECT) { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/BaseGeoShapeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java similarity index 73% rename from server/src/main/java/org/elasticsearch/index/mapper/BaseGeoShapeFieldMapper.java rename to server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java index 20151f301d791..e47060c87e539 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/BaseGeoShapeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java @@ -26,17 +26,22 @@ import org.elasticsearch.Version; import org.elasticsearch.common.Explicit; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.SpatialStrategy; import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.geo.builders.ShapeBuilder.Orientation; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.geo.geometry.Geometry; import org.elasticsearch.index.mapper.LegacyGeoShapeFieldMapper.DeprecatedParameters; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.index.query.QueryShardException; import java.io.IOException; +import java.text.ParseException; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -47,8 +52,8 @@ /** * Base class for {@link GeoShapeFieldMapper} and {@link LegacyGeoShapeFieldMapper} */ -public abstract class BaseGeoShapeFieldMapper extends FieldMapper { - public static final String CONTENT_TYPE = "geo_shape"; +public abstract class AbstractGeometryFieldMapper extends FieldMapper { + public static class Names { public static final ParseField ORIENTATION = new ParseField("orientation"); @@ -62,7 +67,36 @@ public static class Defaults { public static final Explicit IGNORE_Z_VALUE = new Explicit<>(true, false); } - public abstract static class Builder + + /** + * Interface representing an preprocessor in geo-shape indexing pipeline + */ + public interface Indexer { + + Processed prepareForIndexing(Parsed geometry); + + Class processedClass(); + + } + + /** + * interface representing parser in geo shape indexing pipeline + */ + public interface Parser { + + Parsed parse(XContentParser parser, AbstractGeometryFieldMapper mapper) throws IOException, ParseException; + + } + + /** + * interface representing a query builder that generates a query from the given shape + */ + public interface QueryProcessor { + + Query process(Geometry shape, String fieldName, SpatialStrategy strategy, ShapeRelation relation, QueryShardContext context); + } + + public abstract static class Builder extends FieldMapper.Builder { protected Boolean coerce; protected Boolean ignoreMalformed; @@ -152,7 +186,7 @@ protected void setupFieldType(BuilderContext context) { throw new IllegalArgumentException("name cannot be empty string"); } - BaseGeoShapeFieldType ft = (BaseGeoShapeFieldType)fieldType(); + AbstractGeometryFieldType ft = (AbstractGeometryFieldType)fieldType(); ft.setOrientation(orientation().value()); } } @@ -218,10 +252,16 @@ public Mapper.Builder parse(String name, Map node, ParserContext } } - public abstract static class BaseGeoShapeFieldType extends MappedFieldType { + public abstract static class AbstractGeometryFieldType extends MappedFieldType { protected Orientation orientation = Defaults.ORIENTATION.value(); - protected BaseGeoShapeFieldType() { + protected Indexer geometryIndexer; + + protected Parser geometryParser; + + protected QueryProcessor geometryQueryBuilder; + + protected AbstractGeometryFieldType() { setIndexOptions(IndexOptions.DOCS); setTokenized(false); setStored(false); @@ -229,7 +269,7 @@ protected BaseGeoShapeFieldType() { setOmitNorms(true); } - protected BaseGeoShapeFieldType(BaseGeoShapeFieldType ref) { + protected AbstractGeometryFieldType(AbstractGeometryFieldType ref) { super(ref); this.orientation = ref.orientation; } @@ -237,7 +277,7 @@ protected BaseGeoShapeFieldType(BaseGeoShapeFieldType ref) { @Override public boolean equals(Object o) { if (!super.equals(o)) return false; - BaseGeoShapeFieldType that = (BaseGeoShapeFieldType) o; + AbstractGeometryFieldType that = (AbstractGeometryFieldType) o; return orientation == that.orientation; } @@ -246,16 +286,6 @@ public int hashCode() { return Objects.hash(super.hashCode(), orientation); } - @Override - public String typeName() { - return CONTENT_TYPE; - } - - @Override - public void checkCompatibility(MappedFieldType fieldType, List conflicts) { - super.checkCompatibility(fieldType, conflicts); - } - public Orientation orientation() { return this.orientation; } public void setOrientation(Orientation orientation) { @@ -272,16 +302,40 @@ public Query existsQuery(QueryShardContext context) { public Query termQuery(Object value, QueryShardContext context) { throw new QueryShardException(context, "Geo fields do not support exact searching, use dedicated geo queries instead"); } + + public void setGeometryIndexer(Indexer geometryIndexer) { + this.geometryIndexer = geometryIndexer; + } + + protected Indexer geometryIndexer() { + return geometryIndexer; + } + + public void setGeometryParser(Parser geometryParser) { + this.geometryParser = geometryParser; + } + + protected Parser geometryParser() { + return geometryParser; + } + + public void setGeometryQueryBuilder(QueryProcessor geometryQueryBuilder) { + this.geometryQueryBuilder = geometryQueryBuilder; + } + + public QueryProcessor geometryQueryBuilder() { + return geometryQueryBuilder; + } } protected Explicit coerce; protected Explicit ignoreMalformed; protected Explicit ignoreZValue; - protected BaseGeoShapeFieldMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType, - Explicit ignoreMalformed, Explicit coerce, - Explicit ignoreZValue, Settings indexSettings, - MultiFields multiFields, CopyTo copyTo) { + protected AbstractGeometryFieldMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType, + Explicit ignoreMalformed, Explicit coerce, + Explicit ignoreZValue, Settings indexSettings, + MultiFields multiFields, CopyTo copyTo) { super(simpleName, fieldType, defaultFieldType, indexSettings, multiFields, copyTo); this.coerce = coerce; this.ignoreMalformed = ignoreMalformed; @@ -291,7 +345,7 @@ protected BaseGeoShapeFieldMapper(String simpleName, MappedFieldType fieldType, @Override protected void doMerge(Mapper mergeWith) { super.doMerge(mergeWith); - BaseGeoShapeFieldMapper gsfm = (BaseGeoShapeFieldMapper)mergeWith; + AbstractGeometryFieldMapper gsfm = (AbstractGeometryFieldMapper)mergeWith; if (gsfm.coerce.explicit()) { this.coerce = gsfm.coerce; } @@ -310,7 +364,7 @@ protected void parseCreateField(ParseContext context, List field @Override protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException { builder.field("type", contentType()); - BaseGeoShapeFieldType ft = (BaseGeoShapeFieldType)fieldType(); + AbstractGeometryFieldType ft = (AbstractGeometryFieldType)fieldType(); if (includeDefaults || ft.orientation() != Defaults.ORIENTATION.value()) { builder.field(Names.ORIENTATION.getPreferredName(), ft.orientation()); } @@ -338,11 +392,35 @@ public Explicit ignoreZValue() { } public Orientation orientation() { - return ((BaseGeoShapeFieldType)fieldType).orientation(); + return ((AbstractGeometryFieldType)fieldType).orientation(); } + protected abstract void indexShape(ParseContext context, Processed shape); + + /** parsing logic for geometry indexing */ @Override - protected String contentType() { - return CONTENT_TYPE; + public void parse(ParseContext context) throws IOException { + AbstractGeometryFieldType fieldType = (AbstractGeometryFieldType)fieldType(); + + @SuppressWarnings("unchecked") Indexer geometryIndexer = fieldType.geometryIndexer(); + @SuppressWarnings("unchecked") Parser geometryParser = fieldType.geometryParser(); + try { + Processed shape = context.parseExternalValue(geometryIndexer.processedClass()); + if (shape == null) { + Parsed geometry = geometryParser.parse(context.parser(), this); + if (geometry == null) { + return; + } + shape = geometryIndexer.prepareForIndexing(geometry); + } + indexShape(context, shape); + } catch (Exception e) { + if (ignoreMalformed.value() == false) { + throw new MapperParsingException("failed to parse field [{}] of type [{}]", e, fieldType().name(), + fieldType().typeName()); + } + context.addIgnoredField(fieldType().name()); + } } + } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java index 2ce1d5328f3b9..7bd5eba115adc 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java @@ -24,7 +24,6 @@ import org.apache.lucene.geo.Polygon; import org.apache.lucene.index.IndexableField; import org.elasticsearch.common.Explicit; -import org.elasticsearch.common.geo.GeometryIndexer; import org.elasticsearch.common.geo.GeometryParser; import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.settings.Settings; @@ -37,8 +36,8 @@ import org.elasticsearch.geo.geometry.MultiPoint; import org.elasticsearch.geo.geometry.MultiPolygon; import org.elasticsearch.geo.geometry.Point; +import org.elasticsearch.index.query.VectorGeoShapeQueryProcessor; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -62,9 +61,10 @@ *

* "field" : "POLYGON ((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0 0.0)) */ -public class GeoShapeFieldMapper extends BaseGeoShapeFieldMapper { +public class GeoShapeFieldMapper extends AbstractGeometryFieldMapper { + public static final String CONTENT_TYPE = "geo_shape"; - public static class Builder extends BaseGeoShapeFieldMapper.Builder { + public static class Builder extends AbstractGeometryFieldMapper.Builder { public Builder(String name) { super (name, new GeoShapeFieldType(), new GeoShapeFieldType()); } @@ -75,9 +75,21 @@ public GeoShapeFieldMapper build(BuilderContext context) { return new GeoShapeFieldMapper(name, fieldType, defaultFieldType, ignoreMalformed(context), coerce(context), ignoreZValue(), context.indexSettings(), multiFieldsBuilder.build(this, context), copyTo); } + + @Override + protected void setupFieldType(BuilderContext context) { + super.setupFieldType(context); + + GeometryParser geometryParser = new GeometryParser(orientation == ShapeBuilder.Orientation.RIGHT, coerce(context).value(), + ignoreZValue().value()); + + ((GeoShapeFieldType)fieldType()).setGeometryIndexer(new GeoShapeIndexer(orientation == ShapeBuilder.Orientation.RIGHT)); + ((GeoShapeFieldType)fieldType()).setGeometryParser( (parser, mapper) -> geometryParser.parse(parser)); + ((GeoShapeFieldType)fieldType()).setGeometryQueryBuilder(new VectorGeoShapeQueryProcessor()); + } } - public static final class GeoShapeFieldType extends BaseGeoShapeFieldType { + public static final class GeoShapeFieldType extends AbstractGeometryFieldType { public GeoShapeFieldType() { super(); } @@ -90,10 +102,17 @@ protected GeoShapeFieldType(GeoShapeFieldType ref) { public GeoShapeFieldType clone() { return new GeoShapeFieldType(this); } - } - private final GeometryParser geometryParser; - private final GeometryIndexer geometryIndexer; + @Override + public String typeName() { + return CONTENT_TYPE; + } + + @Override + protected Indexer geometryIndexer() { + return new GeoShapeIndexer(orientation == ShapeBuilder.Orientation.RIGHT); + } + } public GeoShapeFieldMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType, Explicit ignoreMalformed, Explicit coerce, @@ -101,8 +120,6 @@ public GeoShapeFieldMapper(String simpleName, MappedFieldType fieldType, MappedF MultiFields multiFields, CopyTo copyTo) { super(simpleName, fieldType, defaultFieldType, ignoreMalformed, coerce, ignoreZValue, indexSettings, multiFields, copyTo); - geometryParser = new GeometryParser(orientation() == ShapeBuilder.Orientation.RIGHT, coerce().value(), ignoreZValue.value()); - geometryIndexer = new GeometryIndexer(true); } @Override @@ -110,35 +127,9 @@ public GeoShapeFieldType fieldType() { return (GeoShapeFieldType) super.fieldType(); } - /** parsing logic for {@link LatLonShape} indexing */ @Override - public void parse(ParseContext context) throws IOException { - try { - - Object shape = context.parseExternalValue(Object.class); - if (shape == null) { - Geometry geometry = geometryParser.parse(context.parser()); - if (geometry == null) { - return; - } - shape = geometryIndexer.prepareForIndexing(geometry); - } - indexShape(context, shape); - } catch (Exception e) { - if (ignoreMalformed.value() == false) { - throw new MapperParsingException("failed to parse field [{}] of type [{}]", e, fieldType().name(), - fieldType().typeName()); - } - context.addIgnoredField(fieldType().name()); - } - } - - private void indexShape(ParseContext context, Object luceneShape) { - if (luceneShape instanceof Geometry) { - ((Geometry) luceneShape).visit(new LuceneGeometryIndexer(context)); - } else { - throw new IllegalArgumentException("invalid shape type found [" + luceneShape.getClass() + "] while indexing shape"); - } + protected void indexShape(ParseContext context, Geometry luceneShape) { + luceneShape.visit(new LuceneGeometryIndexer(context)); } private class LuceneGeometryIndexer implements GeometryVisitor { @@ -232,4 +223,9 @@ private void indexFields(ParseContext context, Field[] fields) { context.doc().add(f); } } + + @Override + protected String contentType() { + return CONTENT_TYPE; + } } diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeometryIndexer.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java similarity index 96% rename from server/src/main/java/org/elasticsearch/common/geo/GeometryIndexer.java rename to server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java index 6d6270e49bbfb..5f1742c99dbcd 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeometryIndexer.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java @@ -18,7 +18,7 @@ */ -package org.elasticsearch.common.geo; +package org.elasticsearch.index.mapper; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.geo.geometry.Circle; @@ -52,7 +52,7 @@ /** * Utility class that converts geometries into Lucene-compatible form */ -public final class GeometryIndexer { +public final class GeoShapeIndexer implements AbstractGeometryFieldMapper.Indexer { private static final double DATELINE = 180; @@ -60,7 +60,7 @@ public final class GeometryIndexer { private final boolean orientation; - public GeometryIndexer(boolean orientation) { + public GeoShapeIndexer(boolean orientation) { this.orientation = orientation; } @@ -176,6 +176,11 @@ public Geometry visit(Rectangle rectangle) { }); } + @Override + public Class processedClass() { + return Geometry.class; + } + /** * Calculate the intersection of a line segment and a vertical dateline. * @@ -666,10 +671,8 @@ private static Edge[] concat(int component, boolean direction, Point[] points, f * Array of edges will be ordered asc by the y-coordinate of the * intersections of edges. * - * @param dateline - * x-coordinate of the dateline - * @param edges - * set of edges that may intersect with the dateline + * @param dateline x-coordinate of the dateline + * @param edges set of edges that may intersect with the dateline * @return number of intersecting edges */ protected static int intersections(double dateline, Edge[] edges) { @@ -697,10 +700,10 @@ private static Edge[] edges(Edge[] edges, int numHoles, List> comp for (int i = 0; i < edges.length; i++) { if (edges[i].component >= 0) { - double[] partitionPoint = new double[3]; - int length = component(edges[i], -(components.size()+numHoles+1), mainEdges, partitionPoint); + double[] partitionPoint = new double[3]; + int length = component(edges[i], -(components.size() + numHoles + 1), mainEdges, partitionPoint); List component = new ArrayList<>(); - component.add(coordinates(edges[i], new Point[length+1], partitionPoint)); + component.add(coordinates(edges[i], new Point[length + 1], partitionPoint)); components.add(component); } } @@ -781,22 +784,22 @@ private static void assign(Edge[] holes, Point[][] points, int numHoles, Edge[] * This method sets the component id of all edges in a ring to a given id and shifts the * coordinates of this component according to the dateline * - * @param edge An arbitrary edge of the component - * @param id id to apply to the component + * @param edge An arbitrary edge of the component + * @param id id to apply to the component * @param edges a list of edges to which all edges of the component will be added (could be null) * @return number of edges that belong to this component */ private static int component(final Edge edge, final int id, final ArrayList edges, double[] partitionPoint) { // find a coordinate that is not part of the dateline Edge any = edge; - while(any.coordinate.getLon() == +DATELINE || any.coordinate.getLon() == -DATELINE) { - if((any = any.next) == edge) { + while (any.coordinate.getLon() == +DATELINE || any.coordinate.getLon() == -DATELINE) { + if ((any = any.next) == edge) { break; } } double shiftOffset = any.coordinate.getLon() > DATELINE ? DATELINE : (any.coordinate.getLon() < -DATELINE ? -DATELINE : 0); - + // run along the border of the component, collect the // edges, shift them according to the dateline and // update the component id @@ -847,14 +850,15 @@ private static int component(final Edge edge, final int id, final ArrayList buildPoints(List> components) { - List result = new ArrayList<>(components.size()); + private static List buildPoints(List> components) { + List result = new ArrayList<>(components.size()); for (int i = 0; i < components.size(); i++) { List component = components.get(i); result.add(buildPolygon(component)); @@ -885,7 +889,7 @@ private static List buildPoints(List> components) { } private static Polygon buildPolygon(List polygon) { - List holes; + List holes; Point[] shell = polygon.get(0); if (polygon.size() > 1) { holes = new ArrayList<>(polygon.size() - 1); @@ -899,7 +903,7 @@ private static Polygon buildPolygon(List polygon) { x[c] = normalizeLon(coords[c].getLon()); y[c] = normalizeLat(coords[c].getLat()); } - holes.add(new org.elasticsearch.geo.geometry.LinearRing(y, x)); + holes.add(new LinearRing(y, x)); } } else { holes = Collections.emptyList(); @@ -924,11 +928,12 @@ private static Point[][] holes(Edge[] holes, int numHoles) { final Point[][] points = new Point[numHoles][]; for (int i = 0; i < numHoles; i++) { - double[] partitionPoint = new double[3]; - int length = component(holes[i], -(i+1), null, partitionPoint); // mark as visited by inverting the sign - points[i] = coordinates(holes[i], new Point[length+1], partitionPoint); + double[] partitionPoint = new double[3]; + int length = component(holes[i], -(i + 1), null, partitionPoint); // mark as visited by inverting the sign + points[i] = coordinates(holes[i], new Point[length + 1], partitionPoint); } return points; } + } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/LegacyGeoShapeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/LegacyGeoShapeFieldMapper.java index c4996eab901dd..47adcbeb97237 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/LegacyGeoShapeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/LegacyGeoShapeFieldMapper.java @@ -46,6 +46,7 @@ import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.index.query.LegacyGeoShapeQueryProcessor; import org.locationtech.spatial4j.shape.Point; import org.locationtech.spatial4j.shape.Shape; import org.locationtech.spatial4j.shape.jts.JtsGeometry; @@ -79,7 +80,7 @@ * @deprecated use {@link GeoShapeFieldMapper} */ @Deprecated -public class LegacyGeoShapeFieldMapper extends BaseGeoShapeFieldMapper { +public class LegacyGeoShapeFieldMapper extends AbstractGeometryFieldMapper, Shape> { public static final String CONTENT_TYPE = "geo_shape"; @@ -183,7 +184,8 @@ private static void checkPrefixTreeSupport(String fieldName) { private static final Logger logger = LogManager.getLogger(LegacyGeoShapeFieldMapper.class); private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(logger); - public static class Builder extends BaseGeoShapeFieldMapper.Builder { + public static class Builder extends AbstractGeometryFieldMapper.Builder { DeprecatedParameters deprecatedParameters; @@ -268,6 +270,10 @@ private void setupPrefixTrees() { protected void setupFieldType(BuilderContext context) { super.setupFieldType(context); + fieldType().setGeometryIndexer(new LegacyGeoShapeIndexer()); + fieldType().setGeometryParser(ShapeParser::parse); + fieldType().setGeometryQueryBuilder(new LegacyGeoShapeQueryProcessor(fieldType())); + // field mapper handles this at build time // but prefix tree strategies require a name, so throw a similar exception if (fieldType().name().isEmpty()) { @@ -297,7 +303,7 @@ public LegacyGeoShapeFieldMapper build(BuilderContext context) { } } - public static final class GeoShapeFieldType extends BaseGeoShapeFieldType { + public static final class GeoShapeFieldType extends AbstractGeometryFieldType { private String tree = DeprecatedParameters.Defaults.TREE; private SpatialStrategy strategy = DeprecatedParameters.Defaults.STRATEGY; @@ -355,6 +361,11 @@ public int hashCode() { defaultDistanceErrorPct); } + @Override + public String typeName() { + return CONTENT_TYPE; + } + @Override public void checkCompatibility(MappedFieldType fieldType, List conflicts) { super.checkCompatibility(fieldType, conflicts); @@ -477,42 +488,26 @@ public GeoShapeFieldType fieldType() { } @Override - public void parse(ParseContext context) throws IOException { - try { - Shape shape = context.parseExternalValue(Shape.class); - if (shape == null) { - ShapeBuilder shapeBuilder = ShapeParser.parse(context.parser(), this); - if (shapeBuilder == null) { - return; - } - shape = shapeBuilder.buildS4J(); - } - if (fieldType().pointsOnly() == true) { - // index configured for pointsOnly - if (shape instanceof XShapeCollection && XShapeCollection.class.cast(shape).pointsOnly()) { - // MULTIPOINT data: index each point separately - List shapes = ((XShapeCollection) shape).getShapes(); - for (Shape s : shapes) { - indexShape(context, s); - } - return; - } else if (shape instanceof Point == false) { - throw new MapperParsingException("[{" + fieldType().name() + "}] is configured for points only but a " - + ((shape instanceof JtsGeometry) ? ((JtsGeometry)shape).getGeom().getGeometryType() : shape.getClass()) - + " was found"); + protected void indexShape(ParseContext context, Shape shape) { + if (fieldType().pointsOnly() == true) { + // index configured for pointsOnly + if (shape instanceof XShapeCollection && XShapeCollection.class.cast(shape).pointsOnly()) { + // MULTIPOINT data: index each point separately + @SuppressWarnings("unchecked") List shapes = ((XShapeCollection) shape).getShapes(); + for (Shape s : shapes) { + doIndexShape(context, s); } + return; + } else if (shape instanceof Point == false) { + throw new MapperParsingException("[{" + fieldType().name() + "}] is configured for points only but a " + + ((shape instanceof JtsGeometry) ? ((JtsGeometry)shape).getGeom().getGeometryType() : shape.getClass()) + + " was found"); } - indexShape(context, shape); - } catch (Exception e) { - if (ignoreMalformed.value() == false) { - throw new MapperParsingException("failed to parse field [{}] of type [{}]", e, fieldType().name(), - fieldType().typeName()); - } - context.addIgnoredField(fieldType.name()); } + doIndexShape(context, shape); } - private void indexShape(ParseContext context, Shape shape) { + private void doIndexShape(ParseContext context, Shape shape) { List fields = new ArrayList<>(Arrays.asList(fieldType().defaultPrefixTreeStrategy().createIndexableFields(shape))); createFieldNamesField(context, fields); for (IndexableField field : fields) { @@ -571,4 +566,9 @@ protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, } } } + + @Override + protected String contentType() { + return CONTENT_TYPE; + } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/LegacyGeoShapeIndexer.java b/server/src/main/java/org/elasticsearch/index/mapper/LegacyGeoShapeIndexer.java new file mode 100644 index 0000000000000..80e3a505f5c35 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/mapper/LegacyGeoShapeIndexer.java @@ -0,0 +1,35 @@ +/* + * 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.mapper; + +import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.locationtech.spatial4j.shape.Shape; + +public class LegacyGeoShapeIndexer implements AbstractGeometryFieldMapper.Indexer, Shape> { + @Override + public Shape prepareForIndexing(ShapeBuilder shapeBuilder) { + return shapeBuilder.buildS4J(); + } + + @Override + public Class processedClass() { + return Shape.class; + } +} 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 57bdb4446beac..7f54f8d261a0b 100644 --- a/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java @@ -20,33 +20,13 @@ package org.elasticsearch.index.query; import org.apache.logging.log4j.LogManager; -import org.apache.lucene.document.LatLonShape; -import org.apache.lucene.geo.Line; -import org.apache.lucene.geo.Polygon; -import org.apache.lucene.search.BooleanClause; -import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.ConstantScoreQuery; -import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; -import org.apache.lucene.spatial.prefix.PrefixTreeStrategy; -import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy; -import org.apache.lucene.spatial.query.SpatialArgs; -import org.apache.lucene.spatial.query.SpatialOperation; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; -import org.elasticsearch.common.geo.GeoShapeType; -import org.elasticsearch.common.geo.GeometryIndexer; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.SpatialStrategy; -import org.elasticsearch.common.geo.builders.EnvelopeBuilder; -import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder; -import org.elasticsearch.common.geo.builders.LineStringBuilder; -import org.elasticsearch.common.geo.builders.MultiLineStringBuilder; -import org.elasticsearch.common.geo.builders.MultiPointBuilder; -import org.elasticsearch.common.geo.builders.MultiPolygonBuilder; -import org.elasticsearch.common.geo.builders.PointBuilder; -import org.elasticsearch.common.geo.builders.PolygonBuilder; import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.geo.parsers.ShapeParser; import org.elasticsearch.common.io.stream.StreamInput; @@ -54,32 +34,17 @@ import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.geo.geometry.Circle; import org.elasticsearch.geo.geometry.Geometry; -import org.elasticsearch.geo.geometry.GeometryCollection; -import org.elasticsearch.geo.geometry.GeometryVisitor; -import org.elasticsearch.geo.geometry.LinearRing; -import org.elasticsearch.geo.geometry.MultiLine; -import org.elasticsearch.geo.geometry.MultiPoint; -import org.elasticsearch.geo.geometry.MultiPolygon; -import org.elasticsearch.geo.geometry.Point; -import org.elasticsearch.geo.geometry.Rectangle; -import org.elasticsearch.index.mapper.BaseGeoShapeFieldMapper; +import org.elasticsearch.index.mapper.AbstractGeometryFieldMapper; import org.elasticsearch.index.mapper.GeoShapeFieldMapper; -import org.elasticsearch.index.mapper.LegacyGeoShapeFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.spatial4j.shape.Shape; 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; -import static org.elasticsearch.index.mapper.GeoShapeFieldMapper.toLucenePolygon; - /** * Derived {@link AbstractGeometryQueryBuilder} that builds a lat, lon GeoShape Query */ @@ -217,12 +182,12 @@ public SpatialStrategy strategy() { @Override protected List validContentTypes() { - return Arrays.asList(BaseGeoShapeFieldMapper.CONTENT_TYPE); + return Arrays.asList(GeoShapeFieldMapper.CONTENT_TYPE); } @Override public String queryFieldType() { - return BaseGeoShapeFieldMapper.CONTENT_TYPE; + return GeoShapeFieldMapper.CONTENT_TYPE; } @Override @@ -245,260 +210,13 @@ protected GeoShapeQueryBuilder newShapeQueryBuilder(String fieldName, Supplier geometryToShapeBuilder(Geometry geometry) { - ShapeBuilder shapeBuilder = geometry.visit(new GeometryVisitor<>() { - @Override - public ShapeBuilder visit(Circle circle) { - throw new UnsupportedOperationException("circle is not supported"); - } - - @Override - public ShapeBuilder visit(GeometryCollection collection) { - GeometryCollectionBuilder shapes = new GeometryCollectionBuilder(); - for (Geometry geometry : collection) { - shapes.shape(geometry.visit(this)); - } - return shapes; - } - - @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); - } - - @Override - public ShapeBuilder visit(LinearRing ring) { - throw new UnsupportedOperationException("circle is not supported"); - } - - @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; - } - - @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); - } - - @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; - } - - @Override - public ShapeBuilder visit(Point point) { - return new PointBuilder(point.getLon(), point.getLat()); - } - - @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; - } - - @Override - public ShapeBuilder visit(Rectangle rectangle) { - return new EnvelopeBuilder(new Coordinate(rectangle.getMinLon(), rectangle.getMaxLat()), - new Coordinate(rectangle.getMaxLon(), rectangle.getMinLat())); - } - }); - return shapeBuilder; - } - - private class ShapeVisitor implements GeometryVisitor { - QueryShardContext context; - MappedFieldType fieldType; - - ShapeVisitor(QueryShardContext context) { - this.context = context; - this.fieldType = context.fieldMapper(fieldName); - } - - @Override - public Query visit(Circle circle) { - throw new QueryShardException(context, "Field [" + fieldName + "] found and unknown shape Circle"); - } - - @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); - } - } - } - - @Override - public Query visit(org.elasticsearch.geo.geometry.Line line) { - validateIsGeoShapeFieldType(); - 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) { - validateIsGeoShapeFieldType(); - Line[] lines = new Line[multiLine.size()]; - for (int i=0; i + * 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<>() { + @Override + public ShapeBuilder visit(Circle circle) { + throw new UnsupportedOperationException("circle is not supported"); + } + + @Override + public ShapeBuilder visit(GeometryCollection collection) { + GeometryCollectionBuilder shapes = new GeometryCollectionBuilder(); + for (Geometry geometry : collection) { + shapes.shape(geometry.visit(this)); + } + return shapes; + } + + @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); + } + + @Override + public ShapeBuilder visit(LinearRing ring) { + throw new UnsupportedOperationException("circle is not supported"); + } + + @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; + } + + @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); + } + + @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; + } + + @Override + public ShapeBuilder visit(Point point) { + return new PointBuilder(point.getLon(), point.getLat()); + } + + @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; + } + + @Override + public ShapeBuilder visit(Rectangle rectangle) { + return new EnvelopeBuilder(new Coordinate(rectangle.getMinLon(), rectangle.getMaxLat()), + new Coordinate(rectangle.getMaxLon(), rectangle.getMinLat())); + } + }); + return shapeBuilder; + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java b/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java new file mode 100644 index 0000000000000..1012e2ec045f3 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java @@ -0,0 +1,171 @@ +/* + * 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.document.LatLonShape; +import org.apache.lucene.geo.Line; +import org.apache.lucene.geo.Polygon; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.geo.GeoShapeType; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.geo.SpatialStrategy; +import org.elasticsearch.geo.geometry.Circle; +import org.elasticsearch.geo.geometry.Geometry; +import org.elasticsearch.geo.geometry.GeometryCollection; +import org.elasticsearch.geo.geometry.GeometryVisitor; +import org.elasticsearch.geo.geometry.LinearRing; +import org.elasticsearch.geo.geometry.MultiLine; +import org.elasticsearch.geo.geometry.MultiPoint; +import org.elasticsearch.geo.geometry.MultiPolygon; +import org.elasticsearch.geo.geometry.Point; +import org.elasticsearch.index.mapper.AbstractGeometryFieldMapper; +import org.elasticsearch.index.mapper.GeoShapeFieldMapper; +import org.elasticsearch.index.mapper.GeoShapeIndexer; +import org.elasticsearch.index.mapper.MappedFieldType; + +import static org.elasticsearch.index.mapper.GeoShapeFieldMapper.toLucenePolygon; + +public class VectorGeoShapeQueryProcessor implements AbstractGeometryFieldMapper.QueryProcessor { + + @Override + public Query process(Geometry shape, String fieldName, SpatialStrategy strategy, ShapeRelation relation, QueryShardContext context) { + // 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(shape, fieldName, relation, context); + } + + protected Query getVectorQueryFromShape(Geometry queryShape, String fieldName, ShapeRelation relation, QueryShardContext context) { + GeoShapeIndexer geometryIndexer = new GeoShapeIndexer(true); + + Geometry processedShape = geometryIndexer.prepareForIndexing(queryShape); + + if (processedShape == null) { + return new MatchNoDocsQuery(); + } + return queryShape.visit(new ShapeVisitor(context, fieldName, relation)); + } + + private class ShapeVisitor implements GeometryVisitor { + QueryShardContext context; + MappedFieldType fieldType; + String fieldName; + ShapeRelation relation; + + ShapeVisitor(QueryShardContext context, String fieldName, ShapeRelation relation) { + this.context = context; + this.fieldType = context.fieldMapper(fieldName); + this.fieldName = fieldName; + this.relation = relation; + } + + @Override + public Query visit(Circle circle) { + throw new QueryShardException(context, "Field [" + fieldName + "] found and unknown shape Circle"); + } + + @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); + } + } + } + + @Override + public Query visit(org.elasticsearch.geo.geometry.Line line) { + validateIsGeoShapeFieldType(); + 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) { + validateIsGeoShapeFieldType(); + Line[] lines = new Line[multiLine.size()]; + for (int i = 0; i < multiLine.size(); i++) { + lines[i] = new Line(multiLine.get(i).getLats(), multiLine.get(i).getLons()); + } + return LatLonShape.newLineQuery(fieldName, relation.getLuceneRelation(), lines); + } + + @Override + public Query visit(MultiPoint multiPoint) { + throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + GeoShapeType.MULTIPOINT + + " queries"); + } + + @Override + public Query visit(MultiPolygon multiPolygon) { + Polygon[] polygons = new Polygon[multiPolygon.size()]; + for (int i = 0; i < multiPolygon.size(); i++) { + polygons[i] = toLucenePolygon(multiPolygon.get(i)); + } + return LatLonShape.newPolygonQuery(fieldName, relation.getLuceneRelation(), polygons); + } + + @Override + public Query visit(Point point) { + validateIsGeoShapeFieldType(); + return LatLonShape.newBoxQuery(fieldName, relation.getLuceneRelation(), + point.getLat(), point.getLat(), point.getLon(), point.getLon()); + } + + @Override + public Query visit(org.elasticsearch.geo.geometry.Polygon polygon) { + return LatLonShape.newPolygonQuery(fieldName, relation.getLuceneRelation(), toLucenePolygon(polygon)); + } + + @Override + public Query visit(org.elasticsearch.geo.geometry.Rectangle r) { + return LatLonShape.newBoxQuery(fieldName, relation.getLuceneRelation(), + r.getMinLat(), r.getMaxLat(), r.getMinLon(), r.getMaxLon()); + } + + private void validateIsGeoShapeFieldType() { + if (fieldType instanceof GeoShapeFieldMapper.GeoShapeFieldType == false) { + throw new QueryShardException(context, "Expected " + GeoShapeFieldMapper.CONTENT_TYPE + + " field type for Field [" + fieldName + "] but found " + fieldType.typeName()); + } + } + } + +} + diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java index b167c30e32c6e..de79acd7c2317 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java @@ -31,7 +31,7 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.engine.EngineFactory; -import org.elasticsearch.index.mapper.BaseGeoShapeFieldMapper; +import org.elasticsearch.index.mapper.AbstractGeometryFieldMapper; import org.elasticsearch.index.mapper.BinaryFieldMapper; import org.elasticsearch.index.mapper.BooleanFieldMapper; import org.elasticsearch.index.mapper.CompletionFieldMapper; @@ -39,6 +39,7 @@ import org.elasticsearch.index.mapper.FieldAliasMapper; import org.elasticsearch.index.mapper.FieldNamesFieldMapper; import org.elasticsearch.index.mapper.GeoPointFieldMapper; +import org.elasticsearch.index.mapper.GeoShapeFieldMapper; import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.IgnoredFieldMapper; import org.elasticsearch.index.mapper.IndexFieldMapper; @@ -132,7 +133,7 @@ public static Map getMappers(List mappe mappers.put(CompletionFieldMapper.CONTENT_TYPE, new CompletionFieldMapper.TypeParser()); mappers.put(FieldAliasMapper.CONTENT_TYPE, new FieldAliasMapper.TypeParser()); mappers.put(GeoPointFieldMapper.CONTENT_TYPE, new GeoPointFieldMapper.TypeParser()); - mappers.put(BaseGeoShapeFieldMapper.CONTENT_TYPE, new BaseGeoShapeFieldMapper.TypeParser()); + mappers.put(GeoShapeFieldMapper.CONTENT_TYPE, new AbstractGeometryFieldMapper.TypeParser()); for (MapperPlugin mapperPlugin : mapperPlugins) { for (Map.Entry entry : mapperPlugin.getMappers().entrySet()) { diff --git a/server/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java b/server/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java index 9e5d7d7c6ce09..2a2f7ce75a3ab 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java +++ b/server/src/test/java/org/elasticsearch/common/geo/BaseGeoParsingTestCase.java @@ -22,6 +22,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.geo.utils.GeographyValidator; +import org.elasticsearch.index.mapper.GeoShapeIndexer; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.hamcrest.ElasticsearchGeoAssertions; import org.locationtech.jts.geom.Geometry; @@ -66,7 +67,7 @@ protected void assertGeometryEquals(Object expected, XContentBuilder geoJson, bo } else { GeometryParser geometryParser = new GeometryParser(true, true, true); org.elasticsearch.geo.geometry.Geometry shape = geometryParser.parse(parser); - shape = new GeometryIndexer(true).prepareForIndexing(shape); + shape = new GeoShapeIndexer(true).prepareForIndexing(shape); ElasticsearchGeoAssertions.assertEquals(expected, shape); } } diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeoJsonShapeParserTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeoJsonShapeParserTests.java index ccfc599f4cadc..1242acc635f6d 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeoJsonShapeParserTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeoJsonShapeParserTests.java @@ -35,6 +35,7 @@ import org.elasticsearch.geo.geometry.MultiLine; import org.elasticsearch.geo.geometry.MultiPoint; import org.elasticsearch.index.mapper.ContentPath; +import org.elasticsearch.index.mapper.GeoShapeIndexer; import org.elasticsearch.index.mapper.LegacyGeoShapeFieldMapper; import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.test.VersionUtils; @@ -1425,7 +1426,7 @@ public void testParseInvalidGeometryCollectionShapes() throws IOException { public Geometry parse(XContentParser parser) throws IOException, ParseException { GeometryParser geometryParser = new GeometryParser(true, true, true); - GeometryIndexer indexer = new GeometryIndexer(true); + GeoShapeIndexer indexer = new GeoShapeIndexer(true); return indexer.prepareForIndexing(geometryParser.parse(parser)); } } diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeoWKTShapeParserTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeoWKTShapeParserTests.java index d8559b3b1260e..11153f6679720 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeoWKTShapeParserTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeoWKTShapeParserTests.java @@ -47,6 +47,7 @@ import org.elasticsearch.geo.geometry.MultiPoint; import org.elasticsearch.index.mapper.ContentPath; import org.elasticsearch.index.mapper.GeoShapeFieldMapper; +import org.elasticsearch.index.mapper.GeoShapeIndexer; import org.elasticsearch.index.mapper.LegacyGeoShapeFieldMapper; import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.test.geo.RandomShapeGenerator; @@ -470,7 +471,7 @@ public void testParseGeometryCollection() throws IOException, ParseException { } else { GeometryCollectionBuilder gcb = RandomShapeGenerator.createGeometryCollection(random()); assertExpected(gcb.buildS4J(), gcb, true); - assertExpected(new GeometryIndexer(true).prepareForIndexing(gcb.buildGeometry()), gcb, false); + assertExpected(new GeoShapeIndexer(true).prepareForIndexing(gcb.buildGeometry()), gcb, false); } } diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeometryIOTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeometryIOTests.java index 14fc710e2683c..7694d6c3d7d62 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeometryIOTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeometryIOTests.java @@ -30,7 +30,7 @@ import org.elasticsearch.test.ESTestCase; import static org.elasticsearch.geo.GeometryTestUtils.randomGeometry; -import static org.elasticsearch.index.query.GeoShapeQueryBuilder.geometryToShapeBuilder; +import static org.elasticsearch.index.query.LegacyGeoShapeQueryProcessor.geometryToShapeBuilder; public class GeometryIOTests extends ESTestCase { diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeometryIndexerTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeometryIndexerTests.java index 5ab5aaff33e05..12a3432eb36f2 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeometryIndexerTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeometryIndexerTests.java @@ -33,6 +33,7 @@ import org.elasticsearch.geo.geometry.Point; import org.elasticsearch.geo.geometry.Polygon; import org.elasticsearch.geo.utils.WellKnownText; +import org.elasticsearch.index.mapper.GeoShapeIndexer; import org.elasticsearch.test.ESTestCase; import java.io.IOException; @@ -42,7 +43,7 @@ public class GeometryIndexerTests extends ESTestCase { - GeometryIndexer indexer = new GeometryIndexer(true); + GeoShapeIndexer indexer = new GeoShapeIndexer(true); private static final WellKnownText WKT = new WellKnownText(true, geometry -> { }); @@ -208,13 +209,13 @@ private Geometry expected(String wkt) throws IOException, ParseException { private Geometry actual(String wkt, boolean rightOrientation) throws IOException, ParseException { Geometry shape = parseGeometry(wkt, rightOrientation); - return new GeometryIndexer(true).prepareForIndexing(shape); + return new GeoShapeIndexer(true).prepareForIndexing(shape); } private Geometry actual(XContentBuilder geoJson, boolean rightOrientation) throws IOException, ParseException { Geometry shape = parseGeometry(geoJson, rightOrientation); - return new GeometryIndexer(true).prepareForIndexing(shape); + return new GeoShapeIndexer(true).prepareForIndexing(shape); } private Geometry parseGeometry(String wkt, boolean rightOrientation) throws IOException, ParseException { diff --git a/server/src/test/java/org/elasticsearch/common/geo/ShapeBuilderTests.java b/server/src/test/java/org/elasticsearch/common/geo/ShapeBuilderTests.java index bd6c4a2da557f..40ff3e8d44bc1 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/ShapeBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/ShapeBuilderTests.java @@ -27,6 +27,7 @@ import org.elasticsearch.common.geo.builders.PointBuilder; import org.elasticsearch.common.geo.builders.PolygonBuilder; import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.index.mapper.GeoShapeIndexer; import org.elasticsearch.test.ESTestCase; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.LineString; @@ -778,6 +779,6 @@ public void testInvalidSelfCrossingPolygon() { } public Object buildGeometry(ShapeBuilder builder) { - return new GeometryIndexer(true).prepareForIndexing(builder.buildGeometry()); + return new GeoShapeIndexer(true).prepareForIndexing(builder.buildGeometry()); } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/ExternalMapper.java b/server/src/test/java/org/elasticsearch/index/mapper/ExternalMapper.java index c165d55e3a5f5..b744ad1609488 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/ExternalMapper.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/ExternalMapper.java @@ -86,7 +86,7 @@ public ExternalMapper build(BuilderContext context) { BinaryFieldMapper binMapper = binBuilder.build(context); BooleanFieldMapper boolMapper = boolBuilder.build(context); GeoPointFieldMapper pointMapper = latLonPointBuilder.build(context); - BaseGeoShapeFieldMapper shapeMapper = shapeBuilder.build(context); + AbstractGeometryFieldMapper shapeMapper = shapeBuilder.build(context); FieldMapper stringMapper = (FieldMapper)stringBuilder.build(context); context.path().remove(); @@ -150,13 +150,13 @@ public Query existsQuery(QueryShardContext context) { private BinaryFieldMapper binMapper; private BooleanFieldMapper boolMapper; private GeoPointFieldMapper pointMapper; - private BaseGeoShapeFieldMapper shapeMapper; + private AbstractGeometryFieldMapper shapeMapper; private FieldMapper stringMapper; public ExternalMapper(String simpleName, MappedFieldType fieldType, String generatedValue, String mapperName, BinaryFieldMapper binMapper, BooleanFieldMapper boolMapper, GeoPointFieldMapper pointMapper, - BaseGeoShapeFieldMapper shapeMapper, FieldMapper stringMapper, Settings indexSettings, + AbstractGeometryFieldMapper shapeMapper, FieldMapper stringMapper, Settings indexSettings, MultiFields multiFields, CopyTo copyTo) { super(simpleName, fieldType, new ExternalFieldType(), indexSettings, multiFields, copyTo); this.generatedValue = generatedValue; @@ -214,7 +214,7 @@ public FieldMapper updateFieldType(Map fullNameToFieldT BinaryFieldMapper binMapperUpdate = (BinaryFieldMapper) binMapper.updateFieldType(fullNameToFieldType); BooleanFieldMapper boolMapperUpdate = (BooleanFieldMapper) boolMapper.updateFieldType(fullNameToFieldType); GeoPointFieldMapper pointMapperUpdate = (GeoPointFieldMapper) pointMapper.updateFieldType(fullNameToFieldType); - BaseGeoShapeFieldMapper shapeMapperUpdate = (BaseGeoShapeFieldMapper) shapeMapper.updateFieldType(fullNameToFieldType); + AbstractGeometryFieldMapper shapeMapperUpdate = (AbstractGeometryFieldMapper) shapeMapper.updateFieldType(fullNameToFieldType); TextFieldMapper stringMapperUpdate = (TextFieldMapper) stringMapper.updateFieldType(fullNameToFieldType); if (update == this && multiFieldsUpdate == multiFields diff --git a/server/src/test/java/org/elasticsearch/index/mapper/GeoShapeFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/GeoShapeFieldMapperTests.java index a5e2d7c31afe2..65485bd5f9782 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/GeoShapeFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/GeoShapeFieldMapperTests.java @@ -280,7 +280,8 @@ public void testSerializeDefaults() throws Exception { .endObject().endObject()); DocumentMapper defaultMapper = parser.parse("type1", new CompressedXContent(mapping)); String serialized = toXContentString((GeoShapeFieldMapper) defaultMapper.mappers().getMapper("location")); - assertTrue(serialized, serialized.contains("\"orientation\":\"" + BaseGeoShapeFieldMapper.Defaults.ORIENTATION.value() + "\"")); + assertTrue(serialized, serialized.contains("\"orientation\":\"" + + AbstractGeometryFieldMapper.Defaults.ORIENTATION.value() + "\"")); } }