Browse Source

ESQL: Support ST_CONTAINS and ST_WITHIN (#106503)

* WIP Started adding ST_CONTAINS

* Add generated evaluators

* Reduced warnings and use correct evaluators

* Refactored tests to remove duplicate code, and fixed Contains/multi-components

* Gradle build disallows using getDeclaredField

* Fixed cases where rectangles cross the dateline

* Fixed meta function tests

* Added ST_WITHIN to support inverting ST_CONTAINS

If the ST_CONTAINS is called with the constant on the left, we either have to create a lot more Evaluators to cover that case, or we have to invert it to ST_WITHIN. This inversion was a much easier option.

* Simplify inversion logic

* Add comment on choice of surrogate approach

* Add unit tests and missing fold() function

* Simple code cleanup

* Add integration tests for literals

* Add more integration tests based on actual data

* Generated documentation files

* Add documentation

* Fixed failing function count test

* Add tests that push-to-source works for ST_CONTAINS and ST_WITHIN

* Test more combinations of WITH/CONTAINS and literal on right and left

This also verifies that the re-writing of CONTAINS to WITHIN or vice versa occurs when the literal is on the left.

* test that physical planning also handles doc-values from STATS

* Added more tests for WITHIN/CONTAINS together with CENTROID

This should test the doc-values for points.

* Add cartesian_point tests

* Add cartesian_shape tests

* Disable Lucene-push-down for CARTESIAN data

This is a limitation in Lucene, which we could address as a performance optimization in a future PR, but since it probably requires Lucene changes, it cannot be done in this work.

* Fix doc links

* Added test data and tests for cartesian multi-polygons

Testing INTERSECTS, CONTAINS and WITHIN with multi-polydon fields

* Use required features for spatial points, shapes and centroid

* 8.13.0 is not yet historical version

This needs to be reverted as soon as 8.13.0 is released

* Added st_intersects and st_contains_within 'features'

* Code review updates

* Re-enable lucene push-down

* Added more required_features

* Fix point contains non-point

* Fix point contains point

* Re-enable lucene push-down in tests too

Forgot to change the physical planner unit tests after re-enabling lucene push-down

* Generate automatic docs

* Use generated examples docs

* Generated examples use '-result' prefix (singular)

* Mark spatial functions as preview/experimental
Craig Taverner 1 year ago
parent
commit
2380492fac
65 changed files with 4621 additions and 325 deletions
  1. 5 0
      docs/changelog/106503.yaml
  2. 1 1
      docs/reference/esql/functions/aggregation-functions.asciidoc
  3. 7 0
      docs/reference/esql/functions/description/st_contains.asciidoc
  4. 2 0
      docs/reference/esql/functions/description/st_intersects.asciidoc
  5. 7 0
      docs/reference/esql/functions/description/st_within.asciidoc
  6. 13 0
      docs/reference/esql/functions/examples/st_contains.asciidoc
  7. 13 0
      docs/reference/esql/functions/examples/st_intersects.asciidoc
  8. 13 0
      docs/reference/esql/functions/examples/st_within.asciidoc
  9. 15 0
      docs/reference/esql/functions/layout/st_contains.asciidoc
  10. 1 0
      docs/reference/esql/functions/layout/st_intersects.asciidoc
  11. 15 0
      docs/reference/esql/functions/layout/st_within.asciidoc
  12. 7 0
      docs/reference/esql/functions/parameters/st_contains.asciidoc
  13. 7 0
      docs/reference/esql/functions/parameters/st_within.asciidoc
  14. 1 0
      docs/reference/esql/functions/signature/st_contains.svg
  15. 1 0
      docs/reference/esql/functions/signature/st_within.svg
  16. 7 3
      docs/reference/esql/functions/spatial-functions.asciidoc
  17. 2 0
      docs/reference/esql/functions/st_centroid.asciidoc
  18. 26 0
      docs/reference/esql/functions/st_contains.asciidoc
  19. 3 13
      docs/reference/esql/functions/st_intersects.asciidoc
  20. 26 0
      docs/reference/esql/functions/st_within.asciidoc
  21. 2 0
      docs/reference/esql/functions/st_x.asciidoc
  22. 2 0
      docs/reference/esql/functions/st_y.asciidoc
  23. 16 0
      docs/reference/esql/functions/types/st_contains.asciidoc
  24. 16 0
      docs/reference/esql/functions/types/st_within.asciidoc
  25. 7 1
      x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java
  26. 11 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/cartesian_multipolygons.csv
  27. 201 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/cartesian_multipolygons.csv-spec
  28. 13 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-cartesian_multipolygons.json
  29. 5 1
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/meta.csv-spec
  30. 534 74
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec
  31. 168 30
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial_shapes.csv-spec
  32. 128 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsCartesianPointDocValuesAndConstantEvaluator.java
  33. 142 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsCartesianPointDocValuesAndSourceEvaluator.java
  34. 132 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsCartesianSourceAndConstantEvaluator.java
  35. 152 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsCartesianSourceAndSourceEvaluator.java
  36. 128 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsGeoPointDocValuesAndConstantEvaluator.java
  37. 151 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsGeoPointDocValuesAndSourceEvaluator.java
  38. 132 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsGeoSourceAndConstantEvaluator.java
  39. 152 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsGeoSourceAndSourceEvaluator.java
  40. 128 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinCartesianPointDocValuesAndConstantEvaluator.java
  41. 142 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinCartesianPointDocValuesAndSourceEvaluator.java
  42. 132 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinCartesianSourceAndConstantEvaluator.java
  43. 152 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinCartesianSourceAndSourceEvaluator.java
  44. 128 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinGeoPointDocValuesAndConstantEvaluator.java
  45. 151 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinGeoPointDocValuesAndSourceEvaluator.java
  46. 132 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinGeoSourceAndConstantEvaluator.java
  47. 152 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinGeoSourceAndSourceEvaluator.java
  48. 4 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  49. 91 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/LuceneComponent2DUtils.java
  50. 287 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContains.java
  51. 2 2
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialEvaluatorFactory.java
  52. 19 4
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersects.java
  53. 18 6
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialRelatesFunction.java
  54. 10 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialRelatesUtils.java
  55. 252 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithin.java
  56. 17 6
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java
  57. 22 1
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java
  58. 40 9
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlFeatures.java
  59. 3 3
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/SpatialRelatesQuery.java
  60. 1 1
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java
  61. 46 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsTests.java
  62. 3 170
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersectsTests.java
  63. 207 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialRelatesFunctionTestCase.java
  64. 46 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinTests.java
  65. 172 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java

+ 5 - 0
docs/changelog/106503.yaml

@@ -0,0 +1,5 @@
+pr: 106503
+summary: "Support ST_CONTAINS and ST_WITHIN"
+area: "ES|QL"
+type: enhancement
+issues: []

+ 1 - 1
docs/reference/esql/functions/aggregation-functions.asciidoc

@@ -16,7 +16,7 @@ The <<esql-stats-by>> function supports these aggregate functions:
 * <<esql-agg-median-absolute-deviation>>
 * <<esql-agg-min>>
 * <<esql-agg-percentile>>
-* <<esql-agg-st-centroid>>
+* experimental:[] <<esql-agg-st-centroid>>
 * <<esql-agg-sum>>
 * <<esql-agg-values>>
 // end::agg_list[]

+ 7 - 0
docs/reference/esql/functions/description/st_contains.asciidoc

@@ -0,0 +1,7 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Returns whether the first geometry contains the second geometry.
+
+NOTE: The second parameter must also have the same coordinate system as the first. This means it is not possible to combine `geo_*` and `cartesian_*` parameters.

+ 2 - 0
docs/reference/esql/functions/description/st_intersects.asciidoc

@@ -3,3 +3,5 @@
 *Description*
 
 Returns whether the two geometries or geometry columns intersect.
+
+NOTE: The second parameter must also have the same coordinate system as the first. This means it is not possible to combine `geo_*` and `cartesian_*` parameters.

+ 7 - 0
docs/reference/esql/functions/description/st_within.asciidoc

@@ -0,0 +1,7 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Returns whether the first geometry is within the second geometry.
+
+NOTE: The second parameter must also have the same coordinate system as the first. This means it is not possible to combine `geo_*` and `cartesian_*` parameters.

+ 13 - 0
docs/reference/esql/functions/examples/st_contains.asciidoc

@@ -0,0 +1,13 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_contains-airport_city_boundaries]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_contains-airport_city_boundaries-result]
+|===
+

+ 13 - 0
docs/reference/esql/functions/examples/st_intersects.asciidoc

@@ -0,0 +1,13 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/spatial.csv-spec[tag=st_intersects-airports]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/spatial.csv-spec[tag=st_intersects-airports-result]
+|===
+

+ 13 - 0
docs/reference/esql/functions/examples/st_within.asciidoc

@@ -0,0 +1,13 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_within-airport_city_boundaries]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_within-airport_city_boundaries-result]
+|===
+

+ 15 - 0
docs/reference/esql/functions/layout/st_contains.asciidoc

@@ -0,0 +1,15 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-st_contains]]
+=== `ST_CONTAINS`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_contains.svg[Embedded,opts=inline]
+
+include::../parameters/st_contains.asciidoc[]
+include::../description/st_contains.asciidoc[]
+include::../types/st_contains.asciidoc[]
+include::../examples/st_contains.asciidoc[]

+ 1 - 0
docs/reference/esql/functions/layout/st_intersects.asciidoc

@@ -12,3 +12,4 @@ image::esql/functions/signature/st_intersects.svg[Embedded,opts=inline]
 include::../parameters/st_intersects.asciidoc[]
 include::../description/st_intersects.asciidoc[]
 include::../types/st_intersects.asciidoc[]
+include::../examples/st_intersects.asciidoc[]

+ 15 - 0
docs/reference/esql/functions/layout/st_within.asciidoc

@@ -0,0 +1,15 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-st_within]]
+=== `ST_WITHIN`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_within.svg[Embedded,opts=inline]
+
+include::../parameters/st_within.asciidoc[]
+include::../description/st_within.asciidoc[]
+include::../types/st_within.asciidoc[]
+include::../examples/st_within.asciidoc[]

+ 7 - 0
docs/reference/esql/functions/parameters/st_contains.asciidoc

@@ -0,0 +1,7 @@
+*Parameters*
+
+`geomA`::
+Geometry column name or variable of geometry type
+
+`geomB`::
+Geometry column name or variable of geometry type

+ 7 - 0
docs/reference/esql/functions/parameters/st_within.asciidoc

@@ -0,0 +1,7 @@
+*Parameters*
+
+`geomA`::
+Geometry column name or variable of geometry type
+
+`geomB`::
+Geometry column name or variable of geometry type

+ 1 - 0
docs/reference/esql/functions/signature/st_contains.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="468" height="46" viewbox="0 0 468 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m152 0h10m32 0h10m80 0h10m32 0h10m80 0h10m32 0h5"/><rect class="s" x="5" y="5" width="152" height="36"/><text class="k" x="15" y="31">ST_CONTAINS</text><rect class="s" x="167" y="5" width="32" height="36" rx="7"/><text class="syn" x="177" y="31">(</text><rect class="s" x="209" y="5" width="80" height="36" rx="7"/><text class="k" x="219" y="31">geomA</text><rect class="s" x="299" y="5" width="32" height="36" rx="7"/><text class="syn" x="309" y="31">,</text><rect class="s" x="341" y="5" width="80" height="36" rx="7"/><text class="k" x="351" y="31">geomB</text><rect class="s" x="431" y="5" width="32" height="36" rx="7"/><text class="syn" x="441" y="31">)</text></svg>

+ 1 - 0
docs/reference/esql/functions/signature/st_within.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="444" height="46" viewbox="0 0 444 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m128 0h10m32 0h10m80 0h10m32 0h10m80 0h10m32 0h5"/><rect class="s" x="5" y="5" width="128" height="36"/><text class="k" x="15" y="31">ST_WITHIN</text><rect class="s" x="143" y="5" width="32" height="36" rx="7"/><text class="syn" x="153" y="31">(</text><rect class="s" x="185" y="5" width="80" height="36" rx="7"/><text class="k" x="195" y="31">geomA</text><rect class="s" x="275" y="5" width="32" height="36" rx="7"/><text class="syn" x="285" y="31">,</text><rect class="s" x="317" y="5" width="80" height="36" rx="7"/><text class="k" x="327" y="31">geomB</text><rect class="s" x="407" y="5" width="32" height="36" rx="7"/><text class="syn" x="417" y="31">)</text></svg>

+ 7 - 3
docs/reference/esql/functions/spatial-functions.asciidoc

@@ -8,11 +8,15 @@
 {esql} supports these spatial functions:
 
 // tag::spatial_list[]
-* <<esql-st_intersects>>
-* <<esql-st_x>>
-* <<esql-st_y>>
+* experimental:[] <<esql-st_intersects>>
+* experimental:[] <<esql-st_contains>>
+* experimental:[] <<esql-st_within>>
+* experimental:[] <<esql-st_x>>
+* experimental:[] <<esql-st_y>>
 // end::spatial_list[]
 
 include::st_intersects.asciidoc[]
+include::st_contains.asciidoc[]
+include::st_within.asciidoc[]
 include::st_x.asciidoc[]
 include::st_y.asciidoc[]

+ 2 - 0
docs/reference/esql/functions/st_centroid.asciidoc

@@ -2,6 +2,8 @@
 [[esql-agg-st-centroid]]
 === `ST_CENTROID`
 
+experimental::[]
+
 Calculate the spatial centroid over a field with spatial point geometry type.
 
 [source.merge.styled,esql]

+ 26 - 0
docs/reference/esql/functions/st_contains.asciidoc

@@ -0,0 +1,26 @@
+[discrete]
+[[esql-st_contains]]
+=== `ST_CONTAINS`
+
+experimental::[]
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_contains.svg[Embedded,opts=inline]
+
+*Parameters*
+
+`geomA`::
+Expression of type `geo_point`, `cartesian_point`, `geo_shape` or `cartesian_shape`. If `null`, the function returns `null`.
+
+`geomB`::
+Expression of type `geo_point`, `cartesian_point`, `geo_shape` or `cartesian_shape`. If `null`, the function returns `null`.
+The second parameter must also have the same coordinate system as the first.
+This means it is not possible to combine `geo_*` and `cartesian_*` parameters.
+
+include::description/st_contains.asciidoc[]
+This is the inverse of the `<<esql-st_within,ST_WITHIN>>` function.
+
+include::types/st_contains.asciidoc[]
+include::examples/st_contains.asciidoc[]

+ 3 - 13
docs/reference/esql/functions/st_intersects.asciidoc

@@ -2,6 +2,8 @@
 [[esql-st_intersects]]
 === `ST_INTERSECTS`
 
+experimental::[]
+
 *Syntax*
 
 [.text-center]
@@ -24,17 +26,5 @@ They intersect if they have any point in common, including their interior points
 (points along lines or within polygons).
 In mathematical terms: ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅
 
-*Supported types*
-
 include::types/st_intersects.asciidoc[]
-
-*Example*
-
-[source.merge.styled,esql]
-----
-include::{esql-specs}/spatial.csv-spec[tag=st_intersects-airports]
-----
-[%header.monospaced.styled,format=dsv,separator=|]
-|===
-include::{esql-specs}/spatial.csv-spec[tag=st_intersects-airports-results]
-|===
+include::examples/st_intersects.asciidoc[]

+ 26 - 0
docs/reference/esql/functions/st_within.asciidoc

@@ -0,0 +1,26 @@
+[discrete]
+[[esql-st_within]]
+=== `ST_WITHIN`
+
+experimental::[]
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_within.svg[Embedded,opts=inline]
+
+*Parameters*
+
+`geomA`::
+Expression of type `geo_point`, `cartesian_point`, `geo_shape` or `cartesian_shape`. If `null`, the function returns `null`.
+
+`geomB`::
+Expression of type `geo_point`, `cartesian_point`, `geo_shape` or `cartesian_shape`. If `null`, the function returns `null`.
+The second parameter must also have the same coordinate system as the first.
+This means it is not possible to combine `geo_*` and `cartesian_*` parameters.
+
+include::description/st_within.asciidoc[]
+This is the inverse of the `<<esql-st_contains,ST_CONTAINS>>` function.
+
+include::types/st_within.asciidoc[]
+include::examples/st_within.asciidoc[]

+ 2 - 0
docs/reference/esql/functions/st_x.asciidoc

@@ -2,6 +2,8 @@
 [[esql-st_x]]
 === `ST_X`
 
+experimental::[]
+
 *Syntax*
 
 [.text-center]

+ 2 - 0
docs/reference/esql/functions/st_y.asciidoc

@@ -2,6 +2,8 @@
 [[esql-st_y]]
 === `ST_Y`
 
+experimental::[]
+
 *Syntax*
 
 [.text-center]

+ 16 - 0
docs/reference/esql/functions/types/st_contains.asciidoc

@@ -0,0 +1,16 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+geomA | geomB | result
+cartesian_point | cartesian_point | boolean
+cartesian_point | cartesian_shape | boolean
+cartesian_shape | cartesian_point | boolean
+cartesian_shape | cartesian_shape | boolean
+geo_point | geo_point | boolean
+geo_point | geo_shape | boolean
+geo_shape | geo_point | boolean
+geo_shape | geo_shape | boolean
+|===

+ 16 - 0
docs/reference/esql/functions/types/st_within.asciidoc

@@ -0,0 +1,16 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+geomA | geomB | result
+cartesian_point | cartesian_point | boolean
+cartesian_point | cartesian_shape | boolean
+cartesian_shape | cartesian_point | boolean
+cartesian_shape | cartesian_shape | boolean
+geo_point | geo_point | boolean
+geo_point | geo_shape | boolean
+geo_shape | geo_point | boolean
+geo_shape | geo_shape | boolean
+|===

+ 7 - 1
x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java

@@ -78,6 +78,11 @@ public class CsvTestsDataLoader {
         "mapping-airport_city_boundaries.json",
         "airport_city_boundaries.csv"
     );
