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
This commit is contained in:
parent
bc90e73932
commit
5aa0a8438f
|
@ -17,7 +17,11 @@
|
|||
|
||||
package org.elasticsearch.common.geo;
|
||||
|
||||
import gnu.trove.map.hash.TIntIntHashMap;
|
||||
import org.elasticsearch.ElasticSearchIllegalArgumentException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* Utilities for encoding and decoding geohashes. Based on
|
||||
|
@ -31,19 +35,9 @@ public class GeoHashUtils {
|
|||
'7', '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n',
|
||||
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
|
||||
|
||||
// private final static Map<Character, Integer> DECODE_MAP = new HashMap<Character, Integer>();
|
||||
|
||||
private final static TIntIntHashMap DECODE_MAP = new TIntIntHashMap();
|
||||
|
||||
public static final int PRECISION = 12;
|
||||
private static final int[] BITS = {16, 8, 4, 2, 1};
|
||||
|
||||
static {
|
||||
for (int i = 0; i < BASE_32.length; i++) {
|
||||
DECODE_MAP.put(BASE_32[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
private GeoHashUtils() {
|
||||
}
|
||||
|
||||
|
@ -112,6 +106,144 @@ public class GeoHashUtils {
|
|||
return geohash.toString();
|
||||
}
|
||||
|
||||
private static final char encode(int x, int y) {
|
||||
return BASE_32[((x&1) + ((y&1)*2) + ((x&2)*2) + ((y&2)*4) + ((x&4)*4)) % 32];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate all neighbors of a given geohash cell.
|
||||
* @param geohash Geohash of the defines cell
|
||||
* @return geohashes of all neighbor cells
|
||||
*/
|
||||
public static String[] neighbors(String geohash) {
|
||||
List<String> neighbors = addNeighbors(geohash, geohash.length(), new ArrayList<String>(8));
|
||||
return neighbors.toArray(new String[neighbors.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the geohash of a neighbor of a geohash
|
||||
*
|
||||
* @param geohash the geohash of a cell
|
||||
* @param level level of the geohash
|
||||
* @param dx delta of the first grid coordinate (must be -1, 0 or +1)
|
||||
* @param dy delta of the second grid coordinate (must be -1, 0 or +1)
|
||||
* @return geohash of the defined cell
|
||||
*/
|
||||
private final static String neighbor(String geohash, int level, int dx, int dy) {
|
||||
int cell = decode(geohash.charAt(level-1));
|
||||
|
||||
// Decoding the Geohash bit pattern to determine grid coordinates
|
||||
int x0 = cell & 1; // first bit of x
|
||||
int y0 = cell & 2; // first bit of y
|
||||
int x1 = cell & 4; // second bit of x
|
||||
int y1 = cell & 8; // second bit of y
|
||||
int x2 = cell & 16; // third bit of x
|
||||
|
||||
// combine the bitpattern to grid coordinates.
|
||||
// note that the semantics of x and y are swapping
|
||||
// on each level
|
||||
int x = x0 + (x1/2) + (x2 / 4);
|
||||
int y = (y0/2) + (y1/4);
|
||||
|
||||
if(level == 1) {
|
||||
// Root cells at north (namely "bcfguvyz") or at
|
||||
// south (namely "0145hjnp") do not have neighbors
|
||||
// in north/south direction
|
||||
if((dy < 0 && y == 0) || (dy > 0 && y == 3)) {
|
||||
return null;
|
||||
} else {
|
||||
return Character.toString(encode(x + dx, y + dy));
|
||||
}
|
||||
} else {
|
||||
// define grid coordinates for next level
|
||||
final int nx = ((level % 2) == 1) ?(x + dx) :(x + dy);
|
||||
final int ny = ((level % 2) == 1) ?(y + dy) :(y + dx);
|
||||
|
||||
// define grid limits for current level
|
||||
final int xLimit = ((level % 2) == 0) ?7 :3;
|
||||
final int yLimit = ((level % 2) == 0) ?3 :7;
|
||||
|
||||
// if the defined neighbor has the same parent a the current cell
|
||||
// encode the cell direcly. Otherwise find the cell next to this
|
||||
// cell recursively. Since encoding wraps around within a cell
|
||||
// it can be encoded here.
|
||||
if(nx >= 0 && nx<=xLimit && ny>=0 && ny<yLimit) {
|
||||
return geohash.substring(0, level - 1) + encode(nx, ny);
|
||||
} else {
|
||||
return neighbor(geohash, level - 1, dx, dy) + encode(nx, ny);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all geohashes of the cells next to a given geohash to a list.
|
||||
*
|
||||
* @param geohash Geohash of a specified cell
|
||||
* @param length level of the given geohash
|
||||
* @param neighbors list to add the neighbors to
|
||||
* @return the given list
|
||||
*/
|
||||
private static final List<String> addNeighbors(String geohash, int length, List<String> neighbors) {
|
||||
String south = neighbor(geohash, length, 0, -1);
|
||||
String north = neighbor(geohash, length, 0, +1);
|
||||
|
||||
if(north != null) {
|
||||
neighbors.add(neighbor(north, length, -1, 0));
|
||||
neighbors.add(north);
|
||||
neighbors.add(neighbor(north, length, +1, 0));
|
||||
}
|
||||
|
||||
neighbors.add(neighbor(geohash, length, -1, 0));
|
||||
neighbors.add(neighbor(geohash, length, +1, 0));
|
||||
|
||||
if(south != null) {
|
||||
neighbors.add(neighbor(south, length, -1, 0));
|
||||
neighbors.add(south);
|
||||
neighbors.add(neighbor(south, length, +1, 0));
|
||||
}
|
||||
|
||||
return neighbors;
|
||||
}
|
||||
|
||||
private static final int decode(char geo) {
|
||||
switch (geo) {
|
||||
case '0': return 0;
|
||||
case '1': return 1;
|
||||
case '2': return 2;
|
||||
case '3': return 3;
|
||||
case '4': return 4;
|
||||
case '5': return 5;
|
||||
case '6': return 6;
|
||||
case '7': return 7;
|
||||
case '8': return 8;
|
||||
case '9': return 9;
|
||||
case 'b': return 10;
|
||||
case 'c': return 11;
|
||||
case 'd': return 12;
|
||||
case 'e': return 13;
|
||||
case 'f': return 14;
|
||||
case 'g': return 15;
|
||||
case 'h': return 16;
|
||||
case 'j': return 17;
|
||||
case 'k': return 18;
|
||||
case 'm': return 19;
|
||||
case 'n': return 20;
|
||||
case 'p': return 21;
|
||||
case 'q': return 22;
|
||||
case 'r': return 23;
|
||||
case 's': return 24;
|
||||
case 't': return 25;
|
||||
case 'u': return 26;
|
||||
case 'v': return 27;
|
||||
case 'w': return 28;
|
||||
case 'x': return 29;
|
||||
case 'y': return 30;
|
||||
case 'z': return 31;
|
||||
default:
|
||||
throw new ElasticSearchIllegalArgumentException("the character '"+geo+"' is not a valid geohash character");
|
||||
}
|
||||
}
|
||||
|
||||
public static GeoPoint decode(String geohash) {
|
||||
GeoPoint point = new GeoPoint();
|
||||
decode(geohash, point);
|
||||
|
@ -125,44 +257,48 @@ public class GeoHashUtils {
|
|||
* @return Array with the latitude at index 0, and longitude at index 1
|
||||
*/
|
||||
public static void decode(String geohash, GeoPoint ret) {
|
||||
// double[] latInterval = {-90.0, 90.0};
|
||||
// double[] lngInterval = {-180.0, 180.0};
|
||||
double latInterval0 = -90.0;
|
||||
double latInterval1 = 90.0;
|
||||
double lngInterval0 = -180.0;
|
||||
double lngInterval1 = 180.0;
|
||||
double[] interval = decodeCell(geohash);
|
||||
ret.reset((interval[0] + interval[1]) / 2D, (interval[2] + interval[3]) / 2D);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the given geohash into a geohash cell defined by the points nothWest and southEast
|
||||
*
|
||||
* @param geohash Geohash to deocde
|
||||
* @param northWest the point north/west of the cell
|
||||
* @param southEast the point south/east of the cell
|
||||
*/
|
||||
public static void decodeCell(String geohash, GeoPoint northWest, GeoPoint southEast) {
|
||||
double[] interval = decodeCell(geohash);
|
||||
northWest.reset(interval[1], interval[2]);
|
||||
southEast.reset(interval[0], interval[3]);
|
||||
}
|
||||
|
||||
private static double[] decodeCell(String geohash) {
|
||||
double[] interval = {-90.0, 90.0, -180.0, 180.0};
|
||||
boolean isEven = true;
|
||||
|
||||
for (int i = 0; i < geohash.length(); i++) {
|
||||
final int cd = DECODE_MAP.get(geohash.charAt(i));
|
||||
final int cd = decode(geohash.charAt(i));
|
||||
|
||||
for (int mask : BITS) {
|
||||
if (isEven) {
|
||||
if ((cd & mask) != 0) {
|
||||
// lngInterval[0] = (lngInterval[0] + lngInterval[1]) / 2D;
|
||||
lngInterval0 = (lngInterval0 + lngInterval1) / 2D;
|
||||
interval[2] = (interval[2] + interval[3]) / 2D;
|
||||
} else {
|
||||
// lngInterval[1] = (lngInterval[0] + lngInterval[1]) / 2D;
|
||||
lngInterval1 = (lngInterval0 + lngInterval1) / 2D;
|
||||
interval[3] = (interval[2] + interval[3]) / 2D;
|
||||
}
|
||||
} else {
|
||||
if ((cd & mask) != 0) {
|
||||
// latInterval[0] = (latInterval[0] + latInterval[1]) / 2D;
|
||||
latInterval0 = (latInterval0 + latInterval1) / 2D;
|
||||
interval[0] = (interval[0] + interval[1]) / 2D;
|
||||
} else {
|
||||
// latInterval[1] = (latInterval[0] + latInterval[1]) / 2D;
|
||||
latInterval1 = (latInterval0 + latInterval1) / 2D;
|
||||
interval[1] = (interval[0] + interval[1]) / 2D;
|
||||
}
|
||||
}
|
||||
isEven = !isEven;
|
||||
}
|
||||
|
||||
}
|
||||
// latitude = (latInterval[0] + latInterval[1]) / 2D;
|
||||
// longitude = (lngInterval[0] + lngInterval[1]) / 2D;
|
||||
|
||||
ret.reset((latInterval0 + latInterval1) / 2D, (lngInterval0 + lngInterval1) / 2D);
|
||||
// return ret;
|
||||
return interval;
|
||||
}
|
||||
}
|
|
@ -19,8 +19,11 @@
|
|||
|
||||
package org.elasticsearch.index.mapper.geo;
|
||||
|
||||
import org.apache.lucene.document.FieldType;
|
||||
import org.apache.lucene.index.FieldInfo.IndexOptions;
|
||||
import static org.elasticsearch.index.mapper.MapperBuilders.doubleField;
|
||||
import static org.elasticsearch.index.mapper.MapperBuilders.stringField;
|
||||
import static org.elasticsearch.index.mapper.core.TypeParsers.parsePathType;
|
||||
import static org.elasticsearch.index.mapper.core.TypeParsers.parseStore;
|
||||
|
||||
import org.elasticsearch.ElasticSearchIllegalArgumentException;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.Strings;
|
||||
|
@ -34,7 +37,14 @@ import org.elasticsearch.common.xcontent.support.XContentMapValues;
|
|||
import org.elasticsearch.index.analysis.NamedAnalyzer;
|
||||
import org.elasticsearch.index.codec.postingsformat.PostingsFormatProvider;
|
||||
import org.elasticsearch.index.fielddata.FieldDataType;
|
||||
import org.elasticsearch.index.mapper.*;
|
||||
import org.elasticsearch.index.mapper.ContentPath;
|
||||
import org.elasticsearch.index.mapper.FieldMapperListener;
|
||||
import org.elasticsearch.index.mapper.Mapper;
|
||||
import org.elasticsearch.index.mapper.MapperParsingException;
|
||||
import org.elasticsearch.index.mapper.MergeContext;
|
||||
import org.elasticsearch.index.mapper.MergeMappingException;
|
||||
import org.elasticsearch.index.mapper.ObjectMapperListener;
|
||||
import org.elasticsearch.index.mapper.ParseContext;
|
||||
import org.elasticsearch.index.mapper.core.AbstractFieldMapper;
|
||||
import org.elasticsearch.index.mapper.core.DoubleFieldMapper;
|
||||
import org.elasticsearch.index.mapper.core.NumberFieldMapper;
|
||||
|
@ -45,10 +55,8 @@ import java.io.IOException;
|
|||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.elasticsearch.index.mapper.MapperBuilders.doubleField;
|
||||
import static org.elasticsearch.index.mapper.MapperBuilders.stringField;
|
||||
import static org.elasticsearch.index.mapper.core.TypeParsers.parsePathType;
|
||||
import static org.elasticsearch.index.mapper.core.TypeParsers.parseStore;
|
||||
import org.apache.lucene.document.FieldType;
|
||||
import org.apache.lucene.index.FieldInfo.IndexOptions;
|
||||
|
||||
/**
|
||||
* Parsing: We handle:
|
||||
|
@ -78,6 +86,7 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
public static final boolean STORE = false;
|
||||
public static final boolean ENABLE_LATLON = false;
|
||||
public static final boolean ENABLE_GEOHASH = false;
|
||||
public static final boolean ENABLE_GEOHASH_PREFIX = false;
|
||||
public static final int PRECISION = GeoHashUtils.PRECISION;
|
||||
public static final boolean NORMALIZE_LAT = true;
|
||||
public static final boolean NORMALIZE_LON = true;
|
||||
|
@ -100,6 +109,8 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
private ContentPath.Type pathType = Defaults.PATH_TYPE;
|
||||
|
||||
private boolean enableGeoHash = Defaults.ENABLE_GEOHASH;
|
||||
|
||||
private boolean enableGeohashPrefix = Defaults.ENABLE_GEOHASH_PREFIX;
|
||||
|
||||
private boolean enableLatLon = Defaults.ENABLE_LATLON;
|
||||
|
||||
|
@ -129,6 +140,11 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder geohashPrefix(boolean enableGeohashPrefix) {
|
||||
this.enableGeohashPrefix = enableGeohashPrefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder enableLatLon(boolean enableLatLon) {
|
||||
this.enableLatLon = enableLatLon;
|
||||
return this;
|
||||
|
@ -157,14 +173,13 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
GeoStringFieldMapper geoStringMapper = new GeoStringFieldMapper.Builder(name)
|
||||
.includeInAll(false).store(store).build(context);
|
||||
|
||||
|
||||
DoubleFieldMapper latMapper = null;
|
||||
DoubleFieldMapper lonMapper = null;
|
||||
|
||||
context.path().add(name);
|
||||
if (enableLatLon) {
|
||||
NumberFieldMapper.Builder latMapperBuilder = doubleField(Names.LAT).includeInAll(false);
|
||||
NumberFieldMapper.Builder lonMapperBuilder = doubleField(Names.LON).includeInAll(false);
|
||||
NumberFieldMapper.Builder<?, ?> latMapperBuilder = doubleField(Names.LAT).includeInAll(false);
|
||||
NumberFieldMapper.Builder<?, ?> lonMapperBuilder = doubleField(Names.LON).includeInAll(false);
|
||||
if (precisionStep != null) {
|
||||
latMapperBuilder.precisionStep(precisionStep);
|
||||
lonMapperBuilder.precisionStep(precisionStep);
|
||||
|
@ -180,7 +195,7 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
|
||||
context.path().pathType(origPathType);
|
||||
|
||||
return new GeoPointFieldMapper(name, pathType, enableLatLon, enableGeoHash, precisionStep, precision,
|
||||
return new GeoPointFieldMapper(name, pathType, enableLatLon, enableGeoHash, enableGeohashPrefix, precisionStep, precision,
|
||||
latMapper, lonMapper, geohashMapper, geoStringMapper,
|
||||
validateLon, validateLat, normalizeLon, normalizeLat);
|
||||
}
|
||||
|
@ -188,7 +203,7 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
|
||||
public static class TypeParser implements Mapper.TypeParser {
|
||||
@Override
|
||||
public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException {
|
||||
public Mapper.Builder<?, ?> parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException {
|
||||
Builder builder = new Builder(name);
|
||||
|
||||
for (Map.Entry<String, Object> entry : node.entrySet()) {
|
||||
|
@ -202,6 +217,8 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
builder.enableLatLon(XContentMapValues.nodeBooleanValue(fieldNode));
|
||||
} else if (fieldName.equals("geohash")) {
|
||||
builder.enableGeoHash(XContentMapValues.nodeBooleanValue(fieldNode));
|
||||
} else if (fieldName.equals("geohash_prefix")) {
|
||||
builder.geohashPrefix(XContentMapValues.nodeBooleanValue(fieldNode));
|
||||
} else if (fieldName.equals("precision_step")) {
|
||||
builder.precisionStep(XContentMapValues.nodeIntegerValue(fieldNode));
|
||||
} else if (fieldName.equals("geohash_precision")) {
|
||||
|
@ -233,6 +250,8 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
private final boolean enableLatLon;
|
||||
|
||||
private final boolean enableGeoHash;
|
||||
|
||||
private final boolean enableGeohashPrefix;
|
||||
|
||||
private final Integer precisionStep;
|
||||
|
||||
|
@ -252,7 +271,7 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
private final boolean normalizeLon;
|
||||
private final boolean normalizeLat;
|
||||
|
||||
public GeoPointFieldMapper(String name, ContentPath.Type pathType, boolean enableLatLon, boolean enableGeoHash, Integer precisionStep, int precision,
|
||||
public GeoPointFieldMapper(String name, ContentPath.Type pathType, boolean enableLatLon, boolean enableGeoHash, boolean enableGeohashPrefix, Integer precisionStep, int precision,
|
||||
DoubleFieldMapper latMapper, DoubleFieldMapper lonMapper, StringFieldMapper geohashMapper, GeoStringFieldMapper geoStringMapper,
|
||||
boolean validateLon, boolean validateLat,
|
||||
boolean normalizeLon, boolean normalizeLat) {
|
||||
|
@ -260,9 +279,10 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
this.pathType = pathType;
|
||||
this.enableLatLon = enableLatLon;
|
||||
this.enableGeoHash = enableGeoHash;
|
||||
this.enableGeohashPrefix = enableGeohashPrefix;
|
||||
this.precisionStep = precisionStep;
|
||||
this.precision = precision;
|
||||
|
||||
|
||||
this.latMapper = latMapper;
|
||||
this.lonMapper = lonMapper;
|
||||
this.geoStringMapper = geoStringMapper;
|
||||
|
@ -293,6 +313,10 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
public GeoStringFieldMapper stringMapper() {
|
||||
return this.geoStringMapper;
|
||||
}
|
||||
|
||||
public StringFieldMapper geoHashStringMapper() {
|
||||
return this.geohashMapper;
|
||||
}
|
||||
|
||||
public boolean isEnableLatLon() {
|
||||
return enableLatLon;
|
||||
|
@ -389,6 +413,16 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
}
|
||||
}
|
||||
|
||||
private void parseGeohashField(ParseContext context, String geohash) throws IOException {
|
||||
int len = enableGeohashPrefix ?Math.min(precision, geohash.length()) :1;
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
context.externalValue(geohash.substring(0, geohash.length() - i));
|
||||
// side effect of this call is adding the field
|
||||
geohashMapper.parse(context);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseLatLon(ParseContext context, double lat, double lon) throws IOException {
|
||||
if (normalizeLat || normalizeLon) {
|
||||
GeoPoint point = new GeoPoint(lat, lon);
|
||||
|
@ -411,8 +445,7 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
context.externalValue(Double.toString(lat) + ',' + Double.toString(lon));
|
||||
geoStringMapper.parse(context);
|
||||
if (enableGeoHash) {
|
||||
context.externalValue(GeoHashUtils.encode(lat, lon, precision));
|
||||
geohashMapper.parse(context);
|
||||
parseGeohashField(context, GeoHashUtils.encode(lat, lon, precision));
|
||||
}
|
||||
if (enableLatLon) {
|
||||
context.externalValue(lat);
|
||||
|
@ -443,8 +476,7 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
context.externalValue(Double.toString(point.lat()) + ',' + Double.toString(point.lon()));
|
||||
geoStringMapper.parse(context);
|
||||
if (enableGeoHash) {
|
||||
context.externalValue(geohash);
|
||||
geohashMapper.parse(context);
|
||||
parseGeohashField(context, geohash);
|
||||
}
|
||||
if (enableLatLon) {
|
||||
context.externalValue(point.lat());
|
||||
|
@ -504,6 +536,9 @@ public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
|
|||
if (enableGeoHash != Defaults.ENABLE_GEOHASH) {
|
||||
builder.field("geohash", enableGeoHash);
|
||||
}
|
||||
if (enableGeohashPrefix != Defaults.ENABLE_GEOHASH_PREFIX) {
|
||||
builder.field("geohash_prefix", enableGeohashPrefix);
|
||||
}
|
||||
if (geoStringMapper.fieldType().stored() != Defaults.STORE) {
|
||||
builder.field("store", geoStringMapper.fieldType().stored());
|
||||
}
|
||||
|
|
|
@ -192,7 +192,7 @@ public abstract class FilterBuilders {
|
|||
* @param name The field name
|
||||
* @param values The terms
|
||||
*/
|
||||
public static TermsFilterBuilder termsFilter(String name, Iterable values) {
|
||||
public static TermsFilterBuilder termsFilter(String name, Iterable<?> values) {
|
||||
return new TermsFilterBuilder(name, values);
|
||||
}
|
||||
|
||||
|
@ -349,6 +349,42 @@ public abstract class FilterBuilders {
|
|||
return new GeoBoundingBoxFilterBuilder(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter based on a bounding box defined by geohash. The field this filter is applied to
|
||||
* must have <code>{"type":"geo_point", "geohash":true}</code>
|
||||
* to work.
|
||||
*
|
||||
* @param fieldname The geopoint field name.
|
||||
*/
|
||||
public static GeohashFilter.Builder geoHashFilter(String fieldname) {
|
||||
return new GeohashFilter.Builder(fieldname);
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter based on a bounding box defined by geohash. The field this filter is applied to
|
||||
* must have <code>{"type":"geo_point", "geohash":true}</code>
|
||||
* to work.
|
||||
*
|
||||
* @param fieldname The geopoint field name.
|
||||
* @param geohash The Geohash to filter
|
||||
*/
|
||||
public static GeohashFilter.Builder geoHashFilter(String fieldname, String geohash) {
|
||||
return new GeohashFilter.Builder(fieldname, geohash);
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter based on a bounding box defined by geohash. The field this filter is applied to
|
||||
* must have <code>{"type":"geo_point", "geohash":true}</code>
|
||||
* to work.
|
||||
*
|
||||
* @param fieldname The geopoint field name
|
||||
* @param geohash The Geohash to filter
|
||||
* @param neighbors should the neighbor cell also be filtered
|
||||
*/
|
||||
public static GeohashFilter.Builder geoHashFilter(String fieldname, String geohash, boolean neighbors) {
|
||||
return new GeohashFilter.Builder(fieldname, geohash, neighbors);
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter to filter based on a polygon defined by a set of locations / points.
|
||||
*
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
package org.elasticsearch.index.query;
|
||||
|
||||
import org.elasticsearch.common.geo.GeoHashUtils;
|
||||
import org.elasticsearch.common.geo.GeoPoint;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
|
||||
|
@ -82,6 +83,19 @@ public class GeoBoundingBoxFilterBuilder extends BaseFilterBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds top left and bottom right by geohash cell.
|
||||
*
|
||||
* @param geohash the geohash of the cell definign the boundingbox
|
||||
*/
|
||||
public GeoBoundingBoxFilterBuilder geohash(String geohash) {
|
||||
topLeft = new GeoPoint();
|
||||
bottomRight = new GeoPoint();
|
||||
GeoHashUtils.decodeCell(geohash, topLeft, bottomRight);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the filter name for the filter that can be used when searching for matched_filters per hit.
|
||||
*/
|
||||
|
|
|
@ -163,7 +163,7 @@ public class GeoBoundingBoxFilterParser implements FilterParser {
|
|||
if (smartMappers == null || !smartMappers.hasMapper()) {
|
||||
throw new QueryParsingException(parseContext.index(), "failed to find geo_point field [" + fieldName + "]");
|
||||
}
|
||||
FieldMapper mapper = smartMappers.mapper();
|
||||
FieldMapper<?> mapper = smartMappers.mapper();
|
||||
if (!(mapper instanceof GeoPointFieldMapper.GeoStringFieldMapper)) {
|
||||
throw new QueryParsingException(parseContext.index(), "field [" + fieldName + "] is not a geo_point field");
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ public class GeoBoundingBoxFilterParser implements FilterParser {
|
|||
if ("indexed".equals(type)) {
|
||||
filter = IndexedGeoBoundingBoxFilter.create(topLeft, bottomRight, geoMapper);
|
||||
} else if ("memory".equals(type)) {
|
||||
IndexGeoPointFieldData indexFieldData = parseContext.fieldData().getForField(mapper);
|
||||
IndexGeoPointFieldData<?> indexFieldData = parseContext.fieldData().getForField(mapper);
|
||||
filter = new InMemoryGeoBoundingBoxFilter(topLeft, bottomRight, indexFieldData);
|
||||
} else {
|
||||
throw new QueryParsingException(parseContext.index(), "geo bounding box type [" + type + "] not supported, either 'indexed' or 'memory' are allowed");
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* Licensed to ElasticSearch and Shay Banon under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. ElasticSearch licenses this
|
||||
* file to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.index.query;
|
||||
|
||||
import org.elasticsearch.ElasticSearchIllegalArgumentException;
|
||||
import org.elasticsearch.ElasticSearchParseException;
|
||||
import org.elasticsearch.common.geo.GeoHashUtils;
|
||||
import org.elasticsearch.common.geo.GeoPoint;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.XContentParser.Token;
|
||||
import org.elasticsearch.index.mapper.FieldMapper;
|
||||
import org.elasticsearch.index.mapper.MapperService;
|
||||
import org.elasticsearch.index.mapper.core.StringFieldMapper;
|
||||
import org.elasticsearch.index.mapper.geo.GeoPointFieldMapper;
|
||||
|
||||
import org.apache.lucene.queries.BooleanFilter;
|
||||
import org.apache.lucene.search.Filter;
|
||||
import org.apache.lucene.search.BooleanClause.Occur;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A gehash filter filters {@link GeoPoint}s by their geohashes. Basically the a
|
||||
* Geohash prefix is defined by the filter and all geohashes that are matching this
|
||||
* prefix will be returned. The <code>neighbors</code> flag allows to filter
|
||||
* geohashes that surround the given geohash. In general the neighborhood of a
|
||||
* geohash is defined by its eight adjacent cells.<br />
|
||||
* The structure of the {@link GeohashFilter} is defined as:
|
||||
* <pre>
|
||||
* "geohash_bbox" {
|
||||
* "field":"location",
|
||||
* "geohash":"u33d8u5dkx8k",
|
||||
* "neighbors":false
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public class GeohashFilter {
|
||||
|
||||
public static final String NAME = "geohash_cell";
|
||||
public static final String FIELDNAME = "field";
|
||||
public static final String GEOHASH = "geohash";
|
||||
public static final String NEIGHBORS = "neighbors";
|
||||
|
||||
/**
|
||||
* Create a new geohash filter for a given set of geohashes. In general this method
|
||||
* returns a boolean filter combining the geohashes OR-wise.
|
||||
*
|
||||
* @param context Context of the filter
|
||||
* @param fieldMapper field mapper for geopoints
|
||||
* @param geohash mandatory geohash
|
||||
* @param geohashes optional array of additional geohashes
|
||||
*
|
||||
* @return a new GeoBoundinboxfilter
|
||||
*/
|
||||
public static Filter create(QueryParseContext context, GeoPointFieldMapper fieldMapper, String geohash, String...geohashes) {
|
||||
if(fieldMapper.geoHashStringMapper() == null) {
|
||||
throw new ElasticSearchIllegalArgumentException("geohash filter needs geohashes to be enabled");
|
||||
}
|
||||
|
||||
StringFieldMapper stringMapper = fieldMapper.geoHashStringMapper();
|
||||
if(geohashes == null || geohashes.length == 0) {
|
||||
return stringMapper.termFilter(geohash, context);
|
||||
} else {
|
||||
BooleanFilter booleanFilter = new BooleanFilter();
|
||||
booleanFilter.add(stringMapper.termFilter(geohash, context), Occur.SHOULD);
|
||||
|
||||
for (int i = 0; i < geohashes.length; i++) {
|
||||
booleanFilter.add(stringMapper.termFilter(geohashes[i], context), Occur.SHOULD);
|
||||
}
|
||||
return booleanFilter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for a geohashfilter. It needs the fields <code>fieldname</code> and
|
||||
* <code>geohash</code> to be set. the default for a neighbor filteing is
|
||||
* <code>false</code>.
|
||||
*/
|
||||
public static class Builder extends BaseFilterBuilder {
|
||||
|
||||
private String fieldname;
|
||||
private String geohash;
|
||||
private boolean neighbors;
|
||||
|
||||
public Builder(String fieldname) {
|
||||
this(fieldname, null, false);
|
||||
}
|
||||
|
||||
public Builder(String fieldname, String geohash) {
|
||||
this(fieldname, geohash, false);
|
||||
}
|
||||
|
||||
public Builder(String fieldname, String geohash, boolean neighbors) {
|
||||
super();
|
||||
this.fieldname = fieldname;
|
||||
this.geohash = geohash;
|
||||
this.neighbors = neighbors;
|
||||
}
|
||||
|
||||
public Builder setGeohash(String geohash) {
|
||||
this.geohash = geohash;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setNeighbors(boolean neighbors) {
|
||||
this.neighbors = neighbors;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setField(String fieldname) {
|
||||
this.fieldname = fieldname;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
|
||||
builder.startObject(NAME);
|
||||
builder.field(FIELDNAME, fieldname);
|
||||
builder.field(GEOHASH, geohash);
|
||||
if(neighbors) {
|
||||
builder.field(NEIGHBORS, neighbors);
|
||||
}
|
||||
builder.endObject();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Parser implements FilterParser {
|
||||
|
||||
@Inject
|
||||
public Parser() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] names() {
|
||||
return new String[]{NAME};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
|
||||
XContentParser parser = parseContext.parser();
|
||||
|
||||
String fieldName = null;
|
||||
String geohash = null;
|
||||
boolean neighbors = false;
|
||||
|
||||
XContentParser.Token token;
|
||||
if((token = parser.currentToken()) != Token.START_OBJECT) {
|
||||
throw new ElasticSearchParseException(NAME + " must be an object");
|
||||
}
|
||||
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if(token == Token.FIELD_NAME) {
|
||||
String field = parser.text();
|
||||
|
||||
if(FIELDNAME.equals(field)) {
|
||||
parser.nextToken();
|
||||
fieldName = parser.text();
|
||||
} else if (GEOHASH.equals(field)) {
|
||||
parser.nextToken();
|
||||
geohash = parser.text();
|
||||
} else if (NEIGHBORS.equals(field)) {
|
||||
parser.nextToken();
|
||||
neighbors = parser.booleanValue();
|
||||
} else {
|
||||
throw new ElasticSearchParseException("unexpected field ["+field+"]");
|
||||
}
|
||||
} else {
|
||||
throw new ElasticSearchParseException("unexpected token ["+token+"]");
|
||||
}
|
||||
}
|
||||
|
||||
MapperService.SmartNameFieldMappers smartMappers = parseContext.smartFieldMappers(fieldName);
|
||||
if (smartMappers == null || !smartMappers.hasMapper()) {
|
||||
throw new QueryParsingException(parseContext.index(), "failed to find geo_point field [" + fieldName + "]");
|
||||
}
|
||||
|
||||
FieldMapper<?> mapper = smartMappers.mapper();
|
||||
if (!(mapper instanceof GeoPointFieldMapper.GeoStringFieldMapper)) {
|
||||
throw new QueryParsingException(parseContext.index(), "field [" + fieldName + "] is not a geo_point field");
|
||||
}
|
||||
|
||||
GeoPointFieldMapper geoMapper = ((GeoPointFieldMapper.GeoStringFieldMapper) mapper).geoMapper();
|
||||
|
||||
if(neighbors) {
|
||||
return create(parseContext, geoMapper, geohash, GeoHashUtils.neighbors(geohash));
|
||||
} else {
|
||||
return create(parseContext, geoMapper, geohash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -132,6 +132,7 @@ public class IndicesQueriesModule extends AbstractModule {
|
|||
fpBinders.addBinding().to(GeoDistanceFilterParser.class).asEagerSingleton();
|
||||
fpBinders.addBinding().to(GeoDistanceRangeFilterParser.class).asEagerSingleton();
|
||||
fpBinders.addBinding().to(GeoBoundingBoxFilterParser.class).asEagerSingleton();
|
||||
fpBinders.addBinding().to(GeohashFilter.Parser.class).asEagerSingleton();
|
||||
fpBinders.addBinding().to(GeoPolygonFilterParser.class).asEagerSingleton();
|
||||
if (ShapesAvailability.JTS_AVAILABLE) {
|
||||
fpBinders.addBinding().to(GeoShapeFilterParser.class).asEagerSingleton();
|
||||
|
|
|
@ -30,20 +30,23 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
|||
import static org.hamcrest.Matchers.anyOf;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.lessThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
|
||||
import org.elasticsearch.action.bulk.BulkItemResponse;
|
||||
import org.elasticsearch.action.bulk.BulkResponse;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.client.Client;
|
||||
import org.elasticsearch.common.Priority;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.geo.GeoHashUtils;
|
||||
import org.elasticsearch.common.geo.GeoPoint;
|
||||
import org.elasticsearch.common.geo.GeoUtils;
|
||||
import org.elasticsearch.common.geo.ShapeBuilder;
|
||||
|
@ -52,8 +55,8 @@ import org.elasticsearch.common.geo.ShapeBuilder.PolygonBuilder;
|
|||
import org.elasticsearch.common.io.Streams;
|
||||
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||
import org.elasticsearch.index.query.FilterBuilders;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.search.SearchHit;
|
||||
import org.elasticsearch.test.integration.AbstractNodesTests;
|
||||
import org.elasticsearch.test.integration.AbstractSharedClusterTest;
|
||||
|
||||
import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
|
||||
|
@ -62,16 +65,12 @@ import org.apache.lucene.spatial.query.SpatialArgs;
|
|||
import org.apache.lucene.spatial.query.SpatialOperation;
|
||||
import org.apache.lucene.spatial.query.UnsupportedSpatialOperation;
|
||||
|
||||
import org.testng.annotations.AfterClass;
|
||||
import org.testng.annotations.BeforeClass;
|
||||
import org.testng.annotations.BeforeTest;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import com.spatial4j.core.context.SpatialContext;
|
||||
import com.spatial4j.core.distance.DistanceUtils;
|
||||
import com.spatial4j.core.exception.InvalidShapeException;
|
||||
import com.spatial4j.core.shape.Point;
|
||||
import com.spatial4j.core.shape.Rectangle;
|
||||
import com.spatial4j.core.shape.Shape;
|
||||
|
||||
/**
|
||||
|
@ -444,6 +443,85 @@ public class GeoFilterTests extends AbstractSharedClusterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGeoHashFilter() throws IOException {
|
||||
String geohash = randomhash(12);
|
||||
String[] neighbors = GeoHashUtils.neighbors(geohash);
|
||||
|
||||
logger.info("Testing geohash boundingbox filter for [{}]", geohash);
|
||||
logger.info("Neighbors {}", Arrays.toString(neighbors));
|
||||
|
||||
String mapping = XContentFactory.jsonBuilder()
|
||||
.startObject()
|
||||
.startObject("location")
|
||||
.startObject("properties")
|
||||
.startObject("pin")
|
||||
.field("type", "geo_point")
|
||||
.field("geohash", true)
|
||||
.field("geohash_prefix", true)
|
||||
.field("latlon", false)
|
||||
.field("store", true)
|
||||
.endObject()
|
||||
.endObject()
|
||||
.endObject()
|
||||
.endObject()
|
||||
.string();
|
||||
|
||||
ensureYellow();
|
||||
|
||||
client().admin().indices().prepareCreate("locations").addMapping("location", mapping).execute().actionGet();
|
||||
|
||||
// Index a pin
|
||||
client().prepareIndex("locations", "location", "1").setCreate(true).setSource("{\"pin\":\""+geohash+"\"}").execute().actionGet();
|
||||
|
||||
// index neighbors
|
||||
for (int i = 0; i < neighbors.length; i++) {
|
||||
client().prepareIndex("locations", "location", "N"+i).setCreate(true).setSource("{\"pin\":\""+neighbors[i]+"\"}").execute().actionGet();
|
||||
}
|
||||
|
||||
// Index parent cell
|
||||
client().prepareIndex("locations", "location", "p").setCreate(true).setSource("{\"pin\":\""+geohash.substring(0, geohash.length()-1)+"\"}").execute().actionGet();
|
||||
|
||||
// index neighbors
|
||||
String[] parentNeighbors = GeoHashUtils.neighbors(geohash.substring(0, geohash.length()-1));
|
||||
for (int i = 0; i < parentNeighbors.length; i++) {
|
||||
client().prepareIndex("locations", "location", "p"+i).setCreate(true).setSource("{\"pin\":\""+parentNeighbors[i]+"\"}").execute().actionGet();
|
||||
}
|
||||
|
||||
client().admin().indices().prepareRefresh("locations").execute().actionGet();
|
||||
|
||||
// Result of this geohash search should contain the geohash only
|
||||
SearchResponse results1 = client().prepareSearch("locations").setQuery(QueryBuilders.matchAllQuery()).setFilter("{\"geohash_cell\": {\"field\": \"pin\", \"geohash\": \""+geohash+"\", \"neighbors\": false}}").execute().actionGet();
|
||||
assertHitCount(results1, 1);
|
||||
|
||||
SearchResponse results2 = client().prepareSearch("locations").setQuery(QueryBuilders.matchAllQuery()).setFilter("{\"geohash_cell\": {\"field\": \"pin\", \"geohash\": \""+geohash.substring(0, geohash.length()-1)+"\", \"neighbors\": true}}").execute().actionGet();
|
||||
// Result of the parent query should contain the parent it self, its neighbors, the child and all its neighbors
|
||||
assertHitCount(results2, 2 + neighbors.length + parentNeighbors.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNeighbors() {
|
||||
// Simple root case
|
||||
assertThat(Arrays.asList(GeoHashUtils.neighbors("7")), containsInAnyOrder("4", "5", "6", "d", "e", "h", "k", "s"));
|
||||
|
||||
// Root cases (Outer cells)
|
||||
assertThat(Arrays.asList(GeoHashUtils.neighbors("0")), containsInAnyOrder("1", "2", "3", "p", "r"));
|
||||
assertThat(Arrays.asList(GeoHashUtils.neighbors("b")), containsInAnyOrder("8", "9", "c", "x", "z"));
|
||||
assertThat(Arrays.asList(GeoHashUtils.neighbors("p")), containsInAnyOrder("n", "q", "r", "0", "2"));
|
||||
assertThat(Arrays.asList(GeoHashUtils.neighbors("z")), containsInAnyOrder("8", "b", "w", "x", "y"));
|
||||
|
||||
// Root crossing dateline
|
||||
assertThat(Arrays.asList(GeoHashUtils.neighbors("2")), containsInAnyOrder("0", "1", "3", "8", "9", "p", "r", "x"));
|
||||
assertThat(Arrays.asList(GeoHashUtils.neighbors("r")), containsInAnyOrder("0", "2", "8", "n", "p", "q", "w", "x"));
|
||||
|
||||
// level1: simple case
|
||||
assertThat(Arrays.asList(GeoHashUtils.neighbors("dk")), containsInAnyOrder("d5", "d7", "de", "dh", "dj", "dm", "ds", "dt"));
|
||||
|
||||
// Level1: crossing cells
|
||||
assertThat(Arrays.asList(GeoHashUtils.neighbors("d5")), containsInAnyOrder("d4", "d6", "d7", "dh", "dk", "9f", "9g", "9u"));
|
||||
assertThat(Arrays.asList(GeoHashUtils.neighbors("d0")), containsInAnyOrder("d1", "d2", "d3", "9b", "9c", "6p", "6r", "3z"));
|
||||
}
|
||||
|
||||
public static double distance(double lat1, double lon1, double lat2, double lon2) {
|
||||
return GeoUtils.EARTH_SEMI_MAJOR_AXIS * DistanceUtils.distHaversineRAD(
|
||||
DistanceUtils.toRadians(lat1),
|
||||
|
@ -465,5 +543,32 @@ public class GeoFilterTests extends AbstractSharedClusterTest {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected static String randomhash(int length) {
|
||||
return randomhash(new Random(), length);
|
||||
}
|
||||
|
||||
protected static String randomhash(Random random) {
|
||||
return randomhash(random, 2 + random.nextInt(10));
|
||||
}
|
||||
|
||||
protected static String randomhash() {
|
||||
return randomhash(new Random());
|
||||
}
|
||||
|
||||
protected static String randomhash(Random random, int length) {
|
||||
final char[] BASE_32 = {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7',
|
||||
'8', '9', 'b', 'c', 'd', 'e', 'f', 'g',
|
||||
'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r',
|
||||
's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < length; i++) {
|
||||
sb.append(BASE_32[random.nextInt(BASE_32.length)]);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue