Skip to content

Commit ab8627b

Browse files
talevySivagurunathanV
authored andcommitted
Adds support for geo-bounds filtering in geogrid aggregations (elastic#50002)
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. This is even more relevant to `geo_shape` where the bounds will restrict the shape to be within the bounds this optional `bounds` parameter is parsed in an equivalent fashion to the bounds specified in the geo_bounding_box query.
1 parent a2ed7e9 commit ab8627b

28 files changed

+726
-96
lines changed

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

Lines changed: 58 additions & 0 deletions
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

Lines changed: 58 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 25 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 28 additions & 3 deletions
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,21 +90,31 @@ 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
87107
String type() {
88108
return TYPE;
89109
}
90110

111+
GeoBoundingBox geoBoundingBox() {
112+
return geoBoundingBox;
113+
}
114+
91115
@Override
92116
public int hashCode() {
93-
return Objects.hash(super.hashCode(), precision);
117+
return Objects.hash(super.hashCode(), precision, geoBoundingBox);
94118
}
95119

96120
@Override
@@ -99,7 +123,8 @@ public boolean equals(Object obj) {
99123
if (obj == null || getClass() != obj.getClass()) return false;
100124
if (super.equals(obj) == false) return false;
101125
GeoTileGridValuesSourceBuilder other = (GeoTileGridValuesSourceBuilder) obj;
102-
return precision == other.precision;
126+
return Objects.equals(precision,other.precision)
127+
&& Objects.equals(geoBoundingBox, other.geoBoundingBox);
103128
}
104129

105130
@Override
@@ -112,7 +137,7 @@ protected CompositeValuesSourceConfig innerBuild(QueryShardContext queryShardCon
112137
ValuesSource.GeoPoint geoPoint = (ValuesSource.GeoPoint) orig;
113138
// is specified in the builder.
114139
final MappedFieldType fieldType = config.fieldContext() != null ? config.fieldContext().fieldType() : null;
115-
CellIdSource cellIdSource = new CellIdSource(geoPoint, precision, GeoTileUtils::longEncode);
140+
CellIdSource cellIdSource = new CellIdSource(geoPoint, precision, geoBoundingBox, GeoTileUtils::longEncode);
116141
return new CompositeValuesSourceConfig(name, fieldType, cellIdSource, DocValueFormat.GEOTILE, order(),
117142
missingBucket(), script() != null);
118143
} else {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.search.aggregations.bucket.geogrid;
20+
21+
import org.elasticsearch.common.geo.GeoBoundingBox;
22+
import org.elasticsearch.index.fielddata.MultiGeoPointValues;
23+
24+
/**
25+
* Class representing {@link CellValues} whose values are filtered
26+
* according to whether they are within the specified {@link GeoBoundingBox}.
27+
*
28+
* The specified bounding box is assumed to be bounded.
29+
*/
30+
class BoundedCellValues extends CellValues {
31+
32+
private final GeoBoundingBox geoBoundingBox;
33+
34+
protected BoundedCellValues(MultiGeoPointValues geoValues, int precision, CellIdSource.GeoPointLongEncoder encoder,
35+
GeoBoundingBox geoBoundingBox) {
36+
super(geoValues, precision, encoder);
37+
this.geoBoundingBox = geoBoundingBox;
38+
}
39+
40+
41+
@Override
42+
int advanceValue(org.elasticsearch.common.geo.GeoPoint target, int valuesIdx) {
43+
if (geoBoundingBox.pointInBounds(target.getLon(), target.getLat())) {
44+
values[valuesIdx] = encoder.encode(target.getLon(), target.getLat(), precision);
45+
return valuesIdx + 1;
46+
}
47+
return valuesIdx;
48+
}
49+
}

0 commit comments

Comments
 (0)