Skip to content

Commit

Permalink
ESQL: Support ST_EXTENT_AGG (#117451)
Browse files Browse the repository at this point in the history
This PR adds support for ST_EXTENT_AGG aggregation, i.e., computing a bounding box over a set of points/shapes (Cartesian or geo). Note the difference between this aggregation and the already implemented scalar function ST_EXTENT.

This isn't a very efficient implementation, and future PRs will attempt to read these extents directly from the doc values.
We currently always use longitude wrapping, i.e., we may wrap around the dateline for a smaller bounding box. Future PRs will let the user control this behavior.
Fixes #104659.
  • Loading branch information
GalLalouche authored Dec 13, 2024
1 parent 140d88c commit 2be4cd9
Show file tree
Hide file tree
Showing 79 changed files with 4,923 additions and 234 deletions.
6 changes: 6 additions & 0 deletions docs/changelog/117451.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 117451
summary: ST_EXTENT aggregation
area: ES|QL
type: feature
issues:
- 104659
2 changes: 2 additions & 0 deletions docs/reference/esql/functions/aggregation-functions.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The <<esql-stats-by>> command supports these aggregate functions:
* <<esql-min>>
* <<esql-percentile>>
* experimental:[] <<esql-st_centroid_agg>>
* experimental:[] <<esql-st_extent_agg>>
* <<esql-std_dev>>
* <<esql-sum>>
* <<esql-top>>
Expand All @@ -33,6 +34,7 @@ include::layout/median_absolute_deviation.asciidoc[]
include::layout/min.asciidoc[]
include::layout/percentile.asciidoc[]
include::layout/st_centroid_agg.asciidoc[]
include::layout/st_extent_agg.asciidoc[]
include::layout/std_dev.asciidoc[]
include::layout/sum.asciidoc[]
include::layout/top.asciidoc[]
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions docs/reference/esql/functions/examples/st_extent_agg.asciidoc

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions docs/reference/esql/functions/kibana/definition/st_extent_agg.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions docs/reference/esql/functions/kibana/docs/st_extent_agg.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions docs/reference/esql/functions/layout/st_extent_agg.asciidoc

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/reference/esql/functions/signature/st_extent_agg.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions docs/reference/esql/functions/types/st_extent_agg.asciidoc

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,15 @@ public static Optional<Rectangle> visitCartesian(Geometry geometry) {
return Optional.empty();
}

public enum WrapLongitude {
NO_WRAP,
WRAP
}

/**
* Determine the BBOX assuming the CRS is geographic (eg WGS84) and optionally wrapping the longitude around the dateline.
*/
public static Optional<Rectangle> visitGeo(Geometry geometry, boolean wrapLongitude) {
public static Optional<Rectangle> visitGeo(Geometry geometry, WrapLongitude wrapLongitude) {
var visitor = new SpatialEnvelopeVisitor(new GeoPointVisitor(wrapLongitude));
if (geometry.visit(visitor)) {
return Optional.of(visitor.getResult());
Expand Down Expand Up @@ -181,40 +186,16 @@ public Rectangle getResult() {
* </ul>
*/
public static class GeoPointVisitor implements PointVisitor {
private double minY = Double.POSITIVE_INFINITY;
private double maxY = Double.NEGATIVE_INFINITY;
private double minNegX = Double.POSITIVE_INFINITY;
private double maxNegX = Double.NEGATIVE_INFINITY;
private double minPosX = Double.POSITIVE_INFINITY;
private double maxPosX = Double.NEGATIVE_INFINITY;

public double getMinY() {
return minY;
}

public double getMaxY() {
return maxY;
}

public double getMinNegX() {
return minNegX;
}
protected double minY = Double.POSITIVE_INFINITY;
protected double maxY = Double.NEGATIVE_INFINITY;
protected double minNegX = Double.POSITIVE_INFINITY;
protected double maxNegX = Double.NEGATIVE_INFINITY;
protected double minPosX = Double.POSITIVE_INFINITY;
protected double maxPosX = Double.NEGATIVE_INFINITY;

public double getMaxNegX() {
return maxNegX;
}

public double getMinPosX() {
return minPosX;
}
private final WrapLongitude wrapLongitude;

public double getMaxPosX() {
return maxPosX;
}

private final boolean wrapLongitude;

public GeoPointVisitor(boolean wrapLongitude) {
public GeoPointVisitor(WrapLongitude wrapLongitude) {
this.wrapLongitude = wrapLongitude;
}

Expand Down Expand Up @@ -253,32 +234,35 @@ public Rectangle getResult() {
return getResult(minNegX, minPosX, maxNegX, maxPosX, maxY, minY, wrapLongitude);
}

private static Rectangle getResult(
protected static Rectangle getResult(
double minNegX,
double minPosX,
double maxNegX,
double maxPosX,
double maxY,
double minY,
boolean wrapLongitude
WrapLongitude wrapLongitude
) {
assert Double.isFinite(maxY);
if (Double.isInfinite(minPosX)) {
return new Rectangle(minNegX, maxNegX, maxY, minY);
} else if (Double.isInfinite(minNegX)) {
return new Rectangle(minPosX, maxPosX, maxY, minY);
} else if (wrapLongitude) {
double unwrappedWidth = maxPosX - minNegX;
double wrappedWidth = (180 - minPosX) - (-180 - maxNegX);
if (unwrappedWidth <= wrappedWidth) {
return new Rectangle(minNegX, maxPosX, maxY, minY);
} else {
return new Rectangle(minPosX, maxNegX, maxY, minY);
}
} else {
return new Rectangle(minNegX, maxPosX, maxY, minY);
return switch (wrapLongitude) {
case NO_WRAP -> new Rectangle(minNegX, maxPosX, maxY, minY);
case WRAP -> maybeWrap(minNegX, minPosX, maxNegX, maxPosX, maxY, minY);
};
}
}

private static Rectangle maybeWrap(double minNegX, double minPosX, double maxNegX, double maxPosX, double maxY, double minY) {
double unwrappedWidth = maxPosX - minNegX;
double wrappedWidth = 360 + maxNegX - minPosX;
return unwrappedWidth <= wrappedWidth
? new Rectangle(minNegX, maxPosX, maxY, minY)
: new Rectangle(minPosX, maxNegX, maxY, minY);
}
}

private boolean isValid() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.elasticsearch.geo.ShapeTestUtils;
import org.elasticsearch.geometry.Point;
import org.elasticsearch.geometry.Rectangle;
import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude;
import org.elasticsearch.test.ESTestCase;

import static org.hamcrest.Matchers.equalTo;
Expand All @@ -36,7 +37,7 @@ public void testVisitCartesianShape() {
public void testVisitGeoShapeNoWrap() {
for (int i = 0; i < 1000; i++) {
var geometry = GeometryTestUtils.randomGeometryWithoutCircle(0, false);
var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, false);
var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.NO_WRAP);
assertNotNull(bbox);
assertTrue(i + ": " + geometry, bbox.isPresent());
var result = bbox.get();
Expand All @@ -48,7 +49,8 @@ public void testVisitGeoShapeNoWrap() {
public void testVisitGeoShapeWrap() {
for (int i = 0; i < 1000; i++) {
var geometry = GeometryTestUtils.randomGeometryWithoutCircle(0, true);
var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, false);
// TODO this should be WRAP instead
var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.NO_WRAP);
assertNotNull(bbox);
assertTrue(i + ": " + geometry, bbox.isPresent());
var result = bbox.get();
Expand Down Expand Up @@ -81,7 +83,7 @@ public void testVisitCartesianPoints() {
}

public void testVisitGeoPointsNoWrapping() {
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(false));
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(WrapLongitude.NO_WRAP));
double minY = Double.MAX_VALUE;
double maxY = -Double.MAX_VALUE;
double minX = Double.MAX_VALUE;
Expand All @@ -103,7 +105,7 @@ public void testVisitGeoPointsNoWrapping() {
}

public void testVisitGeoPointsWrapping() {
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(true));
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(WrapLongitude.WRAP));
double minY = Double.POSITIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
double minNegX = Double.POSITIVE_INFINITY;
Expand Down Expand Up @@ -145,7 +147,7 @@ public void testVisitGeoPointsWrapping() {
}

public void testWillCrossDateline() {
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(true));
var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(WrapLongitude.WRAP));
visitor.visit(new Point(-90.0, 0.0));
visitor.visit(new Point(90.0, 0.0));
assertCrossesDateline(visitor, false);
Expand Down
Loading

0 comments on commit 2be4cd9

Please sign in to comment.