Skip to content

Commit 633b983

Browse files
committed
Add bounds support for geogrid agg on shapes (#51973)
This PR cleans up some aspects of GeoShapeCellValues to support the specialization of bounded geo_shape geo-grid aggregations. This refactor reverts some of the BoundedCellValues constructs. Instead, BoundedGeoTileGridTiler and BoundedGeoHashGridTiler are introduced. As part of this change, the definition/semantics of geo_grid aggs with bounds on geo_point are modified to match the same behavior as geo_shapes, where it is the tile of the point that must intersect the bounds in order for the point to be accounted for
1 parent b336c56 commit 633b983

22 files changed

+1020
-478
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,6 @@ private GeoRelation relateTriangle(int aX, int aY, boolean ab, int bX, int bY, b
322322
int tMinY = StrictMath.min(StrictMath.min(aY, bY), cY);
323323
int tMaxY = StrictMath.max(StrictMath.max(aY, bY), cY);
324324

325-
326325
// 1. check bounding boxes are disjoint, where north and east boundaries are not considered as crossing
327326
if (tMaxX <= minX || tMinX > maxX || tMinY > maxY || tMaxY <= minY) {
328327
return GeoRelation.QUERY_DISJOINT;

server/src/main/java/org/elasticsearch/index/fielddata/MultiGeoValues.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,5 @@ public double minX() {
316316
public double maxX() {
317317
return Math.max(negRight, posRight);
318318
}
319-
320319
}
321320
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
import org.elasticsearch.index.mapper.MappedFieldType;
3232
import org.elasticsearch.index.query.QueryShardContext;
3333
import org.elasticsearch.search.DocValueFormat;
34+
import org.elasticsearch.search.aggregations.bucket.geogrid.BoundedGeoTileGridTiler;
3435
import org.elasticsearch.search.aggregations.bucket.geogrid.CellIdSource;
3536
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoGridTiler;
3637
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileGridAggregationBuilder;
38+
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileGridTiler;
3739
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils;
3840
import org.elasticsearch.search.aggregations.support.ValueType;
3941
import org.elasticsearch.search.aggregations.support.ValuesSource;
@@ -138,7 +140,15 @@ protected CompositeValuesSourceConfig innerBuild(QueryShardContext queryShardCon
138140
ValuesSource.Geo geoValue = (ValuesSource.Geo) orig;
139141
// is specified in the builder.
140142
final MappedFieldType fieldType = config.fieldContext() != null ? config.fieldContext().fieldType() : null;
141-
CellIdSource cellIdSource = new CellIdSource(geoValue, precision, geoBoundingBox, GeoGridTiler.GeoTileGridTiler.INSTANCE);
143+
144+
final GeoGridTiler tiler;
145+
if (geoBoundingBox.isUnbounded()) {
146+
tiler = new GeoTileGridTiler();
147+
} else {
148+
tiler = new BoundedGeoTileGridTiler(geoBoundingBox);
149+
}
150+
151+
CellIdSource cellIdSource = new CellIdSource(geoValue, precision, tiler);
142152
return new CompositeValuesSourceConfig(name, fieldType, cellIdSource, DocValueFormat.GEOTILE, order(),
143153
missingBucket(), script() != null);
144154
} else {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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.common.geo.GeoRelation;
23+
import org.elasticsearch.geometry.Rectangle;
24+
import org.elasticsearch.geometry.utils.Geohash;
25+
import org.elasticsearch.index.fielddata.MultiGeoValues;
26+
27+
public class BoundedGeoHashGridTiler extends GeoHashGridTiler {
28+
private final double boundsTop;
29+
private final double boundsBottom;
30+
private final double boundsWestLeft;
31+
private final double boundsWestRight;
32+
private final double boundsEastLeft;
33+
private final double boundsEastRight;
34+
private final boolean crossesDateline;
35+
36+
BoundedGeoHashGridTiler(GeoBoundingBox geoBoundingBox) {
37+
// split geoBoundingBox into west and east boxes
38+
boundsTop = geoBoundingBox.top();
39+
boundsBottom = geoBoundingBox.bottom();
40+
if (geoBoundingBox.right() < geoBoundingBox.left()) {
41+
boundsWestLeft = -180;
42+
boundsWestRight = geoBoundingBox.right();
43+
boundsEastLeft = geoBoundingBox.left();
44+
boundsEastRight = 180;
45+
crossesDateline = true;
46+
} else { // only set east bounds
47+
boundsEastLeft = geoBoundingBox.left();
48+
boundsEastRight = geoBoundingBox.right();
49+
boundsWestLeft = 0;
50+
boundsWestRight = 0;
51+
crossesDateline = false;
52+
}
53+
}
54+
55+
@Override
56+
public int advancePointValue(long[] values, double x, double y, int precision, int valuesIdx) {
57+
long hash = encode(x, y, precision);
58+
if (cellIntersectsGeoBoundingBox(Geohash.toBoundingBox(Geohash.stringEncode(hash)))) {
59+
values[valuesIdx] = hash;
60+
return valuesIdx + 1;
61+
}
62+
return valuesIdx;
63+
}
64+
65+
boolean cellIntersectsGeoBoundingBox(Rectangle rectangle) {
66+
return (boundsTop >= rectangle.getMinY() && boundsBottom <= rectangle.getMaxY()
67+
&& (boundsEastLeft <= rectangle.getMaxX() && boundsEastRight >= rectangle.getMinX()
68+
|| (crossesDateline && boundsWestLeft <= rectangle.getMaxX() && boundsWestRight >= rectangle.getMinX())));
69+
}
70+
71+
@Override
72+
protected int setValue(CellValues docValues, MultiGeoValues.GeoValue geoValue, MultiGeoValues.BoundingBox bounds, int precision) {
73+
String hash = Geohash.stringEncode(bounds.minX(), bounds.minY(), precision);
74+
GeoRelation relation = relateTile(geoValue, hash);
75+
if (relation != GeoRelation.QUERY_DISJOINT) {
76+
docValues.resizeCell(1);
77+
docValues.add(0, Geohash.longEncode(hash));
78+
return 1;
79+
}
80+
return 0;
81+
}
82+
83+
@Override
84+
protected GeoRelation relateTile(MultiGeoValues.GeoValue geoValue, String hash) {
85+
Rectangle rectangle = Geohash.toBoundingBox(hash);
86+
if (cellIntersectsGeoBoundingBox(rectangle)) {
87+
return geoValue.relate(rectangle);
88+
} else {
89+
return GeoRelation.QUERY_DISJOINT;
90+
}
91+
}
92+
93+
@Override
94+
protected int setValuesForFullyContainedTile(String hash, CellValues values,
95+
int valuesIndex, int targetPrecision) {
96+
String[] hashes = Geohash.getSubGeohashes(hash);
97+
for (int i = 0; i < hashes.length; i++) {
98+
if (hashes[i].length() == targetPrecision ) {
99+
if (cellIntersectsGeoBoundingBox(Geohash.toBoundingBox(hashes[i]))) {
100+
values.add(valuesIndex++, Geohash.longEncode(hashes[i]));
101+
}
102+
} else {
103+
valuesIndex = setValuesForFullyContainedTile(hashes[i], values, valuesIndex, targetPrecision);
104+
}
105+
}
106+
return valuesIndex;
107+
}
108+
}

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

Lines changed: 0 additions & 48 deletions
This file was deleted.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.common.geo.GeoRelation;
23+
import org.elasticsearch.geometry.Rectangle;
24+
import org.elasticsearch.index.fielddata.MultiGeoValues;
25+
26+
public class BoundedGeoTileGridTiler extends GeoTileGridTiler {
27+
private final double boundsTop;
28+
private final double boundsBottom;
29+
private final double boundsWestLeft;
30+
private final double boundsWestRight;
31+
private final double boundsEastLeft;
32+
private final double boundsEastRight;
33+
private final boolean crossesDateline;
34+
35+
public BoundedGeoTileGridTiler(GeoBoundingBox geoBoundingBox) {
36+
// split geoBoundingBox into west and east boxes
37+
boundsTop = geoBoundingBox.top();
38+
boundsBottom = geoBoundingBox.bottom();
39+
if (geoBoundingBox.right() < geoBoundingBox.left()) {
40+
boundsWestLeft = -180;
41+
boundsWestRight = geoBoundingBox.right();
42+
boundsEastLeft = geoBoundingBox.left();
43+
boundsEastRight = 180;
44+
crossesDateline = true;
45+
} else { // only set east bounds
46+
boundsEastLeft = geoBoundingBox.left();
47+
boundsEastRight = geoBoundingBox.right();
48+
boundsWestLeft = 0;
49+
boundsWestRight = 0;
50+
crossesDateline = false;
51+
}
52+
}
53+
54+
public int advancePointValue(long[] values, double x, double y, int precision, int valuesIdx) {
55+
long hash = encode(x, y, precision);
56+
if (cellIntersectsGeoBoundingBox(GeoTileUtils.toBoundingBox(hash))) {
57+
values[valuesIdx] = hash;
58+
return valuesIdx + 1;
59+
}
60+
return valuesIdx;
61+
}
62+
63+
boolean cellIntersectsGeoBoundingBox(Rectangle rectangle) {
64+
return (boundsTop >= rectangle.getMinY() && boundsBottom <= rectangle.getMaxY()
65+
&& (boundsEastLeft <= rectangle.getMaxX() && boundsEastRight >= rectangle.getMinX()
66+
|| (crossesDateline && boundsWestLeft <= rectangle.getMaxX() && boundsWestRight >= rectangle.getMinX())));
67+
}
68+
69+
@Override
70+
public GeoRelation relateTile(MultiGeoValues.GeoValue geoValue, int xTile, int yTile, int precision) {
71+
Rectangle rectangle = GeoTileUtils.toBoundingBox(xTile, yTile, precision);
72+
if (cellIntersectsGeoBoundingBox(rectangle)) {
73+
return geoValue.relate(rectangle);
74+
}
75+
return GeoRelation.QUERY_DISJOINT;
76+
}
77+
78+
@Override
79+
protected int setValue(CellValues docValues, MultiGeoValues.GeoValue geoValue, int xTile, int yTile, int precision) {
80+
if (cellIntersectsGeoBoundingBox(GeoTileUtils.toBoundingBox(xTile, yTile, precision))) {
81+
docValues.resizeCell(1);
82+
docValues.add(0, GeoTileUtils.longEncodeTiles(precision, xTile, yTile));
83+
return 1;
84+
}
85+
return 0;
86+
}
87+
88+
@Override
89+
protected int setValuesForFullyContainedTile(int xTile, int yTile, int zTile, CellValues values, int valuesIndex,
90+
int targetPrecision) {
91+
zTile++;
92+
for (int i = 0; i < 2; i++) {
93+
for (int j = 0; j < 2; j++) {
94+
int nextX = 2 * xTile + i;
95+
int nextY = 2 * yTile + j;
96+
if (zTile == targetPrecision) {
97+
if (cellIntersectsGeoBoundingBox(GeoTileUtils.toBoundingBox(nextX, nextY, zTile))) {
98+
values.add(valuesIndex++, GeoTileUtils.longEncodeTiles(zTile, nextX, nextY));
99+
}
100+
} else {
101+
valuesIndex = setValuesForFullyContainedTile(nextX, nextY, zTile, values, valuesIndex, targetPrecision);
102+
}
103+
}
104+
}
105+
return valuesIndex;
106+
}
107+
}

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

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import org.apache.lucene.index.LeafReaderContext;
2222
import org.apache.lucene.index.SortedNumericDocValues;
2323
import org.elasticsearch.index.fielddata.MultiGeoValues;
24-
import org.elasticsearch.common.geo.GeoBoundingBox;
2524
import org.elasticsearch.index.fielddata.SortedBinaryDocValues;
2625
import org.elasticsearch.index.fielddata.SortedNumericDoubleValues;
2726
import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
@@ -36,13 +35,11 @@ public class CellIdSource extends ValuesSource.Numeric {
3635
private final ValuesSource.Geo valuesSource;
3736
private final int precision;
3837
private final GeoGridTiler encoder;
39-
private final GeoBoundingBox geoBoundingBox;
4038

41-
public CellIdSource(ValuesSource.Geo valuesSource, int precision, GeoBoundingBox geoBoundingBox, GeoGridTiler encoder) {
39+
public CellIdSource(ValuesSource.Geo valuesSource, int precision, GeoGridTiler encoder) {
4240
this.valuesSource = valuesSource;
4341
//different GeoPoints could map to the same or different hashing cells.
4442
this.precision = precision;
45-
this.geoBoundingBox = geoBoundingBox;
4643
this.encoder = encoder;
4744
}
4845

@@ -65,19 +62,10 @@ public SortedNumericDocValues longValues(LeafReaderContext ctx) {
6562
ValuesSourceType vs = geoValues.valuesSourceType();
6663
if (CoreValuesSourceType.GEOPOINT == vs) {
6764
// docValues are geo points
68-
if (geoBoundingBox.isUnbounded()) {
69-
return new UnboundedGeoPointCellValues(geoValues, precision, encoder);
70-
} else {
71-
return new BoundedGeoPointCellValues(geoValues, precision, encoder, geoBoundingBox);
72-
}
65+
return new GeoPointCellValues(geoValues, precision, encoder);
7366
} else if (CoreValuesSourceType.GEOSHAPE == vs || CoreValuesSourceType.GEO == vs) {
7467
// docValues are geo shapes
75-
if (geoBoundingBox.isUnbounded()) {
76-
return new GeoShapeCellValues(geoValues, precision, encoder);
77-
} else {
78-
// TODO(talevy): support unbounded
79-
throw new IllegalArgumentException("bounded geogrid is not supported on geo_shape fields");
80-
}
68+
return new GeoShapeCellValues(geoValues, precision, encoder);
8169
} else {
8270
throw new IllegalArgumentException("unsupported geo type");
8371
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ public boolean advanceExact(int docId) throws IOException {
5656
}
5757
}
5858

59+
// for testing
60+
protected long[] getValues() {
61+
return values;
62+
}
63+
64+
protected void add(int idx, long value) {
65+
values[idx] = value;
66+
}
67+
68+
void resizeCell(int newSize) {
69+
resize(newSize);
70+
}
71+
5972
/**
6073
* Sets the appropriate long-encoded value for <code>target</code>
6174
* in <code>values</code>.

0 commit comments

Comments
 (0)