Skip to content

Commit 187a759

Browse files
committed
Add initial geohash function to ESQL
1 parent 02dc6d4 commit 187a759

File tree

6 files changed

+404
-4
lines changed

6 files changed

+404
-4
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
###############################################
2+
# Tests for geo_grid functions: geohash, geotile, geohex
3+
###############################################
4+
5+
geohashLiteral
6+
ROW location = TO_GEOPOINT("POINT(12.6493508684508 55.6285017221528)")
7+
| EVAL geohash = ST_GEOHASH(location, 4)
8+
;
9+
10+
location:geo_point | geohash:keyword
11+
POINT(12.6493508684508 55.6285017221528) | u3bu
12+
;
13+
14+
geohashLiteralMv
15+
ROW location = TO_GEOPOINT("POINT(12.6493508684508 55.6285017221528)"), precision = [1,2,3,4,5]
16+
| MV_EXPAND precision
17+
| EVAL geohash = ST_GEOHASH(location, precision)
18+
;
19+
20+
location:geo_point | precision:integer | geohash:keyword
21+
POINT (12.6493508684508 55.6285017221528) | 1 | u
22+
POINT (12.6493508684508 55.6285017221528) | 2 | u3
23+
POINT (12.6493508684508 55.6285017221528) | 3 | u3b
24+
POINT (12.6493508684508 55.6285017221528) | 4 | u3bu
25+
POINT (12.6493508684508 55.6285017221528) | 5 | u3bur
26+
;
27+
28+
geohashField
29+
FROM airports
30+
| WHERE abbrev == "CPH"
31+
| EVAL geohash = ST_GEOHASH(location, 7)
32+
| KEEP geohash, abbrev, name, location
33+
;
34+
35+
geohash:keyword | abbrev:keyword | name:text | location:geo_point
36+
u3buryf | CPH | Copenhagen | POINT (12.6493508684508 55.6285017221528)
37+
;
38+
39+
gridGeohashStatsBy
40+
// tag::st_geohash-grid[]
41+
FROM airports
42+
| EVAL geohash = ST_GEOHASH(location, 1)
43+
| STATS
44+
count = COUNT(*),
45+
centroid = ST_CENTROID_AGG(location)
46+
BY geohash
47+
| SORT count DESC, geohash ASC
48+
// end::st_geohash-grid[]
49+
;
50+
51+
// tag::st_geohash-grid-result[]
52+
count:long | centroid:geo_point | geohash:keyword
53+
118 | POINT (-77.41857436454018 26.96522968734409) | d
54+
96 | POINT (23.181679135886952 27.295384635654045) | s
55+
94 | POINT (70.94076107503807 25.691916451026547) | t
56+
90 | POINT (-104.3941700803116 30.811849871650338) | 9
57+
89 | POINT (18.71573683606942 53.165169130707305) | u
58+
85 | POINT (114.3722876966657 24.908398092505248) | w
59+
51 | POINT (-61.44522591713159 -22.87209844956284) | 6
60+
38 | POINT (-9.429514887252529 25.497624435045413) | e
61+
34 | POINT (-111.8071846965262 52.464381378993174) | c
62+
30 | POINT (28.7045472683385 -14.706001980230212) | k
63+
28 | POINT (159.52750137208827 -25.555616633001982) | r
64+
22 | POINT (-4.410395708612421 54.90304926367985) | g
65+
21 | POINT (-69.40534970590046 50.93379438189523) | f
66+
17 | POINT (114.05526293222519 -10.898114638950895) | q
67+
16 | POINT (147.40052131412085 21.054660080408212) | x
68+
13 | POINT (63.64716878519035 54.37333276101317) | v
69+
12 | POINT (-39.53510569408536 -11.72166372067295) | 7
70+
9 | POINT (-150.36503398790956 63.14222150482237) | b
71+
7 | POINT (-167.3069146488394 -17.976190628084755) | 2
72+
6 | POINT (-157.0227657398209 17.60691551025957) | 8
73+
5 | POINT (114.40185610949993 48.99013584572822) | y
74+
4 | POINT (-69.98911972157657 -52.170535867335275) | 4
75+
4 | POINT (51.48641954641789 -14.176777086686343) | m
76+
1 | POINT (-109.43006442859769 -27.15877384878695) | 3
77+
1 | POINT (170.20002692006528 -45.92343100346625) | p
78+
// end::st_geohash-grid-result[]
79+
;
80+
81+
gridGeohashStatsByWhereUK
82+
FROM airports
83+
| 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))"))
84+
| EVAL geohash = ST_GEOHASH(location, 2)
85+
| STATS
86+
count = COUNT(location),
87+
centroid = ST_CENTROID_AGG(location)
88+
BY geohash
89+
| SORT count DESC
90+
;
91+
92+
count:long | centroid:geo_point | geohash:keyword
93+
14 | POINT (-2.5644131543646966 53.38093495994274) | gc
94+
3 | POINT (-2.7510103583335876 58.79020635969937) | gf
95+
;
96+
97+
gridGeohashInStatsBy
98+
FROM airports
99+
| STATS
100+
count = COUNT(location),
101+
centroid = ST_CENTROID_AGG(location)
102+
BY ST_GEOHASH(location, 1)
103+
| SORT count DESC
104+
| KEEP count, centroid
105+
| LIMIT 10
106+
;
107+
108+
count:long | centroid:geo_point
109+
118 | POINT (-77.41857436454018 26.96522968734409)
110+
96 | POINT (23.181679135886952 27.295384635654045)
111+
94 | POINT (70.94076107503807 25.691916451026547)
112+
90 | POINT (-104.3941700803116 30.811849871650338)
113+
89 | POINT (18.71573683606942 53.165169130707305)
114+
85 | POINT (114.3722876966657 24.908398092505248)
115+
51 | POINT (-61.44522591713159 -22.87209844956284)
116+
38 | POINT (-9.429514887252529 25.497624435045413)
117+
34 | POINT (-111.8071846965262 52.464381378993174)
118+
30 | POINT (28.7045472683385 -14.706001980230212)
119+
;
120+
121+
gridGeohashInStatsByWhereUK
122+
FROM airports
123+
| 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))"))
124+
| STATS
125+
count = COUNT(location),
126+
centroid = ST_CENTROID_AGG(location)
127+
BY ST_GEOHASH(location, 2)
128+
| KEEP count, centroid
129+
| SORT count DESC
130+
;
131+
132+
count:long | centroid:geo_point
133+
14 | POINT (-2.5644131543646966 53.38093495994274)
134+
3 | POINT (-2.7510103583335876 58.79020635969937)
135+
;

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialWithin;
5656
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StDistance;
5757
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StEnvelope;
58+
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeohash;
5859
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX;
5960
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMax;
6061
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMin;
@@ -206,7 +207,14 @@ public static List<NamedWriteableRegistry.Entry> unaryScalars() {
206207
}
207208

