geotile_grid implementation (#37842)

Implements `geotile_grid` aggregation

This patch refactors previous implementation https://github.com/elastic/elasticsearch/pull/30240

This code uses the same base classes as `geohash_grid` agg, but uses a different hashing
algorithm to allow zoom consistency.  Each grid bucket is aligned to Web Mercator tiles.
This commit is contained in:
Yuri Astrakhan 2019-01-31 19:11:30 -05:00 committed by GitHub
parent 6c1e9fad47
commit f3cde06a1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1310 additions and 3 deletions

View File

@ -95,6 +95,8 @@ import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilter;
import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilters; import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilters;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoHashGridAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoHashGridAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.geogrid.ParsedGeoHashGrid; import org.elasticsearch.search.aggregations.bucket.geogrid.ParsedGeoHashGrid;
import org.elasticsearch.search.aggregations.bucket.geogrid.ParsedGeoTileGrid;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileGridAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.global.ParsedGlobal; import org.elasticsearch.search.aggregations.bucket.global.ParsedGlobal;
import org.elasticsearch.search.aggregations.bucket.histogram.AutoDateHistogramAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.AutoDateHistogramAggregationBuilder;
@ -1760,6 +1762,7 @@ public class RestHighLevelClient implements Closeable {
map.put(FilterAggregationBuilder.NAME, (p, c) -> ParsedFilter.fromXContent(p, (String) c)); map.put(FilterAggregationBuilder.NAME, (p, c) -> ParsedFilter.fromXContent(p, (String) c));
map.put(InternalSampler.PARSER_NAME, (p, c) -> ParsedSampler.fromXContent(p, (String) c)); map.put(InternalSampler.PARSER_NAME, (p, c) -> ParsedSampler.fromXContent(p, (String) c));
map.put(GeoHashGridAggregationBuilder.NAME, (p, c) -> ParsedGeoHashGrid.fromXContent(p, (String) c)); map.put(GeoHashGridAggregationBuilder.NAME, (p, c) -> ParsedGeoHashGrid.fromXContent(p, (String) c));
map.put(GeoTileGridAggregationBuilder.NAME, (p, c) -> ParsedGeoTileGrid.fromXContent(p, (String) c));
map.put(RangeAggregationBuilder.NAME, (p, c) -> ParsedRange.fromXContent(p, (String) c)); map.put(RangeAggregationBuilder.NAME, (p, c) -> ParsedRange.fromXContent(p, (String) c));
map.put(DateRangeAggregationBuilder.NAME, (p, c) -> ParsedDateRange.fromXContent(p, (String) c)); map.put(DateRangeAggregationBuilder.NAME, (p, c) -> ParsedDateRange.fromXContent(p, (String) c));
map.put(GeoDistanceAggregationBuilder.NAME, (p, c) -> ParsedGeoDistance.fromXContent(p, (String) c)); map.put(GeoDistanceAggregationBuilder.NAME, (p, c) -> ParsedGeoDistance.fromXContent(p, (String) c));

View File

@ -0,0 +1,185 @@
[[search-aggregations-bucket-geotilegrid-aggregation]]
=== GeoTile Grid Aggregation
A multi-bucket aggregation that works on `geo_point` fields and groups points into
buckets that represent cells in a grid. The resulting grid can be sparse and only
contains cells that have matching data. Each cell corresponds to a
https://en.wikipedia.org/wiki/Tiled_web_map[map tile] as used by many online map
sites. Each cell is labeled using a "{zoom}/{x}/{y}" format, where zoom is equal
to the user-specified precision.
* High precision keys have a larger range for x and y, and represent tiles that
cover only a small area.
* Low precision keys have a smaller range for x and y, and represent tiles that
each cover a large area.
See https://wiki.openstreetmap.org/wiki/Zoom_levels[Zoom level documentation]
on how precision (zoom) correlates to size on the ground. Precision for this
aggregation can be between 0 and 29, inclusive.
WARNING: The highest-precision geotile of length 29 produces cells that cover
less than a 10cm by 10cm of land and so high-precision requests can be very
costly in terms of RAM and result sizes. Please see the example below on how
to first filter the aggregation to a smaller geographic area before requesting
high-levels of detail.
The specified field must be of type `geo_point` (which can only be set
explicitly in the mappings) and it can also hold an array of `geo_point`
fields, in which case all points will be taken into account during aggregation.
==== Simple low-precision request
[source,js]
--------------------------------------------------
PUT /museums
{
"mappings": {
"properties": {
"location": {
"type": "geo_point"
}
}
}
}
POST /museums/_bulk?refresh
{"index":{"_id":1}}
{"location": "52.374081,4.912350", "name": "NEMO Science Museum"}
{"index":{"_id":2}}
{"location": "52.369219,4.901618", "name": "Museum Het Rembrandthuis"}
{"index":{"_id":3}}
{"location": "52.371667,4.914722", "name": "Nederlands Scheepvaartmuseum"}
{"index":{"_id":4}}
{"location": "51.222900,4.405200", "name": "Letterenhuis"}
{"index":{"_id":5}}
{"location": "48.861111,2.336389", "name": "Musée du Louvre"}
{"index":{"_id":6}}
{"location": "48.860000,2.327000", "name": "Musée d'Orsay"}
POST /museums/_search?size=0
{
"aggregations" : {
"large-grid" : {
"geotile_grid" : {
"field" : "location",
"precision" : 8
}
}
}
}
--------------------------------------------------
// CONSOLE
Response:
[source,js]
--------------------------------------------------
{
...
"aggregations": {
"large-grid": {
"buckets": [
{
"key" : "8/131/84",
"doc_count" : 3
},
{
"key" : "8/129/88",
"doc_count" : 2
},
{
"key" : "8/131/85",
"doc_count" : 1
}
]
}
}
}
--------------------------------------------------
// TESTRESPONSE[s/\.\.\./"took": $body.took,"_shards": $body._shards,"hits":$body.hits,"timed_out":false,/]
==== High-precision requests
When requesting detailed buckets (typically for displaying a "zoomed in" map)
a filter like <<query-dsl-geo-bounding-box-query,geo_bounding_box>> should be
applied to narrow the subject area otherwise potentially millions of buckets
will be created and returned.
[source,js]
--------------------------------------------------
POST /museums/_search?size=0
{
"aggregations" : {
"zoomed-in" : {
"filter" : {
"geo_bounding_box" : {
"location" : {
"top_left" : "52.4, 4.9",
"bottom_right" : "52.3, 5.0"
}
}
},
"aggregations":{
"zoom1":{
"geotile_grid" : {
"field": "location",
"precision": 22
}
}
}
}
}
}
--------------------------------------------------
// CONSOLE
// TEST[continued]
[source,js]
--------------------------------------------------
{
...
"aggregations" : {
"zoomed-in" : {
"doc_count" : 3,
"zoom1" : {
"buckets" : [
{
"key" : "22/2154412/1378379",
"doc_count" : 1
},
{
"key" : "22/2154385/1378332",
"doc_count" : 1
},
{
"key" : "22/2154259/1378425",
"doc_count" : 1
}
]
}
}
}
}
--------------------------------------------------
// TESTRESPONSE[s/\.\.\./"took": $body.took,"_shards": $body._shards,"hits":$body.hits,"timed_out":false,/]
==== Options
[horizontal]
field:: Mandatory. The name of the field indexed with GeoPoints.
precision:: Optional. The integer zoom of the key used to define
cells/buckets in the results. Defaults to 7.
Values outside of [0,29] will be rejected.
size:: Optional. The maximum number of geohash buckets to return
(defaults to 10,000). When results are trimmed, buckets are
prioritised based on the volumes of documents they contain.
shard_size:: Optional. To allow for more accurate counting of the top cells
returned in the final result the aggregation defaults to
returning `max(10,(size x number-of-shards))` buckets from each
shard. If this heuristic is undesirable, the number considered
from each shard can be over-ridden using this parameter.

View File

@ -0,0 +1,65 @@
setup:
- skip:
version: " - 6.99.99"
reason: "added in 7.0.0"
- do:
indices.create:
include_type_name: false
index: test_1
body:
settings:
number_of_replicas: 0
mappings:
properties:
location:
type: geo_point
---
"Basic test":
- do:
bulk:
refresh: true
body:
- index:
_index: test_1
_id: 1
- location: "52.374081,4.912350"
- index:
_index: test_1
_id: 2
- location: "52.369219,4.901618"
- index:
_index: test_1
_id: 3
- location: "52.371667,4.914722"
- index:
_index: test_1
_id: 4
- location: "51.222900,4.405200"
- index:
_index: test_1
_id: 5
- location: "48.861111,2.336389"
- index:
_index: test_1
_id: 6
- location: "48.860000,2.327000"
- do:
search:
rest_total_hits_as_int: true
body:
aggregations:
grid:
geotile_grid:
field: location
precision: 8
- match: { hits.total: 6 }
- match: { aggregations.grid.buckets.0.key: "8/131/84" }
- match: { aggregations.grid.buckets.0.doc_count: 3 }
- match: { aggregations.grid.buckets.1.key: "8/129/88" }
- match: { aggregations.grid.buckets.1.doc_count: 2 }
- match: { aggregations.grid.buckets.2.key: "8/131/85" }
- match: { aggregations.grid.buckets.2.doc_count: 1 }

View File

@ -110,6 +110,8 @@ import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter;
import org.elasticsearch.search.aggregations.bucket.filter.InternalFilters; import org.elasticsearch.search.aggregations.bucket.filter.InternalFilters;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoHashGridAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoHashGridAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.geogrid.InternalGeoHashGrid; import org.elasticsearch.search.aggregations.bucket.geogrid.InternalGeoHashGrid;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileGridAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.geogrid.InternalGeoTileGrid;
import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.global.InternalGlobal; import org.elasticsearch.search.aggregations.bucket.global.InternalGlobal;
import org.elasticsearch.search.aggregations.bucket.histogram.AutoDateHistogramAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.AutoDateHistogramAggregationBuilder;
@ -422,6 +424,8 @@ public class SearchModule {
GeoDistanceAggregationBuilder::parse).addResultReader(InternalGeoDistance::new)); GeoDistanceAggregationBuilder::parse).addResultReader(InternalGeoDistance::new));
registerAggregation(new AggregationSpec(GeoHashGridAggregationBuilder.NAME, GeoHashGridAggregationBuilder::new, registerAggregation(new AggregationSpec(GeoHashGridAggregationBuilder.NAME, GeoHashGridAggregationBuilder::new,
GeoHashGridAggregationBuilder::parse).addResultReader(InternalGeoHashGrid::new)); GeoHashGridAggregationBuilder::parse).addResultReader(InternalGeoHashGrid::new));
registerAggregation(new AggregationSpec(GeoTileGridAggregationBuilder.NAME, GeoTileGridAggregationBuilder::new,
GeoTileGridAggregationBuilder::parse).addResultReader(InternalGeoTileGrid::new));
registerAggregation(new AggregationSpec(NestedAggregationBuilder.NAME, NestedAggregationBuilder::new, registerAggregation(new AggregationSpec(NestedAggregationBuilder.NAME, NestedAggregationBuilder::new,
NestedAggregationBuilder::parse).addResultReader(InternalNested::new)); NestedAggregationBuilder::parse).addResultReader(InternalNested::new));
registerAggregation(new AggregationSpec(ReverseNestedAggregationBuilder.NAME, ReverseNestedAggregationBuilder::new, registerAggregation(new AggregationSpec(ReverseNestedAggregationBuilder.NAME, ReverseNestedAggregationBuilder::new,

View File

@ -30,6 +30,8 @@ import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregationBui
import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator.KeyedFilter; import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator.KeyedFilter;
import org.elasticsearch.search.aggregations.bucket.geogrid.InternalGeoHashGrid; import org.elasticsearch.search.aggregations.bucket.geogrid.InternalGeoHashGrid;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoHashGridAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoHashGridAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.geogrid.InternalGeoTileGrid;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileGridAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.global.Global; import org.elasticsearch.search.aggregations.bucket.global.Global;
import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder;
@ -250,6 +252,13 @@ public class AggregationBuilders {
return new GeoHashGridAggregationBuilder(name); return new GeoHashGridAggregationBuilder(name);
} }
/**
* Create a new {@link InternalGeoTileGrid} aggregation with the given name.
*/
public static GeoTileGridAggregationBuilder geotileGrid(String name) {
return new GeoTileGridAggregationBuilder(name);
}
/** /**
* Create a new {@link SignificantTerms} aggregation with the given name. * Create a new {@link SignificantTerms} aggregation with the given name.
*/ */

View File

@ -0,0 +1,88 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregatorFactories;
import org.elasticsearch.search.aggregations.AggregatorFactory;
import org.elasticsearch.search.aggregations.support.ValuesSource;
import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory;
import org.elasticsearch.search.aggregations.support.ValuesSourceConfig;
import org.elasticsearch.search.internal.SearchContext;
import java.io.IOException;
import java.util.Map;
public class GeoTileGridAggregationBuilder extends GeoGridAggregationBuilder {
public static final String NAME = "geotile_grid";
private static final int DEFAULT_PRECISION = 7;
private static final int DEFAULT_MAX_NUM_CELLS = 10000;
private static final ObjectParser<GeoGridAggregationBuilder, Void> PARSER = createParser(NAME, GeoTileUtils::parsePrecision);
public GeoTileGridAggregationBuilder(String name) {
super(name);
precision(DEFAULT_PRECISION);
size(DEFAULT_MAX_NUM_CELLS);
shardSize = -1;
}
public GeoTileGridAggregationBuilder(StreamInput in) throws IOException {
super(in);
}
@Override
public GeoGridAggregationBuilder precision(int precision) {
this.precision = GeoTileUtils.checkPrecisionRange(precision);
return this;
}
@Override
protected ValuesSourceAggregatorFactory<ValuesSource.GeoPoint, ?> createFactory(
String name, ValuesSourceConfig<ValuesSource.GeoPoint> config, int precision, int requiredSize, int shardSize,
SearchContext context, AggregatorFactory<?> parent, AggregatorFactories.Builder subFactoriesBuilder,
Map<String, Object> metaData
) throws IOException {
return new GeoTileGridAggregatorFactory(name, config, precision, requiredSize, shardSize, context, parent,
subFactoriesBuilder, metaData);
}
private GeoTileGridAggregationBuilder(GeoTileGridAggregationBuilder clone, AggregatorFactories.Builder factoriesBuilder,
Map<String, Object> metaData) {
super(clone, factoriesBuilder, metaData);
}
@Override
protected AggregationBuilder shallowCopy(AggregatorFactories.Builder factoriesBuilder, Map<String, Object> metaData) {
return new GeoTileGridAggregationBuilder(this, factoriesBuilder, metaData);
}
public static GeoGridAggregationBuilder parse(String aggregationName, XContentParser parser) throws IOException {
return PARSER.parse(parser, new GeoTileGridAggregationBuilder(aggregationName), null);
}
@Override
public String getType() {
return NAME;
}
}

View File

@ -0,0 +1,57 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.elasticsearch.search.aggregations.Aggregator;
import org.elasticsearch.search.aggregations.AggregatorFactories;
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
import org.elasticsearch.search.internal.SearchContext;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Aggregates data expressed as geotile longs (for efficiency's sake) but formats results as geotile strings.
*/
public class GeoTileGridAggregator extends GeoGridAggregator<InternalGeoTileGrid> {
GeoTileGridAggregator(String name, AggregatorFactories factories, CellIdSource valuesSource,
int requiredSize, int shardSize, SearchContext aggregationContext, Aggregator parent,
List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) throws IOException {
super(name, factories, valuesSource, requiredSize, shardSize, aggregationContext, parent, pipelineAggregators, metaData);
}
@Override
InternalGeoTileGrid buildAggregation(String name, int requiredSize, List<InternalGeoGridBucket> buckets,
List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) {
return new InternalGeoTileGrid(name, requiredSize, buckets, pipelineAggregators, metaData);
}
@Override
public InternalGeoTileGrid buildEmptyAggregation() {
return new InternalGeoTileGrid(name, requiredSize, Collections.emptyList(), pipelineAggregators(), metaData());
}
InternalGeoGridBucket newEmptyBucket() {
return new InternalGeoTileGridBucket(0, 0, null);
}
}

View File

@ -0,0 +1,78 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.elasticsearch.search.aggregations.Aggregator;
import org.elasticsearch.search.aggregations.AggregatorFactories;
import org.elasticsearch.search.aggregations.AggregatorFactory;
import org.elasticsearch.search.aggregations.InternalAggregation;
import org.elasticsearch.search.aggregations.NonCollectingAggregator;
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
import org.elasticsearch.search.aggregations.support.ValuesSource;
import org.elasticsearch.search.aggregations.support.ValuesSource.GeoPoint;
import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory;
import org.elasticsearch.search.aggregations.support.ValuesSourceConfig;
import org.elasticsearch.search.internal.SearchContext;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class GeoTileGridAggregatorFactory extends ValuesSourceAggregatorFactory<ValuesSource.GeoPoint, GeoTileGridAggregatorFactory> {
private final int precision;
private final int requiredSize;
private final int shardSize;
GeoTileGridAggregatorFactory(String name, ValuesSourceConfig<GeoPoint> config, int precision, int requiredSize,
int shardSize, SearchContext context, AggregatorFactory<?> parent,
AggregatorFactories.Builder subFactoriesBuilder, Map<String, Object> metaData
) throws IOException {
super(name, config, context, parent, subFactoriesBuilder, metaData);
this.precision = precision;
this.requiredSize = requiredSize;
this.shardSize = shardSize;
}
@Override
protected Aggregator createUnmapped(Aggregator parent, List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData)
throws IOException {
final InternalAggregation aggregation = new InternalGeoTileGrid(name, requiredSize,
Collections.emptyList(), pipelineAggregators, metaData);
return new NonCollectingAggregator(name, context, parent, pipelineAggregators, metaData) {
@Override
public InternalAggregation buildEmptyAggregation() {
return aggregation;
}
};
}
@Override
protected Aggregator doCreateInternal(final ValuesSource.GeoPoint valuesSource, Aggregator parent, boolean collectsFromSingleBucket,
List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) throws IOException {
if (collectsFromSingleBucket == false) {
return asMultiBucketAggregator(this, context, parent);
}
CellIdSource cellIdSource = new CellIdSource(valuesSource, precision, GeoTileUtils::longEncode);
return new GeoTileGridAggregator(name, factories, cellIdSource, requiredSize, shardSize, context, parent,
pipelineAggregators, metaData);
}
}

View File

@ -0,0 +1,195 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import java.io.IOException;
import java.util.Locale;
import static org.elasticsearch.common.geo.GeoUtils.normalizeLat;
import static org.elasticsearch.common.geo.GeoUtils.normalizeLon;
/**
* Implements geotile key hashing, same as used by many map tile implementations.
* The string key is formatted as "zoom/x/y"
* The hash value (long) contains all three of those values compacted into a single 64bit value:
* bits 58..63 -- zoom (0..29)
* bits 29..57 -- X tile index (0..2^zoom)
* bits 0..28 -- Y tile index (0..2^zoom)
*/
final class GeoTileUtils {
private GeoTileUtils() {}
/**
* Largest number of tiles (precision) to use.
* This value cannot be more than (64-5)/2 = 29, because 5 bits are used for zoom level itself (0-31)
* If zoom is not stored inside hash, it would be possible to use up to 32.
* Note that changing this value will make serialization binary-incompatible between versions.
* Another consideration is that index optimizes lat/lng storage, loosing some precision.
* E.g. hash lng=140.74779717298918D lat=45.61884022447444D == "18/233561/93659", but shown as "18/233561/93658"
*/
static final int MAX_ZOOM = 29;
/**
* Bit position of the zoom value within hash - zoom is stored in the most significant 6 bits of a long number.
*/
private static final int ZOOM_SHIFT = MAX_ZOOM * 2;
/**
* Bit mask to extract just the lowest 29 bits of a long
*/
private static final long X_Y_VALUE_MASK = (1L << MAX_ZOOM) - 1;
/**
* Parse an integer precision (zoom level). The {@link ValueType#INT} allows it to be a number or a string.
*
* The precision is expressed as a zoom level between 0 and {@link #MAX_ZOOM} (inclusive).
*
* @param parser {@link XContentParser} to parse the value from
* @return int representing precision
*/
static int parsePrecision(XContentParser parser) throws IOException, ElasticsearchParseException {
final Object node = parser.currentToken().equals(XContentParser.Token.VALUE_NUMBER)
? Integer.valueOf(parser.intValue())
: parser.text();
return XContentMapValues.nodeIntegerValue(node);
}
/**
* Assert the precision value is within the allowed range, and return it if ok, or throw.
*/
static int checkPrecisionRange(int precision) {
if (precision < 0 || precision > MAX_ZOOM) {
throw new IllegalArgumentException("Invalid geotile_grid precision of " +
precision + ". Must be between 0 and " + MAX_ZOOM + ".");
}
return precision;
}
/**
* Encode lon/lat to the geotile based long format.
* The resulting hash contains interleaved tile X and Y coordinates.
* The precision itself is also encoded as a few high bits.
*/
static long longEncode(double longitude, double latitude, int precision) {
// Mathematics for this code was adapted from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Java
// Number of tiles for the current zoom level along the X and Y axis
final long tiles = 1 << checkPrecisionRange(precision);
long xTile = (long) Math.floor((normalizeLon(longitude) + 180) / 360 * tiles);
double latSin = Math.sin(Math.toRadians(normalizeLat(latitude)));
long yTile = (long) Math.floor((0.5 - (Math.log((1 + latSin) / (1 - latSin)) / (4 * Math.PI))) * tiles);
// Edge values may generate invalid values, and need to be clipped.
// For example, polar regions (above/below lat 85.05112878) get normalized.
if (xTile < 0) {
xTile = 0;
}
if (xTile >= tiles) {
xTile = tiles - 1;
}
if (yTile < 0) {
yTile = 0;
}
if (yTile >= tiles) {
yTile = tiles - 1;
}
// Zoom value is placed in front of all the bits used for the geotile
// e.g. when max zoom is 29, the largest index would use 58 bits (57th..0th),
// leaving 5 bits unused for zoom. See MAX_ZOOM comment above.
return ((long) precision << ZOOM_SHIFT) | (xTile << MAX_ZOOM) | yTile;
}
/**
* Parse geotile hash as zoom, x, y integers.
*/
private static int[] parseHash(long hash) {
final int zoom = (int) (hash >>> ZOOM_SHIFT);
final int xTile = (int) ((hash >>> MAX_ZOOM) & X_Y_VALUE_MASK);
final int yTile = (int) (hash & X_Y_VALUE_MASK);
return new int[]{zoom, xTile, yTile};
}
/**
* Encode to a geotile string from the geotile based long format
*/
static String stringEncode(long hash) {
int[] res = parseHash(hash);
validateZXY(res[0], res[1], res[2]);
return "" + res[0] + "/" + res[1] + "/" + res[2];
}
/**
* Decode long hash as a GeoPoint (center of the tile)
*/
static GeoPoint hashToGeoPoint(long hash) {
int[] res = parseHash(hash);
return zxyToGeoPoint(res[0], res[1], res[2]);
}
/**
* Decode a string bucket key in "zoom/x/y" format to a GeoPoint (center of the tile)
*/
static GeoPoint keyToGeoPoint(String hashAsString) {
final String[] parts = hashAsString.split("/", 4);
if (parts.length != 3) {
throw new IllegalArgumentException("Invalid geotile_grid hash string of " +
hashAsString + ". Must be three integers in a form \"zoom/x/y\".");
}
try {
return zxyToGeoPoint(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2]));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid geotile_grid hash string of " +
hashAsString + ". Must be three integers in a form \"zoom/x/y\".", e);
}
}
/**
* Validates Zoom, X, and Y values, and returns the total number of allowed tiles along the x/y axis.
*/
private static int validateZXY(int zoom, int xTile, int yTile) {
final int tiles = 1 << checkPrecisionRange(zoom);
if (xTile < 0 || yTile < 0 || xTile >= tiles || yTile >= tiles) {
throw new IllegalArgumentException(String.format(
Locale.ROOT, "Zoom/X/Y combination is not valid: %d/%d/%d", zoom, xTile, yTile));
}
return tiles;
}
/**
* Converts zoom/x/y integers into a GeoPoint.
*/
private static GeoPoint zxyToGeoPoint(int zoom, int xTile, int yTile) {
final int tiles = validateZXY(zoom, xTile, yTile);
final double n = Math.PI - (2.0 * Math.PI * (yTile + 0.5)) / tiles;
final double lat = Math.toDegrees(Math.atan(Math.sinh(n)));
final double lon = ((xTile + 0.5) / tiles * 360.0) - 180;
return new GeoPoint(lat, lon);
}
}

View File

@ -33,8 +33,6 @@ import java.util.Map;
*/ */
public class InternalGeoHashGrid extends InternalGeoGrid<InternalGeoHashGridBucket> { public class InternalGeoHashGrid extends InternalGeoGrid<InternalGeoHashGridBucket> {
private static final String NAME = "geohash_grid";
InternalGeoHashGrid(String name, int requiredSize, List<InternalGeoGridBucket> buckets, InternalGeoHashGrid(String name, int requiredSize, List<InternalGeoGridBucket> buckets,
List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) { List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) {
super(name, requiredSize, buckets, pipelineAggregators, metaData); super(name, requiredSize, buckets, pipelineAggregators, metaData);
@ -66,6 +64,6 @@ public class InternalGeoHashGrid extends InternalGeoGrid<InternalGeoHashGridBuck
@Override @Override
public String getWriteableName() { public String getWriteableName() {
return NAME; return GeoHashGridAggregationBuilder.NAME;
} }
} }

View File

@ -0,0 +1,69 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.search.aggregations.InternalAggregations;
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* Represents a grid of cells where each cell's location is determined by a geohash.
* All geohashes in a grid are of the same precision and held internally as a single long
* for efficiency's sake.
*/
public class InternalGeoTileGrid extends InternalGeoGrid<InternalGeoTileGridBucket> {
InternalGeoTileGrid(String name, int requiredSize, List<InternalGeoGridBucket> buckets,
List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) {
super(name, requiredSize, buckets, pipelineAggregators, metaData);
}
public InternalGeoTileGrid(StreamInput in) throws IOException {
super(in);
}
@Override
public InternalGeoGrid create(List<InternalGeoGridBucket> buckets) {
return new InternalGeoTileGrid(name, requiredSize, buckets, pipelineAggregators(), metaData);
}
@Override
public InternalGeoGridBucket createBucket(InternalAggregations aggregations, InternalGeoGridBucket prototype) {
return new InternalGeoTileGridBucket(prototype.hashAsLong, prototype.docCount, aggregations);
}
@Override
InternalGeoGrid create(String name, int requiredSize, List buckets, List list, Map metaData) {
return new InternalGeoTileGrid(name, requiredSize, buckets, list, metaData);
}
@Override
Reader getBucketReader() {
return InternalGeoTileGridBucket::new;
}
@Override
public String getWriteableName() {
return GeoTileGridAggregationBuilder.NAME;
}
}

View File

@ -0,0 +1,55 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.search.aggregations.InternalAggregations;
import java.io.IOException;
public class InternalGeoTileGridBucket extends InternalGeoGridBucket<InternalGeoTileGridBucket> {
InternalGeoTileGridBucket(long hashAsLong, long docCount, InternalAggregations aggregations) {
super(hashAsLong, docCount, aggregations);
}
/**
* Read from a stream.
*/
public InternalGeoTileGridBucket(StreamInput in) throws IOException {
super(in);
}
@Override
InternalGeoTileGridBucket buildBucket(InternalGeoGridBucket bucket, long hashAsLong, long docCount,
InternalAggregations aggregations) {
return new InternalGeoTileGridBucket(hashAsLong, docCount, aggregations);
}
@Override
public String getKeyAsString() {
return GeoTileUtils.stringEncode(hashAsLong);
}
@Override
public GeoPoint getKey() {
return GeoTileUtils.hashToGeoPoint(hashAsLong);
}
}

View File

@ -0,0 +1,42 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
public class ParsedGeoTileGrid extends ParsedGeoGrid {
private static ObjectParser<ParsedGeoGrid, Void> PARSER = createParser(ParsedGeoTileGrid::new,
ParsedGeoTileGridBucket::fromXContent, ParsedGeoTileGridBucket::fromXContent);
public static ParsedGeoGrid fromXContent(XContentParser parser, String name) throws IOException {
ParsedGeoGrid aggregation = PARSER.parse(parser, null);
aggregation.setName(name);
return aggregation;
}
@Override
public String getType() {
return GeoTileGridAggregationBuilder.NAME;
}
}

View File

@ -0,0 +1,42 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
public class ParsedGeoTileGridBucket extends ParsedGeoGridBucket {
@Override
public GeoPoint getKey() {
return GeoTileUtils.keyToGeoPoint(hashAsString);
}
@Override
public String getKeyAsString() {
return hashAsString;
}
static ParsedGeoTileGridBucket fromXContent(XContentParser parser) throws IOException {
return parseXContent(parser, false, ParsedGeoTileGridBucket::new, (p, bucket) -> bucket.hashAsString = p.text());
}
}

View File

@ -36,6 +36,7 @@ import org.elasticsearch.search.aggregations.bucket.composite.InternalCompositeT
import org.elasticsearch.search.aggregations.bucket.filter.InternalFilterTests; import org.elasticsearch.search.aggregations.bucket.filter.InternalFilterTests;
import org.elasticsearch.search.aggregations.bucket.filter.InternalFiltersTests; import org.elasticsearch.search.aggregations.bucket.filter.InternalFiltersTests;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoHashGridTests; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoHashGridTests;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileGridTests;
import org.elasticsearch.search.aggregations.bucket.global.InternalGlobalTests; import org.elasticsearch.search.aggregations.bucket.global.InternalGlobalTests;
import org.elasticsearch.search.aggregations.bucket.histogram.InternalAutoDateHistogramTests; import org.elasticsearch.search.aggregations.bucket.histogram.InternalAutoDateHistogramTests;
import org.elasticsearch.search.aggregations.bucket.histogram.InternalDateHistogramTests; import org.elasticsearch.search.aggregations.bucket.histogram.InternalDateHistogramTests;
@ -140,6 +141,7 @@ public class AggregationsTests extends ESTestCase {
aggsTests.add(new InternalFilterTests()); aggsTests.add(new InternalFilterTests());
aggsTests.add(new InternalSamplerTests()); aggsTests.add(new InternalSamplerTests());
aggsTests.add(new GeoHashGridTests()); aggsTests.add(new GeoHashGridTests());
aggsTests.add(new GeoTileGridTests());
aggsTests.add(new InternalRangeTests()); aggsTests.add(new InternalRangeTests());
aggsTests.add(new InternalDateRangeTests()); aggsTests.add(new InternalDateRangeTests());
aggsTests.add(new InternalGeoDistanceTests()); aggsTests.add(new InternalGeoDistanceTests());

View File

@ -39,6 +39,7 @@ import static org.elasticsearch.search.aggregations.AggregationBuilders.dateHist
import static org.elasticsearch.search.aggregations.AggregationBuilders.dateRange; import static org.elasticsearch.search.aggregations.AggregationBuilders.dateRange;
import static org.elasticsearch.search.aggregations.AggregationBuilders.filter; import static org.elasticsearch.search.aggregations.AggregationBuilders.filter;
import static org.elasticsearch.search.aggregations.AggregationBuilders.geohashGrid; import static org.elasticsearch.search.aggregations.AggregationBuilders.geohashGrid;
import static org.elasticsearch.search.aggregations.AggregationBuilders.geotileGrid;
import static org.elasticsearch.search.aggregations.AggregationBuilders.global; import static org.elasticsearch.search.aggregations.AggregationBuilders.global;
import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram; import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram;
import static org.elasticsearch.search.aggregations.AggregationBuilders.ipRange; import static org.elasticsearch.search.aggregations.AggregationBuilders.ipRange;
@ -306,5 +307,20 @@ public class ShardReduceIT extends ESIntegTestCase {
assertThat(histo.getBuckets().size(), equalTo(4)); assertThat(histo.getBuckets().size(), equalTo(4));
} }
public void testGeoTileGrid() throws Exception {
SearchResponse response = client().prepareSearch("idx")
.setQuery(QueryBuilders.matchAllQuery())
.addAggregation(geotileGrid("grid").field("location")
.subAggregation(dateHistogram("histo").field("date").dateHistogramInterval(DateHistogramInterval.DAY)
.minDocCount(0)))
.get();
assertSearchResponse(response);
GeoGrid grid = response.getAggregations().get("grid");
Histogram histo = grid.getBuckets().iterator().next().getAggregations().get("histo");
assertThat(histo.getBuckets().size(), equalTo(4));
}
} }

View File

@ -19,6 +19,7 @@
package org.elasticsearch.search.aggregations.bucket.geogrid; package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.apache.lucene.document.LatLonDocValuesField; import org.apache.lucene.document.LatLonDocValuesField;
import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.RandomIndexWriter; import org.apache.lucene.index.RandomIndexWriter;
@ -89,6 +90,13 @@ public abstract class GeoGridAggregatorTestCase<T extends InternalGeoGridBucket>
double lat = (180d * randomDouble()) - 90d; double lat = (180d * randomDouble()) - 90d;
double lng = (360d * randomDouble()) - 180d; double lng = (360d * randomDouble()) - 180d;
// Precision-adjust longitude/latitude to avoid wrong bucket placement
// Internally, lat/lng get converted to 32 bit integers, loosing some precision.
// This does not affect geohashing because geohash uses the same algorithm,
// but it does affect other bucketing algos, thus we need to do the same steps here.
lng = GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(lng));
lat = GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(lat));
points.add(new LatLonDocValuesField(FIELD_NAME, lat, lng)); points.add(new LatLonDocValuesField(FIELD_NAME, lat, lng));
String hash = hashAsString(lng, lat, precision); String hash = hashAsString(lng, lat, precision);
if (distinctHashesPerDoc.contains(hash) == false) { if (distinctHashesPerDoc.contains(hash) == false) {

View File

@ -0,0 +1,50 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;
public class GeoTileGridAggregatorTests extends GeoGridAggregatorTestCase<InternalGeoTileGridBucket> {
@Override
protected int randomPrecision() {
return randomIntBetween(0, GeoTileUtils.MAX_ZOOM);
}
@Override
protected String hashAsString(double lng, double lat, int precision) {
return GeoTileUtils.stringEncode(GeoTileUtils.longEncode(lng, lat, precision));
}
@Override
protected GeoGridAggregationBuilder createBuilder(String name) {
return new GeoTileGridAggregationBuilder(name);
}
public void testPrecision() {
final GeoGridAggregationBuilder builder = createBuilder("_name");
expectThrows(IllegalArgumentException.class, () -> builder.precision(-1));
expectThrows(IllegalArgumentException.class, () -> builder.precision(30));
int precision = randomIntBetween(0, 29);
builder.precision(precision);
assertEquals(precision, builder.precision());
}
}

View File

@ -0,0 +1,73 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.common.xcontent.XContentParseException;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.test.ESTestCase;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
public class GeoTileGridParserTests extends ESTestCase {
public void testParseValidFromInts() throws Exception {
int precision = randomIntBetween(0, GeoTileUtils.MAX_ZOOM);
XContentParser stParser = createParser(JsonXContent.jsonXContent,
"{\"field\":\"my_loc\", \"precision\":" + precision + ", \"size\": 500, \"shard_size\": 550}");
XContentParser.Token token = stParser.nextToken();
assertSame(XContentParser.Token.START_OBJECT, token);
// can create a factory
assertNotNull(GeoTileGridAggregationBuilder.parse("geotile_grid", stParser));
}
public void testParseValidFromStrings() throws Exception {
int precision = randomIntBetween(0, GeoTileUtils.MAX_ZOOM);
XContentParser stParser = createParser(JsonXContent.jsonXContent,
"{\"field\":\"my_loc\", \"precision\":\"" + precision + "\", \"size\": \"500\", \"shard_size\": \"550\"}");
XContentParser.Token token = stParser.nextToken();
assertSame(XContentParser.Token.START_OBJECT, token);
// can create a factory
assertNotNull(GeoTileGridAggregationBuilder.parse("geotile_grid", stParser));
}
public void testParseErrorOnBooleanPrecision() throws Exception {
XContentParser stParser = createParser(JsonXContent.jsonXContent, "{\"field\":\"my_loc\", \"precision\":false}");
XContentParser.Token token = stParser.nextToken();
assertSame(XContentParser.Token.START_OBJECT, token);
XContentParseException e = expectThrows(XContentParseException.class,
() -> GeoTileGridAggregationBuilder.parse("geotile_grid", stParser));
assertThat(ExceptionsHelper.detailedMessage(e),
containsString("[geotile_grid] precision doesn't support values of type: VALUE_BOOLEAN"));
}
public void testParseErrorOnPrecisionOutOfRange() throws Exception {
XContentParser stParser = createParser(JsonXContent.jsonXContent, "{\"field\":\"my_loc\", \"precision\":\"30\"}");
XContentParser.Token token = stParser.nextToken();
assertSame(XContentParser.Token.START_OBJECT, token);
try {
GeoTileGridAggregationBuilder.parse("geotile_grid", stParser);
fail();
} catch (XContentParseException ex) {
assertThat(ex.getCause(), instanceOf(IllegalArgumentException.class));
assertEquals("Invalid geotile_grid precision of 30. Must be between 0 and 29.", ex.getCause().getMessage());
}
}
}

View File

@ -0,0 +1,56 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.search.aggregations.InternalAggregations;
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
import java.util.List;
import java.util.Map;
public class GeoTileGridTests extends GeoGridTestCase<InternalGeoTileGridBucket, InternalGeoTileGrid> {
@Override
protected InternalGeoTileGrid createInternalGeoGrid(String name, int size, List<InternalGeoGridBucket> buckets,
List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) {
return new InternalGeoTileGrid(name, size, buckets, pipelineAggregators, metaData);
}
@Override
protected Writeable.Reader<InternalGeoTileGrid> instanceReader() {
return InternalGeoTileGrid::new;
}
@Override
protected InternalGeoTileGridBucket createInternalGeoGridBucket(Long key, long docCount, InternalAggregations aggregations) {
return new InternalGeoTileGridBucket(key, docCount, aggregations);
}
@Override
protected long longEncode(double lng, double lat, int precision) {
return GeoTileUtils.longEncode(lng, lat, precision);
}
@Override
protected int randomPrecision() {
// precision values below 8 can lead to parsing errors
return randomIntBetween(8, GeoTileUtils.MAX_ZOOM);
}
}

View File

@ -0,0 +1,209 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.bucket.geogrid;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.test.ESTestCase;
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.MAX_ZOOM;
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.checkPrecisionRange;
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.hashToGeoPoint;
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.keyToGeoPoint;
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.longEncode;
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.stringEncode;
import static org.hamcrest.Matchers.closeTo;
import static org.hamcrest.Matchers.containsString;
public class GeoTileUtilsTests extends ESTestCase {
private static final double GEOTILE_TOLERANCE = 1E-5D;
/**
* Precision validation should throw an error if its outside of the valid range.
*/
public void testCheckPrecisionRange() {
for (int i = 0; i <= 29; i++) {
assertEquals(i, checkPrecisionRange(i));
}
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> checkPrecisionRange(-1));
assertThat(ex.getMessage(), containsString("Invalid geotile_grid precision of -1. Must be between 0 and 29."));
ex = expectThrows(IllegalArgumentException.class, () -> checkPrecisionRange(30));
assertThat(ex.getMessage(), containsString("Invalid geotile_grid precision of 30. Must be between 0 and 29."));
}
/**
* A few hardcoded lat/lng/zoom hashing expectations
*/
public void testLongEncode() {
assertEquals(0x0000000000000000L, longEncode(0, 0, 0));
assertEquals(0x3C00095540001CA5L, longEncode(30, 70, 15));
assertEquals(0x77FFFF4580000000L, longEncode(179.999, 89.999, 29));
assertEquals(0x740000BA7FFFFFFFL, longEncode(-179.999, -89.999, 29));
assertEquals(0x0800000040000001L, longEncode(1, 1, 2));
assertEquals(0x0C00000060000000L, longEncode(-20, 100, 3));
assertEquals(0x71127D27C8ACA67AL, longEncode(13, -15, 28));
assertEquals(0x4C0077776003A9ACL, longEncode(-12, 15, 19));
assertEquals(0x140000024000000EL, longEncode(-328.231870,16.064082, 5));
assertEquals(0x6436F96B60000000L, longEncode(-590.769588,89.549167, 25));
assertEquals(0x6411BD6BA0A98359L, longEncode(999.787079,51.830093, 25));
assertEquals(0x751BD6BBCA983596L, longEncode(999.787079,51.830093, 29));
assertEquals(0x77CF880A20000000L, longEncode(-557.039740,-632.103969, 29));
assertEquals(0x7624FA4FA0000000L, longEncode(13,88, 29));
assertEquals(0x7624FA4FBFFFFFFFL, longEncode(13,-88, 29));
assertEquals(0x0400000020000000L, longEncode(13,89, 1));
assertEquals(0x0400000020000001L, longEncode(13,-89, 1));
assertEquals(0x0400000020000000L, longEncode(13,95, 1));
assertEquals(0x0400000020000001L, longEncode(13,-95, 1));
expectThrows(IllegalArgumentException.class, () -> longEncode(0, 0, -1));
expectThrows(IllegalArgumentException.class, () -> longEncode(-1, 0, MAX_ZOOM + 1));
}
private void assertGeoPointEquals(GeoPoint gp, final double longitude, final double latitude) {
assertThat(gp.lon(), closeTo(longitude, GEOTILE_TOLERANCE));
assertThat(gp.lat(), closeTo(latitude, GEOTILE_TOLERANCE));
}
public void testHashToGeoPoint() {
assertGeoPointEquals(keyToGeoPoint("0/0/0"), 0.0, 0.0);
assertGeoPointEquals(keyToGeoPoint("1/0/0"), -90.0, 66.51326044311186);
assertGeoPointEquals(keyToGeoPoint("1/1/0"), 90.0, 66.51326044311186);
assertGeoPointEquals(keyToGeoPoint("1/0/1"), -90.0, -66.51326044311186);
assertGeoPointEquals(keyToGeoPoint("1/1/1"), 90.0, -66.51326044311186);
assertGeoPointEquals(keyToGeoPoint("29/536870000/10"), 179.99938879162073, 85.05112817241982);
assertGeoPointEquals(keyToGeoPoint("29/10/536870000"), -179.99999295920134, -85.0510760525731);
//noinspection ConstantConditions
expectThrows(NullPointerException.class, () -> keyToGeoPoint(null));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint(""));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint("a"));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint("0"));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint("0/0"));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint("0/0/0/0"));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint("0/-1/-1"));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint("0/-1/0"));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint("0/0/-1"));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint("a/0/0"));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint("0/a/0"));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint("0/0/a"));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint("-1/0/0"));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint((MAX_ZOOM + 1) + "/0/0"));
for (int z = 0; z <= MAX_ZOOM; z++) {
final int zoom = z;
final int max_index = (int) Math.pow(2, zoom);
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint(zoom + "/0/" + max_index));
expectThrows(IllegalArgumentException.class, () -> keyToGeoPoint(zoom + "/" + max_index + "/0"));
}
}
/**
* Make sure that hash produces the expected key, and that the key could be converted to hash via a GeoPoint
*/
private void assertStrCodec(long hash, String key, int zoom) {
assertEquals(key, stringEncode(hash));
final GeoPoint gp = keyToGeoPoint(key);
assertEquals(hash, longEncode(gp.lon(), gp.lat(), zoom));
}
/**
* A few hardcoded lat/lng/zoom hashing expectations
*/
public void testStringEncode() {
assertStrCodec(0x0000000000000000L, "0/0/0", 0);
assertStrCodec(0x3C00095540001CA5L, "15/19114/7333", 15);
assertStrCodec(0x77FFFF4580000000L, "29/536869420/0", 29);
assertStrCodec(0x740000BA7FFFFFFFL, "29/1491/536870911", 29);
assertStrCodec(0x0800000040000001L, "2/2/1", 2);
assertStrCodec(0x0C00000060000000L, "3/3/0", 3);
assertStrCodec(0x71127D27C8ACA67AL, "28/143911230/145532538", 28);
assertStrCodec(0x4C0077776003A9ACL, "19/244667/240044", 19);
assertStrCodec(0x140000024000000EL, "5/18/14", 5);
assertStrCodec(0x6436F96B60000000L, "25/28822363/0", 25);
assertStrCodec(0x6411BD6BA0A98359L, "25/9300829/11109209", 25);
assertStrCodec(0x751BD6BBCA983596L, "29/148813278/177747350", 29);
assertStrCodec(0x77CF880A20000000L, "29/511459409/0", 29);
assertStrCodec(0x7624FA4FA0000000L, "29/287822461/0", 29);
assertStrCodec(0x7624FA4FBFFFFFFFL, "29/287822461/536870911", 29);
assertStrCodec(0x0400000020000000L, "1/1/0", 1);
assertStrCodec(0x0400000020000001L, "1/1/1", 1);
expectThrows(IllegalArgumentException.class, () -> stringEncode(-1L));
expectThrows(IllegalArgumentException.class, () -> stringEncode(0x7800000000000000L)); // z=30
expectThrows(IllegalArgumentException.class, () -> stringEncode(0x0000000000000001L)); // z=0,x=0,y=1
expectThrows(IllegalArgumentException.class, () -> stringEncode(0x0000000020000000L)); // z=0,x=1,y=0
for (int zoom = 0; zoom < 5; zoom++) {
int maxTile = 1 << zoom;
for (int x = 0; x < maxTile; x++) {
for (int y = 0; y < maxTile; y++) {
String expectedTileIndex = zoom + "/" + x + "/" + y;
GeoPoint point = keyToGeoPoint(expectedTileIndex);
String actualTileIndex = stringEncode(longEncode(point.lon(), point.lat(), zoom));
assertEquals(expectedTileIndex, actualTileIndex);
}
}
}
}
/**
* Ensure that for all points at all supported precision levels that the long encoding of a geotile
* is compatible with its String based counterpart
*/
public void testGeoTileAsLongRoutines() {
for (double lat = -90; lat <= 90; lat++) {
for (double lng = -180; lng <= 180; lng++) {
for (int p = 0; p <= 29; p++) {
long hash = longEncode(lng, lat, p);
if (p > 0) {
assertNotEquals(0, hash);
}
// GeoPoint would be in the center of the bucket, thus must produce the same hash
GeoPoint point = hashToGeoPoint(hash);
long hashAsLong2 = longEncode(point.lon(), point.lat(), p);
assertEquals(hash, hashAsLong2);
// Same point should be generated from the string key
assertEquals(point, keyToGeoPoint(stringEncode(hash)));
}
}
}
}
/**
* Make sure the polar regions are handled properly.
* Mercator projection does not show anything above 85 or below -85,
* so ensure they are clipped correctly.
*/
public void testSingularityAtPoles() {
double minLat = -85.05112878;
double maxLat = 85.05112878;
double lon = randomIntBetween(-180, 180);
double lat = randomBoolean()
? randomDoubleBetween(-90, minLat, true)
: randomDoubleBetween(maxLat, 90, true);
double clippedLat = Math.min(Math.max(lat, minLat), maxLat);
int zoom = randomIntBetween(0, MAX_ZOOM);
String tileIndex = stringEncode(longEncode(lon, lat, zoom));
String clippedTileIndex = stringEncode(longEncode(lon, clippedLat, zoom));
assertEquals(tileIndex, clippedTileIndex);
}
}

