소스 검색

Support ST_INTERSECTS between two geometry columns (#104907)

* Support ST_INTERSECTS between geometry column and other geometry or string

* Pushdown to lucene for ST_INTERSECTS on GEO_POINT

* Get geo_shape working in ST_INTERSECTS bypassing SingleValueQuery

* Initial work to support cartesian shape queries in ESQL

* Fixed CSV tests for combined ST_INTERSECTS and ST_CENTROID

* Fixed bug in point-in-shape query for CARTESIAN_POINT

* Added unit tests for SpatialIntersects and fixed a few bugs found

* Added comments to public ShapeQueryBuilder class

* Move calls to random() later to avoid security exception

* Refined type checking support in ST_INTERSECTS

Improved the combinations supported as preparation for removing the uly try/catch way of detecting the difference between WKT and WKB in some code.

* Fixed bugs in incorrect use of doc-values in parameter type matching

Also made a few reminfments, including removing one try/catch approach to differentiating between WKT and WKB.

* Removed second place where we used try/catch to differentiate WKT from WKB

This was a workaround for a mistake in the planning, where we incorrectly mapped incoming types to the wrong FieldEvaluators. We fixed that mistake in an earlier commit.

* Fixed flaky tests were GEO was treated as CARTSIAN

We assumed if the incoming types were constants, they had no CRS, even when they did, which was wrong. For shapes crossing the dateline this lead to different (incorrect) behaviour.

* Fixed a flaky test by removing some point==point optimizations

* Moved spatial intersects to 'spatial' package

When we developed the ST_CENTROID work, this was requested, so let's do it here too.

* Use normal switch on enums

* Cleanup some static utility methods

Now all code paths that can convert a constant string to a geometry use the same code.

* Fixed bugs with non-quantized coordinates, and cleaned up code a little

* Fixed failing test after change to evaluator class names

* Refactored SpatialRelatesFunction into three files, and made evaluatorRules static

This was a general cleanup, making the code more organized, but did also achieve static evaluator rules so we don't re-created these on every query parsing.

* Fixed compile error after rebase

* Removed ConstantAndConstant support, using fold() correctly instead

* better error on circles

* Make sure compound predicates are supported in use-doc-values pushdown

* Testing ENRICH with ST_INTERSECTS

This required adding new data for an ENRICH index, and this data could be tested with a few other related tests, which were also added.

* Added missing mixed-cluster rules for testing only with 8.14

* Fixed some mixed-cluster issues where we failed to mark test for only 8.14

Also added an interesting polygon-polygon intersection case from real data.

* Fix flaky test where cartesian polygons were generated from geo

* Remove support for string literals in ST_INTERSECTS

* Fix failing tests after removing string support

* Removed unused code from previous string literal support (WKT parsing)

* Support case where both fields are points and doc-values

If we have an ST_INTERSECTS and an ST_CENTROID, the centroid asks to load the points as doc-values, and the ST_INTERSECTS needs to therefor support two doc-values points.

* Disallow more than one field from doc-values for ST_INTERSECTS

* Remove unused evaluator classes

* Add tests for multiple doc-values if not in same intersects

* Fix errors after rebase on main

* Fixed bug in missing support for spatial function expressions in EVAL

When a spatial aggregate expects doc-values, this was not being communicated to spatial functions in EVAL, only in WHERE.

* Reduce flaky tests when reading directly from enrich source indices

The test framework does not expect enrich source indices to be used directly in queries, leading to duplicated results on multi-node clusters, so we edit the queries to be less sensitive to this case.

* Fixed failing test

* Code style

* Fixed test file name and added function name annotation

* Added documentation for st_intersects

* Fixed failing show functions test

* Code review changes, notably simplifying the type resolution

* Fixed broken docs link
Craig Taverner 1 년 전
부모
커밋
e14dd54ae9
34개의 변경된 파일3877개의 추가작업 그리고 102개의 파일을 삭제
  1. 6 0
      docs/changelog/104907.yaml
  2. 1 0
      docs/reference/esql/functions/signature/st_intersects.svg
  3. 2 0
      docs/reference/esql/functions/spatial-functions.asciidoc
  4. 40 0
      docs/reference/esql/functions/st_intersects.asciidoc
  5. 12 0
      docs/reference/esql/functions/types/st_intersects.asciidoc
  6. 27 5
      x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java
  7. 8 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/airports_mp.csv
  8. 41 1
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/enrich-IT_tests_only.csv-spec
  9. 3 1
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/meta.csv-spec
  10. 198 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec
  11. 162 2
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial_shapes.csv-spec
  12. 128 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersectsCartesianPointDocValuesAndConstantEvaluator.java
  13. 142 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersectsCartesianPointDocValuesAndSourceEvaluator.java
  14. 132 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersectsCartesianSourceAndConstantEvaluator.java
  15. 152 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersectsCartesianSourceAndSourceEvaluator.java
  16. 128 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersectsGeoPointDocValuesAndConstantEvaluator.java
  17. 151 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersectsGeoPointDocValuesAndSourceEvaluator.java
  18. 132 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersectsGeoSourceAndConstantEvaluator.java
  19. 152 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersectsGeoSourceAndSourceEvaluator.java
  20. 16 1
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/EsqlTypeResolutions.java
  21. 6 3
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  22. 212 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialEvaluatorFactory.java
  23. 226 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersects.java
  24. 297 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialRelatesFunction.java
  25. 105 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialRelatesUtils.java
  26. 13 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java
  27. 56 2
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizer.java
  28. 66 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlExpressionTranslators.java
  29. 287 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/SpatialRelatesQuery.java
  30. 8 4
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypes.java
  31. 40 8
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java
  32. 213 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/SpatialIntersectsTests.java
  33. 703 75
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java
  34. 12 0
      x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/util/SpatialCoordinateTypes.java

+ 6 - 0
docs/changelog/104907.yaml

@@ -0,0 +1,6 @@
+pr: 104907
+summary: Support ST_INTERSECTS between geometry column and other geometry or string
+area: "ES|QL"
+type: enhancement
+issues:
+- 104874

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

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="492" height="46" viewbox="0 0 492 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 31h5m176 0h10m32 0h10m80 0h10m32 0h10m80 0h10m32 0h5"/><rect class="s" x="5" y="5" width="176" height="36"/><text class="k" x="15" y="31">ST_INTERSECTS</text><rect class="s" x="191" y="5" width="32" height="36" rx="7"/><text class="syn" x="201" y="31">(</text><rect class="s" x="233" y="5" width="80" height="36" rx="7"/><text class="k" x="243" y="31">geomA</text><rect class="s" x="323" y="5" width="32" height="36" rx="7"/><text class="syn" x="333" y="31">,</text><rect class="s" x="365" y="5" width="80" height="36" rx="7"/><text class="k" x="375" y="31">geomB</text><rect class="s" x="455" y="5" width="32" height="36" rx="7"/><text class="syn" x="465" y="31">)</text></svg>

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

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

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

@@ -0,0 +1,40 @@
+[discrete]
+[[esql-st_intersects]]
+=== `ST_INTERSECTS`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_intersects.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.
+
+*Description*
+
+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).
+In mathematical terms: ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅
+
+*Supported types*
+
+include::types/st_intersects.asciidoc[]
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/spatial.csv-spec[tag=st_intersects-airports]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/spatial.csv-spec[tag=st_intersects-airports-results]
+|===

+ 12 - 0
docs/reference/esql/functions/types/st_intersects.asciidoc

