Skip to content

Commit af29e5e

Browse files
committed
Adds support for geo-bounds filtering in geogrid aggregations
It is fairly common to filter the geo point candidates in geohash_grid and geotile_grid aggregations according to some viewable bounding box. This change introduces the option of specifying this filter directly in the tiling aggregation.
1 parent e47711f commit af29e5e

24 files changed

+476
-67
lines changed

docs/reference/aggregations/bucket/geohashgrid-aggregation.asciidoc

+58
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,62 @@ var bbox = geohash.decode_bbox('u17');
191191
--------------------------------------------------
192192
// NOTCONSOLE
193193

194+
==== Requests with additional bounding box filtering
195+
196+
The `geohash_grid` aggregation supports an optional `bounds` parameter
197+
that restricts the points considered to those that fall within the
198+
bounds provided. The `bounds` parameter accepts the bounding box in
199+
all the same <<query-dsl-geo-bounding-box-query-accepted-formats,accepted formats>> of the
200+
bounds specified in the Geo Bounding Box Query. This bounding box can be used with or
201+
without an additional `geo_bounding_box` query filtering the points prior to aggregating.
202+
It is an independent bounding box that can intersect with, be equal to, or be disjoint
203+
to any additional `geo_bounding_box` queries defined in the context of the aggregation.
204+
205+
[source,console,id=geohashgrid-aggregation-with-bounds]
206+
--------------------------------------------------
207+
POST /museums/_search?size=0
208+
{
209+
"aggregations" : {
210+
"tiles-in-bounds" : {
211+
"geohash_grid" : {
212+
"field" : "location",
213+
"precision" : 8,
214+
"bounds": {
215+
"top_left" : "53.4375, 4.21875",
216+
"bottom_right" : "52.03125, 5.625"
217+
}
218+
}
219+
}
220+
}
221+
}
222+
--------------------------------------------------
223+
// TEST[continued]
224+
225+
[source,console-result]
226+
--------------------------------------------------
227+
{
228+
...
229+
"aggregations" : {
230+
"tiles-in-bounds" : {
231+
"buckets" : [
232+
{
233+
"key" : "u173zy3j",
234+
"doc_count" : 1
235+
},
236+
{
237+
"key" : "u173zvfz",
238+
"doc_count" : 1
239+
},
240+
{
241+
"key" : "u173zt90",
242+
"doc_count" : 1
243+
}
244+
]
245+
}
246+
}
247+
}
248+
--------------------------------------------------
249+
// TESTRESPONSE[s/\.\.\./"took": $body.took,"_shards": $body._shards,"hits":$body.hits,"timed_out":false,/]
194250

195251
==== Cell dimensions at the equator
196252
The table below shows the metric dimensions for cells covered by various string lengths of geohash.
@@ -230,6 +286,8 @@ precision:: Optional. The string length of the geohashes used to define
230286
to precision levels higher than the supported 12 levels,
231287
(e.g. for distances <5.6cm) the value is rejected.
232288

289+
bounds: Optional. The bounding box to filter the points in the bucket.
290+
233291
size:: Optional. The maximum number of geohash buckets to return
234292
(defaults to 10,000). When results are trimmed, buckets are
235293
prioritised based on the volumes of documents they contain.

docs/reference/aggregations/bucket/geotilegrid-aggregation.asciidoc

+58
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,62 @@ POST /museums/_search?size=0
162162
--------------------------------------------------
163163
// TESTRESPONSE[s/\.\.\./"took": $body.took,"_shards": $body._shards,"hits":$body.hits,"timed_out":false,/]
164164

