diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/transform/transforms/pivot/GeoTileGroupSource.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/transform/transforms/pivot/GeoTileGroupSource.java index ff4538310e591..477f4e4f048d5 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/transform/transforms/pivot/GeoTileGroupSource.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/transform/transforms/pivot/GeoTileGroupSource.java @@ -90,7 +90,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(PRECISION.getPreferredName(), precision); } if (geoBoundingBox != null) { - geoBoundingBox.toXContent(builder, params); + builder.startObject(GeoBoundingBox.BOUNDS_FIELD.getPreferredName()); + geoBoundingBox.toXContentFragment(builder, true); + builder.endObject(); } builder.endObject(); return builder; diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt index 03d28c297d5bd..1adda3bcef102 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt @@ -53,6 +53,12 @@ class org.elasticsearch.common.geo.GeoPoint { double getLon() } +class org.elasticsearch.common.geo.GeoBoundingBox { + org.elasticsearch.common.geo.GeoPoint topLeft() + org.elasticsearch.common.geo.GeoPoint bottomRight() +} + + class org.elasticsearch.index.fielddata.ScriptDocValues$Strings { String get(int) String getValue() @@ -148,6 +154,12 @@ class org.elasticsearch.index.fielddata.ScriptDocValues$Doubles { double getValue() } +class org.elasticsearch.index.fielddata.ScriptDocValues$Geometry { + int getDimensionalType() + org.elasticsearch.common.geo.GeoPoint getCentroid() + org.elasticsearch.common.geo.GeoBoundingBox getBoundingBox() +} + class org.elasticsearch.index.fielddata.ScriptDocValues$GeoPoints { org.elasticsearch.common.geo.GeoPoint get(int) org.elasticsearch.common.geo.GeoPoint getValue() diff --git a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml index fa55b47b803dd..7c7a7390107ee 100644 --- a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml +++ b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml @@ -132,6 +132,56 @@ setup: - match: { hits.hits.0.fields.field.0.lat: 41.1199999647215 } - match: { hits.hits.0.fields.field.0.lon: -71.34000004269183 } + - do: + search: + rest_total_hits_as_int: true + body: + script_fields: + centroid: + script: + source: "doc['geo_point'].getCentroid()" + - match: { hits.hits.0.fields.centroid.0.lat: 41.1199999647215 } + - match: { hits.hits.0.fields.centroid.0.lon: -71.34000004269183 } + + - do: + search: + rest_total_hits_as_int: true + body: + script_fields: + bbox: + script: + source: "doc['geo_point'].getBoundingBox()" + - match: { hits.hits.0.fields.bbox.0.top_left.lat: 41.1199999647215 } + - match: { hits.hits.0.fields.bbox.0.top_left.lon: -71.34000004269183 } + - match: { hits.hits.0.fields.bbox.0.bottom_right.lat: 41.1199999647215 } + - match: { hits.hits.0.fields.bbox.0.bottom_right.lon: -71.34000004269183 } + + - do: + search: + rest_total_hits_as_int: true + body: + script_fields: + topLeft: + script: + source: "doc['geo_point'].getBoundingBox().topLeft()" + bottomRight: + script: + source: "doc['geo_point'].getBoundingBox().bottomRight()" + - match: { hits.hits.0.fields.topLeft.0.lat: 41.1199999647215 } + - match: { hits.hits.0.fields.topLeft.0.lon: -71.34000004269183 } + - match: { hits.hits.0.fields.bottomRight.0.lat: 41.1199999647215 } + - match: { hits.hits.0.fields.bottomRight.0.lon: -71.34000004269183 } + + - do: + search: + rest_total_hits_as_int: true + body: + script_fields: + type: + script: + source: "doc['geo_point'].getDimensionalType()" + - match: { hits.hits.0.fields.type.0: 0 } + --- "ip": - do: diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoPointScriptDocValuesIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoPointScriptDocValuesIT.java new file mode 100644 index 0000000000000..b258b18b7e2a9 --- /dev/null +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoPointScriptDocValuesIT.java @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.search.geo; + +import org.apache.lucene.geo.GeoEncodingUtils; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.document.DocumentField; +import org.elasticsearch.common.geo.GeoBoundingBox; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.geo.GeometryTestUtils; +import org.elasticsearch.index.fielddata.ScriptDocValues; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.script.MockScriptPlugin; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.hamcrest.Matchers; +import org.junit.Before; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.equalTo; + +public class GeoPointScriptDocValuesIT extends ESSingleNodeTestCase { + + @Override + protected Collection> getPlugins() { + return Arrays.asList(CustomScriptPlugin.class); + } + + public static class CustomScriptPlugin extends MockScriptPlugin { + + @Override + protected Map, Object>> pluginScripts() { + Map, Object>> scripts = new HashMap<>(); + + scripts.put("lat", this::scriptLat); + scripts.put("lon", this::scriptLon); + scripts.put("height", this::scriptHeight); + scripts.put("width", this::scriptWidth); + return scripts; + } + + private double scriptHeight(Map vars) { + Map doc = (Map) vars.get("doc"); + ScriptDocValues.Geometry geometry = assertGeometry(doc); + if (geometry.size() == 0) { + return Double.NaN; + } else { + GeoBoundingBox boundingBox = geometry.getBoundingBox(); + return boundingBox.topLeft().lat() - boundingBox.bottomRight().lat(); + } + } + + private double scriptWidth(Map vars) { + Map doc = (Map) vars.get("doc"); + ScriptDocValues.Geometry geometry = assertGeometry(doc); + if (geometry.size() == 0) { + return Double.NaN; + } else { + GeoBoundingBox boundingBox = geometry.getBoundingBox(); + return boundingBox.bottomRight().lon() - boundingBox.topLeft().lon(); + } + } + + private double scriptLat(Map vars) { + Map doc = (Map) vars.get("doc"); + ScriptDocValues.Geometry geometry = assertGeometry(doc); + return geometry.size() == 0 ? Double.NaN : geometry.getCentroid().lat(); + } + + private double scriptLon(Map vars) { + Map doc = (Map) vars.get("doc"); + ScriptDocValues.Geometry geometry = assertGeometry(doc); + return geometry.size() == 0 ? Double.NaN : geometry.getCentroid().lon(); + } + + private ScriptDocValues.Geometry assertGeometry(Map doc) { + ScriptDocValues.Geometry geometry = (ScriptDocValues.Geometry) doc.get("location"); + if (geometry.size() == 0) { + assertThat(geometry.getBoundingBox(), Matchers.nullValue()); + assertThat(geometry.getCentroid(), Matchers.nullValue()); + assertThat(geometry.getDimensionalType(), equalTo(-1)); + } else { + assertThat(geometry.getBoundingBox(), Matchers.notNullValue()); + assertThat(geometry.getCentroid(), Matchers.notNullValue()); + assertThat(geometry.getDimensionalType(), equalTo(0)); + } + return geometry; + } + } + + @Override + protected boolean forbidPrivateIndexSettings() { + return false; + } + + @Before + public void setupTestIndex() throws IOException { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("location").field("type", "geo_point"); + xContentBuilder.endObject().endObject().endObject().endObject(); + assertAcked(client().admin().indices().prepareCreate("test").addMapping("_doc", xContentBuilder)); + ensureGreen(); + } + + public void testRandomPoint() throws Exception { + final double lat = GeometryTestUtils.randomLat(); + final double lon = GeometryTestUtils.randomLon(); + client().prepareIndex("test", "_doc").setId("1") + .setSource(jsonBuilder().startObject() + .field("name", "TestPosition") + .field("location", new double[]{lon, lat}) + .endObject()) + .get(); + + client().admin().indices().prepareRefresh("test").get(); + + SearchResponse searchResponse = client().prepareSearch().addStoredField("_source") + .addScriptField("lat", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lat", Collections.emptyMap())) + .addScriptField("lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lon", Collections.emptyMap())) + .addScriptField("height", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "height", Collections.emptyMap())) + .addScriptField("width", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "width", Collections.emptyMap())) + .get(); + assertSearchResponse(searchResponse); + + final double qLat = GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(lat)); + final double qLon = GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(lon)); + + Map fields = searchResponse.getHits().getHits()[0].getFields(); + assertThat(fields.get("lat").getValue(), equalTo(qLat)); + assertThat(fields.get("lon").getValue(), equalTo(qLon)); + assertThat(fields.get("height").getValue(), equalTo(0d)); + assertThat(fields.get("width").getValue(), equalTo(0d)); + } + + public void testRandomMultiPoint() throws Exception { + final int size = randomIntBetween(2, 20); + final double[] lats = new double[size]; + final double[] lons = new double[size]; + for (int i = 0; i < size; i++) { + lats[i] = GeometryTestUtils.randomLat(); + lons[i] = GeometryTestUtils.randomLon(); + } + + final double[][] values = new double[size][]; + for (int i = 0; i < size; i++) { + values[i] = new double[]{lons[i], lats[i]}; + } + + XContentBuilder builder = jsonBuilder().startObject() + .field("name", "TestPosition") + .field("location", values).endObject(); + client().prepareIndex("test", "_doc").setId("1").setSource(builder).get(); + + client().admin().indices().prepareRefresh("test").get(); + + SearchResponse searchResponse = client().prepareSearch().addStoredField("_source") + .addScriptField("lat", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lat", Collections.emptyMap())) + .addScriptField("lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lon", Collections.emptyMap())) + .addScriptField("height", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "height", Collections.emptyMap())) + .addScriptField("width", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "width", Collections.emptyMap())) + .get(); + assertSearchResponse(searchResponse); + + for (int i = 0; i < size; i++) { + lats[i] = GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(lats[i])); + lons[i] = GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(lons[i])); + } + + final double centroidLon = Arrays.stream(lons).sum() / size; + final double centroidLat = Arrays.stream(lats).sum() / size; + final double width = Arrays.stream(lons).max().getAsDouble() - Arrays.stream(lons).min().getAsDouble(); + final double height = Arrays.stream(lats).max().getAsDouble() - Arrays.stream(lats).min().getAsDouble(); + + Map fields = searchResponse.getHits().getHits()[0].getFields(); + assertThat(fields.get("lat").getValue(), equalTo(centroidLat)); + assertThat(fields.get("lon").getValue(), equalTo(centroidLon)); + assertThat(fields.get("height").getValue(), equalTo(height)); + assertThat(fields.get("width").getValue(), equalTo(width)); + } + + public void testNullPoint() throws Exception { + client().prepareIndex("test", "_doc").setId("1") + .setSource(jsonBuilder().startObject() + .field("name", "TestPosition") + .nullField("location") + .endObject()) + .get(); + + client().admin().indices().prepareRefresh("test").get(); + + SearchResponse searchResponse = client().prepareSearch().addStoredField("_source") + .addScriptField("lat", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lat", Collections.emptyMap())) + .addScriptField("lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lon", Collections.emptyMap())) + .addScriptField("height", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "height", Collections.emptyMap())) + .addScriptField("width", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "width", Collections.emptyMap())) + .get(); + assertSearchResponse(searchResponse); + + Map fields = searchResponse.getHits().getHits()[0].getFields(); + assertThat(fields.get("lat").getValue(), equalTo(Double.NaN)); + assertThat(fields.get("lon").getValue(), equalTo(Double.NaN)); + assertThat(fields.get("height").getValue(), equalTo(Double.NaN)); + assertThat(fields.get("width").getValue(), equalTo(Double.NaN)); + } +} diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeoBoundingBox.java b/server/src/main/java/org/elasticsearch/common/geo/GeoBoundingBox.java index ec33b36d95b32..0cb4617eea27a 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeoBoundingBox.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeoBoundingBox.java @@ -12,7 +12,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.geometry.Geometry; @@ -29,7 +29,7 @@ * A class representing a Geo-Bounding-Box for use by Geo queries and aggregations * that deal with extents/rectangles representing rectangular areas of interest. */ -public class GeoBoundingBox implements ToXContentObject, Writeable { +public class GeoBoundingBox implements ToXContentFragment, Writeable { private static final WellKnownText WKT_PARSER = new WellKnownText(true, new StandardValidator(true)); static final ParseField TOP_RIGHT_FIELD = new ParseField("top_right"); static final ParseField BOTTOM_LEFT_FIELD = new ParseField("bottom_left"); @@ -88,7 +88,7 @@ public double right() { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(BOUNDS_FIELD.getPreferredName()); + builder.startObject(); toXContentFragment(builder, true); builder.endObject(); return builder; diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java b/server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java index 652e290e72721..4413ef8dd1963 100644 --- a/server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java +++ b/server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java @@ -12,6 +12,7 @@ import org.apache.lucene.util.ArrayUtil; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRefBuilder; +import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoUtils; import org.elasticsearch.common.time.DateUtils; @@ -247,10 +248,21 @@ public int size() { } } - public static final class GeoPoints extends ScriptDocValues { + public abstract static class Geometry extends ScriptDocValues { + /** Returns the dimensional type of this geometry */ + public abstract int getDimensionalType(); + /** Returns the bounding box of this geometry */ + public abstract GeoBoundingBox getBoundingBox(); + /** Returns the centroid of this geometry */ + public abstract GeoPoint getCentroid(); + } + + public static final class GeoPoints extends Geometry { private final MultiGeoPointValues in; private GeoPoint[] values = new GeoPoint[0]; + private final GeoPoint centroid = new GeoPoint(); + private final GeoBoundingBox boundingBox = new GeoBoundingBox(new GeoPoint(), new GeoPoint()); private int count; public GeoPoints(MultiGeoPointValues in) { @@ -261,10 +273,25 @@ public GeoPoints(MultiGeoPointValues in) { public void setNextDocId(int docId) throws IOException { if (in.advanceExact(docId)) { resize(in.docValueCount()); + double centroidLat = 0; + double centroidLon = 0; + double maxLon = Double.NEGATIVE_INFINITY; + double minLon = Double.POSITIVE_INFINITY; + double maxLat = Double.NEGATIVE_INFINITY; + double minLat = Double.POSITIVE_INFINITY; for (int i = 0; i < count; i++) { GeoPoint point = in.nextValue(); - values[i] = new GeoPoint(point.lat(), point.lon()); + values[i].reset(point.lat(), point.lon()); + centroidLat += point.getLat(); + centroidLon += point.getLon(); + maxLon = Math.max(maxLon, values[i].getLon()); + minLon = Math.min(minLon, values[i].getLon()); + maxLat = Math.max(maxLat, values[i].getLat()); + minLat = Math.min(minLat, values[i].getLat()); } + centroid.reset(centroidLat / count, centroidLon / count); + boundingBox.topLeft().reset(maxLat, minLon); + boundingBox.bottomRight().reset(minLat, maxLon); } else { resize(0); } @@ -280,7 +307,7 @@ protected void resize(int newSize) { int oldLength = values.length; values = ArrayUtil.grow(values, count); for (int i = oldLength; i < values.length; ++i) { - values[i] = new GeoPoint(); + values[i] = new GeoPoint(); } } } @@ -364,6 +391,21 @@ public double geohashDistanceWithDefault(String geohash, double defaultValue) { } return geohashDistance(geohash); } + + @Override + public int getDimensionalType() { + return size() == 0 ? -1 : 0; + } + + @Override + public GeoPoint getCentroid() { + return size() == 0 ? null : centroid; + } + + @Override + public GeoBoundingBox getBoundingBox() { + return size() == 0 ? null : boundingBox; + } } public static final class Booleans extends ScriptDocValues { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java index d79e70af829ad..2c7336f947c57 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java @@ -159,7 +159,9 @@ protected void innerWriteTo(StreamOutput out) throws IOException { protected void doXContentBody(XContentBuilder builder, Params params) throws IOException { builder.field("precision", precision); if (geoBoundingBox.isUnbounded() == false) { - geoBoundingBox.toXContent(builder, params); + builder.startObject(GeoBoundingBox.BOUNDS_FIELD.getPreferredName()); + geoBoundingBox.toXContentFragment(builder, true); + builder.endObject(); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregationBuilder.java index aa5e998a4665b..2ff200fa83f64 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregationBuilder.java @@ -201,7 +201,9 @@ protected XContentBuilder doXContentBody(XContentBuilder builder, Params params) builder.field(FIELD_SHARD_SIZE.getPreferredName(), shardSize); } if (geoBoundingBox.isUnbounded() == false) { - geoBoundingBox.toXContent(builder, params); + builder.startObject(GeoBoundingBox.BOUNDS_FIELD.getPreferredName()); + geoBoundingBox.toXContentFragment(builder, true); + builder.endObject(); } return builder; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBounds.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBounds.java index 7cd61768f9cf9..3f96a266a16a6 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBounds.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalGeoBounds.java @@ -161,7 +161,9 @@ public Object getProperty(List path) { public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { GeoBoundingBox bbox = resolveGeoBoundingBox(); if (bbox != null) { - bbox.toXContent(builder, params); + builder.startObject(GeoBoundingBox.BOUNDS_FIELD.getPreferredName()); + bbox.toXContentFragment(builder, true); + builder.endObject(); } return builder; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedGeoBounds.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedGeoBounds.java index 5755c210b87ce..b89634b8c2b4f 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedGeoBounds.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ParsedGeoBounds.java @@ -41,7 +41,9 @@ public String getType() { @Override public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { if (geoBoundingBox != null) { - geoBoundingBox.toXContent(builder, params); + builder.startObject(GeoBoundingBox.BOUNDS_FIELD.getPreferredName()); + geoBoundingBox.toXContentFragment(builder, true); + builder.endObject(); } return builder; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/pivot/GeoTileGroupSource.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/pivot/GeoTileGroupSource.java index d39ada712d9c6..3de24af8e878c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/pivot/GeoTileGroupSource.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/pivot/GeoTileGroupSource.java @@ -105,7 +105,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(PRECISION.getPreferredName(), precision); } if (geoBoundingBox != null) { - geoBoundingBox.toXContent(builder, params); + builder.startObject(GeoBoundingBox.BOUNDS_FIELD.getPreferredName()); + geoBoundingBox.toXContentFragment(builder, true); + builder.endObject(); } builder.endObject(); return builder; diff --git a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoShapeScriptDocValuesIT.java b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoShapeScriptDocValuesIT.java new file mode 100644 index 0000000000000..b1fa4d26e2795 --- /dev/null +++ b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoShapeScriptDocValuesIT.java @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.xpack.spatial.search; + +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.document.DocumentField; +import org.elasticsearch.common.geo.GeoBoundingBox; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.geo.GeometryTestUtils; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.utils.WellKnownText; +import org.elasticsearch.index.fielddata.ScriptDocValues; +import org.elasticsearch.index.mapper.GeoShapeIndexer; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.script.MockScriptPlugin; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; +import org.elasticsearch.xpack.spatial.LocalStateSpatialPlugin; +import org.elasticsearch.xpack.spatial.index.fielddata.GeoShapeValues; +import org.elasticsearch.xpack.spatial.util.GeoTestUtils; +import org.hamcrest.Matchers; +import org.junit.Before; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + +public class GeoShapeScriptDocValuesIT extends ESSingleNodeTestCase { + + @Override + protected Collection> getPlugins() { + return Arrays.asList(LocalStateSpatialPlugin.class, LocalStateCompositeXPackPlugin.class, CustomScriptPlugin.class); + } + + public static class CustomScriptPlugin extends MockScriptPlugin { + + @Override + protected Map, Object>> pluginScripts() { + Map, Object>> scripts = new HashMap<>(); + + scripts.put("lat", this::scriptLat); + scripts.put("lon", this::scriptLon); + scripts.put("height", this::scriptHeight); + scripts.put("width", this::scriptWidth); + return scripts; + } + + + private double scriptHeight(Map vars) { + Map doc = (Map) vars.get("doc"); + ScriptDocValues.Geometry geometry = assertGeometry(doc); + if (geometry.size() == 0) { + return Double.NaN; + } else { + GeoBoundingBox boundingBox = geometry.getBoundingBox(); + return boundingBox.topLeft().lat() - boundingBox.bottomRight().lat(); + } + } + + private double scriptWidth(Map vars) { + Map doc = (Map) vars.get("doc"); + ScriptDocValues.Geometry geometry = assertGeometry(doc); + if (geometry.size() == 0) { + return Double.NaN; + } else { + GeoBoundingBox boundingBox = geometry.getBoundingBox(); + return boundingBox.bottomRight().lon() - boundingBox.topLeft().lon(); + } + } + + private double scriptLat(Map vars) { + Map doc = (Map) vars.get("doc"); + ScriptDocValues.Geometry geometry = assertGeometry(doc); + return geometry.size() == 0 ? Double.NaN : geometry.getCentroid().lat(); + } + + private double scriptLon(Map vars) { + Map doc = (Map) vars.get("doc"); + ScriptDocValues.Geometry geometry = assertGeometry(doc); + return geometry.size() == 0 ? Double.NaN : geometry.getCentroid().lon(); + } + + private ScriptDocValues.Geometry assertGeometry(Map doc) { + ScriptDocValues.Geometry geometry = (ScriptDocValues.Geometry) doc.get("location"); + if (geometry.size() == 0) { + assertThat(geometry.getBoundingBox(), Matchers.nullValue()); + assertThat(geometry.getCentroid(), Matchers.nullValue()); + assertThat(geometry.getDimensionalType(), equalTo(-1)); + } else { + assertThat(geometry.getBoundingBox(), Matchers.notNullValue()); + assertThat(geometry.getCentroid(), Matchers.notNullValue()); + assertThat(geometry.getDimensionalType(), greaterThanOrEqualTo(0)); + assertThat(geometry.getDimensionalType(), lessThanOrEqualTo(2)); + } + return geometry; + } + } + + @Override + protected boolean forbidPrivateIndexSettings() { + return false; + } + + @Before + public void setupTestIndex() throws IOException { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("_doc") + .startObject("properties").startObject("location").field("type", "geo_shape"); + xContentBuilder.endObject().endObject().endObject().endObject(); + assertAcked(client().admin().indices().prepareCreate("test").addMapping("_doc", xContentBuilder)); + ensureGreen(); + } + + public void testRandomShape() throws Exception { + GeoShapeIndexer indexer = new GeoShapeIndexer(true, "test"); + Geometry geometry = indexer.prepareForIndexing(randomValueOtherThanMany(g -> { + try { + indexer.prepareForIndexing(g); + return false; + } catch (Exception e) { + return true; + } + }, () -> GeometryTestUtils.randomGeometry(false))); + client().prepareIndex("test", "_doc").setId("1") + .setSource(jsonBuilder().startObject() + .field("name", "TestPosition") + .field("location", WellKnownText.INSTANCE.toWKT(geometry)) + .endObject()) + .get(); + + client().admin().indices().prepareRefresh("test").get(); + + GeoShapeValues.GeoShapeValue value = GeoTestUtils.geoShapeValue(geometry); + + SearchResponse searchResponse = client().prepareSearch().addStoredField("_source") + .addScriptField("lat", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lat", Collections.emptyMap())) + .addScriptField("lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lon", Collections.emptyMap())) + .addScriptField("height", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "height", Collections.emptyMap())) + .addScriptField("width", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "width", Collections.emptyMap())) + .get(); + assertSearchResponse(searchResponse); + Map fields = searchResponse.getHits().getHits()[0].getFields(); + assertThat(fields.get("lat").getValue(), equalTo(value.lat())); + assertThat(fields.get("lon").getValue(), equalTo(value.lon())); + assertThat(fields.get("height").getValue(), equalTo(value.boundingBox().maxY() - value.boundingBox().minY())); + assertThat(fields.get("width").getValue(), equalTo(value.boundingBox().maxX() - value.boundingBox().minX())); + } + + public void testNullShape() throws Exception { + client().prepareIndex("test", "_doc").setId("1") + .setSource(jsonBuilder().startObject() + .field("name", "TestPosition") + .nullField("location") + .endObject()) + .get(); + + client().admin().indices().prepareRefresh("test").get(); + + SearchResponse searchResponse = client().prepareSearch().addStoredField("_source") + .addScriptField("lat", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lat", Collections.emptyMap())) + .addScriptField("lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lon", Collections.emptyMap())) + .addScriptField("height", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "height", Collections.emptyMap())) + .addScriptField("width", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "width", Collections.emptyMap())) + .get(); + assertSearchResponse(searchResponse); + Map fields = searchResponse.getHits().getHits()[0].getFields(); + assertThat(fields.get("lat").getValue(), equalTo(Double.NaN)); + assertThat(fields.get("lon").getValue(), equalTo(Double.NaN)); + assertThat(fields.get("height").getValue(), equalTo(Double.NaN)); + assertThat(fields.get("width").getValue(), equalTo(Double.NaN)); + } +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java index bdec83de9bd5c..5de7f3dc92751 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java @@ -7,6 +7,9 @@ package org.elasticsearch.xpack.spatial.index.fielddata; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.xcontent.ToXContentFragment; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.mapper.GeoShapeIndexer; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.geometry.Rectangle; @@ -37,7 +40,7 @@ public abstract class GeoShapeValues { public static GeoShapeValues EMPTY = new GeoShapeValues() { - private GeoShapeValuesSourceType DEFAULT_VALUES_SOURCE_TYPE = GeoShapeValuesSourceType.instance(); + private final GeoShapeValuesSourceType DEFAULT_VALUES_SOURCE_TYPE = GeoShapeValuesSourceType.instance(); @Override public boolean advanceExact(int doc) { return false; @@ -80,21 +83,28 @@ protected GeoShapeValues() { /** thin wrapper around a {@link GeometryDocValueReader} which encodes / decodes values using * the Geo decoder */ - public static class GeoShapeValue { + public static class GeoShapeValue implements ToXContentFragment { private static final WellKnownText MISSING_GEOMETRY_PARSER = new WellKnownText(true, new GeographyValidator(true)); - + private static final GeoShapeIndexer MISSING_GEOSHAPE_INDEXER = new GeoShapeIndexer(true, "missing"); private final GeometryDocValueReader reader; private final BoundingBox boundingBox; private final Tile2DVisitor tile2DVisitor; - public GeoShapeValue(GeometryDocValueReader reader) { - this.reader = reader; + public GeoShapeValue() { + this.reader = new GeometryDocValueReader(); this.boundingBox = new BoundingBox(); - tile2DVisitor = new Tile2DVisitor(); + this.tile2DVisitor = new Tile2DVisitor(); + } + + /** + * reset the geometry. + */ + public void reset(BytesRef bytesRef) { + this.reader.reset(bytesRef); + this.boundingBox.reset(reader.getExtent(), CoordinateEncoder.GEO); } public BoundingBox boundingBox() { - boundingBox.reset(reader.getExtent(), CoordinateEncoder.GEO); return boundingBox; } @@ -132,17 +142,21 @@ public double lon() { public static GeoShapeValue missing(String missing) { try { - final GeoShapeIndexer indexer = new GeoShapeIndexer(true, "missing"); - final Geometry geometry = indexer.prepareForIndexing(MISSING_GEOMETRY_PARSER.fromWKT(missing)); + final Geometry geometry = MISSING_GEOSHAPE_INDEXER.prepareForIndexing(MISSING_GEOMETRY_PARSER.fromWKT(missing)); final BinaryGeoShapeDocValuesField field = new BinaryGeoShapeDocValuesField("missing"); - field.add(indexer.indexShape(geometry), geometry); - final GeometryDocValueReader reader = new GeometryDocValueReader(); - reader.reset(field.binaryValue()); - return new GeoShapeValue(reader); + field.add(MISSING_GEOSHAPE_INDEXER.indexShape(geometry), geometry); + final GeoShapeValue value = new GeoShapeValue(); + value.reset(field.binaryValue()); + return value; } catch (IOException | ParseException e) { throw new IllegalArgumentException("Can't apply missing value [" + missing + "]", e); } } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + throw new IllegalArgumentException("cannot write xcontent for geo_shape doc value"); + } } public static class BoundingBox { diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/AbstractAtomicGeoShapeShapeFieldData.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/AbstractAtomicGeoShapeShapeFieldData.java index 8794b13c73211..59ed40ad9041f 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/AbstractAtomicGeoShapeShapeFieldData.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/AbstractAtomicGeoShapeShapeFieldData.java @@ -8,11 +8,14 @@ package org.elasticsearch.xpack.spatial.index.fielddata.plain; import org.apache.lucene.util.Accountable; +import org.elasticsearch.common.geo.GeoBoundingBox; +import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.index.fielddata.SortedBinaryDocValues; import org.elasticsearch.xpack.spatial.index.fielddata.GeoShapeValues; import org.elasticsearch.xpack.spatial.index.fielddata.LeafGeoShapeFieldData; +import java.io.IOException; import java.util.Collection; import java.util.Collections; @@ -24,8 +27,8 @@ public final SortedBinaryDocValues getBytesValues() { } @Override - public final ScriptDocValues.BytesRefs getScriptValues() { - throw new UnsupportedOperationException("scripts are not supported by geo_shape doc values"); + public final ScriptDocValues.Geometry getScriptValues() { + return new GeoShapeScriptValues(getGeoShapeValues()); } public static LeafGeoShapeFieldData empty(final int maxDoc) { @@ -51,4 +54,53 @@ public GeoShapeValues getGeoShapeValues() { } }; } + + private static final class GeoShapeScriptValues extends ScriptDocValues.Geometry { + + private final GeoShapeValues in; + private final GeoPoint centroid = new GeoPoint(); + private final GeoBoundingBox boundingBox = new GeoBoundingBox(new GeoPoint(), new GeoPoint()); + private GeoShapeValues.GeoShapeValue value; + + private GeoShapeScriptValues(GeoShapeValues in) { + this.in = in; + } + + @Override + public void setNextDocId(int docId) throws IOException { + if (in.advanceExact(docId)) { + value = in.value(); + centroid.reset(value.lat(), value.lon()); + boundingBox.topLeft().reset(value.boundingBox().maxY(), value.boundingBox().minX()); + boundingBox.bottomRight().reset(value.boundingBox().minY(), value.boundingBox().maxX()); + } else { + value = null; + } + } + + @Override + public int getDimensionalType() { + return value == null ? -1 : value.dimensionalShapeType().ordinal(); + } + + @Override + public GeoPoint getCentroid() { + return value == null ? null : centroid; + } + + @Override + public GeoBoundingBox getBoundingBox() { + return value == null ? null : boundingBox; + } + + @Override + public GeoShapeValues.GeoShapeValue get(int index) { + return value; + } + + @Override + public int size() { + return value == null ? 0 : 1; + } + } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/LatLonShapeDVAtomicShapeFieldData.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/LatLonShapeDVAtomicShapeFieldData.java index f8da671431e82..b41918ffdcc51 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/LatLonShapeDVAtomicShapeFieldData.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/LatLonShapeDVAtomicShapeFieldData.java @@ -11,10 +11,8 @@ import org.apache.lucene.index.DocValues; import org.apache.lucene.index.LeafReader; import org.apache.lucene.util.Accountable; -import org.apache.lucene.util.BytesRef; import org.elasticsearch.search.aggregations.support.ValuesSourceType; import org.elasticsearch.xpack.spatial.index.fielddata.GeoShapeValues; -import org.elasticsearch.xpack.spatial.index.fielddata.GeometryDocValueReader; import org.elasticsearch.xpack.spatial.search.aggregations.support.GeoShapeValuesSourceType; import java.io.IOException; @@ -50,8 +48,7 @@ public void close() { public GeoShapeValues getGeoShapeValues() { try { final BinaryDocValues binaryValues = DocValues.getBinary(reader, fieldName); - final GeometryDocValueReader reader = new GeometryDocValueReader(); - final GeoShapeValues.GeoShapeValue geoShapeValue = new GeoShapeValues.GeoShapeValue(reader); + final GeoShapeValues.GeoShapeValue geoShapeValue = new GeoShapeValues.GeoShapeValue(); return new GeoShapeValues() { @Override @@ -66,8 +63,7 @@ public ValuesSourceType valuesSourceType() { @Override public GeoShapeValue value() throws IOException { - final BytesRef encoded = binaryValues.binaryValue(); - reader.reset(encoded); + geoShapeValue.reset(binaryValues.binaryValue()); return geoShapeValue; } }; diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/util/GeoTestUtils.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/util/GeoTestUtils.java index 3c775b7c66652..f484e9c982ed8 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/util/GeoTestUtils.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/util/GeoTestUtils.java @@ -55,9 +55,10 @@ public static BinaryGeoShapeDocValuesField binaryGeoShapeDocValuesField(String n return field; } - public static GeoShapeValues.GeoShapeValue geoShapeValue(Geometry geometry) throws IOException { - GeometryDocValueReader reader = geometryDocValueReader(geometry, CoordinateEncoder.GEO); - return new GeoShapeValues.GeoShapeValue(reader); + public static GeoShapeValues.GeoShapeValue geoShapeValue(Geometry geometry) { + GeoShapeValues.GeoShapeValue value = new GeoShapeValues.GeoShapeValue(); + value.reset(binaryGeoShapeDocValuesField("test", geometry).binaryValue()); + return value; } public static GeoBoundingBox randomBBox() { diff --git a/x-pack/plugin/spatial/src/yamlRestTest/resources/rest-api-spec/test/70_script_doc_values.yml b/x-pack/plugin/spatial/src/yamlRestTest/resources/rest-api-spec/test/70_script_doc_values.yml new file mode 100644 index 0000000000000..ecde387a7a4cf --- /dev/null +++ b/x-pack/plugin/spatial/src/yamlRestTest/resources/rest-api-spec/test/70_script_doc_values.yml @@ -0,0 +1,93 @@ +setup: + - do: + indices.create: + index: test + body: + settings: + number_of_shards: 1 + mappings: + properties: + geo_shape: + type: geo_shape + + - do: + index: + index: test + id: 1 + body: + geo_shape: "POLYGON((24.04725 59.942,24.04825 59.94125,24.04875 59.94125,24.04875 59.94175,24.048 59.9425,24.0475 59.94275,24.0465 59.94225,24.046 59.94225,24.04575 59.9425,24.04525 59.94225,24.04725 59.942))" + - do: + indices.refresh: {} + +--- +"centroid": + - do: + search: + rest_total_hits_as_int: true + body: + script_fields: + centroid: + script: + source: "doc['geo_shape'].getCentroid()" + - match: { hits.hits.0.fields.centroid.0.lat: 59.942043484188616 } + - match: { hits.hits.0.fields.centroid.0.lon: 24.047588920220733 } + +--- +"bounding box": + - do: + search: + rest_total_hits_as_int: true + body: + script_fields: + bbox: + script: + source: "doc['geo_shape'].getBoundingBox()" + - match: { hits.hits.0.fields.bbox.0.top_left.lat: 59.942749994806945 } + - match: { hits.hits.0.fields.bbox.0.top_left.lon: 24.045249950140715 } + - match: { hits.hits.0.fields.bbox.0.bottom_right.lat: 59.94124996941537 } + - match: { hits.hits.0.fields.bbox.0.bottom_right.lon: 24.048749981448054 } + +--- +"bounding box points": + - do: + search: + rest_total_hits_as_int: true + body: + script_fields: + topLeft: + script: + source: "doc['geo_shape'].getBoundingBox().topLeft()" + bottomRight: + script: + source: "doc['geo_shape'].getBoundingBox().bottomRight()" + - match: { hits.hits.0.fields.topLeft.0.lat: 59.942749994806945 } + - match: { hits.hits.0.fields.topLeft.0.lon: 24.045249950140715 } + - match: { hits.hits.0.fields.bottomRight.0.lat: 59.94124996941537 } + - match: { hits.hits.0.fields.bottomRight.0.lon: 24.048749981448054 } + +--- +"dimensional type": + - do: + search: + rest_total_hits_as_int: true + body: + script_fields: + type: + script: + source: "doc['geo_shape'].getDimensionalType()" + + - match: { hits.hits.0.fields.type.0: 2 } + +--- +"geoshape value": + - do: + catch: /illegal_argument_exception/ + search: + rest_total_hits_as_int: true + body: + script_fields: + type: + script: + source: "doc['geo_shape'].get(0)" + + - match: { error.root_cause.0.reason: "cannot write xcontent for geo_shape doc value" }