@@ -0,0 +1,12 @@
+[%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
+|===

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

@@ -61,6 +61,7 @@ public class CsvTestsDataLoader {
     private static final TestsDataset HEIGHTS = new TestsDataset("heights", "mapping-heights.json", "heights.csv");
     private static final TestsDataset DECADES = new TestsDataset("decades", "mapping-decades.json", "decades.csv");
     private static final TestsDataset AIRPORTS = new TestsDataset("airports", "mapping-airports.json", "airports.csv");
+    private static final TestsDataset AIRPORTS_MP = new TestsDataset("airports_mp", "mapping-airports.json", "airports_mp.csv");
     private static final TestsDataset AIRPORTS_WEB = new TestsDataset("airports_web", "mapping-airports_web.json", "airports_web.csv");
     private static final TestsDataset COUNTRIES_BBOX = new TestsDataset(
         "countries_bbox",
@@ -91,6 +92,7 @@ public class CsvTestsDataLoader {
         Map.entry(HEIGHTS.indexName, HEIGHTS),
         Map.entry(DECADES.indexName, DECADES),
         Map.entry(AIRPORTS.indexName, AIRPORTS),
+        Map.entry(AIRPORTS_MP.indexName, AIRPORTS_MP),
         Map.entry(AIRPORTS_WEB.indexName, AIRPORTS_WEB),
         Map.entry(COUNTRIES_BBOX.indexName, COUNTRIES_BBOX),
         Map.entry(COUNTRIES_BBOX_WEB.indexName, COUNTRIES_BBOX_WEB),
@@ -281,6 +283,7 @@ public class CsvTestsDataLoader {
         CheckedBiFunction<XContent, InputStream, XContentParser, IOException> p,
         Logger logger
     ) throws IOException {
+        ArrayList<String> failures = new ArrayList<>();
         StringBuilder builder = new StringBuilder();
         try (BufferedReader reader = org.elasticsearch.xpack.ql.TestUtils.reader(resource)) {
             String line;
@@ -390,13 +393,19 @@ public class CsvTestsDataLoader {
                 }
                 lineNumber++;
                 if (builder.length() > BULK_DATA_SIZE) {
-                    sendBulkRequest(indexName, builder, client, logger);
+                    sendBulkRequest(indexName, builder, client, logger, failures);
                     builder.setLength(0);
                 }
             }
         }
         if (builder.isEmpty() == false) {
-            sendBulkRequest(indexName, builder, client, logger);
+            sendBulkRequest(indexName, builder, client, logger, failures);
+        }
+        if (failures.isEmpty() == false) {
+            for (String failure : failures) {
+                logger.error(failure);
+            }
+            throw new IOException("Data loading failed with " + failures.size() + " errors: " + failures.get(0));
         }
     }
 
@@ -405,7 +414,8 @@ public class CsvTestsDataLoader {
         return isQuoted ? value : "\"" + value + "\"";
     }
 
-    private static void sendBulkRequest(String indexName, StringBuilder builder, RestClient client, Logger logger) throws IOException {
+    private static void sendBulkRequest(String indexName, StringBuilder builder, RestClient client, Logger logger, List<String> failures)
+        throws IOException {
         // The indexName is optional for a bulk request, but we use it for routing in MultiClusterSpecIT.
         builder.append("\n");
         logger.debug("Sending bulk request of [{}] bytes for [{}]", builder.length(), indexName);
@@ -422,14 +432,26 @@ public class CsvTestsDataLoader {
                 if (Boolean.FALSE.equals(errors)) {
                     logger.info("Data loading of [{}] bytes into [{}] OK", builder.length(), indexName);
                 } else {
-                    throw new IOException("Data loading of [" + indexName + "] failed with errors: " + errors);
+                    addError(failures, indexName, builder, "errors: " + result);
                 }
             }
         } else {
-            throw new IOException("Data loading of [" + indexName + "] failed with status: " + response.getStatusLine());
+            addError(failures, indexName, builder, "status: " + response.getStatusLine());
         }
     }
 
+    private static void addError(List<String> failures, String indexName, StringBuilder builder, String message) {
+        failures.add(
+            format(
+                "Data loading of [{}] bytes into [{}] failed with {}: Data [{}...]",
+                builder.length(),
+                indexName,
+                message,
+                builder.substring(0, 100)
+            )
+        );
+    }
+
     private static void forceMerge(RestClient client, Set<String> indices, Logger logger) throws IOException {
         String pattern = String.join(",", indices);
 

+ 8 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/airports_mp.csv

@@ -0,0 +1,8 @@
+abbrev:keyword,name:text,                                   scalerank:integer,type:keyword,     location:geo_point,                         country:keyword,                city:keyword,                   city_location:geo_point
+XXX,           Atlantis Int'l,                              1,                mid,              POINT(0 0),                                 Atlantis,                       Atlantis,                       POINT(0 0)
+LUH,           Sahnewal,                                    9,                small,            POINT(75.9570722403652 30.8503598561702),   India,                          Ludhiāna,                       POINT(75.85 30.91)
+SSE,           Solapur,                                     9,                mid,              POINT(75.9330597710755 17.625415183635),    India,                          Solāpur,                        POINT(75.92 17.68)
+IXR,           Birsa Munda,                                 9,                mid,              POINT(85.3235970368767 23.3177245989962),   India,                          Rānchi,                         POINT(85.33 23.36)
+AWZ,           Ahwaz,                                       9,                mid,              POINT(48.7471065435931 31.3431585560757),   Iran,                           Ahvāz,                          POINT(48.6692 31.3203)
+GWL,           Gwalior,                                     9,                [mid,military],   POINT(78.2172186546348 26.285487697937),    India,                          Gwalior,                        POINT(78.178 26.2215)
+HOD,           Hodeidah Int'l,                              9,                mid,              POINT(42.97109630194 14.7552534413725),     Yemen,                          Al Ḩudaydah,                    POINT(42.9511 14.8022)

+ 41 - 1
x-pack/plugin/esql/qa/testFixtures/src/main/resources/enrich-IT_tests_only.csv-spec

@@ -152,6 +152,7 @@ a:keyword   | a_lang:keyword
 ["1", "2"]  | ["English", "French"] 
 ;
 
+
 enrichCidr#[skip:-8.13.99, reason:enrich for cidr added in 8.14.0]
 FROM sample_data
 | ENRICH client_cidr_policy ON client_ip WITH env
@@ -170,6 +171,7 @@ client_ip:ip | count_env:i | max_env:keyword
 172.21.3.15  | 2           | Production
 ;
 
+
 enrichCidr2#[skip:-8.99.99, reason:ip_range support not added yet]
 FROM sample_data
 | ENRICH client_cidr_policy ON client_ip WITH env, client_cidr
@@ -187,6 +189,7 @@ client_ip:ip | env:keyword               | client_cidr:ip_range
 172.21.2.162 | [Development, QA]         | 172.21.2.0/24
 ;
 
+
 enrichAgesStatsYear#[skip:-8.13.99, reason:ENRICH extended in 8.14.0]
 FROM employees
 | WHERE birth_date > "1960-01-01"
@@ -207,6 +210,7 @@ birth_year:long | age_group:keyword | count:long
 1960            | Senior            | 8
 ;
 
+
 enrichAgesStatsAgeGroup#[skip:-8.13.99, reason:ENRICH extended in 8.14.0]
 FROM employees
 | WHERE birth_date IS NOT NULL
@@ -221,6 +225,7 @@ count:long | age_group:keyword
 12         | Middle-aged
 ;
 
+
 enrichHeightsStats#[skip:-8.13.99, reason:ENRICH extended in 8.14.0]
 FROM employees
 | ENRICH heights_policy ON height WITH height_group = description
@@ -237,6 +242,7 @@ Tall           | 1.8        | 1.99       | 25
 Very Tall      | 2.0        | 2.1        | 20
 ;
 
+
 enrichDecadesStats#[skip:-8.13.99, reason:ENRICH extended in 8.14.0]
 FROM employees
 | ENRICH decades_policy ON birth_date WITH birth_decade = decade, birth_description = description
@@ -255,6 +261,7 @@ null              | 1980          | null                | Radical Eighties   | 4
 1950              | 1980          | Nifty Fifties       | Radical Eighties   | 34
 ;
 
+
 spatialEnrichmentKeywordMatch#[skip:-8.13.99, reason:ENRICH extended in 8.14.0]
 FROM airports
 | WHERE abbrev == "CPH"
@@ -267,6 +274,7 @@ abbrev:keyword  |  city:keyword  |  city_location:geo_point |  country:keyword
 CPH             |  Copenhagen    |  POINT(12.5683 55.6761)  |  Denmark          |  POINT(12.6493508684508 55.6285017221528) |  Copenhagen  |  Copenhagen    |  Københavns Kommune  |  265
 ;
 
+
 spatialEnrichmentGeoMatch#[skip:-8.13.99, reason:ENRICH extended in 8.14.0]
 FROM airports
 | WHERE abbrev == "CPH"
@@ -279,6 +287,7 @@ abbrev:keyword  |  city:keyword  |  city_location:geo_point |  country:keyword
 CPH             |  Copenhagen    |  POINT(12.5683 55.6761)  |  Denmark          |  POINT(12.6493508684508 55.6285017221528) |  Copenhagen  |  Copenhagen    |  Københavns Kommune  |  265
 ;
 
+
 spatialEnrichmentGeoMatchStats#[skip:-8.13.99, reason:ENRICH extended in 8.14.0]
 required_feature: esql.mv_warn
 
@@ -290,7 +299,38 @@ FROM airports
 warning:Line 3:30: evaluation of [LENGTH(TO_STRING(city_boundary))] failed, treating result as null. Only first 20 failures recorded.
 warning:Line 3:30: java.lang.IllegalArgumentException: single-value function encountered multi-value
 
-
 city_centroid:geo_point    |  count:long  |  min_wkt:integer  |  max_wkt:integer
 POINT(1.396561 24.127649)  |  872         |  88               |  1044
 ;
+
+
+spatialEnrichmentKeywordMatchAndSpatialPredicate#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports
+| ENRICH city_names ON city WITH airport, region, city_boundary
+| MV_EXPAND city_boundary
+| EVAL airport_in_city = ST_INTERSECTS(location, city_boundary)
+| STATS count=COUNT(*) BY airport_in_city
+| SORT count ASC
+;
+
+count:long  |  airport_in_city:boolean
+114         |  null
+396         |  true
+455         |  false
+;
+
+
+spatialEnrichmentKeywordMatchAndSpatialAggregation#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports
+| ENRICH city_names ON city WITH airport, region, city_boundary
+| MV_EXPAND city_boundary
+| EVAL airport_in_city = ST_INTERSECTS(location, city_boundary)
+| STATS count=COUNT(*), centroid=ST_CENTROID(location) BY airport_in_city
+| SORT count ASC
+;
+
+count:long  |  centroid:geo_point            |  airport_in_city:boolean
+114         |  POINT (-24.750062 31.575549)  |  null
+396         |  POINT (-2.534797 20.667712)   |  true
+455         |  POINT (3.090752 27.676442)    |  false
+;

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

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

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

@@ -320,6 +320,173 @@ centroid:geo_point                            | count:long
 POINT(83.16847535921261 28.79002037679311)    | 40
 ;
 
+centroidFromAirportsAfterKeywordPredicateCountryUK#[skip:-8.12.99, reason:st_centroid added in 8.13]
+FROM airports
+| WHERE country == "United Kingdom"
+| STATS centroid=ST_CENTROID(location), count=COUNT()
+;
+
+centroid:geo_point                              | count:long
+POINT (-2.597342072712148 54.33551226578214)    | 17
+;
+
+centroidFromAirportsAfterIntersectsPredicateCountryUK#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports
+| WHERE ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON((1.2305 60.8449, -1.582 61.6899, -10.7227 58.4017, -7.1191 55.3291, -7.9102 54.2139, -5.4492 54.0078, -5.2734 52.3756, -7.8223 49.6676, -5.0977 49.2678, 0.9668 50.5134, 2.5488 52.1065, 2.6367 54.0078, -0.9668 56.4625, 1.2305 60.8449))"))
+| STATS centroid=ST_CENTROID(location), count=COUNT()
+;
+
+centroid:geo_point                              | count:long
+POINT (-2.597342072712148 54.33551226578214)    | 17
+;
+
+intersectsAfterCentroidFromAirportsAfterKeywordPredicateCountryUK#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports
+| WHERE country == "United Kingdom"
+| STATS centroid = ST_CENTROID(location), count=COUNT()
+| EVAL centroid_in_uk = ST_INTERSECTS(centroid, TO_GEOSHAPE("POLYGON((1.2305 60.8449, -1.582 61.6899, -10.7227 58.4017, -7.1191 55.3291, -7.9102 54.2139, -5.4492 54.0078, -5.2734 52.3756, -7.8223 49.6676, -5.0977 49.2678, 0.9668 50.5134, 2.5488 52.1065, 2.6367 54.0078, -0.9668 56.4625, 1.2305 60.8449))"))
+| EVAL centroid_in_iceland = ST_INTERSECTS(centroid, TO_GEOSHAPE("POLYGON ((-25.4883 65.5312, -23.4668 66.7746, -18.4131 67.4749, -13.0957 66.2669, -12.3926 64.4159, -20.1270 62.7346, -24.7852 63.3718, -25.4883 65.5312))"))
+| KEEP centroid, count, centroid_in_uk, centroid_in_iceland
+;
+
+centroid:geo_point                              | count:long | centroid_in_uk:boolean | centroid_in_iceland:boolean
+POINT (-2.597342072712148 54.33551226578214)    | 17         | true                   | false
+;
+
+centroidFromAirportsAfterIntersectsEvalExpression#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports
+| EVAL in_uk = ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON((1.2305 60.8449, -1.582 61.6899, -10.7227 58.4017, -7.1191 55.3291, -7.9102 54.2139, -5.4492 54.0078, -5.2734 52.3756, -7.8223 49.6676, -5.0977 49.2678, 0.9668 50.5134, 2.5488 52.1065, 2.6367 54.0078, -0.9668 56.4625, 1.2305 60.8449))"))
+| EVAL in_iceland = ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON ((-25.4883 65.5312, -23.4668 66.7746, -18.4131 67.4749, -13.0957 66.2669, -12.3926 64.4159, -20.1270 62.7346, -24.7852 63.3718, -25.4883 65.5312))"))
+| STATS centroid = ST_CENTROID(location), count=COUNT() BY in_uk, in_iceland
+| SORT count ASC
+;
+
+centroid:geo_point                             |  count:long  |  in_uk:boolean  |  in_iceland:boolean
+POINT (-21.946634463965893 64.13187285885215)  |  1           |  false          |  true
+POINT (-2.597342072712148 54.33551226578214)   |  17          |  true           |  false
+POINT (0.04453958108176276 23.74658354606057)  |  873         |  false          |  false
+;
+
+centroidFromAirportsAfterIntersectsPredicate#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports
+| WHERE ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))"))
+| STATS centroid=ST_CENTROID(location), count=COUNT()
+;
+
+centroid:geo_point                            | count:long
+POINT (42.97109629958868 14.7552534006536)    | 1
+;
+
+centroidFromAirportsAfterIntersectsCompoundPredicate#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports
+| WHERE scalerank == 9 AND ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))")) AND country == "Yemen"
+| STATS centroid=ST_CENTROID(location), count=COUNT()
+;
+
+centroid:geo_point                            | count:long
+POINT (42.97109629958868 14.7552534006536)    | 1
+;
+
+pointIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports
+| WHERE ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))"))
+;
+
+abbrev:keyword  |  city:keyword    |  city_location:geo_point |  country:keyword  |  location:geo_point                         |  name:text                    |  scalerank:i  |  type:k
+HOD             |  Al Ḩudaydah     |  POINT(42.9511 14.8022)  |  Yemen            |  POINT(42.97109630194 14.7552534413725)     |  Hodeidah Int'l               |  9            |  mid
+;
+
+pointIntersectsLiteralPolygonReversed#[skip:-8.13.99, reason:st_intersects added in 8.14]
+// tag::st_intersects-airports[]
+FROM airports
+| WHERE ST_INTERSECTS(TO_GEOSHAPE("POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))"), location)
+// end::st_intersects-airports[]
+;
+
+// tag::st_intersects-airports-results[]
+abbrev:keyword  |  city:keyword    |  city_location:geo_point |  country:keyword  |  location:geo_point                         |  name:text                    |  scalerank:i  |  type:k
+HOD             |  Al Ḩudaydah     |  POINT(42.9511 14.8022)  |  Yemen            |  POINT(42.97109630194 14.7552534413725)     |  Hodeidah Int'l               |  9            |  mid
+// end::st_intersects-airports-results[]
+;
+
+literalPointIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_GEOPOINT(wkt)
+| WHERE ST_INTERSECTS(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)
+;
+
+literalPointIntersectsLiteralPolygonReversed#[skip:-8.13.99, reason:st_intersects added in 8.14]
+ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_GEOPOINT(wkt)
+| WHERE ST_INTERSECTS(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)
+;
+
+literalPointIntersectsLiteralPolygonOneRow#[skip:-8.13.99, reason:st_intersects added in 8.14]
+ROW intersects = ST_INTERSECTS(TO_GEOPOINT("POINT(0 0)"), TO_GEOSHAPE("POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"))
+;
+
+intersects:boolean
+true
+;
+
+cityInCityBoundary#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airport_city_boundaries
+| EVAL in_city = ST_INTERSECTS(city_location, city_boundary)
+| STATS count=COUNT(*) BY in_city
+| SORT count ASC
+| EVAL cardinality = CASE(count < 10, "very few", count < 100, "few", "many")
+| KEEP cardinality, in_city
+;
+
+cardinality:k  |  in_city:boolean
+"few"          |  false
+"many"         |  true
+;
+
+cityNotInCityBoundaryBiggest#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airport_city_boundaries
+| WHERE NOT ST_INTERSECTS(city_location, city_boundary)
+| EVAL boundary_wkt_length = LENGTH(TO_STRING(city_boundary))
+| SORT boundary_wkt_length DESC
+| KEEP abbrev, airport, city, city_location, boundary_wkt_length, city_boundary
+| LIMIT 1
+;
+
+abbrev:keyword  |  airport:text         | city:keyword  |  city_location:geo_point |  boundary_wkt_length:integer | city_boundary:geo_shape
+SYX             |  Sanya Phoenix Int'l  |  Sanya        |  POINT(109.5036 18.2533) |  598                         | POLYGON((109.1802 18.4609, 109.2304 18.4483, 109.2311 18.4261, 109.2696 18.411, 109.2602 18.3581, 109.2273 18.348, 109.2286 18.2638, 109.2842 18.2665, 109.3518 18.2166, 109.4508 18.1936, 109.4895 18.2281, 109.5137 18.2283, 109.4914 18.2781, 109.5041 18.2948, 109.4809 18.3034, 109.5029 18.3422, 109.5249 18.3375, 109.4993 18.3632, 109.535 18.4007, 109.5104 18.4374, 109.5231 18.4474, 109.5321 18.53, 109.4992 18.5568, 109.4192 18.5646, 109.4029 18.6302, 109.3286 18.5772, 109.309 18.5191, 109.2913 18.5141, 109.2434 18.5607, 109.2022 18.5572, 109.1815 18.5163, 109.1908 18.4711, 109.1802 18.4609)))
+;
+
+airportCityLocationPointIntersection#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports_mp
+| WHERE ST_INTERSECTS(location, city_location)
+;
+
+abbrev:keyword  |  city:keyword  |  city_location:geo_point |  country:keyword  |  location:geo_point  |  name:text       |  scalerank:i  |  type:k
+XXX             |  Atlantis      |  POINT(0 0)              |  Atlantis         |  POINT(0 0)          |  Atlantis Int'l  |  1            |  mid
+;
+
+airportCityLocationPointIntersectionCentroid#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports_mp
+| WHERE ST_INTERSECTS(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 (0 0)         | POINT (0 0)              |  1
+;
+
 geoPointEquals#[skip:-8.12.99, reason:spatial type geo_point improved in 8.13]
 // tag::to_geopoint-equals[]
 ROW wkt = ["POINT(42.97109630194 14.7552534413725)", "POINT(75.8092915005895 22.727749187571)"]
@@ -534,6 +701,37 @@ centroid:cartesian_point                    | count:long
 POINT (726480.0130685265 3359566.331716279) | 849
 ;
 
+cartesianCentroidFromAirportsAfterIntersectsPredicate#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports_web
+| WHERE ST_INTERSECTS(location, TO_CARTESIANSHAPE("POLYGON((4700000 1600000, 4800000 1600000, 4800000 1700000, 4700000 1700000, 4700000 1600000))"))
+| STATS centroid=ST_CENTROID(location), count=COUNT()
+;
+
+centroid:cartesian_point     | count:long
+POINT (4783520.5 1661010.0)  | 1
+;
+
+cartesianPointIntersectsPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports_web
+| WHERE ST_INTERSECTS(location, TO_CARTESIANSHAPE("POLYGON((4700000 1600000, 4800000 1600000, 4800000 1700000, 4700000 1700000, 4700000 1600000))"))
+;
+
+abbrev:keyword | location:cartesian_point                     |  name:text     |  scalerank:i  |  type:k
+HOD            | POINT (4783520.559160681 1661010.0197476079) | Hodeidah Int'l | 9             | mid
+;
+
+literalCartesianPointIntersectsPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_CARTESIANPOINT(wkt)
+| WHERE ST_INTERSECTS(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)
+;
+
 cartesianPointEquals#[skip:-8.12.99, reason:spatial type cartesian_point improved in 8.13]
 // tag::to_cartesianpoint-equals[]
 ROW wkt = ["POINT(4297.11 -1475.53)", "POINT(7580.93 2272.77)"]

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

@@ -54,7 +54,7 @@ abbrev:keyword | name:text    | location:geo_shape                         | cou
 "VLC"          | "Valencia"   | POINT(-0.473474930771676 39.4914597884489) | "Spain"         | "Paterna"    | POINT(-0.4406 39.5028)
 ;
 
-simpleLoadFromCityBoundaries#[skip:-8.12.99, reason: spatial type geo_shape only added in 8.13]
+simpleLoadFromCityBoundaries#[skip:-8.13.99, reason:chunked CSV import support added in 8.14]
 FROM airport_city_boundaries
 | WHERE abbrev == "CPH"
 | EVAL boundary_wkt_length = LENGTH(TO_STRING(city_boundary))
@@ -66,8 +66,121 @@ abbrev:keyword  |  region:text         |  city_location:geo_point |  airport:tex
 CPH             |  Københavns Kommune  |  POINT(12.5683 55.6761)  |  Copenhagen    |  265
 ;
 
-geo_shapeEquals#[skip:-8.12.99, reason: spatial type geo_shape only added in 8.13]
+pointIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports
+| EVAL location = TO_GEOSHAPE(location)
+| WHERE ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))"))
+| KEEP abbrev, name, location, country, city, city_location
+;
+
+abbrev:keyword | name:text      | location:geo_shape                     | country:keyword | city:keyword | city_location:geo_point
+HOD            | Hodeidah Int'l | POINT(42.97109630194 14.7552534413725) | Yemen           | Al Ḩudaydah  | POINT(42.9511 14.8022)
+;
+
+polygonIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airport_city_boundaries
+| WHERE ST_INTERSECTS(city_boundary, TO_GEOSHAPE("POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))"))
+| KEEP abbrev, airport, region, city, city_location
+| LIMIT 1
+;
+
+abbrev:keyword | airport:text         | region:text | city:keyword | city_location:geo_point
+SYX            | Sanya Phoenix Int'l  | 天涯区       | Sanya        | POINT(109.5036 18.2533)
+;
+
+pointIntersectsLiteralPolygonReversed#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports
+| EVAL location = TO_GEOSHAPE(location)
+| WHERE ST_INTERSECTS(TO_GEOSHAPE("POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))"), location)
+| KEEP abbrev, name, location, country, city, city_location
+;
+
+abbrev:keyword | name:text      | location:geo_shape                     | country:keyword | city:keyword | city_location:geo_point
+HOD            | Hodeidah Int'l | POINT(42.97109630194 14.7552534413725) | Yemen           | Al Ḩudaydah  | POINT(42.9511 14.8022)
+;
+
+literalPointIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_GEOPOINT(wkt)
+| WHERE ST_INTERSECTS(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)
+;
+
+literalPointIntersectsLiteralPolygonReversed#[skip:-8.13.99, reason:st_intersects added in 8.14]
+ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_GEOPOINT(wkt)
+| WHERE ST_INTERSECTS(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)
+;
+
+literalPointAsShapeIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_GEOSHAPE(wkt)
+| WHERE ST_INTERSECTS(pt, TO_GEOSHAPE("POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"))
+;
+
+wkt:keyword   | pt:geo_shape
+"POINT(1 1)"  | POINT(1 1)
+"POINT(1 -1)" | POINT(1 -1)
+;
+
+literalPointAsShapeIntersectsLiteralPolygonReversed#[skip:-8.13.99, reason:st_intersects added in 8.14]
+ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_GEOSHAPE(wkt)
+| WHERE ST_INTERSECTS(TO_GEOSHAPE("POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"), pt)
+;
 
