Skip to content

Commit 5aa0a84

Browse files
committed
GeoHash Filter
############## Previous versions of the GeoPointFieldMapper just stored the actual geohash of a point. This commit changes the behavior of storing geohashes by storing the geohash and all its prefixes in decreasing order in the same field. To enable this functionality the option geohash_prefix must be set in the mapping. This behavior allows to filter GeoPoints by their geohashes. Basically a geohash prefix is defined by the filter and all geohashes that match this prefix will be returned. The neighbors flag allows to filter geohashes that surround the given geohash cell. In general the neighborhood of a geohash is defined by its eight adjacent cells. To enable this, the type of filtered fields must be geo_point with geohashes and geohash_prefix enabled. For example: curl -XPUT 'http://127.0.0.1:9200/locations/?pretty=true' -d '{ "mappings" : { "location": { "properties": { "pin": { "type": "geo_point", "geohash": true, "geohash_prefix": true } } } } }' This example defines a mapping for a type location in an index locations with a field pin. The option geohash arranges storing the geohash of the pin field. To filter the results by the geohash a geohash_cell needs to be defined. For example curl -XGET 'http://127.0.0.1:9200/locations/_search?pretty=true' -d '{ "query": { "match_all":{} }, "filter": { "geohash_cell": { "field": "pin", "geohash": "u30", "neighbors": true } } }' This filter will match all geohashes that start with one of the following prefixes: u30, u1r, u32, u33, u1p, u31, u0z, u2b and u2c. Internally the GeoHashFilter is either a simple TermFilter, in case no neighbors should be filtered or a BooleanFilter combining the TermFilters of the geohash and all its neighbors. Closes #2778
1 parent bc90e73 commit 5aa0a84

File tree

8 files changed

+596
-59
lines changed

8 files changed

+596
-59
lines changed

src/main/java/org/elasticsearch/common/geo/GeoHashUtils.java

Lines changed: 168 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717

1818
package org.elasticsearch.common.geo;
1919

20-
import gnu.trove.map.hash.TIntIntHashMap;
20+
import org.elasticsearch.ElasticSearchIllegalArgumentException;
21+
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
2125

2226
/**
2327
* Utilities for encoding and decoding geohashes. Based on
@@ -31,19 +35,9 @@ public class GeoHashUtils {
3135
'7', '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n',
3236
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
3337

34-
// private final static Map<Character, Integer> DECODE_MAP = new HashMap<Character, Integer>();
35-
36-
private final static TIntIntHashMap DECODE_MAP = new TIntIntHashMap();
37-
3838
public static final int PRECISION = 12;
3939
private static final int[] BITS = {16, 8, 4, 2, 1};
4040

41-
static {
42-
for (int i = 0; i < BASE_32.length; i++) {
43-
DECODE_MAP.put(BASE_32[i], i);
44-
}
45-
}
46-
4741
private GeoHashUtils() {
4842
}
4943

@@ -112,6 +106,144 @@ public static String encode(double latitude, double longitude, int precision) {
112106
return geohash.toString();
113107
}
114108

109+
private static final char encode(int x, int y) {
110+
return BASE_32[((x&1) + ((y&1)*2) + ((x&2)*2) + ((y&2)*4) + ((x&4)*4)) % 32];
111+
}
112+
113+
/**
114+
* Calculate all neighbors of a given geohash cell.
115+
* @param geohash Geohash of the defines cell
116+
* @return geohashes of all neighbor cells
117+
*/
118+
public static String[] neighbors(String geohash) {
119+
List<String> neighbors = addNeighbors(geohash, geohash.length(), new ArrayList<String>(8));
120+
return neighbors.toArray(new String[neighbors.size()]);
121+
}
122+
123+
/**
124+
* Calculate the geohash of a neighbor of a geohash
125+
*
126+
* @param geohash the geohash of a cell
127+
* @param level level of the geohash
128+
* @param dx delta of the first grid coordinate (must be -1, 0 or +1)
129+
* @param dy delta of the second grid coordinate (must be -1, 0 or +1)
130+
* @return geohash of the defined cell
131+
*/
132+
private final static String neighbor(String geohash, int level, int dx, int dy) {
133+
int cell = decode(geohash.charAt(level-1));
134+
135+
// Decoding the Geohash bit pattern to determine grid coordinates
136+
int x0 = cell & 1; // first bit of x
137+
int y0 = cell & 2; // first bit of y
138+
int x1 = cell & 4; // second bit of x
139+
int y1 = cell & 8; // second bit of y
140+
int x2 = cell & 16; // third bit of x
141+
142+
// combine the bitpattern to grid coordinates.
143+
// note that the semantics of x and y are swapping
144+
// on each level
145+
int x = x0 + (x1/2) + (x2 / 4);
146+
int y = (y0/2) + (y1/4);
147+
148+
if(level == 1) {
149+
// Root cells at north (namely "bcfguvyz") or at
150+
// south (namely "0145hjnp") do not have neighbors
151+
// in north/south direction
152+
if((dy < 0 && y == 0) || (dy > 0 && y == 3)) {
153+
return null;
154+
} else {
155+
return Character.toString(encode(x + dx, y + dy));
156+
}
157+
} else {
158+
// define grid coordinates for next level
159+
final int nx = ((level % 2) == 1) ?(x + dx) :(x + dy);
160+
final int ny = ((level % 2) == 1) ?(y + dy) :(y + dx);
161+
162+
// define grid limits for current level
163+
final int xLimit = ((level % 2) == 0) ?7 :3;
164+
final int yLimit = ((level % 2) == 0) ?3 :7;
165+
166+
// if the defined neighbor has the same parent a the current cell
167+
// encode the cell direcly. Otherwise find the cell next to this
168+
// cell recursively. Since encoding wraps around within a cell
169+
// it can be encoded here.
170+
if(nx >= 0 && nx<=xLimit && ny>=0 && ny<yLimit) {
171+
return geohash.substring(0, level - 1) + encode(nx, ny);
172+
} else {
173+
return neighbor(geohash, level - 1, dx, dy) + encode(nx, ny);
174+
}
175+
}
176+
}
177+
178+
/**
179+
* Add all geohashes of the cells next to a given geohash to a list.
180+
*
181+
* @param geohash Geohash of a specified cell
182+
* @param length level of the given geohash
183+
* @param neighbors list to add the neighbors to
184+
* @return the given list
185+
*/
186+
private static final List<String> addNeighbors(String geohash, int length, List<String> neighbors) {
187+
String south = neighbor(geohash, length, 0, -1);
188+
String north = neighbor(geohash, length, 0, +1);
189+
190+
if(north != null) {
191+
neighbors.add(neighbor(north, length, -1, 0));
192+
neighbors.add(north);
193+
neighbors.add(neighbor(north, length, +1, 0));
194+
}
195+
196+
neighbors.add(neighbor(geohash, length, -1, 0));
197+
neighbors.add(neighbor(geohash, length, +1, 0));
198+
199+
if(south != null) {
200+
neighbors.add(neighbor(south, length, -1, 0));
201+
neighbors.add(south);
202+
neighbors.add(neighbor(south, length, +1, 0));
203+
}
204+
205+
return neighbors;
206+
}
207+
208+
private static final int decode(char geo) {
209+
switch (geo) {
210+
case '0': return 0;
211+
case '1': return 1;
212+
case '2': return 2;
213+
case '3': return 3;
214+
case '4': return 4;
215+
case '5': return 5;
216+
case '6': return 6;
217+
case '7': return 7;
218+
case '8': return 8;
219+
case '9': return 9;
220+
case 'b': return 10;
221+
case 'c': return 11;
222+
case 'd': return 12;
223+
case 'e': return 13;
224+
case 'f': return 14;
225+
case 'g': return 15;
226+
case 'h': return 16;
227+
case 'j': return 17;
228+
case 'k': return 18;
229+
case 'm': return 19;
230+
case 'n': return 20;
231+
case 'p': return 21;
232+
case 'q': return 22;
233+
case 'r': return 23;
234+
case 's': return 24;
235+
case 't': return 25;
236+
case 'u': return 26;
237+
case 'v': return 27;
238+
case 'w': return 28;
239+
case 'x': return 29;
240+
case 'y': return 30;
241+
case 'z': return 31;
242+
default:
243+
throw new ElasticSearchIllegalArgumentException("the character '"+geo+"' is not a valid geohash character");
244+
}
245+
}
246+
115247
public static GeoPoint decode(String geohash) {
116248
GeoPoint point = new GeoPoint();
117249
decode(geohash, point);
@@ -125,44 +257,48 @@ public static GeoPoint decode(String geohash) {
125257
* @return Array with the latitude at index 0, and longitude at index 1
126258
*/
127259
public static void decode(String geohash, GeoPoint ret) {
128-
// double[] latInterval = {-90.0, 90.0};
129-
// double[] lngInterval = {-180.0, 180.0};
130-
double latInterval0 = -90.0;
131-
double latInterval1 = 90.0;
132-
double lngInterval0 = -180.0;
133-
double lngInterval1 = 180.0;
260+
double[] interval = decodeCell(geohash);
261+
ret.reset((interval[0] + interval[1]) / 2D, (interval[2] + interval[3]) / 2D);
262+
263+
}
264+
265+
/**
266+
* Decodes the given geohash into a geohash cell defined by the points nothWest and southEast
267+
*
268+
* @param geohash Geohash to deocde
269+
* @param northWest the point north/west of the cell
270+
* @param southEast the point south/east of the cell
271+
*/
272+
public static void decodeCell(String geohash, GeoPoint northWest, GeoPoint southEast) {
273+
double[] interval = decodeCell(geohash);
274+
northWest.reset(interval[1], interval[2]);
275+
southEast.reset(interval[0], interval[3]);
276+
}
134277

278+
private static double[] decodeCell(String geohash) {
279+
double[] interval = {-90.0, 90.0, -180.0, 180.0};
135280
boolean isEven = true;
136281

137282
for (int i = 0; i < geohash.length(); i++) {
138-
final int cd = DECODE_MAP.get(geohash.charAt(i));
283+
final int cd = decode(geohash.charAt(i));
139284

140285
for (int mask : BITS) {
141286
if (isEven) {
142287
if ((cd & mask) != 0) {
143-
// lngInterval[0] = (lngInterval[0] + lngInterval[1]) / 2D;
144-
lngInterval0 = (lngInterval0 + lngInterval1) / 2D;
288+
interval[2] = (interval[2] + interval[3]) / 2D;
145289
} else {
146-
// lngInterval[1] = (lngInterval[0] + lngInterval[1]) / 2D;
147-
lngInterval1 = (lngInterval0 + lngInterval1) / 2D;
290+
interval[3] = (interval[2] + interval[3]) / 2D;
148291
}
149292
} else {
150293
if ((cd & mask) != 0) {
151-
// latInterval[0] = (latInterval[0] + latInterval[1]) / 2D;
152-
latInterval0 = (latInterval0 + latInterval1) / 2D;
294+
interval[0] = (interval[0] + interval[1]) / 2D;
153295
} else {
154-
// latInterval[1] = (latInterval[0] + latInterval[1]) / 2D;
155-
latInterval1 = (latInterval0 + latInterval1) / 2D;
296+
interval[1] = (interval[0] + interval[1]) / 2D;
156297
}
157298
}
158299
isEven = !isEven;
159300
}
160-
161301
}
162-
// latitude = (latInterval[0] + latInterval[1]) / 2D;
163-
// longitude = (lngInterval[0] + lngInterval[1]) / 2D;
164-
165-
ret.reset((latInterval0 + latInterval1) / 2D, (lngInterval0 + lngInterval1) / 2D);
166-
// return ret;
302+
return interval;
167303
}
168304
}

0 commit comments

Comments
 (0)