+    private static final TestsDataset CARTESIAN_MULTIPOLYGONS = new TestsDataset(
+        "cartesian_multipolygons",
+        "mapping-cartesian_multipolygons.json",
+        "cartesian_multipolygons.csv"
+    );
 
     public static final Map<String, TestsDataset> CSV_DATASET_MAP = Map.ofEntries(
         Map.entry(EMPLOYEES.indexName, EMPLOYEES),
@@ -96,7 +101,8 @@ public class CsvTestsDataLoader {
         Map.entry(AIRPORTS_WEB.indexName, AIRPORTS_WEB),
         Map.entry(COUNTRIES_BBOX.indexName, COUNTRIES_BBOX),
         Map.entry(COUNTRIES_BBOX_WEB.indexName, COUNTRIES_BBOX_WEB),
-        Map.entry(AIRPORT_CITY_BOUNDARIES.indexName, AIRPORT_CITY_BOUNDARIES)
+        Map.entry(AIRPORT_CITY_BOUNDARIES.indexName, AIRPORT_CITY_BOUNDARIES),
+        Map.entry(CARTESIAN_MULTIPOLYGONS.indexName, CARTESIAN_MULTIPOLYGONS)
     );
 
     private static final EnrichConfig LANGUAGES_ENRICH = new EnrichConfig("languages_policy", "enrich-policy-languages.json");

+ 11 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/cartesian_multipolygons.csv

@@ -0,0 +1,11 @@
+id:l, name:keyword,            shape:cartesian_shape
+0,    Four squares,            "MULTIPOLYGON(((0 0\, 1 0\, 1 1\, 0 1\, 0 0))\, ((2 0\, 3 0\, 3 1\, 2 1\, 2 0))\, ((2 2\, 3 2\, 3 3\, 2 3\, 2 2))\, ((0 2\, 1 2\, 1 3\, 0 3\, 0 2)))"
+1,    Bottom left,             "POLYGON((0 0\, 1 0\, 1 1\, 0 1\, 0 0))"
+2,    Bottom right,            "POLYGON((2 0\, 3 0\, 3 1\, 2 1\, 2 0))"
+3,    Top right,               "POLYGON((2 2\, 3 2\, 3 3\, 2 3\, 2 2))"
+4,    Top left,                "POLYGON((0 2\, 1 2\, 1 3\, 0 3\, 0 2))"
+5,    Four squares with holes, "MULTIPOLYGON(((0 0\, 1 0\, 1 1\, 0 1\, 0 0)\, (0.4 0.4\, 0.6 0.4\, 0.6 0.6\, 0.4 0.6\, 0.4 0.4))\, ((2 0\, 3 0\, 3 1\, 2 1\, 2 0)\, (2.4 0.4\, 2.6 0.4\, 2.6 0.6\, 2.4 0.6\, 2.4 0.4))\, ((2 2\, 3 2\, 3 3\, 2 3\, 2 2)\, (2.4 2.4\, 2.6 2.4\, 2.6 2.6\, 2.4 2.6\, 2.4 2.4))\, ((0 2\, 1 2\, 1 3\, 0 3\, 0 2)\, (0.4 2.4\, 0.6 2.4\, 0.6 2.6\, 0.4 2.6\, 0.4 2.4)))"
+6,    Bottom left with holes,  "POLYGON((0 0\, 1 0\, 1 1\, 0 1\, 0 0)\, (0.4 0.4\, 0.6 0.4\, 0.6 0.6\, 0.4 0.6\, 0.4 0.4))"
+7,    Bottom right with holes, "POLYGON((2 0\, 3 0\, 3 1\, 2 1\, 2 0)\, (2.4 0.4\, 2.6 0.4\, 2.6 0.6\, 2.4 0.6\, 2.4 0.4))"
+8,    Top right with holes,    "POLYGON((2 2\, 3 2\, 3 3\, 2 3\, 2 2)\, (2.4 2.4\, 2.6 2.4\, 2.6 2.6\, 2.4 2.6\, 2.4 2.4))"
+9,    Top left with holes,     "POLYGON((0 2\, 1 2\, 1 3\, 0 3\, 0 2)\, (0.4 2.4\, 0.6 2.4\, 0.6 2.6\, 0.4 2.6\, 0.4 2.4))"

+ 201 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/cartesian_multipolygons.csv-spec

@@ -0,0 +1,201 @@
+####################################################################################################
+# The test data contains four square polygons, with and without holes, and multipolygon combinations of these
+# We test this data against smaller, similar sized and larger query polygons with INTERSECTS, CONTAINS and WITHIN
+
+####################################################################################################
+# Test against a polygon similar in size to the Bottom Left polygon
+
+whereIntersectsSinglePolygon
+required_feature: esql.st_intersects
+
+FROM cartesian_multipolygons
+| WHERE ST_Intersects(shape, TO_CARTESIANSHAPE("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))"))
+| SORT id
+;
+
+id:l | name:keyword | shape:cartesian_shape
+0    | Four squares | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 0, 3 0, 3 1, 2 1, 2 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)), ((0 2, 1 2, 1 3, 0 3, 0 2)))
+1    | Bottom left  | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))
+5    | Four squares with holes | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4)), ((2 0, 3 0, 3 1, 2 1, 2 0), (2.4 0.4, 2.6 0.4, 2.6 0.6, 2.4 0.6, 2.4 0.4)), ((2 2, 3 2, 3 3, 2 3, 2 2), (2.4 2.4, 2.6 2.4, 2.6 2.6, 2.4 2.6, 2.4 2.4)), ((0 2, 1 2, 1 3, 0 3, 0 2), (0.4 2.4, 0.6 2.4, 0.6 2.6, 0.4 2.6, 0.4 2.4)))
+6    | Bottom left with holes  |  POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4))
+;
+
+whereContainsSinglePolygon
+required_feature: esql.st_contains_within
+
+FROM cartesian_multipolygons
+| WHERE ST_Contains(shape, TO_CARTESIANSHAPE("POLYGON((0.001 0.001, 0.999 0.001, 0.999 0.999, 0.001 0.999, 0.001 0.001))"))
+| SORT id
+;
+
+id:l | name:keyword | shape:cartesian_shape
+0    | Four squares | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 0, 3 0, 3 1, 2 1, 2 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)), ((0 2, 1 2, 1 3, 0 3, 0 2)))
+1    | Bottom left  | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))
+;
+
+whereWithinSinglePolygon
+required_feature: esql.st_contains_within
+
+FROM cartesian_multipolygons
+| WHERE ST_Within(shape, TO_CARTESIANSHAPE("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))"))
+| SORT id
+;
+
+id:l | name:keyword | shape:cartesian_shape
+1    | Bottom left  | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))
+6    | Bottom left with holes  |  POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4))
+;
+
+####################################################################################################
+# Test against a polygon smaller in size to the Bottom Left polygon
+
+whereIntersectsSmallerPolygon
+required_feature: esql.st_intersects
+
+FROM cartesian_multipolygons
+| WHERE ST_Intersects(shape, TO_CARTESIANSHAPE("POLYGON((0.2 0.2, 0.8 0.2, 0.8 0.8, 0.2 0.8, 0.2 0.2))"))
+| SORT id
+;
+
+id:l | name:keyword | shape:cartesian_shape
+0    | Four squares | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 0, 3 0, 3 1, 2 1, 2 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)), ((0 2, 1 2, 1 3, 0 3, 0 2)))
+1    | Bottom left  | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))
+5    | Four squares with holes | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4)), ((2 0, 3 0, 3 1, 2 1, 2 0), (2.4 0.4, 2.6 0.4, 2.6 0.6, 2.4 0.6, 2.4 0.4)), ((2 2, 3 2, 3 3, 2 3, 2 2), (2.4 2.4, 2.6 2.4, 2.6 2.6, 2.4 2.6, 2.4 2.4)), ((0 2, 1 2, 1 3, 0 3, 0 2), (0.4 2.4, 0.6 2.4, 0.6 2.6, 0.4 2.6, 0.4 2.4)))
+6    | Bottom left with holes  |  POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4))
+;
+
+whereContainsSmallerPolygon
+required_feature: esql.st_contains_within
+
+FROM cartesian_multipolygons
+| WHERE ST_Contains(shape, TO_CARTESIANSHAPE("POLYGON((0.2 0.2, 0.8 0.2, 0.8 0.8, 0.2 0.8, 0.2 0.2))"))
+| SORT id
+;
+
+id:l | name:keyword | shape:cartesian_shape
+0    | Four squares | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 0, 3 0, 3 1, 2 1, 2 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)), ((0 2, 1 2, 1 3, 0 3, 0 2)))
+1    | Bottom left  | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))
+;
+
+whereWithinSmallerPolygon
+required_feature: esql.st_contains_within
+
+FROM cartesian_multipolygons
+| WHERE ST_Within(shape, TO_CARTESIANSHAPE("POLYGON((0.2 0.2, 0.8 0.2, 0.8 0.8, 0.2 0.8, 0.2 0.2))"))
+| SORT id
+;
+
+id:l | name:keyword | shape:cartesian_shape
+;
+
+####################################################################################################
+# Test against a polygon similar in size to the entire test data
+
+whereIntersectsLargerPolygon
+required_feature: esql.st_intersects
+
+FROM cartesian_multipolygons
+| WHERE ST_Intersects(shape, TO_CARTESIANSHAPE("POLYGON((0 0, 3 0, 3 3, 0 3, 0 0))"))
+| SORT id
+;
+
+id:l | name:keyword            | shape:cartesian_shape
+0    | Four squares            | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 0, 3 0, 3 1, 2 1, 2 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)), ((0 2, 1 2, 1 3, 0 3, 0 2)))
+1    | Bottom left             | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))
+2    | Bottom right            | POLYGON((2 0, 3 0, 3 1, 2 1, 2 0))
+3    | Top right               | POLYGON((2 2, 3 2, 3 3, 2 3, 2 2))
+4    | Top left                | POLYGON((0 2, 1 2, 1 3, 0 3, 0 2))
+5    | Four squares with holes | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4)), ((2 0, 3 0, 3 1, 2 1, 2 0), (2.4 0.4, 2.6 0.4, 2.6 0.6, 2.4 0.6, 2.4 0.4)), ((2 2, 3 2, 3 3, 2 3, 2 2), (2.4 2.4, 2.6 2.4, 2.6 2.6, 2.4 2.6, 2.4 2.4)), ((0 2, 1 2, 1 3, 0 3, 0 2), (0.4 2.4, 0.6 2.4, 0.6 2.6, 0.4 2.6, 0.4 2.4)))
+6    | Bottom left with holes  | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4))
+7    | Bottom right with holes | POLYGON((2 0, 3 0, 3 1, 2 1, 2 0), (2.4 0.4, 2.6 0.4, 2.6 0.6, 2.4 0.6, 2.4 0.4))
+8    | Top right with holes    | POLYGON((2 2, 3 2, 3 3, 2 3, 2 2), (2.4 2.4, 2.6 2.4, 2.6 2.6, 2.4 2.6, 2.4 2.4))
+9    | Top left with holes     | POLYGON((0 2, 1 2, 1 3, 0 3, 0 2), (0.4 2.4, 0.6 2.4, 0.6 2.6, 0.4 2.6, 0.4 2.4))
+;
+
+whereContainsLargerPolygon
+required_feature: esql.st_contains_within
+
+FROM cartesian_multipolygons
+| WHERE ST_Contains(shape, TO_CARTESIANSHAPE("POLYGON((0 0, 3 0, 3 3, 0 3, 0 0))"))
+| SORT id
+;
+
+id:l | name:keyword            | shape:cartesian_shape
+;
+
+whereWithinLargerPolygon
+required_feature: esql.st_contains_within
+
+FROM cartesian_multipolygons
+| WHERE ST_Within(shape, TO_CARTESIANSHAPE("POLYGON((0 0, 3 0, 3 3, 0 3, 0 0))"))
+| SORT id
+;
+
+id:l | name:keyword            | shape:cartesian_shape
+0    | Four squares            | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 0, 3 0, 3 1, 2 1, 2 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)), ((0 2, 1 2, 1 3, 0 3, 0 2)))
+1    | Bottom left             | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))
+2    | Bottom right            | POLYGON((2 0, 3 0, 3 1, 2 1, 2 0))
+3    | Top right               | POLYGON((2 2, 3 2, 3 3, 2 3, 2 2))
+4    | Top left                | POLYGON((0 2, 1 2, 1 3, 0 3, 0 2))
+5    | Four squares with holes | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4)), ((2 0, 3 0, 3 1, 2 1, 2 0), (2.4 0.4, 2.6 0.4, 2.6 0.6, 2.4 0.6, 2.4 0.4)), ((2 2, 3 2, 3 3, 2 3, 2 2), (2.4 2.4, 2.6 2.4, 2.6 2.6, 2.4 2.6, 2.4 2.4)), ((0 2, 1 2, 1 3, 0 3, 0 2), (0.4 2.4, 0.6 2.4, 0.6 2.6, 0.4 2.6, 0.4 2.4)))
+6    | Bottom left with holes  | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4))
+7    | Bottom right with holes | POLYGON((2 0, 3 0, 3 1, 2 1, 2 0), (2.4 0.4, 2.6 0.4, 2.6 0.6, 2.4 0.6, 2.4 0.4))
+8    | Top right with holes    | POLYGON((2 2, 3 2, 3 3, 2 3, 2 2), (2.4 2.4, 2.6 2.4, 2.6 2.6, 2.4 2.6, 2.4 2.4))
+9    | Top left with holes     | POLYGON((0 2, 1 2, 1 3, 0 3, 0 2), (0.4 2.4, 0.6 2.4, 0.6 2.6, 0.4 2.6, 0.4 2.4))
+;
+
+####################################################################################################
+# Test against a polygon larger than all test data
+
+whereIntersectsEvenLargerPolygon
+required_feature: esql.st_intersects
+
+FROM cartesian_multipolygons
+| WHERE ST_Intersects(shape, TO_CARTESIANSHAPE("POLYGON((-1 -1, 4 -1, 4 4, -1 4, -1 -1))"))
+| SORT id
+;
+
+id:l | name:keyword            | shape:cartesian_shape
+0    | Four squares            | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 0, 3 0, 3 1, 2 1, 2 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)), ((0 2, 1 2, 1 3, 0 3, 0 2)))
+1    | Bottom left             | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))
+2    | Bottom right            | POLYGON((2 0, 3 0, 3 1, 2 1, 2 0))
+3    | Top right               | POLYGON((2 2, 3 2, 3 3, 2 3, 2 2))
+4    | Top left                | POLYGON((0 2, 1 2, 1 3, 0 3, 0 2))
+5    | Four squares with holes | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4)), ((2 0, 3 0, 3 1, 2 1, 2 0), (2.4 0.4, 2.6 0.4, 2.6 0.6, 2.4 0.6, 2.4 0.4)), ((2 2, 3 2, 3 3, 2 3, 2 2), (2.4 2.4, 2.6 2.4, 2.6 2.6, 2.4 2.6, 2.4 2.4)), ((0 2, 1 2, 1 3, 0 3, 0 2), (0.4 2.4, 0.6 2.4, 0.6 2.6, 0.4 2.6, 0.4 2.4)))
+6    | Bottom left with holes  | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4))
+7    | Bottom right with holes | POLYGON((2 0, 3 0, 3 1, 2 1, 2 0), (2.4 0.4, 2.6 0.4, 2.6 0.6, 2.4 0.6, 2.4 0.4))
+8    | Top right with holes    | POLYGON((2 2, 3 2, 3 3, 2 3, 2 2), (2.4 2.4, 2.6 2.4, 2.6 2.6, 2.4 2.6, 2.4 2.4))
+9    | Top left with holes     | POLYGON((0 2, 1 2, 1 3, 0 3, 0 2), (0.4 2.4, 0.6 2.4, 0.6 2.6, 0.4 2.6, 0.4 2.4))
+;
+
+whereContainsEvenLargerPolygon
+required_feature: esql.st_contains_within
+
+FROM cartesian_multipolygons
+| WHERE ST_Contains(shape, TO_CARTESIANSHAPE("POLYGON((-1 -1, 4 -1, 4 4, -1 4, -1 -1))"))
+| SORT id
+;
+
+id:l | name:keyword            | shape:cartesian_shape
+;
+
+whereWithinEvenLargerPolygon
+required_feature: esql.st_contains_within
+
+FROM cartesian_multipolygons
+| WHERE ST_Within(shape, TO_CARTESIANSHAPE("POLYGON((-1 -1, 4 -1, 4 4, -1 4, -1 -1))"))
+| SORT id
+;
+
+id:l | name:keyword            | shape:cartesian_shape
+0    | Four squares            | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 0, 3 0, 3 1, 2 1, 2 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)), ((0 2, 1 2, 1 3, 0 3, 0 2)))
+1    | Bottom left             | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))
+2    | Bottom right            | POLYGON((2 0, 3 0, 3 1, 2 1, 2 0))
+3    | Top right               | POLYGON((2 2, 3 2, 3 3, 2 3, 2 2))
+4    | Top left                | POLYGON((0 2, 1 2, 1 3, 0 3, 0 2))
+5    | Four squares with holes | MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4)), ((2 0, 3 0, 3 1, 2 1, 2 0), (2.4 0.4, 2.6 0.4, 2.6 0.6, 2.4 0.6, 2.4 0.4)), ((2 2, 3 2, 3 3, 2 3, 2 2), (2.4 2.4, 2.6 2.4, 2.6 2.6, 2.4 2.6, 2.4 2.4)), ((0 2, 1 2, 1 3, 0 3, 0 2), (0.4 2.4, 0.6 2.4, 0.6 2.6, 0.4 2.6, 0.4 2.4)))
+6    | Bottom left with holes  | POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.4 0.4, 0.6 0.4, 0.6 0.6, 0.4 0.6, 0.4 0.4))
+7    | Bottom right with holes | POLYGON((2 0, 3 0, 3 1, 2 1, 2 0), (2.4 0.4, 2.6 0.4, 2.6 0.6, 2.4 0.6, 2.4 0.4))
+8    | Top right with holes    | POLYGON((2 2, 3 2, 3 3, 2 3, 2 2), (2.4 2.4, 2.6 2.4, 2.6 2.6, 2.4 2.6, 2.4 2.4))
+9    | Top left with holes     | POLYGON((0 2, 1 2, 1 3, 0 3, 0 2), (0.4 2.4, 0.6 2.4, 0.6 2.6, 0.4 2.6, 0.4 2.4))
+;

+ 13 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-cartesian_multipolygons.json

@@ -0,0 +1,13 @@
+{
+  "properties": {
+    "id": {
+      "type": "long"
+    },
+    "name": {
+      "type": "keyword"
+    },
+    "shape": {
+      "type": "shape"
+    }
+  }
+}

+ 5 - 1
x-pack/plugin/esql/qa/testFixtures/src/main/resources/meta.csv-spec

@@ -64,7 +64,9 @@ sinh                     |"double sinh(angle:double|integer|long|unsigned_long)"
 split                    |"keyword split(string:keyword|text, delim:keyword|text)"                                 |[string, delim]             |["keyword|text", "keyword|text"]            |["", ""]                                            |keyword                    | "Split a single valued string into multiple strings."                      | [false, false]       | false | false
 sqrt                     |"double sqrt(number:double|integer|long|unsigned_long)"     |number                       |"double|integer|long|unsigned_long"                 | ""                                                 |double                    | "Returns the square root of a number."                      | false                | false | false
 st_centroid              |"geo_point|cartesian_point st_centroid(field:geo_point|cartesian_point)" |field  |"geo_point|cartesian_point"                         | ""                                                 |"geo_point|cartesian_point"   | "The centroid of a spatial field."                      | false                | false | true
+st_contains              |"boolean st_contains(geomA:geo_point|cartesian_point|geo_shape|cartesian_shape, geomB:geo_point|cartesian_point|geo_shape|cartesian_shape)"   |[geomA, geomB] |["geo_point|cartesian_point|geo_shape|cartesian_shape", "geo_point|cartesian_point|geo_shape|cartesian_shape"] |["Geometry column name or variable of geometry type", "Geometry column name or variable of geometry type"] |boolean | "Returns whether the first geometry contains the second geometry."  | [false, false] | false | false
 st_intersects            |"boolean st_intersects(geomA:geo_point|cartesian_point|geo_shape|cartesian_shape, geomB:geo_point|cartesian_point|geo_shape|cartesian_shape)" |[geomA, geomB] |["geo_point|cartesian_point|geo_shape|cartesian_shape", "geo_point|cartesian_point|geo_shape|cartesian_shape"] |["Geometry column name or variable of geometry type", "Geometry column name or variable of geometry type"] |boolean | "Returns whether the two geometries or geometry columns intersect." | [false, false] | false | false
+st_within                |"boolean st_within(geomA:geo_point|cartesian_point|geo_shape|cartesian_shape, geomB:geo_point|cartesian_point|geo_shape|cartesian_shape)"     |[geomA, geomB] |["geo_point|cartesian_point|geo_shape|cartesian_shape", "geo_point|cartesian_point|geo_shape|cartesian_shape"] |["Geometry column name or variable of geometry type", "Geometry column name or variable of geometry type"] |boolean | "Returns whether the first geometry is within the second geometry." | [false, false] | false | false
 st_x                     |"double st_x(point:geo_point|cartesian_point)"                           |point  |"geo_point|cartesian_point"                         | ""                                                 |double                        | "Extracts the x-coordinate from a point geometry."      | false                | false | false
 st_y                     |"double st_y(point:geo_point|cartesian_point)"                           |point  |"geo_point|cartesian_point"                         | ""                                                 |double                        | "Extracts the y-coordinate from a point geometry."      | false                | false | false
 starts_with              |"boolean starts_with(str:keyword|text, prefix:keyword|text)"                           |[str, prefix]             |["keyword|text", "keyword|text"]            |["", ""]                                            |boolean                    | "Returns a boolean that indicates whether a keyword string starts with another string"                      | [false, false]       | false | false
@@ -168,7 +170,9 @@ double pi()
 "keyword split(string:keyword|text, delim:keyword|text)"
 "double sqrt(number:double|integer|long|unsigned_long)"
 "geo_point|cartesian_point st_centroid(field:geo_point|cartesian_point)"
+"boolean st_contains(geomA:geo_point|cartesian_point|geo_shape|cartesian_shape, geomB:geo_point|cartesian_point|geo_shape|cartesian_shape)"
 "boolean st_intersects(geomA:geo_point|cartesian_point|geo_shape|cartesian_shape, geomB:geo_point|cartesian_point|geo_shape|cartesian_shape)"
