Support distance units in GeoHashGrid aggregation precision (#26291)

Currently the `precision` parameter must be a precision level
in the range of [1,12]. In #5042 it was suggested also supporting
distance units like "1km" to automatically approcimate the needed
precision level. This change adds this support to the Rest API by
making use of GeoUtils#geoHashLevelsForPrecision.

Plain integer values without a unit are still treated as precision
levels like before. Distance values that are too small to be represented
by a precision level of 12 (values approx. less than 0.056m) are
rejected.

Closes #5042
This commit is contained in:
Christoph Büscher 2017-08-21 17:29:28 +02:00 committed by GitHub
parent 4ff12c9a0b
commit 5dae277bb2
3 changed files with 77 additions and 1 deletions

View File

@ -24,11 +24,13 @@ import org.apache.lucene.index.SortedNumericDocValues;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.geo.GeoHashUtils;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.index.fielddata.AbstractSortingNumericDocValues;
import org.elasticsearch.index.fielddata.MultiGeoPointValues;
import org.elasticsearch.index.fielddata.SortedBinaryDocValues;
@ -57,7 +59,29 @@ public class GeoGridAggregationBuilder extends ValuesSourceAggregationBuilder<Va
static {
PARSER = new ObjectParser<>(GeoGridAggregationBuilder.NAME);
ValuesSourceParserHelper.declareGeoFields(PARSER, false, false);
PARSER.declareInt(GeoGridAggregationBuilder::precision, GeoHashGridParams.FIELD_PRECISION);
PARSER.declareField((parser, builder, context) -> {
XContentParser.Token token = parser.currentToken();
if (token.equals(XContentParser.Token.VALUE_NUMBER)) {
builder.precision(XContentMapValues.nodeIntegerValue(parser.intValue()));
} else {
String precision = parser.text();
try {
// we want to treat simple integer strings as precision levels, not distances
builder.precision(XContentMapValues.nodeIntegerValue(Integer.parseInt(precision)));
} catch (NumberFormatException e) {
// try to parse as a distance value
try {
builder.precision(GeoUtils.geoHashLevelsForPrecision(precision));
} catch (NumberFormatException e2) {
// can happen when distance unit is unknown, in this case we simply want to know the reason
throw e2;
} catch (IllegalArgumentException e3) {
// this happens when distance too small, so precision > 12. We'd like to see the original string
throw new IllegalArgumentException("precision too high [" + precision + "]", e3);
}
}
}
}, GeoHashGridParams.FIELD_PRECISION, org.elasticsearch.common.xcontent.ObjectParser.ValueType.INT);
PARSER.declareInt(GeoGridAggregationBuilder::size, GeoHashGridParams.FIELD_SIZE);
PARSER.declareInt(GeoGridAggregationBuilder::shardSize, GeoHashGridParams.FIELD_SHARD_SIZE);
}

View File

@ -19,11 +19,14 @@
package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.test.ESTestCase;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
public class GeoHashGridParserTests extends ESTestCase {
public void testParseValidFromInts() throws Exception {
@ -46,6 +49,46 @@ public class GeoHashGridParserTests extends ESTestCase {
assertNotNull(GeoGridAggregationBuilder.parse("geohash_grid", stParser));
}
public void testParseDistanceUnitPrecision() throws Exception {
double distance = randomDoubleBetween(10.0, 100.00, true);
DistanceUnit unit = randomFrom(DistanceUnit.values());
if (unit.equals(DistanceUnit.MILLIMETERS)) {
distance = 5600 + randomDouble(); // 5.6cm is approx. smallest distance represented by precision 12
}
String distanceString = distance + unit.toString();
XContentParser stParser = createParser(JsonXContent.jsonXContent,
"{\"field\":\"my_loc\", \"precision\": \"" + distanceString + "\", \"size\": \"500\", \"shard_size\": \"550\"}");
XContentParser.Token token = stParser.nextToken();
assertSame(XContentParser.Token.START_OBJECT, token);
// can create a factory
GeoGridAggregationBuilder builder = GeoGridAggregationBuilder.parse("geohash_grid", stParser);
assertNotNull(builder);
assertThat(builder.precision(), greaterThanOrEqualTo(0));
assertThat(builder.precision(), lessThanOrEqualTo(12));
}
public void testParseInvalidUnitPrecision() throws Exception {
XContentParser stParser = createParser(JsonXContent.jsonXContent,
"{\"field\":\"my_loc\", \"precision\": \"10kg\", \"size\": \"500\", \"shard_size\": \"550\"}");
XContentParser.Token token = stParser.nextToken();
assertSame(XContentParser.Token.START_OBJECT, token);
ParsingException ex = expectThrows(ParsingException.class, () -> GeoGridAggregationBuilder.parse("geohash_grid", stParser));
assertEquals("[geohash_grid] failed to parse field [precision]", ex.getMessage());
assertThat(ex.getCause(), instanceOf(NumberFormatException.class));
assertEquals("For input string: \"10kg\"", ex.getCause().getMessage());
}
public void testParseDistanceUnitPrecisionTooSmall() throws Exception {
XContentParser stParser = createParser(JsonXContent.jsonXContent,
"{\"field\":\"my_loc\", \"precision\": \"1cm\", \"size\": \"500\", \"shard_size\": \"550\"}");
XContentParser.Token token = stParser.nextToken();
assertSame(XContentParser.Token.START_OBJECT, token);
ParsingException ex = expectThrows(ParsingException.class, () -> GeoGridAggregationBuilder.parse("geohash_grid", stParser));
assertEquals("[geohash_grid] failed to parse field [precision]", ex.getMessage());
assertThat(ex.getCause(), instanceOf(IllegalArgumentException.class));
assertEquals("precision too high [1cm]", ex.getCause().getMessage());
}
public void testParseErrorOnBooleanPrecision() throws Exception {
XContentParser stParser = createParser(JsonXContent.jsonXContent, "{\"field\":\"my_loc\", \"precision\":false}");
XContentParser.Token token = stParser.nextToken();

View File

@ -149,6 +149,15 @@ field:: Mandatory. The name of the field indexed with GeoPoints.
precision:: Optional. The string length of the geohashes used to define
cells/buckets in the results. Defaults to 5.
The precision can either be defined in terms of the integer
precision levels mentioned above. Values outside of [1,12] will
be rejected.
Alternatively, the precision level can be approximated from a
distance measure like "1km", "10m". The precision level is
calculate such that cells will not exceed the specified
size (diagonal) of the required precision. When this would lead
to precision levels higher than the supported 12 levels,
(e.g. for distances <5.6cm) the value is rejected.
size:: Optional. The maximum number of geohash buckets to return
(defaults to 10,000). When results are trimmed, buckets are