Geo: Polygon based filter, closes #294.

This commit is contained in:
kimchy 2010-08-03 21:52:13 +03:00
parent 39f344c11e
commit 959eb0e703
10 changed files with 526 additions and 0 deletions

View File

@ -0,0 +1,125 @@
/*
* Licensed to Elastic Search and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Elastic Search 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.common.lucene.geo;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.DocIdSet;
import org.apache.lucene.search.Filter;
import org.elasticsearch.common.lucene.docset.GetDocSet;
import org.elasticsearch.index.cache.field.data.FieldDataCache;
import org.elasticsearch.index.field.data.FieldData;
import org.elasticsearch.index.field.data.NumericFieldData;
import java.io.IOException;
/**
* @author kimchy (shay.banon)
*/
public class GeoPolygonFilter extends Filter {
private final Point[] points;
private final String latFieldName;
private final String lonFieldName;
private final FieldData.Type fieldDataType;
private final FieldDataCache fieldDataCache;
public GeoPolygonFilter(Point[] points, String latFieldName, String lonFieldName, FieldData.Type fieldDataType, FieldDataCache fieldDataCache) {
this.points = points;
this.latFieldName = latFieldName;
this.lonFieldName = lonFieldName;
this.fieldDataType = fieldDataType;
this.fieldDataCache = fieldDataCache;
}
public Point[] points() {
return points;
}
public String latFieldName() {
return latFieldName;
}
public String lonFieldName() {
return lonFieldName;
}
@Override public DocIdSet getDocIdSet(IndexReader reader) throws IOException {
final NumericFieldData latFieldData = (NumericFieldData) fieldDataCache.cache(fieldDataType, reader, latFieldName);
final NumericFieldData lonFieldData = (NumericFieldData) fieldDataCache.cache(fieldDataType, reader, lonFieldName);
return new GetDocSet(reader.maxDoc()) {
@Override public boolean get(int doc) throws IOException {
if (!latFieldData.hasValue(doc) || !lonFieldData.hasValue(doc)) {
return false;
}
if (latFieldData.multiValued()) {
double[] lats = latFieldData.doubleValues(doc);
double[] lons = latFieldData.doubleValues(doc);
for (int i = 0; i < lats.length; i++) {
if (pointInPolygon(points, lats[i], lons[i])) {
return true;
}
}
} else {
double lat = latFieldData.doubleValue(doc);
double lon = lonFieldData.doubleValue(doc);
return pointInPolygon(points, lat, lon);
}
return false;
}
};
}
private static boolean pointInPolygon(Point[] points, double lat, double lon) {
int i;
int j = points.length - 1;
boolean inPoly = false;
for (i = 0; i < points.length; i++) {
if (points[i].lon < lon && points[j].lon >= lon
|| points[j].lon < lon && points[i].lon >= lon) {
if (points[i].lat + (lon - points[i].lon) /
(points[j].lon - points[i].lon) * (points[j].lat - points[i].lat) < lat) {
inPoly = !inPoly;
}
}
j = i;
}
return inPoly;
}
public static class Point {
public double lat;
public double lon;
public Point() {
}
public Point(double lat, double lon) {
this.lat = lat;
this.lon = lon;
}
}
}

View File

@ -175,18 +175,42 @@ public abstract class FilterBuilders {
return new QueryFilterBuilder(queryBuilder);
}
/**
* A builder for filter based on a script.
*
* @param script The script to filter by.
*/
public static ScriptFilterBuilder scriptFilter(String script) {
return new ScriptFilterBuilder(script);
}
/**
* A filter to filter based on a specific distance from a specific geo location / point.
*
* @param name The location field name.
*/
public static GeoDistanceFilterBuilder geoDistanceFilter(String name) {
return new GeoDistanceFilterBuilder(name);
}
/**
* A filter to filter based on a bounding box defined by top left and bottom right locations / points
*
* @param name The location field name.
*/
public static GeoBoundingBoxFilterBuilder geoBoundingBoxFilter(String name) {
return new GeoBoundingBoxFilterBuilder(name);
}
/**
* A filter to filter based on a polygon defined by a set of locations / points.
*
* @param name The location field name.
*/
public static GeoPolygonFilterBuilder geoPolygonFilter(String name) {
return new GeoPolygonFilterBuilder(name);
}
public static BoolFilterBuilder boolFilter() {
return new BoolFilterBuilder();
}

