Skip to content

Commit cf0e060

Browse files
authored
Use geohash cell instead of just a corner in geo_bounding_box (#30698)
Treats geohashes as grid cells instead of just points when the geohashes are used to specify the edges in the geo_bounding_box query. For example, if a geohash is used to specify the top_left corner, the top left corner of the geohash cell will be used as the corner of the bounding box. Closes #25154
1 parent b3a4acd commit cf0e060

File tree

7 files changed

+178
-19
lines changed

7 files changed

+178
-19
lines changed

docs/reference/migration/migrate_7_0/search.asciidoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
* Purely negative queries (only MUST_NOT clauses) now return a score of `0`
1313
rather than `1`.
1414

15+
* The boundary specified using geohashes in the `geo_bounding_box` query
16+
now include entire geohash cell, instead of just geohash center.
17+
1518
==== Adaptive replica selection enabled by default
1619

1720
Adaptive replica selection has been enabled by default. If you wish to return to

docs/reference/query-dsl/geo-bounding-box-query.asciidoc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,38 @@ GET /_search
231231
--------------------------------------------------
232232
// CONSOLE
233233

234+
235+
When geohashes are used to specify the bounding the edges of the
236+
bounding box, the geohashes are treated as rectangles. The bounding
237+
box is defined in such a way that its top left corresponds to the top
238+
left corner of the geohash specified in the `top_left` parameter and
239+
its bottom right is defined as the bottom right of the geohash
240+
specified in the `bottom_right` parameter.
241+
242+
In order to specify a bounding box that would match entire area of a
243+
geohash the geohash can be specified in both `top_left` and
244+
`bottom_right` parameters:
245+
246+
[source,js]
247+
--------------------------------------------------
248+
GET /_search
249+
{
250+
"query": {
251+
"geo_bounding_box" : {
252+
"pin.location" : {
253+
"top_left" : "dr",
254+
"bottom_right" : "dr"
255+
}
256+
}
257+
}
258+
}
259+
--------------------------------------------------
260+
// CONSOLE
261+
262+
In this example, the geohash `dr` will produce the bounding box
263+
query with the top left corner at `45.0,-78.75` and the bottom right
264+
corner at `39.375,-67.5`.
265+
234266
[float]
235267
==== Vertices
236268

server/src/main/java/org/elasticsearch/common/geo/GeoPoint.java

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.lucene.document.LatLonDocValuesField;
2323
import org.apache.lucene.document.LatLonPoint;
2424
import org.apache.lucene.geo.GeoEncodingUtils;
25+
import org.apache.lucene.geo.Rectangle;
2526
import org.apache.lucene.index.IndexableField;
2627
import org.apache.lucene.util.BitUtil;
2728
import org.apache.lucene.util.BytesRef;
@@ -85,21 +86,27 @@ public GeoPoint resetFromString(String value) {
8586

8687
public GeoPoint resetFromString(String value, final boolean ignoreZValue) {
8788
if (value.contains(",")) {
88-
String[] vals = value.split(",");
89-
if (vals.length > 3) {
90-
throw new ElasticsearchParseException("failed to parse [{}], expected 2 or 3 coordinates "
91-
+ "but found: [{}]", vals.length);
92-
}
93-
double lat = Double.parseDouble(vals[0].trim());
94-
double lon = Double.parseDouble(vals[1].trim());
95-
if (vals.length > 2) {
96-
GeoPoint.assertZValue(ignoreZValue, Double.parseDouble(vals[2].trim()));
97-
}
98-
return reset(lat, lon);
89+
return resetFromCoordinates(value, ignoreZValue);
9990
}
10091
return resetFromGeoHash(value);
10192
}
10293

94+
95+
public GeoPoint resetFromCoordinates(String value, final boolean ignoreZValue) {
96+
String[] vals = value.split(",");
97+
if (vals.length > 3) {
98+
throw new ElasticsearchParseException("failed to parse [{}], expected 2 or 3 coordinates "
99+
+ "but found: [{}]", vals.length);
100+
}
101+
double lat = Double.parseDouble(vals[0].trim());
102+
double lon = Double.parseDouble(vals[1].trim());
103+
if (vals.length > 2) {
104+
GeoPoint.assertZValue(ignoreZValue, Double.parseDouble(vals[2].trim()));
105+
}
106+
return reset(lat, lon);
107+
}
108+
109+
103110
public GeoPoint resetFromIndexHash(long hash) {
104111
lon = GeoHashUtils.decodeLongitude(hash);
105112
lat = GeoHashUtils.decodeLatitude(hash);

server/src/main/java/org/elasticsearch/common/geo/GeoUtils.java

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,25 @@ public static GeoPoint parseGeoPoint(Object value, final boolean ignoreZValue) t
387387
}
388388
}
389389

390+
/**
391+
* Represents the point of the geohash cell that should be used as the value of geohash
392+
*/
393+
public enum EffectivePoint {
394+
TOP_LEFT,
395+
TOP_RIGHT,
396+
BOTTOM_LEFT,
397+
BOTTOM_RIGHT
398+
}
399+
400+
/**
401+
* Parse a geopoint represented as an object, string or an array. If the geopoint is represented as a geohash,
402+
* the left bottom corner of the geohash cell is used as the geopoint coordinates.GeoBoundingBoxQueryBuilder.java
403+
*/
404+
public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, final boolean ignoreZValue)
405+
throws IOException, ElasticsearchParseException {
406+
return parseGeoPoint(parser, point, ignoreZValue, EffectivePoint.BOTTOM_LEFT);
407+
}
408+
390409
/**
391410
* Parse a {@link GeoPoint} with a {@link XContentParser}. A geopoint has one of the following forms:
392411
*
@@ -401,7 +420,7 @@ public static GeoPoint parseGeoPoint(Object value, final boolean ignoreZValue) t
401420
* @param point A {@link GeoPoint} that will be reset by the values parsed
402421
* @return new {@link GeoPoint} parsed from the parse
403422
*/
404-
public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, final boolean ignoreZValue)
423+
public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, final boolean ignoreZValue, EffectivePoint effectivePoint)
405424
throws IOException, ElasticsearchParseException {
406425
double lat = Double.NaN;
407426
double lon = Double.NaN;
@@ -458,7 +477,7 @@ public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, fina
458477
if(!Double.isNaN(lat) || !Double.isNaN(lon)) {
459478
throw new ElasticsearchParseException("field must be either lat/lon or geohash");
460479
} else {
461-
return point.resetFromGeoHash(geohash);
480+
return parseGeoHash(point, geohash, effectivePoint);
462481
}
463482
} else if (numberFormatException != null) {
464483
throw new ElasticsearchParseException("[{}] and [{}] must be valid double values", numberFormatException, LATITUDE,
@@ -489,12 +508,36 @@ public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, fina
489508
}
490509
return point.reset(lat, lon);
491510
} else if(parser.currentToken() == Token.VALUE_STRING) {
492-
return point.resetFromString(parser.text(), ignoreZValue);
511+
String val = parser.text();
512+
if (val.contains(",")) {
513+
return point.resetFromString(val, ignoreZValue);
514+
} else {
515+
return parseGeoHash(point, val, effectivePoint);
516+
}
517+
493518
} else {
494519
throw new ElasticsearchParseException("geo_point expected");
495520
}
496521
}
497522