+wkt:keyword   | pt:geo_shape
+"POINT(1 1)"  | POINT(1 1)
+"POINT(1 -1)" | POINT(1 -1)
+;
+
+shapeIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM countries_bbox
+| WHERE ST_INTERSECTS(shape, TO_GEOSHAPE("POLYGON((29 -30, 31 -30, 31 -27.3, 29 -27.3, 29 -30))"))
+| SORT id DESC
+;
+
+id:keyword | name:keyword | shape:geo_shape
+ZAF        | South Africa | BBOX(16.483327, 37.892218, -22.136391, -46.969727)
+SWZ        | Swaziland    | BBOX(30.798336, 32.133400, -25.728336, -27.316391)
+LSO        | Lesotho      | BBOX(27.013973, 29.455554, -28.570691, -30.650527)
+;
+
+literalPolygonIntersectsLiteralPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+ROW wkt = ["POLYGON((-20 60, -6 60, -6 66, -20 66, -20 60))", "POLYGON((20 60, 6 60, 6 66, 20 66, 20 60))"]
+| EVAL other = TO_GEOSHAPE("POLYGON((-15 64, -10 64, -10 66, -15 66, -15 64))")
+| MV_EXPAND wkt
+| EVAL shape = TO_GEOSHAPE(wkt)
+| WHERE ST_INTERSECTS(shape, other)
+| KEEP wkt, shape, other
+;
+
+wkt:keyword                                       | shape:geo_shape                                 | other:geo_shape
+"POLYGON((-20 60, -6 60, -6 66, -20 66, -20 60))" | POLYGON((-20 60, -6 60, -6 66, -20 66, -20 60)) | POLYGON((-15 64, -10 64, -10 66, -15 66, -15 64))
+;
+
+literalPolygonIntersectsLiteralPolygonOneRow#[skip:-8.13.99, reason:st_intersects added in 8.14]
+ROW intersects = ST_INTERSECTS(TO_GEOSHAPE("POLYGON((-20 60, -6 60, -6 66, -20 66, -20 60))"), TO_GEOSHAPE("POLYGON((-15 64, -10 64, -10 66, -15 66, -15 64))"))
+;
+
+intersects:boolean
+true
+;
+
+geo_shapeEquals#[skip:-8.12.99, reason: spatial type geo_shape only added in 8.13]
 ROW wkt = ["POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))", "POINT(75.8092915005895 22.727749187571)"]
 | MV_EXPAND wkt
 | EVAL pt = to_geoshape(wkt)
@@ -162,6 +275,53 @@ abbrev:keyword | name:text    | scalerank:integer | type:keyword | location:cart
 "VLC"          | "Valencia"   | 8                 | "mid"        | POINT(-52706.98819688343 4792315.469321795)
 ;
 
+cartesianPointIntersectsPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM airports_web
+| EVAL location = TO_CARTESIANSHAPE(location)
+| WHERE ST_INTERSECTS(location, TO_CARTESIANSHAPE("POLYGON((4700000 1600000, 4800000 1600000, 4800000 1700000, 4700000 1700000, 4700000 1600000))"))
+| KEEP abbrev, name, location, scalerank, type
+;
+
+abbrev:keyword |  name:text     | location:cartesian_shape                     | scalerank:i | type:k
+HOD            | Hodeidah Int'l | POINT (4783520.559160681 1661010.0197476079) | 9           | mid
+;
+
+literalCartesianPointIntersectsPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+ROW wkt = ["POINT(1 1)", "POINT(-1 -1)", "POINT(-1 1)", "POINT(1 -1)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_CARTESIANSHAPE(wkt)
+| WHERE ST_INTERSECTS(pt, TO_CARTESIANSHAPE("POLYGON((0 -1, 1 -1, 1 1, 0 1, 0 -1))"))
+;
+
+wkt:keyword   | pt:cartesian_shape
+"POINT(1 1)"  | POINT(1 1)
+"POINT(1 -1)" | POINT(1 -1)
+;
+
+cartesianShapeIntersectsPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+FROM countries_bbox_web
+| WHERE ST_INTERSECTS(shape, TO_CARTESIANSHAPE("POLYGON((3100000 -3400000, 3500000 -3400000, 3500000 -3150000, 3100000 -3150000, 3100000 -3400000))"))
+| SORT id DESC
+;
+
+id:keyword | name:keyword | shape:cartesian_shape
+ZAF        | South Africa | BBOX(1834915.5679635953, 4218142.412200545, -2527908.4975596936, -5937134.146607068)
+SWZ        | Swaziland    | BBOX(3428455.080322901, 3577073.7249586442, -2965472.9128583763, -3163056.5390926218)
+LSO        | Lesotho      | BBOX(3007181.718244638, 3278977.271857335, -3321117.2692412077, -3587446.106149188)
+;
+
+literalCartesianPolygonIntersectsPolygon#[skip:-8.13.99, reason:st_intersects added in 8.14]
+ROW wkt = ["POLYGON((-2000 6000, -600 6000, -600 6600, -2000 6600, -2000 6000))", "POLYGON((2000 6000, 600 6000, 600 6600, 2000 6600, 2000 6000))"]
+| MV_EXPAND wkt
+| EVAL shape = TO_CARTESIANSHAPE(wkt)
+| EVAL other = TO_CARTESIANSHAPE("POLYGON((-1500 6400, -1000 6400, -1000 6600, -1500 6600, -1500 6400))")
+| WHERE ST_INTERSECTS(shape, other)
+;
+
+wkt:keyword                                                           | shape:cartesian_shape                                               | other:cartesian_shape
+"POLYGON((-2000 6000, -600 6000, -600 6600, -2000 6600, -2000 6000))" | POLYGON((-2000 6000, -600 6000, -600 6600, -2000 6600, -2000 6000)) | POLYGON((-1500 6400, -1000 6400, -1000 6600, -1500 6600, -1500 6400))
+;
+
 cartesianshapeEquals#[skip:-8.12.99, reason: spatial type cartesian_shape only added in 8.13]
 ROW wkt = ["POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))", "POINT(7580.93 2272.77)"]
 | MV_EXPAND wkt

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

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

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

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

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

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

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

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

+ 16 - 1
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/EsqlTypeResolutions.java

@@ -17,6 +17,10 @@ import org.elasticsearch.xpack.ql.type.EsField;
 import java.util.Locale;
 
 import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
+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;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.DEFAULT;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isType;
 
@@ -45,7 +49,18 @@ public class EsqlTypeResolutions {
         return Expression.TypeResolution.TYPE_RESOLVED;
     }
 
+    private static final String[] SPATIAL_TYPE_NAMES = new String[] {
+        GEO_POINT.typeName(),
+        CARTESIAN_POINT.typeName(),
+        GEO_SHAPE.typeName(),
+        CARTESIAN_SHAPE.typeName() };
+    private static final String[] POINT_TYPE_NAMES = new String[] { GEO_POINT.typeName(), CARTESIAN_POINT.typeName() };
+
     public static Expression.TypeResolution isSpatialPoint(Expression e, String operationName, TypeResolutions.ParamOrdinal paramOrd) {
-        return isType(e, EsqlDataTypes::isSpatialPoint, operationName, paramOrd, "geo_point or cartesian_point");
+        return isType(e, EsqlDataTypes::isSpatialPoint, operationName, paramOrd, POINT_TYPE_NAMES);
+    }
+
+    public static Expression.TypeResolution isSpatial(Expression e, String operationName, TypeResolutions.ParamOrdinal paramOrd) {
+        return isType(e, EsqlDataTypes::isSpatial, operationName, paramOrd, SPATIAL_TYPE_NAMES);
     }
 }

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

@@ -78,6 +78,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSort
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSum;
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvZip;
 import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialIntersects;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StY;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat;
@@ -178,9 +179,11 @@ public final class EsqlFunctionRegistry extends FunctionRegistry {
                 def(DateTrunc.class, DateTrunc::new, "date_trunc"),
                 def(Now.class, Now::new, "now") },
             // spatial
-            new FunctionDefinition[] { def(SpatialCentroid.class, SpatialCentroid::new, "st_centroid") },
-            new FunctionDefinition[] { def(StX.class, StX::new, "st_x") },
-            new FunctionDefinition[] { def(StY.class, StY::new, "st_y") },
+            new FunctionDefinition[] {
+                def(SpatialCentroid.class, SpatialCentroid::new, "st_centroid"),
+                def(SpatialIntersects.class, SpatialIntersects::new, "st_intersects"),
+                def(StX.class, StX::new, "st_x"),
+                def(StY.class, StY::new, "st_y") },
             // conditional
             new FunctionDefinition[] { def(Case.class, Case::new, "case") },
             // null

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

@@ -0,0 +1,212 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import org.apache.lucene.geo.Component2D;
+import org.elasticsearch.common.TriFunction;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+
+import java.util.Map;
+import java.util.function.Function;
+
+import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils.asLuceneComponent2D;
+
+/**
+ * SpatialRelatesFunction classes, like SpatialIntersects, support various combinations of incoming types, which can be sourced from
+ * constant literals (foldable), or from the index, which could provide either source values or doc-values. This class is used to
+ * create the appropriate evaluator for the given combination of types.
+ * @param <V>
+ * @param <T>
+ */
+abstract class SpatialEvaluatorFactory<V, T> {
+    protected final TriFunction<Source, V, T, EvalOperator.ExpressionEvaluator.Factory> factoryCreator;
+
+    SpatialEvaluatorFactory(TriFunction<Source, V, T, EvalOperator.ExpressionEvaluator.Factory> factoryCreator) {
+        this.factoryCreator = factoryCreator;
+    }
+
+    public abstract EvalOperator.ExpressionEvaluator.Factory get(
+        SpatialSourceSupplier function,
+        Function<Expression, EvalOperator.ExpressionEvaluator.Factory> toEvaluator
+    );
+
+    public static EvalOperator.ExpressionEvaluator.Factory makeSpatialEvaluator(
+        SpatialSourceSupplier s,
+        Map<SpatialEvaluatorKey, SpatialEvaluatorFactory<?, ?>> evaluatorRules,
+        Function<Expression, EvalOperator.ExpressionEvaluator.Factory> toEvaluator
+    ) {
+        var evaluatorKey = new SpatialEvaluatorKey(
+            s.crsType(),
+            s.leftDocValues(),
+            s.rightDocValues(),
+            fieldKey(s.left()),
+            fieldKey(s.right())
+        );
+        SpatialEvaluatorFactory<?, ?> factory = evaluatorRules.get(evaluatorKey);
+        if (factory == null) {
+            evaluatorKey = evaluatorKey.swapSides();
+            factory = evaluatorRules.get(evaluatorKey);
+            if (factory == null) {
+                throw evaluatorKey.unsupported();
+            }
+            return factory.get(new SwappedSpatialSourceSupplier(s), toEvaluator);
+        }
+        return factory.get(s, toEvaluator);
+    }
+
+    protected static SpatialEvaluatorFieldKey fieldKey(Expression expression) {
+        return new SpatialEvaluatorFieldKey(expression.dataType(), expression.foldable());
+    }
+
+    /**
+     * This interface defines a supplier of the key information needed by the spatial evaluator factories.
+     * The SpatialRelatesFunction will use this to supply the necessary information to the factories.
+     * When we need to swap left and right sides around, we can use a SwappableSpatialSourceSupplier.
+     */
+    interface SpatialSourceSupplier {
+        Source source();
+
+        Expression left();
+
+        Expression right();
+
+        SpatialRelatesFunction.SpatialCrsType crsType();
+
+        boolean leftDocValues();
+
+        boolean rightDocValues();
+    }
+
+    protected static class SwappedSpatialSourceSupplier implements SpatialSourceSupplier {
+        private final SpatialSourceSupplier delegate;
+
+        public SwappedSpatialSourceSupplier(SpatialSourceSupplier delegate) {
+            this.delegate = delegate;
+        }
+
+        @Override
+        public Source source() {
+            return delegate.source();
+        }
+
+        @Override
+        public SpatialRelatesFunction.SpatialCrsType crsType() {
+            return delegate.crsType();
+        }
+
+        @Override
+        public boolean leftDocValues() {
+            return delegate.leftDocValues();
+        }
+
+        @Override
+        public boolean rightDocValues() {
+            return delegate.rightDocValues();
+        }
+
+        @Override
+        public Expression left() {
+            return delegate.right();
+        }
+
+        @Override
+        public Expression right() {
+            return delegate.left();
+        }
+    }
+
+    protected static class SpatialEvaluatorFactoryWithFields extends SpatialEvaluatorFactory<
+        EvalOperator.ExpressionEvaluator.Factory,
+        EvalOperator.ExpressionEvaluator.Factory> {
+        SpatialEvaluatorFactoryWithFields(
+            TriFunction<
+                Source,
+                EvalOperator.ExpressionEvaluator.Factory,
+                EvalOperator.ExpressionEvaluator.Factory,
+                EvalOperator.ExpressionEvaluator.Factory> factoryCreator
+        ) {
+            super(factoryCreator);
+        }
+
+        @Override
+        public EvalOperator.ExpressionEvaluator.Factory get(
+            SpatialSourceSupplier s,
+            Function<Expression, EvalOperator.ExpressionEvaluator.Factory> toEvaluator
+        ) {
+            return factoryCreator.apply(s.source(), toEvaluator.apply(s.left()), toEvaluator.apply(s.right()));
+        }
+    }
+
+    protected static class SpatialEvaluatorWithConstantFactory extends SpatialEvaluatorFactory<
+        EvalOperator.ExpressionEvaluator.Factory,
+        Component2D> {
+
+        SpatialEvaluatorWithConstantFactory(
+            TriFunction<
+                Source,
+                EvalOperator.ExpressionEvaluator.Factory,
+                Component2D,
+                EvalOperator.ExpressionEvaluator.Factory> factoryCreator
+        ) {
+            super(factoryCreator);
+        }
+
+        @Override
+        public EvalOperator.ExpressionEvaluator.Factory get(
+            SpatialSourceSupplier s,
+            Function<Expression, EvalOperator.ExpressionEvaluator.Factory> toEvaluator
+        ) {
+            return factoryCreator.apply(s.source(), toEvaluator.apply(s.left()), asLuceneComponent2D(s.crsType(), s.right()));
+        }
+    }
+
+    protected record SpatialEvaluatorFieldKey(DataType dataType, boolean isConstant) {}
+
+    protected record SpatialEvaluatorKey(
+        SpatialRelatesFunction.SpatialCrsType crsType,
+        boolean leftDocValues,
+        boolean rightDocValues,
+        SpatialEvaluatorFieldKey left,
+        SpatialEvaluatorFieldKey right
+    ) {
+        SpatialEvaluatorKey(SpatialRelatesFunction.SpatialCrsType crsType, SpatialEvaluatorFieldKey left, SpatialEvaluatorFieldKey right) {
+            this(crsType, false, false, left, right);
+        }
+
+        SpatialEvaluatorKey withLeftDocValues() {
+            return new SpatialEvaluatorKey(crsType, true, false, left, right);
+        }
+
+        SpatialEvaluatorKey swapSides() {
+            return new SpatialEvaluatorKey(crsType, rightDocValues, leftDocValues, right, left);
+        }
+
+        static SpatialEvaluatorKey fromSourceAndConstant(DataType left, DataType right) {
+            return new SpatialEvaluatorKey(
+                SpatialRelatesFunction.SpatialCrsType.fromDataType(left),
+                new SpatialEvaluatorFieldKey(left, false),
+                new SpatialEvaluatorFieldKey(right, true)
+            );
+        }
+
+        static SpatialEvaluatorKey fromSources(DataType left, DataType right) {
+            return new SpatialEvaluatorKey(
+                SpatialRelatesFunction.SpatialCrsType.fromDataType(left),
+                new SpatialEvaluatorFieldKey(left, false),
+                new SpatialEvaluatorFieldKey(right, false)
+            );
+        }
+
+        UnsupportedOperationException unsupported() {
+            return new UnsupportedOperationException("Unsupported spatial relation combination: " + this);
+        }
+    }
+}

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

