diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeoShapeUtils.java b/server/src/main/java/org/elasticsearch/common/geo/GeoShapeUtils.java index 7e23385f2adf8..b3105a61e1a57 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeoShapeUtils.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeoShapeUtils.java @@ -18,11 +18,24 @@ */ package org.elasticsearch.common.geo; +import org.apache.lucene.geo.LatLonGeometry; import org.elasticsearch.geometry.Circle; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.GeometryCollection; +import org.elasticsearch.geometry.GeometryVisitor; import org.elasticsearch.geometry.Line; +import org.elasticsearch.geometry.LinearRing; +import org.elasticsearch.geometry.MultiLine; +import org.elasticsearch.geometry.MultiPoint; +import org.elasticsearch.geometry.MultiPolygon; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.Polygon; import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.index.query.QueryShardException; + +import java.util.ArrayList; +import java.util.List; /** @@ -60,6 +73,120 @@ public static org.apache.lucene.geo.Circle toLuceneCircle(Circle circle) { return new org.apache.lucene.geo.Circle(circle.getLat(), circle.getLon(), circle.getRadiusMeters()); } + public static LatLonGeometry[] toLuceneGeometry( + String name, + QueryShardContext context, + Geometry geometry, + List> unsupportedGeometries + ) { + final List geometries = new ArrayList<>(); + geometry.visit(new GeometryVisitor<>() { + @Override + public Void visit(Circle circle) { + checkSupported(circle); + if (circle.isEmpty() == false) { + geometries.add(GeoShapeUtils.toLuceneCircle(circle)); + } + return null; + } + + @Override + public Void visit(GeometryCollection collection) { + checkSupported(collection); + if (collection.isEmpty() == false) { + for (Geometry shape : collection) { + shape.visit(this); + } + } + return null; + } + + @Override + public Void visit(org.elasticsearch.geometry.Line line) { + checkSupported(line); + if (line.isEmpty() == false) { + geometries.add(GeoShapeUtils.toLuceneLine(line)); + } + return null; + } + + @Override + public Void visit(LinearRing ring) { + throw new QueryShardException(context, "Field [" + name + "] found and unsupported shape LinearRing"); + } + + @Override + public Void visit(MultiLine multiLine) { + checkSupported(multiLine); + if (multiLine.isEmpty() == false) { + for (Line line : multiLine) { + visit(line); + } + } + return null; + } + + @Override + public Void visit(MultiPoint multiPoint) { + checkSupported(multiPoint); + if (multiPoint.isEmpty() == false) { + for (Point point : multiPoint) { + visit(point); + } + } + return null; + } + + @Override + public Void visit(MultiPolygon multiPolygon) { + checkSupported(multiPolygon); + if (multiPolygon.isEmpty() == false) { + for (Polygon polygon : multiPolygon) { + visit(polygon); + } + } + return null; + } + + @Override + public Void visit(Point point) { + checkSupported(point); + if (point.isEmpty() == false) { + geometries.add(toLucenePoint(point)); + } + return null; + + } + + @Override + public Void visit(org.elasticsearch.geometry.Polygon polygon) { + checkSupported(polygon); + if (polygon.isEmpty() == false) { + List collector = new ArrayList<>(); + GeoPolygonDecomposer.decomposePolygon(polygon, true, collector); + collector.forEach((p) -> geometries.add(toLucenePolygon(p))); + } + return null; + } + + @Override + public Void visit(Rectangle r) { + checkSupported(r); + if (r.isEmpty() == false) { + geometries.add(toLuceneRectangle(r)); + } + return null; + } + + private void checkSupported(Geometry geometry) { + if (unsupportedGeometries.contains(geometry.getClass())) { + throw new QueryShardException(context, "Field [" + name + "] found and unsupported shape [" + geometry.type() + "]"); + } + } + }); + return geometries.toArray(new LatLonGeometry[geometries.size()]); + } + private GeoShapeUtils() { } diff --git a/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java b/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java index eb519011faa32..24f58bf55fd63 100644 --- a/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java +++ b/server/src/main/java/org/elasticsearch/index/query/VectorGeoShapeQueryProcessor.java @@ -24,29 +24,25 @@ import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; import org.elasticsearch.Version; -import org.elasticsearch.common.geo.GeoLineDecomposer; -import org.elasticsearch.common.geo.GeoPolygonDecomposer; import org.elasticsearch.common.geo.GeoShapeUtils; import org.elasticsearch.common.geo.ShapeRelation; -import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; -import org.elasticsearch.geometry.GeometryCollection; -import org.elasticsearch.geometry.GeometryVisitor; import org.elasticsearch.geometry.Line; -import org.elasticsearch.geometry.LinearRing; import org.elasticsearch.geometry.MultiLine; -import org.elasticsearch.geometry.MultiPoint; -import org.elasticsearch.geometry.MultiPolygon; -import org.elasticsearch.geometry.Point; -import org.elasticsearch.geometry.Polygon; -import org.elasticsearch.geometry.Rectangle; import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class VectorGeoShapeQueryProcessor { + private static final List> WITHIN_UNSUPPORTED_GEOMETRIES = new ArrayList<>(); + static { + WITHIN_UNSUPPORTED_GEOMETRIES.add(Line.class); + WITHIN_UNSUPPORTED_GEOMETRIES.add(MultiLine.class); + } + public Query geoShapeQuery(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) { // CONTAINS queries are not supported by VECTOR strategy for indices created before version 7.5.0 (Lucene 8.3.0) if (relation == ShapeRelation.CONTAINS && context.indexVersionCreated().before(Version.V_7_5_0)) { @@ -58,125 +54,16 @@ public Query geoShapeQuery(Geometry shape, String fieldName, ShapeRelation relat } private Query getVectorQueryFromShape(Geometry queryShape, String fieldName, ShapeRelation relation, QueryShardContext context) { - final LuceneGeometryCollector visitor = new LuceneGeometryCollector(fieldName, context); - queryShape.visit(visitor); - final List geometries = visitor.geometries(); - if (geometries.size() == 0) { - return new MatchNoDocsQuery(); - } - return LatLonShape.newGeometryQuery(fieldName, relation.getLuceneRelation(), - geometries.toArray(new LatLonGeometry[geometries.size()])); - } - - private static class LuceneGeometryCollector implements GeometryVisitor { - private final List geometries = new ArrayList<>(); - private final String name; - private final QueryShardContext context; - - private LuceneGeometryCollector(String name, QueryShardContext context) { - this.name = name; - this.context = context; - } - - List geometries() { - return geometries; - } - - @Override - public Void visit(Circle circle) { - if (circle.isEmpty() == false) { - geometries.add(GeoShapeUtils.toLuceneCircle(circle)); - } - return null; - } - - @Override - public Void visit(GeometryCollection collection) { - for (Geometry shape : collection) { - shape.visit(this); - } - return null; - } - - @Override - public Void visit(org.elasticsearch.geometry.Line line) { - if (line.isEmpty() == false) { - List collector = new ArrayList<>(); - GeoLineDecomposer.decomposeLine(line, collector); - collectLines(collector); - } - return null; - } - - @Override - public Void visit(LinearRing ring) { - throw new QueryShardException(context, "Field [" + name + "] found and unsupported shape LinearRing"); + final LatLonGeometry[] luceneGeometries; + if (relation == ShapeRelation.WITHIN) { + luceneGeometries = GeoShapeUtils.toLuceneGeometry(fieldName, context, queryShape, WITHIN_UNSUPPORTED_GEOMETRIES); + } else { + luceneGeometries = GeoShapeUtils.toLuceneGeometry(fieldName, context, queryShape, Collections.emptyList()); } - - @Override - public Void visit(MultiLine multiLine) { - List collector = new ArrayList<>(); - GeoLineDecomposer.decomposeMultiLine(multiLine, collector); - collectLines(collector); - return null; - } - - @Override - public Void visit(MultiPoint multiPoint) { - for (Point point : multiPoint) { - visit(point); - } - return null; - } - - @Override - public Void visit(MultiPolygon multiPolygon) { - if (multiPolygon.isEmpty() == false) { - List collector = new ArrayList<>(); - GeoPolygonDecomposer.decomposeMultiPolygon(multiPolygon, true, collector); - collectPolygons(collector); - } - return null; - } - - @Override - public Void visit(Point point) { - if (point.isEmpty() == false) { - geometries.add(GeoShapeUtils.toLucenePoint(point)); - } - return null; - - } - - @Override - public Void visit(org.elasticsearch.geometry.Polygon polygon) { - if (polygon.isEmpty() == false) { - List collector = new ArrayList<>(); - GeoPolygonDecomposer.decomposePolygon(polygon, true, collector); - collectPolygons(collector); - } - return null; - } - - @Override - public Void visit(Rectangle r) { - if (r.isEmpty() == false) { - geometries.add(GeoShapeUtils.toLuceneRectangle(r)); - } - return null; - } - - private void collectLines(List geometryLines) { - for (Line line: geometryLines) { - geometries.add(GeoShapeUtils.toLuceneLine(line)); - } - } - - private void collectPolygons(List geometryPolygons) { - for (Polygon polygon : geometryPolygons) { - geometries.add(GeoShapeUtils.toLucenePolygon(polygon)); - } + if (luceneGeometries.length == 0) { + return new MatchNoDocsQuery(); } + return LatLonShape.newGeometryQuery(fieldName, relation.getLuceneRelation(), luceneGeometries); } } diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/RuntimeFields.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/RuntimeFields.java index 730d11a82b30d..62cb58fd94567 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/RuntimeFields.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/RuntimeFields.java @@ -14,6 +14,7 @@ import org.elasticsearch.xpack.runtimefields.mapper.BooleanFieldScript; import org.elasticsearch.xpack.runtimefields.mapper.DateFieldScript; import org.elasticsearch.xpack.runtimefields.mapper.DoubleFieldScript; +import org.elasticsearch.xpack.runtimefields.mapper.GeoPointFieldScript; import org.elasticsearch.xpack.runtimefields.mapper.IpFieldScript; import org.elasticsearch.xpack.runtimefields.mapper.LongFieldScript; import org.elasticsearch.xpack.runtimefields.mapper.RuntimeFieldMapper; @@ -36,6 +37,7 @@ public List> getContexts() { BooleanFieldScript.CONTEXT, DateFieldScript.CONTEXT, DoubleFieldScript.CONTEXT, + GeoPointFieldScript.CONTEXT, IpFieldScript.CONTEXT, LongFieldScript.CONTEXT, StringFieldScript.CONTEXT diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/GeoPointScriptDocValues.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/GeoPointScriptDocValues.java new file mode 100644 index 0000000000000..79a10625a4cc4 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/GeoPointScriptDocValues.java @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.fielddata; + +import org.apache.lucene.geo.GeoEncodingUtils; +import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.index.fielddata.MultiGeoPointValues; +import org.elasticsearch.xpack.runtimefields.mapper.GeoPointFieldScript; + +import java.util.Arrays; + +public final class GeoPointScriptDocValues extends MultiGeoPointValues { + private final GeoPointFieldScript script; + private final GeoPoint point; + private int cursor; + + GeoPointScriptDocValues(GeoPointFieldScript script) { + this.script = script; + this.point = new GeoPoint(); + } + + @Override + public boolean advanceExact(int docId) { + script.runForDoc(docId); + if (script.count() == 0) { + return false; + } + Arrays.sort(script.values(), 0, script.count()); + cursor = 0; + return true; + } + + @Override + public int docValueCount() { + return script.count(); + } + + @Override + public GeoPoint nextValue() { + final long value = script.values()[cursor++]; + final int lat = (int) (value >>> 32); + final int lon = (int) (value & 0xFFFFFFFF); + return point.reset(GeoEncodingUtils.decodeLatitude(lat), GeoEncodingUtils.decodeLongitude(lon)); + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/GeoPointScriptFieldData.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/GeoPointScriptFieldData.java new file mode 100644 index 0000000000000..ef8ef730b73c2 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/fielddata/GeoPointScriptFieldData.java @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.fielddata; + +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.SortField; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.index.fielddata.IndexFieldData; +import org.elasticsearch.index.fielddata.IndexFieldDataCache; +import org.elasticsearch.index.fielddata.IndexGeoPointFieldData; +import org.elasticsearch.index.fielddata.LeafGeoPointFieldData; +import org.elasticsearch.index.fielddata.MultiGeoPointValues; +import org.elasticsearch.index.fielddata.plain.AbstractLeafGeoPointFieldData; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.search.DocValueFormat; +import org.elasticsearch.search.MultiValueMode; +import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; +import org.elasticsearch.search.aggregations.support.ValuesSourceType; +import org.elasticsearch.search.sort.BucketedSort; +import org.elasticsearch.search.sort.SortOrder; +import org.elasticsearch.xpack.runtimefields.mapper.GeoPointFieldScript; + +public class GeoPointScriptFieldData implements IndexGeoPointFieldData { + public static class Builder implements IndexFieldData.Builder { + private final String name; + private final GeoPointFieldScript.LeafFactory leafFactory; + + public Builder(String name, GeoPointFieldScript.LeafFactory leafFactory) { + this.name = name; + this.leafFactory = leafFactory; + } + + @Override + public GeoPointScriptFieldData build(IndexFieldDataCache cache, CircuitBreakerService breakerService) { + return new GeoPointScriptFieldData(name, leafFactory); + } + } + + private final GeoPointFieldScript.LeafFactory leafFactory; + private final String name; + + private GeoPointScriptFieldData(String fieldName, GeoPointFieldScript.LeafFactory leafFactory) { + this.name = fieldName; + this.leafFactory = leafFactory; + } + + @Override + public SortField sortField(Object missingValue, MultiValueMode sortMode, XFieldComparatorSource.Nested nested, boolean reverse) { + throw new IllegalArgumentException("can't sort on geo_point field without using specific sorting feature, like geo_distance"); + } + + @Override + public BucketedSort newBucketedSort( + BigArrays bigArrays, + Object missingValue, + MultiValueMode sortMode, + XFieldComparatorSource.Nested nested, + SortOrder sortOrder, + DocValueFormat format, + int bucketSize, + BucketedSort.ExtraData extra + ) { + throw new IllegalArgumentException("can't sort on geo_point field without using specific sorting feature, like geo_distance"); + } + + @Override + public String getFieldName() { + return name; + } + + @Override + public ValuesSourceType getValuesSourceType() { + return CoreValuesSourceType.GEOPOINT; + } + + @Override + public LeafGeoPointFieldData load(LeafReaderContext context) { + GeoPointFieldScript script = leafFactory.newInstance(context); + return new AbstractLeafGeoPointFieldData() { + @Override + public MultiGeoPointValues getGeoPointValues() { + return new GeoPointScriptDocValues(script); + } + + @Override + public long ramBytesUsed() { + return 0; + } + + @Override + public void close() { + + } + }; + } + + @Override + public LeafGeoPointFieldData loadDirect(LeafReaderContext context) { + return load(context); + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/GeoPointFieldScript.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/GeoPointFieldScript.java new file mode 100644 index 0000000000000..536b007cb8c1e --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/GeoPointFieldScript.java @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.mapper; + +import org.apache.lucene.index.LeafReaderContext; +import org.elasticsearch.painless.spi.Whitelist; +import org.elasticsearch.painless.spi.WhitelistLoader; +import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptFactory; +import org.elasticsearch.search.lookup.SearchLookup; +import org.apache.lucene.document.LatLonDocValuesField; + +import java.util.List; +import java.util.Map; + +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude; + +/** + * Script producing geo points. Similarly to what {@link LatLonDocValuesField} does, + * it encodes the points as a long value. + */ +public abstract class GeoPointFieldScript extends AbstractLongFieldScript { + public static final ScriptContext CONTEXT = newContext("geo_point_script_field", Factory.class); + + static List whitelist() { + return List.of(WhitelistLoader.loadFromResourceFiles(RuntimeFieldsPainlessExtension.class, "geo_point_whitelist.txt")); + } + + @SuppressWarnings("unused") + public static final String[] PARAMETERS = {}; + + public interface Factory extends ScriptFactory { + LeafFactory newFactory(String fieldName, Map params, SearchLookup searchLookup); + } + + public interface LeafFactory { + GeoPointFieldScript newInstance(LeafReaderContext ctx); + } + + public GeoPointFieldScript(String fieldName, Map params, SearchLookup searchLookup, LeafReaderContext ctx) { + super(fieldName, params, searchLookup, ctx); + } + + protected void emit(double lat, double lon) { + int latitudeEncoded = encodeLatitude(lat); + int longitudeEncoded = encodeLongitude(lon); + emit(Long.valueOf((((long) latitudeEncoded) << 32) | (longitudeEncoded & 0xFFFFFFFFL))); + } + + public static class Emit { + private final GeoPointFieldScript script; + + public Emit(GeoPointFieldScript script) { + this.script = script; + } + + public void emit(double lat, double lon) { + script.emit(lat, lon); + } + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/GeoPointScriptFieldType.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/GeoPointScriptFieldType.java new file mode 100644 index 0000000000000..10d524b390e18 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/GeoPointScriptFieldType.java @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.mapper; + +import org.apache.lucene.geo.LatLonGeometry; +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.geo.GeoShapeUtils; +import org.elasticsearch.common.geo.ShapeRelation; +import org.elasticsearch.common.time.DateMathParser; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.Line; +import org.elasticsearch.geometry.MultiLine; +import org.elasticsearch.index.mapper.GeoPointFieldMapper; +import org.elasticsearch.index.mapper.GeoShapeQueryable; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.script.Script; +import org.elasticsearch.search.lookup.SearchLookup; +import org.elasticsearch.xpack.runtimefields.fielddata.GeoPointScriptFieldData; +import org.elasticsearch.xpack.runtimefields.query.GeoPointScriptFieldExistsQuery; +import org.elasticsearch.xpack.runtimefields.query.GeoPointScriptFieldGeoShapeQuery; + +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +public final class GeoPointScriptFieldType extends AbstractScriptFieldType implements GeoShapeQueryable { + + private static final List> UNSUPPORTED_GEOMETRIES = new ArrayList<>(); + static { + UNSUPPORTED_GEOMETRIES.add(Line.class); + UNSUPPORTED_GEOMETRIES.add(MultiLine.class); + } + + GeoPointScriptFieldType(String name, Script script, GeoPointFieldScript.Factory scriptFactory, Map meta) { + super(name, script, scriptFactory::newFactory, meta); + } + + @Override + protected String runtimeType() { + return GeoPointFieldMapper.CONTENT_TYPE; + } + + @Override + protected Query rangeQuery( + Object lowerTerm, + Object upperTerm, + boolean includeLower, + boolean includeUpper, + ZoneId timeZone, + DateMathParser parser, + QueryShardContext context + ) { + throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() + "] does not support range queries"); + } + + @Override + public Query termQuery(Object value, QueryShardContext context) { + throw new IllegalArgumentException( + "Geometry fields do not support exact searching, use dedicated geometry queries instead: [" + name() + "]" + ); + } + + @Override + public GeoPointScriptFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, Supplier searchLookup) { + return new GeoPointScriptFieldData.Builder(name(), leafFactory(searchLookup.get())); + } + + @Override + public Query existsQuery(QueryShardContext context) { + checkAllowExpensiveQueries(context); + return new GeoPointScriptFieldExistsQuery(script, leafFactory(context), name()); + } + + @Override + public Query geoShapeQuery(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) { + if (shape == null) { + return new MatchNoDocsQuery(); + } + final LatLonGeometry[] geometries = GeoShapeUtils.toLuceneGeometry(fieldName, context, shape, UNSUPPORTED_GEOMETRIES); + return new GeoPointScriptFieldGeoShapeQuery(script, leafFactory(context), fieldName, geometries); + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeFieldMapper.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeFieldMapper.java index f4b757d505adb..630ca4df72f5e 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeFieldMapper.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeFieldMapper.java @@ -11,6 +11,7 @@ import org.elasticsearch.index.mapper.BooleanFieldMapper; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.index.mapper.FieldMapper; +import org.elasticsearch.index.mapper.GeoPointFieldMapper; import org.elasticsearch.index.mapper.IpFieldMapper; import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.Mapper; @@ -133,6 +134,20 @@ public static class Builder extends ParametrizedFieldMapper.Builder { builder.meta.getValue() ); }, + GeoPointFieldMapper.CONTENT_TYPE, + (builder, context) -> { + builder.formatAndLocaleNotSupported(); + GeoPointFieldScript.Factory factory = builder.scriptCompiler.compile( + builder.script.getValue(), + GeoPointFieldScript.CONTEXT + ); + return new GeoPointScriptFieldType( + builder.buildFullName(context), + builder.script.getValue(), + factory, + builder.meta.getValue() + ); + }, NumberType.LONG.typeName(), (builder, context) -> { builder.formatAndLocaleNotSupported(); diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeFieldsPainlessExtension.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeFieldsPainlessExtension.java index dcd9ccfb505bd..509110ed4ba26 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeFieldsPainlessExtension.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeFieldsPainlessExtension.java @@ -20,6 +20,7 @@ public Map, List> getContextWhitelists() { Map.entry(BooleanFieldScript.CONTEXT, BooleanFieldScript.whitelist()), Map.entry(DateFieldScript.CONTEXT, DateFieldScript.whitelist()), Map.entry(DoubleFieldScript.CONTEXT, DoubleFieldScript.whitelist()), + Map.entry(GeoPointFieldScript.CONTEXT, GeoPointFieldScript.whitelist()), Map.entry(IpFieldScript.CONTEXT, IpFieldScript.whitelist()), Map.entry(LongFieldScript.CONTEXT, LongFieldScript.whitelist()), Map.entry(StringFieldScript.CONTEXT, StringFieldScript.whitelist()) diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractGeoPointScriptFieldQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractGeoPointScriptFieldQuery.java new file mode 100644 index 0000000000000..067d1b07960dc --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractGeoPointScriptFieldQuery.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.mapper.GeoPointFieldScript; + +/** + * Abstract base class for building queries based on {@link GeoPointFieldScript}. + */ +abstract class AbstractGeoPointScriptFieldQuery extends AbstractScriptFieldQuery { + + AbstractGeoPointScriptFieldQuery(Script script, GeoPointFieldScript.LeafFactory leafFactory, String fieldName) { + super(script, fieldName, leafFactory::newInstance); + } + + @Override + protected boolean matches(GeoPointFieldScript scriptContext, int docId) { + scriptContext.runForDoc(docId); + return matches(scriptContext.values(), scriptContext.count()); + } + + /** + * Does the value match this query? + */ + protected abstract boolean matches(long[] values, int count); +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/GeoPointScriptFieldExistsQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/GeoPointScriptFieldExistsQuery.java new file mode 100644 index 0000000000000..201f78573dea3 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/GeoPointScriptFieldExistsQuery.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.mapper.GeoPointFieldScript; + +public class GeoPointScriptFieldExistsQuery extends AbstractGeoPointScriptFieldQuery { + public GeoPointScriptFieldExistsQuery(Script script, GeoPointFieldScript.LeafFactory leafFactory, String fieldName) { + super(script, leafFactory, fieldName); + } + + @Override + protected boolean matches(long[] values, int count) { + return count > 0; + } + + @Override + public final String toString(String field) { + if (fieldName().contentEquals(field)) { + return getClass().getSimpleName(); + } + return fieldName() + ":" + getClass().getSimpleName(); + } + + // Superclass's equals and hashCode are great for this class +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/GeoPointScriptFieldGeoShapeQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/GeoPointScriptFieldGeoShapeQuery.java new file mode 100644 index 0000000000000..fdf82efb20ec8 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/GeoPointScriptFieldGeoShapeQuery.java @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.LatLonGeometry; +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.mapper.GeoPointFieldScript; + +import java.util.Arrays; +import java.util.Objects; + +public class GeoPointScriptFieldGeoShapeQuery extends AbstractGeoPointScriptFieldQuery { + + // This class should be called Component2DPredicate in Lucene... + private final GeoEncodingUtils.PolygonPredicate predicate; + private final LatLonGeometry[] geometries; + + public GeoPointScriptFieldGeoShapeQuery( + Script script, + GeoPointFieldScript.LeafFactory leafFactory, + String fieldName, + LatLonGeometry... geometries + ) { + super(script, leafFactory, fieldName); + this.geometries = geometries; + predicate = GeoEncodingUtils.createComponentPredicate(LatLonGeometry.create(geometries)); + } + + @Override + protected boolean matches(long[] values, int count) { + for (int i = 0; i < count; i++) { + final long value = values[i]; + final int lat = (int) (value >>> 32); + final int lon = (int) (value & 0xFFFFFFFF); + if (predicate.test(lat, lon)) { + return true; + } + } + return false; + } + + @Override + public final String toString(String field) { + if (fieldName().contentEquals(field)) { + return getClass().getSimpleName(); + } + return fieldName() + ":" + getClass().getSimpleName(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + GeoPointScriptFieldGeoShapeQuery that = (GeoPointScriptFieldGeoShapeQuery) o; + return Arrays.equals(geometries, that.geometries); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), Arrays.hashCode(geometries)); + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/resources/org/elasticsearch/xpack/runtimefields/mapper/geo_point_whitelist.txt b/x-pack/plugin/runtime-fields/src/main/resources/org/elasticsearch/xpack/runtimefields/mapper/geo_point_whitelist.txt new file mode 100644 index 0000000000000..3f24d1a07cc4b --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/resources/org/elasticsearch/xpack/runtimefields/mapper/geo_point_whitelist.txt @@ -0,0 +1,18 @@ +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +# The whitelist for ip-valued runtime fields + +# These two whitelists are required for painless to find the classes +class org.elasticsearch.xpack.runtimefields.mapper.GeoPointFieldScript @no_import { +} +class org.elasticsearch.xpack.runtimefields.mapper.GeoPointFieldScript$Factory @no_import { +} + +static_import { + # The `emit` callback to collect values for the field + void emit(org.elasticsearch.xpack.runtimefields.mapper.GeoPointFieldScript, double, double) bound_to org.elasticsearch.xpack.runtimefields.mapper.GeoPointFieldScript$Emit +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptFieldTypeTestCase.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptFieldTypeTestCase.java index 9e5d876bcf08a..6e173a7c72714 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptFieldTypeTestCase.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptFieldTypeTestCase.java @@ -66,6 +66,14 @@ protected static QueryShardContext mockContext(boolean allowExpensiveQueries) { return mockContext(allowExpensiveQueries, null); } + protected boolean supportsTermQueries() { + return true; + } + + protected boolean supportsRangeQueries() { + return true; + } + protected static QueryShardContext mockContext(boolean allowExpensiveQueries, MappedFieldType mappedFieldType) { MapperService mapperService = mock(MapperService.class); when(mapperService.fieldType(anyString())).thenReturn(mappedFieldType); @@ -102,42 +110,52 @@ public void testRangeQueryWithShapeRelationIsError() { } public void testRangeQueryIsExpensive() { + assumeTrue("Impl does not support range queries", supportsRangeQueries()); checkExpensiveQuery(this::randomRangeQuery); } public void testRangeQueryInLoop() { + assumeTrue("Impl does not support range queries", supportsRangeQueries()); checkLoop(this::randomRangeQuery); } public void testTermQueryIsExpensive() { + assumeTrue("Impl does not support term queries", supportsTermQueries()); checkExpensiveQuery(this::randomTermQuery); } public void testTermQueryInLoop() { + assumeTrue("Impl does not support term queries", supportsTermQueries()); checkLoop(this::randomTermQuery); } public void testTermsQueryIsExpensive() { + assumeTrue("Impl does not support term queries", supportsTermQueries()); checkExpensiveQuery(this::randomTermsQuery); } public void testTermsQueryInLoop() { + assumeTrue("Impl does not support term queries", supportsTermQueries()); checkLoop(this::randomTermsQuery); } public void testPhraseQueryIsError() { + assumeTrue("Impl does not support term queries", supportsTermQueries()); assertQueryOnlyOnText("phrase", () -> simpleMappedFieldType().phraseQuery(null, 1, false)); } public void testPhrasePrefixQueryIsError() { + assumeTrue("Impl does not support term queries", supportsTermQueries()); assertQueryOnlyOnText("phrase prefix", () -> simpleMappedFieldType().phrasePrefixQuery(null, 1, 1)); } public void testMultiPhraseQueryIsError() { + assumeTrue("Impl does not support term queries", supportsTermQueries()); assertQueryOnlyOnText("phrase", () -> simpleMappedFieldType().multiPhraseQuery(null, 1, false)); } public void testSpanPrefixQueryIsError() { + assumeTrue("Impl does not support term queries", supportsTermQueries()); assertQueryOnlyOnText("span prefix", () -> simpleMappedFieldType().spanPrefixQuery(null, null, null)); } diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/GeoPointFieldScriptTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/GeoPointFieldScriptTests.java new file mode 100644 index 0000000000000..f011b46940e26 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/GeoPointFieldScriptTests.java @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.mapper; + +import org.apache.lucene.document.StoredField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.search.lookup.SearchLookup; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; + +public class GeoPointFieldScriptTests extends FieldScriptTestCase { + public static final GeoPointFieldScript.Factory DUMMY = (fieldName, params, lookup) -> ctx -> new GeoPointFieldScript( + fieldName, + params, + lookup, + ctx + ) { + @Override + public void execute() { + emit(0, 0); + } + }; + + @Override + protected ScriptContext context() { + return GeoPointFieldScript.CONTEXT; + } + + @Override + protected GeoPointFieldScript.Factory dummyScript() { + return DUMMY; + } + + public void testTooManyValues() throws IOException { + try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{}")))); + try (DirectoryReader reader = iw.getReader()) { + GeoPointFieldScript script = new GeoPointFieldScript( + "test", + Map.of(), + new SearchLookup(mock(MapperService.class), (ft, lookup) -> null), + reader.leaves().get(0) + ) { + @Override + public void execute() { + for (int i = 0; i <= AbstractFieldScript.MAX_VALUES; i++) { + emit(0, 0); + } + } + }; + Exception e = expectThrows(IllegalArgumentException.class, script::execute); + assertThat( + e.getMessage(), + equalTo("Runtime field [test] is emitting [101] values while the maximum number of values allowed is [100]") + ); + } + } + } +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/GeoPointScriptFieldTypeTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/GeoPointScriptFieldTypeTests.java new file mode 100644 index 0000000000000..bd65acd8ced3b --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/GeoPointScriptFieldTypeTests.java @@ -0,0 +1,275 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.mapper; + +import org.apache.lucene.document.StoredField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.LeafCollector; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorable; +import org.apache.lucene.search.ScoreMode; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.Version; +import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.common.lucene.search.function.ScriptScoreQuery; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.geo.GeometryTestUtils; +import org.elasticsearch.index.fielddata.MultiGeoPointValues; +import org.elasticsearch.index.fielddata.ScriptDocValues; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.plugins.ScriptPlugin; +import org.elasticsearch.script.ScoreScript; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptEngine; +import org.elasticsearch.script.ScriptModule; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.search.MultiValueMode; +import org.elasticsearch.xpack.runtimefields.RuntimeFields; +import org.elasticsearch.xpack.runtimefields.fielddata.GeoPointScriptFieldData; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.util.Collections.emptyMap; +import static org.hamcrest.Matchers.equalTo; + +public class GeoPointScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeTestCase { + + @Override + protected boolean supportsTermQueries() { + return false; + } + + @Override + protected boolean supportsRangeQueries() { + return false; + } + + @Override + public void testDocValues() throws IOException { + try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": {\"lat\": 45.0, \"lon\" : 45.0}}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": {\"lat\": 0.0, \"lon\" : 0.0}}")))); + List results = new ArrayList<>(); + try (DirectoryReader reader = iw.getReader()) { + IndexSearcher searcher = newSearcher(reader); + GeoPointScriptFieldType ft = build("fromLatLon", Map.of()); + GeoPointScriptFieldData ifd = ft.fielddataBuilder("test", mockContext()::lookup).build(null, null); + searcher.search(new MatchAllDocsQuery(), new Collector() { + @Override + public ScoreMode scoreMode() { + return ScoreMode.COMPLETE_NO_SCORES; + } + + @Override + public LeafCollector getLeafCollector(LeafReaderContext context) { + MultiGeoPointValues dv = ifd.load(context).getGeoPointValues(); + return new LeafCollector() { + @Override + public void setScorer(Scorable scorer) {} + + @Override + public void collect(int doc) throws IOException { + if (dv.advanceExact(doc)) { + for (int i = 0; i < dv.docValueCount(); i++) { + final GeoPoint point = dv.nextValue(); + results.add(new GeoPoint(point.lat(), point.lon())); + } + } + } + }; + } + }); + assertThat(results, equalTo(List.of(new GeoPoint(45.0, 45.0), new GeoPoint(0.0, 0.0)))); + } + } + } + + @Override + public void testSort() throws IOException { + GeoPointScriptFieldData ifd = simpleMappedFieldType().fielddataBuilder("test", mockContext()::lookup).build(null, null); + Exception e = expectThrows(IllegalArgumentException.class, () -> ifd.sortField(null, MultiValueMode.MIN, null, false)); + assertThat(e.getMessage(), equalTo("can't sort on geo_point field without using specific sorting feature, like geo_distance")); + } + + @Override + public void testUsedInScript() throws IOException { + try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": {\"lat\": 45.0, \"lon\" : 45.0}}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": {\"lat\": 0.0, \"lon\" : 0.0}}")))); + try (DirectoryReader reader = iw.getReader()) { + IndexSearcher searcher = newSearcher(reader); + QueryShardContext qsc = mockContext(true, simpleMappedFieldType()); + assertThat(searcher.count(new ScriptScoreQuery(new MatchAllDocsQuery(), new Script("test"), new ScoreScript.LeafFactory() { + @Override + public boolean needs_score() { + return false; + } + + @Override + public ScoreScript newInstance(LeafReaderContext ctx) { + return new ScoreScript(Map.of(), qsc.lookup(), ctx) { + @Override + public double execute(ExplanationHolder explanation) { + ScriptDocValues.GeoPoints points = (ScriptDocValues.GeoPoints) getDoc().get("test"); + return (int) points.get(0).lat() + 1; + } + }; + } + }, 2.5f, "test", 0, Version.CURRENT)), equalTo(1)); + } + } + } + + @Override + public void testExistsQuery() throws IOException { + try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": {\"lat\": 45.0, \"lon\" : 45.0}}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": {\"lat\": 0.0, \"lon\" : 0.0}}")))); + try (DirectoryReader reader = iw.getReader()) { + IndexSearcher searcher = newSearcher(reader); + assertThat(searcher.count(simpleMappedFieldType().existsQuery(mockContext())), equalTo(2)); + } + } + } + + @Override + public void testRangeQuery() throws IOException { + Exception e = expectThrows( + IllegalArgumentException.class, + () -> simpleMappedFieldType().rangeQuery("0.0", "45.0", false, false, null, null, null, mockContext()) + ); + assertThat(e.getMessage(), equalTo("Field [test] of type [runtime] does not support range queries")); + } + + @Override + protected Query randomRangeQuery(MappedFieldType ft, QueryShardContext ctx) { + throw new IllegalArgumentException("Unsupported"); + } + + @Override + public void testTermQuery() { + Exception e = expectThrows(IllegalArgumentException.class, () -> simpleMappedFieldType().termQuery("0.0,0.0", mockContext())); + assertThat( + e.getMessage(), + equalTo("Geometry fields do not support exact searching, use dedicated geometry queries instead: [test]") + ); + } + + @Override + protected Query randomTermQuery(MappedFieldType ft, QueryShardContext ctx) { + throw new IllegalArgumentException("Unsupported"); + } + + @Override + public void testTermsQuery() { + Exception e = expectThrows( + IllegalArgumentException.class, + () -> simpleMappedFieldType().termsQuery(List.of("0.0,0.0", "45.0,45.0"), mockContext()) + ); + + assertThat( + e.getMessage(), + equalTo("Geometry fields do not support exact searching, use dedicated geometry queries instead: [test]") + ); + + } + + @Override + protected Query randomTermsQuery(MappedFieldType ft, QueryShardContext ctx) { + return ft.termsQuery(randomList(100, () -> GeometryTestUtils.randomPoint()), mockContext()); + } + + @Override + protected GeoPointScriptFieldType simpleMappedFieldType() throws IOException { + return build("fromLatLon", Map.of()); + } + + @Override + protected MappedFieldType loopFieldType() throws IOException { + return build("loop", Map.of()); + } + + @Override + protected String runtimeType() { + return "geo_point"; + } + + private static GeoPointScriptFieldType build(String code, Map params) throws IOException { + return build(new Script(ScriptType.INLINE, "test", code, params)); + } + + private static GeoPointScriptFieldType build(Script script) throws IOException { + ScriptPlugin scriptPlugin = new ScriptPlugin() { + @Override + public ScriptEngine getScriptEngine(Settings settings, Collection> contexts) { + return new ScriptEngine() { + @Override + public String getType() { + return "test"; + } + + @Override + public Set> getSupportedContexts() { + return Set.of(StringFieldScript.CONTEXT, GeoPointFieldScript.CONTEXT); + } + + @Override + public FactoryType compile( + String name, + String code, + ScriptContext context, + Map params + ) { + @SuppressWarnings("unchecked") + FactoryType factory = (FactoryType) factory(code); + return factory; + } + + private GeoPointFieldScript.Factory factory(String code) { + switch (code) { + case "fromLatLon": + return (fieldName, params, lookup) -> (ctx) -> new GeoPointFieldScript(fieldName, params, lookup, ctx) { + @Override + public void execute() { + Map foo = (Map) getSource().get("foo"); + emit(((Number) foo.get("lat")).doubleValue(), ((Number) foo.get("lon")).doubleValue()); + } + }; + case "loop": + return (fieldName, params, lookup) -> { + // Indicate that this script wants the field call "test", which *is* the name of this field + lookup.forkAndTrackFieldReferences("test"); + throw new IllegalStateException("shoud have thrown on the line above"); + }; + default: + throw new IllegalArgumentException("unsupported script [" + code + "]"); + } + } + }; + } + }; + ScriptModule scriptModule = new ScriptModule(Settings.EMPTY, List.of(scriptPlugin, new RuntimeFields())); + try (ScriptService scriptService = new ScriptService(Settings.EMPTY, scriptModule.engines, scriptModule.contexts)) { + GeoPointFieldScript.Factory factory = scriptService.compile(script, GeoPointFieldScript.CONTEXT); + return new GeoPointScriptFieldType("test", script, factory, emptyMap()); + } + } +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeFieldMapperTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeFieldMapperTests.java index b851c3f6e50d5..90432baa36027 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeFieldMapperTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeFieldMapperTests.java @@ -396,6 +396,9 @@ protected Object buildScriptFactory(ScriptContext context) { if (context == StringFieldScript.CONTEXT) { return StringFieldScriptTests.DUMMY; } + if (context == GeoPointFieldScript.CONTEXT) { + return GeoPointFieldScriptTests.DUMMY; + } throw new IllegalArgumentException("Unsupported context: " + context); }; diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractGeoPointScriptFieldQueryTestCase.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractGeoPointScriptFieldQueryTestCase.java new file mode 100644 index 0000000000000..d6cd0b8369e75 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractGeoPointScriptFieldQueryTestCase.java @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.elasticsearch.xpack.runtimefields.mapper.GeoPointFieldScript; + +import static org.mockito.Mockito.mock; + +public abstract class AbstractGeoPointScriptFieldQueryTestCase extends + AbstractScriptFieldQueryTestCase { + + protected final GeoPointFieldScript.LeafFactory leafFactory = mock(GeoPointFieldScript.LeafFactory.class); + + @Override + public final void testVisit() { + assertEmptyVisit(); + } +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/GeoPointScriptFieldExistsQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/GeoPointScriptFieldExistsQueryTests.java new file mode 100644 index 0000000000000..a1ad9b0948847 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/GeoPointScriptFieldExistsQueryTests.java @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import static org.hamcrest.Matchers.equalTo; + +public class GeoPointScriptFieldExistsQueryTests extends AbstractGeoPointScriptFieldQueryTestCase { + @Override + protected GeoPointScriptFieldExistsQuery createTestInstance() { + return new GeoPointScriptFieldExistsQuery(randomScript(), leafFactory, randomAlphaOfLength(5)); + } + + @Override + protected GeoPointScriptFieldExistsQuery copy(GeoPointScriptFieldExistsQuery orig) { + return new GeoPointScriptFieldExistsQuery(orig.script(), leafFactory, orig.fieldName()); + } + + @Override + protected GeoPointScriptFieldExistsQuery mutate(GeoPointScriptFieldExistsQuery orig) { + if (randomBoolean()) { + new GeoPointScriptFieldExistsQuery(randomValueOtherThan(orig.script(), this::randomScript), leafFactory, orig.fieldName()); + } + return new GeoPointScriptFieldExistsQuery(orig.script(), leafFactory, orig.fieldName() + "modified"); + } + + @Override + public void testMatches() { + assertTrue(createTestInstance().matches(new long[] { 1L }, randomIntBetween(1, Integer.MAX_VALUE))); + assertFalse(createTestInstance().matches(new long[0], 0)); + assertFalse(createTestInstance().matches(new long[1], 0)); + } + + @Override + protected void assertToString(GeoPointScriptFieldExistsQuery query) { + assertThat(query.toString(query.fieldName()), equalTo("GeoPointScriptFieldExistsQuery")); + } +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/GeoPointScriptFieldGeoShapeQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/GeoPointScriptFieldGeoShapeQueryTests.java new file mode 100644 index 0000000000000..c1183d8e7515f --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/GeoPointScriptFieldGeoShapeQueryTests.java @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.geo.Polygon; + +import static org.hamcrest.Matchers.equalTo; + +public class GeoPointScriptFieldGeoShapeQueryTests extends AbstractGeoPointScriptFieldQueryTestCase { + + private static final Polygon polygon1 = new Polygon(new double[] { -10, -10, 10, 10, -10 }, new double[] { -10, 10, 10, -10, -10 }); + private static final Polygon polygon2 = new Polygon(new double[] { -11, -10, 10, 10, -11 }, new double[] { -10, 10, 10, -10, -10 }); + + @Override + protected GeoPointScriptFieldGeoShapeQuery createTestInstance() { + return new GeoPointScriptFieldGeoShapeQuery(randomScript(), leafFactory, randomAlphaOfLength(5), polygon1); + } + + @Override + protected GeoPointScriptFieldGeoShapeQuery copy(GeoPointScriptFieldGeoShapeQuery orig) { + return new GeoPointScriptFieldGeoShapeQuery(orig.script(), leafFactory, orig.fieldName(), polygon1); + } + + @Override + protected GeoPointScriptFieldGeoShapeQuery mutate(GeoPointScriptFieldGeoShapeQuery orig) { + if (randomBoolean()) { + new GeoPointScriptFieldGeoShapeQuery( + randomValueOtherThan(orig.script(), this::randomScript), + leafFactory, + orig.fieldName(), + polygon2 + ); + } + return new GeoPointScriptFieldGeoShapeQuery(orig.script(), leafFactory, orig.fieldName() + "modified", polygon1); + } + + @Override + public void testMatches() { + assertTrue(createTestInstance().matches(new long[] { 1L }, randomIntBetween(1, Integer.MAX_VALUE))); + assertFalse(createTestInstance().matches(new long[0], 0)); + assertFalse(createTestInstance().matches(new long[1], 0)); + } + + @Override + protected void assertToString(GeoPointScriptFieldGeoShapeQuery query) { + assertThat(query.toString(query.fieldName()), equalTo("GeoPointScriptFieldGeoShapeQuery")); + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/100_geo_point.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/100_geo_point.yml new file mode 100644 index 0000000000000..6803b79c79ea7 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/100_geo_point.yml @@ -0,0 +1,154 @@ +--- +setup: + - do: + indices.create: + index: locations + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + properties: + timestamp: + type: date + location: + type: geo_point + location_from_doc_value: + type: runtime + runtime_type: geo_point + script: + source: | + emit(doc["location"].lat, doc["location"].lon); + location_from_source: + type: runtime + runtime_type: geo_point + script: + source: | + emit(params._source.location.lat, params._source.location.lon); + + + - do: + bulk: + index: locations + refresh: true + body: | + {"index":{}} + {"timestamp": "1998-04-30T14:30:17-05:00", "location" : {"lat": 13.5, "lon" : 34.89}} + {"index":{}} + {"timestamp": "1998-04-30T14:30:53-05:00", "location" : {"lat": -7.9, "lon" : 120.78}} + {"index":{}} + {"timestamp": "1998-04-30T14:31:12-05:00", "location" : {"lat": 45.78, "lon" : -173.45}} + {"index":{}} + {"timestamp": "1998-04-30T14:31:19-05:00", "location" : {"lat": 32.45, "lon" : 45.6}} + {"index":{}} + {"timestamp": "1998-04-30T14:31:22-05:00", "location" : {"lat": -63.24, "lon" : 31.0}} + {"index":{}} + {"timestamp": "1998-04-30T14:31:27-05:00", "location" : {"lat": 0.0, "lon" : 0.0}} + + +--- +"get mapping": + - do: + indices.get_mapping: + index: locations + - match: {locations.mappings.properties.location_from_source.type: runtime } + - match: {locations.mappings.properties.location_from_source.runtime_type: geo_point } + - match: + locations.mappings.properties.location_from_source.script.source: | + emit(params._source.location.lat, params._source.location.lon); + - match: {locations.mappings.properties.location_from_source.script.lang: painless } + + +--- +"fetch fields from source": + - do: + search: + index: locations + body: + sort: timestamp + fields: [location, location_from_doc_value, location_from_source] + - match: {hits.total.value: 6} + - match: {hits.hits.0.fields.location.0.type: "Point" } + - match: {hits.hits.0.fields.location.0.coordinates: [34.89, 13.5] } + - match: {hits.hits.0.fields.location_from_doc_value: ["13.499999991618097, 34.889999935403466"] } + - match: {hits.hits.0.fields.location_from_source: ["13.499999991618097, 34.889999935403466"] } + +--- +"exists query": + - do: + search: + index: locations + body: + query: + exists: + field: location_from_source + - match: {hits.total.value: 6} + +--- +"geo bounding box query": + - do: + search: + index: locations + body: + query: + geo_shape: + location_from_source: + shape: + type: "envelope" + coordinates: [ [ -10, 10 ], [ 10, -10 ] ] + - match: {hits.total.value: 1} + +--- +"bounds agg": + - do: + search: + index: locations + body: + aggs: + bounds: + geo_bounds: + field: "location" + wrap_longitude: false + bounds_from_doc_value: + geo_bounds: + field: "location_from_doc_value" + wrap_longitude: false + bounds_from_source: + geo_bounds: + field: "location_from_source" + wrap_longitude: false + - match: {hits.total.value: 6} + - match: {aggregations.bounds.bounds.top_left.lat: 45.7799999602139 } + - match: {aggregations.bounds.bounds.top_left.lon: -173.4500000718981 } + - match: {aggregations.bounds.bounds.bottom_right.lat: -63.240000014193356 } + - match: {aggregations.bounds.bounds.bottom_right.lon: 120.77999993227422 } + - match: {aggregations.bounds_from_doc_value.bounds.top_left.lat: 45.7799999602139 } + - match: {aggregations.bounds_from_doc_value.bounds.top_left.lon: -173.4500000718981 } + - match: {aggregations.bounds_from_doc_value.bounds.bottom_right.lat: -63.240000014193356 } + - match: {aggregations.bounds_from_doc_value.bounds.bottom_right.lon: 120.77999993227422 } + - match: {aggregations.bounds_from_source.bounds.top_left.lat: 45.7799999602139 } + - match: {aggregations.bounds_from_source.bounds.top_left.lon: -173.4500000718981 } + - match: {aggregations.bounds_from_source.bounds.bottom_right.lat: -63.240000014193356 } + - match: {aggregations.bounds_from_source.bounds.bottom_right.lon: 120.77999993227422 } + +--- +"geo_distance sort": + - do: + search: + index: locations + body: + sort: + _geo_distance: + location_from_source: + lat: 0.0 + lon: 0.0 + - match: {hits.total.value: 6} + - match: {hits.hits.0._source.location.lat: 0.0 } + - match: {hits.hits.0._source.location.lon: 0.0 } + - match: {hits.hits.1._source.location.lat: 13.5 } + - match: {hits.hits.1._source.location.lon: 34.89 } + - match: {hits.hits.2._source.location.lat: 32.45 } + - match: {hits.hits.2._source.location.lon: 45.6 } + - match: {hits.hits.3._source.location.lat: -63.24 } + - match: {hits.hits.3._source.location.lon: 31.0 } +