diff --git a/CHANGES.txt b/CHANGES.txt index f72a0bfefee..58308a6c6bc 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -35,7 +35,7 @@ New Features ---------------------- 1. SOLR-1302: Added several new distance based functions, including Great Circle (haversine), Manhattan and Euclidean. - Also added deg() and rad() convenience functions. (gsingers) + Also added geohash(), deg() and rad() convenience functions. See http://wiki.apache.org/solr/FunctionQuery. (gsingers) Optimizations ---------------------- diff --git a/src/common/org/apache/solr/common/params/SolrParams.java b/src/common/org/apache/solr/common/params/SolrParams.java index 74f6684312e..55990ef6296 100644 --- a/src/common/org/apache/solr/common/params/SolrParams.java +++ b/src/common/org/apache/solr/common/params/SolrParams.java @@ -180,6 +180,29 @@ public abstract class SolrParams implements Serializable { } } + /** Returns the Float value of the param, or null if not set */ + public Double getDouble(String param) { + String val = get(param); + try { + return val==null ? null : Double.valueOf(val); + } + catch( Exception ex ) { + throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, ex.getMessage(), ex ); + } + } + + /** Returns the float value of the param, or def if not set */ + public double getDouble(String param, double def) { + String val = get(param); + try { + return val==null ? def : Double.parseDouble(val); + } + catch( Exception ex ) { + throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, ex.getMessage(), ex ); + } + } + + /** Returns the float value of the field param. */ public Float getFieldFloat(String field, String param) { String val = getFieldParam(field, param); @@ -202,6 +225,30 @@ public abstract class SolrParams implements Serializable { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, ex.getMessage(), ex ); } } + + /** Returns the float value of the field param. */ + public Double getFieldDouble(String field, String param) { + String val = getFieldParam(field, param); + try { + return val==null ? null : Double.valueOf(val); + } + catch( Exception ex ) { + throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, ex.getMessage(), ex ); + } + } + + /** Returns the float value of the field param, + or the value for param, or def if neither is set. */ + public double getFieldDouble(String field, String param, double def) { + String val = getFieldParam(field, param); + try { + return val==null ? def : Double.parseDouble(val); + } + catch( Exception ex ) { + throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, ex.getMessage(), ex ); + } + } + /** how to transform a String into a boolean... more flexible than * Boolean.parseBoolean() to enable easier integration with html forms. diff --git a/src/java/org/apache/solr/search/ValueSourceParser.java b/src/java/org/apache/solr/search/ValueSourceParser.java index fb306ffe88a..a9a4abc6449 100755 --- a/src/java/org/apache/solr/search/ValueSourceParser.java +++ b/src/java/org/apache/solr/search/ValueSourceParser.java @@ -45,11 +45,14 @@ import org.apache.solr.search.function.SimpleFloatFunction; import org.apache.solr.search.function.SumFloatFunction; import org.apache.solr.search.function.TopValueSource; import org.apache.solr.search.function.ValueSource; +import org.apache.solr.search.function.LiteralValueSource; import org.apache.solr.search.function.distance.HaversineFunction; import org.apache.solr.search.function.distance.SquaredEuclideanFunction; import org.apache.solr.search.function.distance.VectorDistanceFunction; +import org.apache.solr.search.function.distance.GeohashHaversineFunction; +import org.apache.solr.search.function.distance.GeohashFunction; import org.apache.solr.util.plugin.NamedListInitializedPlugin; import java.io.IOException; @@ -91,6 +94,15 @@ public abstract class ValueSourceParser implements NamedListInitializedPlugin { public void init(NamedList args) { } + }); + standardValueSourceParsers.put("literal", new ValueSourceParser() { + public ValueSource parse(FunctionQParser fp) throws ParseException { + return new LiteralValueSource(fp.getString()); + } + + public void init(NamedList args) { + } + }); standardValueSourceParsers.put("rord", new ValueSourceParser() { public ValueSource parse(FunctionQParser fp) throws ParseException { @@ -333,6 +345,36 @@ public abstract class ValueSourceParser implements NamedListInitializedPlugin { }); + standardValueSourceParsers.put("ghhsin", new ValueSourceParser() { + public ValueSource parse(FunctionQParser fp) throws ParseException { + + ValueSource gh1 = fp.parseValueSource(); + ValueSource gh2 = fp.parseValueSource(); + double radius = fp.parseDouble(); + + return new GeohashHaversineFunction(gh1, gh2, radius); + } + + public void init(NamedList args) { + } + + }); + + standardValueSourceParsers.put("geohash", new ValueSourceParser() { + public ValueSource parse(FunctionQParser fp) throws ParseException { + + ValueSource lat = fp.parseValueSource(); + ValueSource lon = fp.parseValueSource(); + + return new GeohashFunction(lat, lon); + } + + public void init(NamedList args) { + } + + }); + + standardValueSourceParsers.put("rad", new ValueSourceParser() { public ValueSource parse(FunctionQParser fp) throws ParseException { return new RadianFunction(fp.parseValueSource()); diff --git a/src/java/org/apache/solr/search/function/distance/GeohashFunction.java b/src/java/org/apache/solr/search/function/distance/GeohashFunction.java new file mode 100644 index 00000000000..6b203a127cc --- /dev/null +++ b/src/java/org/apache/solr/search/function/distance/GeohashFunction.java @@ -0,0 +1,100 @@ +package org.apache.solr.search.function.distance; +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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. + */ + +import org.apache.solr.search.function.ValueSource; +import org.apache.solr.search.function.DocValues; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.spatial.geohash.GeoHashUtils; + +import java.util.Map; +import java.io.IOException; + + +/** + * Takes in a latitude and longitude ValueSource and produces a GeoHash. + *
+ * Ex: geohash(lat, lon) + * + * + * Note, there is no reciprocal function for this. + **/ +public class GeohashFunction extends ValueSource { + protected ValueSource lat, lon; + + public GeohashFunction(ValueSource lat, ValueSource lon) { + this.lat = lat; + this.lon = lon; + } + + protected String name() { + return "geohash"; + } + + @Override + public DocValues getValues(Map context, IndexReader reader) throws IOException { + final DocValues latDV = lat.getValues(context, reader); + final DocValues lonDV = lon.getValues(context, reader); + + + return new DocValues() { + + @Override + public String strVal(int doc) { + return GeoHashUtils.encode(latDV.doubleVal(doc), lonDV.doubleVal(doc)); + } + + @Override + public String toString(int doc) { + StringBuilder sb = new StringBuilder(); + sb.append(name()).append('('); + sb.append(latDV.toString(doc)).append(',').append(lonDV.toString(doc)); + sb.append(')'); + return sb.toString(); + } + }; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GeohashFunction)) return false; + + GeohashFunction that = (GeohashFunction) o; + + if (!lat.equals(that.lat)) return false; + if (!lon.equals(that.lon)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = lat.hashCode(); + result = 31 * result + lon.hashCode(); + result = 31 * name().hashCode(); + return result; + } + + public String description() { + StringBuilder sb = new StringBuilder(); + sb.append(name()).append('('); + sb.append(lat).append(',').append(lon); + sb.append(')'); + return sb.toString(); + } +} diff --git a/src/java/org/apache/solr/search/function/distance/GeohashHaversineFunction.java b/src/java/org/apache/solr/search/function/distance/GeohashHaversineFunction.java new file mode 100644 index 00000000000..8e5b57df360 --- /dev/null +++ b/src/java/org/apache/solr/search/function/distance/GeohashHaversineFunction.java @@ -0,0 +1,139 @@ +package org.apache.solr.search.function.distance; +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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. + */ + + +import org.apache.solr.search.function.ValueSource; +import org.apache.solr.search.function.DocValues; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.spatial.geohash.GeoHashUtils; + +import java.util.Map; +import java.io.IOException; + + +/** + * Calculate the Haversine distance between two geo hash codes. + * + * + * Ex: ghhsin(ValueSource, ValueSource, radius) + * + * + * @see org.apache.solr.search.function.distance.HaversineFunction for more details on the implementation + * + **/ +public class GeohashHaversineFunction extends ValueSource { + + private ValueSource geoHash1, geoHash2; + private double radius; + + public GeohashHaversineFunction(ValueSource geoHash1, ValueSource geoHash2, double radius) { + this.geoHash1 = geoHash1; + this.geoHash2 = geoHash2; + this.radius = radius; + } + + protected String name() { + return "ghhsin"; + } + + @Override + public DocValues getValues(Map context, IndexReader reader) throws IOException { + final DocValues gh1DV = geoHash1.getValues(context, reader); + final DocValues gh2DV = geoHash2.getValues(context, reader); + + return new DocValues() { + public float floatVal(int doc) { + return (float) doubleVal(doc); + } + + public int intVal(int doc) { + return (int) doubleVal(doc); + } + + public long longVal(int doc) { + return (long) doubleVal(doc); + } + + public double doubleVal(int doc) { + return (double) distance(doc, gh1DV, gh2DV); + } + + public String strVal(int doc) { + return Double.toString(doubleVal(doc)); + } + + @Override + public String toString(int doc) { + StringBuilder sb = new StringBuilder(); + sb.append(name()).append('('); + sb.append(gh1DV.toString(doc)).append(',').append(gh2DV.toString(doc)); + sb.append(')'); + return sb.toString(); + } + }; + } + + protected double distance(int doc, DocValues gh1DV, DocValues gh2DV) { + double result = 0; + String h1 = gh1DV.strVal(doc); + String h2 = gh2DV.strVal(doc); + if (h1.equals(h2) == false){ + double[] h1Pair = GeoHashUtils.decode(h1); + double[] h2Pair = GeoHashUtils.decode(h2); + result = DistanceUtils.haversine(Math.toRadians(h1Pair[0]), Math.toRadians(h1Pair[1]), + Math.toRadians(h2Pair[0]), Math.toRadians(h2Pair[1]), radius); + } + return result; + } + + @Override + public void createWeight(Map context, Searcher searcher) throws IOException { + geoHash1.createWeight(context, searcher); + geoHash2.createWeight(context, searcher); + } + + public boolean equals(Object o) { + if (this.getClass() != o.getClass()) return false; + GeohashHaversineFunction other = (GeohashHaversineFunction) o; + return this.name().equals(other.name()) + && geoHash1.equals(other.geoHash1) && + geoHash2.equals(other.geoHash2) && + radius == other.radius; + } + + @Override + public int hashCode() { + int result; + long temp; + result = geoHash1.hashCode(); + result = 31 * result + geoHash2.hashCode(); + result = 31 * result + name().hashCode(); + temp = radius != +0.0d ? Double.doubleToLongBits(radius) : 0L; + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + public String description() { + StringBuilder sb = new StringBuilder(); + sb.append(name()).append('('); + sb.append(geoHash1).append(',').append(geoHash2); + sb.append(')'); + return sb.toString(); + } +} diff --git a/src/test/org/apache/solr/search/function/distance/DistanceFunctionTest.java b/src/test/org/apache/solr/search/function/distance/DistanceFunctionTest.java index 05ef6606669..47813d20668 100644 --- a/src/test/org/apache/solr/search/function/distance/DistanceFunctionTest.java +++ b/src/test/org/apache/solr/search/function/distance/DistanceFunctionTest.java @@ -18,6 +18,7 @@ package org.apache.solr.search.function.distance; import org.apache.solr.common.SolrException; import org.apache.solr.util.AbstractSolrTestCase; +import org.apache.lucene.spatial.geohash.GeoHashUtils; /** @@ -39,16 +40,24 @@ public class DistanceFunctionTest extends AbstractSolrTestCase { public void testHaversine() throws Exception { - assertU(adoc("id", "1", "x_td", "0", "y_td", "0")); - assertU(adoc("id", "2", "x_td", "0", "y_td", String.valueOf(Math.PI / 2))); - assertU(adoc("id", "3", "x_td", String.valueOf(Math.PI / 2), "y_td", String.valueOf(Math.PI / 2))); - assertU(adoc("id", "4", "x_td", String.valueOf(Math.PI / 4), "y_td", String.valueOf(Math.PI / 4))); + assertU(adoc("id", "1", "x_td", "0", "y_td", "0", "gh_s", GeoHashUtils.encode(32.7693246, -79.9289094))); + assertU(adoc("id", "2", "x_td", "0", "y_td", String.valueOf(Math.PI / 2), "gh_s", GeoHashUtils.encode(32.7693246, -78.9289094))); + assertU(adoc("id", "3", "x_td", String.valueOf(Math.PI / 2), "y_td", String.valueOf(Math.PI / 2), "gh_s", GeoHashUtils.encode(32.7693246, -80.9289094))); + assertU(adoc("id", "4", "x_td", String.valueOf(Math.PI / 4), "y_td", String.valueOf(Math.PI / 4), "gh_s", GeoHashUtils.encode(32.7693246, -81.9289094))); assertU(commit()); //Get the haversine distance between the point 0,0 and the docs above assuming a radius of 1 assertQ(req("fl", "*,score", "q", "{!func}hsin(x_td, y_td, 0, 0, 1)", "fq", "id:1"), "//float[@name='score']='0.0'"); assertQ(req("fl", "*,score", "q", "{!func}hsin(x_td, y_td, 0, 0, 1)", "fq", "id:2"), "//float[@name='score']='" + (float) (Math.PI / 2) + "'"); assertQ(req("fl", "*,score", "q", "{!func}hsin(x_td, y_td, 0, 0, 1)", "fq", "id:3"), "//float[@name='score']='" + (float) (Math.PI / 2) + "'"); assertQ(req("fl", "*,score", "q", "{!func}hsin(x_td, y_td, 0, 0, 1)", "fq", "id:4"), "//float[@name='score']='1.0471976'"); + + //Geo Hash Haversine + //Can verify here: http://www.movable-type.co.uk/scripts/latlong.html, but they use a slightly different radius for the earth, so just be close + assertQ(req("fl", "*,score", "q", "{!func}ghhsin(gh_s, \"" + GeoHashUtils.encode(32, -79) + + "\"," + Constants.EARTH_RADIUS_KM + + ")", "fq", "id:1"), "//float[@name='score']='122.30894'"); + assertQ(req("fl", "*,score", "q", "{!func}ghhsin(gh_s, geohash(32, -79)," + Constants.EARTH_RADIUS_KM + + ")", "fq", "id:1"), "//float[@name='score']='122.30894'"); } public void testVector() throws Exception {