@@ -0,0 +1,226 @@
+/*
+ * 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.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;
+
+public class SpatialIntersects extends SpatialRelatesFunction {
+    protected static final SpatialRelations GEO = new SpatialRelations(
+        ShapeField.QueryRelation.INTERSECTS,
+        SpatialCoordinateTypes.GEO,
+        CoordinateEncoder.GEO,
+        new GeoShapeIndexer(Orientation.CCW, "ST_Intersects")
+    );
+    protected static final SpatialRelations CARTESIAN = new SpatialRelations(
+        ShapeField.QueryRelation.INTERSECTS,
+        SpatialCoordinateTypes.CARTESIAN,
+        CoordinateEncoder.CARTESIAN,
+        new CartesianShapeIndexer("ST_Intersects")
+    );
+
+    @FunctionInfo(returnType = { "boolean" }, description = "Returns whether the two geometries or geometry columns intersect.")
+    public SpatialIntersects(
+        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 SpatialIntersects(Source source, Expression left, Expression right, boolean leftDocValues, boolean rightDocValues) {
+        super(source, left, right, leftDocValues, rightDocValues);
+    }
+
+    @Override
+    public ShapeField.QueryRelation queryRelation() {
+        return ShapeField.QueryRelation.INTERSECTS;
+    }
+
+    @Override
+    public SpatialIntersects 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 SpatialIntersects(source(), left(), right(), leftDV, rightDV);
+    }
+
+    @Override
+    protected SpatialIntersects replaceChildren(Expression newLeft, Expression newRight) {
+        return new SpatialIntersects(source(), newLeft, newRight, leftDocValues, rightDocValues);
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, SpatialIntersects::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
+    protected 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(SpatialIntersectsGeoSourceAndSourceEvaluator.Factory::new)
+                );
+                evaluatorMap.put(
+                    SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType),
+                    new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                        SpatialIntersectsGeoSourceAndConstantEvaluator.Factory::new
+                    )
+                );
+                if (EsqlDataTypes.isSpatialPoint(spatialType)) {
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSources(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorFactoryWithFields(
+                            SpatialIntersectsGeoPointDocValuesAndSourceEvaluator.Factory::new
+                        )
+                    );
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                            SpatialIntersectsGeoPointDocValuesAndConstantEvaluator.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(
+                        SpatialIntersectsCartesianSourceAndSourceEvaluator.Factory::new
+                    )
+                );
+                evaluatorMap.put(
+                    SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType),
+                    new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                        SpatialIntersectsCartesianSourceAndConstantEvaluator.Factory::new
+                    )
+                );
+                if (EsqlDataTypes.isSpatialPoint(spatialType)) {
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSources(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorFactoryWithFields(
+                            SpatialIntersectsCartesianPointDocValuesAndSourceEvaluator.Factory::new
+                        )
+                    );
+                    evaluatorMap.put(
+                        SpatialEvaluatorFactory.SpatialEvaluatorKey.fromSourceAndConstant(spatialType, otherType).withLeftDocValues(),
+                        new SpatialEvaluatorFactory.SpatialEvaluatorWithConstantFactory(
+                            SpatialIntersectsCartesianPointDocValuesAndConstantEvaluator.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);
+    }
+}

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

@@ -0,0 +1,297 @@
+/*
+ * 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.compute.operator.EvalOperator;
+import org.elasticsearch.geometry.Geometry;
+import org.elasticsearch.geometry.Point;
+import org.elasticsearch.index.mapper.ShapeIndexer;
+import org.elasticsearch.lucene.spatial.Component2DVisitor;
+import org.elasticsearch.lucene.spatial.CoordinateEncoder;
+import org.elasticsearch.lucene.spatial.GeometryDocValueReader;
+import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper;
+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.expression.TypeResolutions;
+import org.elasticsearch.xpack.ql.expression.function.scalar.BinaryScalarFunction;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+import org.elasticsearch.xpack.ql.util.SpatialCoordinateTypes;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import static org.apache.lucene.document.ShapeField.QueryRelation.DISJOINT;
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isSpatial;
+import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils.asGeometryDocValueReader;
+import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils.asLuceneComponent2D;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.GEO_POINT;
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.GEO_SHAPE;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.FIRST;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.SECOND;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isType;
+import static org.elasticsearch.xpack.ql.type.DataTypes.isNull;
+
+public abstract class SpatialRelatesFunction extends BinaryScalarFunction
+    implements
+        EvaluatorMapper,
+        SpatialEvaluatorFactory.SpatialSourceSupplier {
+    protected SpatialCrsType crsType;
+    protected final boolean leftDocValues;
+    protected final boolean rightDocValues;
+
+    protected SpatialRelatesFunction(Source source, Expression left, Expression right, boolean leftDocValues, boolean rightDocValues) {
+        super(source, left, right);
+        this.leftDocValues = leftDocValues;
+        this.rightDocValues = rightDocValues;
+    }
+
+    public abstract ShapeField.QueryRelation queryRelation();
+
+    @Override
+    public DataType dataType() {
+        return DataTypes.BOOLEAN;
+    }
+
+    @Override
+    public SpatialCrsType crsType() {
+        if (crsType == null) {
+            resolveType();
+        }
+        return crsType;
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (left().foldable() && right().foldable() == false || isNull(left().dataType())) {
+            // Left is literal, but right is not, check the left field's type against the right field
+            return resolveType(right(), left(), SECOND, FIRST);
+        } else {
+            // All other cases check the right against the left
+            return resolveType(left(), right(), FIRST, SECOND);
+        }
+    }
+
+    private TypeResolution resolveType(
+        Expression leftExpression,
+        Expression rightExpression,
+        TypeResolutions.ParamOrdinal leftOrdinal,
+        TypeResolutions.ParamOrdinal rightOrdinal
+    ) {
+        TypeResolution leftResolution = isSpatial(leftExpression, sourceText(), leftOrdinal);
+        TypeResolution rightResolution = isSpatial(rightExpression, sourceText(), rightOrdinal);
+        if (leftResolution.resolved()) {
+            return resolveType(leftExpression, rightExpression, rightOrdinal);
+        } else if (rightResolution.resolved()) {
+            return resolveType(rightExpression, leftExpression, leftOrdinal);
+        } else {
+            return leftResolution;
+        }
+    }
+
+    protected TypeResolution resolveType(
+        Expression spatialExpression,
+        Expression otherExpression,
+        TypeResolutions.ParamOrdinal otherParamOrdinal
+    ) {
+        if (isNull(spatialExpression.dataType())) {
+            return isSpatial(otherExpression, sourceText(), otherParamOrdinal);
+        }
+        TypeResolution resolution = isSameSpatialType(spatialExpression.dataType(), otherExpression, sourceText(), otherParamOrdinal);
+        if (resolution.unresolved()) {
+            return resolution;
+        }
+        crsType = SpatialCrsType.fromDataType(spatialExpression.dataType());
+        return TypeResolution.TYPE_RESOLVED;
+    }
+
+    public static TypeResolution isSameSpatialType(
+        DataType spatialDataType,
+        Expression expression,
+        String operationName,
+        TypeResolutions.ParamOrdinal paramOrd
+    ) {
+        return isType(
+            expression,
+            dt -> EsqlDataTypes.isSpatial(dt) && spatialCRSCompatible(spatialDataType, dt),
+            operationName,
+            paramOrd,
+            compatibleTypeNames(spatialDataType)
+        );
+    }
+
+    private static final String[] GEO_TYPE_NAMES = new String[] { GEO_POINT.typeName(), GEO_SHAPE.typeName() };
+    private static final String[] CARTESIAN_TYPE_NAMES = new String[] { GEO_POINT.typeName(), GEO_SHAPE.typeName() };
+
+    private static boolean spatialCRSCompatible(DataType spatialDataType, DataType otherDataType) {
+        return EsqlDataTypes.isSpatialGeo(spatialDataType) && EsqlDataTypes.isSpatialGeo(otherDataType)
+            || EsqlDataTypes.isSpatialGeo(spatialDataType) == false && EsqlDataTypes.isSpatialGeo(otherDataType) == false;
+    }
+
+    static String[] compatibleTypeNames(DataType spatialDataType) {
+        return EsqlDataTypes.isSpatialGeo(spatialDataType) ? GEO_TYPE_NAMES : CARTESIAN_TYPE_NAMES;
+    }
+
+    @Override
+    public boolean foldable() {
+        return left().foldable() && right().foldable();
+    }
+
+    /**
+     * Mark the function as expecting the specified fields to arrive as doc-values.
+     */
+    public abstract SpatialRelatesFunction withDocValues(Set<FieldAttribute> attributes);
+
+    /**
+     * Push-down to Lucene is only possible if one field is an indexed spatial field, and the other is a constant spatial or string column.
+     */
+    public boolean canPushToSource(Predicate<FieldAttribute> isAggregatable) {
+        // The use of foldable here instead of SpatialEvaluatorFieldKey.isConstant is intentional to match the behavior of the
+        // Lucene pushdown code in EsqlTranslationHandler::SpatialRelatesTranslator
+        // We could enhance both places to support ReferenceAttributes that refer to constants, but that is a larger change
+        return isPushableFieldAttribute(left(), isAggregatable) && right().foldable()
+            || isPushableFieldAttribute(right(), isAggregatable) && left().foldable();
+    }
+
+    private static boolean isPushableFieldAttribute(Expression exp, Predicate<FieldAttribute> isAggregatable) {
+        return exp instanceof FieldAttribute fa
+            && fa.getExactInfo().hasExact()
+            && isAggregatable.test(fa)
+            && EsqlDataTypes.isSpatial(fa.dataType());
+    }
+
+    @Override
+    public int hashCode() {
+        // NB: the hashcode is currently used for key generation so
+        // to avoid clashes between aggs with the same arguments, add the class name as variation
+        return Objects.hash(getClass(), children(), leftDocValues, rightDocValues);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (super.equals(obj)) {
+            SpatialRelatesFunction other = (SpatialRelatesFunction) obj;
+            return Objects.equals(other.children(), children())
+                && Objects.equals(other.leftDocValues, leftDocValues)
+                && Objects.equals(other.rightDocValues, rightDocValues);
+        }
+        return false;
+    }
+
+    public boolean leftDocValues() {
+        return leftDocValues;
+    }
+
+    public boolean rightDocValues() {
+        return rightDocValues;
+    }
+
+    /**
+     * Produce a map of rules defining combinations of incoming types to the evaluator factory that should be used.
+     */
+    protected abstract Map<SpatialEvaluatorFactory.SpatialEvaluatorKey, SpatialEvaluatorFactory<?, ?>> evaluatorRules();
+
+    @Override
+    public EvalOperator.ExpressionEvaluator.Factory toEvaluator(
+        Function<Expression, EvalOperator.ExpressionEvaluator.Factory> toEvaluator
+    ) {
+        return SpatialEvaluatorFactory.makeSpatialEvaluator(this, evaluatorRules(), toEvaluator);
+    }
+
+    /**
+     * When performing local physical plan optimization, it is necessary to know if this function has a field attribute.
+     * This is because the planner might push down a spatial aggregation to lucene, which results in the field being provided
+     * as doc-values instead of source values, and this function needs to know if it should use doc-values or not.
+     */
+    public boolean hasFieldAttribute(Set<FieldAttribute> foundAttributes) {
+        return foundField(left(), foundAttributes) || foundField(right(), foundAttributes);
+    }
+
+    protected boolean foundField(Expression expression, Set<FieldAttribute> foundAttributes) {
+        return expression instanceof FieldAttribute field && foundAttributes.contains(field);
+    }
+
+    protected enum SpatialCrsType {
+        GEO,
+        CARTESIAN,
+        UNSPECIFIED;
+
+        public static SpatialCrsType fromDataType(DataType dataType) {
+            return EsqlDataTypes.isSpatialGeo(dataType) ? SpatialCrsType.GEO
+                : EsqlDataTypes.isSpatial(dataType) ? SpatialCrsType.CARTESIAN
+                : SpatialCrsType.UNSPECIFIED;
+        }
+    }
+
+    protected static class SpatialRelations {
+        protected final ShapeField.QueryRelation queryRelation;
+        protected final SpatialCoordinateTypes spatialCoordinateType;
+        protected final CoordinateEncoder coordinateEncoder;
+        protected final ShapeIndexer shapeIndexer;
+        protected final SpatialCrsType crsType;
+
+        protected SpatialRelations(
+            ShapeField.QueryRelation queryRelation,
+            SpatialCoordinateTypes spatialCoordinateType,
+            CoordinateEncoder encoder,
+            ShapeIndexer shapeIndexer
+        ) {
+            this.queryRelation = queryRelation;
+            this.spatialCoordinateType = spatialCoordinateType;
+            this.coordinateEncoder = encoder;
+            this.shapeIndexer = shapeIndexer;
+            this.crsType = spatialCoordinateType.equals(SpatialCoordinateTypes.GEO) ? SpatialCrsType.GEO : SpatialCrsType.CARTESIAN;
+        }
+
+        protected boolean geometryRelatesGeometry(BytesRef left, BytesRef right) throws IOException {
+            Component2D rightComponent2D = asLuceneComponent2D(crsType, fromBytesRef(right));
+            return geometryRelatesGeometry(left, rightComponent2D);
+        }
+
+        private Geometry fromBytesRef(BytesRef bytesRef) {
+            return SpatialCoordinateTypes.UNSPECIFIED.wkbToGeometry(bytesRef);
+        }
+
+        protected boolean geometryRelatesGeometry(BytesRef left, Component2D rightComponent2D) throws IOException {
+            Geometry leftGeom = fromBytesRef(left);
+            // We already have a Component2D for the right geometry, so we need to convert the left geometry to a doc-values byte array
+            return geometryRelatesGeometry(asGeometryDocValueReader(coordinateEncoder, shapeIndexer, leftGeom), rightComponent2D);
+        }
+
+        protected boolean geometryRelatesGeometry(GeometryDocValueReader reader, Component2D rightComponent2D) throws IOException {
+            var visitor = Component2DVisitor.getVisitor(rightComponent2D, queryRelation, coordinateEncoder);
+            reader.visit(visitor);
+            return visitor.matches();
+        }
+
+        protected boolean pointRelatesGeometry(long encoded, Geometry geometry) {
+            Component2D component2D = asLuceneComponent2D(crsType, geometry);
+            return pointRelatesGeometry(encoded, component2D);
+        }
+
+        protected boolean pointRelatesGeometry(long encoded, Component2D component2D) {
+            // This code path exists for doc-values points, and we could consider re-using the point class to reduce garbage creation
+            Point point = spatialCoordinateType.longAsPoint(encoded);
+            return geometryRelatesPoint(component2D, point);
+        }
+
+        private boolean geometryRelatesPoint(Component2D component2D, Point point) {
+            boolean contains = component2D.contains(point.getX(), point.getY());
+            return queryRelation == DISJOINT ? contains == false : contains;
+        }
+    }
+}

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

