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
This commit is contained in:
Igor Motov 2018-05-24 14:46:15 -04:00 committed by GitHub
parent b3a4acdf20
commit cf0e0606af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 178 additions and 19 deletions

View File

@ -12,6 +12,9 @@
* Purely negative queries (only MUST_NOT clauses) now return a score of `0` * Purely negative queries (only MUST_NOT clauses) now return a score of `0`
rather than `1`. rather than `1`.
* The boundary specified using geohashes in the `geo_bounding_box` query
now include entire geohash cell, instead of just geohash center.
==== Adaptive replica selection enabled by default ==== Adaptive replica selection enabled by default
Adaptive replica selection has been enabled by default. If you wish to return to Adaptive replica selection has been enabled by default. If you wish to return to

View File

@ -231,6 +231,38 @@ GET /_search
-------------------------------------------------- --------------------------------------------------
// CONSOLE // CONSOLE
When geohashes are used to specify the bounding the edges of the
bounding box, the geohashes are treated as rectangles. The bounding
box is defined in such a way that its top left corresponds to the top
left corner of the geohash specified in the `top_left` parameter and
its bottom right is defined as the bottom right of the geohash
specified in the `bottom_right` parameter.
In order to specify a bounding box that would match entire area of a
geohash the geohash can be specified in both `top_left` and
`bottom_right` parameters:
[source,js]
--------------------------------------------------
GET /_search
{
"query": {
"geo_bounding_box" : {
"pin.location" : {
"top_left" : "dr",
"bottom_right" : "dr"
}
}
}
}
--------------------------------------------------
// CONSOLE
In this example, the geohash `dr` will produce the bounding box
query with the top left corner at `45.0,-78.75` and the bottom right
corner at `39.375,-67.5`.
[float] [float]
==== Vertices ==== Vertices

View File