View File

@ -51,6 +51,8 @@ import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilter;
import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilters; import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilters;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoHashGridAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoHashGridAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.geogrid.ParsedGeoHashGrid; import org.elasticsearch.search.aggregations.bucket.geogrid.ParsedGeoHashGrid;
import org.elasticsearch.search.aggregations.bucket.geogrid.ParsedGeoTileGrid;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileGridAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.global.ParsedGlobal; import org.elasticsearch.search.aggregations.bucket.global.ParsedGlobal;
import org.elasticsearch.search.aggregations.bucket.histogram.AutoDateHistogramAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.AutoDateHistogramAggregationBuilder;
@ -212,6 +214,7 @@ public abstract class InternalAggregationTestCase<T extends InternalAggregation>
map.put(FilterAggregationBuilder.NAME, (p, c) -> ParsedFilter.fromXContent(p, (String) c)); map.put(FilterAggregationBuilder.NAME, (p, c) -> ParsedFilter.fromXContent(p, (String) c));
map.put(InternalSampler.PARSER_NAME, (p, c) -> ParsedSampler.fromXContent(p, (String) c)); map.put(InternalSampler.PARSER_NAME, (p, c) -> ParsedSampler.fromXContent(p, (String) c));
map.put(GeoHashGridAggregationBuilder.NAME, (p, c) -> ParsedGeoHashGrid.fromXContent(p, (String) c)); map.put(GeoHashGridAggregationBuilder.NAME, (p, c) -> ParsedGeoHashGrid.fromXContent(p, (String) c));
map.put(GeoTileGridAggregationBuilder.NAME, (p, c) -> ParsedGeoTileGrid.fromXContent(p, (String) c));
map.put(RangeAggregationBuilder.NAME, (p, c) -> ParsedRange.fromXContent(p, (String) c)); map.put(RangeAggregationBuilder.NAME, (p, c) -> ParsedRange.fromXContent(p, (String) c));
map.put(DateRangeAggregationBuilder.NAME, (p, c) -> ParsedDateRange.fromXContent(p, (String) c)); map.put(DateRangeAggregationBuilder.NAME, (p, c) -> ParsedDateRange.fromXContent(p, (String) c));
map.put(GeoDistanceAggregationBuilder.NAME, (p, c) -> ParsedGeoDistance.fromXContent(p, (String) c)); map.put(GeoDistanceAggregationBuilder.NAME, (p, c) -> ParsedGeoDistance.fromXContent(p, (String) c));