+"boolean st_within(geomA:geo_point|cartesian_point|geo_shape|cartesian_shape, geomB:geo_point|cartesian_point|geo_shape|cartesian_shape)"
 "double st_x(point:geo_point|cartesian_point)"
 "double st_y(point:geo_point|cartesian_point)"
 "boolean starts_with(str:keyword|text, prefix:keyword|text)"
@@ -223,5 +227,5 @@ countFunctions#[skip:-8.13.99]
 meta functions |  stats  a = count(*), b = count(*), c = count(*) |  mv_expand c;
 
 a:long | b:long | c:long
-97     | 97     | 97
+99     | 99     | 99
 ;

File diff suppressed because it is too large
+ 534 - 74
x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec


+ 168 - 30
x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial_shapes.csv-spec

@@ -2,7 +2,9 @@
 # Tests for GEO_SHAPE type
 #
 
-convertFromString#[skip:-8.12.99, reason: spatial type geo_shape only added in 8.13]
+convertFromString
+required_feature: esql.spatial_shapes
+
 // tag::to_geoshape-str[]
 ROW wkt = "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))"
 | EVAL geom = TO_GEOSHAPE(wkt)
@@ -15,7 +17,9 @@ wkt:keyword                                     | geom:geo_shape
 // end::to_geoshape-str-result[]
 ;
 
-convertFromStringArray#[skip:-8.12.99, reason: spatial type geo_shape only added in 8.13]
+convertFromStringArray
+required_feature: esql.spatial_shapes
+
 row wkt = ["POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))", "POINT(75.8092915005895 22.727749187571)"]
 | eval pt = to_geoshape(wkt);
 
@@ -23,7 +27,9 @@ wkt:keyword
 ["POLYGON ((30 10\, 40 40\, 20 40\, 10 20\, 30 10))", "POINT(75.8092915005895 22.727749187571)"] |[POLYGON ((30 10\, 40 40\, 20 40\, 10 20\, 30 10)), POINT(75.8092915005895 22.727749187571)]
 ;
 
-convertFromStringViaPoint#[skip:-8.12.99, reason: spatial type geo_shape only added in 8.13]
+convertFromStringViaPoint
+required_feature: esql.spatial_shapes
+
 ROW wkt = "POINT (30 10)"
 | EVAL point = TO_GEOPOINT(wkt)
 | EVAL shape = TO_GEOSHAPE(point)
@@ -34,14 +40,18 @@ wkt:keyword     | point:geo_point | shape:geo_shape
 ;
 
 # need to work out how to upload WKT
-simpleLoad#[skip:-8.12.99, reason: spatial type geo_shape only added in 8.13]
+simpleLoad
+required_feature: esql.spatial_shapes
+
 FROM countries_bbox | WHERE id == "ISL";
 
 id:keyword|	name:keyword|	shape:geo_shape
 ISL|Iceland|BBOX(-24.538400, -13.499446, 66.536100, 63.390000)
 ;
 
-simpleLoadPointsAsShapes#[skip:-8.12.99, reason: spatial type geo_shape only added in 8.13]
+simpleLoadPointsAsShapes
+required_feature: esql.spatial_shapes
+
 FROM airports
 | WHERE abbrev == "CPH" OR abbrev == "VLC"
 | SORT abbrev
@@ -66,7 +76,12 @@ abbrev:keyword  |  region:text         |  city_location:geo_point |  airport:tex
 CPH             |  Københavns Kommune  |  POINT(12.5683 55.6761)  |  Copenhagen    |  265
 ;
 
-pointIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+###############################################
+# Tests for ST_INTERSECTS with GEO_SHAPE
+
+pointIntersectsLiteralPolygon
+required_feature: esql.st_intersects
+
 FROM airports
 | EVAL location = TO_GEOSHAPE(location)
 | WHERE ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))"))
@@ -77,7 +92,9 @@ abbrev:keyword | name:text      | location:geo_shape                     | count
 HOD            | Hodeidah Int'l | POINT(42.97109630194 14.7552534413725) | Yemen           | Al Ḩudaydah  | POINT(42.9511 14.8022)
 ;
 
-polygonIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+polygonIntersectsLiteralPolygon
+required_feature: esql.st_intersects
+
 FROM airport_city_boundaries
 | WHERE ST_INTERSECTS(city_boundary, TO_GEOSHAPE("POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))"))
 | KEEP abbrev, airport, region, city, city_location
@@ -88,7 +105,9 @@ abbrev:keyword | airport:text         | region:text | city:keyword | city_locati
 SYX            | Sanya Phoenix Int'l  | 天涯区       | Sanya        | POINT(109.5036 18.2533)
 ;
 
-pointIntersectsLiteralPolygonReversed#[skip:-8.13.99, reason:st_intersects added in 8.14]
+pointIntersectsLiteralPolygonReversed
+required_feature: esql.st_intersects
+
 FROM airports
 | EVAL location = TO_GEOSHAPE(location)
 | WHERE ST_INTERSECTS(TO_GEOSHAPE("POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))"), location)
@@ -99,7 +118,9 @@ abbrev:keyword | name:text      | location:geo_shape                     | count
 HOD            | Hodeidah Int'l | POINT(42.97109630194 14.7552534413725) | Yemen           | Al Ḩudaydah  | POINT(42.9511 14.8022)
 ;
 
-literalPointIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+literalPointIntersectsLiteralPolygon
+required_feature: esql.st_intersects
+
 ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
 | MV_EXPAND wkt
 | EVAL pt = TO_GEOPOINT(wkt)
@@ -111,7 +132,9 @@ wkt:keyword   | pt:geo_point
 "POINT(1 -1)" | POINT(1 -1)
 ;
 
-literalPointIntersectsLiteralPolygonReversed#[skip:-8.13.99, reason:st_intersects added in 8.14]
+literalPointIntersectsLiteralPolygonReversed
+required_feature: esql.st_intersects
+
 ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
 | MV_EXPAND wkt
 | EVAL pt = TO_GEOPOINT(wkt)
@@ -123,7 +146,9 @@ wkt:keyword   | pt:geo_point
 "POINT(1 -1)" | POINT(1 -1)
 ;
 
-literalPointAsShapeIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+literalPointAsShapeIntersectsLiteralPolygon
+required_feature: esql.st_intersects
+
 ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
 | MV_EXPAND wkt
 | EVAL pt = TO_GEOSHAPE(wkt)
@@ -135,7 +160,9 @@ wkt:keyword   | pt:geo_shape
 "POINT(1 -1)" | POINT(1 -1)
 ;
 
-literalPointAsShapeIntersectsLiteralPolygonReversed#[skip:-8.13.99, reason:st_intersects added in 8.14]
+literalPointAsShapeIntersectsLiteralPolygonReversed
+required_feature: esql.st_intersects
+
 ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
 | MV_EXPAND wkt
 | EVAL pt = TO_GEOSHAPE(wkt)
@@ -147,7 +174,9 @@ wkt:keyword   | pt:geo_shape
 "POINT(1 -1)" | POINT(1 -1)
 ;
 
-shapeIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+shapeIntersectsLiteralPolygon
+required_feature: esql.st_intersects
+
 FROM countries_bbox
 | WHERE ST_INTERSECTS(shape, TO_GEOSHAPE("POLYGON((29 -30, 31 -30, 31 -27.3, 29 -27.3, 29 -30))"))
 | SORT id DESC
@@ -159,7 +188,9 @@ SWZ        | Swaziland    | BBOX(30.798336, 32.133400, -25.728336, -27.316391)
 LSO        | Lesotho      | BBOX(27.013973, 29.455554, -28.570691, -30.650527)
 ;
 
-literalPolygonIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+literalPolygonIntersectsLiteralPolygon
+required_feature: esql.st_intersects
+
 ROW wkt = ["POLYGON((-20 60, -6 60, -6 66, -20 66, -20 60))", "POLYGON((20 60, 6 60, 6 66, 20 66, 20 60))"]
 | EVAL other = TO_GEOSHAPE("POLYGON((-15 64, -10 64, -10 66, -15 66, -15 64))")
 | MV_EXPAND wkt
@@ -172,7 +203,9 @@ wkt:keyword                                       | shape:geo_shape
 "POLYGON((-20 60, -6 60, -6 66, -20 66, -20 60))" | POLYGON((-20 60, -6 60, -6 66, -20 66, -20 60)) | POLYGON((-15 64, -10 64, -10 66, -15 66, -15 64))
 ;
 
-literalPolygonIntersectsLiteralPolygonOneRow#[skip:-8.13.99, reason:st_intersects added in 8.14]
+literalPolygonIntersectsLiteralPolygonOneRow
+required_feature: esql.st_intersects
+
 ROW intersects = ST_INTERSECTS(TO_GEOSHAPE("POLYGON((-20 60, -6 60, -6 66, -20 66, -20 60))"), TO_GEOSHAPE("POLYGON((-15 64, -10 64, -10 66, -15 66, -15 64))"))
 ;
 
@@ -180,7 +213,49 @@ intersects:boolean
 true
 ;
 
-geo_shapeEquals#[skip:-8.12.99, reason: spatial type geo_shape only added in 8.13]
+###############################################
+# Tests for ST_CONTAINS and ST_WITHIN with GEO_SHAPE
+
+polygonContainsLiteralPolygon
+required_feature: esql.st_contains_within
+
+// tag::st_contains-airport_city_boundaries[]
+FROM airport_city_boundaries
+| WHERE ST_CONTAINS(city_boundary, TO_GEOSHAPE("POLYGON((109.35 18.3, 109.45 18.3, 109.45 18.4, 109.35 18.4, 109.35 18.3))"))
+| KEEP abbrev, airport, region, city, city_location
+// end::st_contains-airport_city_boundaries[]
+| LIMIT 1
+;
+
+// tag::st_contains-airport_city_boundaries-result[]
+abbrev:keyword | airport:text         | region:text | city:keyword | city_location:geo_point
+SYX            | Sanya Phoenix Int'l  | 天涯区       | Sanya        | POINT(109.5036 18.2533)
+// end::st_contains-airport_city_boundaries-result[]
+;
+
+polygonWithinLiteralPolygon
+required_feature: esql.st_contains_within
+
+// tag::st_within-airport_city_boundaries[]
+FROM airport_city_boundaries
+| WHERE ST_WITHIN(city_boundary, TO_GEOSHAPE("POLYGON((109.1 18.15, 109.6 18.15, 109.6 18.65, 109.1 18.65, 109.1 18.15))"))
+| KEEP abbrev, airport, region, city, city_location
+// end::st_within-airport_city_boundaries[]
+| LIMIT 1
+;
+
+// tag::st_within-airport_city_boundaries-result[]
+abbrev:keyword | airport:text         | region:text | city:keyword | city_location:geo_point
+SYX            | Sanya Phoenix Int'l  | 天涯区       | Sanya        | POINT(109.5036 18.2533)
+// end::st_within-airport_city_boundaries-result[]
+;
+
+###############################################
+# Tests for Equality and casting with GEO_SHAPE
+
+geo_shapeEquals
+required_feature: esql.spatial_shapes
+
 ROW wkt = ["POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))", "POINT(75.8092915005895 22.727749187571)"]
 | MV_EXPAND wkt
 | EVAL pt = to_geoshape(wkt)
@@ -191,7 +266,9 @@ wkt:keyword                              |pt:geo_shape
 "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))" |POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))
 ;
 
-geo_shapeNotEquals#[skip:-8.12.99, reason: spatial type geo_shape only added in 8.13]
+geo_shapeNotEquals
+required_feature: esql.spatial_shapes
+
 ROW wkt = ["POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))", "POINT(75.8092915005895 22.727749187571)"]
 | MV_EXPAND wkt
 | EVAL pt = to_geoshape(wkt)
@@ -202,7 +279,9 @@ wkt:keyword                               |pt:geo_shape
 "POINT(75.8092915005895 22.727749187571)" |POINT(75.8092915005895 22.727749187571)
 ;
 
-convertFromStringParseError#[skip:-8.12.99, reason: spatial type geo_shape only added in 8.13]
+convertFromStringParseError
+required_feature: esql.spatial_shapes
+
 row wkt = ["POINTX(42.97109630194 14.7552534413725)", "POINT(75.8092915005895 22.727749187571)", "POINT(111)"]
 | mv_expand wkt
 | eval pt = to_geoshape(wkt)
@@ -222,7 +301,9 @@ wkt:keyword                               |pt:geo_shape
 # Tests for CARTESIAN_SHAPE type
 #
 
-convertCartesianShapeFromString#[skip:-8.12.99, reason: spatial type cartesian_shape only added in 8.13]
+convertCartesianShapeFromString
+required_feature: esql.spatial_shapes
+
 // tag::to_cartesianshape-str[]
 ROW wkt = ["POINT(4297.11 -1475.53)", "POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))"]
 | MV_EXPAND wkt
@@ -237,7 +318,9 @@ wkt:keyword               |geom:cartesian_shape
 // end::to_cartesianshape-str-result[]
 ;
 
-convertCartesianFromStringArray#[skip:-8.12.99, reason:spatial type cartesian_shape only added in 8.13]
+convertCartesianFromStringArray
+required_feature: esql.spatial_shapes
+
 row wkt = ["POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))", "POINT(7580.93 2272.77)"]
 | eval pt = to_cartesianshape(wkt);
 
@@ -245,7 +328,9 @@ wkt:keyword
 ["POLYGON ((3339584.72 1118889.97\, 4452779.63 4865942.27\, 2226389.81 4865942.27\, 1113194.90 2273030.92\, 3339584.72 1118889.97))", "POINT(7580.93 2272.77)"] |[POLYGON ((3339584.72 1118889.97\, 4452779.63 4865942.27\, 2226389.81 4865942.27\, 1113194.90 2273030.92\, 3339584.72 1118889.97)), POINT(7580.93 2272.77)]
 ;
 
-convertCartesianFromStringViaPoint#[skip:-8.12.99, reason: spatial type cartesian_shape only added in 8.13]
+convertCartesianFromStringViaPoint
+required_feature: esql.spatial_shapes
+
 ROW wkt = "POINT (3010 -1010)"
 | EVAL point = TO_CARTESIANPOINT(wkt)
 | EVAL shape = TO_CARTESIANSHAPE(point)
@@ -256,14 +341,18 @@ wkt:keyword          | point:cartesian_point | shape:cartesian_shape
 ;
 
 # need to work out how to upload WKT
-simpleCartesianShapeLoad#[skip:-8.12.99, reason: spatial type cartesian_shape only added in 8.13]
+simpleCartesianShapeLoad
+required_feature: esql.spatial_shapes
+
 FROM countries_bbox_web | WHERE id == "ISL";
 
 id:keyword|	name:keyword|shape:cartesian_shape
 ISL|Iceland|BBOX(-2731602.192501422, -1502751.454502109, 1.0025136653899286E7, 9196525.03584683)
 ;
 
-simpleLoadCartesianPointsAsShapes#[skip:-8.12.99, reason: spatial type cartesian_shape only added in 8.13]
+simpleLoadCartesianPointsAsShapes
+required_feature: esql.spatial_shapes
+
 FROM airports_web
 | WHERE abbrev == "CPH" OR abbrev == "VLC"
 | SORT abbrev
@@ -275,7 +364,12 @@ abbrev:keyword | name:text    | scalerank:integer | type:keyword | location:cart
 "VLC"          | "Valencia"   | 8                 | "mid"        | POINT(-52706.98819688343 4792315.469321795)
 ;
 
-cartesianPointIntersectsPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+###############################################
+# Tests for ST_INTERSECTS with CARTESIAN_SHAPE
+
+cartesianPointIntersectsPolygon
+required_feature: esql.st_intersects
+
 FROM airports_web
 | EVAL location = TO_CARTESIANSHAPE(location)
 | WHERE ST_INTERSECTS(location, TO_CARTESIANSHAPE("POLYGON((4700000 1600000, 4800000 1600000, 4800000 1700000, 4700000 1700000, 4700000 1600000))"))
@@ -286,7 +380,9 @@ abbrev:keyword |  name:text     | location:cartesian_shape                     |
 HOD            | Hodeidah Int'l | POINT (4783520.559160681 1661010.0197476079) | 9           | mid
 ;
 
-literalCartesianPointIntersectsPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+literalCartesianPointIntersectsPolygon
+required_feature: esql.st_intersects
+
 ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
 | MV_EXPAND wkt
 | EVAL pt = TO_CARTESIANSHAPE(wkt)
@@ -298,7 +394,9 @@ wkt:keyword   | pt:cartesian_shape
 "POINT(1 -1)" | POINT(1 -1)
 ;
 
-cartesianShapeIntersectsPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+cartesianShapeIntersectsPolygon
+required_feature: esql.st_intersects
+
 FROM countries_bbox_web
 | WHERE ST_INTERSECTS(shape, TO_CARTESIANSHAPE("POLYGON((3100000 -3400000, 3500000 -3400000, 3500000 -3150000, 3100000 -3150000, 3100000 -3400000))"))
 | SORT id DESC
@@ -310,7 +408,9 @@ SWZ        | Swaziland    | BBOX(3428455.080322901, 3577073.7249586442, -2965472
 LSO        | Lesotho      | BBOX(3007181.718244638, 3278977.271857335, -3321117.2692412077, -3587446.106149188)
 ;
 
-literalCartesianPolygonIntersectsPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+literalCartesianPolygonIntersectsPolygon
+required_feature: esql.st_intersects
+
 ROW wkt = ["POLYGON((-2000 6000, -600 6000, -600 6600, -2000 6600, -2000 6000))", "POLYGON((2000 6000, 600 6000, 600 6600, 2000 6600, 2000 6000))"]
 | MV_EXPAND wkt
 | EVAL shape = TO_CARTESIANSHAPE(wkt)
@@ -322,7 +422,41 @@ wkt:keyword                                                           | shape:ca
 "POLYGON((-2000 6000, -600 6000, -600 6600, -2000 6600, -2000 6000))" | POLYGON((-2000 6000, -600 6000, -600 6600, -2000 6600, -2000 6000)) | POLYGON((-1500 6400, -1000 6400, -1000 6600, -1500 6600, -1500 6400))
 ;
 
-cartesianshapeEquals#[skip:-8.12.99, reason: spatial type cartesian_shape only added in 8.13]
+###############################################
+# Tests for ST_CONTAINS and ST_WITHIN with CARTESIAN_SHAPE
+
+cartesianShapeContainsPolygon
+required_feature: esql.st_contains_within
+
+FROM countries_bbox_web
+| WHERE ST_CONTAINS(shape, TO_CARTESIANSHAPE("POLYGON((3100000 -3400000, 3500000 -3400000, 3500000 -3150000, 3100000 -3150000, 3100000 -3400000))"))
+| SORT id DESC
+;
+
+id:keyword | name:keyword | shape:cartesian_shape
+ZAF        | South Africa | BBOX(1834915.5679635953, 4218142.412200545, -2527908.4975596936, -5937134.146607068)
+;
+
+cartesianShapeWithinPolygon
+required_feature: esql.st_contains_within
+
+FROM countries_bbox_web
+| WHERE ST_WITHIN(shape, TO_CARTESIANSHAPE("POLYGON((1800000 -2500000, 4300000 -2500000, 4300000 -6000000, 1800000 -6000000, 1800000 -2500000))"))
+| SORT id DESC
+;
+
+id:keyword | name:keyword | shape:cartesian_shape
+ZAF        | South Africa | BBOX(1834915.5679635953, 4218142.412200545, -2527908.4975596936, -5937134.146607068)
+SWZ        | Swaziland    | BBOX(3428455.080322901, 3577073.7249586442, -2965472.9128583763, -3163056.5390926218)
+LSO        | Lesotho      | BBOX(3007181.718244638, 3278977.271857335, -3321117.2692412077, -3587446.106149188)
+;
+
+###############################################
+# Tests for Equality and casting with CARTESIAN_SHAPE
+
+cartesianshapeEquals
+required_feature: esql.spatial_shapes
+
 ROW wkt = ["POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))", "POINT(7580.93 2272.77)"]
 | MV_EXPAND wkt
 | EVAL pt = to_cartesianshape(wkt)
@@ -333,7 +467,9 @@ wkt:keyword               |pt:cartesian_shape
 "POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))" |POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))
 ;
 
