diff --git a/docs/changelog/91298.yaml b/docs/changelog/91298.yaml new file mode 100644 index 0000000000000..77077b3b0b293 --- /dev/null +++ b/docs/changelog/91298.yaml @@ -0,0 +1,6 @@ +pr: 91298 +summary: Support `cartesian_bounds` aggregation on point and shape +area: Geo +type: enhancement +issues: + - 90157 diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/GeoBoundsIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/GeoBoundsIT.java index af498a17fe5b2..12bb521358642 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/GeoBoundsIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/GeoBoundsIT.java @@ -10,19 +10,10 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.geo.GeoPoint; -import org.elasticsearch.common.util.BigArray; -import org.elasticsearch.search.aggregations.InternalAggregation; -import org.elasticsearch.search.aggregations.bucket.global.Global; -import org.elasticsearch.search.aggregations.bucket.terms.Terms; -import org.elasticsearch.search.aggregations.bucket.terms.Terms.Bucket; +import org.elasticsearch.common.geo.SpatialPoint; import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.geo.RandomGeoGenerator; -import java.util.List; - -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.elasticsearch.search.aggregations.AggregationBuilders.geoBounds; -import static org.elasticsearch.search.aggregations.AggregationBuilders.global; -import static org.elasticsearch.search.aggregations.AggregationBuilders.terms; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.closeTo; @@ -30,224 +21,97 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.sameInstance; @ESIntegTestCase.SuiteScopeTestCase -public class GeoBoundsIT extends AbstractGeoTestCase { - private static final String aggName = "geoBounds"; +public class GeoBoundsIT extends SpatialBoundsAggregationTestBase { - public void testSingleValuedField() throws Exception { - SearchResponse response = client().prepareSearch(IDX_NAME) - .addAggregation(geoBounds(aggName).field(SINGLE_VALUED_FIELD_NAME).wrapLongitude(false)) + public void testSingleValuedFieldNearDateLine() { + SearchResponse response = client().prepareSearch(DATELINE_IDX_NAME) + .addAggregation(boundsAgg(aggName(), SINGLE_VALUED_FIELD_NAME).wrapLongitude(false)) .get(); assertSearchResponse(response); - GeoBounds geoBounds = response.getAggregations().get(aggName); + GeoPoint geoValuesTopLeft = new GeoPoint(38, -179); + GeoPoint geoValuesBottomRight = new GeoPoint(-24, 178); + + GeoBounds geoBounds = response.getAggregations().get(aggName()); assertThat(geoBounds, notNullValue()); - assertThat(geoBounds.getName(), equalTo(aggName)); + assertThat(geoBounds.getName(), equalTo(aggName())); GeoPoint topLeft = geoBounds.topLeft(); GeoPoint bottomRight = geoBounds.bottomRight(); - assertThat(topLeft.lat(), closeTo(singleTopLeft.lat(), GEOHASH_TOLERANCE)); - assertThat(topLeft.lon(), closeTo(singleTopLeft.lon(), GEOHASH_TOLERANCE)); - assertThat(bottomRight.lat(), closeTo(singleBottomRight.lat(), GEOHASH_TOLERANCE)); - assertThat(bottomRight.lon(), closeTo(singleBottomRight.lon(), GEOHASH_TOLERANCE)); + assertThat(topLeft.getY(), closeTo(geoValuesTopLeft.getY(), GEOHASH_TOLERANCE)); + assertThat(topLeft.getX(), closeTo(geoValuesTopLeft.getX(), GEOHASH_TOLERANCE)); + assertThat(bottomRight.getY(), closeTo(geoValuesBottomRight.getY(), GEOHASH_TOLERANCE)); + assertThat(bottomRight.getX(), closeTo(geoValuesBottomRight.getX(), GEOHASH_TOLERANCE)); } - public void testSingleValuedField_getProperty() throws Exception { - SearchResponse searchResponse = client().prepareSearch(IDX_NAME) - .setQuery(matchAllQuery()) - .addAggregation(global("global").subAggregation(geoBounds(aggName).field(SINGLE_VALUED_FIELD_NAME).wrapLongitude(false))) - .get(); - - assertSearchResponse(searchResponse); - - Global global = searchResponse.getAggregations().get("global"); - assertThat(global, notNullValue()); - assertThat(global.getName(), equalTo("global")); - assertThat(global.getDocCount(), equalTo((long) numDocs)); - assertThat(global.getAggregations(), notNullValue()); - assertThat(global.getAggregations().asMap().size(), equalTo(1)); - - GeoBounds geobounds = global.getAggregations().get(aggName); - assertThat(geobounds, notNullValue()); - assertThat(geobounds.getName(), equalTo(aggName)); - assertThat((GeoBounds) ((InternalAggregation) global).getProperty(aggName), sameInstance(geobounds)); - GeoPoint topLeft = geobounds.topLeft(); - GeoPoint bottomRight = geobounds.bottomRight(); - assertThat(topLeft.lat(), closeTo(singleTopLeft.lat(), GEOHASH_TOLERANCE)); - assertThat(topLeft.lon(), closeTo(singleTopLeft.lon(), GEOHASH_TOLERANCE)); - assertThat(bottomRight.lat(), closeTo(singleBottomRight.lat(), GEOHASH_TOLERANCE)); - assertThat(bottomRight.lon(), closeTo(singleBottomRight.lon(), GEOHASH_TOLERANCE)); - assertThat((double) ((InternalAggregation) global).getProperty(aggName + ".top"), closeTo(singleTopLeft.lat(), GEOHASH_TOLERANCE)); - assertThat((double) ((InternalAggregation) global).getProperty(aggName + ".left"), closeTo(singleTopLeft.lon(), GEOHASH_TOLERANCE)); - assertThat( - (double) ((InternalAggregation) global).getProperty(aggName + ".bottom"), - closeTo(singleBottomRight.lat(), GEOHASH_TOLERANCE) - ); - assertThat( - (double) ((InternalAggregation) global).getProperty(aggName + ".right"), - closeTo(singleBottomRight.lon(), GEOHASH_TOLERANCE) - ); - } + public void testSingleValuedFieldNearDateLineWrapLongitude() { - public void testMultiValuedField() throws Exception { - SearchResponse response = client().prepareSearch(IDX_NAME) - .addAggregation(geoBounds(aggName).field(MULTI_VALUED_FIELD_NAME).wrapLongitude(false)) + GeoPoint geoValuesTopLeft = new GeoPoint(38, 170); + GeoPoint geoValuesBottomRight = new GeoPoint(-24, -175); + SearchResponse response = client().prepareSearch(DATELINE_IDX_NAME) + .addAggregation(boundsAgg(aggName(), SINGLE_VALUED_FIELD_NAME).wrapLongitude(true)) .get(); assertSearchResponse(response); - GeoBounds geoBounds = response.getAggregations().get(aggName); + GeoBounds geoBounds = response.getAggregations().get(aggName()); assertThat(geoBounds, notNullValue()); - assertThat(geoBounds.getName(), equalTo(aggName)); + assertThat(geoBounds.getName(), equalTo(aggName())); GeoPoint topLeft = geoBounds.topLeft(); GeoPoint bottomRight = geoBounds.bottomRight(); - assertThat(topLeft.lat(), closeTo(multiTopLeft.lat(), GEOHASH_TOLERANCE)); - assertThat(topLeft.lon(), closeTo(multiTopLeft.lon(), GEOHASH_TOLERANCE)); - assertThat(bottomRight.lat(), closeTo(multiBottomRight.lat(), GEOHASH_TOLERANCE)); - assertThat(bottomRight.lon(), closeTo(multiBottomRight.lon(), GEOHASH_TOLERANCE)); + assertThat(topLeft.getY(), closeTo(geoValuesTopLeft.getY(), GEOHASH_TOLERANCE)); + assertThat(topLeft.getX(), closeTo(geoValuesTopLeft.getX(), GEOHASH_TOLERANCE)); + assertThat(bottomRight.getY(), closeTo(geoValuesBottomRight.getY(), GEOHASH_TOLERANCE)); + assertThat(bottomRight.getX(), closeTo(geoValuesBottomRight.getX(), GEOHASH_TOLERANCE)); } - public void testUnmapped() throws Exception { - SearchResponse response = client().prepareSearch(UNMAPPED_IDX_NAME) - .addAggregation(geoBounds(aggName).field(SINGLE_VALUED_FIELD_NAME).wrapLongitude(false)) - .get(); - - assertSearchResponse(response); - - GeoBounds geoBounds = response.getAggregations().get(aggName); - assertThat(geoBounds, notNullValue()); - assertThat(geoBounds.getName(), equalTo(aggName)); - GeoPoint topLeft = geoBounds.topLeft(); - GeoPoint bottomRight = geoBounds.bottomRight(); - assertThat(topLeft, equalTo(null)); - assertThat(bottomRight, equalTo(null)); + @Override + protected String aggName() { + return "geoBounds"; } - public void testPartiallyUnmapped() throws Exception { - SearchResponse response = client().prepareSearch(IDX_NAME, UNMAPPED_IDX_NAME) - .addAggregation(geoBounds(aggName).field(SINGLE_VALUED_FIELD_NAME).wrapLongitude(false)) - .get(); - - assertSearchResponse(response); - - GeoBounds geoBounds = response.getAggregations().get(aggName); - assertThat(geoBounds, notNullValue()); - assertThat(geoBounds.getName(), equalTo(aggName)); - GeoPoint topLeft = geoBounds.topLeft(); - GeoPoint bottomRight = geoBounds.bottomRight(); - assertThat(topLeft.lat(), closeTo(singleTopLeft.lat(), GEOHASH_TOLERANCE)); - assertThat(topLeft.lon(), closeTo(singleTopLeft.lon(), GEOHASH_TOLERANCE)); - assertThat(bottomRight.lat(), closeTo(singleBottomRight.lat(), GEOHASH_TOLERANCE)); - assertThat(bottomRight.lon(), closeTo(singleBottomRight.lon(), GEOHASH_TOLERANCE)); + @Override + public GeoBoundsAggregationBuilder boundsAgg(String aggName, String fieldName) { + return new GeoBoundsAggregationBuilder(aggName).field(fieldName).wrapLongitude(false); } - public void testEmptyAggregation() throws Exception { - SearchResponse searchResponse = client().prepareSearch(EMPTY_IDX_NAME) - .setQuery(matchAllQuery()) - .addAggregation(geoBounds(aggName).field(SINGLE_VALUED_FIELD_NAME).wrapLongitude(false)) - .get(); - - assertThat(searchResponse.getHits().getTotalHits().value, equalTo(0L)); - GeoBounds geoBounds = searchResponse.getAggregations().get(aggName); - assertThat(geoBounds, notNullValue()); - assertThat(geoBounds.getName(), equalTo(aggName)); - GeoPoint topLeft = geoBounds.topLeft(); - GeoPoint bottomRight = geoBounds.bottomRight(); - assertThat(topLeft, equalTo(null)); - assertThat(bottomRight, equalTo(null)); + @Override + protected void assertBoundsLimits(SpatialBounds geoBounds) { + assertThat(geoBounds.topLeft().getY(), allOf(greaterThanOrEqualTo(-90.0), lessThanOrEqualTo(90.0))); + assertThat(geoBounds.topLeft().getX(), allOf(greaterThanOrEqualTo(-180.0), lessThanOrEqualTo(180.0))); + assertThat(geoBounds.bottomRight().getY(), allOf(greaterThanOrEqualTo(-90.0), lessThanOrEqualTo(90.0))); + assertThat(geoBounds.bottomRight().getX(), allOf(greaterThanOrEqualTo(-180.0), lessThanOrEqualTo(180.0))); } - public void testSingleValuedFieldNearDateLine() throws Exception { - SearchResponse response = client().prepareSearch(DATELINE_IDX_NAME) - .addAggregation(geoBounds(aggName).field(SINGLE_VALUED_FIELD_NAME).wrapLongitude(false)) - .get(); - - assertSearchResponse(response); - - GeoPoint geoValuesTopLeft = new GeoPoint(38, -179); - GeoPoint geoValuesBottomRight = new GeoPoint(-24, 178); - - GeoBounds geoBounds = response.getAggregations().get(aggName); - assertThat(geoBounds, notNullValue()); - assertThat(geoBounds.getName(), equalTo(aggName)); - GeoPoint topLeft = geoBounds.topLeft(); - GeoPoint bottomRight = geoBounds.bottomRight(); - assertThat(topLeft.lat(), closeTo(geoValuesTopLeft.lat(), GEOHASH_TOLERANCE)); - assertThat(topLeft.lon(), closeTo(geoValuesTopLeft.lon(), GEOHASH_TOLERANCE)); - assertThat(bottomRight.lat(), closeTo(geoValuesBottomRight.lat(), GEOHASH_TOLERANCE)); - assertThat(bottomRight.lon(), closeTo(geoValuesBottomRight.lon(), GEOHASH_TOLERANCE)); + @Override + protected String fieldTypeName() { + return "geo_point"; } - public void testSingleValuedFieldNearDateLineWrapLongitude() throws Exception { - - GeoPoint geoValuesTopLeft = new GeoPoint(38, 170); - GeoPoint geoValuesBottomRight = new GeoPoint(-24, -175); - SearchResponse response = client().prepareSearch(DATELINE_IDX_NAME) - .addAggregation(geoBounds(aggName).field(SINGLE_VALUED_FIELD_NAME).wrapLongitude(true)) - .get(); - - assertSearchResponse(response); - - GeoBounds geoBounds = response.getAggregations().get(aggName); - assertThat(geoBounds, notNullValue()); - assertThat(geoBounds.getName(), equalTo(aggName)); - GeoPoint topLeft = geoBounds.topLeft(); - GeoPoint bottomRight = geoBounds.bottomRight(); - assertThat(topLeft.lat(), closeTo(geoValuesTopLeft.lat(), GEOHASH_TOLERANCE)); - assertThat(topLeft.lon(), closeTo(geoValuesTopLeft.lon(), GEOHASH_TOLERANCE)); - assertThat(bottomRight.lat(), closeTo(geoValuesBottomRight.lat(), GEOHASH_TOLERANCE)); - assertThat(bottomRight.lon(), closeTo(geoValuesBottomRight.lon(), GEOHASH_TOLERANCE)); + @Override + protected GeoPoint makePoint(double x, double y) { + return new GeoPoint(y, x); } - /** - * This test forces the {@link GeoBoundsAggregator} to resize the {@link BigArray}s it uses to ensure they are resized correctly - */ - public void testSingleValuedFieldAsSubAggToHighCardTermsAgg() { - SearchResponse response = client().prepareSearch(HIGH_CARD_IDX_NAME) - .addAggregation( - terms("terms").field(NUMBER_FIELD_NAME) - .subAggregation(geoBounds(aggName).field(SINGLE_VALUED_FIELD_NAME).wrapLongitude(false)) - ) - .get(); - - assertSearchResponse(response); - - Terms terms = response.getAggregations().get("terms"); - assertThat(terms, notNullValue()); - assertThat(terms.getName(), equalTo("terms")); - List buckets = terms.getBuckets(); - assertThat(buckets.size(), equalTo(10)); - for (int i = 0; i < 10; i++) { - Bucket bucket = buckets.get(i); - assertThat(bucket, notNullValue()); - assertThat("InternalBucket " + bucket.getKey() + " has wrong number of documents", bucket.getDocCount(), equalTo(1L)); - GeoBounds geoBounds = bucket.getAggregations().get(aggName); - assertThat(geoBounds, notNullValue()); - assertThat(geoBounds.getName(), equalTo(aggName)); - assertThat(geoBounds.topLeft().getLat(), allOf(greaterThanOrEqualTo(-90.0), lessThanOrEqualTo(90.0))); - assertThat(geoBounds.topLeft().getLon(), allOf(greaterThanOrEqualTo(-180.0), lessThanOrEqualTo(180.0))); - assertThat(geoBounds.bottomRight().getLat(), allOf(greaterThanOrEqualTo(-90.0), lessThanOrEqualTo(90.0))); - assertThat(geoBounds.bottomRight().getLon(), allOf(greaterThanOrEqualTo(-180.0), lessThanOrEqualTo(180.0))); - } + @Override + protected GeoPoint randomPoint() { + return RandomGeoGenerator.randomPoint(random()); } - public void testSingleValuedFieldWithZeroLon() throws Exception { - SearchResponse response = client().prepareSearch(IDX_ZERO_NAME) - .addAggregation(geoBounds(aggName).field(SINGLE_VALUED_FIELD_NAME).wrapLongitude(false)) - .get(); + @Override + protected void resetX(SpatialPoint point, double x) { + ((GeoPoint) point).resetLon(x); + } - assertSearchResponse(response); + @Override + protected void resetY(SpatialPoint point, double y) { + ((GeoPoint) point).resetLat(y); + } - GeoBounds geoBounds = response.getAggregations().get(aggName); - assertThat(geoBounds, notNullValue()); - assertThat(geoBounds.getName(), equalTo(aggName)); - GeoPoint topLeft = geoBounds.topLeft(); - GeoPoint bottomRight = geoBounds.bottomRight(); - assertThat(topLeft.lat(), closeTo(1.0, GEOHASH_TOLERANCE)); - assertThat(topLeft.lon(), closeTo(0.0, GEOHASH_TOLERANCE)); - assertThat(bottomRight.lat(), closeTo(1.0, GEOHASH_TOLERANCE)); - assertThat(bottomRight.lon(), closeTo(0.0, GEOHASH_TOLERANCE)); + @Override + protected GeoPoint reset(SpatialPoint point, double x, double y) { + return ((GeoPoint) point).reset(y, x); } } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/GeoCentroidIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/GeoCentroidIT.java index b2329bb0ef530..d1caf6d5cb80a 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/GeoCentroidIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/GeoCentroidIT.java @@ -10,148 +10,92 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.geo.GeoPoint; -import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.common.geo.SpatialPoint; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoGrid; -import org.elasticsearch.search.aggregations.bucket.global.Global; import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.geo.RandomGeoGenerator; import java.util.List; -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.elasticsearch.search.aggregations.AggregationBuilders.geoCentroid; import static org.elasticsearch.search.aggregations.AggregationBuilders.geohashGrid; -import static org.elasticsearch.search.aggregations.AggregationBuilders.global; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; -import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.sameInstance; /** * Integration Test for GeoCentroid metric aggregator */ @ESIntegTestCase.SuiteScopeTestCase -public class GeoCentroidIT extends AbstractGeoTestCase { - private static final String aggName = "geoCentroid"; +public class GeoCentroidIT extends CentroidAggregationTestBase { - public void testEmptyAggregation() throws Exception { - SearchResponse response = client().prepareSearch(EMPTY_IDX_NAME) - .setQuery(matchAllQuery()) - .addAggregation(geoCentroid(aggName).field(SINGLE_VALUED_FIELD_NAME)) + public void testSingleValueFieldAsSubAggToGeohashGrid() { + SearchResponse response = client().prepareSearch(HIGH_CARD_IDX_NAME) + .addAggregation( + geohashGrid("geoGrid").field(SINGLE_VALUED_FIELD_NAME) + .subAggregation(centroidAgg(aggName()).field(SINGLE_VALUED_FIELD_NAME)) + ) .get(); assertSearchResponse(response); - GeoCentroid geoCentroid = response.getAggregations().get(aggName); - assertThat(response.getHits().getTotalHits().value, equalTo(0L)); - assertThat(geoCentroid, notNullValue()); - assertThat(geoCentroid.getName(), equalTo(aggName)); - assertThat(geoCentroid.centroid(), equalTo(null)); - assertEquals(0, geoCentroid.count()); + GeoGrid grid = response.getAggregations().get("geoGrid"); + assertThat(grid, notNullValue()); + assertThat(grid.getName(), equalTo("geoGrid")); + List buckets = grid.getBuckets(); + for (GeoGrid.Bucket cell : buckets) { + String geohash = cell.getKeyAsString(); + SpatialPoint expectedCentroid = expectedCentroidsForGeoHash.get(geohash); + GeoCentroid centroidAgg = cell.getAggregations().get(aggName()); + assertSameCentroid(centroidAgg.centroid(), expectedCentroid); + } } - public void testUnmapped() throws Exception { - SearchResponse response = client().prepareSearch(UNMAPPED_IDX_NAME) - .addAggregation(geoCentroid(aggName).field(SINGLE_VALUED_FIELD_NAME)) - .get(); - assertSearchResponse(response); - - GeoCentroid geoCentroid = response.getAggregations().get(aggName); - assertThat(geoCentroid, notNullValue()); - assertThat(geoCentroid.getName(), equalTo(aggName)); - assertThat(geoCentroid.centroid(), equalTo(null)); - assertEquals(0, geoCentroid.count()); + @Override + protected String aggName() { + return "geoCentroid"; } - public void testPartiallyUnmapped() throws Exception { - SearchResponse response = client().prepareSearch(IDX_NAME, UNMAPPED_IDX_NAME) - .addAggregation(geoCentroid(aggName).field(SINGLE_VALUED_FIELD_NAME)) - .get(); - assertSearchResponse(response); - - GeoCentroid geoCentroid = response.getAggregations().get(aggName); - assertThat(geoCentroid, notNullValue()); - assertThat(geoCentroid.getName(), equalTo(aggName)); - assertSameCentroid(geoCentroid.centroid(), singleCentroid); - assertEquals(numDocs, geoCentroid.count()); + @Override + public GeoCentroidAggregationBuilder centroidAgg(String name) { + return new GeoCentroidAggregationBuilder(name); } - public void testSingleValuedField() throws Exception { - SearchResponse response = client().prepareSearch(IDX_NAME) - .setQuery(matchAllQuery()) - .addAggregation(geoCentroid(aggName).field(SINGLE_VALUED_FIELD_NAME)) - .get(); - assertSearchResponse(response); - - GeoCentroid geoCentroid = response.getAggregations().get(aggName); - assertThat(geoCentroid, notNullValue()); - assertThat(geoCentroid.getName(), equalTo(aggName)); - assertSameCentroid(geoCentroid.centroid(), singleCentroid); - assertEquals(numDocs, geoCentroid.count()); + /** Geo has different coordinate names than cartesian */ + @Override + protected String coordinateName(String coordinate) { + return switch (coordinate) { + case "x" -> "lon"; + case "y" -> "lat"; + default -> throw new IllegalArgumentException("Unknown coordinate: " + coordinate); + }; } - public void testSingleValueFieldGetProperty() { - SearchResponse response = client().prepareSearch(IDX_NAME) - .setQuery(matchAllQuery()) - .addAggregation(global("global").subAggregation(geoCentroid(aggName).field(SINGLE_VALUED_FIELD_NAME))) - .get(); - assertSearchResponse(response); + @Override + protected String fieldTypeName() { + return "geo_point"; + } - Global global = response.getAggregations().get("global"); - assertThat(global, notNullValue()); - assertThat(global.getName(), equalTo("global")); - assertThat(global.getDocCount(), equalTo((long) numDocs)); - assertThat(global.getAggregations(), notNullValue()); - assertThat(global.getAggregations().asMap().size(), equalTo(1)); - - GeoCentroid geoCentroid = global.getAggregations().get(aggName); - assertThat(geoCentroid, notNullValue()); - assertThat(geoCentroid.getName(), equalTo(aggName)); - assertThat((GeoCentroid) ((InternalAggregation) global).getProperty(aggName), sameInstance(geoCentroid)); - assertSameCentroid(geoCentroid.centroid(), singleCentroid); - assertThat( - ((GeoPoint) ((InternalAggregation) global).getProperty(aggName + ".value")).lat(), - closeTo(singleCentroid.lat(), GEOHASH_TOLERANCE) - ); - assertThat( - ((GeoPoint) ((InternalAggregation) global).getProperty(aggName + ".value")).lon(), - closeTo(singleCentroid.lon(), GEOHASH_TOLERANCE) - ); - assertThat((double) ((InternalAggregation) global).getProperty(aggName + ".lat"), closeTo(singleCentroid.lat(), GEOHASH_TOLERANCE)); - assertThat((double) ((InternalAggregation) global).getProperty(aggName + ".lon"), closeTo(singleCentroid.lon(), GEOHASH_TOLERANCE)); - assertEquals(numDocs, (long) ((InternalAggregation) global).getProperty(aggName + ".count")); + @Override + protected GeoPoint makePoint(double x, double y) { + return new GeoPoint(y, x); } - public void testMultiValuedField() throws Exception { - SearchResponse searchResponse = client().prepareSearch(IDX_NAME) - .setQuery(matchAllQuery()) - .addAggregation(geoCentroid(aggName).field(MULTI_VALUED_FIELD_NAME)) - .get(); - assertSearchResponse(searchResponse); + @Override + protected GeoPoint randomPoint() { + return RandomGeoGenerator.randomPoint(random()); + } - GeoCentroid geoCentroid = searchResponse.getAggregations().get(aggName); - assertThat(geoCentroid, notNullValue()); - assertThat(geoCentroid.getName(), equalTo(aggName)); - assertSameCentroid(geoCentroid.centroid(), multiCentroid); - assertEquals(2 * numDocs, geoCentroid.count()); + @Override + protected void resetX(SpatialPoint point, double x) { + ((GeoPoint) point).resetLon(x); } - public void testSingleValueFieldAsSubAggToGeohashGrid() { - SearchResponse response = client().prepareSearch(HIGH_CARD_IDX_NAME) - .addAggregation( - geohashGrid("geoGrid").field(SINGLE_VALUED_FIELD_NAME).subAggregation(geoCentroid(aggName).field(SINGLE_VALUED_FIELD_NAME)) - ) - .get(); - assertSearchResponse(response); + @Override + protected void resetY(SpatialPoint point, double y) { + ((GeoPoint) point).resetLat(y); + } - GeoGrid grid = response.getAggregations().get("geoGrid"); - assertThat(grid, notNullValue()); - assertThat(grid.getName(), equalTo("geoGrid")); - List buckets = grid.getBuckets(); - for (GeoGrid.Bucket cell : buckets) { - String geohash = cell.getKeyAsString(); - GeoPoint expectedCentroid = expectedCentroidsForGeoHash.get(geohash); - GeoCentroid centroidAgg = cell.getAggregations().get(aggName); - assertSameCentroid(centroidAgg.centroid(), expectedCentroid); - } + @Override + protected GeoPoint reset(SpatialPoint point, double x, double y) { + return ((GeoPoint) point).reset(y, x); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/GeoBounds.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/GeoBounds.java index 828f2e307f7b6..b22199f88116e 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/GeoBounds.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/GeoBounds.java @@ -9,20 +9,8 @@ package org.elasticsearch.search.aggregations.metrics; import org.elasticsearch.common.geo.GeoPoint; -import org.elasticsearch.search.aggregations.Aggregation; /** * An aggregation that computes a bounding box in which all documents of the current bucket are. */ -public interface GeoBounds extends Aggregation { - - /** - * Get the top-left location of the bounding box. - */ - GeoPoint topLeft(); - - /** - * Get the bottom-right location of the bounding box. - */ - GeoPoint bottomRight(); -} +public interface GeoBounds extends SpatialBounds {} diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalBounds.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalBounds.java new file mode 100644 index 0000000000000..6641eea1016cf --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalBounds.java @@ -0,0 +1,122 @@ +/* + * 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.aggregations.metrics; + +import org.elasticsearch.common.geo.BoundingBox; +import org.elasticsearch.common.geo.GeoBoundingBox; +import org.elasticsearch.common.geo.SpatialPoint; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.support.SamplingContext; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public abstract class InternalBounds extends InternalAggregation implements SpatialBounds { + public final double top; + public final double bottom; + + public InternalBounds(String name, double top, double bottom, Map metadata) { + super(name, metadata); + this.top = top; + this.bottom = bottom; + } + + /** + * Read from a stream. + */ + public InternalBounds(StreamInput in) throws IOException { + super(in); + top = in.readDouble(); + bottom = in.readDouble(); + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + out.writeDouble(top); + out.writeDouble(bottom); + } + + @Override + public InternalAggregation finalizeSampling(SamplingContext samplingContext) { + return this; + } + + @Override + protected boolean mustReduceOnSingleInternalAgg() { + return false; + } + + @Override + public Object getProperty(List path) { + if (path.isEmpty()) { + return this; + } else if (path.size() == 1) { + BoundingBox bbox = resolveBoundingBox(); + String bBoxSide = path.get(0); + return switch (bBoxSide) { + case "top" -> bbox.top(); + case "left" -> bbox.left(); + case "bottom" -> bbox.bottom(); + case "right" -> bbox.right(); + default -> throw new IllegalArgumentException("Found unknown path element [" + bBoxSide + "] in [" + getName() + "]"); + }; + } else if (path.size() == 2) { + BoundingBox bbox = resolveBoundingBox(); + T cornerPoint = null; + String cornerString = path.get(0); + cornerPoint = switch (cornerString) { + case "top_left" -> bbox.topLeft(); + case "bottom_right" -> bbox.bottomRight(); + default -> throw new IllegalArgumentException("Found unknown path element [" + cornerString + "] in [" + getName() + "]"); + }; + return selectCoordinate(path.get(1), cornerPoint); + } else { + throw new IllegalArgumentException("path not supported for [" + getName() + "]: " + path); + } + } + + protected abstract Object selectCoordinate(String coordinateString, T cornerPoint); + + @Override + public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { + BoundingBox bbox = resolveBoundingBox(); + if (bbox != null) { + builder.startObject(GeoBoundingBox.BOUNDS_FIELD.getPreferredName()); + bbox.toXContentFragment(builder); + builder.endObject(); + } + return builder; + } + + protected abstract BoundingBox resolveBoundingBox(); + + @Override + public T topLeft() { + BoundingBox bbox = resolveBoundingBox(); + if (bbox == null) { + return null; + } else { + return bbox.topLeft(); + } + } + + @Override + public T bottomRight() { + BoundingBox bbox = resolveBoundingBox(); + if (bbox == null) { + return null; + } else { + return bbox.bottomRight(); + } + } +} 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 77cfa6ecc00e3..bc35090201303 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 @@ -14,17 +14,13 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.search.aggregations.AggregationReduceContext; import org.elasticsearch.search.aggregations.InternalAggregation; -import org.elasticsearch.search.aggregations.support.SamplingContext; -import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Objects; -public class InternalGeoBounds extends InternalAggregation implements GeoBounds { - public final double top; - public final double bottom; +public class InternalGeoBounds extends InternalBounds implements GeoBounds { public final double posLeft; public final double posRight; public final double negLeft; @@ -42,9 +38,7 @@ public InternalGeoBounds( boolean wrapLongitude, Map metadata ) { - super(name, metadata); - this.top = top; - this.bottom = bottom; + super(name, top, bottom, metadata); this.posLeft = posLeft; this.posRight = posRight; this.negLeft = negLeft; @@ -57,8 +51,6 @@ public InternalGeoBounds( */ public InternalGeoBounds(StreamInput in) throws IOException { super(in); - top = in.readDouble(); - bottom = in.readDouble(); posLeft = in.readDouble(); posRight = in.readDouble(); negLeft = in.readDouble(); @@ -68,8 +60,7 @@ public InternalGeoBounds(StreamInput in) throws IOException { @Override protected void doWriteTo(StreamOutput out) throws IOException { - out.writeDouble(top); - out.writeDouble(bottom); + super.doWriteTo(out); out.writeDouble(posLeft); out.writeDouble(posRight); out.writeDouble(negLeft); @@ -117,61 +108,16 @@ public InternalAggregation reduce(List aggregations, Aggreg } @Override - public InternalAggregation finalizeSampling(SamplingContext samplingContext) { - return this; + protected Object selectCoordinate(String coordinateString, GeoPoint cornerPoint) { + return switch (coordinateString) { + case "lat" -> cornerPoint.lat(); + case "lon" -> cornerPoint.lon(); + default -> throw new IllegalArgumentException("Found unknown path element [" + coordinateString + "] in [" + getName() + "]"); + }; } @Override - protected boolean mustReduceOnSingleInternalAgg() { - return false; - } - - @Override - public Object getProperty(List path) { - if (path.isEmpty()) { - return this; - } else if (path.size() == 1) { - GeoBoundingBox geoBoundingBox = resolveGeoBoundingBox(); - String bBoxSide = path.get(0); - return switch (bBoxSide) { - case "top" -> geoBoundingBox.top(); - case "left" -> geoBoundingBox.left(); - case "bottom" -> geoBoundingBox.bottom(); - case "right" -> geoBoundingBox.right(); - default -> throw new IllegalArgumentException("Found unknown path element [" + bBoxSide + "] in [" + getName() + "]"); - }; - } else if (path.size() == 2) { - GeoBoundingBox geoBoundingBox = resolveGeoBoundingBox(); - GeoPoint cornerPoint = null; - String cornerString = path.get(0); - cornerPoint = switch (cornerString) { - case "top_left" -> geoBoundingBox.topLeft(); - case "bottom_right" -> geoBoundingBox.bottomRight(); - default -> throw new IllegalArgumentException("Found unknown path element [" + cornerString + "] in [" + getName() + "]"); - }; - String latLonString = path.get(1); - return switch (latLonString) { - case "lat" -> cornerPoint.lat(); - case "lon" -> cornerPoint.lon(); - default -> throw new IllegalArgumentException("Found unknown path element [" + latLonString + "] in [" + getName() + "]"); - }; - } else { - throw new IllegalArgumentException("path not supported for [" + getName() + "]: " + path); - } - } - - @Override - public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { - GeoBoundingBox bbox = resolveGeoBoundingBox(); - if (bbox != null) { - builder.startObject(GeoBoundingBox.BOUNDS_FIELD.getPreferredName()); - bbox.toXContentFragment(builder); - builder.endObject(); - } - return builder; - } - - private GeoBoundingBox resolveGeoBoundingBox() { + protected GeoBoundingBox resolveBoundingBox() { if (Double.isInfinite(top)) { return null; } else if (Double.isInfinite(posLeft)) { @@ -191,26 +137,6 @@ private GeoBoundingBox resolveGeoBoundingBox() { } } - @Override - public GeoPoint topLeft() { - GeoBoundingBox geoBoundingBox = resolveGeoBoundingBox(); - if (geoBoundingBox == null) { - return null; - } else { - return geoBoundingBox.topLeft(); - } - } - - @Override - public GeoPoint bottomRight() { - GeoBoundingBox geoBoundingBox = resolveGeoBoundingBox(); - if (geoBoundingBox == null) { - return null; - } else { - return geoBoundingBox.bottomRight(); - } - } - @Override public boolean equals(Object obj) { if (this == obj) return true; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/SpatialBounds.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/SpatialBounds.java new file mode 100644 index 0000000000000..5bdd44ca0a058 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/SpatialBounds.java @@ -0,0 +1,28 @@ +/* + * 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.aggregations.metrics; + +import org.elasticsearch.common.geo.SpatialPoint; +import org.elasticsearch.search.aggregations.Aggregation; + +/** + * An aggregation that computes a bounding box in which all documents of the current bucket are. + */ +public interface SpatialBounds extends Aggregation { + + /** + * Get the top-left location of the bounding box. + */ + T topLeft(); + + /** + * Get the bottom-right location of the bounding box. + */ + T bottomRight(); +} diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/support/AggregationInspectionHelper.java b/server/src/main/java/org/elasticsearch/search/aggregations/support/AggregationInspectionHelper.java index 74c4ce507523c..96e8408387d19 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/support/AggregationInspectionHelper.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/support/AggregationInspectionHelper.java @@ -27,10 +27,10 @@ import org.elasticsearch.search.aggregations.bucket.terms.UnmappedSignificantTerms; import org.elasticsearch.search.aggregations.bucket.terms.UnmappedTerms; import org.elasticsearch.search.aggregations.metrics.InternalAvg; +import org.elasticsearch.search.aggregations.metrics.InternalBounds; import org.elasticsearch.search.aggregations.metrics.InternalCardinality; import org.elasticsearch.search.aggregations.metrics.InternalCentroid; import org.elasticsearch.search.aggregations.metrics.InternalExtendedStats; -import org.elasticsearch.search.aggregations.metrics.InternalGeoBounds; import org.elasticsearch.search.aggregations.metrics.InternalHDRPercentileRanks; import org.elasticsearch.search.aggregations.metrics.InternalHDRPercentiles; import org.elasticsearch.search.aggregations.metrics.InternalMedianAbsoluteDeviation; @@ -158,7 +158,7 @@ public static boolean hasValue(InternalExtendedStats agg) { return agg.getCount() > 0; } - public static boolean hasValue(InternalGeoBounds agg) { + public static boolean hasValue(InternalBounds agg) { return (agg.topLeft() == null && agg.bottomRight() == null) == false; } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/AbstractGeoTestCase.java b/test/framework/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractGeoTestCase.java similarity index 66% rename from server/src/test/java/org/elasticsearch/search/aggregations/metrics/AbstractGeoTestCase.java rename to test/framework/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractGeoTestCase.java index 02572eb461fb5..c423ce5773603 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/AbstractGeoTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/search/aggregations/metrics/AbstractGeoTestCase.java @@ -12,7 +12,6 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.Strings; import org.elasticsearch.common.document.DocumentField; -import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.SpatialPoint; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.geometry.utils.Geohash; @@ -20,7 +19,6 @@ import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.test.geo.RandomGeoGenerator; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; @@ -33,14 +31,13 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; -import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.equalTo; @ESIntegTestCase.SuiteScopeTestCase public abstract class AbstractGeoTestCase extends ESIntegTestCase { - protected static final String SINGLE_VALUED_FIELD_NAME = "geo_value"; - protected static final String MULTI_VALUED_FIELD_NAME = "geo_values"; + protected static final String SINGLE_VALUED_FIELD_NAME = "spatial_value"; + protected static final String MULTI_VALUED_FIELD_NAME = "spatial_values"; protected static final String NUMBER_FIELD_NAME = "l_values"; protected static final String UNMAPPED_IDX_NAME = "idx_unmapped"; protected static final String IDX_NAME = "idx"; @@ -48,15 +45,29 @@ public abstract class AbstractGeoTestCase extends ESIntegTestCase { protected static final String DATELINE_IDX_NAME = "dateline_idx"; protected static final String HIGH_CARD_IDX_NAME = "high_card_idx"; protected static final String IDX_ZERO_NAME = "idx_zero"; + protected static final double GEOHASH_TOLERANCE = 1E-5D; + // These fields need to be static because they are shared between test instances using SuiteScopeTestCase protected static int numDocs; protected static int numUniqueGeoPoints; - protected static GeoPoint[] singleValues, multiValues; - protected static GeoPoint singleTopLeft, singleBottomRight, multiTopLeft, multiBottomRight, singleCentroid, multiCentroid, + protected static SpatialPoint[] singleValues, multiValues; + protected static SpatialPoint singleTopLeft, singleBottomRight, multiTopLeft, multiBottomRight, singleCentroid, multiCentroid, unmappedCentroid; protected static Map expectedDocCountsForGeoHash = null; - protected static Map expectedCentroidsForGeoHash = null; - protected static final double GEOHASH_TOLERANCE = 1E-5D; + protected static Map expectedCentroidsForGeoHash = null; + + // These methods allow various implementations of SpatialPoint to be tested (eg. GeoPoint and CartesianPoint) + protected abstract String fieldTypeName(); + + protected abstract SpatialPoint makePoint(double x, double y); + + protected abstract SpatialPoint randomPoint(); + + protected abstract void resetX(SpatialPoint point, double x); + + protected abstract void resetY(SpatialPoint point, double y); + + protected abstract SpatialPoint reset(SpatialPoint point, double x, double y); @Override public void setupSuiteScopeCluster() throws Exception { @@ -64,9 +75,9 @@ public void setupSuiteScopeCluster() throws Exception { assertAcked( prepareCreate(IDX_NAME).setMapping( SINGLE_VALUED_FIELD_NAME, - "type=geo_point", + "type=" + fieldTypeName(), MULTI_VALUED_FIELD_NAME, - "type=geo_point", + "type=" + fieldTypeName(), NUMBER_FIELD_NAME, "type=long", "tag", @@ -74,37 +85,37 @@ public void setupSuiteScopeCluster() throws Exception { ) ); - singleTopLeft = new GeoPoint(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); - singleBottomRight = new GeoPoint(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY); - multiTopLeft = new GeoPoint(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); - multiBottomRight = new GeoPoint(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY); - singleCentroid = new GeoPoint(0, 0); - multiCentroid = new GeoPoint(0, 0); - unmappedCentroid = new GeoPoint(0, 0); + singleTopLeft = makePoint(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY); + singleBottomRight = makePoint(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + multiTopLeft = makePoint(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY); + multiBottomRight = makePoint(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + singleCentroid = makePoint(0, 0); + multiCentroid = makePoint(0, 0); + unmappedCentroid = makePoint(0, 0); numDocs = randomIntBetween(6, 20); numUniqueGeoPoints = randomIntBetween(1, numDocs); expectedDocCountsForGeoHash = new HashMap<>(numDocs * 2); expectedCentroidsForGeoHash = new HashMap<>(numDocs * 2); - singleValues = new GeoPoint[numUniqueGeoPoints]; + singleValues = new SpatialPoint[numUniqueGeoPoints]; for (int i = 0; i < singleValues.length; i++) { - singleValues[i] = RandomGeoGenerator.randomPoint(random()); + singleValues[i] = randomPoint(); updateBoundsTopLeft(singleValues[i], singleTopLeft); updateBoundsBottomRight(singleValues[i], singleBottomRight); } - multiValues = new GeoPoint[numUniqueGeoPoints]; + multiValues = new SpatialPoint[numUniqueGeoPoints]; for (int i = 0; i < multiValues.length; i++) { - multiValues[i] = RandomGeoGenerator.randomPoint(random()); + multiValues[i] = randomPoint(); updateBoundsTopLeft(multiValues[i], multiTopLeft); updateBoundsBottomRight(multiValues[i], multiBottomRight); } List builders = new ArrayList<>(); - GeoPoint singleVal; - final GeoPoint[] multiVal = new GeoPoint[2]; + SpatialPoint singleVal; + final SpatialPoint[] multiVal = new SpatialPoint[2]; double newMVLat, newMVLon; for (int i = 0; i < numDocs; i++) { singleVal = singleValues[i % numUniqueGeoPoints]; @@ -114,15 +125,15 @@ public void setupSuiteScopeCluster() throws Exception { client().prepareIndex(IDX_NAME) .setSource( jsonBuilder().startObject() - .array(SINGLE_VALUED_FIELD_NAME, singleVal.lon(), singleVal.lat()) + .array(SINGLE_VALUED_FIELD_NAME, singleVal.getX(), singleVal.getY()) .startArray(MULTI_VALUED_FIELD_NAME) .startArray() - .value(multiVal[0].lon()) - .value(multiVal[0].lat()) + .value(multiVal[0].getX()) + .value(multiVal[0].getY()) .endArray() .startArray() - .value(multiVal[1].lon()) - .value(multiVal[1].lat()) + .value(multiVal[1].getX()) + .value(multiVal[1].getY()) .endArray() .endArray() .field(NUMBER_FIELD_NAME, i) @@ -130,26 +141,28 @@ public void setupSuiteScopeCluster() throws Exception { .endObject() ) ); - singleCentroid = singleCentroid.reset( - singleCentroid.lat() + (singleVal.lat() - singleCentroid.lat()) / (i + 1), - singleCentroid.lon() + (singleVal.lon() - singleCentroid.lon()) / (i + 1) + singleCentroid = reset( + singleCentroid, + singleCentroid.getX() + (singleVal.getX() - singleCentroid.getX()) / (i + 1), + singleCentroid.getY() + (singleVal.getY() - singleCentroid.getY()) / (i + 1) ); - newMVLat = (multiVal[0].lat() + multiVal[1].lat()) / 2d; - newMVLon = (multiVal[0].lon() + multiVal[1].lon()) / 2d; - multiCentroid = multiCentroid.reset( - multiCentroid.lat() + (newMVLat - multiCentroid.lat()) / (i + 1), - multiCentroid.lon() + (newMVLon - multiCentroid.lon()) / (i + 1) + newMVLat = (multiVal[0].getY() + multiVal[1].getY()) / 2d; + newMVLon = (multiVal[0].getX() + multiVal[1].getX()) / 2d; + multiCentroid = reset( + multiCentroid, + multiCentroid.getX() + (newMVLon - multiCentroid.getX()) / (i + 1), + multiCentroid.getY() + (newMVLat - multiCentroid.getY()) / (i + 1) ); } - assertAcked(prepareCreate(EMPTY_IDX_NAME).setMapping(SINGLE_VALUED_FIELD_NAME, "type=geo_point")); + assertAcked(prepareCreate(EMPTY_IDX_NAME).setMapping(SINGLE_VALUED_FIELD_NAME, "type=" + fieldTypeName())); assertAcked( prepareCreate(DATELINE_IDX_NAME).setMapping( SINGLE_VALUED_FIELD_NAME, - "type=geo_point", + "type=" + fieldTypeName(), MULTI_VALUED_FIELD_NAME, - "type=geo_point", + "type=" + fieldTypeName(), NUMBER_FIELD_NAME, "type=long", "tag", @@ -157,19 +170,19 @@ public void setupSuiteScopeCluster() throws Exception { ) ); - GeoPoint[] geoValues = new GeoPoint[5]; - geoValues[0] = new GeoPoint(38, 178); - geoValues[1] = new GeoPoint(12, -179); - geoValues[2] = new GeoPoint(-24, 170); - geoValues[3] = new GeoPoint(32, -175); - geoValues[4] = new GeoPoint(-11, 178); + SpatialPoint[] geoValues = new SpatialPoint[5]; + geoValues[0] = makePoint(178, 38); + geoValues[1] = makePoint(-179, 12); + geoValues[2] = makePoint(170, -24); + geoValues[3] = makePoint(-175, 32); + geoValues[4] = makePoint(178, -11); for (int i = 0; i < 5; i++) { builders.add( client().prepareIndex(DATELINE_IDX_NAME) .setSource( jsonBuilder().startObject() - .array(SINGLE_VALUED_FIELD_NAME, geoValues[i].lon(), geoValues[i].lat()) + .array(SINGLE_VALUED_FIELD_NAME, geoValues[i].getX(), geoValues[i].getY()) .field(NUMBER_FIELD_NAME, i) .field("tag", "tag" + i) .endObject() @@ -180,9 +193,9 @@ public void setupSuiteScopeCluster() throws Exception { prepareCreate(HIGH_CARD_IDX_NAME).setSettings(Settings.builder().put("number_of_shards", 2)) .setMapping( SINGLE_VALUED_FIELD_NAME, - "type=geo_point", + "type=" + fieldTypeName(), MULTI_VALUED_FIELD_NAME, - "type=geo_point", + "type=" + fieldTypeName(), NUMBER_FIELD_NAME, "type=long,store=true", "tag", @@ -196,15 +209,15 @@ public void setupSuiteScopeCluster() throws Exception { client().prepareIndex(HIGH_CARD_IDX_NAME) .setSource( jsonBuilder().startObject() - .array(SINGLE_VALUED_FIELD_NAME, singleVal.lon(), singleVal.lat()) + .array(SINGLE_VALUED_FIELD_NAME, singleVal.getX(), singleVal.getY()) .startArray(MULTI_VALUED_FIELD_NAME) .startArray() - .value(multiValues[i % numUniqueGeoPoints].lon()) - .value(multiValues[i % numUniqueGeoPoints].lat()) + .value(multiValues[i % numUniqueGeoPoints].getX()) + .value(multiValues[i % numUniqueGeoPoints].getY()) .endArray() .startArray() - .value(multiValues[(i + 1) % numUniqueGeoPoints].lon()) - .value(multiValues[(i + 1) % numUniqueGeoPoints].lat()) + .value(multiValues[(i + 1) % numUniqueGeoPoints].getX()) + .value(multiValues[(i + 1) % numUniqueGeoPoints].getY()) .endArray() .endArray() .field(NUMBER_FIELD_NAME, i) @@ -219,7 +232,7 @@ public void setupSuiteScopeCluster() throws Exception { client().prepareIndex(IDX_ZERO_NAME) .setSource(jsonBuilder().startObject().array(SINGLE_VALUED_FIELD_NAME, 0.0, 1.0).endObject()) ); - assertAcked(prepareCreate(IDX_ZERO_NAME).setMapping(SINGLE_VALUED_FIELD_NAME, "type=geo_point")); + assertAcked(prepareCreate(IDX_ZERO_NAME).setMapping(SINGLE_VALUED_FIELD_NAME, "type=" + fieldTypeName())); indexRandom(true, builders); ensureSearchable(); @@ -250,8 +263,8 @@ public void setupSuiteScopeCluster() throws Exception { assertThat(totalHits, equalTo(2000L)); } - private void updateGeohashBucketsCentroid(final GeoPoint location) { - String hash = Geohash.stringEncode(location.lon(), location.lat(), Geohash.PRECISION); + private void updateGeohashBucketsCentroid(final SpatialPoint location) { + String hash = Geohash.stringEncode(location.getX(), location.getY(), Geohash.PRECISION); for (int precision = Geohash.PRECISION; precision > 0; --precision) { final String h = hash.substring(0, precision); expectedDocCountsForGeoHash.put(h, expectedDocCountsForGeoHash.getOrDefault(h, 0) + 1); @@ -259,46 +272,32 @@ private void updateGeohashBucketsCentroid(final GeoPoint location) { } } - private GeoPoint updateHashCentroid(String hash, final GeoPoint location) { - GeoPoint centroid = expectedCentroidsForGeoHash.getOrDefault(hash, null); + private SpatialPoint updateHashCentroid(String hash, final SpatialPoint location) { + SpatialPoint centroid = expectedCentroidsForGeoHash.getOrDefault(hash, null); if (centroid == null) { - return new GeoPoint(location.lat(), location.lon()); + return makePoint(location.getX(), location.getY()); } final int docCount = expectedDocCountsForGeoHash.get(hash); - final double newLon = centroid.lon() + (location.lon() - centroid.lon()) / docCount; - final double newLat = centroid.lat() + (location.lat() - centroid.lat()) / docCount; - return centroid.reset(newLat, newLon); + final double newLon = centroid.getX() + (location.getX() - centroid.getX()) / docCount; + final double newLat = centroid.getY() + (location.getY() - centroid.getY()) / docCount; + return reset(centroid, newLon, newLat); } - private void updateBoundsBottomRight(GeoPoint geoPoint, GeoPoint currentBound) { - if (geoPoint.lat() < currentBound.lat()) { - currentBound.resetLat(geoPoint.lat()); + private void updateBoundsBottomRight(SpatialPoint point, SpatialPoint currentBound) { + if (point.getY() < currentBound.getY()) { + resetY(currentBound, point.getY()); } - if (geoPoint.lon() > currentBound.lon()) { - currentBound.resetLon(geoPoint.lon()); + if (point.getX() > currentBound.getX()) { + resetX(currentBound, point.getX()); } } - private void updateBoundsTopLeft(GeoPoint geoPoint, GeoPoint currentBound) { - if (geoPoint.lat() > currentBound.lat()) { - currentBound.resetLat(geoPoint.lat()); + private void updateBoundsTopLeft(SpatialPoint point, SpatialPoint currentBound) { + if (point.getY() > currentBound.getY()) { + resetY(currentBound, point.getY()); } - if (geoPoint.lon() < currentBound.lon()) { - currentBound.resetLon(geoPoint.lon()); + if (point.getX() < currentBound.getX()) { + resetX(currentBound, point.getX()); } } - - protected void assertSameCentroid(SpatialPoint centroid, SpatialPoint expectedCentroid) { - String[] names = centroid.getClass() == GeoPoint.class ? new String[] { "longitude", "latitude" } : new String[] { "x", "y" }; - assertThat( - "Mismatching value for '" + names[0] + "' field of centroid", - centroid.getX(), - closeTo(expectedCentroid.getX(), GEOHASH_TOLERANCE) - ); - assertThat( - "Mismatching value for '" + names[1] + "' field of centroid", - centroid.getY(), - closeTo(expectedCentroid.getY(), GEOHASH_TOLERANCE) - ); - } } diff --git a/test/framework/src/main/java/org/elasticsearch/search/aggregations/metrics/CentroidAggregationTestBase.java b/test/framework/src/main/java/org/elasticsearch/search/aggregations/metrics/CentroidAggregationTestBase.java new file mode 100644 index 0000000000000..75f7264434225 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/search/aggregations/metrics/CentroidAggregationTestBase.java @@ -0,0 +1,167 @@ +/* + * 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.aggregations.metrics; + +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.common.geo.SpatialPoint; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.bucket.global.Global; +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; +import org.elasticsearch.test.ESIntegTestCase; + +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.elasticsearch.search.aggregations.AggregationBuilders.global; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; + +/** + * Integration Test for CartesianCentroid metric aggregator + */ +@ESIntegTestCase.SuiteScopeTestCase +public abstract class CentroidAggregationTestBase extends AbstractGeoTestCase { + protected abstract String aggName(); + + protected abstract ValuesSourceAggregationBuilder centroidAgg(String name); + + public void testEmptyAggregation() { + SearchResponse response = client().prepareSearch(EMPTY_IDX_NAME) + .setQuery(matchAllQuery()) + .addAggregation(centroidAgg(aggName()).field(SINGLE_VALUED_FIELD_NAME)) + .get(); + assertSearchResponse(response); + + CentroidAggregation geoCentroid = response.getAggregations().get(aggName()); + assertThat(response.getHits().getTotalHits().value, equalTo(0L)); + assertThat(geoCentroid, notNullValue()); + assertThat(geoCentroid.getName(), equalTo(aggName())); + assertThat(geoCentroid.centroid(), equalTo(null)); + assertEquals(0, geoCentroid.count()); + } + + public void testUnmapped() throws Exception { + SearchResponse response = client().prepareSearch(UNMAPPED_IDX_NAME) + .addAggregation(centroidAgg(aggName()).field(SINGLE_VALUED_FIELD_NAME)) + .get(); + assertSearchResponse(response); + + CentroidAggregation geoCentroid = response.getAggregations().get(aggName()); + assertThat(geoCentroid, notNullValue()); + assertThat(geoCentroid.getName(), equalTo(aggName())); + assertThat(geoCentroid.centroid(), equalTo(null)); + assertEquals(0, geoCentroid.count()); + } + + public void testPartiallyUnmapped() { + SearchResponse response = client().prepareSearch(IDX_NAME, UNMAPPED_IDX_NAME) + .addAggregation(centroidAgg(aggName()).field(SINGLE_VALUED_FIELD_NAME)) + .get(); + assertSearchResponse(response); + + CentroidAggregation geoCentroid = response.getAggregations().get(aggName()); + assertThat(geoCentroid, notNullValue()); + assertThat(geoCentroid.getName(), equalTo(aggName())); + assertSameCentroid(geoCentroid.centroid(), singleCentroid); + assertEquals(numDocs, geoCentroid.count()); + } + + public void testSingleValuedField() { + SearchResponse response = client().prepareSearch(IDX_NAME) + .setQuery(matchAllQuery()) + .addAggregation(centroidAgg(aggName()).field(SINGLE_VALUED_FIELD_NAME)) + .get(); + assertSearchResponse(response); + + CentroidAggregation geoCentroid = response.getAggregations().get(aggName()); + assertThat(geoCentroid, notNullValue()); + assertThat(geoCentroid.getName(), equalTo(aggName())); + assertSameCentroid(geoCentroid.centroid(), singleCentroid); + assertEquals(numDocs, geoCentroid.count()); + } + + public void testSingleValueFieldGetProperty() { + SearchResponse response = client().prepareSearch(IDX_NAME) + .setQuery(matchAllQuery()) + .addAggregation(global("global").subAggregation(centroidAgg(aggName()).field(SINGLE_VALUED_FIELD_NAME))) + .get(); + assertSearchResponse(response); + + Global global = response.getAggregations().get("global"); + assertThat(global, notNullValue()); + assertThat(global.getName(), equalTo("global")); + assertThat(global.getDocCount(), equalTo((long) numDocs)); + assertThat(global.getAggregations(), notNullValue()); + assertThat(global.getAggregations().asMap().size(), equalTo(1)); + + CentroidAggregation geoCentroid = global.getAggregations().get(aggName()); + assertThat(geoCentroid, notNullValue()); + assertThat(geoCentroid.getName(), equalTo(aggName())); + assertThat((CentroidAggregation) ((InternalAggregation) global).getProperty(aggName()), sameInstance(geoCentroid)); + assertSameCentroid(geoCentroid.centroid(), singleCentroid); + assertThat( + ((SpatialPoint) ((InternalAggregation) global).getProperty(aggName() + ".value")).getY(), + closeTo(singleCentroid.getY(), GEOHASH_TOLERANCE) + ); + assertThat( + ((SpatialPoint) ((InternalAggregation) global).getProperty(aggName() + ".value")).getX(), + closeTo(singleCentroid.getX(), GEOHASH_TOLERANCE) + ); + assertThat( + (double) ((InternalAggregation) global).getProperty(aggName() + "." + coordinateName("y")), + closeTo(singleCentroid.getY(), GEOHASH_TOLERANCE) + ); + assertThat( + (double) ((InternalAggregation) global).getProperty(aggName() + "." + coordinateName("x")), + closeTo(singleCentroid.getX(), GEOHASH_TOLERANCE) + ); + assertEquals(numDocs, (long) ((InternalAggregation) global).getProperty(aggName() + ".count")); + } + + public void testMultiValuedField() throws Exception { + SearchResponse searchResponse = client().prepareSearch(IDX_NAME) + .setQuery(matchAllQuery()) + .addAggregation(centroidAgg(aggName()).field(MULTI_VALUED_FIELD_NAME)) + .get(); + assertSearchResponse(searchResponse); + + CentroidAggregation geoCentroid = searchResponse.getAggregations().get(aggName()); + assertThat(geoCentroid, notNullValue()); + assertThat(geoCentroid.getName(), equalTo(aggName())); + assertSameCentroid(geoCentroid.centroid(), multiCentroid); + assertEquals(2 * numDocs, geoCentroid.count()); + } + + /** Override this if the spatial data uses different coordinate names (eg. Geo uses lon/at instead of x/y */ + protected String coordinateName(String coordinate) { + return coordinate; + } + + /** Override this if the spatial data needs custom tolerance calculations (eg. cartesian) */ + protected double tolerance(double a, double b) { + return GEOHASH_TOLERANCE; + } + + /** Override this if the spatial data needs custom normalization (eg. cartesian) */ + protected double normalize(double value) { + return value; + } + + protected void assertSameCentroid(SpatialPoint centroid, SpatialPoint expectedCentroid) { + String[] names = centroid.getClass() == GeoPoint.class ? new String[] { "longitude", "latitude" } : new String[] { "x", "y" }; + double x = normalize(centroid.getX()); + double y = normalize(centroid.getY()); + double ex = normalize(expectedCentroid.getX()); + double ey = normalize(expectedCentroid.getY()); + assertThat("Mismatching value for '" + names[0] + "' field of centroid", x, closeTo(ex, tolerance(x, ex))); + assertThat("Mismatching value for '" + names[1] + "' field of centroid", y, closeTo(ey, tolerance(y, ey))); + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/search/aggregations/metrics/SpatialBoundsAggregationTestBase.java b/test/framework/src/main/java/org/elasticsearch/search/aggregations/metrics/SpatialBoundsAggregationTestBase.java new file mode 100644 index 0000000000000..ac366576c8975 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/search/aggregations/metrics/SpatialBoundsAggregationTestBase.java @@ -0,0 +1,209 @@ +/* + * 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.aggregations.metrics; + +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.geo.SpatialPoint; +import org.elasticsearch.common.util.BigArray; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.bucket.global.Global; +import org.elasticsearch.search.aggregations.bucket.terms.Terms; +import org.elasticsearch.search.aggregations.bucket.terms.Terms.Bucket; +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; +import org.elasticsearch.test.ESIntegTestCase; + +import java.util.List; + +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.elasticsearch.search.aggregations.AggregationBuilders.global; +import static org.elasticsearch.search.aggregations.AggregationBuilders.terms; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; + +@ESIntegTestCase.SuiteScopeTestCase +public abstract class SpatialBoundsAggregationTestBase extends AbstractGeoTestCase { + + protected abstract String aggName(); + + protected abstract ValuesSourceAggregationBuilder boundsAgg(String aggName, String fieldName); + + protected abstract void assertBoundsLimits(SpatialBounds spatialBounds); + + public void testSingleValuedField() throws Exception { + SearchResponse response = client().prepareSearch(IDX_NAME).addAggregation(boundsAgg(aggName(), SINGLE_VALUED_FIELD_NAME)).get(); + + assertSearchResponse(response); + + SpatialBounds geoBounds = response.getAggregations().get(aggName()); + assertThat(geoBounds, notNullValue()); + assertThat(geoBounds.getName(), equalTo(aggName())); + T topLeft = geoBounds.topLeft(); + T bottomRight = geoBounds.bottomRight(); + assertThat(topLeft.getY(), closeTo(singleTopLeft.getY(), GEOHASH_TOLERANCE)); + assertThat(topLeft.getX(), closeTo(singleTopLeft.getX(), GEOHASH_TOLERANCE)); + assertThat(bottomRight.getY(), closeTo(singleBottomRight.getY(), GEOHASH_TOLERANCE)); + assertThat(bottomRight.getX(), closeTo(singleBottomRight.getX(), GEOHASH_TOLERANCE)); + } + + public void testSingleValuedField_getProperty() { + SearchResponse searchResponse = client().prepareSearch(IDX_NAME) + .setQuery(matchAllQuery()) + .addAggregation(global("global").subAggregation(boundsAgg(aggName(), SINGLE_VALUED_FIELD_NAME))) + .get(); + + assertSearchResponse(searchResponse); + + Global global = searchResponse.getAggregations().get("global"); + assertThat(global, notNullValue()); + assertThat(global.getName(), equalTo("global")); + assertThat(global.getDocCount(), equalTo((long) numDocs)); + assertThat(global.getAggregations(), notNullValue()); + assertThat(global.getAggregations().asMap().size(), equalTo(1)); + + SpatialBounds geobounds = global.getAggregations().get(aggName()); + assertThat(geobounds, notNullValue()); + assertThat(geobounds.getName(), equalTo(aggName())); + assertThat((SpatialBounds) ((InternalAggregation) global).getProperty(aggName()), sameInstance(geobounds)); + T topLeft = geobounds.topLeft(); + T bottomRight = geobounds.bottomRight(); + assertThat(topLeft.getY(), closeTo(singleTopLeft.getY(), GEOHASH_TOLERANCE)); + assertThat(topLeft.getX(), closeTo(singleTopLeft.getX(), GEOHASH_TOLERANCE)); + assertThat(bottomRight.getY(), closeTo(singleBottomRight.getY(), GEOHASH_TOLERANCE)); + assertThat(bottomRight.getX(), closeTo(singleBottomRight.getX(), GEOHASH_TOLERANCE)); + assertThat( + (double) ((InternalAggregation) global).getProperty(aggName() + ".top"), + closeTo(singleTopLeft.getY(), GEOHASH_TOLERANCE) + ); + assertThat( + (double) ((InternalAggregation) global).getProperty(aggName() + ".left"), + closeTo(singleTopLeft.getX(), GEOHASH_TOLERANCE) + ); + assertThat( + (double) ((InternalAggregation) global).getProperty(aggName() + ".bottom"), + closeTo(singleBottomRight.getY(), GEOHASH_TOLERANCE) + ); + assertThat( + (double) ((InternalAggregation) global).getProperty(aggName() + ".right"), + closeTo(singleBottomRight.getX(), GEOHASH_TOLERANCE) + ); + } + + public void testMultiValuedField() throws Exception { + SearchResponse response = client().prepareSearch(IDX_NAME).addAggregation(boundsAgg(aggName(), MULTI_VALUED_FIELD_NAME)).get(); + + assertSearchResponse(response); + + SpatialBounds geoBounds = response.getAggregations().get(aggName()); + assertThat(geoBounds, notNullValue()); + assertThat(geoBounds.getName(), equalTo(aggName())); + T topLeft = geoBounds.topLeft(); + T bottomRight = geoBounds.bottomRight(); + assertThat(topLeft.getY(), closeTo(multiTopLeft.getY(), GEOHASH_TOLERANCE)); + assertThat(topLeft.getX(), closeTo(multiTopLeft.getX(), GEOHASH_TOLERANCE)); + assertThat(bottomRight.getY(), closeTo(multiBottomRight.getY(), GEOHASH_TOLERANCE)); + assertThat(bottomRight.getX(), closeTo(multiBottomRight.getX(), GEOHASH_TOLERANCE)); + } + + public void testUnmapped() throws Exception { + SearchResponse response = client().prepareSearch(UNMAPPED_IDX_NAME) + .addAggregation(boundsAgg(aggName(), SINGLE_VALUED_FIELD_NAME)) + .get(); + + assertSearchResponse(response); + + SpatialBounds geoBounds = response.getAggregations().get(aggName()); + assertThat(geoBounds, notNullValue()); + assertThat(geoBounds.getName(), equalTo(aggName())); + T topLeft = geoBounds.topLeft(); + T bottomRight = geoBounds.bottomRight(); + assertThat(topLeft, equalTo(null)); + assertThat(bottomRight, equalTo(null)); + } + + public void testPartiallyUnmapped() throws Exception { + SearchResponse response = client().prepareSearch(IDX_NAME, UNMAPPED_IDX_NAME) + .addAggregation(boundsAgg(aggName(), SINGLE_VALUED_FIELD_NAME)) + .get(); + + assertSearchResponse(response); + + SpatialBounds geoBounds = response.getAggregations().get(aggName()); + assertThat(geoBounds, notNullValue()); + assertThat(geoBounds.getName(), equalTo(aggName())); + T topLeft = geoBounds.topLeft(); + T bottomRight = geoBounds.bottomRight(); + assertThat(topLeft.getY(), closeTo(singleTopLeft.getY(), GEOHASH_TOLERANCE)); + assertThat(topLeft.getX(), closeTo(singleTopLeft.getX(), GEOHASH_TOLERANCE)); + assertThat(bottomRight.getY(), closeTo(singleBottomRight.getY(), GEOHASH_TOLERANCE)); + assertThat(bottomRight.getX(), closeTo(singleBottomRight.getX(), GEOHASH_TOLERANCE)); + } + + public void testEmptyAggregation() throws Exception { + SearchResponse searchResponse = client().prepareSearch(EMPTY_IDX_NAME) + .setQuery(matchAllQuery()) + .addAggregation(boundsAgg(aggName(), SINGLE_VALUED_FIELD_NAME)) + .get(); + + assertThat(searchResponse.getHits().getTotalHits().value, equalTo(0L)); + SpatialBounds geoBounds = searchResponse.getAggregations().get(aggName()); + assertThat(geoBounds, notNullValue()); + assertThat(geoBounds.getName(), equalTo(aggName())); + T topLeft = geoBounds.topLeft(); + T bottomRight = geoBounds.bottomRight(); + assertThat(topLeft, equalTo(null)); + assertThat(bottomRight, equalTo(null)); + } + + /** + * This test forces the bounds {@link MetricsAggregator} to resize the {@link BigArray}s it uses to ensure they are resized correctly + */ + public void testSingleValuedFieldAsSubAggToHighCardTermsAgg() { + SearchResponse response = client().prepareSearch(HIGH_CARD_IDX_NAME) + .addAggregation(terms("terms").field(NUMBER_FIELD_NAME).subAggregation(boundsAgg(aggName(), SINGLE_VALUED_FIELD_NAME))) + .get(); + + assertSearchResponse(response); + + Terms terms = response.getAggregations().get("terms"); + assertThat(terms, notNullValue()); + assertThat(terms.getName(), equalTo("terms")); + List buckets = terms.getBuckets(); + assertThat(buckets.size(), equalTo(10)); + for (int i = 0; i < 10; i++) { + Bucket bucket = buckets.get(i); + assertThat(bucket, notNullValue()); + assertThat("InternalBucket " + bucket.getKey() + " has wrong number of documents", bucket.getDocCount(), equalTo(1L)); + SpatialBounds geoBounds = bucket.getAggregations().get(aggName()); + assertThat(geoBounds, notNullValue()); + assertThat(geoBounds.getName(), equalTo(aggName())); + assertBoundsLimits(geoBounds); + } + } + + public void testSingleValuedFieldWithZeroLon() { + SearchResponse response = client().prepareSearch(IDX_ZERO_NAME) + .addAggregation(boundsAgg(aggName(), SINGLE_VALUED_FIELD_NAME)) + .get(); + + assertSearchResponse(response); + + SpatialBounds geoBounds = response.getAggregations().get(aggName()); + assertThat(geoBounds, notNullValue()); + assertThat(geoBounds.getName(), equalTo(aggName())); + T topLeft = geoBounds.topLeft(); + T bottomRight = geoBounds.bottomRight(); + assertThat(topLeft.getY(), closeTo(1.0, GEOHASH_TOLERANCE)); + assertThat(topLeft.getX(), closeTo(0.0, GEOHASH_TOLERANCE)); + assertThat(bottomRight.getY(), closeTo(1.0, GEOHASH_TOLERANCE)); + assertThat(bottomRight.getX(), closeTo(0.0, GEOHASH_TOLERANCE)); + } +} diff --git a/server/src/test/java/org/elasticsearch/test/geo/RandomGeoGenerator.java b/test/framework/src/main/java/org/elasticsearch/test/geo/RandomGeoGenerator.java similarity index 100% rename from server/src/test/java/org/elasticsearch/test/geo/RandomGeoGenerator.java rename to test/framework/src/main/java/org/elasticsearch/test/geo/RandomGeoGenerator.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/spatial/action/SpatialStatsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/spatial/action/SpatialStatsAction.java index aa579814d0a98..e9d179c68c378 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/spatial/action/SpatialStatsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/spatial/action/SpatialStatsAction.java @@ -41,7 +41,8 @@ private SpatialStatsAction() { public enum Item { GEOLINE, GEOHEX, - CARTESIANCENTROID + CARTESIANCENTROID, + CARTESIANBOUNDS } public static class Request extends BaseNodesRequest implements ToXContentObject { diff --git a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsIT.java b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsIT.java new file mode 100644 index 0000000000000..85e371023348f --- /dev/null +++ b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsIT.java @@ -0,0 +1,75 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.common.geo.SpatialPoint; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.search.aggregations.metrics.SpatialBounds; +import org.elasticsearch.search.aggregations.metrics.SpatialBoundsAggregationTestBase; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.xpack.spatial.LocalStateSpatialPlugin; +import org.elasticsearch.xpack.spatial.common.CartesianPoint; +import org.elasticsearch.xpack.spatial.util.ShapeTestUtils; + +import java.util.Collection; +import java.util.Collections; + +@ESIntegTestCase.SuiteScopeTestCase +public class CartesianBoundsIT extends SpatialBoundsAggregationTestBase { + @Override + protected Collection> nodePlugins() { + return Collections.singleton(LocalStateSpatialPlugin.class); + } + + @Override + protected String aggName() { + return "cartesianBounds"; + } + + @Override + protected CartesianBoundsAggregationBuilder boundsAgg(String aggName, String fieldName) { + return new CartesianBoundsAggregationBuilder(aggName).field(fieldName); + } + + @Override + protected void assertBoundsLimits(SpatialBounds spatialBounds) { + // Cartesian does not have specific bounds limits like geo data does + } + + @Override + protected String fieldTypeName() { + return "point"; + } + + @Override + protected CartesianPoint makePoint(double x, double y) { + return new CartesianPoint((float) x, (float) y); + } + + @Override + protected CartesianPoint randomPoint() { + Point point = ShapeTestUtils.randomPointNotExtreme(false); + return makePoint(point.getX(), point.getY()); + } + + @Override + protected void resetX(SpatialPoint point, double x) { + ((CartesianPoint) point).resetX((float) x); + } + + @Override + protected void resetY(SpatialPoint point, double y) { + ((CartesianPoint) point).resetY((float) y); + } + + @Override + protected CartesianPoint reset(SpatialPoint point, double x, double y) { + return ((CartesianPoint) point).reset((float) x, (float) y); + } +} diff --git a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidIT.java b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidIT.java new file mode 100644 index 0000000000000..ec3c78702596b --- /dev/null +++ b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidIT.java @@ -0,0 +1,81 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.common.geo.SpatialPoint; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.search.aggregations.metrics.CentroidAggregationTestBase; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.xpack.spatial.LocalStateSpatialPlugin; +import org.elasticsearch.xpack.spatial.common.CartesianPoint; +import org.elasticsearch.xpack.spatial.util.ShapeTestUtils; + +import java.util.Collection; +import java.util.Collections; + +/** + * Integration Test for CartesianCentroid metric aggregator + */ +@ESIntegTestCase.SuiteScopeTestCase +public class CartesianCentroidIT extends CentroidAggregationTestBase { + protected String aggName() { + return "cartesianCentroid"; + } + + @Override + protected Collection> nodePlugins() { + return Collections.singleton(LocalStateSpatialPlugin.class); + } + + @Override + protected CartesianCentroidAggregationBuilder centroidAgg(String name) { + return new CartesianCentroidAggregationBuilder(name); + } + + @Override + protected String fieldTypeName() { + return "point"; + } + + @Override + protected CartesianPoint makePoint(double x, double y) { + return new CartesianPoint((float) x, (float) y); + } + + @Override + protected CartesianPoint randomPoint() { + Point point = ShapeTestUtils.randomPointNotExtreme(false); + return makePoint(point.getX(), point.getY()); + } + + @Override + protected void resetX(SpatialPoint point, double x) { + ((CartesianPoint) point).resetX((float) x); + } + + @Override + protected void resetY(SpatialPoint point, double y) { + ((CartesianPoint) point).resetY((float) y); + } + + @Override + protected CartesianPoint reset(SpatialPoint point, double x, double y) { + return ((CartesianPoint) point).reset((float) x, (float) y); + } + + @Override + protected double tolerance(double a, double b) { + return Math.max(Math.abs(a), Math.abs(b)) / 1e5; + } + + @Override + protected double normalize(double value) { + return (float) value; + } +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java index 43d66d3c1f303..42614e69d8685 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java @@ -62,11 +62,15 @@ import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.InternalGeoHexGrid; import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.UnboundedGeoHashGridTiler; import org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid.UnboundedGeoTileGridTiler; +import org.elasticsearch.xpack.spatial.search.aggregations.metrics.CartesianBoundsAggregationBuilder; +import org.elasticsearch.xpack.spatial.search.aggregations.metrics.CartesianBoundsAggregator; import org.elasticsearch.xpack.spatial.search.aggregations.metrics.CartesianCentroidAggregationBuilder; import org.elasticsearch.xpack.spatial.search.aggregations.metrics.CartesianCentroidAggregator; +import org.elasticsearch.xpack.spatial.search.aggregations.metrics.CartesianShapeBoundsAggregator; import org.elasticsearch.xpack.spatial.search.aggregations.metrics.CartesianShapeCentroidAggregator; import org.elasticsearch.xpack.spatial.search.aggregations.metrics.GeoShapeBoundsAggregator; import org.elasticsearch.xpack.spatial.search.aggregations.metrics.GeoShapeCentroidAggregator; +import org.elasticsearch.xpack.spatial.search.aggregations.metrics.InternalCartesianBounds; import org.elasticsearch.xpack.spatial.search.aggregations.metrics.InternalCartesianCentroid; import org.elasticsearch.xpack.spatial.search.aggregations.support.CartesianPointValuesSourceType; import org.elasticsearch.xpack.spatial.search.aggregations.support.CartesianShapeValuesSourceType; @@ -172,7 +176,12 @@ public List getAggregations() { SpatialStatsAction.Item.CARTESIANCENTROID, checkLicense(CartesianCentroidAggregationBuilder.PARSER, GEO_CENTROID_AGG_FEATURE) ) - ).addResultReader(InternalCartesianCentroid::new).setAggregatorRegistrar(this::registerCartesianCentroidAggregator) + ).addResultReader(InternalCartesianCentroid::new).setAggregatorRegistrar(this::registerCartesianCentroidAggregator), + new AggregationSpec( + CartesianBoundsAggregationBuilder.NAME, + CartesianBoundsAggregationBuilder::new, + usage.track(SpatialStatsAction.Item.CARTESIANBOUNDS, CartesianBoundsAggregationBuilder.PARSER) + ).addResultReader(InternalCartesianBounds::new).setAggregatorRegistrar(SpatialPlugin::registerCartesianBoundsAggregators) ); } @@ -190,6 +199,21 @@ private static void registerGeoShapeBoundsAggregator(ValuesSourceRegistry.Builde ); } + private static void registerCartesianBoundsAggregators(ValuesSourceRegistry.Builder builder) { + builder.register( + CartesianBoundsAggregationBuilder.REGISTRY_KEY, + CartesianShapeValuesSourceType.instance(), + CartesianShapeBoundsAggregator::new, + true + ); + builder.register( + CartesianBoundsAggregationBuilder.REGISTRY_KEY, + CartesianPointValuesSourceType.instance(), + CartesianBoundsAggregator::new, + true + ); + } + private void registerGeoShapeCentroidAggregator(ValuesSourceRegistry.Builder builder) { builder.register( GeoCentroidAggregationBuilder.REGISTRY_KEY, diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/CartesianPoint.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/CartesianPoint.java index 19cf42af59d57..c1caa3cad9096 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/CartesianPoint.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/CartesianPoint.java @@ -77,8 +77,14 @@ public CartesianPoint resetFromEncoded(long encoded) { public CartesianPoint resetFromString(String value, final boolean ignoreZValue) { if (value.toLowerCase(Locale.ROOT).contains("point")) { return resetFromWKT(value, ignoreZValue); - } else { + } else if (value.contains(",")) { return resetFromCoordinates(value, ignoreZValue); + } else if (value.contains(".")) { + // This error mimics the structure of the parser error from 'resetFromCoordinates' below + throw new ElasticsearchParseException("failed to parse [{}], expected 2 or 3 coordinates but found: [{}]", value, 1); + } else { + // This error mimics the structure of the Geohash.mortonEncode() error to simplify testing + throw new ElasticsearchParseException("unsupported symbol [{}] in point [{}]", value.charAt(0), value); } } @@ -86,11 +92,7 @@ public CartesianPoint resetFromString(String value, final boolean ignoreZValue) public CartesianPoint resetFromCoordinates(String value, final boolean ignoreZValue) { String[] vals = value.split(","); if (vals.length > 3 || vals.length < 2) { - throw new ElasticsearchParseException( - "failed to parse [{}], expected 2 or 3 coordinates " + "but found: [{}]", - vals, - vals.length - ); + throw new ElasticsearchParseException("failed to parse [{}], expected 2 or 3 coordinates but found: [{}]", vals, vals.length); } final double x; final double y; @@ -98,7 +100,7 @@ public CartesianPoint resetFromCoordinates(String value, final boolean ignoreZVa x = Double.parseDouble(vals[0].trim()); if (Double.isFinite(x) == false) { throw new ElasticsearchParseException( - "invalid [{}] value [{}]; " + "must be between -3.4028234663852886E38 and 3.4028234663852886E38", + "invalid [{}] value [{}]; must be between -3.4028234663852886E38 and 3.4028234663852886E38", X_FIELD, x ); @@ -110,7 +112,7 @@ public CartesianPoint resetFromCoordinates(String value, final boolean ignoreZVa y = Double.parseDouble(vals[1].trim()); if (Double.isFinite(y) == false) { throw new ElasticsearchParseException( - "invalid [{}] value [{}]; " + "must be between -3.4028234663852886E38 and 3.4028234663852886E38", + "invalid [{}] value [{}]; must be between -3.4028234663852886E38 and 3.4028234663852886E38", Y_FIELD, y ); @@ -137,7 +139,7 @@ private CartesianPoint resetFromWKT(String value, boolean ignoreZValue) { } if (geometry.type() != ShapeType.POINT) { throw new ElasticsearchParseException( - "[{}] supports only POINT among WKT primitives, " + "but found {}", + "[{}] supports only POINT among WKT primitives, but found {}", PointFieldMapper.CONTENT_TYPE, geometry.type() ); @@ -228,14 +230,14 @@ public static CartesianPoint parsePoint(Object value, boolean ignoreZValue) thro public static void assertZValue(final boolean ignoreZValue, double zValue) { if (ignoreZValue == false) { throw new ElasticsearchParseException( - "Exception parsing coordinates: found Z value [{}] but [ignore_z_value] " + "parameter is [{}]", + "Exception parsing coordinates: found Z value [{}] but [ignore_z_value] parameter is [{}]", zValue, ignoreZValue ); } if (Double.isFinite(zValue) == false) { throw new ElasticsearchParseException( - "invalid [{}] value [{}]; " + "must be between -3.4028234663852886E38 and 3.4028234663852886E38", + "invalid [{}] value [{}]; must be between -3.4028234663852886E38 and 3.4028234663852886E38", Z_FIELD, zValue ); diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/CartesianShapeValues.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/CartesianShapeValues.java index 975a79e86c414..2751dca2ef891 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/CartesianShapeValues.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/CartesianShapeValues.java @@ -10,6 +10,8 @@ import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.XYGeometry; import org.apache.lucene.geo.XYPoint; +import org.elasticsearch.geometry.utils.GeometryValidator; +import org.elasticsearch.geometry.utils.StandardValidator; import org.elasticsearch.search.aggregations.support.ValuesSourceType; import org.elasticsearch.xpack.spatial.common.CartesianPoint; import org.elasticsearch.xpack.spatial.index.mapper.CartesianShapeIndexer; @@ -48,6 +50,13 @@ protected CartesianShapeValues() { super(CoordinateEncoder.CARTESIAN, CartesianShapeValues.CartesianShapeValue::new, new CartesianShapeIndexer("missing")); } + /** + * Cartesian data is not limited to geographic lat/lon degrees, so we use the standard validator + */ + public GeometryValidator geometryValidator() { + return StandardValidator.instance(true); + } + /** * thin wrapper around a {@link GeometryDocValueReader} which encodes / decodes values using the cartesian decoder */ 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 02a62905bc9b5..fe0ec12e91577 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 @@ -12,6 +12,8 @@ import org.apache.lucene.geo.Point; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.Orientation; +import org.elasticsearch.geometry.utils.GeographyValidator; +import org.elasticsearch.geometry.utils.GeometryValidator; import org.elasticsearch.index.mapper.GeoShapeIndexer; import org.elasticsearch.search.aggregations.support.ValuesSourceType; import org.elasticsearch.xpack.spatial.search.aggregations.support.GeoShapeValuesSourceType; @@ -49,6 +51,13 @@ protected GeoShapeValues() { super(CoordinateEncoder.GEO, GeoShapeValues.GeoShapeValue::new, new GeoShapeIndexer(Orientation.CCW, "missing")); } + /** + * Geo data is limited to geographic lat/lon degrees, so we use the GeographyValidator + */ + public GeometryValidator geometryValidator() { + return GeographyValidator.instance(true); + } + /** * thin wrapper around a {@link GeometryDocValueReader} which encodes / decodes values using the Geo decoder */ diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/ShapeValues.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/ShapeValues.java index cd367019b699e..93cac2971b479 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/ShapeValues.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/ShapeValues.java @@ -12,7 +12,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.geo.SpatialPoint; import org.elasticsearch.geometry.Geometry; -import org.elasticsearch.geometry.utils.GeographyValidator; +import org.elasticsearch.geometry.utils.GeometryValidator; import org.elasticsearch.geometry.utils.WellKnownText; import org.elasticsearch.index.mapper.ShapeIndexer; import org.elasticsearch.search.aggregations.support.ValuesSourceType; @@ -71,9 +71,11 @@ protected ShapeValues(CoordinateEncoder encoder, Supplier supplier, ShapeInde */ public abstract T value() throws IOException; + public abstract GeometryValidator geometryValidator(); + public T missing(String missing) { try { - final Geometry geometry = WellKnownText.fromWKT(GeographyValidator.instance(true), true, missing); + final Geometry geometry = WellKnownText.fromWKT(geometryValidator(), true, missing); final BinaryShapeDocValuesField field = new BinaryShapeDocValuesField("missing", encoder); field.add(missingShapeIndexer.indexShape(geometry), geometry); final T value = supplier.get(); diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/CartesianShapeDVAtomicShapeFieldData.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/CartesianShapeDVAtomicShapeFieldData.java index b51b13430dce8..34e6c569a4e68 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/CartesianShapeDVAtomicShapeFieldData.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/CartesianShapeDVAtomicShapeFieldData.java @@ -46,7 +46,7 @@ public void close() { public CartesianShapeValues getShapeValues() { try { final BinaryDocValues binaryValues = DocValues.getBinary(reader, fieldName); - final CartesianShapeValues.CartesianShapeValue geoShapeValue = new CartesianShapeValues.CartesianShapeValue(); + final CartesianShapeValues.CartesianShapeValue shapeValue = new CartesianShapeValues.CartesianShapeValue(); return new CartesianShapeValues() { @Override @@ -61,8 +61,8 @@ public ValuesSourceType valuesSourceType() { @Override public CartesianShapeValue value() throws IOException { - geoShapeValue.reset(binaryValues.binaryValue()); - return geoShapeValue; + shapeValue.reset(binaryValues.binaryValue()); + return shapeValue; } }; } catch (IOException e) { diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBounds.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBounds.java new file mode 100644 index 0000000000000..36bf0a572b8b9 --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBounds.java @@ -0,0 +1,16 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.search.aggregations.metrics.SpatialBounds; +import org.elasticsearch.xpack.spatial.common.CartesianPoint; + +/** + * An aggregation that computes a bounding box in which all documents of the current bucket are. + */ +public interface CartesianBounds extends SpatialBounds {} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregationBuilder.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregationBuilder.java new file mode 100644 index 0000000000000..4e8e8083c072e --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregationBuilder.java @@ -0,0 +1,110 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.Version; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.AggregatorFactory; +import org.elasticsearch.search.aggregations.support.AggregationContext; +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; +import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; +import org.elasticsearch.search.aggregations.support.ValuesSourceRegistry; +import org.elasticsearch.search.aggregations.support.ValuesSourceType; +import org.elasticsearch.xcontent.ObjectParser; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xpack.spatial.search.aggregations.support.CartesianPointValuesSourceType; + +import java.io.IOException; +import java.util.Map; + +public class CartesianBoundsAggregationBuilder extends ValuesSourceAggregationBuilder.LeafOnly { + public static final String NAME = "cartesian_bounds"; + public static final ValuesSourceRegistry.RegistryKey REGISTRY_KEY = + new ValuesSourceRegistry.RegistryKey<>(NAME, CartesianBoundsAggregatorSupplier.class); + + public static final ObjectParser PARSER = ObjectParser.fromBuilder( + NAME, + CartesianBoundsAggregationBuilder::new + ); + static { + ValuesSourceAggregationBuilder.declareFields(PARSER, false, false, false); + } + + public CartesianBoundsAggregationBuilder(String name) { + super(name); + } + + protected CartesianBoundsAggregationBuilder( + CartesianBoundsAggregationBuilder clone, + AggregatorFactories.Builder factoriesBuilder, + Map metadata + ) { + super(clone, factoriesBuilder, metadata); + } + + @Override + protected ValuesSourceType defaultValueSourceType() { + return CartesianPointValuesSourceType.instance(); + } + + @Override + protected AggregationBuilder shallowCopy(AggregatorFactories.Builder factoriesBuilder, Map metadata) { + return new CartesianBoundsAggregationBuilder(this, factoriesBuilder, metadata); + } + + /** + * Read from a stream. + */ + public CartesianBoundsAggregationBuilder(StreamInput in) throws IOException { + super(in); + } + + @Override + public boolean supportsSampling() { + return true; + } + + @Override + protected void innerWriteTo(StreamOutput out) { + // Do nothing, no extra state to write to stream + } + + @Override + protected CartesianBoundsAggregatorFactory innerBuild( + AggregationContext context, + ValuesSourceConfig config, + AggregatorFactory parent, + AggregatorFactories.Builder subFactoriesBuilder + ) throws IOException { + CartesianBoundsAggregatorSupplier aggregatorSupplier = context.getValuesSourceRegistry().getAggregator(REGISTRY_KEY, config); + return new CartesianBoundsAggregatorFactory(name, config, context, parent, subFactoriesBuilder, metadata, aggregatorSupplier); + } + + @Override + public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { + return builder; + } + + @Override + public String getType() { + return NAME; + } + + @Override + protected ValuesSourceRegistry.RegistryKey getRegistryKey() { + return REGISTRY_KEY; + } + + @Override + public Version getMinimalSupportedVersion() { + return Version.V_8_6_0; + } +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregator.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregator.java new file mode 100644 index 0000000000000..bf574a9a3afdc --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregator.java @@ -0,0 +1,59 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.search.aggregations.AggregationExecutionContext; +import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.LeafBucketCollector; +import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; +import org.elasticsearch.search.aggregations.support.AggregationContext; +import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; +import org.elasticsearch.xpack.spatial.common.CartesianPoint; +import org.elasticsearch.xpack.spatial.search.aggregations.support.CartesianPointValuesSource; + +import java.io.IOException; +import java.util.Map; + +/** + * A metric aggregator that computes a cartesian-bounds from a {@code point} type field + */ +public final class CartesianBoundsAggregator extends CartesianBoundsAggregatorBase { + private final CartesianPointValuesSource valuesSource; + + public CartesianBoundsAggregator( + String name, + AggregationContext context, + Aggregator parent, + ValuesSourceConfig valuesSourceConfig, + Map metadata + ) throws IOException { + super(name, context, parent, valuesSourceConfig.hasValues() == false, metadata); + this.valuesSource = isNoOp() ? null : (CartesianPointValuesSource) valuesSourceConfig.getValuesSource(); + } + + @Override + public LeafBucketCollector getLeafCollector(AggregationExecutionContext aggCtx, LeafBucketCollector sub) { + if (isNoOp()) { + return LeafBucketCollector.NO_OP_COLLECTOR; + } + final CartesianPointValuesSource.MultiCartesianPointValues values = valuesSource.pointValues(aggCtx.getLeafReaderContext()); + return new LeafBucketCollectorBase(sub, values) { + @Override + public void collect(int doc, long bucket) throws IOException { + if (values.advanceExact(doc)) { + maybeResize(bucket); + final int valuesCount = values.docValueCount(); + for (int i = 0; i < valuesCount; ++i) { + CartesianPoint value = values.nextValue(); + addBounds(bucket, value.getY(), value.getY(), value.getX(), value.getX()); + } + } + } + }; + } +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregatorBase.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregatorBase.java new file mode 100644 index 0000000000000..18c6707d1c065 --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregatorBase.java @@ -0,0 +1,107 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.common.util.DoubleArray; +import org.elasticsearch.core.Releasables; +import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.metrics.MetricsAggregator; +import org.elasticsearch.search.aggregations.support.AggregationContext; + +import java.io.IOException; +import java.util.Map; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +/** + * A metric aggregator that computes a cartesian-bounds from a {@code point} type field + */ +public abstract class CartesianBoundsAggregatorBase extends MetricsAggregator { + private final boolean isNoOp; + private DoubleArray tops; + private DoubleArray bottoms; + private DoubleArray lefts; + private DoubleArray rights; + + public CartesianBoundsAggregatorBase( + String name, + AggregationContext context, + Aggregator parent, + boolean isNoOp, + Map metadata + ) throws IOException { + super(name, context, parent, metadata); + this.isNoOp = isNoOp; + if (isNoOp == false) { + tops = bigArrays().newDoubleArray(1, false); + tops.fill(0, tops.size(), Double.NEGATIVE_INFINITY); + bottoms = bigArrays().newDoubleArray(1, false); + bottoms.fill(0, bottoms.size(), Double.POSITIVE_INFINITY); + lefts = bigArrays().newDoubleArray(1, false); + lefts.fill(0, lefts.size(), Double.POSITIVE_INFINITY); + rights = bigArrays().newDoubleArray(1, false); + rights.fill(0, rights.size(), Double.NEGATIVE_INFINITY); + } + } + + protected boolean isNoOp() { + return isNoOp; + } + + protected void addBounds(long bucket, double top, double bottom, double left, double right) { + tops.set(bucket, max(tops.get(bucket), top)); + bottoms.set(bucket, min(bottoms.get(bucket), bottom)); + lefts.set(bucket, min(lefts.get(bucket), left)); + rights.set(bucket, max(rights.get(bucket), right)); + } + + protected void maybeResize(long bucket) { + if (bucket >= tops.size()) { + final long from = tops.size(); + tops = bigArrays().grow(tops, bucket + 1); + tops.fill(from, tops.size(), Double.NEGATIVE_INFINITY); + bottoms = bigArrays().resize(bottoms, tops.size()); + bottoms.fill(from, bottoms.size(), Double.POSITIVE_INFINITY); + lefts = bigArrays().resize(lefts, tops.size()); + lefts.fill(from, lefts.size(), Double.POSITIVE_INFINITY); + rights = bigArrays().resize(rights, tops.size()); + rights.fill(from, rights.size(), Double.NEGATIVE_INFINITY); + } + } + + @Override + public InternalAggregation buildAggregation(long owningBucketOrdinal) { + if (isNoOp) { + return buildEmptyAggregation(); + } + double top = tops.get(owningBucketOrdinal); + double bottom = bottoms.get(owningBucketOrdinal); + double left = lefts.get(owningBucketOrdinal); + double right = rights.get(owningBucketOrdinal); + return new InternalCartesianBounds(name, top, bottom, left, right, metadata()); + } + + @Override + public InternalAggregation buildEmptyAggregation() { + return new InternalCartesianBounds( + name, + Double.NEGATIVE_INFINITY, + Double.POSITIVE_INFINITY, + Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY, + metadata() + ); + } + + @Override + public void doClose() { + Releasables.close(tops, bottoms, lefts, rights); + } +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregatorFactory.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregatorFactory.java new file mode 100644 index 0000000000000..f6ea4c1e898c4 --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregatorFactory.java @@ -0,0 +1,48 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.AggregatorFactory; +import org.elasticsearch.search.aggregations.CardinalityUpperBound; +import org.elasticsearch.search.aggregations.support.AggregationContext; +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; +import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; + +import java.io.IOException; +import java.util.Map; + +class CartesianBoundsAggregatorFactory extends ValuesSourceAggregatorFactory { + + private final CartesianBoundsAggregatorSupplier aggregatorSupplier; + + CartesianBoundsAggregatorFactory( + String name, + ValuesSourceConfig config, + AggregationContext context, + AggregatorFactory parent, + AggregatorFactories.Builder subFactoriesBuilder, + Map metadata, + CartesianBoundsAggregatorSupplier aggregatorSupplier + ) throws IOException { + super(name, config, context, parent, subFactoriesBuilder, metadata); + this.aggregatorSupplier = aggregatorSupplier; + } + + @Override + protected Aggregator createUnmapped(Aggregator parent, Map metadata) throws IOException { + return new CartesianBoundsAggregator(name, context, parent, config, metadata); + } + + @Override + protected Aggregator doCreateInternal(Aggregator parent, CardinalityUpperBound cardinality, Map metadata) + throws IOException { + return aggregatorSupplier.build(name, context, parent, config, metadata); + } +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregatorSupplier.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregatorSupplier.java new file mode 100644 index 0000000000000..a08946eda85db --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregatorSupplier.java @@ -0,0 +1,27 @@ +/* + * 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. + */ +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.metrics.MetricsAggregator; +import org.elasticsearch.search.aggregations.support.AggregationContext; +import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; + +import java.io.IOException; +import java.util.Map; + +@FunctionalInterface +public interface CartesianBoundsAggregatorSupplier { + + MetricsAggregator build( + String name, + AggregationContext context, + Aggregator parent, + ValuesSourceConfig valuesSourceConfig, + Map metadata + ) throws IOException; +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidAggregationBuilder.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidAggregationBuilder.java index 1cdab99a360fd..8435c8539f4f8 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidAggregationBuilder.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidAggregationBuilder.java @@ -112,6 +112,6 @@ protected ValuesSourceRegistry.RegistryKey getRegistryKey() { @Override public Version getMinimalSupportedVersion() { - return Version.V_EMPTY; + return Version.V_8_6_0; } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianShapeBoundsAggregator.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianShapeBoundsAggregator.java new file mode 100644 index 0000000000000..43d53a9f30c50 --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianShapeBoundsAggregator.java @@ -0,0 +1,58 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.search.aggregations.AggregationExecutionContext; +import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.LeafBucketCollector; +import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; +import org.elasticsearch.search.aggregations.support.AggregationContext; +import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; +import org.elasticsearch.xpack.spatial.index.fielddata.CartesianShapeValues; +import org.elasticsearch.xpack.spatial.index.fielddata.ShapeValues; +import org.elasticsearch.xpack.spatial.search.aggregations.support.CartesianShapeValuesSource; + +import java.io.IOException; +import java.util.Map; + +/** + * A metric aggregator that computes a cartesian-bounds from a {@code shape} type field + */ +public final class CartesianShapeBoundsAggregator extends CartesianBoundsAggregatorBase { + private final CartesianShapeValuesSource valuesSource; + + public CartesianShapeBoundsAggregator( + String name, + AggregationContext context, + Aggregator parent, + ValuesSourceConfig valuesSourceConfig, + Map metadata + ) throws IOException { + super(name, context, parent, valuesSourceConfig.hasValues() == false, metadata); + this.valuesSource = isNoOp() ? null : (CartesianShapeValuesSource) valuesSourceConfig.getValuesSource(); + } + + @Override + public LeafBucketCollector getLeafCollector(AggregationExecutionContext aggCtx, LeafBucketCollector sub) { + if (isNoOp()) { + return LeafBucketCollector.NO_OP_COLLECTOR; + } + CartesianShapeValues values = valuesSource.shapeValues(aggCtx.getLeafReaderContext()); + return new LeafBucketCollectorBase(sub, values) { + @Override + public void collect(int doc, long bucket) throws IOException { + if (values.advanceExact(doc)) { + maybeResize(bucket); + CartesianShapeValues.CartesianShapeValue value = values.value(); + ShapeValues.BoundingBox bounds = value.boundingBox(); + addBounds(bucket, bounds.top, bounds.bottom, bounds.minX(), bounds.maxX()); + } + } + }; + } +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/InternalCartesianBounds.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/InternalCartesianBounds.java new file mode 100644 index 0000000000000..8569565de99cc --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/InternalCartesianBounds.java @@ -0,0 +1,107 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.common.geo.BoundingBox; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.search.aggregations.AggregationReduceContext; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.metrics.InternalBounds; +import org.elasticsearch.xpack.spatial.common.CartesianBoundingBox; +import org.elasticsearch.xpack.spatial.common.CartesianPoint; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +public class InternalCartesianBounds extends InternalBounds implements CartesianBounds { + public final double left; + public final double right; + + public InternalCartesianBounds(String name, double top, double bottom, double left, double right, Map metadata) { + super(name, top, bottom, metadata); + this.left = left; + this.right = right; + } + + /** + * Read from a stream. + */ + public InternalCartesianBounds(StreamInput in) throws IOException { + super(in); + left = in.readDouble(); + right = in.readDouble(); + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + super.doWriteTo(out); + out.writeDouble(left); + out.writeDouble(right); + } + + @Override + public String getWriteableName() { + return CartesianBoundsAggregationBuilder.NAME; + } + + @Override + public InternalAggregation reduce(List aggregations, AggregationReduceContext reduceContext) { + double top = Double.NEGATIVE_INFINITY; + double bottom = Double.POSITIVE_INFINITY; + double left = Double.POSITIVE_INFINITY; + double right = Double.NEGATIVE_INFINITY; + + for (InternalAggregation aggregation : aggregations) { + InternalCartesianBounds bounds = (InternalCartesianBounds) aggregation; + top = max(top, bounds.top); + bottom = min(bottom, bounds.bottom); + left = min(left, bounds.left); + right = max(right, bounds.right); + } + return new InternalCartesianBounds(name, top, bottom, left, right, getMetadata()); + } + + @Override + protected Object selectCoordinate(String coordinateString, CartesianPoint cornerPoint) { + return switch (coordinateString) { + case "x" -> cornerPoint.getX(); + case "y" -> cornerPoint.getY(); + default -> throw new IllegalArgumentException("Found unknown path element [" + coordinateString + "] in [" + getName() + "]"); + }; + } + + @Override + protected BoundingBox resolveBoundingBox() { + if (Double.isInfinite(top)) { + return null; + } else { + return new CartesianBoundingBox(new CartesianPoint(left, top), new CartesianPoint(right, bottom)); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + if (super.equals(obj) == false) return false; + + InternalCartesianBounds other = (InternalCartesianBounds) obj; + return top == other.top && bottom == other.bottom && left == other.left && right == other.right; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), bottom, left, right); + } +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/ParsedCartesianBounds.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/ParsedCartesianBounds.java new file mode 100644 index 0000000000000..35537d5da95c4 --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/ParsedCartesianBounds.java @@ -0,0 +1,98 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.search.aggregations.ParsedAggregation; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ObjectParser; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xpack.spatial.common.CartesianBoundingBox; +import org.elasticsearch.xpack.spatial.common.CartesianPoint; + +import java.io.IOException; + +import static org.elasticsearch.common.geo.GeoBoundingBox.BOTTOM_RIGHT_FIELD; +import static org.elasticsearch.common.geo.GeoBoundingBox.BOUNDS_FIELD; +import static org.elasticsearch.common.geo.GeoBoundingBox.TOP_LEFT_FIELD; +import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.xpack.spatial.common.CartesianBoundingBox.X_FIELD; +import static org.elasticsearch.xpack.spatial.common.CartesianBoundingBox.Y_FIELD; + +public class ParsedCartesianBounds extends ParsedAggregation implements CartesianBounds { + + // A top of Double.NEGATIVE_INFINITY yields an empty xContent, so the bounding box is null + @Nullable + private CartesianBoundingBox boundingBox; + + @Override + public String getType() { + return CartesianBoundsAggregationBuilder.NAME; + } + + @Override + public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { + if (boundingBox != null) { + builder.startObject(CartesianBoundingBox.BOUNDS_FIELD.getPreferredName()); + boundingBox.toXContentFragment(builder); + builder.endObject(); + } + return builder; + } + + @Override + @Nullable + public CartesianPoint topLeft() { + return boundingBox != null ? boundingBox.topLeft() : null; + } + + @Override + @Nullable + public CartesianPoint bottomRight() { + return boundingBox != null ? boundingBox.bottomRight() : null; + } + + private static final ObjectParser PARSER = new ObjectParser<>( + ParsedCartesianBounds.class.getSimpleName(), + true, + ParsedCartesianBounds::new + ); + + private static final ConstructingObjectParser, Void> BOUNDS_PARSER = + new ConstructingObjectParser<>( + ParsedCartesianBounds.class.getSimpleName() + "_BOUNDS", + true, + args -> new Tuple<>((CartesianPoint) args[0], (CartesianPoint) args[1]) + ); + + private static final ObjectParser POINT_PARSER = new ObjectParser<>( + ParsedCartesianBounds.class.getSimpleName() + "_POINT", + true, + CartesianPoint::new + ); + + static { + declareAggregationFields(PARSER); + PARSER.declareObject((agg, bbox) -> agg.boundingBox = new CartesianBoundingBox(bbox.v1(), bbox.v2()), BOUNDS_PARSER, BOUNDS_FIELD); + + BOUNDS_PARSER.declareObject(constructorArg(), POINT_PARSER, TOP_LEFT_FIELD); + BOUNDS_PARSER.declareObject(constructorArg(), POINT_PARSER, BOTTOM_RIGHT_FIELD); + + POINT_PARSER.declareDouble(CartesianPoint::resetY, Y_FIELD); + POINT_PARSER.declareDouble(CartesianPoint::resetX, X_FIELD); + } + + public static ParsedCartesianBounds fromXContent(XContentParser parser, final String name) { + ParsedCartesianBounds geoBounds = PARSER.apply(parser, null); + geoBounds.setName(name); + return geoBounds; + } + +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/support/CartesianPointValuesSourceType.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/support/CartesianPointValuesSourceType.java index 18cf0f5100867..e5f17e4b3d0da 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/support/CartesianPointValuesSourceType.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/support/CartesianPointValuesSourceType.java @@ -71,7 +71,7 @@ public ValuesSource replaceMissing( public SortedNumericDocValues sortedNumericDocValues(LeafReaderContext context) { final long xi = XYEncodingUtils.encode((float) missing.getX()); final long yi = XYEncodingUtils.encode((float) missing.getY()); - long encoded = yi | xi << 32; + long encoded = (yi & 0xFFFFFFFFL) | xi << 32; return MissingValues.replaceMissing(pointValuesSource.sortedNumericDocValues(context), encoded); } diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregatorTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregatorTests.java new file mode 100644 index 0000000000000..e5465c91f03bb --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsAggregatorTests.java @@ -0,0 +1,199 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.NumericDocValuesField; +import org.apache.lucene.document.XYDocValuesField; +import org.apache.lucene.geo.XYEncodingUtils; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.store.Directory; +import org.apache.lucene.tests.index.RandomIndexWriter; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.plugins.SearchPlugin; +import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.AggregatorTestCase; +import org.elasticsearch.search.aggregations.support.AggregationInspectionHelper; +import org.elasticsearch.search.aggregations.support.ValuesSourceType; +import org.elasticsearch.xpack.spatial.LocalStateSpatialPlugin; +import org.elasticsearch.xpack.spatial.common.CartesianPoint; +import org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper; +import org.elasticsearch.xpack.spatial.search.aggregations.support.CartesianPointValuesSourceType; +import org.elasticsearch.xpack.spatial.util.ShapeTestUtils; + +import java.io.IOException; +import java.util.List; + +import static java.lang.Math.max; +import static java.lang.Math.min; +import static org.elasticsearch.xpack.spatial.search.aggregations.metrics.InternalCartesianBoundsTests.GEOHASH_TOLERANCE; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.startsWith; + +public class CartesianBoundsAggregatorTests extends AggregatorTestCase { + + @Override + protected List getSearchPlugins() { + return List.of(new LocalStateSpatialPlugin()); + } + + public void testEmpty() throws Exception { + try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) { + CartesianBoundsAggregationBuilder aggBuilder = new CartesianBoundsAggregationBuilder("my_agg").field("field"); + + MappedFieldType fieldType = new PointFieldMapper.PointFieldType("field"); + try (IndexReader reader = w.getReader()) { + IndexSearcher searcher = new IndexSearcher(reader); + InternalCartesianBounds bounds = searchAndReduce(searcher, new AggTestConfig(aggBuilder, fieldType)); + assertTrue(Double.isInfinite(bounds.top)); + assertTrue(Double.isInfinite(bounds.bottom)); + assertTrue(Double.isInfinite(bounds.left)); + assertTrue(Double.isInfinite(bounds.right)); + assertFalse(AggregationInspectionHelper.hasValue(bounds)); + } + } + } + + public void testUnmappedFieldWithDocs() throws Exception { + try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) { + if (randomBoolean()) { + Document doc = new Document(); + doc.add(new XYDocValuesField("field", 0.0f, 0.0f)); + w.addDocument(doc); + } + + CartesianBoundsAggregationBuilder aggBuilder = new CartesianBoundsAggregationBuilder("my_agg").field("non_existent"); + + MappedFieldType fieldType = new PointFieldMapper.PointFieldType("field"); + try (IndexReader reader = w.getReader()) { + IndexSearcher searcher = new IndexSearcher(reader); + InternalCartesianBounds bounds = searchAndReduce(searcher, new AggTestConfig(aggBuilder, fieldType)); + assertTrue(Double.isInfinite(bounds.top)); + assertTrue(Double.isInfinite(bounds.bottom)); + assertTrue(Double.isInfinite(bounds.left)); + assertTrue(Double.isInfinite(bounds.right)); + assertFalse(AggregationInspectionHelper.hasValue(bounds)); + } + } + } + + public void testMissing() throws Exception { + try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) { + Document doc = new Document(); + doc.add(new NumericDocValuesField("not_field", 1000L)); + w.addDocument(doc); + + Point point = ShapeTestUtils.randomPointNotExtreme(false); + double x = XYEncodingUtils.decode(XYEncodingUtils.encode((float) point.getX())); + double y = XYEncodingUtils.decode(XYEncodingUtils.encode((float) point.getY())); + + // valid missing values + for (Object missingVal : List.of("POINT(" + x + " " + y + ")", x + ", " + y, new CartesianPoint(x, y))) { + readAndAssertMissing(w, missingVal, (float) x, (float) y); + } + } + } + + private void readAndAssertMissing(RandomIndexWriter w, Object missingVal, float x, float y) throws IOException { + CartesianBoundsAggregationBuilder aggBuilder = new CartesianBoundsAggregationBuilder("my_agg").field("field").missing(missingVal); + MappedFieldType fieldType = new PointFieldMapper.PointFieldType("field"); + + String description = "Bounds aggregation with missing=" + missingVal; + try (IndexReader reader = w.getReader()) { + IndexSearcher searcher = new IndexSearcher(reader); + InternalCartesianBounds bounds = searchAndReduce(searcher, new AggTestConfig(aggBuilder, fieldType)); + assertThat(description + ": top", bounds.top, closeTo(y, GEOHASH_TOLERANCE)); + assertThat(description + ": bottom", bounds.bottom, closeTo(y, GEOHASH_TOLERANCE)); + assertThat(description + ": left", bounds.left, closeTo(x, GEOHASH_TOLERANCE)); + assertThat(description + ": right", bounds.right, closeTo(x, GEOHASH_TOLERANCE)); + } + } + + public void testInvalidMissing() throws Exception { + try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) { + Document doc = new Document(); + doc.add(new NumericDocValuesField("not_field", 1000L)); + w.addDocument(doc); + + MappedFieldType fieldType = new PointFieldMapper.PointFieldType("field"); + + CartesianBoundsAggregationBuilder aggBuilder = new CartesianBoundsAggregationBuilder("my_agg").field("field") + .missing("invalid"); + try (IndexReader reader = w.getReader()) { + IndexSearcher searcher = new IndexSearcher(reader); + ElasticsearchParseException exception = expectThrows( + ElasticsearchParseException.class, + () -> searchAndReduce(searcher, new AggTestConfig(aggBuilder, fieldType)) + ); + assertThat(exception.getMessage(), startsWith("unsupported symbol")); + } + } + } + + public void testRandom() throws Exception { + double top = Double.NEGATIVE_INFINITY; + double bottom = Double.POSITIVE_INFINITY; + double left = Double.POSITIVE_INFINITY; + double right = Double.NEGATIVE_INFINITY; + int numDocs = randomIntBetween(50, 100); + try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) { + for (int i = 0; i < numDocs; i++) { + Document doc = new Document(); + int numValues = randomIntBetween(1, 5); + for (int j = 0; j < numValues; j++) { + Point point = ShapeTestUtils.randomPointNotExtreme(false); + doc.add(new XYDocValuesField("field", (float) point.getX(), (float) point.getY())); + + // To determine expected values we should imitate the internal encoding behaviour + double x = XYEncodingUtils.decode(XYEncodingUtils.encode((float) point.getX())); + double y = XYEncodingUtils.decode(XYEncodingUtils.encode((float) point.getY())); + top = max(top, y); + bottom = min(bottom, y); + left = min(left, x); + right = max(right, x); + } + w.addDocument(doc); + } + CartesianBoundsAggregationBuilder aggBuilder = new CartesianBoundsAggregationBuilder("my_agg").field("field"); + + MappedFieldType fieldType = new PointFieldMapper.PointFieldType("field"); + try (IndexReader reader = w.getReader()) { + IndexSearcher searcher = new IndexSearcher(reader); + InternalCartesianBounds bounds = searchAndReduce(searcher, new AggTestConfig(aggBuilder, fieldType)); + assertCloseTo("top", numDocs, bounds.top, top); + assertCloseTo("bottom", numDocs, bounds.bottom, bottom); + assertCloseTo("left", numDocs, bounds.left, left); + assertCloseTo("right", numDocs, bounds.right, right); + assertTrue(AggregationInspectionHelper.hasValue(bounds)); + } + } + } + + private void assertCloseTo(String name, long count, double value, double expected) { + assertEquals("Bounds over " + count + " points had incorrect " + name, expected, value, tolerance(value, expected, count)); + } + + private double tolerance(double value, double expected, long count) { + double tolerance = max(Math.abs(expected / 1e5), Math.abs(value / 1e5)); + // Very large numbers have more floating point error, also increasing with count + return tolerance > 1e25 ? tolerance * count : tolerance; + } + + @Override + protected AggregationBuilder createAggBuilderForTypeTest(MappedFieldType fieldType, String fieldName) { + return new CartesianBoundsAggregationBuilder("foo").field(fieldName); + } + + @Override + protected List getSupportedValuesSourceTypes() { + return List.of(CartesianPointValuesSourceType.instance()); + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsTests.java new file mode 100644 index 0000000000000..89e186fafd994 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianBoundsTests.java @@ -0,0 +1,83 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.search.aggregations.AggregationInitializationException; +import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.BaseAggregationTestCase; +import org.elasticsearch.xcontent.XContentParseException; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.spatial.LocalStateSpatialPlugin; + +import java.util.Collection; +import java.util.List; + +import static org.hamcrest.Matchers.containsString; + +public class CartesianBoundsTests extends BaseAggregationTestCase { + + @Override + protected Collection> getPlugins() { + return List.of(LocalStateSpatialPlugin.class); + } + + @Override + protected CartesianBoundsAggregationBuilder createTestAggregatorBuilder() { + CartesianBoundsAggregationBuilder factory = new CartesianBoundsAggregationBuilder(randomAlphaOfLengthBetween(1, 20)); + String field = randomAlphaOfLengthBetween(3, 20); + factory.field(field); + if (randomBoolean()) { + factory.missing("0,0"); + } + return factory; + } + + public void testFailWithSubAgg() throws Exception { + String source = """ + { + "viewport": { + "cartesian_bounds": { + "field": "location" + }, + "aggs": { + "names": { + "terms": { + "field": "name", + "size": 10 + } + } + } + } + } + """; + XContentParser parser = createParser(JsonXContent.jsonXContent, source); + assertSame(XContentParser.Token.START_OBJECT, parser.nextToken()); + Exception e = expectThrows(AggregationInitializationException.class, () -> AggregatorFactories.parseAggregators(parser)); + assertThat(e.toString(), containsString("Aggregator [viewport] of type [cartesian_bounds] cannot accept sub-aggregations")); + } + + public void testFailWithWrapLongitude() throws Exception { + String source = """ + { + "viewport": { + "cartesian_bounds": { + "field": "location", + "wrap_longitude": true + } + } + } + """; + XContentParser parser = createParser(JsonXContent.jsonXContent, source); + assertSame(XContentParser.Token.START_OBJECT, parser.nextToken()); + Exception e = expectThrows(XContentParseException.class, () -> AggregatorFactories.parseAggregators(parser)); + assertThat(e.toString(), containsString("[cartesian_bounds] unknown field [wrap_longitude]")); + } + +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidAggregatorTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidAggregatorTests.java index 6322511081b92..302efa9a4cea7 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidAggregatorTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidAggregatorTests.java @@ -22,7 +22,6 @@ import org.elasticsearch.search.aggregations.AggregatorTestCase; import org.elasticsearch.search.aggregations.support.AggregationInspectionHelper; import org.elasticsearch.search.aggregations.support.ValuesSourceType; -import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.spatial.LocalStateSpatialPlugin; import org.elasticsearch.xpack.spatial.common.CartesianPoint; import org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper; @@ -102,7 +101,7 @@ public void testSingleValuedField() throws Exception { CartesianPoint expectedCentroid = new CartesianPoint(0, 0); CartesianPoint[] singleValues = new CartesianPoint[numUniqueCartesianPoints]; for (int i = 0; i < singleValues.length; i++) { - Point point = ESTestCase.randomValueOtherThanMany(this::extremePoint, () -> ShapeTestUtils.randomPoint(false)); + Point point = ShapeTestUtils.randomPointNotExtreme(false); singleValues[i] = new CartesianPoint(point.getX(), point.getY()); } for (int i = 0; i < numDocs; i++) { @@ -127,7 +126,7 @@ public void testMultiValuedField() throws Exception { CartesianPoint expectedCentroid = new CartesianPoint(0, 0); CartesianPoint[] multiValues = new CartesianPoint[numUniqueCartesianPoints]; for (int i = 0; i < multiValues.length; i++) { - Point point = ESTestCase.randomValueOtherThanMany(this::extremePoint, () -> ShapeTestUtils.randomPoint(false)); + Point point = ShapeTestUtils.randomPointNotExtreme(false); multiValues[i] = new CartesianPoint(point.getX(), point.getY()); } final CartesianPoint[] multiVal = new CartesianPoint[2]; @@ -186,17 +185,6 @@ private double tolerance(double a, double b, long count) { } } - /** - * Since cartesian centroid is stored in Float values, and calculations perform averages over many, - * We cannot support points at the very edge of the range. - * TODO: Consider whether this should be implemented in ShapeTestUtils itself for more tests - */ - private boolean extremePoint(Point point) { - double max = Float.MAX_VALUE / 100; - double min = -Float.MAX_VALUE / 100; - return point.getLon() > max || point.getLon() < min || point.getLat() > max || point.getLat() < min; - } - @Override protected AggregationBuilder createAggBuilderForTypeTest(MappedFieldType fieldType, String fieldName) { return new CartesianCentroidAggregationBuilder("foo").field(fieldName); diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidTests.java index 171d3b16e48f7..944ad7666bc1e 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianCentroidTests.java @@ -11,14 +11,14 @@ import org.elasticsearch.search.aggregations.BaseAggregationTestCase; import org.elasticsearch.xpack.spatial.LocalStateSpatialPlugin; -import java.util.Arrays; import java.util.Collection; +import java.util.List; public class CartesianCentroidTests extends BaseAggregationTestCase { @Override protected Collection> getPlugins() { - return Arrays.asList(LocalStateSpatialPlugin.class); + return List.of(LocalStateSpatialPlugin.class); } @Override diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianShapeBoundsAggregatorTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianShapeBoundsAggregatorTests.java new file mode 100644 index 0000000000000..48554c1509887 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/CartesianShapeBoundsAggregatorTests.java @@ -0,0 +1,284 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.NumericDocValuesField; +import org.apache.lucene.document.XYDocValuesField; +import org.apache.lucene.geo.XYEncodingUtils; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.store.Directory; +import org.apache.lucene.tests.index.RandomIndexWriter; +import org.elasticsearch.common.Randomness; +import org.elasticsearch.common.geo.Orientation; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.MultiPoint; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.plugins.SearchPlugin; +import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.AggregatorTestCase; +import org.elasticsearch.search.aggregations.support.AggregationInspectionHelper; +import org.elasticsearch.search.aggregations.support.ValuesSourceType; +import org.elasticsearch.xpack.spatial.LocalStateSpatialPlugin; +import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper; +import org.elasticsearch.xpack.spatial.search.aggregations.support.CartesianPointValuesSourceType; +import org.elasticsearch.xpack.spatial.search.aggregations.support.CartesianShapeValuesSourceType; +import org.elasticsearch.xpack.spatial.util.GeoTestUtils; +import org.elasticsearch.xpack.spatial.util.ShapeTestUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static java.lang.Math.max; +import static java.lang.Math.min; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.startsWith; + +public class CartesianShapeBoundsAggregatorTests extends AggregatorTestCase { + static final double GEOHASH_TOLERANCE = 1E-5D; + + @Override + protected List getSearchPlugins() { + return List.of(new LocalStateSpatialPlugin()); + } + + @Override + protected AggregationBuilder createAggBuilderForTypeTest(MappedFieldType fieldType, String fieldName) { + return new CartesianBoundsAggregationBuilder("foo").field(fieldName); + } + + @Override + protected List getSupportedValuesSourceTypes() { + return List.of(CartesianPointValuesSourceType.instance(), CartesianShapeValuesSourceType.instance()); + } + + public void testEmpty() throws Exception { + try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) { + CartesianBoundsAggregationBuilder aggBuilder = new CartesianBoundsAggregationBuilder("my_agg").field("field"); + + MappedFieldType fieldType = new ShapeFieldMapper.ShapeFieldType( + "field", + true, + true, + Orientation.RIGHT, + null, + Collections.emptyMap() + ); + try (IndexReader reader = w.getReader()) { + IndexSearcher searcher = new IndexSearcher(reader); + InternalCartesianBounds bounds = searchAndReduce(searcher, new AggTestConfig(aggBuilder, fieldType)); + assertTrue(Double.isInfinite(bounds.top)); + assertTrue(Double.isInfinite(bounds.bottom)); + assertTrue(Double.isInfinite(bounds.left)); + assertTrue(Double.isInfinite(bounds.right)); + assertFalse(AggregationInspectionHelper.hasValue(bounds)); + } + } + } + + public void testUnmappedFieldWithDocs() throws Exception { + try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) { + if (randomBoolean()) { + Document doc = new Document(); + doc.add(new XYDocValuesField("field", 0.0f, 0.0f)); + w.addDocument(doc); + } + + CartesianBoundsAggregationBuilder aggBuilder = new CartesianBoundsAggregationBuilder("my_agg").field("non_existent"); + + MappedFieldType fieldType = new ShapeFieldMapper.ShapeFieldType( + "field", + true, + true, + Orientation.RIGHT, + null, + Collections.emptyMap() + ); + try (IndexReader reader = w.getReader()) { + IndexSearcher searcher = new IndexSearcher(reader); + InternalCartesianBounds bounds = searchAndReduce(searcher, new AggTestConfig(aggBuilder, fieldType)); + assertTrue(Double.isInfinite(bounds.top)); + assertTrue(Double.isInfinite(bounds.bottom)); + assertTrue(Double.isInfinite(bounds.left)); + assertTrue(Double.isInfinite(bounds.right)); + assertFalse(AggregationInspectionHelper.hasValue(bounds)); + } + } + } + + public void testMissing() throws Exception { + try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) { + Document doc = new Document(); + doc.add(new NumericDocValuesField("not_field", 1000L)); + w.addDocument(doc); + + MappedFieldType fieldType = new ShapeFieldMapper.ShapeFieldType( + "field", + true, + true, + Orientation.RIGHT, + null, + Collections.emptyMap() + ); + + Point point = ShapeTestUtils.randomPointNotExtreme(false); + double x = XYEncodingUtils.decode(XYEncodingUtils.encode((float) point.getX())); + double y = XYEncodingUtils.decode(XYEncodingUtils.encode((float) point.getY())); + Object missingVal = "POINT(" + x + " " + y + ")"; + + CartesianBoundsAggregationBuilder aggBuilder = new CartesianBoundsAggregationBuilder("my_agg").field("field") + .missing(missingVal); + + try (IndexReader reader = w.getReader()) { + IndexSearcher searcher = new IndexSearcher(reader); + InternalCartesianBounds bounds = searchAndReduce(searcher, new AggTestConfig(aggBuilder, fieldType)); + assertThat(bounds.top, equalTo(y)); + assertThat(bounds.bottom, equalTo(y)); + assertThat(bounds.left, equalTo(x)); + assertThat(bounds.right, equalTo(x)); + } + } + } + + public void testInvalidMissing() throws Exception { + try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) { + Document doc = new Document(); + doc.add(new NumericDocValuesField("not_field", 1000L)); + w.addDocument(doc); + + MappedFieldType fieldType = new ShapeFieldMapper.ShapeFieldType( + "field", + true, + true, + Orientation.RIGHT, + null, + Collections.emptyMap() + ); + + CartesianBoundsAggregationBuilder aggBuilder = new CartesianBoundsAggregationBuilder("my_agg").field("field") + .missing("invalid"); + try (IndexReader reader = w.getReader()) { + IndexSearcher searcher = new IndexSearcher(reader); + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> searchAndReduce(searcher, new AggTestConfig(aggBuilder, fieldType)) + ); + assertThat(exception.getMessage(), startsWith("Unknown geometry type")); + } + } + } + + public void testRandomShapes() throws Exception { + TestPointCollection expectedExtent = new TestPointCollection(); + try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) { + addRandomMultiPointDocs(w, expectedExtent); + readAndAssertExtent(w, expectedExtent); + } + } + + public void testSpecificMultiPointNegNeg() throws Exception { + double value = Randomness.get().nextDouble(0, 1000); + doTestSpecificMultiPoint(new Point(-value, -value)); + doTestSpecificMultiPoint(new Point(-value, -value), new Point(-value / 2, -value / 2)); + } + + public void testSpecificMultiPointNegPos() throws Exception { + double value = Randomness.get().nextDouble(0, 1000); + doTestSpecificMultiPoint(new Point(-value, value)); + doTestSpecificMultiPoint(new Point(-value, value), new Point(-value / 2, value / 2)); + } + + public void testSpecificMultiPointPosPos() throws Exception { + double value = Randomness.get().nextDouble(0, 1000); + doTestSpecificMultiPoint(new Point(value, value)); + doTestSpecificMultiPoint(new Point(value, value), new Point(value / 2, value / 2)); + } + + public void testSpecificMultiPointPosNeg() throws Exception { + double value = Randomness.get().nextDouble(0, 1000); + doTestSpecificMultiPoint(new Point(value, -value)); + doTestSpecificMultiPoint(new Point(value, -value), new Point(value / 2, -value / 2)); + } + + public void testSpecificMultiPoint() throws Exception { + double value = Randomness.get().nextDouble(100, 1000); + doTestSpecificMultiPoint(new Point(-value, value), new Point(-value, -value), new Point(value, value), new Point(value, -value)); + } + + private void doTestSpecificMultiPoint(Point... data) throws Exception { + TestPointCollection collection = new TestPointCollection(); + try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) { + Document doc = new Document(); + for (Point point : data) { + collection.add(point); + } + Geometry geometry = new MultiPoint(collection.points); + doc.add(GeoTestUtils.binaryCartesianShapeDocValuesField("field", geometry)); + w.addDocument(doc); + readAndAssertExtent(w, collection); + } + } + + private void addRandomMultiPointDocs(RandomIndexWriter w, TestPointCollection points) throws IOException { + int numDocs = randomIntBetween(50, 100); + for (int i = 0; i < numDocs; i++) { + Document doc = new Document(); + int numValues = randomIntBetween(1, 5); + for (int j = 0; j < numValues; j++) { + points.add(ShapeTestUtils.randomPointNotExtreme(false)); + } + Geometry geometry = new MultiPoint(points.points); + doc.add(GeoTestUtils.binaryCartesianShapeDocValuesField("field", geometry)); + w.addDocument(doc); + } + } + + private void readAndAssertExtent(RandomIndexWriter w, TestPointCollection points) throws IOException { + String description = "Bounds over " + points.points.size() + " points"; + CartesianBoundsAggregationBuilder aggBuilder = new CartesianBoundsAggregationBuilder("my_agg").field("field"); + + MappedFieldType fieldType = new ShapeFieldMapper.ShapeFieldType( + "field", + true, + true, + Orientation.RIGHT, + null, + Collections.emptyMap() + ); + try (IndexReader reader = w.getReader()) { + IndexSearcher searcher = new IndexSearcher(reader); + InternalCartesianBounds bounds = searchAndReduce(searcher, new AggTestConfig(aggBuilder, fieldType)); + assertThat(description + ": top", bounds.top, closeTo(points.top, GEOHASH_TOLERANCE)); + assertThat(description + ": bottom", bounds.bottom, closeTo(points.bottom, GEOHASH_TOLERANCE)); + assertThat(description + ": left", bounds.left, closeTo(points.left, GEOHASH_TOLERANCE)); + assertThat(description + ": right", bounds.right, closeTo(points.right, GEOHASH_TOLERANCE)); + assertTrue(description + ": hasValue(bounds)", AggregationInspectionHelper.hasValue(bounds)); + } + } + + private static class TestPointCollection { + double top = Double.NEGATIVE_INFINITY; + double bottom = Double.POSITIVE_INFINITY; + double left = Double.POSITIVE_INFINITY; + double right = Double.NEGATIVE_INFINITY; + ArrayList points = new ArrayList<>(); + + private void add(Point point) { + top = max((float) top, (float) point.getY()); + bottom = min((float) bottom, (float) point.getY()); + left = min((float) left, (float) point.getX()); + right = max((float) right, (float) point.getX()); + points.add(point); + } + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/InternalCartesianBoundsTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/InternalCartesianBoundsTests.java new file mode 100644 index 0000000000000..eef718d302c5a --- /dev/null +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/metrics/InternalCartesianBoundsTests.java @@ -0,0 +1,145 @@ +/* + * 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. + */ + +package org.elasticsearch.xpack.spatial.search.aggregations.metrics; + +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.common.util.Maps; +import org.elasticsearch.plugins.SearchPlugin; +import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.ParsedAggregation; +import org.elasticsearch.search.aggregations.support.SamplingContext; +import org.elasticsearch.test.InternalAggregationTestCase; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xpack.spatial.LocalStateSpatialPlugin; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.lang.Math.max; +import static java.lang.Math.min; +import static org.hamcrest.Matchers.closeTo; + +public class InternalCartesianBoundsTests extends InternalAggregationTestCase { + static final double GEOHASH_TOLERANCE = 1E-5D; + + @Override + protected SearchPlugin registerPlugin() { + return new LocalStateSpatialPlugin(); + } + + @Override + protected List getNamedXContents() { + return CollectionUtils.appendToCopy( + super.getNamedXContents(), + new NamedXContentRegistry.Entry( + Aggregation.class, + new ParseField(CartesianBoundsAggregationBuilder.NAME), + (p, c) -> ParsedCartesianBounds.fromXContent(p, (String) c) + ) + ); + } + + @Override + protected InternalCartesianBounds createTestInstance(String name, Map metadata) { + // we occasionally want to test top = Double.NEGATIVE_INFINITY since this triggers empty xContent object + double top = frequently() ? randomDouble() : Double.NEGATIVE_INFINITY; + return new InternalCartesianBounds(name, top, randomDouble(), randomDouble(), randomDouble(), metadata); + } + + @Override + protected void assertReduced(InternalCartesianBounds reduced, List inputs) { + double top = Double.NEGATIVE_INFINITY; + double bottom = Double.POSITIVE_INFINITY; + double left = Double.POSITIVE_INFINITY; + double right = Double.NEGATIVE_INFINITY; + for (InternalCartesianBounds bounds : inputs) { + top = max(top, bounds.top); + bottom = min(bottom, bounds.bottom); + left = min(left, bounds.left); + right = max(right, bounds.right); + } + assertValueClose(reduced.top, top); + assertValueClose(reduced.bottom, bottom); + assertValueClose(reduced.left, left); + assertValueClose(reduced.right, right); + } + + private static void assertValueClose(double expected, double actual) { + if (Double.isInfinite(expected) == false) { + assertThat(expected, closeTo(actual, GEOHASH_TOLERANCE)); + } else { + assertTrue(Double.isInfinite(actual)); + } + } + + @Override + protected boolean supportsSampling() { + return true; + } + + @Override + protected void assertSampled(InternalCartesianBounds sampled, InternalCartesianBounds reduced, SamplingContext samplingContext) { + assertValueClose(sampled.top, reduced.top); + assertValueClose(sampled.bottom, reduced.bottom); + assertValueClose(sampled.left, reduced.left); + assertValueClose(sampled.right, reduced.right); + } + + @Override + protected void assertFromXContent(InternalCartesianBounds aggregation, ParsedAggregation parsedAggregation) { + assertTrue(parsedAggregation instanceof ParsedCartesianBounds); + ParsedCartesianBounds parsed = (ParsedCartesianBounds) parsedAggregation; + + assertEquals(aggregation.topLeft(), parsed.topLeft()); + assertEquals(aggregation.bottomRight(), parsed.bottomRight()); + } + + @Override + protected InternalCartesianBounds mutateInstance(InternalCartesianBounds instance) { + String name = instance.getName(); + double top = instance.top; + double bottom = instance.bottom; + double left = instance.left; + double right = instance.right; + Map metadata = instance.getMetadata(); + switch (between(0, 5)) { + case 0: + name += randomAlphaOfLength(5); + break; + case 1: + if (Double.isFinite(top)) { + top += between(1, 20); + } else { + top = randomDouble(); + } + break; + case 2: + bottom += between(1, 20); + break; + case 3: + left += between(1, 20); + break; + case 4: + right += between(1, 20); + break; + case 5: + if (metadata == null) { + metadata = Maps.newMapWithExpectedSize(1); + } else { + metadata = new HashMap<>(instance.getMetadata()); + } + metadata.put(randomAlphaOfLength(15), randomInt()); + break; + default: + throw new AssertionError("Illegal randomisation branch"); + } + return new InternalCartesianBounds(name, top, bottom, left, right, metadata); + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/util/ShapeTestUtils.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/util/ShapeTestUtils.java index a6f4f2e86ef76..96667493de21c 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/util/ShapeTestUtils.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/util/ShapeTestUtils.java @@ -49,6 +49,24 @@ public static Point randomPoint(boolean hasAlt) { return new Point(randomValue(), randomValue()); } + public static Point randomPointNotExtreme(boolean hasAlt) { + return ESTestCase.randomValueOtherThanMany(ShapeTestUtils::extremePoint, () -> ShapeTestUtils.randomPoint(hasAlt)); + } + + public static Point randomPointNotExtreme() { + return ShapeTestUtils.randomPointNotExtreme(ESTestCase.randomBoolean()); + } + + /** + * Since cartesian centroid is stored in Float values, and calculations perform averages over many, + * We cannot support points at the very edge of the range. + */ + public static boolean extremePoint(Point point) { + double max = Float.MAX_VALUE / 100; + double min = -Float.MAX_VALUE / 100; + return point.getLon() > max || point.getLon() < min || point.getLat() > max || point.getLat() < min; + } + public static double randomAlt() { return ESTestCase.randomDouble() * XShapeTestUtil.CENTER_SCALE_FACTOR; } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/spatial/10_bounds.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/spatial/10_bounds.yml new file mode 100644 index 0000000000000..dcf0b1e6f1d94 --- /dev/null +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/spatial/10_bounds.yml @@ -0,0 +1,260 @@ +--- +"Test geo_bounds aggregation on geo_shape field": + - do: + indices.create: + index: locations + body: + mappings: + properties: + location: + type: geo_shape + + - do: + index: + index: locations + id: point_with_doc_values + body: { location: "POINT(34.25 -21.76)" } + + - do: + indices.refresh: {} + + - do: + search: + rest_total_hits_as_int: true + index: locations + size: 0 + body: + aggs: + my_agg: + geo_bounds: + field: location + wrap_longitude: true + - match: { hits.total: 1 } + - match: { aggregations.my_agg.bounds.top_left.lat: -21.760000032372773 } + - match: { aggregations.my_agg.bounds.top_left.lon: 34.24999997019768 } + +--- +"Test cartesian_bounds aggregation on point field with point": + - do: + indices.create: + index: locations + body: + mappings: + properties: + location: + type: point + + - do: + index: + index: locations + id: point_with_doc_values + body: { location: "POINT(342.5 -217.6)" } + + - do: + indices.refresh: {} + + - do: + search: + rest_total_hits_as_int: true + index: locations + size: 0 + body: + aggs: + my_agg: + cartesian_bounds: + field: location + - match: { hits.total: 1 } + - match: { aggregations.my_agg.bounds.top_left.x: 342.5 } + - match: { aggregations.my_agg.bounds.top_left.y: -217.60000610351562 } + - match: { aggregations.my_agg.bounds.bottom_right.x: 342.5 } + - match: { aggregations.my_agg.bounds.bottom_right.y: -217.60000610351562 } + +--- +"Test cartesian_bounds aggregation on shape field with point": + - do: + indices.create: + index: locations + body: + mappings: + properties: + location: + type: shape + + - do: + index: + index: locations + id: point_with_doc_values + body: { location: "POINT(342.5 -217.6)" } + + - do: + indices.refresh: {} + + - do: + search: + rest_total_hits_as_int: true + index: locations + size: 0 + body: + aggs: + my_agg: + cartesian_bounds: + field: location + - match: { hits.total: 1 } + - match: { aggregations.my_agg.bounds.top_left.x: 342.5 } + - match: { aggregations.my_agg.bounds.top_left.y: -217.60000610351562 } + - match: { aggregations.my_agg.bounds.bottom_right.x: 342.5 } + - match: { aggregations.my_agg.bounds.bottom_right.y: -217.60000610351562 } + +--- +"Test cartesian_bounds aggregation on point field with points": + - do: + indices.create: + index: locations + body: + mappings: + properties: + location: + type: point + + - do: + bulk: + refresh: true + body: + - index: + _index: locations + _id: "1" + - '{"location": "POINT(491.2350 5237.4081)", "city": "Amsterdam", "name": "NEMO Science Museum"}' + - index: + _index: locations + _id: "2" + - '{"location": "POINT(490.1618 5236.9219)", "city": "Amsterdam", "name": "Museum Het Rembrandthuis"}' + - index: + _index: locations + _id: "3" + - '{"location": "POINT(491.4722 5237.1667)", "city": "Amsterdam", "name": "Nederlands Scheepvaartmuseum"}' + - index: + _index: locations + _id: "4" + - '{"location": "POINT(440.5200 5122.2900)", "city": "Antwerp", "name": "Letterenhuis"}' + - index: + _index: locations + _id: "5" + - '{"location": "POINT(233.6389 4886.1111)", "city": "Paris", "name": "Musée du Louvre"}' + - index: + _index: locations + _id: "6" + - '{"location": "POINT(232.7000 4886.0000)", "city": "Paris", "name": "Musée dOrsay"}' + + - do: + indices.refresh: {} + + - do: + search: + rest_total_hits_as_int: true + index: locations + size: 0 + body: + aggs: + my_agg: + cartesian_bounds: + field: location + - match: { hits.total: 6 } + - match: { aggregations.my_agg.bounds.top_left.x: 232.6999969482422 } + - match: { aggregations.my_agg.bounds.top_left.y: 5237.408203125 } + - match: { aggregations.my_agg.bounds.bottom_right.x: 491.4721984863281 } + - match: { aggregations.my_agg.bounds.bottom_right.y: 4886.0 } + +--- +"Test cartesian_bounds aggregation on shape field with points": + - do: + indices.create: + index: locations + body: + mappings: + properties: + location: + type: shape + + - do: + bulk: + refresh: true + body: + - index: + _index: locations + _id: "1" + - '{"location": "POINT(491.2350 5237.4081)", "city": "Amsterdam", "name": "NEMO Science Museum"}' + - index: + _index: locations + _id: "2" + - '{"location": "POINT(490.1618 5236.9219)", "city": "Amsterdam", "name": "Museum Het Rembrandthuis"}' + - index: + _index: locations + _id: "3" + - '{"location": "POINT(491.4722 5237.1667)", "city": "Amsterdam", "name": "Nederlands Scheepvaartmuseum"}' + - index: + _index: locations + _id: "4" + - '{"location": "POINT(440.5200 5122.2900)", "city": "Antwerp", "name": "Letterenhuis"}' + - index: + _index: locations + _id: "5" + - '{"location": "POINT(233.6389 4886.1111)", "city": "Paris", "name": "Musée du Louvre"}' + - index: + _index: locations + _id: "6" + - '{"location": "POINT(232.7000 4886.0000)", "city": "Paris", "name": "Musée dOrsay"}' + + - do: + indices.refresh: {} + + - do: + search: + rest_total_hits_as_int: true + index: locations + size: 0 + body: + aggs: + my_agg: + cartesian_bounds: + field: location + - match: { hits.total: 6 } + - match: { aggregations.my_agg.bounds.top_left.x: 232.6999969482422 } + - match: { aggregations.my_agg.bounds.top_left.y: 5237.408203125 } + - match: { aggregations.my_agg.bounds.bottom_right.x: 491.4721984863281 } + - match: { aggregations.my_agg.bounds.bottom_right.y: 4886.0000 } + +--- +"Test cartesian_bounds aggregation on shape field with polygon": + - do: + indices.create: + index: locations + body: + mappings: + properties: + location: + type: shape + + - do: + index: + index: locations + id: big_rectangle + body: { location: "POLYGON((-1000 -1000, 1000 -1000, 1000 1000, -1000 1000, -1000 -1000))" } + + - do: + indices.refresh: {} + + - do: + search: + rest_total_hits_as_int: true + index: locations + size: 0 + body: + aggs: + my_agg: + cartesian_bounds: + field: location + - match: { hits.total: 1 } + - match: { aggregations.my_agg.bounds.top_left.x: -1000.0 } + - match: { aggregations.my_agg.bounds.top_left.y: 1000.0 } + - match: { aggregations.my_agg.bounds.bottom_right.x: 1000.0 } + - match: { aggregations.my_agg.bounds.bottom_right.y: -1000.0 } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/spatial/10_geo_bounds.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/spatial/10_geo_bounds.yml deleted file mode 100644 index d47f047bd6af8..0000000000000 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/spatial/10_geo_bounds.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -"Test geo_bounds aggregation on geo_shape field": - - do: - indices.create: - index: locations - body: - mappings: - properties: - location: - type: geo_shape - - - do: - index: - index: locations - id: point_with_doc_values - body: { location: "POINT(34.25 -21.76)" } - - - do: - indices.refresh: {} - - - do: - search: - rest_total_hits_as_int: true - index: locations - size: 0 - body: - aggs: - my_agg: - geo_bounds: - field: location - wrap_longitude: true - - match: {hits.total: 1 } - - match: { aggregations.my_agg.bounds.top_left.lat: -21.760000032372773 } - - match: { aggregations.my_agg.bounds.top_left.lon: 34.24999997019768 }