523+
private static GeoPoint parseGeoHash(GeoPoint point, String geohash, EffectivePoint effectivePoint) {
524+
if (effectivePoint == EffectivePoint.BOTTOM_LEFT) {
525+
return point.resetFromGeoHash(geohash);
526+
} else {
527+
Rectangle rectangle = GeoHashUtils.bbox(geohash);
528+
switch (effectivePoint) {
529+
case TOP_LEFT:
530+
return point.reset(rectangle.maxLat, rectangle.minLon);
531+
case TOP_RIGHT:
532+
return point.reset(rectangle.maxLat, rectangle.maxLon);
533+
case BOTTOM_RIGHT:
534+
return point.reset(rectangle.minLat, rectangle.maxLon);
535+
default:
536+
throw new IllegalArgumentException("Unsupported effective point " + effectivePoint);
537+
}
538+
}
539+
}
540+
498541
/**
499542
* Parse a precision that can be expressed as an integer or a distance measure like "1km", "10m".
500543
*

server/src/main/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilder.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -491,19 +491,19 @@ public static Rectangle parseBoundingBox(XContentParser parser) throws IOExcepti
491491
right = parser.doubleValue();
492492
} else {
493493
if (TOP_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
494-
GeoUtils.parseGeoPoint(parser, sparse);
494+
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_LEFT);
495495
top = sparse.getLat();
496496
left = sparse.getLon();
497497
} else if (BOTTOM_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
498-
GeoUtils.parseGeoPoint(parser, sparse);
498+
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.BOTTOM_RIGHT);
499499
bottom = sparse.getLat();
500500
right = sparse.getLon();
501501
} else if (TOP_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
502-
GeoUtils.parseGeoPoint(parser, sparse);
502+
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_RIGHT);
503503
top = sparse.getLat();
504504
right = sparse.getLon();
505505
} else if (BOTTOM_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
506-
GeoUtils.parseGeoPoint(parser, sparse);
506+
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.BOTTOM_LEFT);
507507
bottom = sparse.getLat();
508508
left = sparse.getLon();
509509
} else {
@@ -515,7 +515,8 @@ public static Rectangle parseBoundingBox(XContentParser parser) throws IOExcepti
515515
}
516516
}
517517
if (envelope != null) {
518-
if ((Double.isNaN(top) || Double.isNaN(bottom) || Double.isNaN(left) || Double.isNaN(right)) == false) {
518+
if (Double.isNaN(top) == false || Double.isNaN(bottom) == false || Double.isNaN(left) == false ||
519+
Double.isNaN(right) == false) {
519520
throw new ElasticsearchParseException("failed to parse bounding box. Conflicting definition found "
520521
+ "using well-known text and explicit corners.");
521522
}

server/src/test/java/org/elasticsearch/index/query/GeoBoundingBoxQueryBuilderTests.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.lucene.search.IndexOrDocValuesQuery;
2525
import org.apache.lucene.search.MatchNoDocsQuery;
2626
import org.apache.lucene.search.Query;
27+
import org.elasticsearch.ElasticsearchParseException;
2728
import org.elasticsearch.common.geo.GeoPoint;
2829
import org.elasticsearch.common.geo.GeoUtils;
2930
import org.elasticsearch.index.mapper.MappedFieldType;
@@ -450,6 +451,64 @@ public void testFromWKT() throws IOException {
450451
assertEquals(expectedJson, GeoExecType.MEMORY, parsed.type());
451452
}
452453

454+
public void testFromGeohash() throws IOException {
455+
String json =
456+
"{\n" +
457+
" \"geo_bounding_box\" : {\n" +
458+
" \"pin.location\" : {\n" +
459+
" \"top_left\" : \"dr\",\n" +
460+
" \"bottom_right\" : \"dq\"\n" +
461+
" },\n" +
462+
" \"validation_method\" : \"STRICT\",\n" +
463+
" \"type\" : \"MEMORY\",\n" +
464+
" \"ignore_unmapped\" : false,\n" +
465+
" \"boost\" : 1.0\n" +
466+
" }\n" +
467+
"}";
468+
469+
String expectedJson =
470+
"{\n" +
471+
" \"geo_bounding_box\" : {\n" +
472+
" \"pin.location\" : {\n" +
473+
" \"top_left\" : [ -78.75, 45.0 ],\n" +
474+
" \"bottom_right\" : [ -67.5, 33.75 ]\n" +
475+
" },\n" +
476+
" \"validation_method\" : \"STRICT\",\n" +
477+
" \"type\" : \"MEMORY\",\n" +
478+
" \"ignore_unmapped\" : false,\n" +
479+
" \"boost\" : 1.0\n" +
480+
" }\n" +
481+
"}";
482+
GeoBoundingBoxQueryBuilder parsed = (GeoBoundingBoxQueryBuilder) parseQuery(json);
483+
checkGeneratedJson(expectedJson, parsed);
484+
assertEquals(json, "pin.location", parsed.fieldName());
485+
assertEquals(json, -78.75, parsed.topLeft().getLon(), 0.0001);
486+
assertEquals(json, 45.0, parsed.topLeft().getLat(), 0.0001);
487+
assertEquals(json, -67.5, parsed.bottomRight().getLon(), 0.0001);
488+
assertEquals(json, 33.75, parsed.bottomRight().getLat(), 0.0001);
489+
assertEquals(json, 1.0, parsed.boost(), 0.0001);
490+
assertEquals(json, GeoExecType.MEMORY, parsed.type());
491+
}
492+
493+
public void testMalformedGeohashes() {
494+
String jsonGeohashAndWkt =
495+
"{\n" +
496+
" \"geo_bounding_box\" : {\n" +
497+
" \"pin.location\" : {\n" +
498+
" \"top_left\" : [ -78.75, 45.0 ],\n" +
499+
" \"wkt\" : \"BBOX (-74.1, -71.12, 40.73, 40.01)\"\n" +
500+
" },\n" +
501+
" \"validation_method\" : \"STRICT\",\n" +
502+
" \"type\" : \"MEMORY\",\n" +
503+
" \"ignore_unmapped\" : false,\n" +
504+
" \"boost\" : 1.0\n" +
505+
" }\n" +
506+
"}";
507+
508+
ElasticsearchParseException e1 = expectThrows(ElasticsearchParseException.class, () -> parseQuery(jsonGeohashAndWkt));
509+
assertThat(e1.getMessage(), containsString("Conflicting definition found using well-known text and explicit corners."));
510+
}
511+
453512
@Override
454513
public void testMustRewrite() throws IOException {
455514
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);

server/src/test/java/org/elasticsearch/index/search/geo/GeoUtilsTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,20 @@ public void testPrefixTreeCellSizes() {
610610
}
611611
}
612612

613+
public void testParseGeoPointGeohashPositions() throws IOException {
614+
assertNormalizedPoint(parseGeohash("drt5", GeoUtils.EffectivePoint.TOP_LEFT), new GeoPoint(42.890625, -71.71875));
615+
assertNormalizedPoint(parseGeohash("drt5", GeoUtils.EffectivePoint.TOP_RIGHT), new GeoPoint(42.890625, -71.3671875));
616+
assertNormalizedPoint(parseGeohash("drt5", GeoUtils.EffectivePoint.BOTTOM_LEFT), new GeoPoint(42.71484375, -71.71875));
617+
assertNormalizedPoint(parseGeohash("drt5", GeoUtils.EffectivePoint.BOTTOM_RIGHT), new GeoPoint(42.71484375, -71.3671875));
618+
assertNormalizedPoint(parseGeohash("drtk", GeoUtils.EffectivePoint.BOTTOM_LEFT), new GeoPoint(42.890625, -71.3671875));
619+
}
620+
621+
private GeoPoint parseGeohash(String geohash, GeoUtils.EffectivePoint effectivePoint) throws IOException {
622+
XContentParser parser = createParser(jsonBuilder().startObject().field("geohash", geohash).endObject());
623+
parser.nextToken();
624+
return GeoUtils.parseGeoPoint(parser, new GeoPoint(), randomBoolean(), effectivePoint);
625+
}
626+
613627
private static void assertNormalizedPoint(GeoPoint input, GeoPoint expected) {
614628
GeoUtils.normalizePoint(input);
615629
if (Double.isNaN(expected.lat())) {

0 commit comments

Comments
 (0)