-cartesianShapeNotEquals#[skip:-8.12.99, reason: spatial type cartesian_shape only added in 8.13]
+cartesianShapeNotEquals
+required_feature: esql.spatial_shapes
+
 ROW wkt = ["POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))", "POINT(7580.93 2272.77)"]
 | MV_EXPAND wkt
 | EVAL pt = to_cartesianshape(wkt)
@@ -344,7 +480,9 @@ wkt:keyword              |pt:cartesian_shape
 "POINT(7580.93 2272.77)" |POINT(7580.93 2272.77)
 ;
 
-convertCartesianShapeFromStringParseError#[skip:-8.12.99, reason: spatial type cartesian_shape only added in 8.13]
+convertCartesianShapeFromStringParseError
+required_feature: esql.spatial_shapes
+
 row wkt = ["POINTX(4297.11 -1475.53)", "POINT(7580.93 2272.77)", "POINT(111)"]
 | mv_expand wkt
 | eval pt = to_cartesianshape(wkt)

+ 128 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsCartesianPointDocValuesAndConstantEvaluator.java

@@ -0,0 +1,128 @@
+// 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.geo.Component2D;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialContains}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialContainsCartesianPointDocValuesAndConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final Component2D rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialContainsCartesianPointDocValuesAndConstantEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, Component2D rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock leftValueBlock = (LongBlock) leftValue.eval(page)) {
+      LongVector leftValueVector = leftValueBlock.asVector();
+      if (leftValueVector == null) {
+        return eval(page.getPositionCount(), leftValueBlock);
+      }
+      return eval(page.getPositionCount(), leftValueVector);
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongBlock leftValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialContains.processCartesianPointDocValuesAndConstant(leftValueBlock.getLong(leftValueBlock.getFirstValueIndex(p)), rightValue));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongVector leftValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialContains.processCartesianPointDocValuesAndConstant(leftValueVector.getLong(p), rightValue));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialContainsCartesianPointDocValuesAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final Component2D rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        Component2D rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialContainsCartesianPointDocValuesAndConstantEvaluator get(DriverContext context) {
+      return new SpatialContainsCartesianPointDocValuesAndConstantEvaluator(source, leftValue.get(context), rightValue, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialContainsCartesianPointDocValuesAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 142 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsCartesianPointDocValuesAndSourceEvaluator.java

@@ -0,0 +1,142 @@
+// 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.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BooleanVector;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialContains}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialContainsCartesianPointDocValuesAndSourceEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final EvalOperator.ExpressionEvaluator rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialContainsCartesianPointDocValuesAndSourceEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, EvalOperator.ExpressionEvaluator rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock leftValueBlock = (LongBlock) leftValue.eval(page)) {
+      try (BytesRefBlock rightValueBlock = (BytesRefBlock) rightValue.eval(page)) {
+        LongVector leftValueVector = leftValueBlock.asVector();
+        if (leftValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        BytesRefVector rightValueVector = rightValueBlock.asVector();
+        if (rightValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        return eval(page.getPositionCount(), leftValueVector, rightValueVector).asBlock();
+      }
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongBlock leftValueBlock,
+      BytesRefBlock rightValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.getValueCount(p) != 1) {
+          if (rightValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendBoolean(SpatialContains.processCartesianPointDocValuesAndSource(leftValueBlock.getLong(leftValueBlock.getFirstValueIndex(p)), rightValueBlock.getBytesRef(rightValueBlock.getFirstValueIndex(p), rightValueScratch)));
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanVector eval(int positionCount, LongVector leftValueVector,
+      BytesRefVector rightValueVector) {
+    try(BooleanVector.Builder result = driverContext.blockFactory().newBooleanVectorBuilder(positionCount)) {
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendBoolean(SpatialContains.processCartesianPointDocValuesAndSource(leftValueVector.getLong(p), rightValueVector.getBytesRef(p, rightValueScratch)));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialContainsCartesianPointDocValuesAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue, rightValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final EvalOperator.ExpressionEvaluator.Factory rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        EvalOperator.ExpressionEvaluator.Factory rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialContainsCartesianPointDocValuesAndSourceEvaluator get(DriverContext context) {
+      return new SpatialContainsCartesianPointDocValuesAndSourceEvaluator(source, leftValue.get(context), rightValue.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialContainsCartesianPointDocValuesAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 132 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsCartesianSourceAndConstantEvaluator.java

@@ -0,0 +1,132 @@
+// 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.geo.Component2D;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialContains}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialContainsCartesianSourceAndConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final Component2D rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialContainsCartesianSourceAndConstantEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, Component2D rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock leftValueBlock = (BytesRefBlock) leftValue.eval(page)) {
+      BytesRefVector leftValueVector = leftValueBlock.asVector();
+      if (leftValueVector == null) {
+        return eval(page.getPositionCount(), leftValueBlock);
+      }
+      return eval(page.getPositionCount(), leftValueVector);
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefBlock leftValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialContains.processCartesianSourceAndConstant(leftValueBlock.getBytesRef(leftValueBlock.getFirstValueIndex(p), leftValueScratch), rightValue));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefVector leftValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialContains.processCartesianSourceAndConstant(leftValueVector.getBytesRef(p, leftValueScratch), rightValue));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialContainsCartesianSourceAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final Component2D rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        Component2D rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialContainsCartesianSourceAndConstantEvaluator get(DriverContext context) {
+      return new SpatialContainsCartesianSourceAndConstantEvaluator(source, leftValue.get(context), rightValue, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialContainsCartesianSourceAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 152 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsCartesianSourceAndSourceEvaluator.java

@@ -0,0 +1,152 @@
+// 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.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialContains}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialContainsCartesianSourceAndSourceEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final EvalOperator.ExpressionEvaluator rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialContainsCartesianSourceAndSourceEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, EvalOperator.ExpressionEvaluator rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock leftValueBlock = (BytesRefBlock) leftValue.eval(page)) {
+      try (BytesRefBlock rightValueBlock = (BytesRefBlock) rightValue.eval(page)) {
+        BytesRefVector leftValueVector = leftValueBlock.asVector();
+        if (leftValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        BytesRefVector rightValueVector = rightValueBlock.asVector();
+        if (rightValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        return eval(page.getPositionCount(), leftValueVector, rightValueVector);
+      }
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefBlock leftValueBlock,
+      BytesRefBlock rightValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.getValueCount(p) != 1) {
+          if (rightValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialContains.processCartesianSourceAndSource(leftValueBlock.getBytesRef(leftValueBlock.getFirstValueIndex(p), leftValueScratch), rightValueBlock.getBytesRef(rightValueBlock.getFirstValueIndex(p), rightValueScratch)));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefVector leftValueVector,
+      BytesRefVector rightValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialContains.processCartesianSourceAndSource(leftValueVector.getBytesRef(p, leftValueScratch), rightValueVector.getBytesRef(p, rightValueScratch)));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialContainsCartesianSourceAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue, rightValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final EvalOperator.ExpressionEvaluator.Factory rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        EvalOperator.ExpressionEvaluator.Factory rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialContainsCartesianSourceAndSourceEvaluator get(DriverContext context) {
+      return new SpatialContainsCartesianSourceAndSourceEvaluator(source, leftValue.get(context), rightValue.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialContainsCartesianSourceAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 128 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsGeoPointDocValuesAndConstantEvaluator.java

@@ -0,0 +1,128 @@
+// 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.geo.Component2D;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialContains}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialContainsGeoPointDocValuesAndConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final Component2D rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialContainsGeoPointDocValuesAndConstantEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, Component2D rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock leftValueBlock = (LongBlock) leftValue.eval(page)) {
+      LongVector leftValueVector = leftValueBlock.asVector();
+      if (leftValueVector == null) {
+        return eval(page.getPositionCount(), leftValueBlock);
+      }
+      return eval(page.getPositionCount(), leftValueVector);
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongBlock leftValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialContains.processGeoPointDocValuesAndConstant(leftValueBlock.getLong(leftValueBlock.getFirstValueIndex(p)), rightValue));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongVector leftValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialContains.processGeoPointDocValuesAndConstant(leftValueVector.getLong(p), rightValue));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialContainsGeoPointDocValuesAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final Component2D rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        Component2D rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialContainsGeoPointDocValuesAndConstantEvaluator get(DriverContext context) {
+      return new SpatialContainsGeoPointDocValuesAndConstantEvaluator(source, leftValue.get(context), rightValue, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialContainsGeoPointDocValuesAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 151 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsGeoPointDocValuesAndSourceEvaluator.java

@@ -0,0 +1,151 @@
+// 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.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialContains}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialContainsGeoPointDocValuesAndSourceEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final EvalOperator.ExpressionEvaluator rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialContainsGeoPointDocValuesAndSourceEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, EvalOperator.ExpressionEvaluator rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock leftValueBlock = (LongBlock) leftValue.eval(page)) {
+      try (BytesRefBlock rightValueBlock = (BytesRefBlock) rightValue.eval(page)) {
+        LongVector leftValueVector = leftValueBlock.asVector();
+        if (leftValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        BytesRefVector rightValueVector = rightValueBlock.asVector();
+        if (rightValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        return eval(page.getPositionCount(), leftValueVector, rightValueVector);
+      }
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongBlock leftValueBlock,
+      BytesRefBlock rightValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.getValueCount(p) != 1) {
+          if (rightValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialContains.processGeoPointDocValuesAndSource(leftValueBlock.getLong(leftValueBlock.getFirstValueIndex(p)), rightValueBlock.getBytesRef(rightValueBlock.getFirstValueIndex(p), rightValueScratch)));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongVector leftValueVector,
+      BytesRefVector rightValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialContains.processGeoPointDocValuesAndSource(leftValueVector.getLong(p), rightValueVector.getBytesRef(p, rightValueScratch)));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialContainsGeoPointDocValuesAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue, rightValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final EvalOperator.ExpressionEvaluator.Factory rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        EvalOperator.ExpressionEvaluator.Factory rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialContainsGeoPointDocValuesAndSourceEvaluator get(DriverContext context) {
+      return new SpatialContainsGeoPointDocValuesAndSourceEvaluator(source, leftValue.get(context), rightValue.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialContainsGeoPointDocValuesAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 132 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsGeoSourceAndConstantEvaluator.java

@@ -0,0 +1,132 @@
+// 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.geo.Component2D;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialContains}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialContainsGeoSourceAndConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final Component2D rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialContainsGeoSourceAndConstantEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, Component2D rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock leftValueBlock = (BytesRefBlock) leftValue.eval(page)) {
+      BytesRefVector leftValueVector = leftValueBlock.asVector();
+      if (leftValueVector == null) {
+        return eval(page.getPositionCount(), leftValueBlock);
+      }
+      return eval(page.getPositionCount(), leftValueVector);
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefBlock leftValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialContains.processGeoSourceAndConstant(leftValueBlock.getBytesRef(leftValueBlock.getFirstValueIndex(p), leftValueScratch), rightValue));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefVector leftValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialContains.processGeoSourceAndConstant(leftValueVector.getBytesRef(p, leftValueScratch), rightValue));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialContainsGeoSourceAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final Component2D rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        Component2D rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialContainsGeoSourceAndConstantEvaluator get(DriverContext context) {
+      return new SpatialContainsGeoSourceAndConstantEvaluator(source, leftValue.get(context), rightValue, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialContainsGeoSourceAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 152 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsGeoSourceAndSourceEvaluator.java

@@ -0,0 +1,152 @@
+// 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.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialContains}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialContainsGeoSourceAndSourceEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final EvalOperator.ExpressionEvaluator rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialContainsGeoSourceAndSourceEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, EvalOperator.ExpressionEvaluator rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock leftValueBlock = (BytesRefBlock) leftValue.eval(page)) {
+      try (BytesRefBlock rightValueBlock = (BytesRefBlock) rightValue.eval(page)) {
+        BytesRefVector leftValueVector = leftValueBlock.asVector();
+        if (leftValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        BytesRefVector rightValueVector = rightValueBlock.asVector();
+        if (rightValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        return eval(page.getPositionCount(), leftValueVector, rightValueVector);
+      }
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefBlock leftValueBlock,
+      BytesRefBlock rightValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.getValueCount(p) != 1) {
+          if (rightValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialContains.processGeoSourceAndSource(leftValueBlock.getBytesRef(leftValueBlock.getFirstValueIndex(p), leftValueScratch), rightValueBlock.getBytesRef(rightValueBlock.getFirstValueIndex(p), rightValueScratch)));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefVector leftValueVector,
+      BytesRefVector rightValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialContains.processGeoSourceAndSource(leftValueVector.getBytesRef(p, leftValueScratch), rightValueVector.getBytesRef(p, rightValueScratch)));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialContainsGeoSourceAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue, rightValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final EvalOperator.ExpressionEvaluator.Factory rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        EvalOperator.ExpressionEvaluator.Factory rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialContainsGeoSourceAndSourceEvaluator get(DriverContext context) {
+      return new SpatialContainsGeoSourceAndSourceEvaluator(source, leftValue.get(context), rightValue.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialContainsGeoSourceAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 128 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinCartesianPointDocValuesAndConstantEvaluator.java

@@ -0,0 +1,128 @@
+// 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.geo.Component2D;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialWithin}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialWithinCartesianPointDocValuesAndConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final Component2D rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialWithinCartesianPointDocValuesAndConstantEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, Component2D rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock leftValueBlock = (LongBlock) leftValue.eval(page)) {
+      LongVector leftValueVector = leftValueBlock.asVector();
+      if (leftValueVector == null) {
+        return eval(page.getPositionCount(), leftValueBlock);
+      }
+      return eval(page.getPositionCount(), leftValueVector);
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongBlock leftValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialWithin.processCartesianPointDocValuesAndConstant(leftValueBlock.getLong(leftValueBlock.getFirstValueIndex(p)), rightValue));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongVector leftValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialWithin.processCartesianPointDocValuesAndConstant(leftValueVector.getLong(p), rightValue));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialWithinCartesianPointDocValuesAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final Component2D rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        Component2D rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialWithinCartesianPointDocValuesAndConstantEvaluator get(DriverContext context) {
+      return new SpatialWithinCartesianPointDocValuesAndConstantEvaluator(source, leftValue.get(context), rightValue, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialWithinCartesianPointDocValuesAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 142 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinCartesianPointDocValuesAndSourceEvaluator.java

@@ -0,0 +1,142 @@
+// 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.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BooleanVector;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialWithin}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialWithinCartesianPointDocValuesAndSourceEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final EvalOperator.ExpressionEvaluator rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialWithinCartesianPointDocValuesAndSourceEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, EvalOperator.ExpressionEvaluator rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock leftValueBlock = (LongBlock) leftValue.eval(page)) {
+      try (BytesRefBlock rightValueBlock = (BytesRefBlock) rightValue.eval(page)) {
+        LongVector leftValueVector = leftValueBlock.asVector();
+        if (leftValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        BytesRefVector rightValueVector = rightValueBlock.asVector();
+        if (rightValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        return eval(page.getPositionCount(), leftValueVector, rightValueVector).asBlock();
+      }
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongBlock leftValueBlock,
+      BytesRefBlock rightValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.getValueCount(p) != 1) {
+          if (rightValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendBoolean(SpatialWithin.processCartesianPointDocValuesAndSource(leftValueBlock.getLong(leftValueBlock.getFirstValueIndex(p)), rightValueBlock.getBytesRef(rightValueBlock.getFirstValueIndex(p), rightValueScratch)));
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanVector eval(int positionCount, LongVector leftValueVector,
+      BytesRefVector rightValueVector) {
+    try(BooleanVector.Builder result = driverContext.blockFactory().newBooleanVectorBuilder(positionCount)) {
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendBoolean(SpatialWithin.processCartesianPointDocValuesAndSource(leftValueVector.getLong(p), rightValueVector.getBytesRef(p, rightValueScratch)));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialWithinCartesianPointDocValuesAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue, rightValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final EvalOperator.ExpressionEvaluator.Factory rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        EvalOperator.ExpressionEvaluator.Factory rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialWithinCartesianPointDocValuesAndSourceEvaluator get(DriverContext context) {
+      return new SpatialWithinCartesianPointDocValuesAndSourceEvaluator(source, leftValue.get(context), rightValue.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialWithinCartesianPointDocValuesAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 132 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinCartesianSourceAndConstantEvaluator.java

@@ -0,0 +1,132 @@
+// 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.geo.Component2D;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialWithin}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialWithinCartesianSourceAndConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final Component2D rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialWithinCartesianSourceAndConstantEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, Component2D rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock leftValueBlock = (BytesRefBlock) leftValue.eval(page)) {
+      BytesRefVector leftValueVector = leftValueBlock.asVector();
+      if (leftValueVector == null) {
+        return eval(page.getPositionCount(), leftValueBlock);
+      }
+      return eval(page.getPositionCount(), leftValueVector);
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefBlock leftValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialWithin.processCartesianSourceAndConstant(leftValueBlock.getBytesRef(leftValueBlock.getFirstValueIndex(p), leftValueScratch), rightValue));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefVector leftValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialWithin.processCartesianSourceAndConstant(leftValueVector.getBytesRef(p, leftValueScratch), rightValue));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialWithinCartesianSourceAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final Component2D rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        Component2D rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialWithinCartesianSourceAndConstantEvaluator get(DriverContext context) {
+      return new SpatialWithinCartesianSourceAndConstantEvaluator(source, leftValue.get(context), rightValue, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialWithinCartesianSourceAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 152 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinCartesianSourceAndSourceEvaluator.java

@@ -0,0 +1,152 @@
+// 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.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialWithin}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialWithinCartesianSourceAndSourceEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final EvalOperator.ExpressionEvaluator rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialWithinCartesianSourceAndSourceEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, EvalOperator.ExpressionEvaluator rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock leftValueBlock = (BytesRefBlock) leftValue.eval(page)) {
+      try (BytesRefBlock rightValueBlock = (BytesRefBlock) rightValue.eval(page)) {
+        BytesRefVector leftValueVector = leftValueBlock.asVector();
+        if (leftValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        BytesRefVector rightValueVector = rightValueBlock.asVector();
+        if (rightValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        return eval(page.getPositionCount(), leftValueVector, rightValueVector);
+      }
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefBlock leftValueBlock,
+      BytesRefBlock rightValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.getValueCount(p) != 1) {
+          if (rightValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialWithin.processCartesianSourceAndSource(leftValueBlock.getBytesRef(leftValueBlock.getFirstValueIndex(p), leftValueScratch), rightValueBlock.getBytesRef(rightValueBlock.getFirstValueIndex(p), rightValueScratch)));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefVector leftValueVector,
+      BytesRefVector rightValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialWithin.processCartesianSourceAndSource(leftValueVector.getBytesRef(p, leftValueScratch), rightValueVector.getBytesRef(p, rightValueScratch)));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialWithinCartesianSourceAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue, rightValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final EvalOperator.ExpressionEvaluator.Factory rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        EvalOperator.ExpressionEvaluator.Factory rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialWithinCartesianSourceAndSourceEvaluator get(DriverContext context) {
+      return new SpatialWithinCartesianSourceAndSourceEvaluator(source, leftValue.get(context), rightValue.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialWithinCartesianSourceAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 128 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinGeoPointDocValuesAndConstantEvaluator.java

@@ -0,0 +1,128 @@
+// 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.geo.Component2D;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialWithin}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialWithinGeoPointDocValuesAndConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final Component2D rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialWithinGeoPointDocValuesAndConstantEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, Component2D rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock leftValueBlock = (LongBlock) leftValue.eval(page)) {
+      LongVector leftValueVector = leftValueBlock.asVector();
+      if (leftValueVector == null) {
+        return eval(page.getPositionCount(), leftValueBlock);
+      }
+      return eval(page.getPositionCount(), leftValueVector);
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongBlock leftValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialWithin.processGeoPointDocValuesAndConstant(leftValueBlock.getLong(leftValueBlock.getFirstValueIndex(p)), rightValue));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongVector leftValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialWithin.processGeoPointDocValuesAndConstant(leftValueVector.getLong(p), rightValue));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialWithinGeoPointDocValuesAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final Component2D rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        Component2D rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialWithinGeoPointDocValuesAndConstantEvaluator get(DriverContext context) {
+      return new SpatialWithinGeoPointDocValuesAndConstantEvaluator(source, leftValue.get(context), rightValue, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialWithinGeoPointDocValuesAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 151 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinGeoPointDocValuesAndSourceEvaluator.java

@@ -0,0 +1,151 @@
+// 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.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.LongBlock;
+import org.elasticsearch.compute.data.LongVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialWithin}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialWithinGeoPointDocValuesAndSourceEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final EvalOperator.ExpressionEvaluator rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialWithinGeoPointDocValuesAndSourceEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, EvalOperator.ExpressionEvaluator rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (LongBlock leftValueBlock = (LongBlock) leftValue.eval(page)) {
+      try (BytesRefBlock rightValueBlock = (BytesRefBlock) rightValue.eval(page)) {
+        LongVector leftValueVector = leftValueBlock.asVector();
+        if (leftValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        BytesRefVector rightValueVector = rightValueBlock.asVector();
+        if (rightValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        return eval(page.getPositionCount(), leftValueVector, rightValueVector);
+      }
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongBlock leftValueBlock,
+      BytesRefBlock rightValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.getValueCount(p) != 1) {
+          if (rightValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialWithin.processGeoPointDocValuesAndSource(leftValueBlock.getLong(leftValueBlock.getFirstValueIndex(p)), rightValueBlock.getBytesRef(rightValueBlock.getFirstValueIndex(p), rightValueScratch)));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, LongVector leftValueVector,
+      BytesRefVector rightValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialWithin.processGeoPointDocValuesAndSource(leftValueVector.getLong(p), rightValueVector.getBytesRef(p, rightValueScratch)));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialWithinGeoPointDocValuesAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue, rightValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final EvalOperator.ExpressionEvaluator.Factory rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        EvalOperator.ExpressionEvaluator.Factory rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialWithinGeoPointDocValuesAndSourceEvaluator get(DriverContext context) {
+      return new SpatialWithinGeoPointDocValuesAndSourceEvaluator(source, leftValue.get(context), rightValue.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialWithinGeoPointDocValuesAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 132 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinGeoSourceAndConstantEvaluator.java

@@ -0,0 +1,132 @@
+// 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.geo.Component2D;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialWithin}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialWithinGeoSourceAndConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final Component2D rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialWithinGeoSourceAndConstantEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, Component2D rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock leftValueBlock = (BytesRefBlock) leftValue.eval(page)) {
+      BytesRefVector leftValueVector = leftValueBlock.asVector();
+      if (leftValueVector == null) {
+        return eval(page.getPositionCount(), leftValueBlock);
+      }
+      return eval(page.getPositionCount(), leftValueVector);
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefBlock leftValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialWithin.processGeoSourceAndConstant(leftValueBlock.getBytesRef(leftValueBlock.getFirstValueIndex(p), leftValueScratch), rightValue));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefVector leftValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialWithin.processGeoSourceAndConstant(leftValueVector.getBytesRef(p, leftValueScratch), rightValue));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialWithinGeoSourceAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final Component2D rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        Component2D rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialWithinGeoSourceAndConstantEvaluator get(DriverContext context) {
+      return new SpatialWithinGeoSourceAndConstantEvaluator(source, leftValue.get(context), rightValue, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialWithinGeoSourceAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 152 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinGeoSourceAndSourceEvaluator.java

@@ -0,0 +1,152 @@
+// 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.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BooleanBlock;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link SpatialWithin}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialWithinGeoSourceAndSourceEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final EvalOperator.ExpressionEvaluator rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialWithinGeoSourceAndSourceEvaluator(Source source,
+      EvalOperator.ExpressionEvaluator leftValue, EvalOperator.ExpressionEvaluator rightValue,
+      DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.leftValue = leftValue;
+    this.rightValue = rightValue;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock leftValueBlock = (BytesRefBlock) leftValue.eval(page)) {
+      try (BytesRefBlock rightValueBlock = (BytesRefBlock) rightValue.eval(page)) {
+        BytesRefVector leftValueVector = leftValueBlock.asVector();
+        if (leftValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        BytesRefVector rightValueVector = rightValueBlock.asVector();
+        if (rightValueVector == null) {
+          return eval(page.getPositionCount(), leftValueBlock, rightValueBlock);
+        }
+        return eval(page.getPositionCount(), leftValueVector, rightValueVector);
+      }
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefBlock leftValueBlock,
+      BytesRefBlock rightValueBlock) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (leftValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (leftValueBlock.getValueCount(p) != 1) {
+          if (leftValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (rightValueBlock.getValueCount(p) != 1) {
+          if (rightValueBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBoolean(SpatialWithin.processGeoSourceAndSource(leftValueBlock.getBytesRef(leftValueBlock.getFirstValueIndex(p), leftValueScratch), rightValueBlock.getBytesRef(rightValueBlock.getFirstValueIndex(p), rightValueScratch)));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BooleanBlock eval(int positionCount, BytesRefVector leftValueVector,
+      BytesRefVector rightValueVector) {
+    try(BooleanBlock.Builder result = driverContext.blockFactory().newBooleanBlockBuilder(positionCount)) {
+      BytesRef leftValueScratch = new BytesRef();
+      BytesRef rightValueScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBoolean(SpatialWithin.processGeoSourceAndSource(leftValueVector.getBytesRef(p, leftValueScratch), rightValueVector.getBytesRef(p, rightValueScratch)));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialWithinGeoSourceAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(leftValue, rightValue);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory leftValue;
+
+    private final EvalOperator.ExpressionEvaluator.Factory rightValue;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory leftValue,
+        EvalOperator.ExpressionEvaluator.Factory rightValue) {
+      this.source = source;
+      this.leftValue = leftValue;
+      this.rightValue = rightValue;
+    }
+
+    @Override
+    public SpatialWithinGeoSourceAndSourceEvaluator get(DriverContext context) {
+      return new SpatialWithinGeoSourceAndSourceEvaluator(source, leftValue.get(context), rightValue.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialWithinGeoSourceAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 4 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java

@@ -79,7 +79,9 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSort
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSum;
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvZip;
 import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialContains;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialIntersects;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialWithin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StY;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat;
@@ -182,7 +184,9 @@ public final class EsqlFunctionRegistry extends FunctionRegistry {
             // spatial
             new FunctionDefinition[] {
                 def(SpatialCentroid.class, SpatialCentroid::new, "st_centroid"),
+                def(SpatialContains.class, SpatialContains::new, "st_contains"),
                 def(SpatialIntersects.class, SpatialIntersects::new, "st_intersects"),
+                def(SpatialWithin.class, SpatialWithin::new, "st_within"),
                 def(StX.class, StX::new, "st_x"),
                 def(StY.class, StY::new, "st_y") },
             // conditional

+ 91 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/LuceneComponent2DUtils.java

@@ -0,0 +1,91 @@
+/*
+ * 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.geo.Component2D;
+import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.geo.Rectangle;
+import org.apache.lucene.geo.XYGeometry;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This utilities class provides access to protected methods in Lucene using alternative APIs.
+ * For example, the 'create' method returns the original Component2D array, instead of a Component2D containing
+ * a component tree of potentially multiple components. This is particularly useful for algorithms that need to
+ * operate on each component individually.
+ */
+public class LuceneComponent2DUtils {
+    /**
+     * This method is based on LatLonGeometry.create, but returns an array of Component2D objects for multi-component geometries.
+     */
+    public static Component2D[] createLatLonComponents(LatLonGeometry... latLonGeometries) {
+        if (latLonGeometries == null) {
+            throw new IllegalArgumentException("geometries must not be null");
+        } else if (latLonGeometries.length == 0) {
+            throw new IllegalArgumentException("geometries must not be empty");
+        } else {
+            final List<Component2D> components = new ArrayList<>(latLonGeometries.length);
+
+            for (int i = 0; i < latLonGeometries.length; ++i) {
+                if (latLonGeometries[i] == null) {
+                    throw new IllegalArgumentException("geometries[" + i + "] must not be null");
+                }
+
+                if (latLonGeometries[i] instanceof Rectangle rectangle && rectangle.crossesDateline()) {
+                    addRectangle(components, rectangle);
+                } else {
+                    components.add(LatLonGeometry.create(latLonGeometries[i]));
+                }
+            }
+
+            return components.toArray(new Component2D[0]);
+        }
+    }
+
+    private static void addRectangle(List<Component2D> components, Rectangle rectangle) {
+        double minLongitude = rectangle.minLon;
+        boolean crossesDateline = rectangle.minLon > rectangle.maxLon;
+        if (minLongitude == 180.0 && crossesDateline) {
+            minLongitude = -180.0;
+            crossesDateline = false;
+        }
+        if (crossesDateline) {
+            Rectangle left = new Rectangle(rectangle.minLat, rectangle.maxLat, -180.0, rectangle.maxLon);
+            Rectangle right = new Rectangle(rectangle.minLat, rectangle.maxLat, minLongitude, 180.0);
+            components.add(LatLonGeometry.create(left));
+            components.add(LatLonGeometry.create(right));
+        } else {
+            components.add(LatLonGeometry.create(rectangle));
+        }
+    }
+
+    /**
+     * This method is based on XYGeometry.create, but returns an array of Component2D objects for multi-component geometries.
+     */
+    public static Component2D[] createXYComponents(XYGeometry... xyGeometries) {
+        if (xyGeometries == null) {
+            throw new IllegalArgumentException("geometries must not be null");
+        } else if (xyGeometries.length == 0) {
+            throw new IllegalArgumentException("geometries must not be empty");
+        } else {
+            Component2D[] components = new Component2D[xyGeometries.length];
+
+            for (int i = 0; i < xyGeometries.length; ++i) {
+                if (xyGeometries[i] == null) {
+                    throw new IllegalArgumentException("geometries[" + i + "] must not be null");
+                }
+
+                components[i] = XYGeometry.create(xyGeometries[i]);
+            }
+
+            return components;
+        }
+    }
+}

+ 287 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContains.java

@@ -0,0 +1,287 @@
+/*
+ * 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.document.ShapeField;
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.geo.Orientation;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+import org.elasticsearch.geometry.Geometry;
+import org.elasticsearch.index.mapper.GeoShapeIndexer;
+import org.elasticsearch.index.mapper.ShapeIndexer;
+import org.elasticsearch.lucene.spatial.CartesianShapeIndexer;
+import org.elasticsearch.lucene.spatial.CoordinateEncoder;
+import org.elasticsearch.lucene.spatial.GeometryDocValueReader;
+import org.elasticsearch.xpack.esql.expression.function.Example;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.FieldAttribute;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.util.SpatialCoordinateTypes;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils.asGeometryDocValueReader;
+import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils.asLuceneComponent2Ds;
+import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils.makeGeometryFromLiteral;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.CARTESIAN_POINT;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.CARTESIAN_SHAPE;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.GEO_POINT;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.GEO_SHAPE;
+
+/**
+ * This is the primary class for supporting the function ST_CONTAINS.
+ * The bulk of the capabilities are within the parent class SpatialRelatesFunction,
+ * which supports all the relations in the ShapeField.QueryRelation enum.
+ * Here we simply wire the rules together specific to ST_CONTAINS and QueryRelation.CONTAINS.
+ */
+public class SpatialContains extends SpatialRelatesFunction {
+    // public for test access with reflection
+    public static final SpatialRelationsContains GEO = new SpatialRelationsContains(
+        SpatialCoordinateTypes.GEO,
+        CoordinateEncoder.GEO,
+        new GeoShapeIndexer(Orientation.CCW, "ST_Contains")
+    );
+    // public for test access with reflection
+    public static final SpatialRelationsContains CARTESIAN = new SpatialRelationsContains(
+        SpatialCoordinateTypes.CARTESIAN,
+        CoordinateEncoder.CARTESIAN,
+        new CartesianShapeIndexer("ST_Contains")
+    );
+
+    /**
+     * We override the normal behaviour for CONTAINS because we need to test each component separately.
+     * This applies to multi-component geometries (MultiPolygon, etc.) as well as polygons that cross the dateline.
+     */
+    static final class SpatialRelationsContains extends SpatialRelations {
+        SpatialRelationsContains(SpatialCoordinateTypes spatialCoordinateType, CoordinateEncoder encoder, ShapeIndexer shapeIndexer) {
+            super(ShapeField.QueryRelation.CONTAINS, spatialCoordinateType, encoder, shapeIndexer);
+        }
+
+        @Override
+        protected boolean geometryRelatesGeometry(BytesRef left, BytesRef right) throws IOException {
+            Component2D[] rightComponent2Ds = asLuceneComponent2Ds(crsType, fromBytesRef(right));
+            return geometryRelatesGeometries(left, rightComponent2Ds);
+        }
+
+        private boolean geometryRelatesGeometries(BytesRef left, Component2D[] rightComponent2Ds) throws IOException {
+            Geometry leftGeom = fromBytesRef(left);
+            GeometryDocValueReader leftDocValueReader = asGeometryDocValueReader(coordinateEncoder, shapeIndexer, leftGeom);
+            return geometryRelatesGeometries(leftDocValueReader, rightComponent2Ds);
+        }
+
+        private boolean geometryRelatesGeometries(GeometryDocValueReader leftDocValueReader, Component2D[] rightComponent2Ds)
+            throws IOException {
+            for (Component2D rightComponent2D : rightComponent2Ds) {
+                // Every component of the right geometry must be contained within the left geometry for this to pass
+                if (geometryRelatesGeometry(leftDocValueReader, rightComponent2D) == false) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    @FunctionInfo(
+        returnType = { "boolean" },
+        description = "Returns whether the first geometry contains the second geometry.",
+        note = "The second parameter must also have the same coordinate system as the first. "
+            + "This means it is not possible to combine `geo_*` and `cartesian_*` parameters.",
+        examples = @Example(file = "spatial_shapes", tag = "st_contains-airport_city_boundaries")
+    )
+    public SpatialContains(
+        Source source,
+        @Param(
+            name = "geomA",
+            type = { "geo_point", "cartesian_point", "geo_shape", "cartesian_shape" },
+            description = "Geometry column name or variable of geometry type"
+        ) Expression left,
+        @Param(
+            name = "geomB",
+            type = { "geo_point", "cartesian_point", "geo_shape", "cartesian_shape" },
+            description = "Geometry column name or variable of geometry type"
+        ) Expression right
+    ) {
+        this(source, left, right, false, false);
+    }
+
+    SpatialContains(Source source, Expression left, Expression right, boolean leftDocValues, boolean rightDocValues) {
+        super(source, left, right, leftDocValues, rightDocValues);
+    }
+
+    @Override
+    public ShapeField.QueryRelation queryRelation() {
+        return ShapeField.QueryRelation.CONTAINS;
+    }
+
+    @Override
+    public SpatialContains withDocValues(Set<FieldAttribute> attributes) {
+        // Only update the docValues flags if the field is found in the attributes
+        boolean leftDV = leftDocValues || foundField(left(), attributes);
+        boolean rightDV = rightDocValues || foundField(right(), attributes);
+        return new SpatialContains(source(), left(), right(), leftDV, rightDV);
+    }
+
+    @Override
+    protected SpatialContains replaceChildren(Expression newLeft, Expression newRight) {
+        return new SpatialContains(source(), newLeft, newRight, leftDocValues, rightDocValues);
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, SpatialContains::new, left(), right());
+    }
+
+    @Override
+    public Object fold() {
+        try {
+            GeometryDocValueReader docValueReader = asGeometryDocValueReader(crsType, left());
+            Geometry rightGeom = makeGeometryFromLiteral(right());
+            Component2D[] components = asLuceneComponent2Ds(crsType, rightGeom);
+            return (crsType == SpatialCrsType.GEO)
+                ? GEO.geometryRelatesGeometries(docValueReader, components)
+                : CARTESIAN.geometryRelatesGeometries(docValueReader, components);
+        } catch (IOException e) {
+            throw new IllegalArgumentException("Failed to fold constant fields: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    Map<SpatialEvaluatorFactory.SpatialEvaluatorKey, SpatialEvaluatorFactory<?, ?>> evaluatorRules() {
+        return evaluatorMap;
+    }
+
+    /**
+     * To keep the number of evaluators to a minimum, we swap the arguments to get the WITHIN relation.
+     * This also makes other optimizations, like lucene-pushdown, simpler to develop.
+     */
+    @Override
+    public SpatialRelatesFunction surrogate() {
+        if (left().foldable() && right().foldable() == false) {
+            return new SpatialWithin(source(), right(), left(), rightDocValues, leftDocValues);
+        }
+        return this;
+    }
+
+    private static final Map<SpatialEvaluatorFactory.SpatialEvaluatorKey, SpatialEvaluatorFactory<?, ?>> evaluatorMap = new HashMap<>();
+
+    static {
+        // Support geo_point and geo_shape from source and constant combinations
+        for (DataType spatialType : new DataType[] { GEO_POINT, GEO_SHAPE }) {
+            for (DataType otherType : new DataType[] { GEO_POINT, GEO_SHAPE }) {
+                evaluatorMap.put(
+                    SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSources(spatialType, otherType),
+                    new SpatialEvaluatorFactory.SpatialEvaluatorFactoryWithFields(SpatialContainsGeoSourceAndSourceEvaluator.Factory::new)
+                );
+                evaluatorMap.put(
+                    SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType),
+                    new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                        SpatialContainsGeoSourceAndConstantEvaluator.Factory::new
+                    )
+                );
+                if (EsqlDataTypes.isSpatialPoint(spatialType)) {
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSources(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorFactoryWithFields(
+                            SpatialContainsGeoPointDocValuesAndSourceEvaluator.Factory::new
+                        )
+                    );
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                            SpatialContainsGeoPointDocValuesAndConstantEvaluator.Factory::new
+                        )
+                    );
+                }
+            }
+        }
+
+        // Support cartesian_point and cartesian_shape from source and constant combinations
+        for (DataType spatialType : new DataType[] { CARTESIAN_POINT, CARTESIAN_SHAPE }) {
+            for (DataType otherType : new DataType[] { CARTESIAN_POINT, CARTESIAN_SHAPE }) {
+                evaluatorMap.put(
+                    SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSources(spatialType, otherType),
+                    new SpatialEvaluatorFactory.SpatialEvaluatorFactoryWithFields(
+                        SpatialContainsCartesianSourceAndSourceEvaluator.Factory::new
+                    )
+                );
+                evaluatorMap.put(
+                    SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType),
+                    new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                        SpatialContainsCartesianSourceAndConstantEvaluator.Factory::new
+                    )
+                );
+                if (EsqlDataTypes.isSpatialPoint(spatialType)) {
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSources(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorFactoryWithFields(
+                            SpatialContainsCartesianPointDocValuesAndSourceEvaluator.Factory::new
+                        )
+                    );
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                            SpatialContainsCartesianPointDocValuesAndConstantEvaluator.Factory::new
+                        )
+                    );
+                }
+            }
+        }
+    }
+
+    @Evaluator(extraName = "GeoSourceAndConstant", warnExceptions = { IllegalArgumentException.class, IOException.class })
+    static boolean processGeoSourceAndConstant(BytesRef leftValue, @Fixed Component2D rightValue) throws IOException {
+        return GEO.geometryRelatesGeometry(leftValue, rightValue);
+    }
+
+    @Evaluator(extraName = "GeoSourceAndSource", warnExceptions = { IllegalArgumentException.class, IOException.class })
+    static boolean processGeoSourceAndSource(BytesRef leftValue, BytesRef rightValue) throws IOException {
+        return GEO.geometryRelatesGeometry(leftValue, rightValue);
+    }
+
+    @Evaluator(extraName = "GeoPointDocValuesAndConstant", warnExceptions = { IllegalArgumentException.class })
+    static boolean processGeoPointDocValuesAndConstant(long leftValue, @Fixed Component2D rightValue) {
+        return GEO.pointRelatesGeometry(leftValue, rightValue);
+    }
+
+    @Evaluator(extraName = "GeoPointDocValuesAndSource", warnExceptions = { IllegalArgumentException.class })
+    static boolean processGeoPointDocValuesAndSource(long leftValue, BytesRef rightValue) {
+        Geometry geometry = SpatialCoordinateTypes.UNSPECIFIED.wkbToGeometry(rightValue);
+        return GEO.pointRelatesGeometry(leftValue, geometry);
+    }
+
+    @Evaluator(extraName = "CartesianSourceAndConstant", warnExceptions = { IllegalArgumentException.class, IOException.class })
+    static boolean processCartesianSourceAndConstant(BytesRef leftValue, @Fixed Component2D rightValue) throws IOException {
+        return CARTESIAN.geometryRelatesGeometry(leftValue, rightValue);
+    }
+
+    @Evaluator(extraName = "CartesianSourceAndSource", warnExceptions = { IllegalArgumentException.class, IOException.class })
+    static boolean processCartesianSourceAndSource(BytesRef leftValue, BytesRef rightValue) throws IOException {
+        return CARTESIAN.geometryRelatesGeometry(leftValue, rightValue);
+    }
+
+    @Evaluator(extraName = "CartesianPointDocValuesAndConstant", warnExceptions = { IllegalArgumentException.class })
+    static boolean processCartesianPointDocValuesAndConstant(long leftValue, @Fixed Component2D rightValue) {
+        return CARTESIAN.pointRelatesGeometry(leftValue, rightValue);
+    }
+
+    @Evaluator(extraName = "CartesianPointDocValuesAndSource")
+    static boolean processCartesianPointDocValuesAndSource(long leftValue, BytesRef rightValue) {
+        Geometry geometry = SpatialCoordinateTypes.UNSPECIFIED.wkbToGeometry(rightValue);
+        return CARTESIAN.pointRelatesGeometry(leftValue, geometry);
+    }
+}

+ 2 - 2
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialEvaluatorFactory.java

@@ -38,7 +38,7 @@ abstract class SpatialEvaluatorFactory<V, T> {
         Function<Expression, EvalOperator.ExpressionEvaluator.Factory> toEvaluator
     );
 
-    public static EvalOperator.ExpressionEvaluator.Factory makeSpatialEvaluator(
+    static EvalOperator.ExpressionEvaluator.Factory makeSpatialEvaluator(
         SpatialSourceSupplier s,
         Map<SpatialEvaluatorKey, SpatialEvaluatorFactory<?, ?>> evaluatorRules,
         Function<Expression, EvalOperator.ExpressionEvaluator.Factory> toEvaluator
@@ -170,7 +170,7 @@ abstract class SpatialEvaluatorFactory<V, T> {
 
     protected record SpatialEvaluatorFieldKey(DataType dataType, boolean isConstant) {}
 
-    protected record SpatialEvaluatorKey(
+    record SpatialEvaluatorKey(
         SpatialRelatesFunction.SpatialCrsType crsType,
         boolean leftDocValues,
         boolean rightDocValues,

+ 19 - 4
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersects.java

@@ -18,6 +18,7 @@ import org.elasticsearch.index.mapper.GeoShapeIndexer;
 import org.elasticsearch.lucene.spatial.CartesianShapeIndexer;
 import org.elasticsearch.lucene.spatial.CoordinateEncoder;
 import org.elasticsearch.lucene.spatial.GeometryDocValueReader;
+import org.elasticsearch.xpack.esql.expression.function.Example;
 import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
 import org.elasticsearch.xpack.esql.expression.function.Param;
 import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
@@ -40,21 +41,35 @@ import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.CARTESIAN_SHAPE;
 import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.GEO_POINT;
 import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.GEO_SHAPE;
 
+/**
+ * This is the primary class for supporting the function ST_INTERSECTS.
+ * The bulk of the capabilities are within the parent class SpatialRelatesFunction,
+ * which supports all the relations in the ShapeField.QueryRelation enum.
+ * Here we simply wire the rules together specific to ST_INTERSECTS and QueryRelation.INTERSECTS.
+ */
 public class SpatialIntersects extends SpatialRelatesFunction {
-    protected static final SpatialRelations GEO = new SpatialRelations(
+    // public for test access with reflection
+    public static final SpatialRelations GEO = new SpatialRelations(
         ShapeField.QueryRelation.INTERSECTS,
         SpatialCoordinateTypes.GEO,
         CoordinateEncoder.GEO,
         new GeoShapeIndexer(Orientation.CCW, "ST_Intersects")
     );
-    protected static final SpatialRelations CARTESIAN = new SpatialRelations(
+    // public for test access with reflection
+    public static final SpatialRelations CARTESIAN = new SpatialRelations(
         ShapeField.QueryRelation.INTERSECTS,
         SpatialCoordinateTypes.CARTESIAN,
         CoordinateEncoder.CARTESIAN,
         new CartesianShapeIndexer("ST_Intersects")
     );
 
-    @FunctionInfo(returnType = { "boolean" }, description = "Returns whether the two geometries or geometry columns intersect.")
+    @FunctionInfo(
+        returnType = { "boolean" },
+        description = "Returns whether the two geometries or geometry columns intersect.",
+        note = "The second parameter must also have the same coordinate system as the first. "
+            + "This means it is not possible to combine `geo_*` and `cartesian_*` parameters.",
+        examples = @Example(file = "spatial", tag = "st_intersects-airports")
+    )
     public SpatialIntersects(
         Source source,
         @Param(
@@ -112,7 +127,7 @@ public class SpatialIntersects extends SpatialRelatesFunction {
     }
 
     @Override
-    protected Map<SpatialEvaluatorFactory.SpatialEvaluatorKey, SpatialEvaluatorFactory<?, ?>> evaluatorRules() {
+    Map<SpatialEvaluatorFactory.SpatialEvaluatorKey, SpatialEvaluatorFactory<?, ?>> evaluatorRules() {
         return evaluatorMap;
     }
 

+ 18 - 6
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialRelatesFunction.java

@@ -35,6 +35,7 @@ import java.util.Set;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import static org.apache.lucene.document.ShapeField.QueryRelation.CONTAINS;
 import static org.apache.lucene.document.ShapeField.QueryRelation.DISJOINT;
 import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isSpatial;
 import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils.asGeometryDocValueReader;
@@ -203,7 +204,14 @@ public abstract class SpatialRelatesFunction extends BinaryScalarFunction
     /**
      * Produce a map of rules defining combinations of incoming types to the evaluator factory that should be used.
      */
-    protected abstract Map<SpatialEvaluatorFactory.SpatialEvaluatorKey, SpatialEvaluatorFactory<?, ?>> evaluatorRules();
+    abstract Map<SpatialEvaluatorFactory.SpatialEvaluatorKey, SpatialEvaluatorFactory<?, ?>> evaluatorRules();
+
+    /**
+     * Some spatial functions can replace themselves with alternatives that are more efficient for certain cases.
+     */
+    public SpatialRelatesFunction surrogate() {
+        return this;
+    }
 
     @Override
     public EvalOperator.ExpressionEvaluator.Factory toEvaluator(
@@ -262,7 +270,7 @@ public abstract class SpatialRelatesFunction extends BinaryScalarFunction
             return geometryRelatesGeometry(left, rightComponent2D);
         }
 
-        private Geometry fromBytesRef(BytesRef bytesRef) {
+        protected Geometry fromBytesRef(BytesRef bytesRef) {
             return SpatialCoordinateTypes.UNSPECIFIED.wkbToGeometry(bytesRef);
         }
 
@@ -286,12 +294,16 @@ public abstract class SpatialRelatesFunction extends BinaryScalarFunction
         protected boolean pointRelatesGeometry(long encoded, Component2D component2D) {
             // This code path exists for doc-values points, and we could consider re-using the point class to reduce garbage creation
             Point point = spatialCoordinateType.longAsPoint(encoded);
-            return geometryRelatesPoint(component2D, point);
+            return pointRelatesGeometry(point, component2D);
         }
 
-        private boolean geometryRelatesPoint(Component2D component2D, Point point) {
-            boolean contains = component2D.contains(point.getX(), point.getY());
-            return queryRelation == DISJOINT ? contains == false : contains;
+        private boolean pointRelatesGeometry(Point point, Component2D component2D) {
+            if (queryRelation == CONTAINS) {
+                return component2D.withinPoint(point.getX(), point.getY()) == Component2D.WithinRelation.CANDIDATE;
+            } else {
+                boolean contains = component2D.contains(point.getX(), point.getY());
+                return queryRelation == DISJOINT ? contains == false : contains;
+            }
         }
     }
 }

+ 10 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialRelatesUtils.java

@@ -50,6 +50,16 @@ public class SpatialRelatesUtils {
         }
     }
 
+    static Component2D[] asLuceneComponent2Ds(SpatialRelatesFunction.SpatialCrsType crsType, Geometry geometry) {
+        if (crsType == SpatialRelatesFunction.SpatialCrsType.GEO) {
+            var luceneGeometries = LuceneGeometriesUtils.toLatLonGeometry(geometry, true, t -> {});
+            return LuceneComponent2DUtils.createLatLonComponents(luceneGeometries);
+        } else {
+            var luceneGeometries = LuceneGeometriesUtils.toXYGeometry(geometry, t -> {});
+            return LuceneComponent2DUtils.createXYComponents(luceneGeometries);
+        }
+    }
+
     /**
      * This function is used to convert a spatial constant to a doc-values byte array.
      * When both left and right sides are constants, we convert the left to a doc-values byte array and the right to a Component2D.

+ 252 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithin.java

@@ -0,0 +1,252 @@
+/*
+ * 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.document.ShapeField;
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.geo.Orientation;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+import org.elasticsearch.geometry.Geometry;
+import org.elasticsearch.index.mapper.GeoShapeIndexer;
+import org.elasticsearch.lucene.spatial.CartesianShapeIndexer;
+import org.elasticsearch.lucene.spatial.CoordinateEncoder;
+import org.elasticsearch.lucene.spatial.GeometryDocValueReader;
+import org.elasticsearch.xpack.esql.expression.SurrogateExpression;
+import org.elasticsearch.xpack.esql.expression.function.Example;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.FieldAttribute;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.util.SpatialCoordinateTypes;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils.asGeometryDocValueReader;
+import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils.asLuceneComponent2D;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.CARTESIAN_POINT;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.CARTESIAN_SHAPE;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.GEO_POINT;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.GEO_SHAPE;
+
+/**
+ * This is the primary class for supporting the function ST_WITHIN.
+ * The bulk of the capabilities are within the parent class SpatialRelatesFunction,
+ * which supports all the relations in the ShapeField.QueryRelation enum.
+ * Here we simply wire the rules together specific to ST_WITHIN and QueryRelation.WITHIN.
+ */
+public class SpatialWithin extends SpatialRelatesFunction implements SurrogateExpression {
+    // public for test access with reflection
+    public static final SpatialRelations GEO = new SpatialRelations(
+        ShapeField.QueryRelation.WITHIN,
+        SpatialCoordinateTypes.GEO,
+        CoordinateEncoder.GEO,
+        new GeoShapeIndexer(Orientation.CCW, "ST_Within")
+    );
+    // public for test access with reflection
+    public static final SpatialRelations CARTESIAN = new SpatialRelations(
+        ShapeField.QueryRelation.WITHIN,
+        SpatialCoordinateTypes.CARTESIAN,
+        CoordinateEncoder.CARTESIAN,
+        new CartesianShapeIndexer("ST_Within")
+    );
+
+    @FunctionInfo(
+        returnType = { "boolean" },
+        description = "Returns whether the first geometry is within the second geometry.",
+        note = "The second parameter must also have the same coordinate system as the first. "
+            + "This means it is not possible to combine `geo_*` and `cartesian_*` parameters.",
+        examples = @Example(file = "spatial_shapes", tag = "st_within-airport_city_boundaries")
+    )
+    public SpatialWithin(
+        Source source,
+        @Param(
+            name = "geomA",
+            type = { "geo_point", "cartesian_point", "geo_shape", "cartesian_shape" },
+            description = "Geometry column name or variable of geometry type"
+        ) Expression left,
+        @Param(
+            name = "geomB",
+            type = { "geo_point", "cartesian_point", "geo_shape", "cartesian_shape" },
+            description = "Geometry column name or variable of geometry type"
+        ) Expression right
+    ) {
+        this(source, left, right, false, false);
+    }
+
+    SpatialWithin(Source source, Expression left, Expression right, boolean leftDocValues, boolean rightDocValues) {
+        super(source, left, right, leftDocValues, rightDocValues);
+    }
+
+    @Override
+    public ShapeField.QueryRelation queryRelation() {
+        return ShapeField.QueryRelation.WITHIN;
+    }
+
+    @Override
+    public SpatialWithin withDocValues(Set<FieldAttribute> attributes) {
+        // Only update the docValues flags if the field is found in the attributes
+        boolean leftDV = leftDocValues || foundField(left(), attributes);
+        boolean rightDV = rightDocValues || foundField(right(), attributes);
+        return new SpatialWithin(source(), left(), right(), leftDV, rightDV);
+    }
+
+    @Override
+    protected SpatialWithin replaceChildren(Expression newLeft, Expression newRight) {
+        return new SpatialWithin(source(), newLeft, newRight, leftDocValues, rightDocValues);
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, SpatialWithin::new, left(), right());
+    }
+
+    @Override
+    public Object fold() {
+        try {
+            GeometryDocValueReader docValueReader = asGeometryDocValueReader(crsType, left());
+            Component2D component2D = asLuceneComponent2D(crsType, right());
+            return (crsType == SpatialCrsType.GEO)
+                ? GEO.geometryRelatesGeometry(docValueReader, component2D)
+                : CARTESIAN.geometryRelatesGeometry(docValueReader, component2D);
+        } catch (IOException e) {
+            throw new IllegalArgumentException("Failed to fold constant fields: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    Map<SpatialEvaluatorFactory.SpatialEvaluatorKey, SpatialEvaluatorFactory<?, ?>> evaluatorRules() {
+        return evaluatorMap;
+    }
+
+    /**
+     * To keep the number of evaluators to a minimum, we swap the arguments to get the CONTAINS relation.
+     * This also makes other optimizations, like lucene-pushdown, simpler to develop.
+     */
+    @Override
+    public SpatialRelatesFunction surrogate() {
+        if (left().foldable() && right().foldable() == false) {
+            return new SpatialContains(source(), right(), left(), rightDocValues, leftDocValues);
+        }
+        return this;
+    }
+
+    private static final Map<SpatialEvaluatorFactory.SpatialEvaluatorKey, SpatialEvaluatorFactory<?, ?>> evaluatorMap = new HashMap<>();
+
+    static {
+        // Support geo_point and geo_shape from source and constant combinations
+        for (DataType spatialType : new DataType[] { GEO_POINT, GEO_SHAPE }) {
+            for (DataType otherType : new DataType[] { GEO_POINT, GEO_SHAPE }) {
+                evaluatorMap.put(
+                    SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSources(spatialType, otherType),
+                    new SpatialEvaluatorFactory.SpatialEvaluatorFactoryWithFields(SpatialWithinGeoSourceAndSourceEvaluator.Factory::new)
+                );
+                evaluatorMap.put(
+                    SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType),
+                    new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(SpatialWithinGeoSourceAndConstantEvaluator.Factory::new)
+                );
+                if (EsqlDataTypes.isSpatialPoint(spatialType)) {
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSources(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorFactoryWithFields(
+                            SpatialWithinGeoPointDocValuesAndSourceEvaluator.Factory::new
+                        )
+                    );
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                            SpatialWithinGeoPointDocValuesAndConstantEvaluator.Factory::new
+                        )
+                    );
+                }
+            }
+        }
+
+        // Support cartesian_point and cartesian_shape from source and constant combinations
+        for (DataType spatialType : new DataType[] { CARTESIAN_POINT, CARTESIAN_SHAPE }) {
+            for (DataType otherType : new DataType[] { CARTESIAN_POINT, CARTESIAN_SHAPE }) {
+                evaluatorMap.put(
+                    SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSources(spatialType, otherType),
+                    new SpatialEvaluatorFactory.SpatialEvaluatorFactoryWithFields(
+                        SpatialWithinCartesianSourceAndSourceEvaluator.Factory::new
+                    )
+                );
+                evaluatorMap.put(
+                    SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType),
+                    new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                        SpatialWithinCartesianSourceAndConstantEvaluator.Factory::new
+                    )
+                );
+                if (EsqlDataTypes.isSpatialPoint(spatialType)) {
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSources(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorFactoryWithFields(
+                            SpatialWithinCartesianPointDocValuesAndSourceEvaluator.Factory::new
+                        )
+                    );
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                            SpatialWithinCartesianPointDocValuesAndConstantEvaluator.Factory::new
+                        )
+                    );
+                }
+            }
+        }
+    }
+
+    @Evaluator(extraName = "GeoSourceAndConstant", warnExceptions = { IllegalArgumentException.class, IOException.class })
+    static boolean processGeoSourceAndConstant(BytesRef leftValue, @Fixed Component2D rightValue) throws IOException {
+        return GEO.geometryRelatesGeometry(leftValue, rightValue);
+    }
+
+    @Evaluator(extraName = "GeoSourceAndSource", warnExceptions = { IllegalArgumentException.class, IOException.class })
+    static boolean processGeoSourceAndSource(BytesRef leftValue, BytesRef rightValue) throws IOException {
+        return GEO.geometryRelatesGeometry(leftValue, rightValue);
+    }
+
+    @Evaluator(extraName = "GeoPointDocValuesAndConstant", warnExceptions = { IllegalArgumentException.class })
+    static boolean processGeoPointDocValuesAndConstant(long leftValue, @Fixed Component2D rightValue) {
+        return GEO.pointRelatesGeometry(leftValue, rightValue);
+    }
+
+    @Evaluator(extraName = "GeoPointDocValuesAndSource", warnExceptions = { IllegalArgumentException.class })
+    static boolean processGeoPointDocValuesAndSource(long leftValue, BytesRef rightValue) {
+        Geometry geometry = SpatialCoordinateTypes.UNSPECIFIED.wkbToGeometry(rightValue);
+        return GEO.pointRelatesGeometry(leftValue, geometry);
+    }
+
+    @Evaluator(extraName = "CartesianSourceAndConstant", warnExceptions = { IllegalArgumentException.class, IOException.class })
+    static boolean processCartesianSourceAndConstant(BytesRef leftValue, @Fixed Component2D rightValue) throws IOException {
+        return CARTESIAN.geometryRelatesGeometry(leftValue, rightValue);
+    }
+
+    @Evaluator(extraName = "CartesianSourceAndSource", warnExceptions = { IllegalArgumentException.class, IOException.class })
+    static boolean processCartesianSourceAndSource(BytesRef leftValue, BytesRef rightValue) throws IOException {
+        return CARTESIAN.geometryRelatesGeometry(leftValue, rightValue);
+    }
+
+    @Evaluator(extraName = "CartesianPointDocValuesAndConstant", warnExceptions = { IllegalArgumentException.class })
+    static boolean processCartesianPointDocValuesAndConstant(long leftValue, @Fixed Component2D rightValue) {
+        return CARTESIAN.pointRelatesGeometry(leftValue, rightValue);
+    }
+
+    @Evaluator(extraName = "CartesianPointDocValuesAndSource")
+    static boolean processCartesianPointDocValuesAndSource(long leftValue, BytesRef rightValue) {
+        Geometry geometry = SpatialCoordinateTypes.UNSPECIFIED.wkbToGeometry(rightValue);
+        return CARTESIAN.pointRelatesGeometry(leftValue, geometry);
+    }
+}

+ 17 - 6
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java

@@ -101,7 +101,10 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSort
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSum;
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvZip;
 import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialContains;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialIntersects;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesFunction;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialWithin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StY;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat;
@@ -391,7 +394,9 @@ public final class PlanNamedTypes {
             of(ScalarFunction.class, Pow.class, PlanNamedTypes::writePow, PlanNamedTypes::readPow),
             of(ScalarFunction.class, StartsWith.class, PlanNamedTypes::writeStartsWith, PlanNamedTypes::readStartsWith),
             of(ScalarFunction.class, EndsWith.class, PlanNamedTypes::writeEndsWith, PlanNamedTypes::readEndsWith),
-            of(ScalarFunction.class, SpatialIntersects.class, PlanNamedTypes::writeIntersects, PlanNamedTypes::readIntersects),
+            of(ScalarFunction.class, SpatialIntersects.class, PlanNamedTypes::writeSpatialRelatesFunction, PlanNamedTypes::readIntersects),
+            of(ScalarFunction.class, SpatialContains.class, PlanNamedTypes::writeSpatialRelatesFunction, PlanNamedTypes::readContains),
+            of(ScalarFunction.class, SpatialWithin.class, PlanNamedTypes::writeSpatialRelatesFunction, PlanNamedTypes::readWithin),
             of(ScalarFunction.class, Substring.class, PlanNamedTypes::writeSubstring, PlanNamedTypes::readSubstring),
             of(ScalarFunction.class, Left.class, PlanNamedTypes::writeLeft, PlanNamedTypes::readLeft),
             of(ScalarFunction.class, Right.class, PlanNamedTypes::writeRight, PlanNamedTypes::readRight),
@@ -1494,11 +1499,17 @@ public final class PlanNamedTypes {
         return new SpatialIntersects(Source.EMPTY, in.readExpression(), in.readExpression());
     }
 
-    static void writeIntersects(PlanStreamOutput out, SpatialIntersects intersects) throws IOException {
-        List<Expression> fields = intersects.children();
-        assert fields.size() == 2;
-        out.writeExpression(fields.get(0));
-        out.writeExpression(fields.get(1));
+    static SpatialContains readContains(PlanStreamInput in) throws IOException {
+        return new SpatialContains(Source.EMPTY, in.readExpression(), in.readExpression());
+    }
+
+    static SpatialWithin readWithin(PlanStreamInput in) throws IOException {
+        return new SpatialWithin(Source.EMPTY, in.readExpression(), in.readExpression());
+    }
+
+    static void writeSpatialRelatesFunction(PlanStreamOutput out, SpatialRelatesFunction spatialRelatesFunction) throws IOException {
+        out.writeExpression(spatialRelatesFunction.left());
+        out.writeExpression(spatialRelatesFunction.right());
     }
 
     static Now readNow(PlanStreamInput in) throws IOException {

+ 22 - 1
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java

@@ -19,6 +19,7 @@ import org.elasticsearch.xpack.esql.expression.SurrogateExpression;
 import org.elasticsearch.xpack.esql.expression.function.aggregate.Count;
 import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case;
 import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesFunction;
 import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In;
 import org.elasticsearch.xpack.esql.plan.logical.Enrich;
 import org.elasticsearch.xpack.esql.plan.logical.Eval;
@@ -120,7 +121,8 @@ public class LogicalPlanOptimizer extends ParameterizedRuleExecutor<LogicalPlan,
             new SubstituteSurrogates(),
             new ReplaceRegexMatch(),
             new ReplaceAliasingEvalWithProject(),
-            new SkipQueryOnEmptyMappings()
+            new SkipQueryOnEmptyMappings(),
+            new SubstituteSpatialSurrogates()
             // new NormalizeAggregate(), - waits on https://github.com/elastic/elasticsearch/issues/100634
         );
     }
@@ -297,6 +299,25 @@ public class LogicalPlanOptimizer extends ParameterizedRuleExecutor<LogicalPlan,
         }
     }
 
+    /**
+     * Currently this works similarly to SurrogateExpression, leaving the logic inside the expressions,
+     * so each can decide for itself whether or not to change to a surrogate expression.
+     * But what is actually being done is similar to LiteralsOnTheRight. We can consider in the future moving
+     * this in either direction, reducing the number of rules, but for now,
+     * it's a separate rule to reduce the risk of unintended interactions with other rules.
+     */
+    static class SubstituteSpatialSurrogates extends OptimizerRules.OptimizerExpressionRule<SpatialRelatesFunction> {
+
+        SubstituteSpatialSurrogates() {
+            super(TransformDirection.UP);
+        }
+
+        @Override
+        protected SpatialRelatesFunction rule(SpatialRelatesFunction function) {
+            return function.surrogate();
+        }
+    }
+
     static class ConvertStringToByteRef extends OptimizerRules.OptimizerExpressionRule<Literal> {
 
         ConvertStringToByteRef() {

+ 40 - 9
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlFeatures.java

@@ -39,9 +39,14 @@ public class EsqlFeatures implements FeatureSpecification {
     private static final NodeFeature MV_WARN = new NodeFeature("esql.mv_warn");
 
     /**
-     * Support for loading {@code geo_point} fields. Added in #102177.
+     * Support for loading {@code geo_point} and {@code cartesian_point} fields. Added in #102177.
      */
-    private static final NodeFeature GEO_POINT_SUPPORT = new NodeFeature("esql.geo_point");
+    private static final NodeFeature SPATIAL_POINTS = new NodeFeature("esql.spatial_points");
+
+    /**
+     * Changed precision of {@code geo_point} and {@code cartesian_point} fields, by loading from source into WKB. Done in #103691.
+     */
+    private static final NodeFeature SPATIAL_POINTS_FROM_SOURCE = new NodeFeature("esql.spatial_points_from_source");
 
     /**
      * When we added the warnings when conversion functions fail. Like {@code TO_INT('foo')}.
@@ -55,10 +60,25 @@ public class EsqlFeatures implements FeatureSpecification {
      */
     private static final NodeFeature POW_DOUBLE = new NodeFeature("esql.pow_double");
 
-    // /**
-    // * Support for loading {@code geo_point} fields.
-    // */
-    // private static final NodeFeature GEO_SHAPE_SUPPORT = new NodeFeature("esql.geo_shape");
+    /**
+     * Support for loading {@code geo_shape} and {@code cartesian_shape} fields. Done in #104269.
+     */
+    private static final NodeFeature SPATIAL_SHAPES = new NodeFeature("esql.spatial_shapes");
+
+    /**
+     * Support for spatial aggregation {@code ST_CENTROID}. Done in #104269.
+     */
+    private static final NodeFeature ST_CENTROID = new NodeFeature("esql.st_centroid");
+
+    /**
+     * Support for spatial aggregation {@code ST_INTERSECTS}. Done in #104907.
+     */
+    private static final NodeFeature ST_INTERSECTS = new NodeFeature("esql.st_intersects");
+
+    /**
+     * Support for spatial aggregation {@code ST_CONTAINS} and {@code ST_WITHIN}. Done in #106503.
+     */
+    private static final NodeFeature ST_CONTAINS_WITHIN = new NodeFeature("esql.st_contains_within");
 
     /**
      * The introduction of the {@code VALUES} agg.
@@ -77,7 +97,19 @@ public class EsqlFeatures implements FeatureSpecification {
 
     @Override
     public Set<NodeFeature> getFeatures() {
-        return Set.of(ASYNC_QUERY, AGG_VALUES, MV_SORT, DISABLE_NULLABLE_OPTS, ST_X_Y, FROM_OPTIONS);
+        return Set.of(
+            ASYNC_QUERY,
+            AGG_VALUES,
+            MV_SORT,
+            DISABLE_NULLABLE_OPTS,
+            ST_X_Y,
+            FROM_OPTIONS,
+            SPATIAL_POINTS_FROM_SOURCE,
+            SPATIAL_SHAPES,
+            ST_CENTROID,
+            ST_INTERSECTS,
+            ST_CONTAINS_WITHIN
+        );
     }
 
     @Override
@@ -85,10 +117,9 @@ public class EsqlFeatures implements FeatureSpecification {
         return Map.ofEntries(
             Map.entry(TransportEsqlStatsAction.ESQL_STATS_FEATURE, Version.V_8_11_0),
             Map.entry(MV_WARN, Version.V_8_12_0),
-            Map.entry(GEO_POINT_SUPPORT, Version.V_8_12_0),
+            Map.entry(SPATIAL_POINTS, Version.V_8_12_0),
             Map.entry(CONVERT_WARN, Version.V_8_12_0),
             Map.entry(POW_DOUBLE, Version.V_8_12_0)
-            // Map.entry(GEO_SHAPE_SUPPORT, Version.V_8_13_0)
         );
     }
 }

+ 3 - 3
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/SpatialRelatesQuery.java

@@ -231,9 +231,9 @@ public class SpatialRelatesQuery extends Query {
             SearchExecutionContext context
         ) {
             final boolean hasDocValues = context.getFieldType(fieldName).hasDocValues();
-            // only the intersects relation is supported for indexed cartesian point types
-            if (relation != ShapeField.QueryRelation.INTERSECTS) {
-                throw new QueryShardException(context, relation + " query relation not supported for Field [" + fieldName + "].");
+            if (geometry.type() != ShapeType.POINT && relation == ShapeField.QueryRelation.CONTAINS) {
+                // A point field can never contain a non-point geometry
+                return new MatchNoDocsQuery();
             }
             final Consumer<ShapeType> checker = t -> {
                 if (t == ShapeType.POINT || t == ShapeType.MULTIPOINT || t == ShapeType.LINESTRING || t == ShapeType.MULTILINESTRING) {

+ 1 - 1
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java

@@ -1223,7 +1223,7 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
         writeToTempDir("layout", rendered, "asciidoc");
     }
 
-    private static String functionName() {
+    protected static String functionName() {
         Class<?> testClass = getTestClass();
         if (testClass.isAnnotationPresent(FunctionName.class)) {
             FunctionName functionNameAnnotation = testClass.getAnnotation(FunctionName.class);

+ 46 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContainsTests.java

@@ -0,0 +1,46 @@
+/*
+ * 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.elasticsearch.xpack.esql.expression.function.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+@FunctionName("st_contains")
+public class SpatialContainsTests extends SpatialRelatesFunctionTestCase {
+    public SpatialContainsTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        List<TestCaseSupplier> suppliers = new ArrayList<>();
+        DataType[] geoDataTypes = { EsqlDataTypes.GEO_POINT, EsqlDataTypes.GEO_SHAPE };
+        SpatialRelatesFunctionTestCase.addSpatialCombinations(suppliers, geoDataTypes);
+        DataType[] cartesianDataTypes = { EsqlDataTypes.CARTESIAN_POINT, EsqlDataTypes.CARTESIAN_SHAPE };
+        SpatialRelatesFunctionTestCase.addSpatialCombinations(suppliers, cartesianDataTypes);
+        return parameterSuppliersFromTypedData(
+            errorsForCasesWithoutExamples(anyNullIsNull(true, suppliers), SpatialContainsTests::typeErrorMessage)
+        );
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new SpatialContains(source, args.get(0), args.get(1));
+    }
+}

+ 3 - 170
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersectsTests.java

@@ -7,38 +7,22 @@
 
 package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
 
-import joptsimple.internal.Strings;
-
 import com.carrotsearch.randomizedtesting.annotations.Name;
 import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
 
-import org.apache.lucene.util.BytesRef;
-import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
 import org.elasticsearch.xpack.esql.expression.function.FunctionName;
 import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
 import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
 import org.elasticsearch.xpack.ql.expression.Expression;
-import org.elasticsearch.xpack.ql.expression.TypeResolutions;
 import org.elasticsearch.xpack.ql.tree.Source;
 import org.elasticsearch.xpack.ql.type.DataType;
-import org.elasticsearch.xpack.ql.type.DataTypes;
-import org.elasticsearch.xpack.ql.util.SpatialCoordinateTypes;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Locale;
-import java.util.Set;
 import java.util.function.Supplier;
 
-import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesFunction.compatibleTypeNames;
-import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.isSpatial;
-import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.isSpatialGeo;
-import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.isString;
-
 @FunctionName("st_intersects")
-
-public class SpatialIntersectsTests extends AbstractFunctionTestCase {
+public class SpatialIntersectsTests extends SpatialRelatesFunctionTestCase {
     public SpatialIntersectsTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
         this.testCase = testCaseSupplier.get();
     }
@@ -47,9 +31,9 @@ public class SpatialIntersectsTests extends AbstractFunctionTestCase {
     public static Iterable<Object[]> parameters() {
         List<TestCaseSupplier> suppliers = new ArrayList<>();
         DataType[] geoDataTypes = { EsqlDataTypes.GEO_POINT, EsqlDataTypes.GEO_SHAPE };
-        addSpatialCombinations(suppliers, geoDataTypes);
+        SpatialRelatesFunctionTestCase.addSpatialCombinations(suppliers, geoDataTypes);
         DataType[] cartesianDataTypes = { EsqlDataTypes.CARTESIAN_POINT, EsqlDataTypes.CARTESIAN_SHAPE };
-        addSpatialCombinations(suppliers, cartesianDataTypes);
+        SpatialRelatesFunctionTestCase.addSpatialCombinations(suppliers, cartesianDataTypes);
         return parameterSuppliersFromTypedData(
             errorsForCasesWithoutExamples(anyNullIsNull(true, suppliers), SpatialIntersectsTests::typeErrorMessage)
         );
@@ -59,155 +43,4 @@ public class SpatialIntersectsTests extends AbstractFunctionTestCase {
     protected Expression build(Source source, List<Expression> args) {
         return new SpatialIntersects(source, args.get(0), args.get(1));
     }
-
-    private static void addSpatialCombinations(List<TestCaseSupplier> suppliers, DataType[] dataTypes) {
-        for (DataType leftType : dataTypes) {
-            TestCaseSupplier.TypedDataSupplier leftDataSupplier = testCaseSupplier(leftType);
-            for (DataType rightType : dataTypes) {
-                if (typeCompatible(leftType, rightType)) {
-                    TestCaseSupplier.TypedDataSupplier rightDataSupplier = testCaseSupplier(rightType);
-                    suppliers.add(
-                        TestCaseSupplier.testCaseSupplier(
-                            leftDataSupplier,
-                            rightDataSupplier,
-                            SpatialIntersectsTests::spatialEvaluatorString,
-                            DataTypes.BOOLEAN,
-                            (l, r) -> expected(l, leftType, r, rightType)
-                        )
-                    );
-                }
-            }
-        }
-    }
-
-    /**
-     * Build the expected error message for an invalid type signature.
-     */
-    protected static String typeErrorMessage(boolean includeOrdinal, List<Set<DataType>> validPerPosition, List<DataType> types) {
-        List<Integer> badArgPositions = new ArrayList<>();
-        for (int i = 0; i < types.size(); i++) {
-            if (validPerPosition.get(i).contains(types.get(i)) == false) {
-                badArgPositions.add(i);
-            }
-        }
-        if (badArgPositions.size() == 0) {
-            return oneInvalid(1, 0, includeOrdinal, types);
-        } else if (badArgPositions.size() == 1) {
-            int badArgPosition = badArgPositions.get(0);
-            int goodArgPosition = badArgPosition == 0 ? 1 : 0;
-            if (isSpatial(types.get(goodArgPosition)) == false) {
-                return oneInvalid(badArgPosition, -1, includeOrdinal, types);
-            } else {
-                return oneInvalid(badArgPosition, goodArgPosition, includeOrdinal, types);
-            }
-        } else {
-            return oneInvalid(0, -1, includeOrdinal, types);
-        }
-    }
-
-    private static String oneInvalid(int badArgPosition, int goodArgPosition, boolean includeOrdinal, List<DataType> types) {
-        String ordinal = includeOrdinal ? TypeResolutions.ParamOrdinal.fromIndex(badArgPosition).name().toLowerCase(Locale.ROOT) + " " : "";
-        String expectedType = goodArgPosition >= 0
-            ? compatibleTypes(types.get(goodArgPosition))
-            : "geo_point, cartesian_point, geo_shape or cartesian_shape";
-        String name = types.get(badArgPosition).typeName();
-        return ordinal + "argument of [] must be [" + expectedType + "], found value [" + name + "] type [" + name + "]";
-    }
-
-    private static String compatibleTypes(DataType spatialDataType) {
-        return Strings.join(compatibleTypeNames(spatialDataType), " or ");
-    }
-
-    private static TestCaseSupplier.TypedDataSupplier testCaseSupplier(DataType dataType) {
-        return switch (dataType.esType()) {
-            case "geo_point" -> TestCaseSupplier.geoPointCases(() -> false).get(0);
-            case "geo_shape" -> TestCaseSupplier.geoShapeCases(() -> false).get(0);
-            case "cartesian_point" -> TestCaseSupplier.cartesianPointCases(() -> false).get(0);
-            case "cartesian_shape" -> TestCaseSupplier.cartesianShapeCases(() -> false).get(0);
-            default -> throw new IllegalArgumentException("Unsupported datatype for ST_INTERSECTS: " + dataType);
-        };
-    }
-
-    private static Object expected(Object left, DataType leftType, Object right, DataType rightType) {
-        if (typeCompatible(leftType, rightType) == false) {
-            return null;
-        }
-        // TODO cast objects to right type and check intersection
-        BytesRef leftWKB = asGeometryWKB(left, leftType);
-        BytesRef rightWKB = asGeometryWKB(right, rightType);
-        SpatialRelatesFunction.SpatialRelations spatialIntersects = spatialRelations(left, leftType, right, rightType);
-        try {
-            return spatialIntersects.geometryRelatesGeometry(leftWKB, rightWKB);
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    private static SpatialRelatesFunction.SpatialRelations spatialRelations(
-        Object left,
-        DataType leftType,
-        Object right,
-        DataType rightType
-    ) {
-        if (isSpatialGeo(leftType) || isSpatialGeo(rightType)) {
-            return SpatialIntersects.GEO;
-        } else if (isSpatial(leftType) || isSpatial(rightType)) {
-            return SpatialIntersects.CARTESIAN;
-        } else {
-            throw new IllegalArgumentException(
-                "Unsupported left and right types: left["
-                    + leftType.esType()
-                    + ":"
-                    + left.getClass().getSimpleName()
-                    + "] right["
-                    + rightType.esType()
-                    + ":"
-                    + right.getClass().getSimpleName()
-                    + "]"
-            );
-        }
-    }
-
-    private static BytesRef asGeometryWKB(Object object, DataType dataType) {
-        if (isString(dataType)) {
-            return SpatialCoordinateTypes.UNSPECIFIED.wktToWkb(object.toString());
-        } else if (object instanceof BytesRef wkb) {
-            return wkb;
-        } else {
-            throw new IllegalArgumentException("Invalid geometry base type for " + dataType + ": " + object.getClass().getSimpleName());
-        }
-    }
-
-    private static boolean typeCompatible(DataType leftType, DataType rightType) {
-        if (isSpatial(leftType) && isSpatial(rightType)) {
-            // Both must be GEO_* or both must be CARTESIAN_*
-            return countGeo(leftType, rightType) != 1;
-        }
-        return true;
-    }
-
-    private static DataType pickSpatialType(DataType leftType, DataType rightType) {
-        if (isSpatial(leftType)) {
-            return leftType;
-        } else if (isSpatial(rightType)) {
-            return rightType;
-        } else {
-            throw new IllegalArgumentException("Invalid spatial types: " + leftType + " and " + rightType);
-        }
-    }
-
-    private static String spatialEvaluatorString(DataType leftType, DataType rightType) {
-        String crsType = isSpatialGeo(pickSpatialType(leftType, rightType)) ? "Geo" : "Cartesian";
-        return "SpatialIntersects" + crsType + "SourceAndSourceEvaluator[leftValue=Attribute[channel=0], rightValue=Attribute[channel=1]]";
-    }
-
-    private static int countGeo(DataType... types) {
-        int count = 0;
-        for (DataType type : types) {
-            if (isSpatialGeo(type)) {
-                count++;
-            }
-        }
-        return count;
-    }
 }

+ 207 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialRelatesFunctionTestCase.java

@@ -0,0 +1,207 @@
+/*
+ * 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 joptsimple.internal.Strings;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.ql.expression.TypeResolutions;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+import org.elasticsearch.xpack.ql.util.SpatialCoordinateTypes;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesFunction.compatibleTypeNames;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.isSpatial;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.isSpatialGeo;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.isString;
+
+public abstract class SpatialRelatesFunctionTestCase extends AbstractFunctionTestCase {
+
+    private static String getFunctionClassName() {
+        Class<?> testClass = getTestClass();
+        String testClassName = testClass.getSimpleName();
+        return testClassName.replace("Tests", "");
+    }
+
+    private static Class<?> getSpatialRelatesFunctionClass() throws ClassNotFoundException {
+        String functionClassName = getFunctionClassName();
+        return Class.forName("org.elasticsearch.xpack.esql.expression.function.scalar.spatial." + functionClassName);
+    }
+
+    private static SpatialRelatesFunction.SpatialRelations getRelationsField(String name) {
+        try {
+            Field field = getSpatialRelatesFunctionClass().getField(name);
+            Object value = field.get(null);
+            return (SpatialRelatesFunction.SpatialRelations) value;
+        } catch (NoSuchFieldException | ClassNotFoundException | IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    protected static void addSpatialCombinations(List<TestCaseSupplier> suppliers, DataType[] dataTypes) {
+        for (DataType leftType : dataTypes) {
+            TestCaseSupplier.TypedDataSupplier leftDataSupplier = testCaseSupplier(leftType);
+            for (DataType rightType : dataTypes) {
+                if (typeCompatible(leftType, rightType)) {
+                    TestCaseSupplier.TypedDataSupplier rightDataSupplier = testCaseSupplier(rightType);
+                    suppliers.add(
+                        TestCaseSupplier.testCaseSupplier(
+                            leftDataSupplier,
+                            rightDataSupplier,
+                            SpatialRelatesFunctionTestCase::spatialEvaluatorString,
+                            DataTypes.BOOLEAN,
+                            (l, r) -> expected(l, leftType, r, rightType)
+                        )
+                    );
+                }
+            }
+        }
+    }
+
+    /**
+     * Build the expected error message for an invalid type signature.
+     */
+    protected static String typeErrorMessage(boolean includeOrdinal, List<Set<DataType>> validPerPosition, List<DataType> types) {
+        List<Integer> badArgPositions = new ArrayList<>();
+        for (int i = 0; i < types.size(); i++) {
+            if (validPerPosition.get(i).contains(types.get(i)) == false) {
+                badArgPositions.add(i);
+            }
+        }
+        if (badArgPositions.isEmpty()) {
+            return oneInvalid(1, 0, includeOrdinal, types);
+        } else if (badArgPositions.size() == 1) {
+            int badArgPosition = badArgPositions.get(0);
+            int goodArgPosition = badArgPosition == 0 ? 1 : 0;
+            if (isSpatial(types.get(goodArgPosition)) == false) {
+                return oneInvalid(badArgPosition, -1, includeOrdinal, types);
+            } else {
+                return oneInvalid(badArgPosition, goodArgPosition, includeOrdinal, types);
+            }
+        } else {
+            return oneInvalid(0, -1, includeOrdinal, types);
+        }
+    }
+
+    private static String oneInvalid(int badArgPosition, int goodArgPosition, boolean includeOrdinal, List<DataType> types) {
+        String ordinal = includeOrdinal ? TypeResolutions.ParamOrdinal.fromIndex(badArgPosition).name().toLowerCase(Locale.ROOT) + " " : "";
+        String expectedType = goodArgPosition >= 0
+            ? compatibleTypes(types.get(goodArgPosition))
+            : "geo_point, cartesian_point, geo_shape or cartesian_shape";
+        String name = types.get(badArgPosition).typeName();
+        return ordinal + "argument of [] must be [" + expectedType + "], found value [" + name + "] type [" + name + "]";
+    }
+
+    private static String compatibleTypes(DataType spatialDataType) {
+        return Strings.join(compatibleTypeNames(spatialDataType), " or ");
+    }
+
+    private static TestCaseSupplier.TypedDataSupplier testCaseSupplier(DataType dataType) {
+        return switch (dataType.esType()) {
+            case "geo_point" -> TestCaseSupplier.geoPointCases(() -> false).get(0);
+            case "geo_shape" -> TestCaseSupplier.geoShapeCases(() -> false).get(0);
+            case "cartesian_point" -> TestCaseSupplier.cartesianPointCases(() -> false).get(0);
+            case "cartesian_shape" -> TestCaseSupplier.cartesianShapeCases(() -> false).get(0);
+            default -> throw new IllegalArgumentException("Unsupported datatype for " + functionName() + ": " + dataType);
+        };
+    }
+
+    private static Object expected(Object left, DataType leftType, Object right, DataType rightType) {
+        if (typeCompatible(leftType, rightType) == false) {
+            return null;
+        }
+        // TODO cast objects to right type and check intersection
+        BytesRef leftWKB = asGeometryWKB(left, leftType);
+        BytesRef rightWKB = asGeometryWKB(right, rightType);
+        SpatialRelatesFunction.SpatialRelations spatialRelations = spatialRelations(left, leftType, right, rightType);
+        try {
+            return spatialRelations.geometryRelatesGeometry(leftWKB, rightWKB);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static SpatialRelatesFunction.SpatialRelations spatialRelations(
+        Object left,
+        DataType leftType,
+        Object right,
+        DataType rightType
+    ) {
+        if (isSpatialGeo(leftType) || isSpatialGeo(rightType)) {
+            return getRelationsField("GEO");
+        } else if (isSpatial(leftType) || isSpatial(rightType)) {
+            return getRelationsField("CARTESIAN");
+        } else {
+            throw new IllegalArgumentException(
+                "Unsupported left and right types: left["
+                    + leftType.esType()
+                    + ":"
+                    + left.getClass().getSimpleName()
+                    + "] right["
+                    + rightType.esType()
+                    + ":"
+                    + right.getClass().getSimpleName()
+                    + "]"
+            );
+        }
+    }
+
+    private static BytesRef asGeometryWKB(Object object, DataType dataType) {
+        if (isString(dataType)) {
+            return SpatialCoordinateTypes.UNSPECIFIED.wktToWkb(object.toString());
+        } else if (object instanceof BytesRef wkb) {
+            return wkb;
+        } else {
+            throw new IllegalArgumentException("Invalid geometry base type for " + dataType + ": " + object.getClass().getSimpleName());
+        }
+    }
+
+    private static boolean typeCompatible(DataType leftType, DataType rightType) {
+        if (isSpatial(leftType) && isSpatial(rightType)) {
+            // Both must be GEO_* or both must be CARTESIAN_*
+            return countGeo(leftType, rightType) != 1;
+        }
+        return true;
+    }
+
+    private static DataType pickSpatialType(DataType leftType, DataType rightType) {
+        if (isSpatial(leftType)) {
+            return leftType;
+        } else if (isSpatial(rightType)) {
+            return rightType;
+        } else {
+            throw new IllegalArgumentException("Invalid spatial types: " + leftType + " and " + rightType);
+        }
+    }
+
+    private static String spatialEvaluatorString(DataType leftType, DataType rightType) {
+        String crsType = isSpatialGeo(pickSpatialType(leftType, rightType)) ? "Geo" : "Cartesian";
+        return getFunctionClassName()
+            + crsType
+            + "SourceAndSourceEvaluator[leftValue=Attribute[channel=0], rightValue=Attribute[channel=1]]";
+    }
+
+    private static int countGeo(DataType... types) {
+        int count = 0;
+        for (DataType type : types) {
+            if (isSpatialGeo(type)) {
+                count++;
+            }
+        }
+        return count;
+    }
+}

+ 46 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithinTests.java

@@ -0,0 +1,46 @@
+/*
+ * 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.elasticsearch.xpack.esql.expression.function.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+@FunctionName("st_within")
+public class SpatialWithinTests extends SpatialRelatesFunctionTestCase {
+    public SpatialWithinTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        List<TestCaseSupplier> suppliers = new ArrayList<>();
+        DataType[] geoDataTypes = { EsqlDataTypes.GEO_POINT, EsqlDataTypes.GEO_SHAPE };
+        SpatialRelatesFunctionTestCase.addSpatialCombinations(suppliers, geoDataTypes);
+        DataType[] cartesianDataTypes = { EsqlDataTypes.CARTESIAN_POINT, EsqlDataTypes.CARTESIAN_SHAPE };
+        SpatialRelatesFunctionTestCase.addSpatialCombinations(suppliers, cartesianDataTypes);
+        return parameterSuppliersFromTypedData(
+            errorsForCasesWithoutExamples(anyNullIsNull(true, suppliers), SpatialWithinTests::typeErrorMessage)
+        );
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new SpatialWithin(source, args.get(0), args.get(1));
+    }
+}

+ 172 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java

@@ -42,7 +42,10 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialCentroi
 import org.elasticsearch.xpack.esql.expression.function.aggregate.Sum;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToGeoPoint;
 import org.elasticsearch.xpack.esql.expression.function.scalar.math.Round;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialContains;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialIntersects;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesFunction;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialWithin;
 import org.elasticsearch.xpack.esql.parser.EsqlParser;
 import org.elasticsearch.xpack.esql.plan.logical.Enrich;
 import org.elasticsearch.xpack.esql.plan.logical.Eval;
@@ -120,6 +123,7 @@ import static org.elasticsearch.xpack.esql.EsqlTestUtils.withDefaultLimitWarning
 import static org.elasticsearch.xpack.esql.SerializationTestUtils.assertSerialization;
 import static org.elasticsearch.xpack.esql.plan.physical.AggregateExec.Mode.FINAL;
 import static org.elasticsearch.xpack.esql.plan.physical.AggregateExec.Mode.PARTIAL;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.CARTESIAN_POINT;
 import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.GEO_POINT;
 import static org.elasticsearch.xpack.ql.expression.Expressions.name;
 import static org.elasticsearch.xpack.ql.expression.Expressions.names;
@@ -2925,6 +2929,174 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
         }
     }
 
+    private record TestSpatialRelation(ShapeRelation relation, TestDataSource index, boolean literalRight, boolean canPushToSource) {
+        String function() {
+            return switch (relation) {
+                case INTERSECTS -> "ST_INTERSECTS";
+                case WITHIN -> "ST_WITHIN";
+                case CONTAINS -> "ST_CONTAINS";
+                default -> throw new IllegalArgumentException("Unsupported relation: " + relation);
+            };
+        }
+
+        Class<? extends SpatialRelatesFunction> functionClass() {
+            return switch (relation) {
+                case INTERSECTS -> SpatialIntersects.class;
+                case WITHIN -> literalRight ? SpatialWithin.class : SpatialContains.class;
+                case CONTAINS -> literalRight ? SpatialContains.class : SpatialWithin.class;
+                default -> throw new IllegalArgumentException("Unsupported relation: " + relation);
+            };
+        }
+
+        ShapeRelation relationship() {
+            return switch (relation) {
+                case WITHIN -> literalRight ? ShapeRelation.WITHIN : ShapeRelation.CONTAINS;
+                case CONTAINS -> literalRight ? ShapeRelation.CONTAINS : ShapeRelation.WITHIN;
+                default -> relation;
+            };
+        }
+
+        DataType locationType() {
+            return index.index.name().endsWith("_web") ? CARTESIAN_POINT : GEO_POINT;
+        }
+
+        String castFunction() {
+            return index.index.name().endsWith("_web") ? "TO_CARTESIANSHAPE" : "TO_GEOSHAPE";
+        }
+
+        String predicate() {
+            String field = "location";
+            String literal = castFunction() + "(\"POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))\")";
+            return literalRight ? function() + "(" + field + ", " + literal + ")" : function() + "(" + literal + ", " + field + ")";
+        }
+    }
+
+    public void testPushDownSpatialRelatesStringToSource() {
+        TestSpatialRelation[] tests = new TestSpatialRelation[] {
+            new TestSpatialRelation(ShapeRelation.INTERSECTS, airports, true, true),
+            new TestSpatialRelation(ShapeRelation.INTERSECTS, airports, false, true),
+            new TestSpatialRelation(ShapeRelation.WITHIN, airports, true, true),
+            new TestSpatialRelation(ShapeRelation.WITHIN, airports, false, true),
+            new TestSpatialRelation(ShapeRelation.CONTAINS, airports, true, true),
+            new TestSpatialRelation(ShapeRelation.CONTAINS, airports, false, true),
+            new TestSpatialRelation(ShapeRelation.INTERSECTS, airportsWeb, true, true),
+            new TestSpatialRelation(ShapeRelation.INTERSECTS, airportsWeb, false, true),
+            new TestSpatialRelation(ShapeRelation.WITHIN, airportsWeb, true, true),
+            new TestSpatialRelation(ShapeRelation.WITHIN, airportsWeb, false, true),
+            new TestSpatialRelation(ShapeRelation.CONTAINS, airportsWeb, true, true),
+            new TestSpatialRelation(ShapeRelation.CONTAINS, airportsWeb, false, true) };
+        for (TestSpatialRelation test : tests) {
+            var plan = this.physicalPlan("FROM " + test.index.index.name() + " | WHERE " + test.predicate(), test.index);
+            var limit = as(plan, LimitExec.class);
+            var exchange = as(limit.child(), ExchangeExec.class);
+            var fragment = as(exchange.child(), FragmentExec.class);
+            var limit2 = as(fragment.fragment(), Limit.class);
+            var filter = as(limit2.child(), Filter.class);
+            assertThat(test.predicate(), filter.condition(), instanceOf(test.functionClass()));
+
+            var optimized = optimizedPlan(plan);
+            var topLimit = as(optimized, LimitExec.class);
+            exchange = as(topLimit.child(), ExchangeExec.class);
+            var project = as(exchange.child(), ProjectExec.class);
+            var fieldExtract = as(project.child(), FieldExtractExec.class);
+            if (test.canPushToSource) {
+                var source = source(fieldExtract.child());
+                // TODO: bring back SingleValueQuery once it can handle LeafShapeFieldData
+                // var condition = as(sv(source.query(), "location"), AbstractGeometryQueryBuilder.class);
+                var condition = as(source.query(), SpatialRelatesQuery.ShapeQueryBuilder.class);
+                assertThat("Geometry field name: " + test.predicate(), condition.fieldName(), equalTo("location"));
+                assertThat("Spatial relationship: " + test.predicate(), condition.relation(), equalTo(test.relationship()));
+                assertThat("Geometry is Polygon: " + test.predicate(), condition.shape().type(), equalTo(ShapeType.POLYGON));
+                var polygon = as(condition.shape(), Polygon.class);
+                assertThat("Polygon shell length: " + test.predicate(), polygon.getPolygon().length(), equalTo(5));
+                assertThat("Polygon holes: " + test.predicate(), polygon.getNumberOfHoles(), equalTo(0));
+            } else {
+                // Currently CARTESIAN fields do not support lucene push-down for CONTAINS/WITHIN
+                var limitExec = as(fieldExtract.child(), LimitExec.class);
+                var filterExec = as(limitExec.child(), FilterExec.class);
+                var fieldExtractLocation = as(filterExec.child(), FieldExtractExec.class);
+                assertThat(test.predicate(), fieldExtractLocation.attributesToExtract().size(), equalTo(1));
+                assertThat(test.predicate(), fieldExtractLocation.attributesToExtract().get(0).name(), equalTo("location"));
+                var source = source(fieldExtractLocation.child());
+                assertThat(test.predicate(), source.query(), equalTo(null));
+            }
+        }
+    }
+
+    public void testPushDownSpatialRelatesStringToSourceAndUseDocValuesForCentroid() {
+        TestSpatialRelation[] tests = new TestSpatialRelation[] {
+            new TestSpatialRelation(ShapeRelation.INTERSECTS, airports, true, true),
+            new TestSpatialRelation(ShapeRelation.INTERSECTS, airports, false, true),
+            new TestSpatialRelation(ShapeRelation.WITHIN, airports, true, true),
+            new TestSpatialRelation(ShapeRelation.WITHIN, airports, false, true),
+            new TestSpatialRelation(ShapeRelation.CONTAINS, airports, true, true),
+            new TestSpatialRelation(ShapeRelation.CONTAINS, airports, false, true),
+            new TestSpatialRelation(ShapeRelation.WITHIN, airportsWeb, true, true),
+            new TestSpatialRelation(ShapeRelation.WITHIN, airportsWeb, false, true),
+            new TestSpatialRelation(ShapeRelation.CONTAINS, airportsWeb, true, true),
+            new TestSpatialRelation(ShapeRelation.CONTAINS, airportsWeb, false, true) };
+        for (TestSpatialRelation test : tests) {
+            var centroidExpr = "centroid=ST_CENTROID(location), count=COUNT()";
+            var plan = this.physicalPlan(
+                "FROM " + test.index.index.name() + " | WHERE " + test.predicate() + " | STATS " + centroidExpr,
+                test.index
+            );
+            var limit = as(plan, LimitExec.class);
+            var agg = as(limit.child(), AggregateExec.class);
+            assertThat("No groupings in aggregation", agg.groupings().size(), equalTo(0));
+            // Before optimization the aggregation does not use doc-values
+            assertAggregation(agg, "count", Count.class);
+            assertAggregation(agg, "centroid", SpatialCentroid.class, test.locationType(), false);
+            var exchange = as(agg.child(), ExchangeExec.class);
+            var fragment = as(exchange.child(), FragmentExec.class);
+            var fAgg = as(fragment.fragment(), Aggregate.class);
+            var filter = as(fAgg.child(), Filter.class);
+            assertThat(test.predicate(), filter.condition(), instanceOf(test.functionClass()));
+
+            // Now verify that optimization re-writes the ExchangeExec and pushed down the filter into the Lucene query
+            var optimized = optimizedPlan(plan);
+            limit = as(optimized, LimitExec.class);
+            agg = as(limit.child(), AggregateExec.class);
+            // Above the exchange (in coordinator) the aggregation is not using doc-values
+            assertAggregation(agg, "count", Count.class);
+            assertAggregation(agg, "centroid", SpatialCentroid.class, test.locationType(), false);
+            exchange = as(agg.child(), ExchangeExec.class);
+            agg = as(exchange.child(), AggregateExec.class);
+            assertThat("Aggregation is PARTIAL", agg.getMode(), equalTo(PARTIAL));
+            // below the exchange (in data node) the aggregation is using doc-values
+            assertAggregation(agg, "count", Count.class);
+            assertAggregation(agg, "centroid", SpatialCentroid.class, test.locationType(), true);
+            if (test.canPushToSource) {
+                var extract = as(agg.child(), FieldExtractExec.class);
+                assertTrue(
+                    "Expect field attribute to be extracted as doc-values",
+                    extract.attributesToExtract()
+                        .stream()
+                        .allMatch(attr -> extract.hasDocValuesAttribute(attr) && attr.dataType() == test.locationType())
+                );
+                var source = source(extract.child());
+                // TODO: bring back SingleValueQuery once it can handle LeafShapeFieldData
+                // var condition = as(sv(source.query(), "location"), AbstractGeometryQueryBuilder.class);
+                var condition = as(source.query(), SpatialRelatesQuery.ShapeQueryBuilder.class);
+                assertThat("Geometry field name: " + test.predicate(), condition.fieldName(), equalTo("location"));
+                assertThat("Spatial relationship: " + test.predicate(), condition.relation(), equalTo(test.relationship()));
+                assertThat("Geometry is Polygon: " + test.predicate(), condition.shape().type(), equalTo(ShapeType.POLYGON));
+                var polygon = as(condition.shape(), Polygon.class);
+                assertThat("Polygon shell length: " + test.predicate(), polygon.getPolygon().length(), equalTo(5));
+                assertThat("Polygon holes: " + test.predicate(), polygon.getNumberOfHoles(), equalTo(0));
+            } else {
+                // Currently CARTESIAN fields do not support lucene push-down for CONTAINS/WITHIN
+                var filterExec = as(agg.child(), FilterExec.class);
+                var fieldExtractLocation = as(filterExec.child(), FieldExtractExec.class);
+                assertThat(test.predicate(), fieldExtractLocation.attributesToExtract().size(), equalTo(1));
+                assertThat(test.predicate(), fieldExtractLocation.attributesToExtract().get(0).name(), equalTo("location"));
+                var source = source(fieldExtractLocation.child());
+                assertThat(test.predicate(), source.query(), equalTo(null));
+
+            }
+        }
+    }
+
     /**
      * Plan:
      * Plan:

Some files were not shown because too many files changed in this diff