@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import org.apache.lucene.geo.Component2D;
+import org.apache.lucene.geo.LatLonGeometry;
+import org.apache.lucene.geo.XYGeometry;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.geo.LuceneGeometriesUtils;
+import org.elasticsearch.common.geo.Orientation;
+import org.elasticsearch.geometry.Circle;
+import org.elasticsearch.geometry.Geometry;
+import org.elasticsearch.geometry.ShapeType;
+import org.elasticsearch.index.mapper.GeoShapeIndexer;
+import org.elasticsearch.index.mapper.ShapeIndexer;
+import org.elasticsearch.lucene.spatial.CartesianShapeIndexer;
+import org.elasticsearch.lucene.spatial.CentroidCalculator;
+import org.elasticsearch.lucene.spatial.CoordinateEncoder;
+import org.elasticsearch.lucene.spatial.GeometryDocValueReader;
+import org.elasticsearch.lucene.spatial.GeometryDocValueWriter;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.util.SpatialCoordinateTypes;
+
+import java.io.IOException;
+
+import static org.elasticsearch.xpack.ql.planner.ExpressionTranslators.valueOf;
+
+public class SpatialRelatesUtils {
+
+    /**
+     * This function is used to convert a spatial constant to a lucene Component2D.
+     * When both left and right sides are constants, we convert the left to a doc-values byte array and the right to a Component2D.
+     */
+    static Component2D asLuceneComponent2D(SpatialRelatesFunction.SpatialCrsType crsType, Expression expression) {
+        return asLuceneComponent2D(crsType, makeGeometryFromLiteral(expression));
+    }
+
+    static Component2D asLuceneComponent2D(SpatialRelatesFunction.SpatialCrsType crsType, Geometry geometry) {
+        if (crsType == SpatialRelatesFunction.SpatialCrsType.GEO) {
+            var luceneGeometries = LuceneGeometriesUtils.toLatLonGeometry(geometry, true, t -> {});
+            return LatLonGeometry.create(luceneGeometries);
+        } else {
+            var luceneGeometries = LuceneGeometriesUtils.toXYGeometry(geometry, t -> {});
+            return XYGeometry.create(luceneGeometries);
+        }
+    }
+
+    /**
+     * This function is used to convert a spatial constant to a doc-values byte array.
+     * When both left and right sides are constants, we convert the left to a doc-values byte array and the right to a Component2D.
+     */
+    static GeometryDocValueReader asGeometryDocValueReader(SpatialRelatesFunction.SpatialCrsType crsType, Expression expression)
+        throws IOException {
+        Geometry geometry = makeGeometryFromLiteral(expression);
+        if (crsType == SpatialRelatesFunction.SpatialCrsType.GEO) {
+            return asGeometryDocValueReader(
+                CoordinateEncoder.GEO,
+                new GeoShapeIndexer(Orientation.CCW, "SpatialRelatesFunction"),
+                geometry
+            );
+        } else {
+            return asGeometryDocValueReader(CoordinateEncoder.CARTESIAN, new CartesianShapeIndexer("SpatialRelatesFunction"), geometry);
+        }
+
+    }
+
+    /**
+     * Converting shapes into doc-values byte arrays is needed under two situations:
+     * - If both left and right are constants, we convert the right to Component2D and the left to doc-values for comparison
+     * - If the right is a constant and no lucene push-down was possible, we get WKB in the left and convert it to doc-values for comparison
+     */
+    static GeometryDocValueReader asGeometryDocValueReader(CoordinateEncoder encoder, ShapeIndexer shapeIndexer, Geometry geometry)
+        throws IOException {
+        GeometryDocValueReader reader = new GeometryDocValueReader();
+        CentroidCalculator centroidCalculator = new CentroidCalculator();
+        if (geometry instanceof Circle) {
+            // Both the centroid calculator and the shape indexer do not support circles
+            throw new IllegalArgumentException(ShapeType.CIRCLE + " geometry is not supported");
+        }
+        centroidCalculator.add(geometry);
+        reader.reset(GeometryDocValueWriter.write(shapeIndexer.indexShape(geometry), encoder, centroidCalculator));
+        return reader;
+    }
+
+    /**
+     * This function is used in two places, when evaluating a spatial constant in the SpatialRelatesFunction, as well as when
+     * we do lucene-pushdown of spatial functions.
+     */
+    public static Geometry makeGeometryFromLiteral(Expression expr) {
+        Object value = valueOf(expr);
+
+        if (value instanceof BytesRef bytesRef) {
+            return SpatialCoordinateTypes.UNSPECIFIED.wkbToGeometry(bytesRef);
+        } else {
+            throw new IllegalArgumentException(
+                "Unsupported combination of literal [" + value.getClass().getSimpleName() + "] of type [" + expr.dataType() + "]"
+            );
+        }
+    }
+}

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

@@ -102,6 +102,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSort
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSum;
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvZip;
 import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialIntersects;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX;
 import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StY;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat;
@@ -387,6 +388,7 @@ public final class PlanNamedTypes {
             of(ScalarFunction.class, Pow.class, PlanNamedTypes::writePow, PlanNamedTypes::readPow),
             of(ScalarFunction.class, StartsWith.class, PlanNamedTypes::writeStartsWith, PlanNamedTypes::readStartsWith),
             of(ScalarFunction.class, EndsWith.class, PlanNamedTypes::writeEndsWith, PlanNamedTypes::readEndsWith),
+            of(ScalarFunction.class, SpatialIntersects.class, PlanNamedTypes::writeIntersects, PlanNamedTypes::readIntersects),
             of(ScalarFunction.class, Substring.class, PlanNamedTypes::writeSubstring, PlanNamedTypes::readSubstring),
             of(ScalarFunction.class, Left.class, PlanNamedTypes::writeLeft, PlanNamedTypes::readLeft),
             of(ScalarFunction.class, Right.class, PlanNamedTypes::writeRight, PlanNamedTypes::readRight),
@@ -1470,6 +1472,17 @@ public final class PlanNamedTypes {
         out.writeExpression(fields.get(1));
     }
 
+    static SpatialIntersects readIntersects(PlanStreamInput in) throws IOException {
+        return new SpatialIntersects(Source.EMPTY, in.readExpression(), in.readExpression());
+    }
+
+    static void writeIntersects(PlanStreamOutput out, SpatialIntersects intersects) throws IOException {
+        List<Expression> fields = intersects.children();
+        assert fields.size() == 2;
+        out.writeExpression(fields.get(0));
+        out.writeExpression(fields.get(1));
+    }
+
     static Now readNow(PlanStreamInput in) throws IOException {
         return new Now(in.readSource(), in.configuration());
     }

+ 56 - 2
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizer.java

@@ -16,6 +16,7 @@ import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.Inse
 import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.NotEquals;
 import org.elasticsearch.xpack.esql.expression.function.aggregate.Count;
 import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesFunction;
 import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In;
 import org.elasticsearch.xpack.esql.optimizer.PhysicalOptimizerRules.OptimizerRule;
 import org.elasticsearch.xpack.esql.plan.physical.AggregateExec;
@@ -24,6 +25,7 @@ import org.elasticsearch.xpack.esql.plan.physical.EsSourceExec;
 import org.elasticsearch.xpack.esql.plan.physical.EsStatsQueryExec;
 import org.elasticsearch.xpack.esql.plan.physical.EsStatsQueryExec.Stat;
 import org.elasticsearch.xpack.esql.plan.physical.EsTimeseriesQueryExec;
+import org.elasticsearch.xpack.esql.plan.physical.EvalExec;
 import org.elasticsearch.xpack.esql.plan.physical.ExchangeExec;
 import org.elasticsearch.xpack.esql.plan.physical.FieldExtractExec;
 import org.elasticsearch.xpack.esql.plan.physical.FilterExec;
@@ -269,6 +271,8 @@ public class LocalPhysicalPlanOptimizer extends ParameterizedRuleExecutor<Physic
             } else if (exp instanceof CIDRMatch cidrMatch) {
                 return isAttributePushable(cidrMatch.ipField(), cidrMatch, hasIdenticalDelegate)
                     && Expressions.foldable(cidrMatch.matches());
+            } else if (exp instanceof SpatialRelatesFunction bc) {
+                return bc.canPushToSource(LocalPhysicalPlanOptimizer::isAggregatable);
             }
             return false;
         }
