Browse Source

ESQL: Support ST_DISJOINT (#107007)

* WIP Started developing ST_DISJOINT

Initially based on ST_INTERSECTS

* Fix functions list and add spatial point integration tests

* Update docs/changelog/107007.yaml

* More tests for shapes and cartesian-multigeoms

* Some more tests to highlight issues with DISJOINT on cartesian point indices

* Disable Lucene push-down for DISJOINT on cartesian point indices

* Added docs for ST_DISJOINT

* Support DISJOINT in the lucene-pushdown code for cartesian point indexes

* Re-enable push-to-source for DISJOINT on cartesian_point indices

* Fix docs example

* Try fix internal docs links which are not being rendered

* Fixed disjoint on empty geometry

* Added tests on empty linestring, and changed lucene push-down to exception

In lucene code only LineString can be empty, but in Elasticsearch even that is not allowed, resulting in parsing errors. So we cannot get to this code in the lucene push-down and now throw an error instead. The tests now assert on the warnings.

Note that for any predicate DISJOINT and INTERSECTS alike, the predicate fails, because the parsing error results in null, the function returns null, the predicate interprets this as false, and no documents match. This null-in-null-out rule means that DISJOINT and INTERSECTS give the same answer on invalid geometries.
Craig Taverner 1 year ago
parent
commit
a7b38394d9
38 changed files with 1912 additions and 27 deletions
  1. 5 0
      docs/changelog/107007.yaml
  2. 0 2
      docs/reference/esql/functions/description/st_contains.asciidoc
  3. 5 0
      docs/reference/esql/functions/description/st_disjoint.asciidoc
  4. 0 2
      docs/reference/esql/functions/description/st_intersects.asciidoc
  5. 0 2
      docs/reference/esql/functions/description/st_within.asciidoc
  6. 13 0
      docs/reference/esql/functions/examples/st_disjoint.asciidoc
  7. 15 0
      docs/reference/esql/functions/layout/st_disjoint.asciidoc
  8. 9 0
      docs/reference/esql/functions/parameters/st_disjoint.asciidoc
  9. 1 0
      docs/reference/esql/functions/signature/st_disjoint.svg
  10. 2 0
      docs/reference/esql/functions/spatial-functions.asciidoc
  11. 1 1
      docs/reference/esql/functions/st_contains.asciidoc
  12. 27 0
      docs/reference/esql/functions/st_disjoint.asciidoc
  13. 1 0
      docs/reference/esql/functions/st_intersects.asciidoc
  14. 1 1
      docs/reference/esql/functions/st_within.asciidoc
  15. 16 0
      docs/reference/esql/functions/types/st_disjoint.asciidoc
  16. 68 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/cartesian_multipolygons.csv-spec
  17. 5 1
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/meta.csv-spec
  18. 227 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec
  19. 52 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial_shapes.csv-spec
  20. 128 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointCartesianPointDocValuesAndConstantEvaluator.java
  21. 142 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointCartesianPointDocValuesAndSourceEvaluator.java
  22. 132 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointCartesianSourceAndConstantEvaluator.java
  23. 152 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointCartesianSourceAndSourceEvaluator.java
  24. 128 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointGeoPointDocValuesAndConstantEvaluator.java
  25. 151 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointGeoPointDocValuesAndSourceEvaluator.java
  26. 132 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointGeoSourceAndConstantEvaluator.java
  27. 152 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointGeoSourceAndSourceEvaluator.java
  28. 2 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  29. 0 2
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialContains.java
  30. 239 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjoint.java
  31. 0 2
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersects.java
  32. 5 1
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialRelatesFunction.java
  33. 0 2
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialWithin.java
  34. 6 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java
  35. 7 1
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlFeatures.java
  36. 29 10
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/SpatialRelatesQuery.java
  37. 46 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointTests.java
  38. 13 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java

+ 5 - 0
docs/changelog/107007.yaml

@@ -0,0 +1,5 @@
+pr: 107007
+summary: "ESQL: Support ST_DISJOINT"
+area: ES|QL
+type: enhancement
+issues: []

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

@@ -3,5 +3,3 @@
 *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.

+ 5 - 0
docs/reference/esql/functions/description/st_disjoint.asciidoc

@@ -0,0 +1,5 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Returns whether the two geometries or geometry columns are disjoint.

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

@@ -3,5 +3,3 @@
 *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.

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

@@ -3,5 +3,3 @@
 *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_disjoint.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_disjoint-airport_city_boundaries]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/spatial_shapes.csv-spec[tag=st_disjoint-airport_city_boundaries-result]
+|===
+

+ 15 - 0
docs/reference/esql/functions/layout/st_disjoint.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_disjoint]]
+=== `ST_DISJOINT`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_disjoint.svg[Embedded,opts=inline]
+
+include::../parameters/st_disjoint.asciidoc[]
+include::../description/st_disjoint.asciidoc[]
+include::../types/st_disjoint.asciidoc[]
+include::../examples/st_disjoint.asciidoc[]

+ 9 - 0
docs/reference/esql/functions/parameters/st_disjoint.asciidoc

@@ -0,0 +1,9 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*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_disjoint.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_DISJOINT</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>

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

@@ -9,6 +9,7 @@
 
 // tag::spatial_list[]
 * experimental:[] <<esql-st_intersects>>
+* experimental:[] <<esql-st_disjoint>>
 * experimental:[] <<esql-st_contains>>
 * experimental:[] <<esql-st_within>>
 * experimental:[] <<esql-st_x>>
@@ -16,6 +17,7 @@
 // end::spatial_list[]
 
 include::st_intersects.asciidoc[]
+include::st_disjoint.asciidoc[]
 include::st_contains.asciidoc[]
 include::st_within.asciidoc[]
 include::st_x.asciidoc[]

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

@@ -20,7 +20,7 @@ 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.
+This is the inverse of the <<esql-st_within,ST_WITHIN>> function.
 
 include::types/st_contains.asciidoc[]
 include::examples/st_contains.asciidoc[]

+ 27 - 0
docs/reference/esql/functions/st_disjoint.asciidoc

@@ -0,0 +1,27 @@
+[discrete]
+[[esql-st_disjoint]]
+=== `ST_DISJOINT`
+
+experimental::[]
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_disjoint.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_disjoint.asciidoc[]
+This is the inverse of the <<esql-st_intersects,ST_INTERSECTS>> function.
+In mathematical terms: ST_Disjoint(A, B) ⇔ A ⋂ B = ∅
+
+include::types/st_disjoint.asciidoc[]
+include::examples/st_disjoint.asciidoc[]

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

@@ -24,6 +24,7 @@ This means it is not possible to combine `geo_*` and `cartesian_*` parameters.
 Returns true if two geometries intersect.
 They intersect if they have any point in common, including their interior points
 (points along lines or within polygons).
+This is the inverse of the <<esql-st_disjoint,ST_DISJOINT>> function.
 In mathematical terms: ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅
 
 include::types/st_intersects.asciidoc[]

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

@@ -20,7 +20,7 @@ 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.
+This is the inverse of the <<esql-st_contains,ST_CONTAINS>> function.
 
 include::types/st_within.asciidoc[]
 include::examples/st_within.asciidoc[]

+ 16 - 0
docs/reference/esql/functions/types/st_disjoint.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
+|===

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

@@ -52,6 +52,29 @@ id:l | name:keyword            | shape:cartesian_shape
 16   | Bottom left point       | POINT(0.5 0.5)
 ;
 
+whereDisjointSinglePolygon
+required_feature: esql.st_disjoint
+
+FROM cartesian_multipolygons
+| WHERE ST_Disjoint(shape, TO_CARTESIANSHAPE("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))"))
+| SORT id
+;
+
+id:l | name:keyword             | shape:cartesian_shape
+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))
+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))
+12   | Bottom right diagonal    | LINESTRING(2 0, 3 1)
+13   | Top right diagonal       | LINESTRING(2 2, 3 3)
+14   | Top left diagonal        | LINESTRING(0 2, 1 3)
+17   | Bottom right point       | POINT(2.5 0.5)
+18   | Top right point          | POINT(2.5 2.5)
+19   | Top left point           | POINT(0.5 2.5)
+;
+
 ####################################################################################################
 # Test against a polygon smaller in size to the Bottom Left polygon
 
@@ -99,6 +122,29 @@ id:l | name:keyword | shape:cartesian_shape
 16   | Bottom left point       | POINT(0.5 0.5)
 ;
 
+whereDisjointSmallerPolygon
+required_feature: esql.st_disjoint
+
+FROM cartesian_multipolygons
+| WHERE ST_Disjoint(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
+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))
+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))
+12   | Bottom right diagonal    | LINESTRING(2 0, 3 1)
+13   | Top right diagonal       | LINESTRING(2 2, 3 3)
+14   | Top left diagonal        | LINESTRING(0 2, 1 3)
+17   | Bottom right point       | POINT(2.5 0.5)
+18   | Top right point          | POINT(2.5 2.5)
+19   | Top left point           | POINT(0.5 2.5);
+;
+
 ####################################################################################################
 # Test against a polygon similar in size to the entire test data
 
@@ -175,6 +221,17 @@ id:l | name:keyword            | shape:cartesian_shape
 19   | Top left point          | POINT(0.5 2.5)
 ;
 
+whereDisjointLargerPolygon
+required_feature: esql.st_disjoint
+
+FROM cartesian_multipolygons
+| WHERE ST_Disjoint(shape, TO_CARTESIANSHAPE("POLYGON((0 0, 3 0, 3 3, 0 3, 0 0))"))
+| SORT id
+;
+
+id:l | name:keyword            | shape:cartesian_shape
+;
+
 ####################################################################################################
 # Test against a polygon larger than all test data
 
@@ -250,3 +307,14 @@ id:l | name:keyword            | shape:cartesian_shape
 18   | Top right point         | POINT(2.5 2.5)
 19   | Top left point          | POINT(0.5 2.5)
 ;
+
+whereDisjointEvenLargerPolygon
+required_feature: esql.st_disjoint
+
+FROM cartesian_multipolygons
+| WHERE ST_Disjoint(shape, TO_CARTESIANSHAPE("POLYGON((-1 -1, 4 -1, 4 4, -1 4, -1 -1))"))
+| SORT id
+;
+
+id:l | name:keyword            | shape:cartesian_shape
+;

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

@@ -66,6 +66,7 @@ double pi()
 "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_disjoint(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)"
