The `geohash_cell` filter now adapts the format of other geo-filters. The oject fieldnames match the fieldnames document names automatically. This invalidates the `field` field in previeous versions. The value these fields value is a `geo_point` value (all formats supported) which is internally translated to a geohash. Since those points alway have a maximum precision (level 12) a `precision` definition has been included. This precision can either be defined as *length* of the geohash-string or as *distance*. It's assumed the a distance without any unit is a geohash-length.

```
GET 'http://127.0.0.1:9200/locations/_search?pretty=true' -d '{
    "query": {
        "match_all":{}
    },
    "filter": {
        "geohash_cell": {
			"pin": {
				"lat": 13.4080,
				"lon": 52.5186
			},
            "precision": 3,
            "neighbors": true
        }
    }
}'
```
Closes #3229
This commit is contained in:
Florian Schilling 2013-06-24 18:35:12 +02:00
parent d094042b08
commit 84fa9ead4d
5 changed files with 210 additions and 21 deletions

View File

@ -170,7 +170,12 @@ public class GeoHashUtils {
if (nx >= 0 && nx <= xLimit && ny >= 0 && ny < yLimit) { if (nx >= 0 && nx <= xLimit && ny >= 0 && ny < yLimit) {
return geohash.substring(0, level - 1) + encode(nx, ny); return geohash.substring(0, level - 1) + encode(nx, ny);
} else { } else {
return neighbor(geohash, level - 1, dx, dy) + encode(nx, ny); String neighbor = neighbor(geohash, level - 1, dx, dy);
if(neighbor != null) {
return neighbor + encode(nx, ny);
} else {
return null;
}
} }
} }
} }

View File

