diff --git a/docs/changelog/136309.yaml b/docs/changelog/136309.yaml
new file mode 100644
index 0000000000000..f27dcca302bc9
--- /dev/null
+++ b/docs/changelog/136309.yaml
@@ -0,0 +1,6 @@
+pr: 136309
+summary: Adds ST_SIMPLIFY geospatial function
+area: ES|QL
+type: enhancement
+issues:
+ - 44747
diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/st_simplify.md b/docs/reference/query-languages/esql/_snippets/functions/description/st_simplify.md
new file mode 100644
index 0000000000000..066834132bb4b
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/description/st_simplify.md
@@ -0,0 +1,6 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Description**
+
+Simplifies the input geometry by applying the Douglas-Peucker algorithm with a specified tolerance. Vertices that fall within the tolerance distance from the simplified shape are removed. Note that the resulting geometry may be invalid, even if the original input was valid.
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/st_simplify.md b/docs/reference/query-languages/esql/_snippets/functions/examples/st_simplify.md
new file mode 100644
index 0000000000000..52ba8652b850d
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/examples/st_simplify.md
@@ -0,0 +1,14 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Example**
+
+```esql
+ROW wkt = "POLYGON ((7.998 53.827, 9.470 53.068, 15.754 53.801, 16.523 57.160, 11.162 57.868, 8.064 57.445, 6.219 55.317, 7.998 53.827))"
+| EVAL simplified = ST_SIMPLIFY(TO_GEOSHAPE(wkt), 0.7)
+```
+
+| wkt:keyword | simplified:geo_shape |
+| --- | --- |
+| POLYGON ((7.998 53.827, 9.470 53.068, 15.754 53.801, 16.523 57.160, 11.162 57.868, 8.064 57.445, 6.219 55.317, 7.998 53.827)) | POLYGON ((9.47 53.068, 15.754 53.801, 16.523 57.16, 8.064 57.445, 6.219 55.317, 9.47 53.068)) |
+
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/st_simplify.md b/docs/reference/query-languages/esql/_snippets/functions/layout/st_simplify.md
new file mode 100644
index 0000000000000..1e31764f27628
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/layout/st_simplify.md
@@ -0,0 +1,27 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+## `ST_SIMPLIFY` [esql-st_simplify]
+```{applies_to}
+stack: preview 9.4.0
+serverless: preview
+```
+
+**Syntax**
+
+:::{image} ../../../images/functions/st_simplify.svg
+:alt: Embedded
+:class: text-center
+:::
+
+
+:::{include} ../parameters/st_simplify.md
+:::
+
+:::{include} ../description/st_simplify.md
+:::
+
+:::{include} ../types/st_simplify.md
+:::
+
+:::{include} ../examples/st_simplify.md
+:::
diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/st_simplify.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/st_simplify.md
new file mode 100644
index 0000000000000..9271c3d03b100
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/st_simplify.md
@@ -0,0 +1,10 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Parameters**
+
+`geometry`
+: Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`.
+
+`tolerance`
+: Tolerance for the geometry simplification, in the units of the input SRS
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/st_simplify.md b/docs/reference/query-languages/esql/_snippets/functions/types/st_simplify.md
new file mode 100644
index 0000000000000..4902ee3237cd4
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/types/st_simplify.md
@@ -0,0 +1,11 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Supported types**
+
+| geometry | tolerance | result |
+| --- | --- | --- |
+| cartesian_point | double | cartesian_point |
+| cartesian_shape | double | cartesian_shape |
+| geo_point | double | geo_point |
+| geo_shape | double | geo_shape |
+
diff --git a/docs/reference/query-languages/esql/_snippets/lists/spatial-functions.md b/docs/reference/query-languages/esql/_snippets/lists/spatial-functions.md
index 8ffdebed1a899..4812a5df8dd44 100644
--- a/docs/reference/query-languages/esql/_snippets/lists/spatial-functions.md
+++ b/docs/reference/query-languages/esql/_snippets/lists/spatial-functions.md
@@ -13,3 +13,4 @@
* [`ST_GEOTILE`](../../functions-operators/spatial-functions.md#esql-st_geotile) {applies_to}`stack: preview` {applies_to}`serverless: preview`
* [`ST_GEOHEX`](../../functions-operators/spatial-functions.md#esql-st_geohex) {applies_to}`stack: preview` {applies_to}`serverless: preview`
* [`ST_GEOHASH`](../../functions-operators/spatial-functions.md#esql-st_geohash) {applies_to}`stack: preview` {applies_to}`serverless: preview`
+* [`ST_SIMPLIFY`](../../functions-operators/spatial-functions.md#esql-st_simplify) {applies_to}`stack: preview` {applies_to}`serverless: preview`
diff --git a/docs/reference/query-languages/esql/functions-operators/spatial-functions.md b/docs/reference/query-languages/esql/functions-operators/spatial-functions.md
index df91b73e257a9..09a2fcc8bd450 100644
--- a/docs/reference/query-languages/esql/functions-operators/spatial-functions.md
+++ b/docs/reference/query-languages/esql/functions-operators/spatial-functions.md
@@ -59,3 +59,6 @@ mapped_pages:
:::{include} ../_snippets/functions/layout/st_geohash.md
:::
+
+:::{include} ../_snippets/functions/layout/st_simplify.md
+:::
diff --git a/docs/reference/query-languages/esql/images/functions/st_simplify.svg b/docs/reference/query-languages/esql/images/functions/st_simplify.svg
new file mode 100644
index 0000000000000..cea940a2f2bd2
--- /dev/null
+++ b/docs/reference/query-languages/esql/images/functions/st_simplify.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/st_simplify.json b/docs/reference/query-languages/esql/kibana/definition/functions/st_simplify.json
new file mode 100644
index 0000000000000..89ea74fa57aac
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/definition/functions/st_simplify.json
@@ -0,0 +1,85 @@
+{
+ "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.",
+ "type" : "scalar",
+ "name" : "st_simplify",
+ "description" : "Simplifies the input geometry by applying the Douglas-Peucker algorithm with a specified tolerance. Vertices that fall within the tolerance distance from the simplified shape are removed. Note that the resulting geometry may be invalid, even if the original input was valid.",
+ "signatures" : [
+ {
+ "params" : [
+ {
+ "name" : "geometry",
+ "type" : "cartesian_point",
+ "optional" : false,
+ "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "tolerance",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Tolerance for the geometry simplification, in the units of the input SRS"
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "cartesian_point"
+ },
+ {
+ "params" : [
+ {
+ "name" : "geometry",
+ "type" : "cartesian_shape",
+ "optional" : false,
+ "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "tolerance",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Tolerance for the geometry simplification, in the units of the input SRS"
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "cartesian_shape"
+ },
+ {
+ "params" : [
+ {
+ "name" : "geometry",
+ "type" : "geo_point",
+ "optional" : false,
+ "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "tolerance",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Tolerance for the geometry simplification, in the units of the input SRS"
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "geo_point"
+ },
+ {
+ "params" : [
+ {
+ "name" : "geometry",
+ "type" : "geo_shape",
+ "optional" : false,
+ "description" : "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. If `null`, the function returns `null`."
+ },
+ {
+ "name" : "tolerance",
+ "type" : "double",
+ "optional" : false,
+ "description" : "Tolerance for the geometry simplification, in the units of the input SRS"
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "geo_shape"
+ }
+ ],
+ "examples" : [
+ "ROW wkt = \"POLYGON ((7.998 53.827, 9.470 53.068, 15.754 53.801, 16.523 57.160, 11.162 57.868, 8.064 57.445, 6.219 55.317, 7.998 53.827))\"\n| EVAL simplified = ST_SIMPLIFY(TO_GEOSHAPE(wkt), 0.7)"
+ ],
+ "preview" : true,
+ "snapshot_only" : false
+}
diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/st_simplify.md b/docs/reference/query-languages/esql/kibana/docs/functions/st_simplify.md
new file mode 100644
index 0000000000000..1e46dc4642693
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/docs/functions/st_simplify.md
@@ -0,0 +1,9 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+### ST SIMPLIFY
+Simplifies the input geometry by applying the Douglas-Peucker algorithm with a specified tolerance. Vertices that fall within the tolerance distance from the simplified shape are removed. Note that the resulting geometry may be invalid, even if the original input was valid.
+
+```esql
+ROW wkt = "POLYGON ((7.998 53.827, 9.470 53.068, 15.754 53.801, 16.523 57.160, 11.162 57.868, 8.064 57.445, 6.219 55.317, 7.998 53.827))"
+| EVAL simplified = ST_SIMPLIFY(TO_GEOSHAPE(wkt), 0.7)
+```
diff --git a/x-pack/plugin/esql-core/build.gradle b/x-pack/plugin/esql-core/build.gradle
index 95eabb8faa1e8..f7c28ad026e4b 100644
--- a/x-pack/plugin/esql-core/build.gradle
+++ b/x-pack/plugin/esql-core/build.gradle
@@ -15,6 +15,7 @@ base {
dependencies {
api project(path: xpackModule('mapper-version'))
+ api "org.locationtech.jts:jts-core:${versions.jts}"
testImplementation project(':test:framework')
testImplementation(testArtifact(project(xpackModule('core'))))
}
diff --git a/x-pack/plugin/esql-core/licenses/jts-core-LICENSE.txt b/x-pack/plugin/esql-core/licenses/jts-core-LICENSE.txt
new file mode 100644
index 0000000000000..bc03db03a5926
--- /dev/null
+++ b/x-pack/plugin/esql-core/licenses/jts-core-LICENSE.txt
@@ -0,0 +1,31 @@
+Eclipse Distribution License - v 1.0
+
+Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors.
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ Neither the name of the Eclipse Foundation, Inc. nor the names of its
+ contributors may be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/x-pack/plugin/esql-core/licenses/jts-core-NOTICE.txt b/x-pack/plugin/esql-core/licenses/jts-core-NOTICE.txt
new file mode 100644
index 0000000000000..8d1c8b69c3fce
--- /dev/null
+++ b/x-pack/plugin/esql-core/licenses/jts-core-NOTICE.txt
@@ -0,0 +1 @@
+
diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/util/SpatialCoordinateTypes.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/util/SpatialCoordinateTypes.java
index 019aabda5058e..e0ed0549518aa 100644
--- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/util/SpatialCoordinateTypes.java
+++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/util/SpatialCoordinateTypes.java
@@ -12,10 +12,19 @@
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.geometry.Geometry;
import org.elasticsearch.geometry.Point;
+import org.elasticsearch.geometry.Rectangle;
import org.elasticsearch.geometry.utils.GeographyValidator;
import org.elasticsearch.geometry.utils.GeometryValidator;
import org.elasticsearch.geometry.utils.WellKnownBinary;
import org.elasticsearch.geometry.utils.WellKnownText;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.Polygon;
+import org.locationtech.jts.geom.impl.CoordinateArraySequence;
+import org.locationtech.jts.io.ParseException;
+import org.locationtech.jts.io.WKTReader;
+import org.locationtech.jts.io.WKTWriter;
import java.nio.ByteOrder;
@@ -125,4 +134,31 @@ public String wkbToWkt(BytesRef wkb) {
public Geometry wkbToGeometry(BytesRef wkb) {
return WellKnownBinary.fromWKB(validator(), false, wkb.bytes, wkb.offset, wkb.length);
}
+
+ public org.locationtech.jts.geom.Geometry wkbToJtsGeometry(BytesRef wkb) throws ParseException, IllegalArgumentException {
+ String wkt = wkbToWkt(wkb);
+ if (wkt.startsWith("BBOX")) {
+ Geometry geometry = WellKnownBinary.fromWKB(GeometryValidator.NOOP, true, wkb.bytes, wkb.offset, wkb.length);
+ if (geometry instanceof Rectangle rect) {
+ var bottomLeft = new Coordinate(rect.getMinX(), rect.getMinY());
+ var bottomRight = new Coordinate(rect.getMaxX(), rect.getMinY());
+ var topRight = new Coordinate(rect.getMaxX(), rect.getMaxY());
+ var topLeft = new Coordinate(rect.getMinX(), rect.getMaxY());
+
+ var coordinates = new Coordinate[] { bottomLeft, bottomRight, topRight, topLeft, bottomLeft };
+ var geomFactory = new GeometryFactory();
+ var linearRing = new LinearRing(new CoordinateArraySequence(coordinates), geomFactory);
+ return new Polygon(linearRing, null, geomFactory);
+ }
+ }
+ WKTReader reader = new WKTReader();
+ return reader.read(wkt);
+ }
+
+ public BytesRef jtsGeometryToWkb(org.locationtech.jts.geom.Geometry jtsGeometry) {
+ WKTWriter writer = new WKTWriter();
+ String wkt = writer.write(jtsGeometry);
+ return wktToWkb(wkt);
+ }
+
}
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial-jts.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial-jts.csv-spec
new file mode 100644
index 0000000000000..e9c383e1072bb
--- /dev/null
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial-jts.csv-spec
@@ -0,0 +1,240 @@
+###############################################
+# Tests for ST_SIMPLIFY
+###############################################
+
+stSimplifyLiteralPolygon
+required_capability: st_simplify
+
+// tag::st_simplify[]
+ROW wkt = "POLYGON ((7.998 53.827, 9.470 53.068, 15.754 53.801, 16.523 57.160, 11.162 57.868, 8.064 57.445, 6.219 55.317, 7.998 53.827))"
+| EVAL simplified = ST_SIMPLIFY(TO_GEOSHAPE(wkt), 0.7)
+// end::st_simplify[]
+;
+
+// tag::st_simplify-result[]
+wkt:keyword | simplified:geo_shape
+POLYGON ((7.998 53.827, 9.470 53.068, 15.754 53.801, 16.523 57.160, 11.162 57.868, 8.064 57.445, 6.219 55.317, 7.998 53.827)) | POLYGON ((9.47 53.068, 15.754 53.801, 16.523 57.16, 8.064 57.445, 6.219 55.317, 9.47 53.068))
+// end::st_simplify-result[]
+;
+
+stSimplifyLiteralPoints
+required_capability: st_simplify
+
+// tag::st_simplify_points[]
+ROW wkt = ["POINT (7.998 53.827)", "POINT (9.470 53.068)", "POINT (15.754 53.801)", "POINT (16.523 57.160)", "POINT (11.162 57.868)", "POINT (8.064 57.445)", "POINT (6.219 55.317)", "POINT (7.998 53.827)"]
+| EVAL simplified = ST_SIMPLIFY(TO_GEOPOINT(wkt), 0.1)
+// end::st_simplify_points[]
+;
+
+// tag::st_simplify_points-result[]
+wkt:keyword | simplified:geo_point
+[POINT (7.998 53.827), POINT (9.470 53.068), POINT (15.754 53.801), POINT (16.523 57.160), POINT (11.162 57.868), POINT (8.064 57.445), POINT (6.219 55.317), POINT (7.998 53.827)] | GEOMETRYCOLLECTION (POINT (7.998 53.827),POINT (9.47 53.068),POINT (15.754 53.801),POINT (16.523 57.16),POINT (11.162 57.868),POINT (8.064 57.445),POINT (6.219 55.317),POINT (7.998 53.827))
+// end::st_simplify_points-result[]
+;
+
+stSimplifyCityBoundaries
+required_capability: st_intersects
+required_capability: st_simplify
+
+// tag::st_simplify_city_boundaries[]
+FROM airport_city_boundaries
+| WHERE ST_INTERSECTS(city_location, TO_GEOSHAPE("POLYGON ((7.998 53.827, 9.470 53.068, 15.754 53.801, 16.523 57.160, 11.162 57.868, 8.064 57.445, 6.219 55.317, 7.998 53.827))"))
+| EVAL o_len = LENGTH(TO_STRING(city_boundary))
+| EVAL city_boundary = ST_SIMPLIFY(city_boundary, 0.05)
+| EVAL s_len = LENGTH(TO_STRING(city_boundary))
+| SORT city
+| KEEP city, o_len, s_len, city_boundary
+// end::st_simplify_city_boundaries[]
+;
+
+// tag::st_simplify_city_boundaries-result[]
+city:keyword | o_len:i | s_len:i | city_boundary:geo_shape
+Copenhagen | 265 | 76 | POLYGON ((12.453 55.7122, 12.5332 55.6318, 12.6398 55.7224, 12.453 55.7122))
+Gothenburg | 112 | 78 | POLYGON ((11.8941 57.6889, 12.0885 57.6823, 12.0541 57.7338, 11.8941 57.6889))
+Norderstedt | 221 | 75 | POLYGON ((9.9348 53.6785, 10.0729 53.7097, 9.9833 53.7595, 9.9348 53.6785))
+// end::st_simplify_city_boundaries-result[]
+;
+
+stSimplifyNoSimplification
+required_capability: st_simplify
+
+ROW geo_shape = TO_GEOSHAPE("POLYGON((0 0, 1 0.1, 2 0, 2 2, 1 1.9, 0 2, 0 0))")
+| EVAL result = ST_SIMPLIFY(geo_shape, 0.01)
+| KEEP result
+;
+
+result:geo_shape
+POLYGON ((0.0 0.0, 1.0 0.1, 2.0 0.0, 2.0 2.0, 1.0 1.9, 0.0 2.0, 0.0 0.0))
+;
+
+stSimplifyWithSimplification
+required_capability: st_simplify
+
+ROW geo_shape = TO_GEOSHAPE("POLYGON((0 0, 1 0.1, 2 0, 2 2, 1 1.9, 0 2, 0 0))")
+| EVAL result = ST_SIMPLIFY(geo_shape, 0.2)
+| KEEP result
+;
+
+result:geo_shape
+POLYGON ((0.0 0.0, 2.0 0.0, 2.0 2.0, 0.0 2.0, 0.0 0.0))
+;
+
+stSimplifyFoldableGeometry
+required_capability: st_simplify
+
+ROW result = ST_SIMPLIFY(TO_GEOSHAPE("POLYGON((0 0, 1 0.1, 2 0, 2 2, 1 1.9, 0 2, 0 0))") , 0.2)
+;
+
+result:geo_shape
+POLYGON ((0.0 0.0, 2.0 0.0, 2.0 2.0, 0.0 2.0, 0.0 0.0))
+;
+
+stSimplifyEmptySimplification
+required_capability: st_simplify
+
+ROW geo_shape = TO_GEOSHAPE("POLYGON((0 0, 1 0.1, 2 0, 2 2, 1 1.9, 0 2, 0 0))")
+| EVAL result = ST_SIMPLIFY(geo_shape, 2.0)
+| KEEP result
+;
+
+result:geo_shape
+POLYGON EMPTY
+;
+
+stSimplifyNull
+required_capability: st_simplify
+
+ROW geo_shape = NULL
+| EVAL result = ST_SIMPLIFY(geo_shape, 2.0)
+| KEEP result
+;
+
+result:null
+NULL
+;
+
+stSimplifyCartesianPoint
+required_capability: st_simplify
+
+ROW wkt = ["POINT(-97.11 95.53)", "POINT(80.93 72.77)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_CARTESIANPOINT(wkt)
+| EVAL result = ST_SIMPLIFY(pt, 2.0)
+| KEEP result
+;
+
+result:cartesian_point
+POINT (-97.11 95.53)
+POINT (80.93 72.77)
+;
+
+stSimplifyCartesianShape
+required_capability: st_simplify
+
+ROW wkt = ["POINT(97.11 75.53)", "POLYGON((0 0, 1 0.1, 2 0, 2 2, 1 1.9, 0 2, 0 0))"]
+| MV_EXPAND wkt
+| EVAL geom = TO_CARTESIANSHAPE(wkt)
+| EVAL result = ST_SIMPLIFY(geom, 0.2)
+| KEEP result
+;
+
+result:cartesian_shape
+POINT (97.11 75.53)
+POLYGON ((0.0 0.0, 2.0 0.0, 2.0 2.0, 0.0 2.0, 0.0 0.0))
+;
+
+stSimplifyWithIntegerTolerance
+required_capability: st_simplify
+
+ROW geo_shape = TO_GEOSHAPE("POLYGON((0 0, 1 0.1, 2 0, 2 2, 1 1.9, 0 2, 0 0))")
+| EVAL result = ST_SIMPLIFY(geo_shape, 2)
+| KEEP result
+;
+
+result:geo_shape
+POLYGON EMPTY
+;
+
+stSimplifyBbox
+required_capability: st_simplify
+
+ROW geo_shape = TO_GEOSHAPE("BBOX (0, 2, 2, 0)")
+| EVAL result1 = ST_SIMPLIFY(geo_shape, 2), result2 = ST_SIMPLIFY(geo_shape, 0.5)
+| KEEP result1, result2
+;
+
+result1:geo_shape | result2:geo_shape
+POLYGON EMPTY | POLYGON ((0.0 0.0, 2.0 0.0, 2.0 2.0, 0.0 2.0, 0.0 0.0))
+;
+
+stSimplifyPostGisFirstExample
+required_capability: st_simplify
+
+ROW geom = TO_GEOSHAPE("POLYGON((11 3,10.914448613738104 1.694738077799484,10.659258262890683 0.411809548974793,10.238795325112868 -0.826834323650898,9.660254037844387 -1.999999999999999,8.933533402912353 -3.087614290087205,8.071067811865476 -4.071067811865475,7.087614290087206 -4.933533402912351,6.000000000000001 -5.660254037844386,4.826834323650898 -6.238795325112868,3.58819045102521 -6.659258262890681,2.305261922200517 -6.914448613738104,1 -7,-0.305261922200514 -6.914448613738106,-1.588190451025206 -6.659258262890683,-2.826834323650895 -6.238795325112868,-3.999999999999998 -5.660254037844387,-5.087614290087204 -4.933533402912354,-6.071067811865475 -4.071067811865476,-6.93353340291235 -3.087614290087209,-7.660254037844386 -2.000000000000004,-8.238795325112868 -0.826834323650899,-8.659258262890681 0.41180954897479,-8.914448613738104 1.69473807779948,-9 3,-8.914448613738106 4.305261922200513,-8.659258262890685 5.588190451025204,-8.23879532511287 6.826834323650893,-7.660254037844389 7.999999999999997,-6.933533402912354 9.087614290087203,-6.071067811865479 10.071067811865472,-5.087614290087209 10.93353340291235,-4.000000000000004 11.660254037844384,-2.826834323650903 12.238795325112864,-1.588190451025215 12.659258262890681,-0.305261922200525 12.914448613738102,1 13,2.305261922200513 12.914448613738106,3.588190451025203 12.659258262890685,4.826834323650892 12.23879532511287,5.999999999999993 11.66025403784439,7.087614290087199 10.933533402912357,8.071067811865474 10.071067811865477,8.93353340291235 9.08761429008721,9.660254037844384 8.000000000000004,10.238795325112864 6.826834323650904,10.659258262890681 5.588190451025216,10.914448613738102 4.305261922200526,11 3))")
+| EVAL np01_notbadcircle = ST_Simplify(geom, 0.1)
+| KEEP np01_notbadcircle
+;
+
+np01_notbadcircle:geo_shape
+ POLYGON((11.0 3.0,10.914448613738104 1.694738077799484,10.238795325112868 -0.826834323650898,8.933533402912353 -3.087614290087205,8.071067811865476 -4.071067811865475,6.000000000000001 -5.660254037844386,4.826834323650898 -6.238795325112868,2.305261922200517 -6.914448613738104,1.0 -7.0,-0.305261922200514 -6.914448613738106,-2.826834323650895 -6.238795325112868,-5.087614290087204 -4.933533402912354,-6.071067811865475 -4.071067811865476,-7.660254037844386 -2.000000000000004,-8.238795325112868 -0.826834323650899,-8.914448613738104 1.69473807779948,-9.0 3.0,-8.914448613738106 4.305261922200513,-8.23879532511287 6.826834323650893,-7.660254037844389 7.999999999999997,-6.071067811865479 10.071067811865472,-4.000000000000004 11.660254037844384,-2.826834323650903 12.238795325112864,-1.588190451025215 12.659258262890681,1.0 13.0,2.305261922200513 12.914448613738106,4.826834323650892 12.23879532511287,5.999999999999993 11.66025403784439,8.071067811865474 10.071067811865477,8.93353340291235 9.08761429008721,10.238795325112864 6.826834323650904,10.659258262890681 5.588190451025216,11.0 3.0))
+;
+
+stSimplifyPostGisSecondExample
+required_capability: st_simplify
+
+ROW geom = TO_GEOSHAPE("POLYGON((11 3,10.914448613738104 1.694738077799484,10.659258262890683 0.411809548974793,10.238795325112868 -0.826834323650898,9.660254037844387 -1.999999999999999,8.933533402912353 -3.087614290087205,8.071067811865476 -4.071067811865475,7.087614290087206 -4.933533402912351,6.000000000000001 -5.660254037844386,4.826834323650898 -6.238795325112868,3.58819045102521 -6.659258262890681,2.305261922200517 -6.914448613738104,1 -7,-0.305261922200514 -6.914448613738106,-1.588190451025206 -6.659258262890683,-2.826834323650895 -6.238795325112868,-3.999999999999998 -5.660254037844387,-5.087614290087204 -4.933533402912354,-6.071067811865475 -4.071067811865476,-6.93353340291235 -3.087614290087209,-7.660254037844386 -2.000000000000004,-8.238795325112868 -0.826834323650899,-8.659258262890681 0.41180954897479,-8.914448613738104 1.69473807779948,-9 3,-8.914448613738106 4.305261922200513,-8.659258262890685 5.588190451025204,-8.23879532511287 6.826834323650893,-7.660254037844389 7.999999999999997,-6.933533402912354 9.087614290087203,-6.071067811865479 10.071067811865472,-5.087614290087209 10.93353340291235,-4.000000000000004 11.660254037844384,-2.826834323650903 12.238795325112864,-1.588190451025215 12.659258262890681,-0.305261922200525 12.914448613738102,1 13,2.305261922200513 12.914448613738106,3.588190451025203 12.659258262890685,4.826834323650892 12.23879532511287,5.999999999999993 11.66025403784439,7.087614290087199 10.933533402912357,8.071067811865474 10.071067811865477,8.93353340291235 9.08761429008721,9.660254037844384 8.000000000000004,10.238795325112864 6.826834323650904,10.659258262890681 5.588190451025216,10.914448613738102 4.305261922200526,11 3))")
+| EVAL np05_notquitecircle = ST_Simplify(geom, 0.5)
+| KEEP np05_notquitecircle
+;
+
+np05_notquitecircle:geo_shape
+POLYGON((11 3,10.238795325112868 -0.826834323650898,8.071067811865476 -4.071067811865475,4.826834323650898 -6.238795325112868,1 -7,-2.826834323650895 -6.238795325112868,-6.071067811865475 -4.071067811865476,-8.238795325112868 -0.826834323650899,-9 3,-8.23879532511287 6.826834323650893,-6.071067811865479 10.071067811865472,-2.826834323650903 12.238795325112864,1 13,4.826834323650892 12.23879532511287,8.071067811865474 10.071067811865477,10.238795325112864 6.826834323650904,11 3))
+;
+
+stSimplifyPostGisThirdExample
+required_capability: st_simplify
+
+ROW geom = TO_GEOSHAPE("POLYGON((11 3,10.914448613738104 1.694738077799484,10.659258262890683 0.411809548974793,10.238795325112868 -0.826834323650898,9.660254037844387 -1.999999999999999,8.933533402912353 -3.087614290087205,8.071067811865476 -4.071067811865475,7.087614290087206 -4.933533402912351,6.000000000000001 -5.660254037844386,4.826834323650898 -6.238795325112868,3.58819045102521 -6.659258262890681,2.305261922200517 -6.914448613738104,1 -7,-0.305261922200514 -6.914448613738106,-1.588190451025206 -6.659258262890683,-2.826834323650895 -6.238795325112868,-3.999999999999998 -5.660254037844387,-5.087614290087204 -4.933533402912354,-6.071067811865475 -4.071067811865476,-6.93353340291235 -3.087614290087209,-7.660254037844386 -2.000000000000004,-8.238795325112868 -0.826834323650899,-8.659258262890681 0.41180954897479,-8.914448613738104 1.69473807779948,-9 3,-8.914448613738106 4.305261922200513,-8.659258262890685 5.588190451025204,-8.23879532511287 6.826834323650893,-7.660254037844389 7.999999999999997,-6.933533402912354 9.087614290087203,-6.071067811865479 10.071067811865472,-5.087614290087209 10.93353340291235,-4.000000000000004 11.660254037844384,-2.826834323650903 12.238795325112864,-1.588190451025215 12.659258262890681,-0.305261922200525 12.914448613738102,1 13,2.305261922200513 12.914448613738106,3.588190451025203 12.659258262890685,4.826834323650892 12.23879532511287,5.999999999999993 11.66025403784439,7.087614290087199 10.933533402912357,8.071067811865474 10.071067811865477,8.93353340291235 9.08761429008721,9.660254037844384 8.000000000000004,10.238795325112864 6.826834323650904,10.659258262890681 5.588190451025216,10.914448613738102 4.305261922200526,11 3))")
+| EVAL np1_octagon = ST_Simplify(geom, 1)
+| KEEP np1_octagon
+;
+
+np1_octagon:geo_shape
+POLYGON((11 3,8.071067811865476 -4.071067811865475,1 -7,-6.071067811865475 -4.071067811865476,-9 3,-6.071067811865479 10.071067811865472,1 13,8.071067811865474 10.071067811865477,11 3))
+;
+
+stSimplifyPostGisFourthExample
+required_capability: st_simplify
+
+ROW geom = TO_GEOSHAPE("POLYGON((11 3,10.914448613738104 1.694738077799484,10.659258262890683 0.411809548974793,10.238795325112868 -0.826834323650898,9.660254037844387 -1.999999999999999,8.933533402912353 -3.087614290087205,8.071067811865476 -4.071067811865475,7.087614290087206 -4.933533402912351,6.000000000000001 -5.660254037844386,4.826834323650898 -6.238795325112868,3.58819045102521 -6.659258262890681,2.305261922200517 -6.914448613738104,1 -7,-0.305261922200514 -6.914448613738106,-1.588190451025206 -6.659258262890683,-2.826834323650895 -6.238795325112868,-3.999999999999998 -5.660254037844387,-5.087614290087204 -4.933533402912354,-6.071067811865475 -4.071067811865476,-6.93353340291235 -3.087614290087209,-7.660254037844386 -2.000000000000004,-8.238795325112868 -0.826834323650899,-8.659258262890681 0.41180954897479,-8.914448613738104 1.69473807779948,-9 3,-8.914448613738106 4.305261922200513,-8.659258262890685 5.588190451025204,-8.23879532511287 6.826834323650893,-7.660254037844389 7.999999999999997,-6.933533402912354 9.087614290087203,-6.071067811865479 10.071067811865472,-5.087614290087209 10.93353340291235,-4.000000000000004 11.660254037844384,-2.826834323650903 12.238795325112864,-1.588190451025215 12.659258262890681,-0.305261922200525 12.914448613738102,1 13,2.305261922200513 12.914448613738106,3.588190451025203 12.659258262890685,4.826834323650892 12.23879532511287,5.999999999999993 11.66025403784439,7.087614290087199 10.933533402912357,8.071067811865474 10.071067811865477,8.93353340291235 9.08761429008721,9.660254037844384 8.000000000000004,10.238795325112864 6.826834323650904,10.659258262890681 5.588190451025216,10.914448613738102 4.305261922200526,11 3))")
+| EVAL np100_geometrygoesaway = ST_Simplify(geom, 100)
+| KEEP np100_geometrygoesaway
+;
+
+np100_geometrygoesaway:geo_shape
+POLYGON EMPTY
+;
+
+stSimplifyPostGisFifthExample
+required_capability: st_simplify
+
+ROW geom = TO_CARTESIANSHAPE("MULTILINESTRING ((20 180, 20 150, 50 150, 50 100, 110 150, 150 140, 170 120), (20 10, 80 30, 90 120), (90 120, 130 130), (130 130, 130 70, 160 40, 180 60, 180 90, 140 80), (50 40, 70 40, 80 70, 70 60, 60 60, 50 50, 50 40))")
+| EVAL result = ST_Simplify(geom, 40)
+| KEEP result
+;
+
+result:cartesian_shape
+MULTILINESTRING((20 180,50 100,170 120),(20 10,90 120),(90 120,130 130),(130 130,160 40,180 90,140 80),(50 40,80 70,50 40))
+;
+
+stSimplifyPostGisSixthExample
+required_capability: st_simplify
+
+ROW geom = TO_CARTESIANSHAPE("MULTIPOLYGON (((90 110, 80 180, 50 160, 10 170, 10 140, 20 110, 90 110)), ((40 80, 100 100, 120 160, 170 180, 190 70, 140 10, 110 40, 60 40, 40 80), (180 70, 170 110, 142.5 128.5, 128.5 77.5, 90 60, 180 70)))")
+| EVAL result = ST_Simplify(geom, 40)
+| KEEP result
+;
+
+result:cartesian_shape
+POLYGON ((40.0 80.0, 79.0 110.0, 20.0 110.0, 10.0 170.0, 80.0 180.0, 88.91089108910892 117.62376237623762, 170.0 180.0, 156.93726937269372 105.97785977859779, 142.5 128.5, 90.0 60.0, 150.0 66.66666666666667, 140.0 10.0, 40.0 80.0))
+;
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableCartesianPointDocValuesAndFoldableToleranceEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableCartesianPointDocValuesAndFoldableToleranceEvaluator.java
new file mode 100644
index 0000000000000..f78e806d5de8b
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableCartesianPointDocValuesAndFoldableToleranceEvaluator.java
@@ -0,0 +1,129 @@
+// 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.esql.expression.function.scalar.spatial;
+
+import java.io.IOException;
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StSimplify}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class StSimplifyNonFoldableCartesianPointDocValuesAndFoldableToleranceEvaluator implements EvalOperator.ExpressionEvaluator {
+ private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(StSimplifyNonFoldableCartesianPointDocValuesAndFoldableToleranceEvaluator.class);
+
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator left;
+
+ private final double tolerance;
+
+ private final DriverContext driverContext;
+
+ private Warnings warnings;
+
+ public StSimplifyNonFoldableCartesianPointDocValuesAndFoldableToleranceEvaluator(Source source,
+ EvalOperator.ExpressionEvaluator left, double tolerance, DriverContext driverContext) {
+ this.source = source;
+ this.left = left;
+ this.tolerance = tolerance;
+ this.driverContext = driverContext;
+ }
+
+ @Override
+ public Block eval(Page page) {
+ try (LongBlock leftBlock = (LongBlock) left.eval(page)) {
+ return eval(page.getPositionCount(), leftBlock);
+ }
+ }
+
+ @Override
+ public long baseRamBytesUsed() {
+ long baseRamBytesUsed = BASE_RAM_BYTES_USED;
+ baseRamBytesUsed += left.baseRamBytesUsed();
+ return baseRamBytesUsed;
+ }
+
+ public BytesRefBlock eval(int positionCount, LongBlock leftBlock) {
+ try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ boolean allBlocksAreNulls = true;
+ if (!leftBlock.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (allBlocksAreNulls) {
+ result.appendNull();
+ continue position;
+ }
+ try {
+ StSimplify.processCartesianPointDocValuesAndConstantTolerance(result, p, leftBlock, this.tolerance);
+ } catch (IllegalArgumentException | IOException e) {
+ warnings().registerException(e);
+ result.appendNull();
+ }
+ }
+ return result.build();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "StSimplifyNonFoldableCartesianPointDocValuesAndFoldableToleranceEvaluator[" + "left=" + left + ", tolerance=" + tolerance + "]";
+ }
+
+ @Override
+ public void close() {
+ Releasables.closeExpectNoException(left);
+ }
+
+ private Warnings warnings() {
+ if (warnings == null) {
+ this.warnings = Warnings.createWarnings(
+ driverContext.warningsMode(),
+ source.source().getLineNumber(),
+ source.source().getColumnNumber(),
+ source.text()
+ );
+ }
+ return warnings;
+ }
+
+ static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator.Factory left;
+
+ private final double tolerance;
+
+ public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory left, double tolerance) {
+ this.source = source;
+ this.left = left;
+ this.tolerance = tolerance;
+ }
+
+ @Override
+ public StSimplifyNonFoldableCartesianPointDocValuesAndFoldableToleranceEvaluator get(
+ DriverContext context) {
+ return new StSimplifyNonFoldableCartesianPointDocValuesAndFoldableToleranceEvaluator(source, left.get(context), tolerance, context);
+ }
+
+ @Override
+ public String toString() {
+ return "StSimplifyNonFoldableCartesianPointDocValuesAndFoldableToleranceEvaluator[" + "left=" + left + ", tolerance=" + tolerance + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableGeoPointDocValuesAndFoldableToleranceEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableGeoPointDocValuesAndFoldableToleranceEvaluator.java
new file mode 100644
index 0000000000000..1608f4cc0fa1c
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableGeoPointDocValuesAndFoldableToleranceEvaluator.java
@@ -0,0 +1,130 @@
+// 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.esql.expression.function.scalar.spatial;
+
+import java.io.IOException;
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StSimplify}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class StSimplifyNonFoldableGeoPointDocValuesAndFoldableToleranceEvaluator implements EvalOperator.ExpressionEvaluator {
+ private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(StSimplifyNonFoldableGeoPointDocValuesAndFoldableToleranceEvaluator.class);
+
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator point;
+
+ private final double tolerance;
+
+ private final DriverContext driverContext;
+
+ private Warnings warnings;
+
+ public StSimplifyNonFoldableGeoPointDocValuesAndFoldableToleranceEvaluator(Source source,
+ EvalOperator.ExpressionEvaluator point, double tolerance, DriverContext driverContext) {
+ this.source = source;
+ this.point = point;
+ this.tolerance = tolerance;
+ this.driverContext = driverContext;
+ }
+
+ @Override
+ public Block eval(Page page) {
+ try (LongBlock pointBlock = (LongBlock) point.eval(page)) {
+ return eval(page.getPositionCount(), pointBlock);
+ }
+ }
+
+ @Override
+ public long baseRamBytesUsed() {
+ long baseRamBytesUsed = BASE_RAM_BYTES_USED;
+ baseRamBytesUsed += point.baseRamBytesUsed();
+ return baseRamBytesUsed;
+ }
+
+ public BytesRefBlock eval(int positionCount, LongBlock pointBlock) {
+ try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ boolean allBlocksAreNulls = true;
+ if (!pointBlock.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (allBlocksAreNulls) {
+ result.appendNull();
+ continue position;
+ }
+ try {
+ StSimplify.processGeoPointDocValuesAndConstantTolerance(result, p, pointBlock, this.tolerance);
+ } catch (IllegalArgumentException | IOException e) {
+ warnings().registerException(e);
+ result.appendNull();
+ }
+ }
+ return result.build();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "StSimplifyNonFoldableGeoPointDocValuesAndFoldableToleranceEvaluator[" + "point=" + point + ", tolerance=" + tolerance + "]";
+ }
+
+ @Override
+ public void close() {
+ Releasables.closeExpectNoException(point);
+ }
+
+ private Warnings warnings() {
+ if (warnings == null) {
+ this.warnings = Warnings.createWarnings(
+ driverContext.warningsMode(),
+ source.source().getLineNumber(),
+ source.source().getColumnNumber(),
+ source.text()
+ );
+ }
+ return warnings;
+ }
+
+ static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator.Factory point;
+
+ private final double tolerance;
+
+ public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory point,
+ double tolerance) {
+ this.source = source;
+ this.point = point;
+ this.tolerance = tolerance;
+ }
+
+ @Override
+ public StSimplifyNonFoldableGeoPointDocValuesAndFoldableToleranceEvaluator get(
+ DriverContext context) {
+ return new StSimplifyNonFoldableGeoPointDocValuesAndFoldableToleranceEvaluator(source, point.get(context), tolerance, context);
+ }
+
+ @Override
+ public String toString() {
+ return "StSimplifyNonFoldableGeoPointDocValuesAndFoldableToleranceEvaluator[" + "point=" + point + ", tolerance=" + tolerance + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableGeometryAndFoldableToleranceEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableGeometryAndFoldableToleranceEvaluator.java
new file mode 100644
index 0000000000000..72fcae95c4930
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableGeometryAndFoldableToleranceEvaluator.java
@@ -0,0 +1,127 @@
+// 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.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.compute.operator.Warnings;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StSimplify}.
+ * This class is generated. Edit {@code EvaluatorImplementer} instead.
+ */
+public final class StSimplifyNonFoldableGeometryAndFoldableToleranceEvaluator implements EvalOperator.ExpressionEvaluator {
+ private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(StSimplifyNonFoldableGeometryAndFoldableToleranceEvaluator.class);
+
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator geometry;
+
+ private final double tolerance;
+
+ private final DriverContext driverContext;
+
+ private Warnings warnings;
+
+ public StSimplifyNonFoldableGeometryAndFoldableToleranceEvaluator(Source source,
+ EvalOperator.ExpressionEvaluator geometry, double tolerance, DriverContext driverContext) {
+ this.source = source;
+ this.geometry = geometry;
+ this.tolerance = tolerance;
+ this.driverContext = driverContext;
+ }
+
+ @Override
+ public Block eval(Page page) {
+ try (BytesRefBlock geometryBlock = (BytesRefBlock) geometry.eval(page)) {
+ return eval(page.getPositionCount(), geometryBlock);
+ }
+ }
+
+ @Override
+ public long baseRamBytesUsed() {
+ long baseRamBytesUsed = BASE_RAM_BYTES_USED;
+ baseRamBytesUsed += geometry.baseRamBytesUsed();
+ return baseRamBytesUsed;
+ }
+
+ public BytesRefBlock eval(int positionCount, BytesRefBlock geometryBlock) {
+ try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+ position: for (int p = 0; p < positionCount; p++) {
+ boolean allBlocksAreNulls = true;
+ if (!geometryBlock.isNull(p)) {
+ allBlocksAreNulls = false;
+ }
+ if (allBlocksAreNulls) {
+ result.appendNull();
+ continue position;
+ }
+ try {
+ StSimplify.processNonFoldableGeometryAndConstantTolerance(result, p, geometryBlock, this.tolerance);
+ } catch (IllegalArgumentException e) {
+ warnings().registerException(e);
+ result.appendNull();
+ }
+ }
+ return result.build();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "StSimplifyNonFoldableGeometryAndFoldableToleranceEvaluator[" + "geometry=" + geometry + ", tolerance=" + tolerance + "]";
+ }
+
+ @Override
+ public void close() {
+ Releasables.closeExpectNoException(geometry);
+ }
+
+ private Warnings warnings() {
+ if (warnings == null) {
+ this.warnings = Warnings.createWarnings(
+ driverContext.warningsMode(),
+ source.source().getLineNumber(),
+ source.source().getColumnNumber(),
+ source.text()
+ );
+ }
+ return warnings;
+ }
+
+ static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator.Factory geometry;
+
+ private final double tolerance;
+
+ public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory geometry,
+ double tolerance) {
+ this.source = source;
+ this.geometry = geometry;
+ this.tolerance = tolerance;
+ }
+
+ @Override
+ public StSimplifyNonFoldableGeometryAndFoldableToleranceEvaluator get(DriverContext context) {
+ return new StSimplifyNonFoldableGeometryAndFoldableToleranceEvaluator(source, geometry.get(context), tolerance, context);
+ }
+
+ @Override
+ public String toString() {
+ return "StSimplifyNonFoldableGeometryAndFoldableToleranceEvaluator[" + "geometry=" + geometry + ", tolerance=" + tolerance + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
index fdd49945e9e66..4fd58b81fd942 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
@@ -78,6 +78,11 @@ public enum Cap {
*/
ST_DISJOINT,
+ /**
+ * Support for spatial simplification {@code ST_SIMPLIFY}
+ */
+ ST_SIMPLIFY,
+
/**
* The introduction of the {@code VALUES} agg.
*/
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
index a17c40d3b30fe..a6a3b7fffeb4d 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
@@ -74,6 +74,7 @@
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeohash;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeohex;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeotile;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StSimplify;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMax;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMin;
@@ -263,7 +264,8 @@ private static List spatials() {
StDistance.ENTRY,
StGeohash.ENTRY,
StGeotile.ENTRY,
- StGeohex.ENTRY
+ StGeohex.ENTRY,
+ StSimplify.ENTRY
);
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
index 5a566f9b68808..e259dcaa17073 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
@@ -184,6 +184,7 @@
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeohash;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeohex;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeotile;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StSimplify;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMax;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMin;
@@ -464,6 +465,7 @@ private static FunctionDefinition[][] functions() {
def(SpatialWithin.class, SpatialWithin::new, "st_within"),
def(StDistance.class, StDistance::new, "st_distance"),
def(StEnvelope.class, StEnvelope::new, "st_envelope"),
+ def(StSimplify.class, StSimplify::new, "st_simplify"),
def(StGeohash.class, StGeohash::new, "st_geohash"),
def(StGeotile.class, StGeotile::new, "st_geotile"),
def(StGeohex.class, StGeohex::new, "st_geohex"),
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDocValuesFunction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDocValuesFunction.java
new file mode 100644
index 0000000000000..407d7bb0cf5cc
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDocValuesFunction.java
@@ -0,0 +1,56 @@
+/*
+ * 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.esql.expression.function.scalar.spatial;
+
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Spatial functions that can take doc values as an argument can inherit from this class.
+ * Examples: StGeohash, StGeotile, StGeohex and StSimplify
+ */
+public abstract class SpatialDocValuesFunction extends EsqlScalarFunction {
+ protected final boolean spatialDocValues;
+
+ protected SpatialDocValuesFunction(Source source, List expressions, boolean spatialDocValues) {
+ super(source, expressions);
+ this.spatialDocValues = spatialDocValues;
+ }
+
+ /**
+ * Mark the function as expecting the specified field to arrive as doc-values.
+ * This only applies to geo_point and cartesian_point types.
+ */
+ public abstract SpatialDocValuesFunction withDocValues(boolean useDocValues);
+
+ @Override
+ public int hashCode() {
+ // NB: the hashcode is currently used for key generation, so to avoid clashes
+ // between aggs with the same arguments, add the class name as variation
+ return Objects.hash(getClass(), children(), spatialDocValues);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (super.equals(obj)) {
+ SpatialDocValuesFunction other = (SpatialDocValuesFunction) obj;
+ return Objects.equals(other.children(), children()) && Objects.equals(other.spatialDocValues, spatialDocValues);
+ }
+ return false;
+ }
+
+ public abstract Expression spatialField();
+
+ public boolean spatialDocValues() {
+ return spatialDocValues;
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialGridFunction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialGridFunction.java
index b1421d25340f7..eb6d94d2a568c 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialGridFunction.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialGridFunction.java
@@ -20,7 +20,6 @@
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.xpack.esql.LicenseAware;
import org.elasticsearch.xpack.esql.core.expression.Expression;
-import org.elasticsearch.xpack.esql.core.expression.function.scalar.ScalarFunction;
import org.elasticsearch.xpack.esql.core.tree.Source;
import org.elasticsearch.xpack.esql.expression.function.OptionalArgument;
import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
@@ -29,7 +28,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.Objects;
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST;
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND;
@@ -44,33 +42,35 @@
* Spatial functions that take one spatial argument, one parameter and one optional bounds can inherit from this class.
* Obvious choices are: StGeohash, StGeotile and StGeohex.
*/
-public abstract class SpatialGridFunction extends ScalarFunction implements OptionalArgument, LicenseAware {
+public abstract class SpatialGridFunction extends SpatialDocValuesFunction implements OptionalArgument, LicenseAware {
protected final Expression spatialField;
protected final Expression parameter;
protected final Expression bounds;
- protected final boolean spatialDocsValues;
protected SpatialGridFunction(
Source source,
Expression spatialField,
Expression parameter,
Expression bounds,
- boolean spatialDocsValues
+ boolean spatialDocValues
) {
- super(source, bounds == null ? Arrays.asList(spatialField, parameter) : Arrays.asList(spatialField, parameter, bounds));
+ super(
+ source,
+ bounds == null ? Arrays.asList(spatialField, parameter) : Arrays.asList(spatialField, parameter, bounds),
+ spatialDocValues
+ );
this.spatialField = spatialField;
this.parameter = parameter;
this.bounds = bounds;
- this.spatialDocsValues = spatialDocsValues;
}
- protected SpatialGridFunction(StreamInput in, boolean spatialDocsValues) throws IOException {
+ protected SpatialGridFunction(StreamInput in, boolean spatialDocValues) throws IOException {
this(
Source.readFrom((PlanStreamInput) in),
in.readNamedWriteable(Expression.class),
in.readNamedWriteable(Expression.class),
in.readOptionalNamedWriteable(Expression.class),
- spatialDocsValues
+ spatialDocValues
);
}
@@ -90,12 +90,6 @@ public boolean licenseCheck(XPackLicenseState state) {
};
}
- /**
- * Mark the function as expecting the specified field to arrive as doc-values.
- * This only applies to geo_point and cartesian_point types.
- */
- public abstract SpatialGridFunction withDocValues(boolean useDocValues);
-
@Override
protected TypeResolution resolveType() {
if (childrenResolved() == false) {
@@ -152,26 +146,6 @@ protected static GeoBoundingBox asGeoBoundingBox(Rectangle rectangle) {
);
}
- @Override
- public int hashCode() {
- // NB: the hashcode is currently used for key generation so
- // to avoid clashes between aggs with the same arguments, add the class name as variation
- return Objects.hash(getClass(), children(), spatialDocsValues);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (super.equals(obj)) {
- SpatialGridFunction other = (SpatialGridFunction) obj;
- return Objects.equals(other.children(), children()) && Objects.equals(other.spatialDocsValues, spatialDocsValues);
- }
- return false;
- }
-
- public boolean spatialDocsValues() {
- return spatialDocsValues;
- }
-
@Override
public final SpatialGridFunction replaceChildren(List newChildren) {
Expression newSpatialField = newChildren.get(0);
@@ -187,6 +161,7 @@ public final SpatialGridFunction replaceChildren(List newChildren) {
protected abstract SpatialGridFunction replaceChildren(Expression newSpatialField, Expression newParameter, Expression newBounds);
+ @Override
public Expression spatialField() {
return spatialField;
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StGeohash.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StGeohash.java
index b8f87ae23b90b..1114391b7646a 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StGeohash.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StGeohash.java
@@ -154,7 +154,7 @@ private StGeohash(StreamInput in) throws IOException {
@Override
public SpatialGridFunction withDocValues(boolean useDocValues) {
// Only update the docValues flags if the field is found in the attributes
- boolean docValues = this.spatialDocsValues || useDocValues;
+ boolean docValues = this.spatialDocValues || useDocValues;
return new StGeohash(source(), spatialField, parameter, bounds, docValues);
}
@@ -190,7 +190,7 @@ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvalua
GeoBoundingBox bbox = asGeoBoundingBox(bounds.fold(toEvaluator.foldCtx()));
int precision = (int) parameter.fold(toEvaluator.foldCtx());
GeoHashBoundedGrid.Factory bounds = new GeoHashBoundedGrid.Factory(precision, bbox);
- return spatialDocsValues
+ return spatialDocValues
? new StGeohashFromFieldDocValuesAndLiteralAndLiteralEvaluator.Factory(
source(),
toEvaluator.apply(spatialField()),
@@ -199,7 +199,7 @@ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvalua
: new StGeohashFromFieldAndLiteralAndLiteralEvaluator.Factory(source(), toEvaluator.apply(spatialField), bounds::get);
} else {
int precision = checkPrecisionRange((int) parameter.fold(toEvaluator.foldCtx()));
- return spatialDocsValues
+ return spatialDocValues
? new StGeohashFromFieldDocValuesAndLiteralEvaluator.Factory(source(), toEvaluator.apply(spatialField()), precision)
: new StGeohashFromFieldAndLiteralEvaluator.Factory(source(), toEvaluator.apply(spatialField), precision);
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StGeohex.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StGeohex.java
index 08adbde76e0d5..738eb2a2cf6c1 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StGeohex.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StGeohex.java
@@ -160,7 +160,7 @@ public boolean licenseCheck(XPackLicenseState state) {
@Override
public SpatialGridFunction withDocValues(boolean useDocValues) {
// Only update the docValues flags if the field is found in the attributes
- boolean docValues = this.spatialDocsValues || useDocValues;
+ boolean docValues = this.spatialDocValues || useDocValues;
return new StGeohex(source(), spatialField, parameter, bounds, docValues);
}
@@ -196,7 +196,7 @@ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvalua
GeoBoundingBox bbox = asGeoBoundingBox(bounds.fold(toEvaluator.foldCtx()));
int precision = (int) parameter.fold(toEvaluator.foldCtx());
GeoHexBoundedGrid.Factory bounds = new GeoHexBoundedGrid.Factory(precision, bbox);
- return spatialDocsValues
+ return spatialDocValues
? new StGeohexFromFieldDocValuesAndLiteralAndLiteralEvaluator.Factory(
source(),
toEvaluator.apply(spatialField()),
@@ -205,7 +205,7 @@ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvalua
: new StGeohexFromFieldAndLiteralAndLiteralEvaluator.Factory(source(), toEvaluator.apply(spatialField), bounds::get);
} else {
int precision = checkPrecisionRange((int) parameter.fold(toEvaluator.foldCtx()));
- return spatialDocsValues
+ return spatialDocValues
? new StGeohexFromFieldDocValuesAndLiteralEvaluator.Factory(source(), toEvaluator.apply(spatialField()), precision)
: new StGeohexFromFieldAndLiteralEvaluator.Factory(source(), toEvaluator.apply(spatialField), precision);
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StGeotile.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StGeotile.java
index 3e12ecef07d07..4a0cf3beafdef 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StGeotile.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StGeotile.java
@@ -151,7 +151,7 @@ private StGeotile(StreamInput in) throws IOException {
@Override
public SpatialGridFunction withDocValues(boolean useDocValues) {
// Only update the docValues flags if the field is found in the attributes
- boolean docValues = this.spatialDocsValues || useDocValues;
+ boolean docValues = this.spatialDocValues || useDocValues;
return new StGeotile(source(), spatialField, parameter, bounds, docValues);
}
@@ -187,7 +187,7 @@ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvalua
GeoBoundingBox bbox = asGeoBoundingBox(bounds.fold(toEvaluator.foldCtx()));
int precision = (int) parameter.fold(toEvaluator.foldCtx());
GeoTileBoundedGrid.Factory bounds = new GeoTileBoundedGrid.Factory(precision, bbox);
- return spatialDocsValues
+ return spatialDocValues
? new StGeotileFromFieldDocValuesAndLiteralAndLiteralEvaluator.Factory(
source(),
toEvaluator.apply(spatialField()),
@@ -196,7 +196,7 @@ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvalua
: new StGeotileFromFieldAndLiteralAndLiteralEvaluator.Factory(source(), toEvaluator.apply(spatialField), bounds::get);
} else {
int precision = checkPrecisionRange((int) parameter.fold(toEvaluator.foldCtx()));
- return spatialDocsValues
+ return spatialDocValues
? new StGeotileFromFieldDocValuesAndLiteralEvaluator.Factory(source(), toEvaluator.apply(spatialField()), precision)
: new StGeotileFromFieldAndLiteralEvaluator.Factory(source(), toEvaluator.apply(spatialField), precision);
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java
new file mode 100644
index 0000000000000..22cf41657c627
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java
@@ -0,0 +1,320 @@
+/*
+ * 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.esql.expression.function.scalar.spatial;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+import org.elasticsearch.compute.ann.Position;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.FoldContext;
+import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes;
+import org.elasticsearch.xpack.esql.expression.function.Example;
+import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo;
+import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.io.ParseException;
+import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.function.Function;
+
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.CARTESIAN;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.GEO;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED;
+
+public class StSimplify extends SpatialDocValuesFunction {
+ public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
+ Expression.class,
+ "StSimplify",
+ StSimplify::new
+ );
+ private static final BlockProcessor processor = new BlockProcessor(UNSPECIFIED);
+ private static final BlockProcessor geoProcessor = new BlockProcessor(GEO);
+ private static final BlockProcessor cartesianProcessor = new BlockProcessor(CARTESIAN);
+ private final Expression geometry;
+ private final Expression tolerance;
+
+ @FunctionInfo(
+ returnType = { "geo_point", "geo_shape", "cartesian_point", "cartesian_shape" },
+ description = "Simplifies the input geometry by applying the Douglas-Peucker algorithm with a specified tolerance. "
+ + "Vertices that fall within the tolerance distance from the simplified shape are removed. "
+ + "Note that the resulting geometry may be invalid, even if the original input was valid.",
+ preview = true,
+ appliesTo = { @FunctionAppliesTo(lifeCycle = FunctionAppliesToLifecycle.PREVIEW, version = "9.4.0") },
+ examples = @Example(file = "spatial-jts", tag = "st_simplify")
+ )
+ public StSimplify(
+ Source source,
+ @Param(
+ name = "geometry",
+ type = { "geo_point", "geo_shape", "cartesian_point", "cartesian_shape" },
+ description = "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. "
+ + "If `null`, the function returns `null`."
+ ) Expression geometry,
+ @Param(
+ name = "tolerance",
+ type = { "double" },
+ description = "Tolerance for the geometry simplification, in the units of the input SRS"
+ ) Expression tolerance
+ ) {
+ this(source, geometry, tolerance, false);
+ }
+
+ private StSimplify(Source source, Expression geometry, Expression tolerance, boolean spatialDocValues) {
+ super(source, List.of(geometry, tolerance), spatialDocValues);
+ this.geometry = geometry;
+ this.tolerance = tolerance;
+ }
+
+ private StSimplify(StreamInput in) throws IOException {
+ this(Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class), in.readNamedWriteable(Expression.class));
+ }
+
+ @Override
+ public DataType dataType() {
+ return tolerance.dataType() == DataType.NULL ? DataType.NULL : geometry.dataType();
+ }
+
+ @Override
+ public Expression replaceChildren(List newChildren) {
+ return new StSimplify(source(), newChildren.get(0), newChildren.get(1));
+ }
+
+ @Override
+ protected NodeInfo extends Expression> info() {
+ return NodeInfo.create(this, StSimplify::new, geometry, tolerance);
+ }
+
+ @Override
+ public String getWriteableName() {
+ return ENTRY.name;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ source().writeTo(out);
+ out.writeNamedWriteable(geometry);
+ out.writeNamedWriteable(tolerance);
+ }
+
+ @Override
+ public SpatialDocValuesFunction withDocValues(boolean useDocValues) {
+ return new StSimplify(source(), geometry, tolerance, true);
+ }
+
+ @Override
+ public Expression spatialField() {
+ return geometry;
+ }
+
+ @Override
+ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+ EvalOperator.ExpressionEvaluator.Factory geometryEvaluator = toEvaluator.apply(geometry);
+
+ if (tolerance.foldable() == false) {
+ throw new IllegalArgumentException("tolerance must be foldable");
+ }
+ var toleranceExpression = tolerance.fold(toEvaluator.foldCtx());
+ double inputTolerance = getInputTolerance(toleranceExpression);
+ if (spatialDocValues && geometry.dataType() == DataType.GEO_POINT) {
+ return new StSimplifyNonFoldableGeoPointDocValuesAndFoldableToleranceEvaluator.Factory(
+ source(),
+ geometryEvaluator,
+ inputTolerance
+ );
+ } else if (spatialDocValues && geometry.dataType() == DataType.CARTESIAN_POINT) {
+ return new StSimplifyNonFoldableCartesianPointDocValuesAndFoldableToleranceEvaluator.Factory(
+ source(),
+ geometryEvaluator,
+ inputTolerance
+ );
+ }
+ return new StSimplifyNonFoldableGeometryAndFoldableToleranceEvaluator.Factory(source(), geometryEvaluator, inputTolerance);
+ }
+
+ @Override
+ public boolean foldable() {
+ return geometry.foldable() && tolerance.foldable();
+ }
+
+ @Override
+ public Object fold(FoldContext foldCtx) {
+ var toleranceExpression = tolerance.fold(foldCtx);
+ double inputTolerance = getInputTolerance(toleranceExpression);
+ Object input = geometry.fold(foldCtx);
+ if (input instanceof List> list) {
+ return processor.processSingleGeometry(processor.asJtsGeometry(list), inputTolerance);
+ } else if (input instanceof BytesRef inputGeometry) {
+ return processor.processSingleGeometry(inputGeometry, inputTolerance);
+ } else {
+ throw new IllegalArgumentException("unsupported block type: " + input.getClass().getSimpleName());
+ }
+ }
+
+ @Evaluator(extraName = "NonFoldableGeometryAndFoldableTolerance", warnExceptions = { IllegalArgumentException.class })
+ static void processNonFoldableGeometryAndConstantTolerance(
+ BytesRefBlock.Builder builder,
+ @Position int p,
+ BytesRefBlock geometry,
+ @Fixed double tolerance
+ ) {
+ processor.processGeometries(builder, p, geometry, tolerance);
+ }
+
+ @Evaluator(
+ extraName = "NonFoldableGeoPointDocValuesAndFoldableTolerance",
+ warnExceptions = { IllegalArgumentException.class, IOException.class }
+ )
+ static void processGeoPointDocValuesAndConstantTolerance(
+ BytesRefBlock.Builder builder,
+ @Position int p,
+ LongBlock point,
+ @Fixed double tolerance
+ ) throws IOException {
+ geoProcessor.processPoints(builder, p, point, tolerance);
+ }
+
+ private static double getInputTolerance(Object toleranceExpression) {
+ double inputTolerance;
+
+ if (toleranceExpression instanceof Number number) {
+ inputTolerance = number.doubleValue();
+ } else {
+ throw new IllegalArgumentException("tolerance for st_simplify must be an integer or floating-point number");
+ }
+
+ if (inputTolerance < 0) {
+ throw new IllegalArgumentException("tolerance must not be negative");
+ }
+ return inputTolerance;
+ }
+
+ @Evaluator(
+ extraName = "NonFoldableCartesianPointDocValuesAndFoldableTolerance",
+ warnExceptions = { IllegalArgumentException.class, IOException.class }
+ )
+ static void processCartesianPointDocValuesAndConstantTolerance(
+ BytesRefBlock.Builder builder,
+ @Position int p,
+ LongBlock left,
+ @Fixed double tolerance
+ ) throws IOException {
+ cartesianProcessor.processPoints(builder, p, left, tolerance);
+ }
+
+ private static class BlockProcessor {
+ private final SpatialCoordinateTypes spatialCoordinateType;
+ private final GeometryFactory geometryFactory = new GeometryFactory();
+
+ BlockProcessor(SpatialCoordinateTypes spatialCoordinateType) {
+ this.spatialCoordinateType = spatialCoordinateType;
+ }
+
+ private BytesRef processSingleGeometry(BytesRef inputGeometry, double inputTolerance) {
+ if (inputGeometry == null) {
+ return null;
+ }
+ try {
+ return processSingleGeometry(UNSPECIFIED.wkbToJtsGeometry(inputGeometry), inputTolerance);
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("could not parse the geometry expression: " + e);
+ }
+ }
+
+ private BytesRef processSingleGeometry(Geometry jtsGeometry, double inputTolerance) {
+ Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, inputTolerance);
+ return UNSPECIFIED.jtsGeometryToWkb(simplifiedGeometry);
+ }
+
+ private void processPoints(BytesRefBlock.Builder builder, int p, LongBlock left, double tolerance) throws IOException {
+ if (left.getValueCount(p) < 1) {
+ builder.appendNull();
+ } else {
+ final Geometry jtsGeometry = asJtsMultiPoint(left, p, spatialCoordinateType::longAsPoint);
+ Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, tolerance);
+ builder.appendBytesRef(UNSPECIFIED.jtsGeometryToWkb(simplifiedGeometry));
+ }
+ }
+
+ private void processGeometries(BytesRefBlock.Builder builder, int p, BytesRefBlock left, double tolerance) {
+ if (left.getValueCount(p) < 1) {
+ builder.appendNull();
+ } else {
+ final Geometry jtsGeometry = asJtsGeometry(left, p);
+ Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, tolerance);
+ builder.appendBytesRef(UNSPECIFIED.jtsGeometryToWkb(simplifiedGeometry));
+ }
+ }
+
+ private Geometry asJtsMultiPoint(LongBlock valueBlock, int position, Function decoder) {
+ final int firstValueIndex = valueBlock.getFirstValueIndex(position);
+ final int valueCount = valueBlock.getValueCount(position);
+ if (valueCount == 1) {
+ Point point = decoder.apply(valueBlock.getLong(firstValueIndex));
+ return geometryFactory.createPoint(new Coordinate(point.getX(), point.getY()));
+ }
+ final Coordinate[] coordinates = new Coordinate[valueCount];
+ for (int i = 0; i < valueCount; i++) {
+ Point point = decoder.apply(valueBlock.getLong(firstValueIndex + i));
+ coordinates[i] = new Coordinate(point.getX(), point.getY());
+ }
+ return geometryFactory.createMultiPointFromCoords(coordinates);
+ }
+
+ private Geometry asJtsGeometry(BytesRefBlock valueBlock, int position) {
+ try {
+ final int firstValueIndex = valueBlock.getFirstValueIndex(position);
+ final int valueCount = valueBlock.getValueCount(position);
+ BytesRef scratch = new BytesRef();
+ if (valueCount == 1) {
+ return UNSPECIFIED.wkbToJtsGeometry(valueBlock.getBytesRef(firstValueIndex, scratch));
+ }
+ final Geometry[] geometries = new Geometry[valueCount];
+ for (int i = 0; i < valueCount; i++) {
+ geometries[i] = UNSPECIFIED.wkbToJtsGeometry(valueBlock.getBytesRef(firstValueIndex, scratch));
+ }
+ return geometryFactory.createGeometryCollection(geometries);
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("could not parse the geometry expression: " + e);
+ }
+ }
+
+ private Geometry asJtsGeometry(List> values) {
+ try {
+ final Geometry[] geometries = new Geometry[values.size()];
+ for (int i = 0; i < values.size(); i++) {
+ if (values.get(i) instanceof BytesRef inputGeometry) {
+ geometries[i] = UNSPECIFIED.wkbToJtsGeometry(inputGeometry);
+ } else {
+ throw new IllegalArgumentException("unsupported list element type: " + values.get(i).getClass().getSimpleName());
+ }
+ }
+ return geometryFactory.createGeometryCollection(geometries);
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("could not parse the geometry expression: " + e);
+ }
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/SpatialDocValuesExtraction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/SpatialDocValuesExtraction.java
index e6b848540c355..c2f1e7ba44e60 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/SpatialDocValuesExtraction.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/SpatialDocValuesExtraction.java
@@ -16,7 +16,7 @@
import org.elasticsearch.xpack.esql.core.type.DataType;
import org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialAggregateFunction;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.BinarySpatialFunction;
-import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialGridFunction;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialDocValuesFunction;
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesFunction;
import org.elasticsearch.xpack.esql.optimizer.LocalPhysicalOptimizerContext;
import org.elasticsearch.xpack.esql.optimizer.PhysicalOptimizerRules;
@@ -37,12 +37,12 @@
/**
* This rule is responsible for marking spatial fields to be extracted from doc-values instead of source values.
* This is a very specific optimization that is only used in the context of spatial aggregations.
- * Normally spatial fields are extracted from source values because this maintains original precision, but is very slow.
- * Simply loading from doc-values loses precision for points, and loses the geometry topological information for shapes.
+ * Normally spatial fields are extracted from source values because this maintains original precision but is very slow.
+ * Simply loading from doc-values loses precision for points and loses the geometry topological information for shapes.
* For this reason we only consider loading from doc values under very specific conditions:
*
*
The spatial data is consumed by a spatial aggregation (eg. ST_CENTROIDS_AGG, negating the need for precision.
- *
This aggregation is planned to run on the data node, so the doc-values Blocks are never transmit to the coordinator node.
+ *
This aggregation is planned to run on the data node, so the doc-values Blocks are never transmitted to the coordinator node.
*
The data node index in question has doc-values stored for the field in question.
*
* While we do not support transmitting spatial doc-values to the coordinator node, it is still important on the data node to ensure
@@ -59,15 +59,15 @@
*
* The question has been raised why the spatial functions need to know if they are using doc-values or not. At first glance one might
* perceive ES|QL functions as being logical planning only constructs, reflecting only the intent of the user. This, however, is not true.
- * The ES|QL functions all contain the runtime implementation of the functions behaviour, in the form of one or more static methods,
+ * The ES|QL functions all contain the runtime implementation of the function's behaviour, in the form of one or more static methods,
* as well as a toEvaluator() instance method that is used to generates Block traversal code to call these runtime
* implementations, based on some internal state of the instance of the function. In most cases this internal state contains information
* determined during the logical planning phase, such as the field name and type, and whether it is a literal and can be folded.
* In the case of spatial functions, the internal state also contains information about whether the function is using doc-values or not.
- * This knowledge is determined in the class being described here, and is only determined during local physical planning on each data
+ * This knowledge is determined in the class being described here and is only determined during local physical planning on each data
* node. This is because the decision to use doc-values is based on the local data node's index configuration, and the local physical plan
* is the only place where this information is available. This also means that the knowledge of the usage of doc-values does not need
- * to be serialized between nodes, and is only used locally.
+ * to be serialized between nodes and is only used locally.
*/
public class SpatialDocValuesExtraction extends PhysicalOptimizerRules.ParameterizedOptimizerRule<
UnaryExec,
@@ -107,7 +107,7 @@ protected PhysicalPlan rule(UnaryExec planNode, LocalPhysicalOptimizerContext ct
List changed = fields.stream()
.map(
f -> (Alias) f.transformDown(BinarySpatialFunction.class, s -> withDocValues(s, foundAttributes))
- .transformDown(SpatialGridFunction.class, s -> withDocValues(s, foundAttributes))
+ .transformDown(SpatialDocValuesFunction.class, s -> withDocValues(s, foundAttributes))
)
.toList();
if (changed.equals(fields) == false) {
@@ -119,7 +119,7 @@ protected PhysicalPlan rule(UnaryExec planNode, LocalPhysicalOptimizerContext ct
// to support shapes, we need to consider loading shape doc-values for both centroid and relates (ST_INTERSECTS)
var condition = filterExec.condition()
.transformDown(BinarySpatialFunction.class, s -> withDocValues(s, foundAttributes))
- .transformDown(SpatialGridFunction.class, s -> withDocValues(s, foundAttributes));
+ .transformDown(SpatialDocValuesFunction.class, s -> withDocValues(s, foundAttributes));
if (filterExec.condition().equals(condition) == false) {
exec = new FilterExec(filterExec.source(), filterExec.child(), condition);
}
@@ -157,10 +157,10 @@ && allowedForDocValues(fieldAttribute, ctx.searchStats(), agg, foundAttributes))
}
}
});
- // Search for spatial grid functions in EVALs
+ // Search in EVALs for functions that can take doc values, namely St_Simplify, St_Geotile, St_Geohex, St_Geohash
exec.forEachDown(EvalExec.class, evalExec -> {
for (Alias field : evalExec.fields()) {
- field.forEachDown(SpatialGridFunction.class, spatialAggFunc -> {
+ field.forEachDown(SpatialDocValuesFunction.class, spatialAggFunc -> {
if (spatialAggFunc.spatialField() instanceof FieldAttribute fieldAttribute
&& allowedForDocValues(fieldAttribute, ctx.searchStats(), exec, foundAttributes)) {
foundAttributes.add(fieldAttribute);
@@ -185,7 +185,7 @@ private BinarySpatialFunction withDocValues(BinarySpatialFunction spatial, Set foundAttributes) {
+ private SpatialDocValuesFunction withDocValues(SpatialDocValuesFunction spatial, Set foundAttributes) {
// Only update the docValues flags if the field is found in the attributes
boolean found = foundField(spatial.spatialField(), foundAttributes);
return found ? spatial.withDocValues(found) : spatial;
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java
index ff690482bfadc..e0fdd8aba713a 100644
--- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java
@@ -59,6 +59,7 @@
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.AfterClass;
+import org.junit.AssumptionViolatedException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
@@ -964,15 +965,20 @@ public static Set signatures(Class> testClass) {
}
for (Object p : params) {
TestCaseSupplier tcs = (TestCaseSupplier) ((Object[]) p)[0];
- TestCaseSupplier.TestCase tc = tcs.get();
- if (tc.getExpectedTypeError() != null) {
- continue;
- }
- if (tc.getData().stream().anyMatch(t -> t.type() == DataType.NULL)) {
- continue;
+ try {
+ TestCaseSupplier.TestCase tc = tcs.get();
+ if (tc.getExpectedTypeError() != null) {
+ continue;
+ }
+ if (tc.getData().stream().anyMatch(t -> t.type() == DataType.NULL)) {
+ continue;
+ }
+ List sig = tc.getData().stream().map(d -> new DocsV3Support.Param(d.type(), d.appliesTo())).toList();
+ signatures.add(new DocsV3Support.TypeSignature(signatureTypes(testClass, sig), tc.expectedType()));
+ } catch (AssumptionViolatedException ignored) {
+ // Throwing an AssumptionViolatedException in a test is a valid way of ignoring a test in junit.
+ // We catch that exception always to keep filling the signatures collection
}
- List sig = tc.getData().stream().map(d -> new DocsV3Support.Param(d.type(), d.appliesTo())).toList();
- signatures.add(new DocsV3Support.TypeSignature(signatureTypes(testClass, sig), tc.expectedType()));
}
return signatures;
}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyTests.java
new file mode 100644
index 0000000000000..982c1fa11cf8c
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyTests.java
@@ -0,0 +1,139 @@
+/*
+ * 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.esql.expression.function.scalar.spatial;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.hamcrest.Matchers;
+import org.junit.AssumptionViolatedException;
+import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.xpack.esql.core.type.DataType.CARTESIAN_POINT;
+import static org.elasticsearch.xpack.esql.core.type.DataType.CARTESIAN_SHAPE;
+import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE;
+import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_POINT;
+import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_SHAPE;
+import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED;
+
+@FunctionName("st_simplify")
+public class StSimplifyTests extends AbstractScalarFunctionTestCase {
+ public StSimplifyTests(@Name("TestCase") Supplier testCaseSupplier) {
+ this.testCase = testCaseSupplier.get();
+ }
+
+ @ParametersFactory
+ public static Iterable