@@ -453,7 +457,7 @@ public class LocalPhysicalPlanOptimizer extends ParameterizedRuleExecutor<Physic
     private static class SpatialDocValuesExtraction extends OptimizerRule<AggregateExec> {
         @Override
         protected PhysicalPlan rule(AggregateExec aggregate) {
-            var foundAttributes = new HashSet<Attribute>();
+            var foundAttributes = new HashSet<FieldAttribute>();
 
             PhysicalPlan plan = aggregate.transformDown(UnaryExec.class, exec -> {
                 if (exec instanceof AggregateExec agg) {
@@ -461,7 +465,8 @@ public class LocalPhysicalPlanOptimizer extends ParameterizedRuleExecutor<Physic
                     var changedAggregates = false;
                     for (NamedExpression aggExpr : agg.aggregates()) {
                         if (aggExpr instanceof Alias as && as.child() instanceof SpatialAggregateFunction af) {
-                            if (af.field() instanceof FieldAttribute fieldAttribute) {
+                            if (af.field() instanceof FieldAttribute fieldAttribute
+                                && allowedForDocValues(fieldAttribute, agg, foundAttributes)) {
                                 // We need to both mark the field to load differently, and change the spatial function to know to use it
                                 foundAttributes.add(fieldAttribute);
                                 changedAggregates = true;
@@ -484,6 +489,36 @@ public class LocalPhysicalPlanOptimizer extends ParameterizedRuleExecutor<Physic
                         );
                     }
                 }
+                if (exec instanceof EvalExec evalExec) {
+                    List<Alias> fields = evalExec.fields();
+                    List<Alias> changed = fields.stream()
+                        .map(
+                            f -> (Alias) f.transformDown(
+                                SpatialRelatesFunction.class,
+                                spatialRelatesFunction -> (spatialRelatesFunction.hasFieldAttribute(foundAttributes))
+                                    ? spatialRelatesFunction.withDocValues(foundAttributes)
+                                    : spatialRelatesFunction
+                            )
+                        )
+                        .toList();
+                    if (changed.equals(fields) == false) {
+                        exec = new EvalExec(exec.source(), exec.child(), changed);
+                    }
+                }
+                if (exec instanceof FilterExec filterExec) {
+                    // Note that ST_CENTROID does not support shapes, but SpatialRelatesFunction does, so when we extend the centroid
+                    // to support shapes, we need to consider loading shape doc-values for both centroid and relates (ST_INTERSECTS)
+                    var condition = filterExec.condition()
+                        .transformDown(
+                            SpatialRelatesFunction.class,
+                            spatialRelatesFunction -> (spatialRelatesFunction.hasFieldAttribute(foundAttributes))
+                                ? spatialRelatesFunction.withDocValues(foundAttributes)
+                                : spatialRelatesFunction
+                        );
+                    if (filterExec.condition().equals(condition) == false) {
+                        exec = new FilterExec(filterExec.source(), filterExec.child(), condition);
+                    }
+                }
                 if (exec instanceof FieldExtractExec fieldExtractExec) {
                     // Tell the field extractor that it should extract the field from doc-values instead of source values
                     var attributesToExtract = fieldExtractExec.attributesToExtract();
@@ -501,5 +536,24 @@ public class LocalPhysicalPlanOptimizer extends ParameterizedRuleExecutor<Physic
             });
             return plan;
         }
+
+        /**
+         * This function disallows the use of more than one field for doc-values extraction in the same spatial relation function.
+         * This is because comparing two doc-values fields is not supported in the current implementation.
+         */
+        private boolean allowedForDocValues(FieldAttribute fieldAttribute, AggregateExec agg, Set<FieldAttribute> foundAttributes) {
+            var candidateDocValuesAttributes = new HashSet<>(foundAttributes);
+            candidateDocValuesAttributes.add(fieldAttribute);
+            var spatialRelatesAttributes = new HashSet<FieldAttribute>();
+            agg.forEachExpressionDown(SpatialRelatesFunction.class, relatesFunction -> {
+                candidateDocValuesAttributes.forEach(candidate -> {
+                    if (relatesFunction.hasFieldAttribute(Set.of(candidate))) {
+                        spatialRelatesAttributes.add(candidate);
+                    }
+                });
+            });
+            // Disallow more than one spatial field to be extracted using doc-values (for now)
+            return spatialRelatesAttributes.size() < 2;
+        }
     }
 }

+ 66 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlExpressionTranslators.java

@@ -7,9 +7,11 @@
 
 package org.elasticsearch.xpack.esql.planner;
 
+import org.apache.lucene.document.ShapeField;
 import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.common.lucene.BytesRefs;
 import org.elasticsearch.common.time.DateFormatter;
+import org.elasticsearch.geometry.Geometry;
 import org.elasticsearch.search.DocValueFormat;
 import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.Equals;
 import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.GreaterThan;
@@ -19,7 +21,10 @@ import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.Less
 import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.LessThanOrEqual;
 import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.NotEquals;
 import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesFunction;
+import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils;
 import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.NullEquals;
+import org.elasticsearch.xpack.esql.querydsl.query.SpatialRelatesQuery;
 import org.elasticsearch.xpack.ql.QlIllegalArgumentException;
 import org.elasticsearch.xpack.ql.expression.Expression;
 import org.elasticsearch.xpack.ql.expression.Expressions;
@@ -50,6 +55,7 @@ import java.time.ZonedDateTime;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Supplier;
 
 import static org.elasticsearch.xpack.ql.type.DataTypes.IP;
 import static org.elasticsearch.xpack.ql.type.DataTypes.UNSIGNED_LONG;
@@ -61,6 +67,7 @@ public final class EsqlExpressionTranslators {
     public static final List<ExpressionTranslator<?>> QUERY_TRANSLATORS = List.of(
         new EqualsIgnoreCaseTranslator(),
         new BinaryComparisons(),
+        new SpatialRelatesTranslator(),
         new ExpressionTranslators.Ranges(),
         new ExpressionTranslators.BinaryLogic(),
         new ExpressionTranslators.IsNulls(),
@@ -348,4 +355,63 @@ public final class EsqlExpressionTranslators {
             return ExpressionTranslators.Scalars.doTranslate(f, handler);
         }
     }
+
+    public static class SpatialRelatesTranslator extends ExpressionTranslator<SpatialRelatesFunction> {
+
+        @Override
+        protected Query asQuery(SpatialRelatesFunction bc, TranslatorHandler handler) {
+            return doTranslate(bc, handler);
+        }
+
+        public static void checkSpatialRelatesFunction(Expression constantExpression, ShapeField.QueryRelation queryRelation) {
+            Check.isTrue(
+                constantExpression.foldable(),
+                "Line {}:{}: Comparisons against fields are not (currently) supported; offender [{}] in [ST_{}]",
+                constantExpression.sourceLocation().getLineNumber(),
+                constantExpression.sourceLocation().getColumnNumber(),
+                Expressions.name(constantExpression),
+                queryRelation
+            );
+        }
+
+        /**
+         * We should normally be using the real `wrapFunctionQuery` above, so we get the benefits of `SingleValueQuery`,
+         * but at the moment `SingleValueQuery` makes use of `SortDocValues` to determine if the results are single or multi-valued,
+         * and LeafShapeFieldData does not support `SortedBinaryDocValues getBytesValues()`.
+         * Skipping this code path entirely is a temporary workaround while separate work is being done to simplify `SingleValueQuery`
+         * to rather rely on a new method on `LeafFieldData`. This is both for the benefit of the spatial queries, as well as an
+         * improvement overall.
+         * TODO: Remove this method and call the parent method once the SingleValueQuery improvements have been made
+         */
+        public static Query wrapFunctionQuery(Expression field, Supplier<Query> querySupplier) {
+            return ExpressionTranslator.wrapIfNested(querySupplier.get(), field);
+        }
+
+        public static Query doTranslate(SpatialRelatesFunction bc, TranslatorHandler handler) {
+            if (bc.left().foldable()) {
+                checkSpatialRelatesFunction(bc.left(), bc.queryRelation());
+                return wrapFunctionQuery(bc.right(), () -> translate(bc, handler, bc.right(), bc.left()));
+            } else {
+                checkSpatialRelatesFunction(bc.right(), bc.queryRelation());
+                return wrapFunctionQuery(bc.left(), () -> translate(bc, handler, bc.left(), bc.right()));
+            }
+        }
+
+        static Query translate(
+            SpatialRelatesFunction bc,
+            TranslatorHandler handler,
+            Expression spatialExpression,
+            Expression constantExpression
+        ) {
+            TypedAttribute attribute = checkIsPushableAttribute(spatialExpression);
+            String name = handler.nameOf(attribute);
+
+            try {
+                Geometry shape = SpatialRelatesUtils.makeGeometryFromLiteral(constantExpression);
+                return new SpatialRelatesQuery(bc.source(), name, bc.queryRelation(), shape, attribute.dataType());
+            } catch (IllegalArgumentException e) {
+                throw new QlIllegalArgumentException(e.getMessage(), e);
+            }
+        }
+    }
 }

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

@@ -0,0 +1,287 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.querydsl.query;
+
+import org.apache.lucene.document.ShapeField;
+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.ConstantScoreQuery;
+import org.apache.lucene.search.IndexOrDocValuesQuery;
+import org.apache.lucene.search.MatchNoDocsQuery;
+import org.elasticsearch.TransportVersion;
+import org.elasticsearch.common.geo.LuceneGeometriesUtils;
+import org.elasticsearch.common.geo.ShapeRelation;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.geometry.Geometry;
+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.QueryBuilder;
+import org.elasticsearch.index.query.QueryShardException;
+import org.elasticsearch.index.query.SearchExecutionContext;
+import org.elasticsearch.lucene.spatial.CartesianShapeDocValuesQuery;
+import org.elasticsearch.search.sort.NestedSortBuilder;
+import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
+import org.elasticsearch.xpack.ql.querydsl.query.Query;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.CARTESIAN_POINT;
+
+public class SpatialRelatesQuery extends Query {
+    private final String field;
+    private final ShapeField.QueryRelation queryRelation;
+    private final Geometry shape;
+    private final DataType dataType;
+
+    public SpatialRelatesQuery(Source source, String field, ShapeField.QueryRelation queryRelation, Geometry shape, DataType dataType) {
+        super(source);
+        this.field = field;
+        this.queryRelation = queryRelation;
+        this.shape = shape;
+        this.dataType = dataType;
+    }
+
+    @Override
+    public boolean containsNestedField(String path, String field) {
+        return false;
+    }
+
+    @Override
+    public Query addNestedField(String path, String field, String format, boolean hasDocValues) {
+        return null;
+    }
+
+    @Override
+    public void enrichNestedSort(NestedSortBuilder sort) {
+
+    }
+
+    @Override
+    public QueryBuilder asBuilder() {
+        return EsqlDataTypes.isSpatialGeo(dataType) ? new GeoShapeQueryBuilder() : new CartesianShapeQueryBuilder();
+    }
+
+    @Override
+    protected String innerToString() {
+        throw new IllegalArgumentException("SpatialRelatesQuery.innerToString() not implemented");
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(field, queryRelation, shape, dataType);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+
+        SpatialRelatesQuery other = (SpatialRelatesQuery) obj;
+        return Objects.equals(field, other.field)
+            && Objects.equals(queryRelation, other.queryRelation)
+            && Objects.equals(shape, other.shape)
+            && Objects.equals(dataType, other.dataType);
+    }
+
+    public ShapeRelation shapeRelation() {
+        return switch (queryRelation) {
+            case INTERSECTS -> ShapeRelation.INTERSECTS;
+            case DISJOINT -> ShapeRelation.DISJOINT;
+            case WITHIN -> ShapeRelation.WITHIN;
+            case CONTAINS -> ShapeRelation.CONTAINS;
+        };
+    }
+
+    /**
+     * This class is a minimal implementation of the QueryBuilder interface.
+     * We only need the toQuery method, but ESQL makes extensive use of QueryBuilder and trimming that interface down for ESQL only would
+     * be a large undertaking.
+     * Note that this class is only public for testing in PhysicalPlanOptimizerTests.
+     */
+    public abstract class ShapeQueryBuilder implements QueryBuilder {
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            throw new UnsupportedOperationException("Unimplemented: toXContent()");
+        }
+
+        @Override
+        public TransportVersion getMinimalSupportedVersion() {
+            throw new UnsupportedOperationException("Unimplemented: toXContent()");
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            throw new UnsupportedOperationException("Unimplemented: toXContent()");
+        }
+
+        @Override
+        public org.apache.lucene.search.Query toQuery(SearchExecutionContext context) throws IOException {
+            final MappedFieldType fieldType = context.getFieldType(field);
+            if (fieldType == null) {
+                throw new QueryShardException(context, "failed to find type for field [" + field + "]");
+            }
+            return buildShapeQuery(context, fieldType);
+        }
+
+        abstract org.apache.lucene.search.Query buildShapeQuery(SearchExecutionContext context, MappedFieldType fieldType);
+
+        @Override
+        public QueryBuilder queryName(String queryName) {
+            throw new UnsupportedOperationException("Unimplemented: String");
+        }
+
+        @Override
+        public String queryName() {
+            throw new UnsupportedOperationException("Unimplemented: queryName");
+        }
+
+        @Override
+        public float boost() {
+            return 0;
+        }
+
+        @Override
+        public QueryBuilder boost(float boost) {
+            throw new UnsupportedOperationException("Unimplemented: float");
+        }
+
+        @Override
+        public String getName() {
+            throw new UnsupportedOperationException("Unimplemented: getName");
+        }
+
+        /** Public for testing */
+        public String fieldName() {
+            return field;
+        }
+
+        /** Public for testing */
+        public ShapeRelation relation() {
+            return shapeRelation();
+        }
+
+        /** Public for testing */
+        public Geometry shape() {
+            return shape;
+        }
+    }
+
+    private class GeoShapeQueryBuilder extends ShapeQueryBuilder {
+        public final String NAME = "geo_shape";
+
+        @Override
+        public String getWriteableName() {
+            return "GeoShapeQueryBuilder";
+        }
+
+        @Override
+        org.apache.lucene.search.Query buildShapeQuery(SearchExecutionContext context, MappedFieldType fieldType) {
+            if ((fieldType instanceof GeoShapeQueryable) == false) {
+                throw new QueryShardException(
+                    context,
+                    "Field [" + field + "] is of unsupported type [" + fieldType.typeName() + "] for [" + NAME + "] query"
+                );
+            }
+            final GeoShapeQueryable ft = (GeoShapeQueryable) fieldType;
+            return new ConstantScoreQuery(ft.geoShapeQuery(context, fieldType.name(), shapeRelation(), shape));
+        }
+    }
+
+    private class CartesianShapeQueryBuilder extends ShapeQueryBuilder {
+        @Override
+        public String getWriteableName() {
+            return "CartesianShapeQueryBuilder";
+        }
+
+        @Override
+        org.apache.lucene.search.Query buildShapeQuery(SearchExecutionContext context, MappedFieldType fieldType) {
+            org.apache.lucene.search.Query innerQuery = dataType == CARTESIAN_POINT
+                ? pointShapeQuery(shape, fieldType.name(), queryRelation, context)
+                : shapeShapeQuery(shape, fieldType.name(), queryRelation, context);
+            return new ConstantScoreQuery(innerQuery);
+        }
+
+        /**
+         * This code is based on the ShapeQueryPointProcessor.shapeQuery() method
+         */
+        private static org.apache.lucene.search.Query pointShapeQuery(
+            Geometry geometry,
+            String fieldName,
+            ShapeField.QueryRelation relation,
+            SearchExecutionContext context
+        ) {
+            final boolean hasDocValues = context.getFieldType(fieldName).hasDocValues();
+            // only the intersects relation is supported for indexed cartesian point types
+            if (relation != ShapeField.QueryRelation.INTERSECTS) {
+                throw new QueryShardException(context, relation + " query relation not supported for Field [" + fieldName + "].");
+            }
+            final Consumer<ShapeType> checker = t -> {
+                if (t == ShapeType.POINT || t == ShapeType.MULTIPOINT || t == ShapeType.LINESTRING || t == ShapeType.MULTILINESTRING) {
+                    throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + t + " queries");
+                }
+            };
+            final XYGeometry[] luceneGeometries = LuceneGeometriesUtils.toXYGeometry(geometry, checker);
+            org.apache.lucene.search.Query query = XYPointField.newGeometryQuery(fieldName, luceneGeometries);
+            if (hasDocValues) {
+                final org.apache.lucene.search.Query queryDocValues = XYDocValuesField.newSlowGeometryQuery(fieldName, luceneGeometries);
+                query = new IndexOrDocValuesQuery(query, queryDocValues);
+            }
+            return query;
+        }
+
+        /**
+         * This code is based on the ShapeQueryProcessor.shapeQuery() method
+         */
+        private static org.apache.lucene.search.Query shapeShapeQuery(
+            Geometry geometry,
+            String fieldName,
+            ShapeField.QueryRelation relation,
+            SearchExecutionContext context
+        ) {
+            final boolean hasDocValues = context.getFieldType(fieldName).hasDocValues();
+            // CONTAINS queries are not supported by VECTOR strategy for indices created before version 7.5.0 (Lucene 8.3.0);
+            if (relation == ShapeField.QueryRelation.CONTAINS && context.indexVersionCreated().before(IndexVersions.V_7_5_0)) {
+                throw new QueryShardException(context, relation + " query relation not supported for Field [" + fieldName + "].");
+            }
+            if (geometry == null || geometry.isEmpty()) {
+                return new MatchNoDocsQuery();
+            }
+            final XYGeometry[] luceneGeometries;
+            try {
+                luceneGeometries = LuceneGeometriesUtils.toXYGeometry(geometry, t -> {});
+            } catch (IllegalArgumentException e) {
+                throw new QueryShardException(context, "Exception creating query on Field [" + fieldName + "] " + e.getMessage(), e);
+            }
+            org.apache.lucene.search.Query query = XYShape.newGeometryQuery(fieldName, relation, luceneGeometries);
+            if (hasDocValues) {
+                final org.apache.lucene.search.Query queryDocValues = new CartesianShapeDocValuesQuery(
+                    fieldName,
+                    relation,
+                    luceneGeometries
+                );
+                query = new IndexOrDocValuesQuery(query, queryDocValues);
+            }
+            return query;
+        }
+    }
+}