165+
==== Requests with additional bounding box filtering
166+
167+
The `geotile_grid` aggregation supports an optional `bounds` parameter
168+
that restricts the points considered to those that fall within the
169+
bounds provided. The `bounds` parameter accepts the bounding box in
170+
all the same <<query-dsl-geo-bounding-box-query-accepted-formats,accepted formats>> of the
171+
bounds specified in the Geo Bounding Box Query. This bounding box can be used with or
172+
without an additional `geo_bounding_box` query filtering the points prior to aggregating.
173+
It is an independent bounding box that can intersect with, be equal to, or be disjoint
174+
to any additional `geo_bounding_box` queries defined in the context of the aggregation.
175+
176+
[source,console,id=geotilegrid-aggregation-with-bounds]
177+
--------------------------------------------------
178+
POST /museums/_search?size=0
179+
{
180+
"aggregations" : {
181+
"tiles-in-bounds" : {
182+
"geotile_grid" : {
183+
"field" : "location",
184+
"precision" : 22,
185+
"bounds": {
186+
"top_left" : "52.4, 4.9",
187+
"bottom_right" : "52.3, 5.0"
188+
}
189+
}
190+
}
191+
}
192+
}
193+
--------------------------------------------------
194+
// TEST[continued]
195+
196+
[source,console-result]
197+
--------------------------------------------------
198+
{
199+
...
200+
"aggregations" : {
201+
"tiles-in-bounds" : {
202+
"buckets" : [
203+
{
204+
"key" : "22/2154412/1378379",
205+
"doc_count" : 1
206+
},
207+
{
208+
"key" : "22/2154385/1378332",
209+
"doc_count" : 1
210+
},
211+
{
212+
"key" : "22/2154259/1378425",
213+
"doc_count" : 1
214+
}
215+
]
216+
}
217+
}
218+
}
219+
--------------------------------------------------
220+
// TESTRESPONSE[s/\.\.\./"took": $body.took,"_shards": $body._shards,"hits":$body.hits,"timed_out":false,/]
165221

166222
==== Options
167223

@@ -172,6 +228,8 @@ precision:: Optional. The integer zoom of the key used to define
172228
cells/buckets in the results. Defaults to 7.
173229
Values outside of [0,29] will be rejected.
174230

231+
bounds: Optional. The bounding box to filter the points in the bucket.
232+
175233
size:: Optional. The maximum number of geohash buckets to return
176234
(defaults to 10,000). When results are trimmed, buckets are
177235
prioritised based on the volumes of documents they contain.

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

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ be executed in memory or indexed. See <<geo-bbox-type,Type>> below for further d
8484
Default is `memory`.
8585
|=======================================================================
8686

87+
[[query-dsl-geo-bounding-box-query-accepted-formats]]
8788
[float]
8889
==== Accepted Formats
8990

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

+25
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ public GeoBoundingBox(StreamInput input) throws IOException {
6868
this.bottomRight = input.readGeoPoint();
6969
}
7070

71+
public boolean isUnbounded() {
72+
return Double.isNaN(topLeft.lon()) || Double.isNaN(topLeft.lat())
73+
|| Double.isNaN(bottomRight.lon()) || Double.isNaN(bottomRight.lat());
74+
}
75+
7176
public GeoPoint topLeft() {
7277
return topLeft;
7378
}
@@ -120,6 +125,26 @@ public XContentBuilder toXContentFragment(XContentBuilder builder, boolean build
120125
return builder;
121126
}
122127

