Просмотр исходного кода

Add two new OGC functions ST_X and ST_Y (#105768)

* Add two new OGC functions ST_X and ST_Y

Recently Nik did work that involved extracting the X and Y coordinates from geo_point data using `to_string(field)` followed by a DISSECT command to re-parse the string to get the X and Y coordinates.

This is much more efficiently achieved using existing known OGC functions `ST_X` and `ST_Y`.

* Update docs/changelog/105768.yaml

* Fixed invalid changelog yaml

* Fixed mixed cluster tests

* Fixed tests and added docs

* Removed false impression that these functions were different for geo/cartesian

With the use of WKB as the core type in the compute engine, many spatial functions are actually the same between these two types, so we should not give the impression they are different.

* Code review comments and reduced object creation.

* Revert temporary StringUtils hack, and fix bug in x/y extraction from WKB

* Revert object creation reduction

* Fixed mistakes in documentation
Craig Taverner 1 год назад
Родитель
Сommit
8d93a934f6
21 измененных файлов с 683 добавлено и 5 удалено
  1. 5 0
      docs/changelog/105768.yaml
  2. 4 0
      docs/reference/esql/esql-functions-operators.asciidoc
  3. 1 0
      docs/reference/esql/functions/signature/st_x.svg
  4. 1 0
      docs/reference/esql/functions/signature/st_y.svg
  5. 16 0
      docs/reference/esql/functions/spatial-functions.asciidoc
  6. 33 0
      docs/reference/esql/functions/st_x.asciidoc
  7. 33 0
      docs/reference/esql/functions/st_y.asciidoc
  8. 6 0
      docs/reference/esql/functions/types/st_x.asciidoc
  9. 6 0
      docs/reference/esql/functions/types/st_y.asciidoc
  10. 7 3
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/show.csv-spec
  11. 44 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec
  12. 127 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXFromWKBEvaluator.java
  13. 127 0
      x-pack/plugin/esql/src/main/java/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYFromWKBEvaluator.java
  14. 4 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  15. 73 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StX.java
  16. 73 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StY.java
  17. 6 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java
  18. 2 1
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java
  19. 50 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXTests.java
  20. 50 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYTests.java
  21. 15 1
      x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/util/SpatialCoordinateTypes.java

+ 5 - 0
docs/changelog/105768.yaml

@@ -0,0 +1,5 @@
+pr: 105768
+summary: Add two new OGC functions ST_X and ST_Y
+area: "ES|QL"
+type: enhancement
+issues: []

+ 4 - 0
docs/reference/esql/esql-functions-operators.asciidoc

@@ -21,6 +21,9 @@ include::functions/string-functions.asciidoc[tag=string_list]
 <<esql-date-time-functions>>::
 include::functions/date-time-functions.asciidoc[tag=date_list]
 
+<<esql-spatial-functions>>::
+include::functions/spatial-functions.asciidoc[tag=spatial_list]
+
 <<esql-type-conversion-functions>>::
 include::functions/type-conversion-functions.asciidoc[tag=type_list]
 
@@ -37,6 +40,7 @@ include::functions/aggregation-functions.asciidoc[]
 include::functions/math-functions.asciidoc[]
 include::functions/string-functions.asciidoc[]
 include::functions/date-time-functions.asciidoc[]
+include::functions/spatial-functions.asciidoc[]
 include::functions/type-conversion-functions.asciidoc[]
 include::functions/conditional-functions-and-expressions.asciidoc[]
 include::functions/mv-functions.asciidoc[]

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

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="252" height="46" viewbox="0 0 252 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 31h5m68 0h10m32 0h10m80 0h10m32 0h5"/><rect class="s" x="5" y="5" width="68" height="36"/><text class="k" x="15" y="31">ST_X</text><rect class="s" x="83" y="5" width="32" height="36" rx="7"/><text class="syn" x="93" y="31">(</text><rect class="s" x="125" y="5" width="80" height="36" rx="7"/><text class="k" x="135" y="31">point</text><rect class="s" x="215" y="5" width="32" height="36" rx="7"/><text class="syn" x="225" y="31">)</text></svg>

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

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="252" height="46" viewbox="0 0 252 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 31h5m68 0h10m32 0h10m80 0h10m32 0h5"/><rect class="s" x="5" y="5" width="68" height="36"/><text class="k" x="15" y="31">ST_Y</text><rect class="s" x="83" y="5" width="32" height="36" rx="7"/><text class="syn" x="93" y="31">(</text><rect class="s" x="125" y="5" width="80" height="36" rx="7"/><text class="k" x="135" y="31">point</text><rect class="s" x="215" y="5" width="32" height="36" rx="7"/><text class="syn" x="225" y="31">)</text></svg>

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

@@ -0,0 +1,16 @@
+[[esql-spatial-functions]]
+==== {esql} spatial functions
+
+++++
+<titleabbrev>Spatial functions</titleabbrev>
+++++
+
+{esql} supports these spatial functions:
+
+// tag::spatial_list[]
+* <<esql-st_x>>
+* <<esql-st_y>>
+// end::spatial_list[]
+
+include::st_x.asciidoc[]
+include::st_y.asciidoc[]

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

@@ -0,0 +1,33 @@
+[discrete]
+[[esql-st_x]]
+=== `ST_X`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_x.svg[Embedded,opts=inline]
+
+*Parameters*
+
+`point`::
+Expression of type `geo_point` or `cartesian_point`. If `null`, the function returns `null`.
+
+*Description*
+
+Extracts the `x` coordinate from the supplied point.
+If the points is of type `geo_point` this is equivalent to extracting the `longitude` value.
+
+*Supported types*
+
+include::types/st_x.asciidoc[]
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/spatial.csv-spec[tag=st_x_y]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/spatial.csv-spec[tag=st_x_y-result]
+|===

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

@@ -0,0 +1,33 @@
+[discrete]
+[[esql-st_y]]
+=== `ST_Y`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/st_y.svg[Embedded,opts=inline]
+
+*Parameters*
+
+`point`::
+Expression of type `geo_point` or `cartesian_point`. If `null`, the function returns `null`.
+
+*Description*
+
+Extracts the `y` coordinate from the supplied point.
+If the points is of type `geo_point` this is equivalent to extracting the `latitude` value.
+
+*Supported types*
+
+include::types/st_y.asciidoc[]
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/spatial.csv-spec[tag=st_x_y]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/spatial.csv-spec[tag=st_x_y-result]
+|===

+ 6 - 0
docs/reference/esql/functions/types/st_x.asciidoc

@@ -0,0 +1,6 @@
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+point | result
+cartesian_point | double
+geo_point | double
+|===

+ 6 - 0
docs/reference/esql/functions/types/st_y.asciidoc

@@ -0,0 +1,6 @@
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+point | result
+cartesian_point | double
+geo_point | double
+|===

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

@@ -68,6 +68,8 @@ 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_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
 substring                |"keyword substring(str:keyword|text, start:integer, ?length:integer)"                     |[str, start, length]       |["keyword|text", "integer", "integer"]         |["", "", ""]                                        |keyword                    | "Returns a substring of a string, specified by a start position and an optional length"                      | [false, false, true]| false | false
 sum                      |"long sum(field:double|integer|long)"                                          |field                     |"double|integer|long"                 | ""                                                 |long                    | "The sum of a numeric field."                      | false                | false | true
@@ -103,7 +105,7 @@ trim                     |"keyword|text trim(str:keyword|text)"
 ;
 
 
-showFunctionsSynopsis#[skip:-8.12.99]
+showFunctionsSynopsis#[skip:-8.13.99]
 show functions | keep synopsis;
 
 synopsis:keyword
@@ -165,6 +167,8 @@ 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)"
+"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)"
 "keyword substring(str:keyword|text, start:integer, ?length:integer)"
 "long sum(field:double|integer|long)"
