Skip to content

Commit 82ad403

Browse files
authored
SQL: Add ST_Distance function to geosql (#39973)
* SQL: Add ST_Distance function to geosql Adds ST_Distance function that works on points. No other shapes are supported at the moments. No optimization and conversion into geo_distance filter is done yet. Relates to #29872
1 parent 223f756 commit 82ad403

File tree

25 files changed

+559
-21
lines changed

25 files changed

+559
-21
lines changed

docs/reference/sql/functions/geo.asciidoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,29 @@ Returns the geometry from WKT representation. The return type is geometry.
5454
["source","sql",subs="attributes,macros"]
5555
--------------------------------------------------
5656
include-tagged::{sql-specs}/geo/docs.csv-spec[aswkt]
57+
--------------------------------------------------
58+
59+
[[sql-functions-geo-st-distance]]
60+
===== `ST_Distance`
61+
62+
.Synopsis:
63+
[source, sql]
64+
--------------------------------------------------
65+
ST_Distance(geometry<1>, geometry<2>)
66+
--------------------------------------------------
67+
68+
*Input*:
69+
70+
<1> source geometry
71+
<2> target geometry
72+
73+
*Output*: Double
74+
75+
.Description:
76+
77+
Returns the distance between geometries in meters. Both geometries have to be points. The return type is double.
78+
79+
["source","sql",subs="attributes,macros"]
80+
--------------------------------------------------
81+
include-tagged::{sql-specs}/geo/docs.csv-spec[distance]
5782
--------------------------------------------------
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.sql.qa.multi_node;
8+
9+
import org.elasticsearch.xpack.sql.qa.geo.GeoCsvSpecTestCase;
10+
import org.elasticsearch.xpack.sql.qa.jdbc.CsvTestUtils.CsvTestCase;
11+
12+
public class GeoJdbcCsvSpecIT extends GeoCsvSpecTestCase {
13+
public GeoJdbcCsvSpecIT(String fileName, String groupName, String testName, Integer lineNumber, CsvTestCase testCase) {
14+
super(fileName, groupName, testName, lineNumber, testCase);
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.sql.qa.multi_node;
8+
9+
import org.elasticsearch.xpack.sql.qa.geo.GeoSqlSpecTestCase;
10+
11+
public class GeoJdbcSqlSpecIT extends GeoSqlSpecTestCase {
12+
public GeoJdbcSqlSpecIT(String fileName, String groupName, String testName, Integer lineNumber, String query) {
13+
super(fileName, groupName, testName, lineNumber, query);
14+
}
15+
}

x-pack/plugin/sql/qa/src/main/resources/command.csv-spec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ DATABASE |SCALAR
132132
USER |SCALAR
133133
ST_ASTEXT |SCALAR
134134
ST_ASWKT |SCALAR
135+
ST_DISTANCE |SCALAR
135136
ST_GEOMFROMTEXT |SCALAR
136137
ST_WKTTOSQL |SCALAR
137138
SCORE |SCORE

x-pack/plugin/sql/qa/src/main/resources/docs.csv-spec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ DATABASE |SCALAR
309309
USER |SCALAR
310310
ST_ASTEXT |SCALAR
311311
ST_ASWKT |SCALAR
312+
ST_DISTANCE |SCALAR
312313
ST_GEOMFROMTEXT |SCALAR
313314
ST_WKTTOSQL |SCALAR
314315
SCORE |SCORE

x-pack/plugin/sql/qa/src/main/resources/geo/docs.csv-spec

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,13 @@ SELECT CAST(ST_WKTToSQL('POINT (10 20)') AS STRING) location;
2525
point (10.0 20.0)
2626
// end::wkttosql
2727
;
28+
29+
30+
selectDistance
31+
// tag::distance
32+
SELECT ST_Distance(ST_WKTToSQL('POINT (10 20)'), ST_WKTToSQL('POINT (20 30)')) distance;
33+
34+
distance:d
35+
1499101.2889383635
36+
// end::distance
37+
;

x-pack/plugin/sql/qa/src/main/resources/geo/geosql.csv-spec

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,58 @@ Asia |Singapore
146146
Asia |Sydney
147147
Europe |Amsterdam
148148
Europe |Berlin
149-
;
149+
;
150+
151+
152+
selectCitiesByDistance
153+
SELECT region, city, ST_Distance(location, ST_WktToSQL('POINT (-71 42)')) distance FROM geo WHERE distance < 5000000 ORDER BY region, city;
154+
155+
region:s | city:s | distance:d
156+
Americas |Chicago |1373941.5140200066
157+
Americas |Mountain View |4335936.909375596
158+
Americas |New York |285839.6579622518
159+
Americas |Phoenix |3692895.0346903414
160+
Americas |San Francisco |4343565.010996301
161+
;
162+
163+
selectCitiesByDistanceFloored
164+
SELECT region, city, FLOOR(ST_Distance(location, ST_WktToSQL('POINT (-71 42)'))) distance FROM geo WHERE distance < 5000000 ORDER BY region, city;
165+
166+
region:s | city:s | distance:l
167+
Americas |Chicago |1373941
168+
Americas |Mountain View |4335936
169+
Americas |New York |285839
170+
Americas |Phoenix |3692895
171+
Americas |San Francisco |4343565
172+
;
173+
174+
selectCitiesOrderByDistance
175+
SELECT region, city FROM geo ORDER BY ST_Distance(location, ST_WktToSQL('POINT (-71 42)')) ;
176+
177+
region:s | city:s
178+
Americas |New York
179+
Americas |Chicago
180+
Americas |Phoenix
181+
Americas |Mountain View
182+
Americas |San Francisco
183+
Europe |London
184+
Europe |Paris
185+
Europe |Amsterdam
186+
Europe |Berlin
187+
Europe |Munich
188+
Asia |Tokyo
189+
Asia |Seoul
190+
Asia |Hong Kong
191+
Asia |Singapore
192+
Asia |Sydney
193+
;
194+
195+
groupCitiesByDistance
196+
SELECT COUNT(*) count, FIRST(region) region FROM geo GROUP BY FLOOR(ST_Distance(location, ST_WktToSQL('POINT (-71 42)'))/5000000);
197+
198+
count:l | region:s
199+
5 |Americas
200+
5 |Europe
201+
3 |Asia
202+
2 |Asia
203+
;

x-pack/plugin/sql/qa/src/main/resources/geo/geosql.sql-spec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ SELECT city, ST_GEOMFROMTEXT(ST_ASWKT(location)) shape_wkt, region FROM "geo" OR
1717
selectRegionUsingWktToSqlWithoutConvertion
1818
SELECT region, city, shape, ST_GEOMFROMTEXT(region_point) region_wkt FROM geo ORDER BY region, city;
1919

20-
selectCitiesWithAGroupByWktToSql
20+
selectCitiesWithGroupByWktToSql
2121
SELECT COUNT(city) city_by_region, ST_GEOMFROMTEXT(region_point) region_geom FROM geo WHERE city LIKE '%a%' GROUP BY region_geom ORDER BY city_by_region;
2222

23-
selectCitiesWithEOrderByWktToSql
23+
selectCitiesWithOrderByWktToSql
2424
SELECT region, city, UCASE(ST_ASWKT(ST_GEOMFROMTEXT(region_point))) region_wkt FROM geo WHERE city LIKE '%e%' ORDER BY region_wkt, city;

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.WeekOfYear;
4747
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.Year;
4848
import org.elasticsearch.xpack.sql.expression.function.scalar.geo.StAswkt;
49+
import org.elasticsearch.xpack.sql.expression.function.scalar.geo.StDistance;
4950
import org.elasticsearch.xpack.sql.expression.function.scalar.geo.StWkttosql;
5051
import org.elasticsearch.xpack.sql.expression.function.scalar.math.ACos;
5152
import org.elasticsearch.xpack.sql.expression.function.scalar.math.ASin;
@@ -254,7 +255,9 @@ private void defineDefaultFunctions() {
254255

255256
// Geo Functions
256257
addToMap(def(StAswkt.class, StAswkt::new, "ST_ASWKT", "ST_ASTEXT"),
257-
def(StWkttosql.class, StWkttosql::new, "ST_WKTTOSQL", "ST_GEOMFROMTEXT"));
258+
def(StWkttosql.class, StWkttosql::new, "ST_WKTTOSQL", "ST_GEOMFROMTEXT"),
259+
def(StDistance.class, StDistance::new, "ST_DISTANCE")
260+
);
258261

259262
// Special
260263
addToMap(def(Score.class, Score::new, "SCORE"));

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NonIsoDateTimeProcessor;
1313
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.QuarterProcessor;
1414
import org.elasticsearch.xpack.sql.expression.function.scalar.geo.GeoProcessor;
15+
import org.elasticsearch.xpack.sql.expression.function.scalar.geo.StDistanceProcessor;
1516
import org.elasticsearch.xpack.sql.expression.function.scalar.geo.StWkttosqlProcessor;
1617
import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryMathProcessor;
1718
import org.elasticsearch.xpack.sql.expression.function.scalar.math.MathProcessor;
@@ -94,8 +95,10 @@ public static List<NamedWriteableRegistry.Entry> getNamedWriteables() {
9495
entries.add(new Entry(Processor.class, LocateFunctionProcessor.NAME, LocateFunctionProcessor::new));
9596
entries.add(new Entry(Processor.class, ReplaceFunctionProcessor.NAME, ReplaceFunctionProcessor::new));
9697
entries.add(new Entry(Processor.class, SubstringFunctionProcessor.NAME, SubstringFunctionProcessor::new));
98+
// geo
9799
entries.add(new Entry(Processor.class, GeoProcessor.NAME, GeoProcessor::new));
98100
entries.add(new Entry(Processor.class, StWkttosqlProcessor.NAME, StWkttosqlProcessor::new));
101+
entries.add(new Entry(Processor.class, StDistanceProcessor.NAME, StDistanceProcessor::new));
99102
return entries;
100103
}
101104

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/GeoProcessor.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66
package org.elasticsearch.xpack.sql.expression.function.scalar.geo;
77

8-
import org.elasticsearch.common.geo.GeoPoint;
98
import org.elasticsearch.common.io.stream.StreamInput;
109
import org.elasticsearch.common.io.stream.StreamOutput;
1110
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
@@ -18,9 +17,7 @@ public class GeoProcessor implements Processor {
1817

1918
private interface GeoShapeFunction<R> {
2019
default R apply(Object o) {
21-
if (o instanceof GeoPoint) {
22-
return doApply(new GeoShape(((GeoPoint) o).getLon(), ((GeoPoint) o).getLat()));
23-
} else if (o instanceof GeoShape) {
20+
if (o instanceof GeoShape) {
2421
return doApply((GeoShape) o);
2522
} else {
2623
throw new SqlIllegalArgumentException("A geo_point or geo_shape is required; received [{}]", o);

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/geo/GeoShape.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,29 @@
55
*/
66
package org.elasticsearch.xpack.sql.expression.function.scalar.geo;
77

8+
import org.elasticsearch.common.geo.GeoUtils;
89
import org.elasticsearch.common.geo.builders.PointBuilder;
910
import org.elasticsearch.common.geo.builders.ShapeBuilder;
1011
import org.elasticsearch.common.geo.parsers.ShapeParser;
12+
import org.elasticsearch.common.io.stream.NamedWriteable;
13+
import org.elasticsearch.common.io.stream.StreamInput;
14+
import org.elasticsearch.common.io.stream.StreamOutput;
1115
import org.elasticsearch.common.xcontent.ToXContentFragment;
1216
import org.elasticsearch.common.xcontent.XContentBuilder;
17+
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
1318

1419
import java.io.IOException;
20+
import java.util.Objects;
1521

1622
/**
1723
* Wrapper class to represent a GeoShape in SQL
1824
*
1925
* It is required to override the XContent serialization. The ShapeBuilder serializes using GeoJSON by default,
2026
* but in SQL we need the serialization to be WKT-based.
2127
*/
22-
public class GeoShape implements ToXContentFragment {
28+
public class GeoShape implements ToXContentFragment, NamedWriteable {
29+
30+
public static final String NAME = "geo";
2331

2432
private final ShapeBuilder<?, ?, ?> shapeBuilder;
2533

@@ -31,6 +39,15 @@ public GeoShape(Object value) throws IOException {
3139
shapeBuilder = ShapeParser.parse(value);
3240
}
3341

42+
public GeoShape(StreamInput in) throws IOException {
43+
shapeBuilder = ShapeParser.parse(in.readString());
44+
}
45+
46+
@Override
47+
public void writeTo(StreamOutput out) throws IOException {
48+
out.writeString(shapeBuilder.toWKT());
49+
}
50+
3451
@Override
3552
public String toString() {
3653
return shapeBuilder.toWKT();
@@ -40,4 +57,41 @@ public String toString() {
4057
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
4158
return builder.value(shapeBuilder.toWKT());
4259
}
60+
61+
public static double distance(GeoShape shape1, GeoShape shape2) {
62+
if (shape1.shapeBuilder instanceof PointBuilder == false) {
63+
throw new SqlIllegalArgumentException("distance calculation is only supported for points; received [{}]", shape1);
64+
}
65+
if (shape2.shapeBuilder instanceof PointBuilder == false) {
66+
throw new SqlIllegalArgumentException("distance calculation is only supported for points; received [{}]", shape2);
67+
}
68+
double srcLat = ((PointBuilder) shape1.shapeBuilder).latitude();
69+
double srcLon = ((PointBuilder) shape1.shapeBuilder).longitude();
70+
double dstLat = ((PointBuilder) shape2.shapeBuilder).latitude();
71+
double dstLon = ((PointBuilder) shape2.shapeBuilder).longitude();
72+
return GeoUtils.arcDistance(srcLat, srcLon, dstLat, dstLon);
73+
}
74+
75+
@Override
76+
public boolean equals(Object o) {
77+
if (this == o) {
78+
return true;
79+
}
80+
if (o == null || getClass() != o.getClass()) {
81+
return false;
82+
}
83+
GeoShape geoShape = (GeoShape) o;
84+
return shapeBuilder.equals(geoShape.shapeBuilder);
85+
}
86+
87+
@Override
88+
public int hashCode() {
89+
return Objects.hash(shapeBuilder);
90+
}
91+
92+
@Override
93+
public String getWriteableName() {
94+
return NAME;
95+
}
96+
4397
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.sql.expression.function.scalar.geo;
8+
9+
import org.elasticsearch.xpack.sql.expression.Expression;
10+
import org.elasticsearch.xpack.sql.expression.Expressions;
11+
import org.elasticsearch.xpack.sql.expression.FieldAttribute;
12+
import org.elasticsearch.xpack.sql.expression.function.scalar.BinaryScalarFunction;
13+
import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe;
14+
import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate;
15+
import org.elasticsearch.xpack.sql.tree.NodeInfo;
16+
import org.elasticsearch.xpack.sql.tree.Source;
17+
import org.elasticsearch.xpack.sql.type.DataType;
18+
19+
import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isGeo;
20+
import static org.elasticsearch.xpack.sql.expression.function.scalar.geo.StDistanceProcessor.process;
21+
import static org.elasticsearch.xpack.sql.expression.gen.script.ParamsBuilder.paramsBuilder;
22+
23+
/**
24+
* Calculates the distance between two points
25+
*/
26+
public class StDistance extends BinaryScalarFunction {
27+
28+
public StDistance(Source source, Expression source1, Expression source2) {
29+
super(source, source1, source2);
30+
}
31+
32+
@Override
33+
protected StDistance replaceChildren(Expression newLeft, Expression newRight) {
34+
return new StDistance(source(), newLeft, newRight);
35+
}
36+
37+
@Override
38+
protected TypeResolution resolveType() {
39+
if (!childrenResolved()) {
40+
return new TypeResolution("Unresolved children");
41+
}
42+
43+
TypeResolution resolution = isGeo(left(), functionName(), Expressions.ParamOrdinal.FIRST);
44+
if (resolution.unresolved()) {
45+
return resolution;
46+
}
47+
48+
return isGeo(right(), functionName(), Expressions.ParamOrdinal.SECOND);
49+
}
50+
51+
@Override
52+
public DataType dataType() {
53+
return DataType.DOUBLE;
54+
}
55+
56+
@Override
57+
protected NodeInfo<StDistance> info() {
58+
return NodeInfo.create(this, StDistance::new, left(), right());
59+
}
60+
61+
@Override
62+
public ScriptTemplate scriptWithField(FieldAttribute field) {
63+
return new ScriptTemplate(processScript("{sql}.geoDocValue(doc,{})"),
64+
paramsBuilder().variable(field.exactAttribute().name()).build(),
65+
dataType());
66+
}
67+
68+
@Override
69+
public Object fold() {
70+
return process(left().fold(), right().fold());
71+
}
72+
73+
@Override
74+
protected Pipe makePipe() {
75+
return new StDistancePipe(source(), this, Expressions.pipe(left()), Expressions.pipe(right()));
76+
}
77+
78+
@Override
79+
protected String scriptMethodName() {
80+
return "stDistance";
81+
}
82+
}

0 commit comments

Comments
 (0)