128+
/**
129+
* If the bounding box crosses the date-line (left greater-than right) then the
130+
* longitude of the point need only to be higher than the left or lower
131+
* than the right. Otherwise, it must be both.
132+
*
133+
* @param lon the longitude of the point
134+
* @param lat the latitude of the point
135+
* @return whether the point (lon, lat) is in the specified bounding box
136+
*/
137+
public boolean pointInBounds(double lon, double lat) {
138+
if (lat >= bottom() && lat <= top()) {
139+
if (left() <= right()) {
140+
return lon >= left() && lon <= right();
141+
} else {
142+
return lon >= left() || lon <= right();
143+
}
144+
}
145+
return false;
146+
}
147+
123148
@Override
124149
public void writeTo(StreamOutput out) throws IOException {
125150
out.writeGeoPoint(topLeft);

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

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public static boolean isValidLongitude(double longitude) {
9393
return true;
9494
}
9595

96+
9697
/**
9798
* Calculate the width (in meters) of geohash cells at a specific level
9899
* @param level geohash level must be greater or equal to zero

server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GeoTileGridValuesSourceBuilder.java

+24-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919

2020
package org.elasticsearch.search.aggregations.bucket.composite;
2121

22+
import org.elasticsearch.Version;
2223
import org.elasticsearch.common.ParseField;
24+
import org.elasticsearch.common.geo.GeoBoundingBox;
25+
import org.elasticsearch.common.geo.GeoPoint;
2326
import org.elasticsearch.common.io.stream.StreamInput;
2427
import org.elasticsearch.common.io.stream.StreamOutput;
2528
import org.elasticsearch.common.xcontent.ObjectParser;
@@ -45,6 +48,8 @@ public class GeoTileGridValuesSourceBuilder extends CompositeValuesSourceBuilder
4548
static {
4649
PARSER = new ObjectParser<>(GeoTileGridValuesSourceBuilder.TYPE);
4750
PARSER.declareInt(GeoTileGridValuesSourceBuilder::precision, new ParseField("precision"));
51+
PARSER.declareField(((p, builder, context) -> builder.geoBoundingBox(GeoBoundingBox.parseBoundingBox(p))),
52+
GeoBoundingBox.BOUNDS_FIELD, ObjectParser.ValueType.OBJECT);
4853
CompositeValuesSourceParserHelper.declareValuesSourceFields(PARSER, ValueType.NUMERIC);
4954
}
5055

@@ -53,6 +58,7 @@ static GeoTileGridValuesSourceBuilder parse(String name, XContentParser parser)
5358
}
5459

5560
private int precision = GeoTileGridAggregationBuilder.DEFAULT_PRECISION;
61+
private GeoBoundingBox geoBoundingBox = new GeoBoundingBox(new GeoPoint(Double.NaN, Double.NaN), new GeoPoint(Double.NaN, Double.NaN));
5662

5763
GeoTileGridValuesSourceBuilder(String name) {
5864
super(name);
@@ -61,13 +67,21 @@ static GeoTileGridValuesSourceBuilder parse(String name, XContentParser parser)
6167
GeoTileGridValuesSourceBuilder(StreamInput in) throws IOException {
6268
super(in);
6369
this.precision = in.readInt();
70+
if (in.getVersion().onOrAfter(Version.V_8_0_0)) {
71+
this.geoBoundingBox = new GeoBoundingBox(in);
72+
}
6473
}
6574

6675
public GeoTileGridValuesSourceBuilder precision(int precision) {
6776
this.precision = GeoTileUtils.checkPrecisionRange(precision);
6877
return this;
6978
}
7079

80+
public GeoTileGridValuesSourceBuilder geoBoundingBox(GeoBoundingBox geoBoundingBox) {
81+
this.geoBoundingBox = geoBoundingBox;
82+
return this;
83+
}
84+
7185
@Override
7286
public GeoTileGridValuesSourceBuilder format(String format) {
7387
throw new IllegalArgumentException("[format] is not supported for [" + TYPE + "]");
@@ -76,11 +90,17 @@ public GeoTileGridValuesSourceBuilder format(String format) {
7690
@Override
7791
protected void innerWriteTo(StreamOutput out) throws IOException {
7892
out.writeInt(precision);
93+
if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
94+
geoBoundingBox.writeTo(out);
95+
}
7996
}
8097

8198
@Override
8299
protected void doXContentBody(XContentBuilder builder, Params params) throws IOException {
83100
builder.field("precision", precision);
101+
if (geoBoundingBox.isUnbounded() == false) {
102+
geoBoundingBox.toXContent(builder, params);
103+
}
84104
}
85105

86106
@Override
@@ -90,7 +110,7 @@ String type() {
90110

91111
@Override
92112
public int hashCode() {
93-
return Objects.hash(super.hashCode(), precision);
113+
return Objects.hash(super.hashCode(), precision, geoBoundingBox);
94114
}
95115

96116
@Override
@@ -99,7 +119,8 @@ public boolean equals(Object obj) {
99119
if (obj == null || getClass() != obj.getClass()) return false;
100120
if (super.equals(obj) == false) return false;
101121
GeoTileGridValuesSourceBuilder other = (GeoTileGridValuesSourceBuilder) obj;
102-
return precision == other.precision;
122+
return Objects.equals(precision,other.precision)
123+
&& Objects.equals(geoBoundingBox, other.geoBoundingBox);
103124
}
104125

105126
@Override
@@ -112,7 +133,7 @@ protected CompositeValuesSourceConfig innerBuild(QueryShardContext queryShardCon
112133
ValuesSource.GeoPoint geoPoint = (ValuesSource.GeoPoint) orig;
113134
// is specified in the builder.
114135
final MappedFieldType fieldType = config.fieldContext() != null ? config.fieldContext().fieldType() : null;
115-
CellIdSource cellIdSource = new CellIdSource(geoPoint, precision, GeoTileUtils::longEncode);
136+
CellIdSource cellIdSource = new CellIdSource(geoPoint, precision, geoBoundingBox, GeoTileUtils::longEncode);
116137
return new CompositeValuesSourceConfig(name, fieldType, cellIdSource, DocValueFormat.GEOTILE, order(),
117138
missingBucket(), script() != null);
118139
} else {

server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellIdSource.java

+16-6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.apache.lucene.index.LeafReaderContext;
2222
import org.apache.lucene.index.SortedNumericDocValues;
23+
import org.elasticsearch.common.geo.GeoBoundingBox;
2324
import org.elasticsearch.index.fielddata.AbstractSortingNumericDocValues;
2425
import org.elasticsearch.index.fielddata.MultiGeoPointValues;
2526
import org.elasticsearch.index.fielddata.SortedBinaryDocValues;
@@ -36,11 +37,13 @@ public class CellIdSource extends ValuesSource.Numeric {
3637
private final ValuesSource.GeoPoint valuesSource;
3738
private final int precision;
3839
private final GeoPointLongEncoder encoder;
40+
private final GeoBoundingBox geoBoundingBox;
3941

40-
public CellIdSource(GeoPoint valuesSource, int precision, GeoPointLongEncoder encoder) {
42+
public CellIdSource(GeoPoint valuesSource,int precision, GeoBoundingBox geoBoundingBox, GeoPointLongEncoder encoder) {
4143
this.valuesSource = valuesSource;
4244
//different GeoPoints could map to the same or different hashing cells.
4345
this.precision = precision;
46+
this.geoBoundingBox = geoBoundingBox;
4447
this.encoder = encoder;
4548
}
4649

@@ -55,7 +58,7 @@ public boolean isFloatingPoint() {
5558

5659
@Override
5760
public SortedNumericDocValues longValues(LeafReaderContext ctx) {
58-
return new CellValues(valuesSource.geoPointValues(ctx), precision, encoder);
61+
return new CellValues(valuesSource.geoPointValues(ctx), precision, geoBoundingBox, encoder);
5962
}
6063

6164
@Override
@@ -81,21 +84,28 @@ private static class CellValues extends AbstractSortingNumericDocValues {
8184
private MultiGeoPointValues geoValues;
8285
private int precision;
8386
private GeoPointLongEncoder encoder;
87+
private GeoBoundingBox geoBoundingBox;
8488

85-
protected CellValues(MultiGeoPointValues geoValues, int precision, GeoPointLongEncoder encoder) {
89+
protected CellValues(MultiGeoPointValues geoValues, int precision, GeoBoundingBox geoBoundingBox, GeoPointLongEncoder encoder) {
8690
this.geoValues = geoValues;
8791
this.precision = precision;
8892
this.encoder = encoder;
93+
this.geoBoundingBox = geoBoundingBox;
8994
}
9095

9196
@Override
9297
public boolean advanceExact(int docId) throws IOException {
9398
if (geoValues.advanceExact(docId)) {
94-
resize(geoValues.docValueCount());
95-
for (int i = 0; i < docValueCount(); ++i) {
99+
int docValueCount = geoValues.docValueCount();
100+
resize(docValueCount);
101+
int j = 0;
102+
for (int i = 0; i < docValueCount; i++) {
96103
org.elasticsearch.common.geo.GeoPoint target = geoValues.nextValue();
97-
values[i] = encoder.encode(target.getLon(), target.getLat(), precision);
104+
if (geoBoundingBox.isUnbounded() || geoBoundingBox.pointInBounds(target.getLon(), target.getLat())) {
105+
values[j++] = encoder.encode(target.getLon(), target.getLat(), precision);
106+
}
98107
}
108+
resize(j);
99109
sort();
100110
return true;
101111
} else {

0 commit comments

Comments
 (0)