@@ -216,9 +220,9 @@ sinh                     | "double sinh(n:double|integer|long|unsigned_long)"
 
 
 // see https://github.com/elastic/elasticsearch/issues/102120
-countFunctions#[skip:-8.12.99]
+countFunctions#[skip:-8.13.99]
 show functions |  stats  a = count(*), b = count(*), c = count(*) |  mv_expand c;
 
 a:long | b:long | c:long
-90     | 90     | 90
+92     | 92     | 92
 ;

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

@@ -69,6 +69,30 @@ c:geo_point
 POINT(39.58327988510707 20.619513023697994)
 ;
 
+centroidFromString4#[skip:-8.13.99, reason:st_x and st_y added in 8.14]
+ROW wkt = ["POINT(42.97109629958868 14.7552534006536)", "POINT(75.80929149873555 22.72774917539209)", "POINT(-0.030548143003023033 24.37553649504829)"]
+| MV_EXPAND wkt
+| EVAL pt = TO_GEOPOINT(wkt)
+| STATS c = ST_CENTROID(pt)
+| EVAL x = ST_X(c), y = ST_Y(c);
+
+c:geo_point                                 | x:double          | y:double
+POINT(39.58327988510707 20.619513023697994) | 39.58327988510707 | 20.619513023697994
+;
+
+stXFromString#[skip:-8.13.99, reason:st_x and st_y added in 8.14]
+// tag::st_x_y[]
+ROW point = TO_GEOPOINT("POINT(42.97109629958868 14.7552534006536)")
+| EVAL x =  ST_X(point), y = ST_Y(point)
+// end::st_x_y[]
+;
+
+// tag::st_x_y-result[]
+point:geo_point                            | x:double          | y:double
+POINT(42.97109629958868 14.7552534006536)  | 42.97109629958868 | 14.7552534006536
+// end::st_x_y-result[]
+;
+
 simpleLoad#[skip:-8.12.99, reason:spatial type geo_point improved precision in 8.13]
 FROM airports | WHERE scalerank == 9 | SORT abbrev | WHERE length(name) > 12;
 