View File

@ -0,0 +1,66 @@
/*
* Licensed to Elastic Search and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Elastic Search 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.xcontent;
import org.elasticsearch.common.collect.Lists;
import org.elasticsearch.common.lucene.geo.GeoHashUtils;
import org.elasticsearch.common.lucene.geo.GeoPolygonFilter;
import org.elasticsearch.common.xcontent.builder.XContentBuilder;
import java.io.IOException;
import java.util.List;
/**
* @author kimchy (shay.banon)
*/
public class GeoPolygonFilterBuilder extends BaseFilterBuilder {
private final String name;
private final List<GeoPolygonFilter.Point> points = Lists.newArrayList();
public GeoPolygonFilterBuilder(String name) {
this.name = name;
}
public GeoPolygonFilterBuilder addPoint(double lat, double lon) {
points.add(new GeoPolygonFilter.Point(lat, lon));
return this;
}
public GeoPolygonFilterBuilder addPoint(String geohash) {
double[] values = GeoHashUtils.decode(geohash);
return addPoint(values[0], values[1]);
}
@Override protected void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(GeoPolygonFilterParser.NAME);
builder.startObject(name);
builder.startArray("points");
for (GeoPolygonFilter.Point point : points) {
builder.startArray().value(point.lat).value(point.lon).endArray();
}
builder.endArray();
builder.endObject();
builder.endObject();
}
}

View File

@ -0,0 +1,156 @@
/*
* Licensed to Elastic Search and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Elastic Search 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.xcontent;
import org.apache.lucene.search.Filter;
import org.elasticsearch.common.collect.Lists;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.lucene.geo.GeoHashUtils;
import org.elasticsearch.common.lucene.geo.GeoPolygonFilter;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.AbstractIndexComponent;
import org.elasticsearch.index.Index;
import org.elasticsearch.index.mapper.FieldMapper;
import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.index.mapper.xcontent.XContentGeoPointFieldMapper;
import org.elasticsearch.index.query.QueryParsingException;
import org.elasticsearch.index.settings.IndexSettings;
import java.io.IOException;
import java.util.List;
/**
* <pre>
* {
* "pin.location" : {
* "points" : [
* { "lat" : 12, "lon" : 40},
* {}
* ]
* }
* }
* </pre>
*
* @author kimchy (shay.banon)
*/
public class GeoPolygonFilterParser extends AbstractIndexComponent implements XContentFilterParser {
public static final String NAME = "geo_polygon";
@Inject public GeoPolygonFilterParser(Index index, @IndexSettings Settings indexSettings) {
super(index, indexSettings);
}
@Override public String[] names() {
return new String[]{NAME, "geoPolygon"};
}
@Override public Filter parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
XContentParser parser = parseContext.parser();
XContentParser.Token token = parser.nextToken();
assert token == XContentParser.Token.FIELD_NAME;
String latFieldName = parser.currentName() + XContentGeoPointFieldMapper.Names.LAT_SUFFIX;
String lonFieldName = parser.currentName() + XContentGeoPointFieldMapper.Names.LON_SUFFIX;
// now, we move after the field name, which starts the object
token = parser.nextToken();
assert token == XContentParser.Token.START_OBJECT;
List<GeoPolygonFilter.Point> points = Lists.newArrayList();
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token == XContentParser.Token.START_ARRAY) {
if ("points".equals(currentFieldName)) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token == XContentParser.Token.START_ARRAY) {
GeoPolygonFilter.Point point = new GeoPolygonFilter.Point();
token = parser.nextToken();
point.lat = parser.doubleValue();
token = parser.nextToken();
point.lon = parser.doubleValue();
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
}
points.add(point);
} else if (token == XContentParser.Token.START_OBJECT) {
GeoPolygonFilter.Point point = new GeoPolygonFilter.Point();
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token.isValue()) {
if (currentFieldName.equals(XContentGeoPointFieldMapper.Names.LAT)) {
point.lat = parser.doubleValue();
} else if (currentFieldName.equals(XContentGeoPointFieldMapper.Names.LON)) {
point.lon = parser.doubleValue();
} else if (currentFieldName.equals(XContentGeoPointFieldMapper.Names.GEOHASH)) {
double[] values = GeoHashUtils.decode(parser.text());
point.lat = values[0];
point.lon = values[1];
}
}
}
points.add(point);
} else if (token.isValue()) {
GeoPolygonFilter.Point point = new GeoPolygonFilter.Point();
String value = parser.text();
int comma = value.indexOf(',');
if (comma != -1) {
point.lat = Double.parseDouble(value.substring(0, comma).trim());
point.lon = Double.parseDouble(value.substring(comma + 1).trim());
} else {
double[] values = GeoHashUtils.decode(value);
point.lat = values[0];
point.lon = values[1];
}
points.add(point);
}
}
}
}
}
if (points.isEmpty()) {
throw new QueryParsingException(index, "no points defined for geo_polygon filter");
}
MapperService mapperService = parseContext.mapperService();
FieldMapper mapper = mapperService.smartNameFieldMapper(latFieldName);
if (mapper == null) {
throw new QueryParsingException(index, "failed to find lat field [" + latFieldName + "]");
}
latFieldName = mapper.names().indexName();
mapper = mapperService.smartNameFieldMapper(lonFieldName);
if (mapper == null) {
throw new QueryParsingException(index, "failed to find lon field [" + lonFieldName + "]");
}
lonFieldName = mapper.names().indexName();
return new GeoPolygonFilter(points.toArray(new GeoPolygonFilter.Point[points.size()]), latFieldName, lonFieldName, mapper.fieldDataType(), parseContext.indexCache().fieldData());
}
}