208209
private static List<NamedWriteableRegistry.Entry> spatials() {
209-
return List.of(SpatialContains.ENTRY, SpatialDisjoint.ENTRY, SpatialIntersects.ENTRY, SpatialWithin.ENTRY, StDistance.ENTRY);
210+
return List.of(
211+
SpatialContains.ENTRY,
212+
SpatialDisjoint.ENTRY,
213+
SpatialIntersects.ENTRY,
214+
SpatialWithin.ENTRY,
215+
StDistance.ENTRY,
216+
StGeohash.ENTRY
217+
);
210218
}
211219

212220
private static List<NamedWriteableRegistry.Entry> arithmetics() {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialWithin;
119119
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StDistance;
120120
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StEnvelope;
121+
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeohash;
121122
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX;
122123
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMax;
123124
import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMin;
@@ -365,7 +366,8 @@ private static FunctionDefinition[][] functions() {
365366
def(StYMax.class, StYMax::new, "st_ymax"),
366367
def(StYMin.class, StYMin::new, "st_ymin"),
367368
def(StX.class, StX::new, "st_x"),
368-
def(StY.class, StY::new, "st_y") },
369+
def(StY.class, StY::new, "st_y"),
370+
def(StGeohash.class, StGeohash::new, "st_geohash") },
369371
// conditional
370372
new FunctionDefinition[] { def(Case.class, Case::new, "case") },
371373
// null
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.esql.expression.function.scalar.spatial;
9+
10+
import org.apache.lucene.util.BytesRef;
11+
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
12+
import org.elasticsearch.common.io.stream.StreamInput;
13+
import org.elasticsearch.compute.ann.Evaluator;
14+
import org.elasticsearch.compute.ann.Fixed;
15+
import org.elasticsearch.compute.operator.EvalOperator;
16+
import org.elasticsearch.geometry.Point;
17+
import org.elasticsearch.geometry.utils.Geohash;
18+
import org.elasticsearch.xpack.esql.core.expression.Expression;
19+
import org.elasticsearch.xpack.esql.core.expression.FoldContext;
20+
import org.elasticsearch.xpack.esql.core.expression.function.scalar.BinaryScalarFunction;
21+
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
22+
import org.elasticsearch.xpack.esql.core.tree.Source;
23+
import org.elasticsearch.xpack.esql.core.type.DataType;
24+
import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper;
25+
import org.elasticsearch.xpack.esql.expression.function.Example;
26+
import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
27+
import org.elasticsearch.xpack.esql.expression.function.Param;
28+
29+
import java.io.IOException;
30+
31+
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST;
32+
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND;
33+
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType;
34+
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isWholeNumber;
35+
import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_POINT;
36+
import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD;
37+
import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.GEO;
38+
39+
/**
40+
* Calculates the geohash of geo_point geometries.
41+
*/
42+
public class StGeohash extends UnarySpatialBinaryFunction implements EvaluatorMapper {
43+
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
44+
Expression.class,
45+
"StGeohash",
46+
StGeohash::new
47+
);
48+
49+
@FunctionInfo(
50+
returnType = "keyword",
51+
description = "Calculates the `geohash` of the supplied geo_point at the specified precision.",
52+
examples = @Example(file = "spatial-grid", tag = "st_geohash-grid")
53+
)
54+
public StGeohash(
55+
Source source,
56+
@Param(
57+
name = "point",
58+
type = { "geo_point" },
59+
description = "Expression of type `geo_point`. If `null`, the function returns `null`."
60+
) Expression field,
61+
@Param(
62+
name = "precision",
63+
type = { "integer" },
64+
description = "Expression of type `integer`. If `null`, the function returns `null`."
65+
) Expression precision
66+
) {
67+
this(source, field, precision, false);
68+
}
69+
70+
private StGeohash(Source source, Expression field, Expression precision, boolean spatialDocValues) {
71+
super(source, field, precision, spatialDocValues);
72+
}
73+
74+
private StGeohash(StreamInput in) throws IOException {
75+
super(in, false);
76+
}
77+
78+
@Override
79+
public UnarySpatialBinaryFunction withDocValues(boolean useDocValues) {
80+
// Only update the docValues flags if the field is found in the attributes
81+
boolean leftDV = this.spatialDocsValues || useDocValues;
82+
return new StGeohash(source(), left(), right(), leftDV);
83+
}
84+
85+
@Override
86+
public String getWriteableName() {
87+
return ENTRY.name;
88+
}
89+
90+
@Override
91+
public DataType dataType() {
92+
return KEYWORD;
93+
}
94+
95+
@Override
96+
protected BinaryScalarFunction replaceChildren(Expression newLeft, Expression newRight) {
97+
return new StGeohash(source(), newLeft, newRight);
98+
}
99+
100+
@Override
101+
protected NodeInfo<? extends Expression> info() {
102+
return NodeInfo.create(this, StGeohash::new, left(), right());
103+
}
104+
105+
@Override
106+
protected TypeResolution resolveType() {
107+
if (childrenResolved() == false) {
108+
return new TypeResolution("Unresolved children");
109+
}
110+
111+
TypeResolution resolution = isGeoPoint(left(), sourceText());
112+
if (resolution.unresolved()) {
113+
return resolution;
114+
}
115+
116+
resolution = isWholeNumber(right(), sourceText(), SECOND);
117+
if (resolution.unresolved()) {
118+
return resolution;
119+
}
120+
121+
return TypeResolution.TYPE_RESOLVED;
122+
}
123+
124+
protected static Expression.TypeResolution isGeoPoint(Expression e, String operationName) {
125+
126+
return isType(e, t -> t.equals(GEO_POINT), operationName, FIRST, GEO_POINT.typeName());
127+
}
128+
129+
@Override
130+
public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
131+
// TODO: Implement
132+
if (left().foldable()) {
133+
// Assume right is not foldable, since that would be dealt with in isFoldable() and fold()
134+
var point = (BytesRef) left().fold(toEvaluator.foldCtx());
135+
return new StGeohashFromLiteralAndFieldEvaluator.Factory(source(), point, toEvaluator.apply(right()));
136+
} else if (right().foldable()) {
137+
// Assume left is not foldable, since that would be dealt with in isFoldable() and fold()
138+
int precision = (int) right().fold(toEvaluator.foldCtx());
139+
return spatialDocsValues
140+
? new StGeohashFromFieldDocValuesAndLiteralEvaluator.Factory(source(), toEvaluator.apply(left()), precision)
141+
: new StGeohashFromFieldAndLiteralEvaluator.Factory(source(), toEvaluator.apply(left()), precision);
142+
} else {
143+
// Both arguments come from index fields
144+
return spatialDocsValues
145+
? new StGeohashFromFieldDocValuesAndFieldEvaluator.Factory(source(), toEvaluator.apply(left()), toEvaluator.apply(right()))
146+
: new StGeohashFromFieldAndFieldEvaluator.Factory(source(), toEvaluator.apply(left()), toEvaluator.apply(right()));
147+
}
148+
}
149+
150+
@Override
151+
public Object fold(FoldContext ctx) {
152+
var point = (BytesRef) left().fold(ctx);
153+
int precision = (int) right().fold(ctx);
154+
return calculateGeohash(GEO.wkbAsPoint(point), precision);
155+
}
156+
157+
@Evaluator(extraName = "FromFieldAndLiteral", warnExceptions = { IllegalArgumentException.class })
158+
static BytesRef fromFieldAndLiteral(BytesRef in, @Fixed int precision) {
159+
return calculateGeohash(GEO.wkbAsPoint(in), precision);
160+
}
161+
162+
@Evaluator(extraName = "FromFieldDocValuesAndLiteral", warnExceptions = { IllegalArgumentException.class })
163+
static BytesRef fromFieldDocValuesAndLiteral(long encoded, @Fixed int precision) {
164+
return calculateGeohash(GEO.longAsPoint(encoded), precision);
165+
}
166+
167+
@Evaluator(extraName = "FromFieldAndField", warnExceptions = { IllegalArgumentException.class })
168+
static BytesRef fromFieldAndField(BytesRef in, int precision) {
169+
return calculateGeohash(GEO.wkbAsPoint(in), precision);
170+
}
171+
172+
@Evaluator(extraName = "FromFieldDocValuesAndField", warnExceptions = { IllegalArgumentException.class })
173+
static BytesRef fromFieldDocValuesAndField(long encoded, int precision) {
174+
return calculateGeohash(GEO.longAsPoint(encoded), precision);
175+
}
176+
177+
@Evaluator(extraName = "FromLiteralAndField", warnExceptions = { IllegalArgumentException.class })
178+
static BytesRef fromLiteralAndField(@Fixed BytesRef in, int precision) {
179+
return calculateGeohash(GEO.wkbAsPoint(in), precision);
180+
}
181+
182+
protected static BytesRef calculateGeohash(Point point, int precision) {
183+
return new BytesRef(Geohash.stringEncode(point.getX(), point.getY(), precision));
184+
}
185+
}

0 commit comments

Comments
 (0)