@@ -175,6 +176,7 @@ split         |[string, delim]                     |["keyword|text", "keyword|te
 sqrt          |number                              |"double|integer|long|unsigned_long"                                                                                               |[""]
 st_centroid   |field                               |"geo_point|cartesian_point"                                                                                                       |[""]
 st_contains   |[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]
+st_disjoint   |[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]
 st_intersects |[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]
 st_within     |[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]
 st_x          |point                               |"geo_point|cartesian_point"                                                                                                       |[""]
@@ -285,6 +287,7 @@ split         |Split a single valued string into multiple strings.
 sqrt          |Returns the square root of a number.
 st_centroid   |The centroid of a spatial field.
 st_contains   |Returns whether the first geometry contains the second geometry.
+st_disjoint   |Returns whether the two geometries or geometry columns are disjoint.
 st_intersects |Returns whether the two geometries or geometry columns intersect.
 st_within     |Returns whether the first geometry is within the second geometry.
 st_x          |Extracts the x-coordinate from a point geometry.
@@ -396,6 +399,7 @@ split         |keyword
 sqrt          |double                                                                                                                      |false                       |false           |false
 st_centroid   |"geo_point|cartesian_point"                                                                                                 |false                       |false           |true
 st_contains   |boolean                                                                                                                     |[false, false]              |false           |false
+st_disjoint   |boolean                                                                                                                     |[false, false]              |false           |false
 st_intersects |boolean                                                                                                                     |[false, false]              |false           |false
 st_within     |boolean                                                                                                                     |[false, false]              |false           |false
 st_x          |double                                                                                                                      |false                       |false           |false
@@ -451,5 +455,5 @@ countFunctions#[skip:-8.13.99]
 meta functions |  stats  a = count(*), b = count(*), c = count(*) |  mv_expand c;
 
 a:long | b:long | c:long
-101    | 101    | 101
+102    | 102    | 102
 ;

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

@@ -605,6 +605,91 @@ location:geo_point  | city_location:geo_point  |  count:long
 POINT (0 0)         | POINT (0 0)              |  1
 ;
 
+###############################################
+# Tests for ST_DISJOINT on GEO_POINT type
+
+literalPolygonDisjointLiteralPoint
+required_feature: esql.st_disjoint
+
+ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_GEOPOINT(wkt)
+| WHERE ST_DISJOINT(TO_GEOSHAPE("POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"), pt)
+;
+
+wkt:keyword   | pt:geo_point
+"POINT(-1 -1)"  | POINT(-1 -1)
+"POINT(-1 1)" | POINT(-1 1)
+;
+
+literalPointDisjointLiteralPolygon
+required_feature: esql.st_disjoint
+
+ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_GEOPOINT(wkt)
+| WHERE ST_DISJOINT(pt, TO_GEOSHAPE("POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"))
+;
+
+wkt:keyword   | pt:geo_point
+"POINT(-1 -1)"  | POINT(-1 -1)
+"POINT(-1 1)" | POINT(-1 1)
+;
+
+literalPolygonDisjointLiteralPointOneRow
+required_feature: esql.st_disjoint
+
+ROW disjoint = ST_DISJOINT(TO_GEOSHAPE("POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"), TO_GEOPOINT("POINT(0 0)"))
+;
+
+disjoint:boolean
+false
+;
+
+literalPointDisjointLiteralPolygonOneRow
+required_feature: esql.st_disjoint
+
+ROW disjoint = ST_DISJOINT(TO_GEOPOINT("POINT(-1 0)"), TO_GEOSHAPE("POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"))
+;
+
+disjoint:boolean
+true
+;
+
+pointDisjointLiteralPolygon
+required_feature: esql.st_disjoint
+
+FROM airports
+| WHERE ST_DISJOINT(location, TO_GEOSHAPE("POLYGON((-10 -60, 120 -60, 120 60, -10 60, -10 -60))"))
+| EVAL x = ST_X(location), y = ST_Y(location)
+| EVAL x = FLOOR(x / 100), y = FLOOR(y / 100)
+| STATS count=COUNT() BY x, y
+| KEEP x, y, count
+| SORT x ASC, y ASC
+;
+
+x:double  |  y:double  |  count:long
+-2        |  -1        |  8
+-2        |  0         |  94
+-1        |  -1        |  67
+-1        |  0         |  201
+0         |  0         |  15
+1         |  -1        |  33
+1         |  0         |  53
+;
+
+airportCityLocationPointDisjointCentroid
+required_feature: esql.st_disjoint
+
+FROM airports_mp
+| WHERE ST_DISJOINT(location, city_location)
+| STATS location=ST_CENTROID(location), city_location=ST_CENTROID(city_location), count=COUNT()
+;
+
+location:geo_point                         | city_location:geo_point                      |  count:long
+POINT (67.8581917192787 24.02956652920693) | POINT (67.81638333333332 24.048999999999996) |  6
+;
+
 ###############################################
 # Tests for ST_CONTAINS on GEO_POINT type
 
@@ -1167,6 +1252,148 @@ centroid:cartesian_point    | count:long
 POINT (4783520.5 1661010.0) | 1
 ;
 
+cartesianPointIntersectsLiteralPolygonCount
+required_feature: esql.st_intersects
+
+FROM airports_web
+| WHERE ST_INTERSECTS(location, TO_CARTESIANSHAPE("POLYGON((0 -60000000, 120000000 -60000000, 120000000 60000000, 0 60000000, 0 -60000000))"))
+| STATS count=COUNT()
+;
+
+count:long
+444
+;
+
+###############################################
+# Tests for ST_DISJOINT on CARTESIAN_POINT type
+
+literalPolygonDisjointLiteralCartesianPoint
+required_feature: esql.st_disjoint
+
+ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_CARTESIANPOINT(wkt)
+| WHERE ST_DISJOINT(TO_CARTESIANSHAPE("POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"), pt)
+;
+
+wkt:keyword     | pt:cartesian_point
+"POINT(-1 -1)"  | POINT(-1 -1)
+"POINT(-1 1)"   | POINT(-1 1)
+;
+
+literalCartesianPointDisjointLiteralPolygon
+required_feature: esql.st_disjoint
+
+ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_CARTESIANPOINT(wkt)
+| WHERE ST_DISJOINT(pt, TO_CARTESIANSHAPE("POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"))
+;
+
+wkt:keyword     | pt:cartesian_point
+"POINT(-1 -1)"  | POINT(-1 -1)
+"POINT(-1 1)"   | POINT(-1 1)
+;
+
+literalPolygonDisjointLiteralCartesianPointOneRow
+required_feature: esql.st_disjoint
+
+ROW disjoint = ST_DISJOINT(TO_CARTESIANSHAPE("POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"), TO_CARTESIANPOINT("POINT(0 0)"))
+;
+
+disjoint:boolean
+false
+;
+
+literalCartesianPointDisjointLiteralPolygonOneRow
+required_feature: esql.st_disjoint
+
+ROW disjoint = ST_DISJOINT(TO_CARTESIANPOINT("POINT(-1 0)"), TO_CARTESIANSHAPE("POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"))
+;
+
+disjoint:boolean
+true
+;
+
+cartesianPointDisjointLiteralPolygonCount
+required_feature: esql.st_disjoint
+
+FROM airports_web
+| WHERE ST_DISJOINT(location, TO_CARTESIANSHAPE("POLYGON((0 -60000000, 120000000 -60000000, 120000000 60000000, 0 60000000, 0 -60000000))"))
+| STATS count=COUNT()
+;
+
+count:long
+405
+;
+
+cartesianPointIntersectsDisjointLiteralPolygonCount
+required_feature: esql.st_disjoint
+
+FROM airports_web
+| EVAL intersects = ST_INTERSECTS(location, TO_CARTESIANSHAPE("POLYGON((0 -60000000, 120000000 -60000000, 120000000 60000000, 0 60000000, 0 -60000000))"))
+| EVAL disjoint = ST_DISJOINT(location, TO_CARTESIANSHAPE("POLYGON((0 -60000000, 120000000 -60000000, 120000000 60000000, 0 60000000, 0 -60000000))"))
+| STATS count=COUNT() BY intersects, disjoint
+| SORT intersects DESC, disjoint DESC
+| KEEP intersects, disjoint, count
+;
+
+intersects:boolean  |  disjoint:boolean  |  count:long
+true                |  false             |  444
+false               |  true              |  405
+;
+
+cartesianPointDisjointLiteralPolygon
+required_feature: esql.st_disjoint
+
+FROM airports_web
+| WHERE ST_DISJOINT(location, TO_CARTESIANSHAPE("POLYGON((0 -60000000, 120000000 -60000000, 120000000 60000000, 0 60000000, 0 -60000000))"))
+| EVAL x = ST_X(location), y = ST_Y(location)
+| EVAL x = FLOOR(x / 10000000), y = FLOOR(y / 10000000)
+| STATS count=COUNT() BY x, y
+| KEEP x, y, count
+| SORT x ASC, y ASC
+;
+
+x:double  |  y:double  |  count:long
+-2        |  -1        |  8
+-2        |  0         |  136
+-2        |  1         |  3
+-1        |  -1        |  64
+-1        |  0         |  192
+-1        |  1         |  2
+;
+
+cartesianPointDisjointEmptyGeometry
+required_feature: esql.st_disjoint
+
+FROM airports_web
+| WHERE ST_DISJOINT(location, TO_CARTESIANSHAPE("LINESTRING()"))
+| STATS count=COUNT()
+;
+
+warning:Line 2:31: evaluation of [TO_CARTESIANSHAPE(\"LINESTRING()\")] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:31: java.lang.IllegalArgumentException: Failed to parse WKT: expected number but found: ')'
+
+count:long
+0
+;
+
+cartesianPointDisjointInvalidGeometry
+required_feature: esql.st_disjoint
+
+FROM airports_web
+| WHERE ST_DISJOINT(location, TO_CARTESIANSHAPE("Invalid Geometry"))
+| STATS count=COUNT()
+;
+
+warning:Line 2:31: evaluation of [TO_CARTESIANSHAPE(\"Invalid Geometry\")] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:31: java.lang.IllegalArgumentException: Failed to parse WKT: Unknown geometry type: invalid
+
+count:long
+0
+;
+
 ###############################################
 # Tests for ST_CONTAINS on CARTESIAN_POINT type
 

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

@@ -213,6 +213,27 @@ intersects:boolean
 true
 ;
 
+###############################################
+# Tests for ST_DISJOINT with GEO_SHAPE
+
+polygonDisjointLiteralPolygon
+required_feature: esql.st_disjoint
+
+// tag::st_disjoint-airport_city_boundaries[]
+FROM airport_city_boundaries
+| WHERE ST_DISJOINT(city_boundary, TO_GEOSHAPE("POLYGON((-10 -60, 120 -60, 120 60, -10 60, -10 -60))"))
+| KEEP abbrev, airport, region, city, city_location
+// end::st_disjoint-airport_city_boundaries[]
+| SORT abbrev
+| LIMIT 1
+;
+
+// tag::st_disjoint-airport_city_boundaries-result[]
+abbrev:keyword | airport:text                 | region:text        | city:keyword       | city_location:geo_point
+ACA            | General Juan N Alvarez Int'l | Acapulco de Juárez | Acapulco de Juárez | POINT (-99.8825 16.8636)
+// end::st_disjoint-airport_city_boundaries-result[]
+;
+
 ###############################################
 # Tests for ST_CONTAINS and ST_WITHIN with GEO_SHAPE
 
@@ -422,6 +443,37 @@ 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))
 ;
 
+###############################################
+# Tests for ST_DISJOINT with CARTESIAN_SHAPE
+
+cartesianPolygonDisjointLiteralPolygon
+required_feature: esql.st_disjoint
+
+FROM countries_bbox_web
+| WHERE ST_DISJOINT(shape, TO_CARTESIANSHAPE("POLYGON((3100000 -3400000, 3500000 -3400000, 3500000 -3150000, 3100000 -3150000, 3100000 -3400000))"))
+| SORT id DESC
+| LIMIT 1
+;
+
+id:keyword | name:keyword | shape:cartesian_shape
+ZWE        | Zimbabwe     | BBOX (2809472.180051312, 3681512.6693309383, -1760356.671722378, -2561396.0054164226)
+;
+
+cartesianPolygonDisjointEmptyGeometry
+required_feature: esql.st_disjoint
+
+FROM countries_bbox_web
+| WHERE ST_DISJOINT(shape, TO_CARTESIANSHAPE("LINESTRING()"))
+| STATS count=COUNT()
+;
+
+warning:Line 2:28: evaluation of [TO_CARTESIANSHAPE(\"LINESTRING()\")] failed, treating result as null. Only first 20 failures recorded.
+warning:Line 2:28: java.lang.IllegalArgumentException: Failed to parse WKT: expected number but found: ')'
+
+count:long
+0
+;
+
 ###############################################
 # Tests for ST_CONTAINS and ST_WITHIN with CARTESIAN_SHAPE
 

+ 128 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointCartesianPointDocValuesAndConstantEvaluator.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 SpatialDisjoint}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialDisjointCartesianPointDocValuesAndConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final Component2D rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialDisjointCartesianPointDocValuesAndConstantEvaluator(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(SpatialDisjoint.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(SpatialDisjoint.processCartesianPointDocValuesAndConstant(leftValueVector.getLong(p), rightValue));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialDisjointCartesianPointDocValuesAndConstantEvaluator[" + "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 SpatialDisjointCartesianPointDocValuesAndConstantEvaluator get(DriverContext context) {
+      return new SpatialDisjointCartesianPointDocValuesAndConstantEvaluator(source, leftValue.get(context), rightValue, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialDisjointCartesianPointDocValuesAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 142 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointCartesianPointDocValuesAndSourceEvaluator.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 SpatialDisjoint}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialDisjointCartesianPointDocValuesAndSourceEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final EvalOperator.ExpressionEvaluator rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialDisjointCartesianPointDocValuesAndSourceEvaluator(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(SpatialDisjoint.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(SpatialDisjoint.processCartesianPointDocValuesAndSource(leftValueVector.getLong(p), rightValueVector.getBytesRef(p, rightValueScratch)));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialDisjointCartesianPointDocValuesAndSourceEvaluator[" + "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 SpatialDisjointCartesianPointDocValuesAndSourceEvaluator get(DriverContext context) {
+      return new SpatialDisjointCartesianPointDocValuesAndSourceEvaluator(source, leftValue.get(context), rightValue.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialDisjointCartesianPointDocValuesAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 132 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointCartesianSourceAndConstantEvaluator.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 SpatialDisjoint}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialDisjointCartesianSourceAndConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final Component2D rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialDisjointCartesianSourceAndConstantEvaluator(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(SpatialDisjoint.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(SpatialDisjoint.processCartesianSourceAndConstant(leftValueVector.getBytesRef(p, leftValueScratch), rightValue));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialDisjointCartesianSourceAndConstantEvaluator[" + "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 SpatialDisjointCartesianSourceAndConstantEvaluator get(DriverContext context) {
+      return new SpatialDisjointCartesianSourceAndConstantEvaluator(source, leftValue.get(context), rightValue, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialDisjointCartesianSourceAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 152 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointCartesianSourceAndSourceEvaluator.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 SpatialDisjoint}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialDisjointCartesianSourceAndSourceEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final EvalOperator.ExpressionEvaluator rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialDisjointCartesianSourceAndSourceEvaluator(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(SpatialDisjoint.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(SpatialDisjoint.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 "SpatialDisjointCartesianSourceAndSourceEvaluator[" + "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 SpatialDisjointCartesianSourceAndSourceEvaluator get(DriverContext context) {
+      return new SpatialDisjointCartesianSourceAndSourceEvaluator(source, leftValue.get(context), rightValue.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialDisjointCartesianSourceAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 128 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointGeoPointDocValuesAndConstantEvaluator.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 SpatialDisjoint}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialDisjointGeoPointDocValuesAndConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final Component2D rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialDisjointGeoPointDocValuesAndConstantEvaluator(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(SpatialDisjoint.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(SpatialDisjoint.processGeoPointDocValuesAndConstant(leftValueVector.getLong(p), rightValue));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialDisjointGeoPointDocValuesAndConstantEvaluator[" + "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 SpatialDisjointGeoPointDocValuesAndConstantEvaluator get(DriverContext context) {
+      return new SpatialDisjointGeoPointDocValuesAndConstantEvaluator(source, leftValue.get(context), rightValue, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialDisjointGeoPointDocValuesAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 151 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointGeoPointDocValuesAndSourceEvaluator.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 SpatialDisjoint}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialDisjointGeoPointDocValuesAndSourceEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final EvalOperator.ExpressionEvaluator rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialDisjointGeoPointDocValuesAndSourceEvaluator(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(SpatialDisjoint.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(SpatialDisjoint.processGeoPointDocValuesAndSource(leftValueVector.getLong(p), rightValueVector.getBytesRef(p, rightValueScratch)));
+        } catch (IllegalArgumentException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialDisjointGeoPointDocValuesAndSourceEvaluator[" + "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 SpatialDisjointGeoPointDocValuesAndSourceEvaluator get(DriverContext context) {
+      return new SpatialDisjointGeoPointDocValuesAndSourceEvaluator(source, leftValue.get(context), rightValue.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialDisjointGeoPointDocValuesAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 132 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointGeoSourceAndConstantEvaluator.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 SpatialDisjoint}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialDisjointGeoSourceAndConstantEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final Component2D rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialDisjointGeoSourceAndConstantEvaluator(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(SpatialDisjoint.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(SpatialDisjoint.processGeoSourceAndConstant(leftValueVector.getBytesRef(p, leftValueScratch), rightValue));
+        } catch (IllegalArgumentException | IOException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "SpatialDisjointGeoSourceAndConstantEvaluator[" + "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 SpatialDisjointGeoSourceAndConstantEvaluator get(DriverContext context) {
+      return new SpatialDisjointGeoSourceAndConstantEvaluator(source, leftValue.get(context), rightValue, context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialDisjointGeoSourceAndConstantEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

+ 152 - 0
x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointGeoSourceAndSourceEvaluator.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 SpatialDisjoint}.
+ * This class is generated. Do not edit it.
+ */
+public final class SpatialDisjointGeoSourceAndSourceEvaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator leftValue;
+
+  private final EvalOperator.ExpressionEvaluator rightValue;
+
+  private final DriverContext driverContext;
+
+  public SpatialDisjointGeoSourceAndSourceEvaluator(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(SpatialDisjoint.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(SpatialDisjoint.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 "SpatialDisjointGeoSourceAndSourceEvaluator[" + "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 SpatialDisjointGeoSourceAndSourceEvaluator get(DriverContext context) {
+      return new SpatialDisjointGeoSourceAndSourceEvaluator(source, leftValue.get(context), rightValue.get(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "SpatialDisjointGeoSourceAndSourceEvaluator[" + "leftValue=" + leftValue + ", rightValue=" + rightValue + "]";
+    }
+  }
+}

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

@@ -81,6 +81,7 @@ 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.SpatialDisjoint;
 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;
@@ -189,6 +190,7 @@ public final class EsqlFunctionRegistry extends FunctionRegistry {
             new FunctionDefinition[] {
                 def(SpatialCentroid.class, SpatialCentroid::new, "st_centroid"),
                 def(SpatialContains.class, SpatialContains::new, "st_contains"),
+                def(SpatialDisjoint.class, SpatialDisjoint::new, "st_disjoint"),
                 def(SpatialIntersects.class, SpatialIntersects::new, "st_intersects"),
                 def(SpatialWithin.class, SpatialWithin::new, "st_within"),
                 def(StX.class, StX::new, "st_x"),

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

@@ -112,8 +112,6 @@ public class SpatialContains extends SpatialRelatesFunction {
     @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(

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

@@ -0,0 +1,239 @@
+/*
+ * 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.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_DISJOINT.
+ * 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_DISJOINT and QueryRelation.DISJOINT.
+ */
+public class SpatialDisjoint extends SpatialRelatesFunction {
+    // public for test access with reflection
+    public static final SpatialRelations GEO = new SpatialRelations(
+        ShapeField.QueryRelation.DISJOINT,
+        SpatialCoordinateTypes.GEO,
+        CoordinateEncoder.GEO,
+        new GeoShapeIndexer(Orientation.CCW, "ST_Disjoint")
+    );
+    // public for test access with reflection
+    public static final SpatialRelations CARTESIAN = new SpatialRelations(
+        ShapeField.QueryRelation.DISJOINT,
+        SpatialCoordinateTypes.CARTESIAN,
+        CoordinateEncoder.CARTESIAN,
+        new CartesianShapeIndexer("ST_Disjoint")
+    );
+
+    @FunctionInfo(
+        returnType = { "boolean" },
+        description = "Returns whether the two geometries or geometry columns are disjoint.",
+        examples = @Example(file = "spatial_shapes", tag = "st_disjoint-airport_city_boundaries")
+    )
+    public SpatialDisjoint(
+        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);
+    }
+
+    private SpatialDisjoint(Source source, Expression left, Expression right, boolean leftDocValues, boolean rightDocValues) {
+        super(source, left, right, leftDocValues, rightDocValues);
+    }
+
+    @Override
+    public ShapeField.QueryRelation queryRelation() {
+        return ShapeField.QueryRelation.DISJOINT;
+    }
+
+    @Override
+    public SpatialDisjoint 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 SpatialDisjoint(source(), left(), right(), leftDV, rightDV);
+    }
+
+    @Override
+    protected SpatialDisjoint replaceChildren(Expression newLeft, Expression newRight) {
+        return new SpatialDisjoint(source(), newLeft, newRight, leftDocValues, rightDocValues);
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, SpatialDisjoint::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;
+    }
+
+    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(SpatialDisjointGeoSourceAndSourceEvaluator.Factory::new)
+                );
+                evaluatorMap.put(
+                    SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType),
+                    new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                        SpatialDisjointGeoSourceAndConstantEvaluator.Factory::new
+                    )
+                );
+                if (EsqlDataTypes.isSpatialPoint(spatialType)) {
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSources(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorFactoryWithFields(
+                            SpatialDisjointGeoPointDocValuesAndSourceEvaluator.Factory::new
+                        )
+                    );
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                            SpatialDisjointGeoPointDocValuesAndConstantEvaluator.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(
+                        SpatialDisjointCartesianSourceAndSourceEvaluator.Factory::new
+                    )
+                );
+                evaluatorMap.put(
+                    SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType),
+                    new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                        SpatialDisjointCartesianSourceAndConstantEvaluator.Factory::new
+                    )
+                );
+                if (EsqlDataTypes.isSpatialPoint(spatialType)) {
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSources(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorFactoryWithFields(
+                            SpatialDisjointCartesianPointDocValuesAndSourceEvaluator.Factory::new
+                        )
+                    );
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                            SpatialDisjointCartesianPointDocValuesAndConstantEvaluator.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);
+    }
+}

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

@@ -66,8 +66,6 @@ public class SpatialIntersects extends SpatialRelatesFunction {
     @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(

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

@@ -116,10 +116,14 @@ public abstract class SpatialRelatesFunction extends BinaryScalarFunction
         if (resolution.unresolved()) {
             return resolution;
         }
-        crsType = SpatialCrsType.fromDataType(spatialExpression.dataType());
+        setCrsType(spatialExpression.dataType());
         return TypeResolution.TYPE_RESOLVED;
     }
 
+    protected void setCrsType(DataType dataType) {
+        crsType = SpatialCrsType.fromDataType(dataType);
+    }
+
     public static TypeResolution isSameSpatialType(
         DataType spatialDataType,
         Expression expression,

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

@@ -67,8 +67,6 @@ public class SpatialWithin extends SpatialRelatesFunction implements SurrogateEx
     @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(

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

@@ -103,6 +103,7 @@ 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.SpatialDisjoint;
 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;
@@ -398,6 +399,7 @@ public final class PlanNamedTypes {
             of(ScalarFunction.class, StartsWith.class, PlanNamedTypes::writeStartsWith, PlanNamedTypes::readStartsWith),
             of(ScalarFunction.class, EndsWith.class, PlanNamedTypes::writeEndsWith, PlanNamedTypes::readEndsWith),
             of(ScalarFunction.class, SpatialIntersects.class, PlanNamedTypes::writeSpatialRelatesFunction, PlanNamedTypes::readIntersects),
+            of(ScalarFunction.class, SpatialDisjoint.class, PlanNamedTypes::writeSpatialRelatesFunction, PlanNamedTypes::readDisjoint),
             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),
@@ -1504,6 +1506,10 @@ public final class PlanNamedTypes {
         return new SpatialIntersects(Source.EMPTY, in.readExpression(), in.readExpression());
     }
 
+    static SpatialDisjoint readDisjoint(PlanStreamInput in) throws IOException {
+        return new SpatialDisjoint(Source.EMPTY, in.readExpression(), in.readExpression());
+    }
+
     static SpatialContains readContains(PlanStreamInput in) throws IOException {
         return new SpatialContains(Source.EMPTY, in.readExpression(), in.readExpression());
     }

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

@@ -80,6 +80,11 @@ public class EsqlFeatures implements FeatureSpecification {
      */
     private static final NodeFeature ST_CONTAINS_WITHIN = new NodeFeature("esql.st_contains_within");
 
+    /**
+     * Support for spatial aggregation {@code ST_DISJOINT}. Done in #107007.
+     */
+    private static final NodeFeature ST_DISJOINT = new NodeFeature("esql.st_disjoint");
+
     /**
      * The introduction of the {@code VALUES} agg.
      */
@@ -108,7 +113,8 @@ public class EsqlFeatures implements FeatureSpecification {
             SPATIAL_SHAPES,
             ST_CENTROID,
             ST_INTERSECTS,
-            ST_CONTAINS_WITHIN
+            ST_CONTAINS_WITHIN,
+            ST_DISJOINT
         );
     }
 

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

@@ -12,6 +12,8 @@ import org.apache.lucene.document.XYDocValuesField;
 import org.apache.lucene.document.XYPointField;
 import org.apache.lucene.document.XYShape;
 import org.apache.lucene.geo.XYGeometry;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.ConstantScoreQuery;
 import org.apache.lucene.search.IndexOrDocValuesQuery;
 import org.apache.lucene.search.MatchNoDocsQuery;
@@ -24,6 +26,7 @@ import org.elasticsearch.geometry.ShapeType;
 import org.elasticsearch.index.IndexVersions;
 import org.elasticsearch.index.mapper.GeoShapeQueryable;
 import org.elasticsearch.index.mapper.MappedFieldType;
+import org.elasticsearch.index.query.ExistsQueryBuilder;
 import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.index.query.QueryShardException;
 import org.elasticsearch.index.query.SearchExecutionContext;
@@ -221,7 +224,16 @@ public class SpatialRelatesQuery extends Query {
         }
 
         /**
-         * This code is based on the ShapeQueryPointProcessor.shapeQuery() method
+         * This code is based on the ShapeQueryPointProcessor.shapeQuery() method, with additional support for two special cases:
+         * <ul>
+         *     <li>
+         *         DISJOINT queries (using {@code EXISTS && !INTERSECTS}, similar to {@code LegacyGeoShapeQueryProcessor.geoShapeQuery()})
+         *     </li>
+         *     <li>
+         *         CONTAINS queries (if the shape is a point, INTERSECTS is used, otherwise a MatchNoDocsQuery is built,
+         *         similar to {@code LatLonPoint.makeContainsGeometryQuery()})
+         *     </li>
+         * </ul>
          */
         private static org.apache.lucene.search.Query pointShapeQuery(
             Geometry geometry,
@@ -231,20 +243,28 @@ public class SpatialRelatesQuery extends Query {
         ) {
             final boolean hasDocValues = context.getFieldType(fieldName).hasDocValues();
             if (geometry == null || geometry.isEmpty()) {
-                // Should never be null, but can be an empty geometry
-                return new MatchNoDocsQuery();
+                throw new QueryShardException(context, "Invalid/empty geometry");
             }
             if (geometry.type() != ShapeType.POINT && relation == ShapeField.QueryRelation.CONTAINS) {
-                // A point field can never contain a non-point geometry
-                return new MatchNoDocsQuery();
+                return new MatchNoDocsQuery("A point field can never contain a non-point geometry");
             }
             final XYGeometry[] luceneGeometries = LuceneGeometriesUtils.toXYGeometry(geometry, t -> {});
-            org.apache.lucene.search.Query query = XYPointField.newGeometryQuery(fieldName, luceneGeometries);
+            org.apache.lucene.search.Query intersects = XYPointField.newGeometryQuery(fieldName, luceneGeometries);
+            if (relation == ShapeField.QueryRelation.DISJOINT) {
+                // XYPointField does not support DISJOINT queries, so we build one as EXISTS && !INTERSECTS
+                BooleanQuery.Builder bool = new BooleanQuery.Builder();
+                org.apache.lucene.search.Query exists = ExistsQueryBuilder.newFilter(context, fieldName, false);
+                bool.add(exists, BooleanClause.Occur.MUST);
+                bool.add(intersects, BooleanClause.Occur.MUST_NOT);
+                return bool.build();
+            }
+
+            // Point-Intersects works for all cases except CONTAINS(shape) and DISJOINT, which are handled separately above
             if (hasDocValues) {
                 final org.apache.lucene.search.Query queryDocValues = XYDocValuesField.newSlowGeometryQuery(fieldName, luceneGeometries);
-                query = new IndexOrDocValuesQuery(query, queryDocValues);
+                intersects = new IndexOrDocValuesQuery(intersects, queryDocValues);
             }
-            return query;
+            return intersects;
         }
 
         /**
@@ -262,8 +282,7 @@ public class SpatialRelatesQuery extends Query {
                 throw new QueryShardException(context, relation + " query relation not supported for Field [" + fieldName + "].");
             }
             if (geometry == null || geometry.isEmpty()) {
-                // Should never be null, but can be an empty geometry
-                return new MatchNoDocsQuery();
+                throw new QueryShardException(context, "Invalid/empty geometry");
             }
             final XYGeometry[] luceneGeometries;
             try {

+ 46 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialDisjointTests.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_disjoint")
+public class SpatialDisjointTests extends SpatialRelatesFunctionTestCase {
+    public SpatialDisjointTests(@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), SpatialDisjointTests::typeErrorMessage)
+        );
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new SpatialDisjoint(source, args.get(0), args.get(1));
+    }
+}

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

@@ -43,6 +43,7 @@ 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.SpatialDisjoint;
 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;
@@ -2933,6 +2934,7 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
         String function() {
             return switch (relation) {
                 case INTERSECTS -> "ST_INTERSECTS";
+                case DISJOINT -> "ST_DISJOINT";
                 case WITHIN -> "ST_WITHIN";
                 case CONTAINS -> "ST_CONTAINS";
                 default -> throw new IllegalArgumentException("Unsupported relation: " + relation);
@@ -2942,6 +2944,7 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
         Class<? extends SpatialRelatesFunction> functionClass() {
             return switch (relation) {
                 case INTERSECTS -> SpatialIntersects.class;
+                case DISJOINT -> SpatialDisjoint.class;
                 case WITHIN -> literalRight ? SpatialWithin.class : SpatialContains.class;
                 case CONTAINS -> literalRight ? SpatialContains.class : SpatialWithin.class;
                 default -> throw new IllegalArgumentException("Unsupported relation: " + relation);
@@ -2975,12 +2978,16 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
         TestSpatialRelation[] tests = new TestSpatialRelation[] {
             new TestSpatialRelation(ShapeRelation.INTERSECTS, airports, true, true),
             new TestSpatialRelation(ShapeRelation.INTERSECTS, airports, false, true),
+            new TestSpatialRelation(ShapeRelation.DISJOINT, airports, true, true),
+            new TestSpatialRelation(ShapeRelation.DISJOINT, 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.DISJOINT, airportsWeb, true, true),
+            new TestSpatialRelation(ShapeRelation.DISJOINT, airportsWeb, false, true),
             new TestSpatialRelation(ShapeRelation.WITHIN, airportsWeb, true, true),
             new TestSpatialRelation(ShapeRelation.WITHIN, airportsWeb, false, true),
             new TestSpatialRelation(ShapeRelation.CONTAINS, airportsWeb, true, true),
@@ -3027,10 +3034,16 @@ public class PhysicalPlanOptimizerTests extends ESTestCase {
         TestSpatialRelation[] tests = new TestSpatialRelation[] {
             new TestSpatialRelation(ShapeRelation.INTERSECTS, airports, true, true),
             new TestSpatialRelation(ShapeRelation.INTERSECTS, airports, false, true),
+            new TestSpatialRelation(ShapeRelation.DISJOINT, airports, true, true),
+            new TestSpatialRelation(ShapeRelation.DISJOINT, 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.DISJOINT, airportsWeb, true, true),
+            new TestSpatialRelation(ShapeRelation.DISJOINT, airportsWeb, false, true),
             new TestSpatialRelation(ShapeRelation.WITHIN, airportsWeb, true, true),
             new TestSpatialRelation(ShapeRelation.WITHIN, airportsWeb, false, true),
             new TestSpatialRelation(ShapeRelation.CONTAINS, airportsWeb, true, true),