@@ -87,6 +111,17 @@ WIIT            |  Bandar Lampung  |  POINT(105.2667 -5.45)   |  Indonesia
 ZAH             |  Zāhedān         |  POINT(60.8628 29.4964)  |  Iran             |  POINT(60.900708564915 29.4752941956573)    |  Zahedan Int'l                |  9            |  mid 
 ;
 
+stXFromAirportsSupportsNull#[skip:-8.13.99, reason:st_x and st_y added in 8.14]
+FROM airports
+| EVAL x = FLOOR(ABS(ST_X(city_location))/200), y = FLOOR(ABS(ST_Y(city_location))/100)
+| STATS c = count(*) BY x, y
+;
+
+c:long  | x:double  | y:double
+872     | 0.0       | 0.0
+19      | null      | null
+;
+
 centroidFromAirports#[skip:-8.12.99, reason:st_centroid added in 8.13]
 // tag::st_centroid-airports[]
 FROM airports
@@ -399,6 +434,15 @@ c:cartesian_point
 POINT(3949.163965353159 1078.2645465797348)
 ;
 
+stXFromCartesianString#[skip:-8.13.99, reason:st_x and st_y added in 8.14]
+ROW point = TO_CARTESIANPOINT("POINT(4297.10986328125 -1475.530029296875)")
+| EVAL x =  ST_X(point), y = ST_Y(point)
+;
+
+point:cartesian_point                       | x:double          | y:double
+POINT(4297.10986328125 -1475.530029296875)  | 4297.10986328125 | -1475.530029296875
+;
+
 simpleCartesianLoad#[skip:-8.12.99, reason:spatial type cartesian_point improved precision in 8.13]
 FROM airports_web | WHERE scalerank == 9 | SORT abbrev | WHERE length(name) > 12;
 

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

