Skip to content

Commit

Permalink
Add two new OGC functions ST_X and ST_Y (#105768)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
craigtaverner authored Mar 6, 2024
1 parent 882b92a commit 8d93a93
Show file tree
Hide file tree
Showing 21 changed files with 683 additions and 5 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/105768.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 105768
summary: Add two new OGC functions ST_X and ST_Y
area: "ES|QL"
type: enhancement
issues: []
4 changes: 4 additions & 0 deletions docs/reference/esql/esql-functions-operators.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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[]
Expand Down
1 change: 1 addition & 0 deletions docs/reference/esql/functions/signature/st_x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/reference/esql/functions/signature/st_y.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions docs/reference/esql/functions/spatial-functions.asciidoc
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions docs/reference/esql/functions/st_x.asciidoc
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions docs/reference/esql/functions/st_y.asciidoc
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions docs/reference/esql/functions/types/st_x.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[%header.monospaced.styled,format=dsv,separator=|]
|===
point | result
cartesian_point | double
geo_point | double
|===
6 changes: 6 additions & 0 deletions docs/reference/esql/functions/types/st_y.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[%header.monospaced.styled,format=dsv,separator=|]
|===
point | result
cartesian_point | double
geo_point | double
|===
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)"
Expand Down Expand Up @@ -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
;
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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 + "]";
}
}
}
Loading

0 comments on commit 8d93a93

Please sign in to comment.