@ -19,11 +19,21 @@
package org.elasticsearch.common.geo; package org.elasticsearch.common.geo;
import java.io.IOException;
import org.elasticsearch.ElasticSearchParseException;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentParser.Token;
import org.elasticsearch.index.mapper.geo.GeoPointFieldMapper;
/** /**
* *
*/ */
public class GeoPoint { public class GeoPoint {
public static final String LATITUDE = GeoPointFieldMapper.Names.LAT;
public static final String LONGITUDE = GeoPointFieldMapper.Names.LON;
private double lat; private double lat;
private double lon; private double lon;
@ -123,4 +133,92 @@ public class GeoPoint {
public String toString() { public String toString() {
return "[" + lat + ", " + lon + "]"; return "[" + lat + ", " + lon + "]";
} }
/**
* Parse a {@link GeoPoint} with a {@link XContentParser}:
*
* @param parser {@link XContentParser} to parse the value from
* @return new {@link GeoPoint} parsed from the parse
*
* @throws IOException
* @throws ElasticSearchParseException
*/
public static GeoPoint parse(XContentParser parser) throws IOException, ElasticSearchParseException {
return parse(parser, new GeoPoint());
}
/**
* Parse a {@link GeoPoint} with a {@link XContentParser}. A geopoint has one of the following forms:
*
* <ul>
* <li>Object: <pre>{&quot;lat&quot;: <i>&lt;latitude&gt;</i>, &quot;lon&quot;: <i>&lt;longitude&gt;</i>}</pre></li>
* <li>String: <pre>&quot;<i>&lt;latitude&gt;</i>,<i>&lt;longitude&gt;</i>&quot;</pre></li>
* <li>Geohash: <pre>&quot;<i>&lt;geohash&gt;</i>&quot;</pre></li>
* <li>Array: <pre>[<i>&lt;longitude&gt;</i>,<i>&lt;latitude&gt;</i>]</pre></li>
* </ul>
*
* @param parser {@link XContentParser} to parse the value from
* @param point A {@link GeoPoint} that will be reset by the values parsed
* @return new {@link GeoPoint} parsed from the parse
*
* @throws IOException
* @throws ElasticSearchParseException
*/
public static GeoPoint parse(XContentParser parser, GeoPoint point) throws IOException, ElasticSearchParseException {
if(parser.currentToken() == Token.START_OBJECT) {
while(parser.nextToken() != Token.END_OBJECT) {
if(parser.currentToken() == Token.FIELD_NAME) {
String field = parser.text();
if(LATITUDE.equals(field)) {
if(parser.nextToken() == Token.VALUE_NUMBER) {
point.resetLat(parser.doubleValue());
} else {
throw new ElasticSearchParseException("latitude must be a number");
}
} else if (LONGITUDE.equals(field)) {
if(parser.nextToken() == Token.VALUE_NUMBER) {
point.resetLon(parser.doubleValue());
} else {
throw new ElasticSearchParseException("latitude must be a number");
}
} else {
throw new ElasticSearchParseException("field must be either '"+LATITUDE+"' or '"+LONGITUDE+"'");
}
} else {
throw new ElasticSearchParseException("Token '"+parser.currentToken()+"' not allowed");
}
}
return point;
} else if(parser.currentToken() == Token.START_ARRAY) {
int element = 0;
while(parser.nextToken() != Token.END_ARRAY) {
if(parser.currentToken() == Token.VALUE_NUMBER) {
element++;
if(element == 1) {
point.resetLon(parser.doubleValue());
} else if(element == 2) {
point.resetLat(parser.doubleValue());
} else {
throw new ElasticSearchParseException("only two values allowed");
}
} else {
throw new ElasticSearchParseException("Numeric value expected");
}
}
return point;
} else if(parser.currentToken() == Token.VALUE_STRING) {
String data = parser.text();
int comma = data.indexOf(',');
if(comma > 0) {
double lat = Double.parseDouble(data.substring(0, comma).trim());
double lon = Double.parseDouble(data.substring(comma+1).trim());
return point.reset(lat, lon);
} else {
point.resetFromGeoHash(data);
return point;
}
} else {
throw new ElasticSearchParseException("geo_point expected");
}
}
} }

View File

@ -21,6 +21,7 @@ package org.elasticsearch.index.query;
import com.spatial4j.core.shape.Shape; import com.spatial4j.core.shape.Shape;
import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.ShapeRelation;
/** /**
@ -372,6 +373,18 @@ public abstract class FilterBuilders {
return new GeohashFilter.Builder(fieldname, 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>{&quot;type&quot;:&quot;geo_point&quot;, &quot;geohash&quot;:true}</code>
* to work.
*
* @param fieldname The geopoint field name.
* @param point a geopoint within the geohash bucket
*/
public static GeohashFilter.Builder geoHashFilter(String fieldname, GeoPoint point) {
return new GeohashFilter.Builder(fieldname, point);
}
/** /**
* A filter based on a bounding box defined by geohash. The field this filter is applied to * A filter based on a bounding box defined by geohash. The field this filter is applied to
* must have <code>{&quot;type&quot;:&quot;geo_point&quot;, &quot;geohash&quot;:true}</code> * must have <code>{&quot;type&quot;:&quot;geo_point&quot;, &quot;geohash&quot;:true}</code>

View File

@ -26,7 +26,9 @@ import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings; import org.elasticsearch.common.Strings;
import org.elasticsearch.common.geo.GeoHashUtils; import org.elasticsearch.common.geo.GeoHashUtils;
import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentParser.Token; import org.elasticsearch.common.xcontent.XContentParser.Token;
@ -56,9 +58,8 @@ import java.util.List;
public class GeohashFilter { public class GeohashFilter {
public static final String NAME = "geohash_cell"; 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"; public static final String NEIGHBORS = "neighbors";
public static final String PRECISION = "precision";
/** /**
* Create a new geohash filter for a given set of geohashes. In general this method * Create a new geohash filter for a given set of geohashes. In general this method
@ -90,15 +91,23 @@ public class GeohashFilter {
* <code>false</code>. * <code>false</code>.
*/ */
public static class Builder extends BaseFilterBuilder { public static class Builder extends BaseFilterBuilder {
// we need to store the geohash rather than the corresponding point,
// because a transformation from a geohash to a point an back to the
// geohash will extend the accuracy of the hash to max precision
// i.e. by filing up with z's.
private String fieldname; private String fieldname;
private String geohash; private String geohash;
private int levels = -1;
private boolean neighbors; private boolean neighbors;
public Builder(String fieldname) { public Builder(String fieldname) {
this(fieldname, null, false); this(fieldname, null, false);
} }
public Builder(String fieldname, GeoPoint point) {
this(fieldname, point.geohash(), false);
}
public Builder(String fieldname, String geohash) { public Builder(String fieldname, String geohash) {
this(fieldname, geohash, false); this(fieldname, geohash, false);
} }
@ -110,11 +119,31 @@ public class GeohashFilter {
this.neighbors = neighbors; this.neighbors = neighbors;
} }
public Builder setPoint(GeoPoint point) {
this.geohash = point.getGeohash();
return this;
}
public Builder setPoint(double lat, double lon) {
this.geohash = GeoHashUtils.encode(lat, lon);
return this;
}
public Builder setGeohash(String geohash) { public Builder setGeohash(String geohash) {
this.geohash = geohash; this.geohash = geohash;
return this; return this;
} }
public Builder setPrecision(int levels) {
this.levels = levels;
return this;
}
public Builder setPrecision(String precision) {
double meters = DistanceUnit.parse(precision, DistanceUnit.METERS, DistanceUnit.METERS);
return setPrecision(GeoUtils.geoHashLevelsForPrecision(meters));
}
public Builder setNeighbors(boolean neighbors) { public Builder setNeighbors(boolean neighbors) {
this.neighbors = neighbors; this.neighbors = neighbors;
return this; return this;
@ -128,11 +157,14 @@ public class GeohashFilter {
@Override @Override
protected void doXContent(XContentBuilder builder, Params params) throws IOException { protected void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(NAME); builder.startObject(NAME);
builder.field(FIELDNAME, fieldname);
builder.field(GEOHASH, geohash);
if (neighbors) { if (neighbors) {
builder.field(NEIGHBORS, neighbors); builder.field(NEIGHBORS, neighbors);
} }
if(levels > 0) {
builder.field(PRECISION, levels);
}
builder.field(fieldname, geohash);
builder.endObject(); builder.endObject();
} }
} }
@ -154,6 +186,7 @@ public class GeohashFilter {
String fieldName = null; String fieldName = null;
String geohash = null; String geohash = null;
int levels = -1;
boolean neighbors = false; boolean neighbors = false;
XContentParser.Token token; XContentParser.Token token;
@ -161,21 +194,35 @@ public class GeohashFilter {
throw new ElasticSearchParseException(NAME + " must be an object"); throw new ElasticSearchParseException(NAME + " must be an object");
} }
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { while ((token = parser.nextToken()) != Token.END_OBJECT) {
if (token == Token.FIELD_NAME) { if (token == Token.FIELD_NAME) {
String field = parser.text(); String field = parser.text();
if (FIELDNAME.equals(field)) { if (PRECISION.equals(field)) {
parser.nextToken(); token = parser.nextToken();
fieldName = parser.text(); if(token == Token.VALUE_NUMBER) {
} else if (GEOHASH.equals(field)) { levels = parser.intValue();
parser.nextToken(); } else if(token == Token.VALUE_STRING) {
geohash = parser.text(); double meters = DistanceUnit.parse(parser.text(), DistanceUnit.METERS, DistanceUnit.METERS);
levels = GeoUtils.geoHashLevelsForPrecision(meters);
}
} else if (NEIGHBORS.equals(field)) { } else if (NEIGHBORS.equals(field)) {
parser.nextToken(); parser.nextToken();
neighbors = parser.booleanValue(); neighbors = parser.booleanValue();
} else { } else {
throw new ElasticSearchParseException("unexpected field [" + field + "]"); fieldName = field;
token = parser.nextToken();
if(token == Token.VALUE_STRING) {
// A string indicates either a gehash or a lat/lon string
String location = parser.text();
if(location.indexOf(",")>0) {
geohash = GeoPoint.parse(parser).geohash();
} else {
geohash = location;
}
} else {
geohash = GeoPoint.parse(parser).geohash();
}
} }
} else { } else {
throw new ElasticSearchParseException("unexpected token [" + token + "]"); throw new ElasticSearchParseException("unexpected token [" + token + "]");
@ -194,6 +241,11 @@ public class GeohashFilter {
GeoPointFieldMapper geoMapper = ((GeoPointFieldMapper.GeoStringFieldMapper) mapper).geoMapper(); GeoPointFieldMapper geoMapper = ((GeoPointFieldMapper.GeoStringFieldMapper) mapper).geoMapper();
if(levels > 0) {
int len = Math.min(levels, geohash.length());
geohash = geohash.substring(0, len);
}
if (neighbors) { if (neighbors) {
return create(parseContext, geoMapper, geohash, GeoHashUtils.neighbors(geohash)); return create(parseContext, geoMapper, geohash, GeoHashUtils.neighbors(geohash));
} else { } else {

View File

@ -441,11 +441,14 @@ public class GeoFilterTests extends AbstractSharedClusterTest {
@Test @Test
public void testGeoHashFilter() throws IOException { public void testGeoHashFilter() throws IOException {
String geohash = randomhash(12); String geohash = randomhash(10);
List<String> neighbors = GeoHashUtils.neighbors(geohash);
logger.info("Testing geohash boundingbox filter for [{}]", geohash); logger.info("Testing geohash boundingbox filter for [{}]", geohash);
List<String> neighbors = GeoHashUtils.neighbors(geohash);
List<String> parentNeighbors = GeoHashUtils.neighbors(geohash.substring(0, geohash.length() - 1));
logger.info("Neighbors {}", neighbors); logger.info("Neighbors {}", neighbors);
logger.info("Parent Neighbors {}", parentNeighbors);
String mapping = XContentFactory.jsonBuilder() String mapping = XContentFactory.jsonBuilder()
.startObject() .startObject()
@ -477,7 +480,6 @@ public class GeoFilterTests extends AbstractSharedClusterTest {
client().prepareIndex("locations", "location", "p").setCreate(true).setSource("{\"pin\":\"" + geohash.substring(0, geohash.length() - 1) + "\"}").execute().actionGet(); client().prepareIndex("locations", "location", "p").setCreate(true).setSource("{\"pin\":\"" + geohash.substring(0, geohash.length() - 1) + "\"}").execute().actionGet();
// index neighbors // index neighbors
List<String> parentNeighbors = GeoHashUtils.neighbors(geohash.substring(0, geohash.length() - 1));
for (int i = 0; i < parentNeighbors.size(); i++) { for (int i = 0; i < parentNeighbors.size(); i++) {
client().prepareIndex("locations", "location", "p" + i).setCreate(true).setSource("{\"pin\":\"" + parentNeighbors.get(i) + "\"}").execute().actionGet(); client().prepareIndex("locations", "location", "p" + i).setCreate(true).setSource("{\"pin\":\"" + parentNeighbors.get(i) + "\"}").execute().actionGet();
} }
@ -485,12 +487,31 @@ public class GeoFilterTests extends AbstractSharedClusterTest {
client().admin().indices().prepareRefresh("locations").execute().actionGet(); client().admin().indices().prepareRefresh("locations").execute().actionGet();
// Result of this geohash search should contain the geohash only // 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(); SearchResponse results1 = client().prepareSearch("locations").setQuery(QueryBuilders.matchAllQuery()).setFilter("{\"geohash_cell\": {\"pin\": \"" + geohash + "\", \"neighbors\": false}}").execute().actionGet();
assertHitCount(results1, 1); 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 // Result of the parent query should contain the parent it self, its neighbors, the child and all its neighbors
SearchResponse results2 = client().prepareSearch("locations").setQuery(QueryBuilders.matchAllQuery()).setFilter("{\"geohash_cell\": {\"pin\": \"" + geohash.substring(0, geohash.length() - 1) + "\", \"neighbors\": true}}").execute().actionGet();
assertHitCount(results2, 2 + neighbors.size() + parentNeighbors.size()); assertHitCount(results2, 2 + neighbors.size() + parentNeighbors.size());
// Testing point formats and precision
GeoPoint point = GeoHashUtils.decode(geohash);
int precision = geohash.length();
logger.info("Testing lat/lon format");
String pointTest1 = "{\"geohash_cell\": {\"pin\": {\"lat\": " + point.lat() + ",\"lon\": " + point.lon() + "},\"precision\": " + precision + ",\"neighbors\": true}}";
SearchResponse results3 = client().prepareSearch("locations").setQuery(QueryBuilders.matchAllQuery()).setFilter(pointTest1).execute().actionGet();
assertHitCount(results3, neighbors.size() + 1);
logger.info("Testing String format");
String pointTest2 = "{\"geohash_cell\": {\"pin\": \"" + point.lat() + "," + point.lon() + "\",\"precision\": " + precision + ",\"neighbors\": true}}";
SearchResponse results4 = client().prepareSearch("locations").setQuery(QueryBuilders.matchAllQuery()).setFilter(pointTest2).execute().actionGet();
assertHitCount(results4, neighbors.size() + 1);
logger.info("Testing Array format");
String pointTest3 = "{\"geohash_cell\": {\"pin\": [" + point.lon() + "," + point.lat() + "],\"precision\": " + precision + ",\"neighbors\": true}}";
SearchResponse results5 = client().prepareSearch("locations").setQuery(QueryBuilders.matchAllQuery()).setFilter(pointTest3).execute().actionGet();
assertHitCount(results5, neighbors.size() + 1);
} }
@Test @Test