View File

@ -88,6 +88,7 @@ public class XContentQueryParserRegistry {
add(filterParsersMap, new ScriptFilterParser(index, indexSettings));
add(filterParsersMap, new GeoDistanceFilterParser(index, indexSettings));
add(filterParsersMap, new GeoBoundingBoxFilterParser(index, indexSettings));
add(filterParsersMap, new GeoPolygonFilterParser(index, indexSettings));
add(filterParsersMap, new QueryFilterParser(index, indexSettings));
add(filterParsersMap, new BoolFilterParser(index, indexSettings));
add(filterParsersMap, new AndFilterParser(index, indexSettings));

View File

@ -25,6 +25,7 @@ import org.apache.lucene.search.spans.*;
import org.apache.lucene.util.NumericUtils;
import org.elasticsearch.common.lucene.geo.GeoBoundingBoxFilter;
import org.elasticsearch.common.lucene.geo.GeoDistanceFilter;
import org.elasticsearch.common.lucene.geo.GeoPolygonFilter;
import org.elasticsearch.common.lucene.search.*;
import org.elasticsearch.common.lucene.search.function.BoostScoreFunction;
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
@ -1074,6 +1075,78 @@ public class SimpleIndexQueryParserTests {
assertThat(filter.bottomRight().lon, closeTo(-80, 0.00001));
}
@Test public void testGeoPolygonFilter1() throws IOException {
IndexQueryParser queryParser = newQueryParser();
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/xcontent/geo_polygon1.json");
Query parsedQuery = queryParser.parse(query);
assertThat(parsedQuery, instanceOf(FilteredQuery.class));
FilteredQuery filteredQuery = (FilteredQuery) parsedQuery;
GeoPolygonFilter filter = (GeoPolygonFilter) filteredQuery.getFilter();
assertThat(filter.latFieldName(), equalTo("location.lat"));
assertThat(filter.lonFieldName(), equalTo("location.lon"));
assertThat(filter.points().length, equalTo(3));
assertThat(filter.points()[0].lat, closeTo(40, 0.00001));
assertThat(filter.points()[0].lon, closeTo(-70, 0.00001));
assertThat(filter.points()[1].lat, closeTo(30, 0.00001));
assertThat(filter.points()[1].lon, closeTo(-80, 0.00001));
assertThat(filter.points()[2].lat, closeTo(20, 0.00001));
assertThat(filter.points()[2].lon, closeTo(-90, 0.00001));
}
@Test public void testGeoPolygonFilter2() throws IOException {
IndexQueryParser queryParser = newQueryParser();
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/xcontent/geo_polygon2.json");
Query parsedQuery = queryParser.parse(query);
assertThat(parsedQuery, instanceOf(FilteredQuery.class));
FilteredQuery filteredQuery = (FilteredQuery) parsedQuery;
GeoPolygonFilter filter = (GeoPolygonFilter) filteredQuery.getFilter();
assertThat(filter.latFieldName(), equalTo("location.lat"));
assertThat(filter.lonFieldName(), equalTo("location.lon"));
assertThat(filter.points().length, equalTo(3));
assertThat(filter.points()[0].lat, closeTo(40, 0.00001));
assertThat(filter.points()[0].lon, closeTo(-70, 0.00001));
assertThat(filter.points()[1].lat, closeTo(30, 0.00001));
assertThat(filter.points()[1].lon, closeTo(-80, 0.00001));
assertThat(filter.points()[2].lat, closeTo(20, 0.00001));
assertThat(filter.points()[2].lon, closeTo(-90, 0.00001));
}
@Test public void testGeoPolygonFilter3() throws IOException {
IndexQueryParser queryParser = newQueryParser();
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/xcontent/geo_polygon3.json");
Query parsedQuery = queryParser.parse(query);
assertThat(parsedQuery, instanceOf(FilteredQuery.class));
FilteredQuery filteredQuery = (FilteredQuery) parsedQuery;
GeoPolygonFilter filter = (GeoPolygonFilter) filteredQuery.getFilter();
assertThat(filter.latFieldName(), equalTo("location.lat"));
assertThat(filter.lonFieldName(), equalTo("location.lon"));
assertThat(filter.points().length, equalTo(3));
assertThat(filter.points()[0].lat, closeTo(40, 0.00001));
assertThat(filter.points()[0].lon, closeTo(-70, 0.00001));
assertThat(filter.points()[1].lat, closeTo(30, 0.00001));
assertThat(filter.points()[1].lon, closeTo(-80, 0.00001));
assertThat(filter.points()[2].lat, closeTo(20, 0.00001));
assertThat(filter.points()[2].lon, closeTo(-90, 0.00001));
}
@Test public void testGeoPolygonFilter4() throws IOException {
IndexQueryParser queryParser = newQueryParser();
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/xcontent/geo_polygon4.json");
Query parsedQuery = queryParser.parse(query);
assertThat(parsedQuery, instanceOf(FilteredQuery.class));
FilteredQuery filteredQuery = (FilteredQuery) parsedQuery;
GeoPolygonFilter filter = (GeoPolygonFilter) filteredQuery.getFilter();
assertThat(filter.latFieldName(), equalTo("location.lat"));
assertThat(filter.lonFieldName(), equalTo("location.lon"));
assertThat(filter.points().length, equalTo(3));
assertThat(filter.points()[0].lat, closeTo(40, 0.00001));
assertThat(filter.points()[0].lon, closeTo(-70, 0.00001));
assertThat(filter.points()[1].lat, closeTo(30, 0.00001));
assertThat(filter.points()[1].lon, closeTo(-80, 0.00001));
assertThat(filter.points()[2].lat, closeTo(20, 0.00001));
assertThat(filter.points()[2].lon, closeTo(-90, 0.00001));
}
private XContentIndexQueryParser newQueryParser() throws IOException {
return new XContentIndexQueryParser(new Index("test"), EMPTY_SETTINGS, new ScriptService(EMPTY_SETTINGS),
newMapperService(), new IndexCache(index), new RobinIndexEngine(index), new AnalysisService(index), null, null, null, "test", null);

View File

@ -0,0 +1,18 @@
{
"filtered" : {
"query" : {
"match_all" : {}
},
"filter" : {
"geo_polygon" : {
"person.location" : {
"points" : [
[40, -70],
[30, -80],
[20, -90]
]
}
}
}
}
}

View File

@ -0,0 +1,27 @@
{
"filtered" : {
"query" : {
"match_all" : {}
},
"filter" : {
"geo_polygon" : {
"person.location" : {
"points" : [
{
"lat" : 40,
"lon" : -70
},
{
"lat" : 30,
"lon" : -80
},
{
"lat" : 20,
"lon" : -90
}
]
}
}
}
}
}

View File

@ -0,0 +1,18 @@
{
"filtered" : {
"query" : {
"match_all" : {}
},
"filter" : {
"geo_polygon" : {
"person.location" : {
"points" : [
"40, -70",
"30, -80",
"20, -90"
]
}
}
}
}
}

View File

@ -0,0 +1,18 @@
{
"filtered" : {
"query" : {
"match_all" : {}
},
"filter" : {
"geo_polygon" : {
"person.location" : {
"points" : [
"drn5x1g8cu2y",
"30, -80",
"20, -90"
]
}
}
}
}
}