mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-02-17 02:14:54 +00:00
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:
parent
b3a4acdf20
commit
cf0e0606af
@ -12,6 +12,9 @@
|
||||
* Purely negative queries (only MUST_NOT clauses) now return a score of `0`
|
||||
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 has been enabled by default. If you wish to return to
|
||||
|
@ -231,6 +231,38 @@ GET /_search
|
||||
--------------------------------------------------
|
||||
// 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]
|
||||
==== Vertices
|
||||
|
||||
|
@ -22,6 +22,7 @@ package org.elasticsearch.common.geo;
|
||||
import org.apache.lucene.document.LatLonDocValuesField;
|
||||
import org.apache.lucene.document.LatLonPoint;
|
||||
import org.apache.lucene.geo.GeoEncodingUtils;
|
||||
import org.apache.lucene.geo.Rectangle;
|
||||
import org.apache.lucene.index.IndexableField;
|
||||
import org.apache.lucene.util.BitUtil;
|
||||
import org.apache.lucene.util.BytesRef;
|
||||
@ -85,21 +86,27 @@ public final class GeoPoint implements ToXContentFragment {
|
||||
|
||||
public GeoPoint resetFromString(String value, final boolean ignoreZValue) {
|
||||
if (value.contains(",")) {
|
||||
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);
|
||||
return resetFromCoordinates(value, ignoreZValue);
|
||||
}
|
||||
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) {
|
||||
lon = GeoHashUtils.decodeLongitude(hash);
|
||||
lat = GeoHashUtils.decodeLatitude(hash);
|
||||
|
@ -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:
|
||||
*
|
||||
@ -401,7 +420,7 @@ public class GeoUtils {
|
||||
* @param point A {@link GeoPoint} that will be reset by the values parsed
|
||||
* @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 {
|
||||
double lat = Double.NaN;
|
||||
double lon = Double.NaN;
|
||||
@ -458,7 +477,7 @@ public class GeoUtils {
|
||||
if(!Double.isNaN(lat) || !Double.isNaN(lon)) {
|
||||
throw new ElasticsearchParseException("field must be either lat/lon or geohash");
|
||||
} else {
|
||||
return point.resetFromGeoHash(geohash);
|
||||
return parseGeoHash(point, geohash, effectivePoint);
|
||||
}
|
||||
} else if (numberFormatException != null) {
|
||||
throw new ElasticsearchParseException("[{}] and [{}] must be valid double values", numberFormatException, LATITUDE,
|
||||
@ -489,12 +508,36 @@ public class GeoUtils {
|
||||
}
|
||||
return point.reset(lat, lon);
|
||||
} 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 {
|
||||
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".
|
||||
*
|
||||
|
@ -491,19 +491,19 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
|
||||
right = parser.doubleValue();
|
||||
} else {
|
||||
if (TOP_LEFT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
|
||||
GeoUtils.parseGeoPoint(parser, sparse);
|
||||
GeoUtils.parseGeoPoint(parser, sparse, false, GeoUtils.EffectivePoint.TOP_LEFT);
|
||||
top = sparse.getLat();
|
||||
left = sparse.getLon();
|
||||
} 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();
|
||||
right = sparse.getLon();
|
||||
} 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();
|
||||
right = sparse.getLon();
|
||||
} 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();
|
||||
left = sparse.getLon();
|
||||
} else {
|
||||
@ -515,7 +515,8 @@ public class GeoBoundingBoxQueryBuilder extends AbstractQueryBuilder<GeoBounding
|
||||
}
|
||||
}
|
||||
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 "
|
||||
+ "using well-known text and explicit corners.");
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import org.apache.lucene.document.LatLonPoint;
|
||||
import org.apache.lucene.search.IndexOrDocValuesQuery;
|
||||
import org.apache.lucene.search.MatchNoDocsQuery;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.elasticsearch.ElasticsearchParseException;
|
||||
import org.elasticsearch.common.geo.GeoPoint;
|
||||
import org.elasticsearch.common.geo.GeoUtils;
|
||||
import org.elasticsearch.index.mapper.MappedFieldType;
|
||||
@ -450,6 +451,64 @@ public class GeoBoundingBoxQueryBuilderTests extends AbstractQueryTestCase<GeoBo
|
||||
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
|
||||
public void testMustRewrite() throws IOException {
|
||||
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
|
||||
|
@ -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) {
|
||||
GeoUtils.normalizePoint(input);
|
||||
if (Double.isNaN(expected.lat())) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user