@ -22,6 +22,7 @@ package org.elasticsearch.common.geo;
import org.apache.lucene.document.LatLonDocValuesField; import org.apache.lucene.document.LatLonDocValuesField;
import org.apache.lucene.document.LatLonPoint; import org.apache.lucene.document.LatLonPoint;
import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.geo.Rectangle;
import org.apache.lucene.index.IndexableField; import org.apache.lucene.index.IndexableField;
import org.apache.lucene.util.BitUtil; import org.apache.lucene.util.BitUtil;
import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRef;
@ -85,21 +86,27 @@ public final class GeoPoint implements ToXContentFragment {
public GeoPoint resetFromString(String value, final boolean ignoreZValue) { public GeoPoint resetFromString(String value, final boolean ignoreZValue) {
if (value.contains(",")) { if (value.contains(",")) {
String[] vals = value.split(","); return resetFromCoordinates(value, ignoreZValue);
if (vals.length > 3) {
throw new ElasticsearchParseException("failed to parse [{}], expected 2 or 3 coordinates "
+ "but found: [{}]", vals.length);
}
double lat = Double.parseDouble(vals[0].trim());
double lon = Double.parseDouble(vals[1].trim());
if (vals.length > 2) {
GeoPoint.assertZValue(ignoreZValue, Double.parseDouble(vals[2].trim()));
}
return reset(lat, lon);
} }
return resetFromGeoHash(value); return resetFromGeoHash(value);
} }
public GeoPoint resetFromCoordinates(String value, final boolean ignoreZValue) {
String[] vals = value.split(",");
if (vals.length > 3) {
throw new ElasticsearchParseException("failed to parse [{}], expected 2 or 3 coordinates "
+ "but found: [{}]", vals.length);
}
double lat = Double.parseDouble(vals[0].trim());
double lon = Double.parseDouble(vals[1].trim());
if (vals.length > 2) {
GeoPoint.assertZValue(ignoreZValue, Double.parseDouble(vals[2].trim()));
}
return reset(lat, lon);
}
public GeoPoint resetFromIndexHash(long hash) { public GeoPoint resetFromIndexHash(long hash) {
lon = GeoHashUtils.decodeLongitude(hash); lon = GeoHashUtils.decodeLongitude(hash);
lat = GeoHashUtils.decodeLatitude(hash); lat = GeoHashUtils.decodeLatitude(hash);

View File

@ -387,6 +387,25 @@ public class GeoUtils {
} }
} }
/**
* Represents the point of the geohash cell that should be used as the value of geohash
*/
public enum EffectivePoint {
TOP_LEFT,
TOP_RIGHT,
BOTTOM_LEFT,
BOTTOM_RIGHT
}
/**
* Parse a geopoint represented as an object, string or an array. If the geopoint is represented as a geohash,
* the left bottom corner of the geohash cell is used as the geopoint coordinates.GeoBoundingBoxQueryBuilder.java
*/
public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, final boolean ignoreZValue)
throws IOException, ElasticsearchParseException {
return parseGeoPoint(parser, point, ignoreZValue, EffectivePoint.BOTTOM_LEFT);
}
/** /**
* Parse a {@link GeoPoint} with a {@link XContentParser}. A geopoint has one of the following forms: * Parse a {@link GeoPoint} with a {@link XContentParser}. A geopoint has one of the following forms:
* *
@ -401,7 +420,7 @@ public class GeoUtils {
* @param point A {@link GeoPoint} that will be reset by the values parsed * @param point A {@link GeoPoint} that will be reset by the values parsed
* @return new {@link GeoPoint} parsed from the parse * @return new {@link GeoPoint} parsed from the parse
*/ */
public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, final boolean ignoreZValue) public static GeoPoint parseGeoPoint(XContentParser parser, GeoPoint point, final boolean ignoreZValue, EffectivePoint effectivePoint)
throws IOException, ElasticsearchParseException { throws IOException, ElasticsearchParseException {
double lat = Double.NaN; double lat = Double.NaN;
double lon = Double.NaN; double lon = Double.NaN;
@ -458,7 +477,7 @@ public class GeoUtils {
if(!Double.isNaN(lat) || !Double.isNaN(lon)) { if(!Double.isNaN(lat) || !Double.isNaN(lon)) {
throw new ElasticsearchParseException("field must be either lat/lon or geohash"); throw new ElasticsearchParseException("field must be either lat/lon or geohash");
} else { } else {
return point.resetFromGeoHash(geohash); return parseGeoHash(point, geohash, effectivePoint);
} }
} else if (numberFormatException != null) { } else if (numberFormatException != null) {
throw new ElasticsearchParseException("[{}] and [{}] must be valid double values", numberFormatException, LATITUDE, throw new ElasticsearchParseException("[{}] and [{}] must be valid double values", numberFormatException, LATITUDE,
@ -489,12 +508,36 @@ public class GeoUtils {
} }
return point.reset(lat, lon); return point.reset(lat, lon);
} else if(parser.currentToken() == Token.VALUE_STRING) { } else if(parser.currentToken() == Token.VALUE_STRING) {
return point.resetFromString(parser.text(), ignoreZValue); String val = parser.text();
if (val.contains(",")) {
return point.resetFromString(val, ignoreZValue);
} else {
return parseGeoHash(point, val, effectivePoint);
}
} else { } else {
throw new ElasticsearchParseException("geo_point expected"); throw new ElasticsearchParseException("geo_point expected");
} }
} }
private static GeoPoint parseGeoHash(GeoPoint point, String geohash, EffectivePoint effectivePoint) {
if (effectivePoint == EffectivePoint.BOTTOM_LEFT) {
return point.resetFromGeoHash(geohash);
} else {
Rectangle rectangle = GeoHashUtils.bbox(geohash);
switch (effectivePoint) {
case TOP_LEFT:
return point.reset(rectangle.maxLat, rectangle.minLon);
case TOP_RIGHT:
return point.reset(rectangle.maxLat, rectangle.maxLon);
case BOTTOM_RIGHT:
return point.reset(rectangle.minLat, rectangle.maxLon);
default:
throw new IllegalArgumentException("Unsupported effective point " + effectivePoint);
}
}
}
/** /**
* Parse a precision that can be expressed as an integer or a distance measure like "1km", "10m". * Parse a precision that can be expressed as an integer or a distance measure like "1km", "10m".
* *

View File

@ -491,19 +491,19 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
right = parser.doubleValue(); right = parser.doubleValue();
} else { } else {
if (TOP_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { if (TOP_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
GeoUtils.parseGeoPoint(parser, sparse); GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_LEFT);
top = sparse.getLat(); top = sparse.getLat();
left = sparse.getLon(); left = sparse.getLon();
} else if (BOTTOM_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { } else if (BOTTOM_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
GeoUtils.parseGeoPoint(parser, sparse); GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.BOTTOM_RIGHT);
bottom = sparse.getLat(); bottom = sparse.getLat();
right = sparse.getLon(); right = sparse.getLon();
} else if (TOP_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { } else if (TOP_RIGHT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
GeoUtils.parseGeoPoint(parser, sparse); GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_RIGHT);
top = sparse.getLat(); top = sparse.getLat();
right = sparse.getLon(); right = sparse.getLon();
} else if (BOTTOM_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { } else if (BOTTOM_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
GeoUtils.parseGeoPoint(parser, sparse); GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.BOTTOM_LEFT);
bottom = sparse.getLat(); bottom = sparse.getLat();
left = sparse.getLon(); left = sparse.getLon();
} else { } else {
@ -515,7 +515,8 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
} }
} }
if (envelope != null) { if (envelope != null) {
if ((Double.isNaN(top) || Double.isNaN(bottom) || Double.isNaN(left) || Double.isNaN(right)) == false) { if (Double.isNaN(top) == false || Double.isNaN(bottom) == false || Double.isNaN(left) == false ||
Double.isNaN(right) == false) {
throw new ElasticsearchParseException("failed to parse bounding box. Conflicting definition found " throw new ElasticsearchParseException("failed to parse bounding box. Conflicting definition found "
+ "using well-known text and explicit corners."); + "using well-known text and explicit corners.");
} }

View File

@ -24,6 +24,7 @@ import org.apache.lucene.document.LatLonPoint;
import org.apache.lucene.search.IndexOrDocValuesQuery; import org.apache.lucene.search.IndexOrDocValuesQuery;
import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query; import org.apache.lucene.search.Query;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoUtils; import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MappedFieldType;
@ -450,6 +451,64 @@ public class GeoBoundingBoxQueryBuilderTests extends AbstractQueryTestCase<GeoBo
assertEquals(expectedJson, GeoExecType.MEMORY, parsed.type()); assertEquals(expectedJson, GeoExecType.MEMORY, parsed.type());
} }
public void testFromGeohash() throws IOException {
String json =
"{\n" +
" \"geo_bounding_box\" : {\n" +
" \"pin.location\" : {\n" +
" \"top_left\" : \"dr\",\n" +
" \"bottom_right\" : \"dq\"\n" +
" },\n" +
" \"validation_method\" : \"STRICT\",\n" +
" \"type\" : \"MEMORY\",\n" +
" \"ignore_unmapped\" : false,\n" +
" \"boost\" : 1.0\n" +
" }\n" +
"}";
String expectedJson =
"{\n" +
" \"geo_bounding_box\" : {\n" +
" \"pin.location\" : {\n" +
" \"top_left\" : [ -78.75, 45.0 ],\n" +
" \"bottom_right\" : [ -67.5, 33.75 ]\n" +
" },\n" +
" \"validation_method\" : \"STRICT\",\n" +
" \"type\" : \"MEMORY\",\n" +
" \"ignore_unmapped\" : false,\n" +
" \"boost\" : 1.0\n" +
" }\n" +
"}";
GeoBoundingBoxQueryBuilder parsed = (GeoBoundingBoxQueryBuilder) parseQuery(json);
checkGeneratedJson(expectedJson, parsed);
assertEquals(json, "pin.location", parsed.fieldName());
assertEquals(json, -78.75, parsed.topLeft().getLon(), 0.0001);
assertEquals(json, 45.0, parsed.topLeft().getLat(), 0.0001);
assertEquals(json, -67.5, parsed.bottomRight().getLon(), 0.0001);
assertEquals(json, 33.75, parsed.bottomRight().getLat(), 0.0001);
assertEquals(json, 1.0, parsed.boost(), 0.0001);
assertEquals(json, GeoExecType.MEMORY, parsed.type());
}
public void testMalformedGeohashes() {
String jsonGeohashAndWkt =
"{\n" +
" \"geo_bounding_box\" : {\n" +
" \"pin.location\" : {\n" +
" \"top_left\" : [ -78.75, 45.0 ],\n" +
" \"wkt\" : \"BBOX (-74.1, -71.12, 40.73, 40.01)\"\n" +
" },\n" +
" \"validation_method\" : \"STRICT\",\n" +
" \"type\" : \"MEMORY\",\n" +
" \"ignore_unmapped\" : false,\n" +
" \"boost\" : 1.0\n" +
" }\n" +
"}";
ElasticsearchParseException e1 = expectThrows(ElasticsearchParseException.class, () -> parseQuery(jsonGeohashAndWkt));
assertThat(e1.getMessage(), containsString("Conflicting definition found using well-known text and explicit corners."));
}
@Override @Override
public void testMustRewrite() throws IOException { public void testMustRewrite() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);

View File

@ -610,6 +610,20 @@ public class GeoUtilsTests extends ESTestCase {
} }
} }
public void testParseGeoPointGeohashPositions() throws IOException {
assertNormalizedPoint(parseGeohash("drt5", GeoUtils.EffectivePoint.TOP_LEFT), new GeoPoint(42.890625, -71.71875));
assertNormalizedPoint(parseGeohash("drt5", GeoUtils.EffectivePoint.TOP_RIGHT), new GeoPoint(42.890625, -71.3671875));
assertNormalizedPoint(parseGeohash("drt5", GeoUtils.EffectivePoint.BOTTOM_LEFT), new GeoPoint(42.71484375, -71.71875));
assertNormalizedPoint(parseGeohash("drt5", GeoUtils.EffectivePoint.BOTTOM_RIGHT), new GeoPoint(42.71484375, -71.3671875));
assertNormalizedPoint(parseGeohash("drtk", GeoUtils.EffectivePoint.BOTTOM_LEFT), new GeoPoint(42.890625, -71.3671875));
}
private GeoPoint parseGeohash(String geohash, GeoUtils.EffectivePoint effectivePoint) throws IOException {
XContentParser parser = createParser(jsonBuilder().startObject().field("geohash", geohash).endObject());
parser.nextToken();
return GeoUtils.parseGeoPoint(parser, new GeoPoint(), randomBoolean(), effectivePoint);
}
private static void assertNormalizedPoint(GeoPoint input, GeoPoint expected) { private static void assertNormalizedPoint(GeoPoint input, GeoPoint expected) {
GeoUtils.normalizePoint(input); GeoUtils.normalizePoint(input);
if (Double.isNaN(expected.lat())) { if (Double.isNaN(expected.lat())) {