Skip to content

Commit e4f2c31

Browse files
authored
Add geo_shape support for geotile_grid and geohash_grid (#55966) (#56228)
this commit adds aggregation support for the geo_shape field type on geo*_grid aggregations. it introduces a Tiler for both tiles and hashes that enables a new type of ValuesSource to replace the GeoPoint's CellIdSource. This makes it possible for the existing Aggregator to be re-used, so no new implementations of the grid aggregators are added.
1 parent 6a51017 commit e4f2c31

File tree

29 files changed

+2160
-36
lines changed

29 files changed

+2160
-36
lines changed

libs/geo/src/main/java/org/elasticsearch/geometry/utils/Geohash.java

+41
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ public class Geohash {
5353
/** Bit encoded representation of the latitude of north pole */
5454
private static final long MAX_LAT_BITS = (0x1L << (PRECISION * 5 / 2)) - 1;
5555

56+
// Below code is adapted from the spatial4j library (GeohashUtils.java) Apache 2.0 Licensed
57+
private static final double[] precisionToLatHeight, precisionToLonWidth;
58+
static {
59+
precisionToLatHeight = new double[PRECISION + 1];
60+
precisionToLonWidth = new double[PRECISION + 1];
61+
precisionToLatHeight[0] = 90*2;
62+
precisionToLonWidth[0] = 180*2;
63+
boolean even = false;
64+
for(int i = 1; i <= PRECISION; i++) {
65+
precisionToLatHeight[i] = precisionToLatHeight[i-1] / (even ? 8 : 4);
66+
precisionToLonWidth[i] = precisionToLonWidth[i-1] / (even ? 4 : 8);
67+
even = ! even;
68+
}
69+
}
5670

5771
// no instance:
5872
private Geohash() {
@@ -97,6 +111,16 @@ public static Rectangle toBoundingBox(final String geohash) {
97111
}
98112
}
99113

114+
/** Array of geohashes one level below the baseGeohash. Sorted. */
115+
public static String[] getSubGeohashes(String baseGeohash) {
116+
String[] hashes = new String[BASE_32.length];
117+
for (int i = 0; i < BASE_32.length; i++) {//note: already sorted
118+
char c = BASE_32[i];
119+
hashes[i] = baseGeohash+c;
120+
}
121+
return hashes;
122+
}
123+
100124
/**
101125
* Calculate all neighbors of a given geohash cell.
102126
*
@@ -201,6 +225,13 @@ public static final String getNeighbor(String geohash, int level, int dx, int dy
201225
}
202226
}
203227

228+
/**
229+
* Encode a string geohash to the geohash based long format (lon/lat interleaved, 4 least significant bits = level)
230+
*/
231+
public static final long longEncode(String hash) {
232+
return longEncode(hash, hash.length());
233+
}
234+
204235
/**
205236
* Encode lon/lat to the geohash based long format (lon/lat interleaved, 4 least significant bits = level)
206237
*/
@@ -290,6 +321,16 @@ public static long mortonEncode(final String hash) {
290321
return BitUtil.flipFlop(l);
291322
}
292323

324+
/** approximate width of geohash tile for a specific precision in degrees */
325+
public static double lonWidthInDegrees(int precision) {
326+
return precisionToLonWidth[precision];
327+
}
328+
329+
/** approximate height of geohash tile for a specific precision in degrees */
330+
public static double latHeightInDegrees(int precision) {
331+
return precisionToLatHeight[precision];
332+
}
333+
293334
private static long encodeLatLon(final double lat, final double lon) {
294335
// encode lat/lon flipping the sign bit so negative ints sort before positive ints
295336
final int latEnc = encodeLatitude(lat) ^ 0x80000000;

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.search.aggregations.LeafBucketCollector;
2929
import org.elasticsearch.search.aggregations.LeafBucketCollectorBase;
3030
import org.elasticsearch.search.aggregations.bucket.BucketsAggregator;
31+
import org.elasticsearch.search.aggregations.support.ValuesSource;
3132
import org.elasticsearch.search.internal.SearchContext;
3233

3334
import java.io.IOException;
@@ -43,10 +44,11 @@ public abstract class GeoGridAggregator<T extends InternalGeoGrid> extends Bucke
4344

4445
protected final int requiredSize;
4546
protected final int shardSize;
46-
protected final CellIdSource valuesSource;
47+
protected final ValuesSource.Numeric valuesSource;
4748
protected final LongHash bucketOrds;
49+
protected SortedNumericDocValues values;
4850

49-
GeoGridAggregator(String name, AggregatorFactories factories, CellIdSource valuesSource,
51+
GeoGridAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource,
5052
int requiredSize, int shardSize, SearchContext aggregationContext,
5153
Aggregator parent, Map<String, Object> metadata) throws IOException {
5254
super(name, factories, aggregationContext, parent, metadata);
@@ -67,7 +69,7 @@ public ScoreMode scoreMode() {
6769
@Override
6870
public LeafBucketCollector getLeafCollector(LeafReaderContext ctx,
6971
final LeafBucketCollector sub) throws IOException {
70-
final SortedNumericDocValues values = valuesSource.longValues(ctx);
72+
values = valuesSource.longValues(ctx);
7173
return new LeafBucketCollectorBase(sub, null) {
7274
@Override
7375
public void collect(int doc, long bucket) throws IOException {

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

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

2121
import org.elasticsearch.search.aggregations.Aggregator;
2222
import org.elasticsearch.search.aggregations.AggregatorFactories;
23+
import org.elasticsearch.search.aggregations.support.ValuesSource;
2324
import org.elasticsearch.search.internal.SearchContext;
2425

2526
import java.io.IOException;
@@ -32,9 +33,9 @@
3233
*/
3334
public class GeoHashGridAggregator extends GeoGridAggregator<InternalGeoHashGrid> {
3435

35-
GeoHashGridAggregator(String name, AggregatorFactories factories, CellIdSource valuesSource,
36-
int requiredSize, int shardSize, SearchContext aggregationContext,
37-
Aggregator parent, Map<String, Object> metadata) throws IOException {
36+
public GeoHashGridAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource,
37+
int requiredSize, int shardSize, SearchContext aggregationContext,
38+
Aggregator parent, Map<String, Object> metadata) throws IOException {
3839
super(name, factories, valuesSource, requiredSize, shardSize, aggregationContext, parent, metadata);
3940
}
4041

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.elasticsearch.search.aggregations.Aggregator;
2323
import org.elasticsearch.search.aggregations.AggregatorFactories;
24+
import org.elasticsearch.search.aggregations.support.ValuesSource;
2425
import org.elasticsearch.search.internal.SearchContext;
2526

2627
import java.io.IOException;
@@ -33,9 +34,9 @@
3334
*/
3435
public class GeoTileGridAggregator extends GeoGridAggregator<InternalGeoTileGrid> {
3536

36-
GeoTileGridAggregator(String name, AggregatorFactories factories, CellIdSource valuesSource,
37-
int requiredSize, int shardSize, SearchContext aggregationContext,
38-
Aggregator parent, Map<String, Object> metadata) throws IOException {
37+
public GeoTileGridAggregator(String name, AggregatorFactories factories, ValuesSource.Numeric valuesSource,
38+
int requiredSize, int shardSize, SearchContext aggregationContext,
39+
Aggregator parent, Map<String, Object> metadata) throws IOException {
3940
super(name, factories, valuesSource, requiredSize, shardSize, aggregationContext, parent, metadata);
4041
}
4142

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

+87-17
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818
*/
1919
package org.elasticsearch.search.aggregations.bucket.geogrid;
2020

21+
import org.apache.lucene.geo.GeoEncodingUtils;
22+
import org.apache.lucene.util.SloppyMath;
2123
import org.elasticsearch.ElasticsearchParseException;
2224
import org.elasticsearch.common.geo.GeoPoint;
2325
import org.elasticsearch.common.util.ESSloppyMath;
2426
import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
2527
import org.elasticsearch.common.xcontent.XContentParser;
2628
import org.elasticsearch.common.xcontent.support.XContentMapValues;
29+
import org.elasticsearch.geometry.Rectangle;
2730

2831
import java.io.IOException;
2932
import java.util.Locale;
@@ -43,6 +46,8 @@ public final class GeoTileUtils {
4346

4447
private GeoTileUtils() {}
4548

49+
private static final double PI_DIV_2 = Math.PI / 2;
50+
4651
/**
4752
* Largest number of tiles (precision) to use.
4853
* This value cannot be more than (64-5)/2 = 29, because 5 bits are used for zoom level itself (0-31)
@@ -53,6 +58,18 @@ private GeoTileUtils() {}
5358
*/
5459
public static final int MAX_ZOOM = 29;
5560

61+
/**
62+
* The geo-tile map is clipped at 85.05112878 to 90 and -85.05112878 to -90
63+
*/
64+
public static final double LATITUDE_MASK = 85.0511287798066;
65+
66+
/**
67+
* Since shapes are encoded, their boundaries are to be compared to against the encoded/decoded values of <code>LATITUDE_MASK</code>
68+
*/
69+
public static final double NORMALIZED_LATITUDE_MASK = GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(LATITUDE_MASK));
70+
public static final double NORMALIZED_NEGATIVE_LATITUDE_MASK =
71+
GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(-LATITUDE_MASK));
72+
5673
/**
5774
* Bit position of the zoom value within hash - zoom is stored in the most significant 6 bits of a long number.
5875
*/
@@ -63,6 +80,7 @@ private GeoTileUtils() {}
6380
*/
6481
private static final long X_Y_VALUE_MASK = (1L << MAX_ZOOM) - 1;
6582

83+
6684
/**
6785
* Parse an integer precision (zoom level). The {@link ValueType#INT} allows it to be a number or a string.
6886
*
@@ -90,37 +108,65 @@ public static int checkPrecisionRange(int precision) {
90108
}
91109

92110
/**
93-
* Encode lon/lat to the geotile based long format.
94-
* The resulting hash contains interleaved tile X and Y coordinates.
95-
* The precision itself is also encoded as a few high bits.
111+
* Calculates the x-coordinate in the tile grid for the specified longitude given
112+
* the number of tile columns for a pre-determined zoom-level.
113+
*
114+
* @param longitude the longitude to use when determining the tile x-coordinate
115+
* @param tiles the number of tiles per row for a pre-determined zoom-level
96116
*/
97-
public static long longEncode(double longitude, double latitude, int precision) {
98-
// Mathematics for this code was adapted from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Java
99-
100-
// Number of tiles for the current zoom level along the X and Y axis
101-
final long tiles = 1 << checkPrecisionRange(precision);
102-
103-
long xTile = (long) Math.floor((normalizeLon(longitude) + 180) / 360 * tiles);
117+
public static int getXTile(double longitude, long tiles) {
118+
// normalizeLon treats this as 180, which is not friendly for tile mapping
119+
if (longitude == -180) {
120+
return 0;
121+
}
104122

105-
double latSin = Math.sin(Math.toRadians(normalizeLat(latitude)));
106-
long yTile = (long) Math.floor((0.5 - (Math.log((1 + latSin) / (1 - latSin)) / (4 * Math.PI))) * tiles);
123+
int xTile = (int) Math.floor((normalizeLon(longitude) + 180) / 360 * tiles);
107124

108125
// Edge values may generate invalid values, and need to be clipped.
109126
// For example, polar regions (above/below lat 85.05112878) get normalized.
110127
if (xTile < 0) {
111-
xTile = 0;
128+
return 0;
112129
}
113130
if (xTile >= tiles) {
114-
xTile = tiles - 1;
131+
return (int) tiles - 1;
115132
}
133+
134+
return xTile;
135+
}
136+
137+
/**
138+
* Calculates the y-coordinate in the tile grid for the specified longitude given
139+
* the number of tile rows for pre-determined zoom-level.
140+
*
141+
* @param latitude the latitude to use when determining the tile y-coordinate
142+
* @param tiles the number of tiles per column for a pre-determined zoom-level
143+
*/
144+
public static int getYTile(double latitude, long tiles) {
145+
double latSin = SloppyMath.cos(PI_DIV_2 - Math.toRadians(normalizeLat(latitude)));
146+
int yTile = (int) Math.floor((0.5 - (Math.log((1 + latSin) / (1 - latSin)) / (4 * Math.PI))) * tiles);
147+
116148
if (yTile < 0) {
117149
yTile = 0;
118150
}
119151
if (yTile >= tiles) {
120-
yTile = tiles - 1;
152+
return (int) tiles - 1;
121153
}
122154

123-
return longEncode((long) precision, xTile, yTile);
155+
return yTile;
156+
}
157+
158+
/**
159+
* Encode lon/lat to the geotile based long format.
160+
* The resulting hash contains interleaved tile X and Y coordinates.
161+
* The precision itself is also encoded as a few high bits.
162+
*/
163+
public static long longEncode(double longitude, double latitude, int precision) {
164+
// Mathematics for this code was adapted from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Java
165+
// Number of tiles for the current zoom level along the X and Y axis
166+
final long tiles = 1 << checkPrecisionRange(precision);
167+
long xTile = getXTile(longitude, tiles);
168+
long yTile = getYTile(latitude, tiles);
169+
return longEncodeTiles(precision, xTile, yTile);
124170
}
125171

126172
/**
@@ -131,7 +177,14 @@ public static long longEncode(double longitude, double latitude, int precision)
131177
*/
132178
public static long longEncode(String hashAsString) {
133179
int[] parsed = parseHash(hashAsString);
134-
return longEncode((long)parsed[0], (long)parsed[1], (long)parsed[2]);
180+
return longEncode((long) parsed[0], (long) parsed[1], (long) parsed[2]);
181+
}
182+
183+
public static long longEncodeTiles(int precision, long xTile, long yTile) {
184+
// Zoom value is placed in front of all the bits used for the geotile
185+
// e.g. when max zoom is 29, the largest index would use 58 bits (57th..0th),
186+
// leaving 5 bits unused for zoom. See MAX_ZOOM comment above.
187+
return ((long) precision << ZOOM_SHIFT) | (xTile << MAX_ZOOM) | yTile;
135188
}
136189

137190
/**
@@ -193,6 +246,23 @@ static GeoPoint keyToGeoPoint(String hashAsString) {
193246
return zxyToGeoPoint(hashAsInts[0], hashAsInts[1], hashAsInts[2]);
194247
}
195248

249+
public static Rectangle toBoundingBox(long hash) {
250+
int[] hashAsInts = parseHash(hash);
251+
return toBoundingBox(hashAsInts[1], hashAsInts[2], hashAsInts[0]);
252+
}
253+
254+
public static Rectangle toBoundingBox(int xTile, int yTile, int precision) {
255+
final double tiles = validateZXY(precision, xTile, yTile);
256+
final double minN = Math.PI - (2.0 * Math.PI * (yTile + 1)) / tiles;
257+
final double maxN = Math.PI - (2.0 * Math.PI * (yTile)) / tiles;
258+
final double minY = Math.toDegrees(ESSloppyMath.atan(ESSloppyMath.sinh(minN)));
259+
final double minX = ((xTile) / tiles * 360.0) - 180;
260+
final double maxY = Math.toDegrees(ESSloppyMath.atan(ESSloppyMath.sinh(maxN)));
261+
final double maxX = ((xTile + 1) / tiles * 360.0) - 180;
262+
263+
return new Rectangle(minX, maxX, maxY, minY);
264+
}
265+
196266
/**
197267
* Validates Zoom, X, and Y values, and returns the total number of allowed tiles along the x/y axis.
198268
*/

server/src/test/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoTileUtilsTests.java

+24-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.search.aggregations.bucket.geogrid;
2121

2222
import org.elasticsearch.common.geo.GeoPoint;
23+
import org.elasticsearch.geometry.Rectangle;
2324
import org.elasticsearch.test.ESTestCase;
2425

2526
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.MAX_ZOOM;
@@ -28,8 +29,10 @@
2829
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.keyToGeoPoint;
2930
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.longEncode;
3031
import static org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils.stringEncode;
32+
import static org.hamcrest.Matchers.anyOf;
3133
import static org.hamcrest.Matchers.closeTo;
3234
import static org.hamcrest.Matchers.containsString;
35+
import static org.hamcrest.Matchers.equalTo;
3336

3437
public class GeoTileUtilsTests extends ESTestCase {
3538

@@ -219,8 +222,8 @@ public void testGeoTileAsLongRoutines() {
219222
* so ensure they are clipped correctly.
220223
*/
221224
public void testSingularityAtPoles() {
222-
double minLat = -85.05112878;
223-
double maxLat = 85.05112878;
225+
double minLat = -GeoTileUtils.LATITUDE_MASK;
226+
double maxLat = GeoTileUtils.LATITUDE_MASK;
224227
double lon = randomIntBetween(-180, 180);
225228
double lat = randomBoolean()
226229
? randomDoubleBetween(-90, minLat, true)
@@ -231,4 +234,23 @@ public void testSingularityAtPoles() {
231234
String clippedTileIndex = stringEncode(longEncode(lon, clippedLat, zoom));
232235
assertEquals(tileIndex, clippedTileIndex);
233236
}
237+
238+
public void testPointToTile() {
239+
int zoom = randomIntBetween(0, MAX_ZOOM);
240+
int tiles = 1 << zoom;
241+
int xTile = randomIntBetween(0, zoom);
242+
int yTile = randomIntBetween(0, zoom);
243+
Rectangle rectangle = GeoTileUtils.toBoundingBox(xTile, yTile, zoom);
244+
// check corners
245+
assertThat(GeoTileUtils.getXTile(rectangle.getMinX(), tiles), equalTo(xTile));
246+
assertThat(GeoTileUtils.getXTile(rectangle.getMaxX(), tiles), equalTo(Math.min(tiles - 1, xTile + 1)));
247+
assertThat(GeoTileUtils.getYTile(rectangle.getMaxY(), tiles), anyOf(equalTo(yTile - 1), equalTo(yTile)));
248+
assertThat(GeoTileUtils.getYTile(rectangle.getMinY(), tiles), anyOf(equalTo(yTile + 1), equalTo(yTile)));
249+
// check point inside
250+
double x = randomDoubleBetween(rectangle.getMinX(), rectangle.getMaxX(), false);
251+
double y = randomDoubleBetween(rectangle.getMinY(), rectangle.getMaxY(), false);
252+
assertThat(GeoTileUtils.getXTile(x, tiles), equalTo(xTile));
253+
assertThat(GeoTileUtils.getYTile(y, tiles), equalTo(yTile));
254+
255+
}
234256
}

x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java

+2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ public enum Feature {
9191

9292
SPATIAL_GEO_CENTROID(OperationMode.GOLD, true),
9393

94+
SPATIAL_GEO_GRID(OperationMode.GOLD, true),
95+
9496
ANALYTICS(OperationMode.MISSING, true);
9597

9698
final OperationMode minimumOperationMode;

x-pack/plugin/spatial/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ restResources {
2727

2828
testClusters.integTest {
2929
setting 'xpack.license.self_generated.type', 'trial'
30+
setting 'indices.breaker.request.limit', '25kb'
3031
testDistribution = 'DEFAULT'
3132
}
3233

0 commit comments

Comments
 (0)