diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java index f89ca6425c9..1bc5190c298 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java @@ -258,6 +258,7 @@ public class IndexQueryParserModule extends AbstractModule { bindings.processXContentQueryFilter(PrefixFilterParser.NAME, PrefixFilterParser.class); bindings.processXContentQueryFilter(ScriptFilterParser.NAME, ScriptFilterParser.class); bindings.processXContentQueryFilter(GeoDistanceFilterParser.NAME, GeoDistanceFilterParser.class); + bindings.processXContentQueryFilter(GeoDistanceRangeFilterParser.NAME, GeoDistanceRangeFilterParser.class); bindings.processXContentQueryFilter(GeoBoundingBoxFilterParser.NAME, GeoBoundingBoxFilterParser.class); bindings.processXContentQueryFilter(GeoPolygonFilterParser.NAME, GeoPolygonFilterParser.class); bindings.processXContentQueryFilter(QueryFilterParser.NAME, QueryFilterParser.class); diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/FilterBuilders.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/FilterBuilders.java index 58010fcd0dc..61c2ddc0301 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/FilterBuilders.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/FilterBuilders.java @@ -270,6 +270,15 @@ public abstract class FilterBuilders { return new GeoDistanceFilterBuilder(name); } + /** + * A filter to filter based on a specific range from a specific geo location / point. + * + * @param name The location field name. + */ + public static GeoDistanceRangeFilterBuilder geoDistanceRangeFilter(String name) { + return new GeoDistanceRangeFilterBuilder(name); + } + /** * A filter to filter based on a bounding box defined by top left and bottom right locations / points * diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/GeoDistanceRangeFilterBuilder.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/GeoDistanceRangeFilterBuilder.java new file mode 100644 index 00000000000..669815408a7 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/GeoDistanceRangeFilterBuilder.java @@ -0,0 +1,163 @@ +/* + * 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.xcontent.XContentBuilder; +import org.elasticsearch.index.search.geo.GeoDistance; + +import java.io.IOException; + +/** + * @author kimchy (shay.banon) + */ +public class GeoDistanceRangeFilterBuilder extends BaseFilterBuilder { + + private final String name; + + private Object from; + private Object to; + private boolean includeLower = true; + private boolean includeUpper = true; + + private double lat; + + private double lon; + + private String geohash; + + private GeoDistance geoDistance; + + private Boolean cache; + + private String filterName; + + public GeoDistanceRangeFilterBuilder(String name) { + this.name = name; + } + + public GeoDistanceRangeFilterBuilder point(double lat, double lon) { + this.lat = lat; + this.lon = lon; + return this; + } + + public GeoDistanceRangeFilterBuilder lat(double lat) { + this.lat = lat; + return this; + } + + public GeoDistanceRangeFilterBuilder lon(double lon) { + this.lon = lon; + return this; + } + + public GeoDistanceRangeFilterBuilder from(Object from) { + this.from = from; + return this; + } + + public GeoDistanceRangeFilterBuilder to(Object to) { + this.to = to; + return this; + } + + public GeoDistanceRangeFilterBuilder gt(Object from) { + this.from = from; + this.includeLower = false; + return this; + } + + public GeoDistanceRangeFilterBuilder gte(Object from) { + this.from = from; + this.includeLower = true; + return this; + } + + public GeoDistanceRangeFilterBuilder lt(Object to) { + this.to = to; + this.includeUpper = false; + return this; + } + + public GeoDistanceRangeFilterBuilder lte(Object to) { + this.to = to; + this.includeUpper = true; + return this; + } + + public GeoDistanceRangeFilterBuilder includeLower(boolean includeLower) { + this.includeLower = includeLower; + return this; + } + + public GeoDistanceRangeFilterBuilder includeUpper(boolean includeUpper) { + this.includeUpper = includeUpper; + return this; + } + + public GeoDistanceRangeFilterBuilder geohash(String geohash) { + this.geohash = geohash; + return this; + } + + public GeoDistanceRangeFilterBuilder geoDistance(GeoDistance geoDistance) { + this.geoDistance = geoDistance; + return this; + } + + /** + * Sets the filter name for the filter that can be used when searching for matched_filters per hit. + */ + public GeoDistanceRangeFilterBuilder filterName(String filterName) { + this.filterName = filterName; + return this; + } + + /** + * Should the filter be cached or not. Defaults to false. + */ + public GeoDistanceRangeFilterBuilder cache(boolean cache) { + this.cache = cache; + return this; + } + + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(GeoDistanceRangeFilterParser.NAME); + if (geohash != null) { + builder.field(name, geohash); + } else { + builder.startArray(name).value(lon).value(lat).endArray(); + } + builder.field("from", from); + builder.field("to", to); + builder.field("include_lower", includeLower); + builder.field("include_upper", includeUpper); + if (geoDistance != null) { + builder.field("distance_type", geoDistance.name().toLowerCase()); + } + if (filterName != null) { + builder.field("_name", filterName); + } + if (cache != null) { + builder.field("_cache", cache); + } + builder.endObject(); + } +} diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/GeoDistanceRangeFilterParser.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/GeoDistanceRangeFilterParser.java new file mode 100644 index 00000000000..70c4da2f3a4 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/GeoDistanceRangeFilterParser.java @@ -0,0 +1,236 @@ +/* + * 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.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.DistanceUnit; +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.geo.GeoPointFieldDataType; +import org.elasticsearch.index.mapper.xcontent.geo.GeoPointFieldMapper; +import org.elasticsearch.index.query.QueryParsingException; +import org.elasticsearch.index.search.geo.GeoDistance; +import org.elasticsearch.index.search.geo.GeoDistanceRangeFilter; +import org.elasticsearch.index.search.geo.GeoHashUtils; +import org.elasticsearch.index.settings.IndexSettings; + +import java.io.IOException; + +import static org.elasticsearch.index.query.support.QueryParsers.*; + +/** + *
+ * { + * "name.lat" : 1.1, + * "name.lon" : 1.2, + * } + *+ * + * @author kimchy (shay.banon) + */ +public class GeoDistanceRangeFilterParser extends AbstractIndexComponent implements XContentFilterParser { + + public static final String NAME = "geo_distance_range"; + + @Inject public GeoDistanceRangeFilterParser(Index index, @IndexSettings Settings indexSettings) { + super(index, indexSettings); + } + + @Override public String[] names() { + return new String[]{NAME, "geoDistanceRange"}; + } + + @Override public Filter parse(QueryParseContext parseContext) throws IOException, QueryParsingException { + XContentParser parser = parseContext.parser(); + + XContentParser.Token token; + + boolean cache = false; + String filterName = null; + String currentFieldName = null; + double lat = 0; + double lon = 0; + String fieldName = null; + Object vFrom = null; + Object vTo = null; + boolean includeLower = true; + boolean includeUpper = true; + DistanceUnit unit = DistanceUnit.KILOMETERS; // default unit + GeoDistance geoDistance = GeoDistance.ARC; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_ARRAY) { + token = parser.nextToken(); + lon = parser.doubleValue(); + token = parser.nextToken(); + lat = parser.doubleValue(); + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + + } + fieldName = currentFieldName; + } else if (token == XContentParser.Token.START_OBJECT) { + // the json in the format of -> field : { lat : 30, lon : 12 } + String currentName = parser.currentName(); + fieldName = currentFieldName; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentName = parser.currentName(); + } else if (token.isValue()) { + if (currentName.equals(GeoPointFieldMapper.Names.LAT)) { + lat = parser.doubleValue(); + } else if (currentName.equals(GeoPointFieldMapper.Names.LON)) { + lon = parser.doubleValue(); + } else if (currentName.equals(GeoPointFieldMapper.Names.GEOHASH)) { + double[] values = GeoHashUtils.decode(parser.text()); + lat = values[0]; + lon = values[1]; + } + } + } + } else if (token.isValue()) { + if (currentFieldName.equals("from")) { + if (token == XContentParser.Token.VALUE_NULL) { + } else if (token == XContentParser.Token.VALUE_STRING) { + vFrom = parser.text(); // a String + } else { + vFrom = parser.numberValue(); // a Number + } + } else if (currentFieldName.equals("to")) { + if (token == XContentParser.Token.VALUE_NULL) { + } + if (token == XContentParser.Token.VALUE_STRING) { + vTo = parser.text(); // a String + } else { + vTo = parser.numberValue(); // a Number + } + } else if ("include_lower".equals(currentFieldName) || "includeLower".equals(currentFieldName)) { + includeLower = parser.booleanValue(); + } else if ("include_upper".equals(currentFieldName) || "includeUpper".equals(currentFieldName)) { + includeUpper = parser.booleanValue(); + } else if ("gt".equals(currentFieldName)) { + if (token == XContentParser.Token.VALUE_NULL) { + } else if (token == XContentParser.Token.VALUE_STRING) { + vFrom = parser.text(); // a String + } else { + vFrom = parser.numberValue(); // a Number + } + includeLower = false; + } else if ("gte".equals(currentFieldName) || "ge".equals(currentFieldName)) { + if (token == XContentParser.Token.VALUE_NULL) { + } else if (token == XContentParser.Token.VALUE_STRING) { + vFrom = parser.text(); // a String + } else { + vFrom = parser.numberValue(); // a Number + } + includeLower = true; + } else if ("lt".equals(currentFieldName)) { + if (token == XContentParser.Token.VALUE_NULL) { + } + if (token == XContentParser.Token.VALUE_STRING) { + vTo = parser.text(); // a String + } else { + vTo = parser.numberValue(); // a Number + } + includeUpper = false; + } else if ("lte".equals(currentFieldName) || "le".equals(currentFieldName)) { + if (token == XContentParser.Token.VALUE_NULL) { + } + if (token == XContentParser.Token.VALUE_STRING) { + vTo = parser.text(); // a String + } else { + vTo = parser.numberValue(); // a Number + } + includeUpper = true; + } else if (currentFieldName.equals("unit")) { + unit = DistanceUnit.fromString(parser.text()); + } else if (currentFieldName.equals("distance_type") || currentFieldName.equals("distanceType")) { + geoDistance = GeoDistance.fromString(parser.text()); + } else if (currentFieldName.endsWith(GeoPointFieldMapper.Names.LAT_SUFFIX)) { + lat = parser.doubleValue(); + fieldName = currentFieldName.substring(0, currentFieldName.length() - GeoPointFieldMapper.Names.LAT_SUFFIX.length()); + } else if (currentFieldName.endsWith(GeoPointFieldMapper.Names.LON_SUFFIX)) { + lon = parser.doubleValue(); + fieldName = currentFieldName.substring(0, currentFieldName.length() - GeoPointFieldMapper.Names.LON_SUFFIX.length()); + } else if (currentFieldName.endsWith(GeoPointFieldMapper.Names.GEOHASH_SUFFIX)) { + double[] values = GeoHashUtils.decode(parser.text()); + lat = values[0]; + lon = values[1]; + fieldName = currentFieldName.substring(0, currentFieldName.length() - GeoPointFieldMapper.Names.GEOHASH_SUFFIX.length()); + } else if ("_name".equals(currentFieldName)) { + filterName = parser.text(); + } else if ("_cache".equals(currentFieldName)) { + cache = parser.booleanValue(); + } else { + // assume the value is the actual value + String value = parser.text(); + int comma = value.indexOf(','); + if (comma != -1) { + lat = Double.parseDouble(value.substring(0, comma).trim()); + lon = Double.parseDouble(value.substring(comma + 1).trim()); + } else { + double[] values = GeoHashUtils.decode(value); + lat = values[0]; + lon = values[1]; + } + fieldName = currentFieldName; + } + } + } + + double from; + double to; + if (vFrom instanceof Number) { + from = unit.toMiles(((Number) vFrom).doubleValue()); + } else { + from = DistanceUnit.parse((String) vFrom, unit, DistanceUnit.MILES); + } + if (vTo instanceof Number) { + to = unit.toMiles(((Number) vTo).doubleValue()); + } else { + to = DistanceUnit.parse((String) vTo, unit, DistanceUnit.MILES); + } + + MapperService mapperService = parseContext.mapperService(); + FieldMapper mapper = mapperService.smartNameFieldMapper(fieldName); + if (mapper == null) { + throw new QueryParsingException(index, "failed to find geo_point field [" + fieldName + "]"); + } + if (mapper.fieldDataType() != GeoPointFieldDataType.TYPE) { + throw new QueryParsingException(index, "field [" + fieldName + "] is not a geo_point field"); + } + fieldName = mapper.names().indexName(); + + Filter filter = new GeoDistanceRangeFilter(lat, lon, from, to, includeLower, includeUpper, geoDistance, fieldName, parseContext.indexCache().fieldData()); + if (cache) { + filter = parseContext.cacheFilter(filter); + } + filter = wrapSmartNameFilter(filter, parseContext.smartFieldMappers(fieldName), parseContext); + if (filterName != null) { + parseContext.addNamedFilter(filterName, filter); + } + return filter; + } +} diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/search/geo/GeoDistanceRangeFilter.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/search/geo/GeoDistanceRangeFilter.java new file mode 100644 index 00000000000..9dad488c8d7 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/search/geo/GeoDistanceRangeFilter.java @@ -0,0 +1,161 @@ +/* + * 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.search.geo; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.DocIdSet; +import org.apache.lucene.search.Filter; +import org.apache.lucene.util.NumericUtils; +import org.elasticsearch.common.lucene.docset.GetDocSet; +import org.elasticsearch.common.unit.DistanceUnit; +import org.elasticsearch.index.cache.field.data.FieldDataCache; +import org.elasticsearch.index.mapper.xcontent.geo.GeoPointFieldData; +import org.elasticsearch.index.mapper.xcontent.geo.GeoPointFieldDataType; + +import java.io.IOException; + +/** + * @author kimchy (shay.banon) + */ +public class GeoDistanceRangeFilter extends Filter { + + private final double lat; + + private final double lon; + + private final double inclusiveLowerPoint; // in miles + private final double inclusiveUpperPoint; // in miles + + private final GeoDistance geoDistance; + + private final String fieldName; + + private final FieldDataCache fieldDataCache; + + public GeoDistanceRangeFilter(double lat, double lon, Double lowerVal, Double upperVal, boolean includeLower, boolean includeUpper, GeoDistance geoDistance, String fieldName, FieldDataCache fieldDataCache) { + this.lat = lat; + this.lon = lon; + this.geoDistance = geoDistance; + this.fieldName = fieldName; + this.fieldDataCache = fieldDataCache; + + if (lowerVal != null) { + double f = lowerVal.doubleValue(); + long i = NumericUtils.doubleToSortableLong(f); + inclusiveLowerPoint = NumericUtils.sortableLongToDouble(includeLower ? i : (i + 1L)); + } else { + inclusiveLowerPoint = Double.NEGATIVE_INFINITY; + } + if (upperVal != null) { + double f = upperVal.doubleValue(); + long i = NumericUtils.doubleToSortableLong(f); + inclusiveUpperPoint = NumericUtils.sortableLongToDouble(includeUpper ? i : (i - 1L)); + } else { + inclusiveUpperPoint = Double.POSITIVE_INFINITY; + } + } + + public double lat() { + return lat; + } + + public double lon() { + return lon; + } + + public GeoDistance geoDistance() { + return geoDistance; + } + + public String fieldName() { + return fieldName; + } + + @Override public DocIdSet getDocIdSet(IndexReader reader) throws IOException { + final GeoPointFieldData fieldData = (GeoPointFieldData) fieldDataCache.cache(GeoPointFieldDataType.TYPE, reader, fieldName); + return new GetDocSet(reader.maxDoc()) { + + @Override public boolean isCacheable() { + // not cacheable for several reasons: + // 1. It is only relevant when _cache is set to true, and then, we really want to create in mem bitset + // 2. Its already fast without in mem bitset, since it works with field data + return false; + } + + @Override public boolean get(int doc) throws IOException { + if (!fieldData.hasValue(doc)) { + return false; + } + + if (fieldData.multiValued()) { + double[] lats = fieldData.latValues(doc); + double[] lons = fieldData.lonValues(doc); + for (int i = 0; i < lats.length; i++) { + double d = geoDistance.calculate(lat, lon, lats[i], lons[i], DistanceUnit.MILES); + if (d >= inclusiveLowerPoint && d <= inclusiveUpperPoint) { + return true; + } + } + return false; + } else { + double d = geoDistance.calculate(lat, lon, fieldData.latValue(doc), fieldData.lonValue(doc), DistanceUnit.MILES); + if (d >= inclusiveLowerPoint && d <= inclusiveUpperPoint) { + return true; + } + return false; + } + } + }; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GeoDistanceRangeFilter filter = (GeoDistanceRangeFilter) o; + + if (Double.compare(filter.inclusiveLowerPoint, inclusiveLowerPoint) != 0) return false; + if (Double.compare(filter.inclusiveUpperPoint, inclusiveUpperPoint) != 0) return false; + if (Double.compare(filter.lat, lat) != 0) return false; + if (Double.compare(filter.lon, lon) != 0) return false; + if (fieldName != null ? !fieldName.equals(filter.fieldName) : filter.fieldName != null) return false; + if (geoDistance != filter.geoDistance) return false; + + return true; + } + + @Override + public int hashCode() { + int result; + long temp; + temp = lat != +0.0d ? Double.doubleToLongBits(lat) : 0L; + result = (int) (temp ^ (temp >>> 32)); + temp = lon != +0.0d ? Double.doubleToLongBits(lon) : 0L; + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = inclusiveLowerPoint != +0.0d ? Double.doubleToLongBits(inclusiveLowerPoint) : 0L; + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = inclusiveUpperPoint != +0.0d ? Double.doubleToLongBits(inclusiveUpperPoint) : 0L; + result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + (geoDistance != null ? geoDistance.hashCode() : 0); + result = 31 * result + (fieldName != null ? fieldName.hashCode() : 0); + return result; + } +} diff --git a/modules/test/integration/src/test/java/org/elasticsearch/test/integration/search/geo/GeoDistanceTests.java b/modules/test/integration/src/test/java/org/elasticsearch/test/integration/search/geo/GeoDistanceTests.java index 229429d5638..046ca5dd806 100644 --- a/modules/test/integration/src/test/java/org/elasticsearch/test/integration/search/geo/GeoDistanceTests.java +++ b/modules/test/integration/src/test/java/org/elasticsearch/test/integration/search/geo/GeoDistanceTests.java @@ -140,6 +140,15 @@ public class GeoDistanceTests extends AbstractNodesTests { assertThat(hit.id(), anyOf(equalTo("1"), equalTo("3"), equalTo("4"), equalTo("5"))); } + searchResponse = client.prepareSearch() // from NY + .setQuery(filteredQuery(matchAllQuery(), geoDistanceRangeFilter("location").from("1.0km").to("2.0km").point(40.7143528, -74.0059731))) + .execute().actionGet(); + assertThat(searchResponse.hits().getTotalHits(), equalTo(2l)); + assertThat(searchResponse.hits().hits().length, equalTo(2)); + for (SearchHit hit : searchResponse.hits()) { + assertThat(hit.id(), anyOf(equalTo("4"), equalTo("5"))); + } + // SORTING searchResponse = client.prepareSearch().setQuery(matchAllQuery())