+ 8 - 4
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypes.java

@@ -45,10 +45,10 @@ public final class EsqlDataTypes {
 
     public static final DataType DATE_PERIOD = new DataType("DATE_PERIOD", null, 3 * Integer.BYTES, false, false, false);
     public static final DataType TIME_DURATION = new DataType("TIME_DURATION", null, Integer.BYTES + Long.BYTES, false, false, false);
-    public static final DataType GEO_POINT = new DataType("geo_point", Double.BYTES * 2, false, false, false);
-    public static final DataType CARTESIAN_POINT = new DataType("cartesian_point", Double.BYTES * 2, false, false, false);
-    public static final DataType GEO_SHAPE = new DataType("geo_shape", Integer.MAX_VALUE, false, false, false);
-    public static final DataType CARTESIAN_SHAPE = new DataType("cartesian_shape", Integer.MAX_VALUE, false, false, false);
+    public static final DataType GEO_POINT = new DataType("geo_point", Double.BYTES * 2, false, false, true);
+    public static final DataType CARTESIAN_POINT = new DataType("cartesian_point", Double.BYTES * 2, false, false, true);
+    public static final DataType GEO_SHAPE = new DataType("geo_shape", Integer.MAX_VALUE, false, false, true);
+    public static final DataType CARTESIAN_SHAPE = new DataType("cartesian_shape", Integer.MAX_VALUE, false, false, true);
 
     private static final Collection<DataType> TYPES = Stream.of(
         BOOLEAN,
@@ -175,6 +175,10 @@ public final class EsqlDataTypes {
         return t == GEO_POINT || t == CARTESIAN_POINT || t == GEO_SHAPE || t == CARTESIAN_SHAPE;
     }
 
+    public static boolean isSpatialGeo(DataType t) {
+        return t == GEO_POINT || t == GEO_SHAPE;
+    }
+
     public static boolean isSpatialPoint(DataType t) {
         return t == GEO_POINT || t == CARTESIAN_POINT;
     }

+ 40 - 8
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java

@@ -214,6 +214,16 @@ public record TestCaseSupplier(String name, List<DataType> types, Supplier<TestC
         }
     }
 
+    public static TestCaseSupplier testCaseSupplier(
+        TypedDataSupplier lhsSupplier,
+        TypedDataSupplier rhsSupplier,
+        BiFunction<DataType, DataType, String> evaluatorToString,
+        DataType expectedType,
+        BinaryOperator<Object> expectedValue
+    ) {
+        return testCaseSupplier(lhsSupplier, rhsSupplier, evaluatorToString, expectedType, expectedValue, List.of());
+    }
+
     private static TestCaseSupplier testCaseSupplier(
         TypedDataSupplier lhsSupplier,
         TypedDataSupplier rhsSupplier,
@@ -938,31 +948,53 @@ public record TestCaseSupplier(String name, List<DataType> types, Supplier<TestC
         );
     }
 
-    private static List<TypedDataSupplier> geoPointCases() {
-        return List.of(new TypedDataSupplier("<geo_point>", () -> GEO.asWkb(GeometryTestUtils.randomPoint()), EsqlDataTypes.GEO_POINT));
+    public static List<TypedDataSupplier> geoPointCases() {
+        return geoPointCases(ESTestCase::randomBoolean);
     }
 
-    private static List<TypedDataSupplier> cartesianPointCases() {
+    public static List<TypedDataSupplier> cartesianPointCases() {
+        return cartesianPointCases(ESTestCase::randomBoolean);
+    }
+
+    public static List<TypedDataSupplier> geoShapeCases() {
+        return geoShapeCases(ESTestCase::randomBoolean);
+    }
+
+    public static List<TypedDataSupplier> cartesianShapeCases() {
+        return cartesianShapeCases(ESTestCase::randomBoolean);
+    }
+
+    public static List<TypedDataSupplier> geoPointCases(Supplier<Boolean> hasAlt) {
         return List.of(
-            new TypedDataSupplier("<cartesian_point>", () -> CARTESIAN.asWkb(ShapeTestUtils.randomPoint()), EsqlDataTypes.CARTESIAN_POINT)
+            new TypedDataSupplier("<geo_point>", () -> GEO.asWkb(GeometryTestUtils.randomPoint(hasAlt.get())), EsqlDataTypes.GEO_POINT)
+        );
+    }
+
+    public static List<TypedDataSupplier> cartesianPointCases(Supplier<Boolean> hasAlt) {
+        return List.of(
+            new TypedDataSupplier(
+                "<cartesian_point>",
+                () -> CARTESIAN.asWkb(ShapeTestUtils.randomPoint(hasAlt.get())),
+                EsqlDataTypes.CARTESIAN_POINT
+            )
         );
     }
 
-    private static List<TypedDataSupplier> geoShapeCases() {
+    public static List<TypedDataSupplier> geoShapeCases(Supplier<Boolean> hasAlt) {
         return List.of(
             new TypedDataSupplier(
                 "<geo_shape>",
-                () -> GEO.asWkb(GeometryTestUtils.randomGeometry(ESTestCase.randomBoolean())),
+                () -> GEO.asWkb(GeometryTestUtils.randomGeometryWithoutCircle(0, hasAlt.get())),
                 EsqlDataTypes.GEO_SHAPE
             )
         );
     }
 
-    private static List<TypedDataSupplier> cartesianShapeCases() {
+    public static List<TypedDataSupplier> cartesianShapeCases(Supplier<Boolean> hasAlt) {
         return List.of(
             new TypedDataSupplier(
                 "<cartesian_shape>",
-                () -> CARTESIAN.asWkb(ShapeTestUtils.randomGeometry(ESTestCase.randomBoolean())),
+                () -> CARTESIAN.asWkb(ShapeTestUtils.randomGeometry(hasAlt.get())),
                 EsqlDataTypes.CARTESIAN_SHAPE
             )
         );

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

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

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 703 - 75
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java


+ 12 - 0
x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/util/SpatialCoordinateTypes.java

@@ -108,7 +108,19 @@ public enum SpatialCoordinateTypes {
         }
     }
 
+    public Geometry wktToGeometry(String wkt) {
+        try {
+            return WellKnownText.fromWKT(GeometryValidator.NOOP, false, wkt);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("Failed to parse WKT: " + e.getMessage(), e);
+        }
+    }
+
     public String wkbToWkt(BytesRef wkb) {
         return WellKnownText.fromWKB(wkb.bytes, wkb.offset, wkb.length);
     }
+
+    public Geometry wkbToGeometry(BytesRef wkb) {
+        return WellKnownBinary.fromWKB(GeometryValidator.NOOP, false, wkb.bytes, wkb.offset, wkb.length);
+    }
 }

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.