@@ -0,0 +1,127 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StX}.
+ * This class is generated. Do not edit it.
+ */
+public final class StXFromWKBEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+  public StXFromWKBEvaluator(EvalOperator.ExpressionEvaluator field, Source source,
+      DriverContext driverContext) {
+    super(driverContext, field, source);
+  }
+
+  @Override
+  public String name() {
+    return "StXFromWKB";
+  }
+
+  @Override
+  public Block evalVector(Vector v) {
+    BytesRefVector vector = (BytesRefVector) v;
+    int positionCount = v.getPositionCount();
+    BytesRef scratchPad = new BytesRef();
+    if (vector.isConstant()) {
+      try {
+        return driverContext.blockFactory().newConstantDoubleBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+      } catch (IllegalArgumentException  e) {
+        registerException(e);
+        return driverContext.blockFactory().newConstantNullBlock(positionCount);
+      }
+    }
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      for (int p = 0; p < positionCount; p++) {
+        try {
+          builder.appendDouble(evalValue(vector, p, scratchPad));
+        } catch (IllegalArgumentException  e) {
+          registerException(e);
+          builder.appendNull();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StX.fromWellKnownBinary(value);
+  }
+
+  @Override
+  public Block evalBlock(Block b) {
+    BytesRefBlock block = (BytesRefBlock) b;
+    int positionCount = block.getPositionCount();
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      BytesRef scratchPad = new BytesRef();
+      for (int p = 0; p < positionCount; p++) {
+        int valueCount = block.getValueCount(p);
+        int start = block.getFirstValueIndex(p);
+        int end = start + valueCount;
+        boolean positionOpened = false;
+        boolean valuesAppended = false;
+        for (int i = start; i < end; i++) {
+          try {
+            double value = evalValue(block, i, scratchPad);
+            if (positionOpened == false && valueCount > 1) {
+              builder.beginPositionEntry();
+              positionOpened = true;
+            }
+            builder.appendDouble(value);
+            valuesAppended = true;
+          } catch (IllegalArgumentException  e) {
+            registerException(e);
+          }
+        }
+        if (valuesAppended == false) {
+          builder.appendNull();
+        } else if (positionOpened) {
+          builder.endPositionEntry();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StX.fromWellKnownBinary(value);
+  }
+
+  public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    public Factory(EvalOperator.ExpressionEvaluator.Factory field, Source source) {
+      this.field = field;
+      this.source = source;
+    }
+
+    @Override
+    public StXFromWKBEvaluator get(DriverContext context) {
+      return new StXFromWKBEvaluator(field.get(context), source, context);
+    }
+
+    @Override
+    public String toString() {
+      return "StXFromWKBEvaluator[field=" + field + "]";
+    }
+  }
+}

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

@@ -0,0 +1,127 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.DoubleBlock;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StY}.
+ * This class is generated. Do not edit it.
+ */
+public final class StYFromWKBEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+  public StYFromWKBEvaluator(EvalOperator.ExpressionEvaluator field, Source source,
+      DriverContext driverContext) {
+    super(driverContext, field, source);
+  }
+
+  @Override
+  public String name() {
+    return "StYFromWKB";
+  }
+
+  @Override
+  public Block evalVector(Vector v) {
+    BytesRefVector vector = (BytesRefVector) v;
+    int positionCount = v.getPositionCount();
+    BytesRef scratchPad = new BytesRef();
+    if (vector.isConstant()) {
+      try {
+        return driverContext.blockFactory().newConstantDoubleBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+      } catch (IllegalArgumentException  e) {
+        registerException(e);
+        return driverContext.blockFactory().newConstantNullBlock(positionCount);
+      }
+    }
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      for (int p = 0; p < positionCount; p++) {
+        try {
+          builder.appendDouble(evalValue(vector, p, scratchPad));
+        } catch (IllegalArgumentException  e) {
+          registerException(e);
+          builder.appendNull();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StY.fromWellKnownBinary(value);
+  }
+
+  @Override
+  public Block evalBlock(Block b) {
+    BytesRefBlock block = (BytesRefBlock) b;
+    int positionCount = block.getPositionCount();
+    try (DoubleBlock.Builder builder = driverContext.blockFactory().newDoubleBlockBuilder(positionCount)) {
+      BytesRef scratchPad = new BytesRef();
+      for (int p = 0; p < positionCount; p++) {
+        int valueCount = block.getValueCount(p);
+        int start = block.getFirstValueIndex(p);
+        int end = start + valueCount;
+        boolean positionOpened = false;
+        boolean valuesAppended = false;
+        for (int i = start; i < end; i++) {
+          try {
+            double value = evalValue(block, i, scratchPad);
+            if (positionOpened == false && valueCount > 1) {
+              builder.beginPositionEntry();
+              positionOpened = true;
+            }
+            builder.appendDouble(value);
+            valuesAppended = true;
+          } catch (IllegalArgumentException  e) {
+            registerException(e);
+          }
+        }
+        if (valuesAppended == false) {
+          builder.appendNull();
+        } else if (positionOpened) {
+          builder.endPositionEntry();
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static double evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+    BytesRef value = container.getBytesRef(index, scratchPad);
+    return StY.fromWellKnownBinary(value);
+  }
+
+  public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    public Factory(EvalOperator.ExpressionEvaluator.Factory field, Source source) {
+      this.field = field;
+      this.source = source;
+    }
+
+    @Override
+    public StYFromWKBEvaluator get(DriverContext context) {
+      return new StYFromWKBEvaluator(field.get(context), source, context);
+    }
+
+    @Override
+    public String toString() {
+      return "StYFromWKBEvaluator[field=" + field + "]";
+    }
+  }
+}

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

@@ -75,6 +75,8 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMedi
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSum;
 import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
+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;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.EndsWith;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.LTrim;
@@ -174,6 +176,8 @@ public final class EsqlFunctionRegistry extends FunctionRegistry {
                 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") },
             // conditional
             new FunctionDefinition[] { def(Case.class, Case::new, "case") },
             // null

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

@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.ann.ConvertEvaluator;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.esql.expression.function.scalar.UnaryScalarFunction;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.TypeResolutions;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+
+import java.util.List;
+import java.util.function.Function;
+
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isSpatialPoint;
+import static org.elasticsearch.xpack.ql.type.DataTypes.DOUBLE;
+import static org.elasticsearch.xpack.ql.util.SpatialCoordinateTypes.UNSPECIFIED;
+
+/**
+ * Extracts the x-coordinate from a point geometry.
+ * For cartesian geometries, the x-coordinate is the first coordinate.
+ * For geographic geometries, the x-coordinate is the longitude.
+ * The function `st_x` is defined in the <a href="https://www.ogc.org/standard/sfs/">OGC Simple Feature Access</a> standard.
+ * Alternatively it is well described in PostGIS documentation at <a href="https://postgis.net/docs/ST_X.html">PostGIS:ST_X</a>.
+ */
+public class StX extends UnaryScalarFunction {
+    @FunctionInfo(returnType = "double", description = "Extracts the x-coordinate from a point geometry.")
+    public StX(Source source, @Param(name = "point", type = { "geo_point", "cartesian_point" }) Expression field) {
+        super(source, field);
+    }
+
+    @Override
+    protected Expression.TypeResolution resolveType() {
+        return isSpatialPoint(field(), sourceText(), TypeResolutions.ParamOrdinal.DEFAULT);
+    }
+
+    @Override
+    public EvalOperator.ExpressionEvaluator.Factory toEvaluator(
+        Function<Expression, EvalOperator.ExpressionEvaluator.Factory> toEvaluator
+    ) {
+        return new StXFromWKBEvaluator.Factory(toEvaluator.apply(field()), source());
+    }
+
+    @Override
+    public DataType dataType() {
+        return DOUBLE;
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new StX(source(), newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, StX::new, field());
+    }
+
+    @ConvertEvaluator(extraName = "FromWKB", warnExceptions = { IllegalArgumentException.class })
+    static double fromWellKnownBinary(BytesRef in) {
+        return UNSPECIFIED.wkbAsPoint(in).getX();
+    }
+}

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

@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.compute.ann.ConvertEvaluator;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.esql.expression.function.scalar.UnaryScalarFunction;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.TypeResolutions;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+
+import java.util.List;
+import java.util.function.Function;
+
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isSpatialPoint;
+import static org.elasticsearch.xpack.ql.type.DataTypes.DOUBLE;
+import static org.elasticsearch.xpack.ql.util.SpatialCoordinateTypes.UNSPECIFIED;
+
+/**
+ * Extracts the y-coordinate from a point geometry.
+ * For cartesian geometries, the y-coordinate is the second coordinate.
+ * For geographic geometries, the y-coordinate is the latitude.
+ * The function `st_y` is defined in the <a href="https://www.ogc.org/standard/sfs/">OGC Simple Feature Access</a> standard.
+ * Alternatively it is well described in PostGIS documentation at <a href="https://postgis.net/docs/ST_Y.html">PostGIS:ST_Y</a>.
+ */
+public class StY extends UnaryScalarFunction {
+    @FunctionInfo(returnType = "double", description = "Extracts the y-coordinate from a point geometry.")
+    public StY(Source source, @Param(name = "point", type = { "geo_point", "cartesian_point" }) Expression field) {
+        super(source, field);
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        return isSpatialPoint(field(), sourceText(), TypeResolutions.ParamOrdinal.DEFAULT);
+    }
+
+    @Override
+    public EvalOperator.ExpressionEvaluator.Factory toEvaluator(
+        Function<Expression, EvalOperator.ExpressionEvaluator.Factory> toEvaluator
+    ) {
+        return new StYFromWKBEvaluator.Factory(toEvaluator.apply(field()), source());
+    }
+
+    @Override
+    public DataType dataType() {
+        return DOUBLE;
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new StY(source(), newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, StY::new, field());
+    }
+
+    @ConvertEvaluator(extraName = "FromWKB", warnExceptions = { IllegalArgumentException.class })
+    static double fromWellKnownBinary(BytesRef in) {
+        return UNSPECIFIED.wkbAsPoint(in).getY();
+    }
+}

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

@@ -99,6 +99,8 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMedi
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMin;
 import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSum;
 import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
+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;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.EndsWith;
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.LTrim;
@@ -340,6 +342,8 @@ public final class PlanNamedTypes {
             of(ESQL_UNARY_SCLR_CLS, Sin.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Sinh.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Sqrt.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
+            of(ESQL_UNARY_SCLR_CLS, StX.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
+            of(ESQL_UNARY_SCLR_CLS, StY.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Tan.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Tanh.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, ToBoolean.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
@@ -1248,6 +1252,8 @@ public final class PlanNamedTypes {
         entry(name(Sin.class), Sin::new),
         entry(name(Sinh.class), Sinh::new),
         entry(name(Sqrt.class), Sqrt::new),
+        entry(name(StX.class), StX::new),
+        entry(name(StY.class), StY::new),
         entry(name(Tan.class), Tan::new),
         entry(name(Tanh.class), Tanh::new),
         entry(name(ToBoolean.class), ToBoolean::new),

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

@@ -967,7 +967,8 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
         Map.entry(
             Set.of(EsqlDataTypes.CARTESIAN_POINT, EsqlDataTypes.CARTESIAN_SHAPE, DataTypes.KEYWORD, DataTypes.TEXT, DataTypes.NULL),
             "cartesian_point or cartesian_shape or string"
-        )
+        ),
+        Map.entry(Set.of(EsqlDataTypes.GEO_POINT, EsqlDataTypes.CARTESIAN_POINT, DataTypes.NULL), "geo_point or cartesian_point")
     );
 
     // TODO: generate this message dynamically, a la AbstractConvertFunction#supportedTypesNames()?

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

@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.xpack.ql.type.DataTypes.DOUBLE;
+import static org.elasticsearch.xpack.ql.util.SpatialCoordinateTypes.UNSPECIFIED;
+
+@FunctionName("st_x")
+public class StXTests extends AbstractFunctionTestCase {
+    public StXTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        String expectedEvaluator = "StXFromWKBEvaluator[field=Attribute[channel=0]]";
+        final List<TestCaseSupplier> suppliers = new ArrayList<>();
+        TestCaseSupplier.forUnaryGeoPoint(suppliers, expectedEvaluator, DOUBLE, StXTests::valueOf, List.of());
+        TestCaseSupplier.forUnaryCartesianPoint(suppliers, expectedEvaluator, DOUBLE, StXTests::valueOf, List.of());
+        return parameterSuppliersFromTypedData(errorsForCasesWithoutExamples(anyNullIsNull(true, suppliers)));
+    }
+
+    private static double valueOf(BytesRef wkb) {
+        return UNSPECIFIED.wkbAsPoint(wkb).getX();
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new StX(source, args.get(0));
+    }
+}

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

@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.xpack.ql.type.DataTypes.DOUBLE;
+import static org.elasticsearch.xpack.ql.util.SpatialCoordinateTypes.UNSPECIFIED;
+
+@FunctionName("st_y")
+public class StYTests extends AbstractFunctionTestCase {
+    public StYTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        String expectedEvaluator = "StYFromWKBEvaluator[field=Attribute[channel=0]]";
+        final List<TestCaseSupplier> suppliers = new ArrayList<>();
+        TestCaseSupplier.forUnaryGeoPoint(suppliers, expectedEvaluator, DOUBLE, StYTests::valueOf, List.of());
+        TestCaseSupplier.forUnaryCartesianPoint(suppliers, expectedEvaluator, DOUBLE, StYTests::valueOf, List.of());
+        return parameterSuppliersFromTypedData(errorsForCasesWithoutExamples(anyNullIsNull(true, suppliers)));
+    }
+
+    private static double valueOf(BytesRef wkb) {
+        return UNSPECIFIED.wkbAsPoint(wkb).getY();
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new StY(source, args.get(0));
+    }
+}

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

@@ -56,6 +56,15 @@ public enum SpatialCoordinateTypes {
             final long yi = XYEncodingUtils.encode((float) y);
             return (yi & 0xFFFFFFFFL) | xi << 32;
         }
+    },
+    UNSPECIFIED {
+        public Point longAsPoint(long encoded) {
+            throw new UnsupportedOperationException("Cannot convert long to point without specifying coordinate type");
+        }
+
+        public long pointAsLong(double x, double y) {
+            throw new UnsupportedOperationException("Cannot convert point to long without specifying coordinate type");
+        }
     };
 
     public abstract Point longAsPoint(long encoded);
@@ -63,9 +72,14 @@ public enum SpatialCoordinateTypes {
     public abstract long pointAsLong(double x, double y);
 
     public long wkbAsLong(BytesRef wkb) {
+        Point point = wkbAsPoint(wkb);
+        return pointAsLong(point.getX(), point.getY());
+    }
+
+    public Point wkbAsPoint(BytesRef wkb) {
         Geometry geometry = WellKnownBinary.fromWKB(GeometryValidator.NOOP, false, wkb.bytes, wkb.offset, wkb.length);
         if (geometry instanceof Point point) {
-            return pointAsLong(point.getX(), point.getY());
+            return point;
         } else {
             throw new IllegalArgumentException("Unsupported geometry: " + geometry.type());
         }