SOLR-1568: add bbox, implement sfilt to restrict to dist

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1001129 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Yonik Seeley 2010-09-25 03:19:49 +00:00
parent 21ca796da8
commit c1e2880e4a
9 changed files with 524 additions and 148 deletions

View File

@ -96,7 +96,7 @@ public class QueryComponent extends SearchComponent
if (fqs!=null && fqs.length!=0) {
List<Query> filters = rb.getFilters();
if (filters==null) {
filters = new ArrayList<Query>();
filters = new ArrayList<Query>(fqs.length);
rb.setFilters( filters );
}
for (String fq : fqs) {

View File

@ -18,17 +18,19 @@ package org.apache.solr.schema;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.MultiFields;
import org.apache.lucene.search.*;
import org.apache.lucene.spatial.DistanceUtils;
import org.apache.lucene.spatial.tier.InvalidGeoException;
import org.apache.lucene.util.Bits;
import org.apache.solr.common.SolrException;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.response.XMLWriter;
import org.apache.solr.search.QParser;
import org.apache.solr.search.SolrIndexReader;
import org.apache.solr.search.SpatialOptions;
import org.apache.solr.search.function.DocValues;
import org.apache.solr.search.function.ValueSource;
import org.apache.solr.search.function.VectorValueSource;
@ -36,6 +38,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@ -82,175 +85,130 @@ public class LatLonType extends AbstractSubTypeFieldType implements SpatialQuery
return f;
}
@Override
public Query createSpatialQuery(QParser parser, SpatialOptions options) {
BooleanQuery result = new BooleanQuery();
double[] point = new double[0];
double[] point = null;
try {
point = DistanceUtils.parseLatitudeLongitude(options.pointStr);
} catch (InvalidGeoException e) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
}
// lat & lon in degrees
double latCenter = point[LAT];
double lonCenter = point[LONG];
point[0] = point[0] * DistanceUtils.DEGREES_TO_RADIANS;
point[1] = point[1] * DistanceUtils.DEGREES_TO_RADIANS;
//Get the distance
double[] ur = new double[2];
double[] ll = new double[2];
double[] tmp = new double[2];
//these calculations aren't totally accurate, but it should be good enough
//TODO: Optimize to do in single calculations. Would need to deal with poles, prime meridian, etc.
double [] north = DistanceUtils.pointOnBearing(point[LAT], point[LONG], options.distance, 0, tmp, options.radius);
//This returns the point as radians, but we need degrees b/c that is what the field is stored as
ur[LAT] = north[LAT] * DistanceUtils.RADIANS_TO_DEGREES;//get it now, as we are going to reuse tmp
double ur_lat = north[LAT] * DistanceUtils.RADIANS_TO_DEGREES;//get it now, as we are going to reuse tmp
double [] east = DistanceUtils.pointOnBearing(point[LAT], point[LONG], options.distance, DistanceUtils.DEG_90_AS_RADS, tmp, options.radius);
ur[LONG] = east[LONG] * DistanceUtils.RADIANS_TO_DEGREES;
double ur_lon = east[LONG] * DistanceUtils.RADIANS_TO_DEGREES;
double [] south = DistanceUtils.pointOnBearing(point[LAT], point[LONG], options.distance, DistanceUtils.DEG_180_AS_RADS, tmp, options.radius);
ll[LAT] = south[LAT] * DistanceUtils.RADIANS_TO_DEGREES;
double ll_lat = south[LAT] * DistanceUtils.RADIANS_TO_DEGREES;
double [] west = DistanceUtils.pointOnBearing(point[LAT], point[LONG], options.distance, DistanceUtils.DEG_270_AS_RADS, tmp, options.radius);
ll[LONG] = west[LONG] * DistanceUtils.RADIANS_TO_DEGREES;
double ll_lon = west[LONG] * DistanceUtils.RADIANS_TO_DEGREES;
SchemaField subSF;
Query range;
//TODO: can we reuse our bearing calculations?
double angDist = DistanceUtils.angularDistance(options.distance,
options.radius);//in radians
//for the poles, do something slightly different
//Also, note point[LAT] is in radians, but ur and ll are in degrees
if (point[LAT] + angDist > DistanceUtils.DEG_90_AS_RADS) { //we cross the north pole
double latMin = -90.0, latMax = 90.0, lonMin = -180.0, lonMax = 180.0;
double lon2Min = -180.0, lon2Max = 180.0; // optional second longitude restriction
// for the poles, do something slightly different - a polar "cap".
// Also, note point[LAT] is in radians, but ur and ll are in degrees
if (point[LAT] + angDist > DistanceUtils.DEG_90_AS_RADS) { // we cross the north pole
//we don't need a longitude boundary at all
latMin = Math.min(ll_lat, ur_lat);
} else if (point[LAT] - angDist < -DistanceUtils.DEG_90_AS_RADS) { // we cross the south pole
latMax = Math.max(ll_lat, ur_lat);
} else {
// set the latitude restriction as normal
latMin = ll_lat;
latMax = ur_lat;
double minLat = Math.min(ll[LAT], ur[LAT]);
subSF = subField(options.field, LAT);
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(minLat),
"90", true, true);
result.add(range, BooleanClause.Occur.MUST);
} else if (point[LAT] - angDist < -DistanceUtils.DEG_90_AS_RADS) {//we cross the south pole
subSF = subField(options.field, LAT);
double maxLat = Math.max(ll[LAT], ur[LAT]);
range = subSF.getType().getRangeQuery(parser, subSF,
"-90", String.valueOf(maxLat), true, true);
result.add(range, BooleanClause.Occur.MUST);
} else{
//Latitude
//we may need to generate multiple queries depending on the range
//Are we crossing the 180 deg. longitude, if so, we need to do some special things
if (ll[LONG] > 0.0 && ur[LONG] < 0.0) {
//TODO: refactor into common code, etc.
//Now check other side of the Equator
if (ll[LAT] < 0.0 && ur[LAT] > 0.0) {
addEquatorialBoundary(parser, options, result, ur[LAT], ll[LAT]);
} //check poles
else {
subSF = subField(options.field, LAT);
//not crossing the equator
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(ll[LAT]),
String.valueOf(ur[LAT]), true, true);
result.add(range, BooleanClause.Occur.MUST);
}
//Longitude
addMeridianBoundary(parser, options, result, ur[LONG], ll[LONG], "180.0", "-180.0");
if (ll_lon > ur_lon) {
// we crossed the +-180 deg longitude... need to make
// range queries of (-180 TO ur) OR (ll TO 180)
lonMin = -180;
lonMax = ur_lon;
lon2Min = ll_lon;
lon2Max = 180;
} else {
lonMin = ll_lon;
lonMax = ur_lon;
}
}
} else if (ll[LONG] < 0.0 && ur[LONG] > 0.0) {//prime meridian (0 degrees
//Now check other side of the Equator
if (ll[LAT] < 0.0 && ur[LAT] > 0.0) {
addEquatorialBoundary(parser, options, result, ur[LAT], ll[LAT]);
} else {
subSF = subField(options.field, LAT);
//not crossing the equator
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(ll[LAT]),
String.valueOf(ur[LAT]), true, true);
result.add(range, BooleanClause.Occur.MUST);
}
//Longitude
addMeridianBoundary(parser, options, result, ur[LONG], ll[LONG], "0.0", ".0");
} else {// we are all in the Eastern or Western hemi
//Now check other side of the Equator
if (ll[LAT] < 0.0 && ur[LAT] > 0.0) {
addEquatorialBoundary(parser, options, result, ur[LAT], ll[LAT]);
} else {//we are all in either the Northern or the Southern Hemi.
//TODO: nice to move this up so that it is the first thing and we can avoid the extra checks since
//this is actually the most likely case
subSF = subField(options.field, LAT);
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(ll[LAT]),
String.valueOf(ur[LAT]), true, true);
result.add(range, BooleanClause.Occur.MUST);
// Now that we've figured out the ranges, build them!
SchemaField latField = subField(options.field, LAT);
SchemaField lonField = subField(options.field, LONG);
}
//Longitude, all in the same hemi
subSF = subField(options.field, LONG);
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(ll[LONG]),
String.valueOf(ur[LONG]), true, true);
result.add(range, BooleanClause.Occur.MUST);
if (options.bbox) {
BooleanQuery result = new BooleanQuery(); // only used if box==true
Query latRange = latField.getType().getRangeQuery(parser, latField,
String.valueOf(latMin),
String.valueOf(latMax),
true, true);
result.add(latRange, BooleanClause.Occur.MUST);
if (lonMin != -180 || lonMax != 180) {
Query lonRange = lonField.getType().getRangeQuery(parser, lonField,
String.valueOf(lonMin),
String.valueOf(lonMax),
true, true);
if (lon2Min != -180 || lon2Max != 180) {
// another valid longitude range
BooleanQuery bothLons = new BooleanQuery();
bothLons.add(lonRange, BooleanClause.Occur.SHOULD);
lonRange = lonField.getType().getRangeQuery(parser, lonField,
String.valueOf(lon2Min),
String.valueOf(lon2Max),
true, true);
bothLons.add(lonRange, BooleanClause.Occur.SHOULD);
lonRange = bothLons;
}
result.add(lonRange, BooleanClause.Occur.MUST);
}
return result;
}
/**
* Add a boundary condition around a meridian
* @param parser
* @param options
* @param result
* @param upperRightLon
* @param lowerLeftLon
* @param eastern
* @param western
*/
private void addMeridianBoundary(QParser parser, SpatialOptions options, BooleanQuery result, double upperRightLon,
double lowerLeftLon, String eastern, String western) {
SchemaField subSF;
Query range;
BooleanQuery lonQ = new BooleanQuery();
subSF = subField(options.field, LONG);
//Eastern Hemisphere
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(lowerLeftLon),
eastern, true, true);
lonQ.add(range, BooleanClause.Occur.SHOULD);
//Western hemi
range = subSF.getType().getRangeQuery(parser, subSF,
western,
String.valueOf(upperRightLon), true, true);
lonQ.add(range, BooleanClause.Occur.SHOULD);
//One or the other must occur
result.add(lonQ, BooleanClause.Occur.MUST);
}
SpatialDistanceQuery spatial = new SpatialDistanceQuery();
spatial.origField = options.field.getName();
spatial.latSource = latField.getType().getValueSource(latField, parser);
spatial.lonSource = lonField.getType().getValueSource(lonField, parser);
spatial.latMin = latMin;
spatial.latMax = latMax;
spatial.lonMin = lonMin;
spatial.lonMax = lonMax;
spatial.lon2Min = lon2Min;
spatial.lon2Max = lon2Max;
spatial.lon2 = lon2Min != -180 || lon2Max != 180;
/**
* Add query conditions for boundaries like the equator, poles and meridians
*
* @param parser
* @param options
* @param result
* @param upperRight
* @param lowerLeft
*/
protected void addEquatorialBoundary(QParser parser, SpatialOptions options, BooleanQuery result, double upperRight, double lowerLeft) {
SchemaField subSF;
Query range;
BooleanQuery tmpQ = new BooleanQuery();
subSF = subField(options.field, LAT);
//southern hemi.
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(lowerLeft),
"0", true, true);
tmpQ.add(range, BooleanClause.Occur.SHOULD);
//northern hemi
range = subSF.getType().getRangeQuery(parser, subSF,
"0", String.valueOf(upperRight), true, true);
tmpQ.add(range, BooleanClause.Occur.SHOULD);
//One or the other must occur
result.add(tmpQ, BooleanClause.Occur.MUST);
spatial.latCenter = latCenter;
spatial.lonCenter = lonCenter;
spatial.dist = options.distance;
spatial.planetRadius = options.radius;
spatial.calcDist = !options.bbox;
return spatial;
}
@Override
@ -312,3 +270,291 @@ class LatLonValueSource extends VectorValueSource {
return name() + "(" + sf.getName() + ")";
}
}
////////////////////////////////////////////////////////////////////////////////////////////
// TODO: recast as a value source that doesn't have to match all docs
class SpatialDistanceQuery extends Query {
String origField;
ValueSource latSource;
ValueSource lonSource;
double lonMin, lonMax, lon2Min, lon2Max, latMin, latMax;
boolean lon2;
boolean calcDist; // actually calculate the distance with haversine
double latCenter;
double lonCenter;
double dist;
double planetRadius;
@Override
public Query rewrite(IndexReader reader) throws IOException {
return this;
}
@Override
public void extractTerms(Set terms) {}
protected class SpatialWeight extends Weight {
protected Searcher searcher;
protected float queryNorm;
protected float queryWeight;
protected Map latContext;
protected Map lonContext;
public SpatialWeight(Searcher searcher) throws IOException {
this.searcher = searcher;
this.latContext = latSource.newContext();
this.lonContext = lonSource.newContext();
latSource.createWeight(latContext, searcher);
lonSource.createWeight(lonContext, searcher);
}
@Override
public Query getQuery() {
return SpatialDistanceQuery.this;
}
@Override
public float getValue() {
return queryWeight;
}
@Override
public float sumOfSquaredWeights() throws IOException {
queryWeight = getBoost();
return queryWeight * queryWeight;
}
@Override
public void normalize(float norm) {
this.queryNorm = norm;
queryWeight *= this.queryNorm;
}
@Override
public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder, boolean topScorer) throws IOException {
return new SpatialScorer(getSimilarity(searcher), reader, this);
}
@Override
public Explanation explain(IndexReader reader, int doc) throws IOException {
SolrIndexReader topReader = (SolrIndexReader)reader;
SolrIndexReader[] subReaders = topReader.getLeafReaders();
int[] offsets = topReader.getLeafOffsets();
int readerPos = SolrIndexReader.readerIndex(doc, offsets);
int readerBase = offsets[readerPos];
return ((SpatialScorer)scorer(subReaders[readerPos], true, true)).explain(doc-readerBase);
}
}
protected class SpatialScorer extends Scorer {
final IndexReader reader;
final SpatialWeight weight;
final int maxDoc;
final float qWeight;
int doc=-1;
final DocValues latVals;
final DocValues lonVals;
final Bits delDocs;
final double lonMin, lonMax, lon2Min, lon2Max, latMin, latMax;
final boolean lon2;
final boolean calcDist;
final double latCenterRad;
final double lonCenterRad;
final double latCenterRad_cos;
final double dist;
final double planetRadius;
int lastDistDoc;
double lastDist;
public SpatialScorer(Similarity similarity, IndexReader reader, SpatialWeight w) throws IOException {
super(similarity);
this.weight = w;
this.qWeight = w.getValue();
this.reader = reader;
this.maxDoc = reader.maxDoc();
this.delDocs = reader.hasDeletions() ? MultiFields.getDeletedDocs(reader) : null;
latVals = latSource.getValues(weight.latContext, reader);
lonVals = lonSource.getValues(weight.lonContext, reader);
this.lonMin = SpatialDistanceQuery.this.lonMin;
this.lonMax = SpatialDistanceQuery.this.lonMax;
this.lon2Min = SpatialDistanceQuery.this.lon2Min;
this.lon2Max = SpatialDistanceQuery.this.lon2Max;
this.latMin = SpatialDistanceQuery.this.latMin;
this.latMax = SpatialDistanceQuery.this.latMax;
this.lon2 = SpatialDistanceQuery.this.lon2;
this.calcDist = SpatialDistanceQuery.this.calcDist;
this.latCenterRad = SpatialDistanceQuery.this.latCenter * DistanceUtils.DEGREES_TO_RADIANS;
this.lonCenterRad = SpatialDistanceQuery.this.lonCenter * DistanceUtils.DEGREES_TO_RADIANS;
this.latCenterRad_cos = this.calcDist ? Math.cos(latCenterRad) : 0;
this.dist = SpatialDistanceQuery.this.dist;
this.planetRadius = SpatialDistanceQuery.this.planetRadius;
}
boolean match() {
// longitude should generally be more restrictive than latitude
// (e.g. in the US, it immediately separates the coasts, and in world search separates
// US from Europe from Asia, etc.
double lon = lonVals.doubleVal(doc);
if (! ((lon >= lonMin && lon <=lonMax) || (lon2 && lon >= lon2Min && lon <= lon2Max)) ) {
return false;
}
double lat = latVals.doubleVal(doc);
if ( !(lat >= latMin && lat <= latMax) ) {
return false;
}
if (!calcDist) return true;
// TODO: test for internal box where we wouldn't need to calculate the distance
return dist(lat, lon) <= dist;
}
double dist(double lat, double lon) {
double latRad = lat * DistanceUtils.DEGREES_TO_RADIANS;
double lonRad = lon * DistanceUtils.DEGREES_TO_RADIANS;
// haversine, specialized to avoid a cos() call on latCenterRad
double diffX = latCenterRad - latRad;
double diffY = lonCenterRad - lonRad;
double hsinX = Math.sin(diffX * 0.5);
double hsinY = Math.sin(diffY * 0.5);
double h = hsinX * hsinX +
(latCenterRad_cos * Math.cos(latRad) * hsinY * hsinY);
double result = (planetRadius * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h)));
// save the results of this calculation
lastDistDoc = doc;
lastDist = result;
return result;
}
@Override
public int docID() {
return doc;
}
// instead of matching all docs, we could also embed a query.
// the score could either ignore the subscore, or boost it.
// Containment: floatline(foo:myTerm, "myFloatField", 1.0, 0.0f)
// Boost: foo:myTerm^floatline("myFloatField",1.0,0.0f)
@Override
public int nextDoc() throws IOException {
for(;;) {
++doc;
if (doc>=maxDoc) {
return doc=NO_MORE_DOCS;
}
if (delDocs != null && delDocs.get(doc)) continue;
if (!match()) continue;
return doc;
}
}
@Override
public int advance(int target) throws IOException {
// this will work even if target==NO_MORE_DOCS
doc=target-1;
return nextDoc();
}
@Override
public float score() throws IOException {
double dist = (doc == lastDistDoc) ? lastDist : dist(latVals.doubleVal(doc), lonVals.doubleVal(doc));
return (float)(dist * qWeight);
}
public Explanation explain(int doc) throws IOException {
advance(doc);
boolean matched = this.doc == doc;
this.doc = doc;
float sc = matched ? score() : 0;
double dist = dist(latVals.doubleVal(doc), lonVals.doubleVal(doc));
String description = SpatialDistanceQuery.this.toString();
Explanation result = new ComplexExplanation
(this.doc == doc, sc, description + " product of:");
// result.addDetail(new Explanation((float)dist, "hsin("+latVals.explain(doc)+","+lonVals.explain(doc)));
result.addDetail(new Explanation((float)dist, "hsin("+latVals.doubleVal(doc)+","+lonVals.doubleVal(doc)));
result.addDetail(new Explanation(getBoost(), "boost"));
result.addDetail(new Explanation(weight.queryNorm,"queryNorm"));
return result;
}
}
@Override
public Weight createWeight(Searcher searcher) throws IOException {
return new SpatialWeight(searcher);
}
/** Prints a user-readable version of this query. */
@Override
public String toString(String field)
{
float boost = getBoost();
return (boost!=1.0?"(":"") +
"sfilt(latlonSource="+origField +"(" + latSource + "," + lonSource + ")"
+",latCenter="+latCenter+",lonCenter="+lonCenter
+",dist=" + dist
+",latMin=" + latMin + ",latMax="+latMax
+",lonMin=" + lonMin + ",lonMax"+lonMax
+",lon2Min=" + lon2Min + ",lon2Max" + lon2Max
+",calcDist="+calcDist
+",planetRadius="+planetRadius
+")"
+ (boost==1.0 ? "" : ")^"+boost);
}
/** Returns true if <code>o</code> is equal to this. */
@Override
public boolean equals(Object o) {
if (SpatialDistanceQuery.class != o.getClass()) return false;
SpatialDistanceQuery other = (SpatialDistanceQuery)o;
return this.latCenter == other.latCenter
&& this.lonCenter == other.lonCenter
&& this.latMin == other.latMin
&& this.latMax == other.latMax
&& this.lonMin == other.lonMin
&& this.lonMax == other.lonMax
&& this.lon2Min == other.lon2Min
&& this.lon2Max == other.lon2Max
&& this.dist == other.dist
&& this.planetRadius == other.planetRadius
&& this.calcDist == other.calcDist
&& this.lonSource.equals(other.lonSource)
&& this.latSource.equals(other.latSource)
&& this.getBoost() == other.getBoost()
;
}
/** Returns a hash code value for this object. */
@Override
public int hashCode() {
// don't bother making the hash expensive - the center latitude + min longitude will be very uinque
long hash = Double.doubleToLongBits(latCenter);
hash = hash * 31 + Double.doubleToLongBits(lonMin);
return (int)(hash >> 32 + hash);
}
}

View File

@ -39,6 +39,7 @@ public abstract class QParserPlugin implements NamedListInitializedPlugin {
NestedQParserPlugin.NAME, NestedQParserPlugin.class,
FunctionRangeQParserPlugin.NAME, FunctionRangeQParserPlugin.class,
SpatialFilterQParserPlugin.NAME, SpatialFilterQParserPlugin.class,
SpatialBoxQParserPlugin.NAME, SpatialBoxQParserPlugin.class,
};
/** return a {@link QParser} */

View File

@ -662,6 +662,80 @@ public class SolrIndexSearcher extends IndexSearcher implements SolrInfoMBean {
return answer;
}
Filter getFilter(Query q) throws IOException {
if (q == null) return null;
// TODO: support pure negative queries?
// if (q instanceof) {
// }
return getDocSet(q).getTopFilter();
}
Filter getFilter(DocSet setFilter, List<Query> queries) throws IOException {
Filter answer = setFilter == null ? null : setFilter.getTopFilter();
if (queries == null || queries.size() == 0) {
return answer;
}
if (answer == null && queries.size() == 1) {
return getFilter(queries.get(0));
}
DocSet finalSet=null;
int nDocSets =0;
boolean[] neg = new boolean[queries.size()];
DocSet[] sets = new DocSet[queries.size()];
Query[] nocache = new Query[queries.size()];
int smallestIndex = -1;
int smallestCount = Integer.MAX_VALUE;
for (Query q : queries) {
// if (q instanceof)
Query posQuery = QueryUtils.getAbs(q);
sets[nDocSets] = getPositiveDocSet(posQuery);
// Negative query if absolute value different from original
if (q==posQuery) {
neg[nDocSets] = false;
// keep track of the smallest positive set.
// This optimization is only worth it if size() is cached, which it would
// be if we don't do any set operations.
int sz = sets[nDocSets].size();
if (sz<smallestCount) {
smallestCount=sz;
smallestIndex=nDocSets;
finalSet = sets[nDocSets];
}
} else {
neg[nDocSets] = true;
}
nDocSets++;
}
// if no positive queries, start off with all docs
if (finalSet==null) finalSet = getPositiveDocSet(matchAllDocsQuery);
// do negative queries first to shrink set size
for (int i=0; i<sets.length; i++) {
if (neg[i]) finalSet = finalSet.andNot(sets[i]);
}
for (int i=0; i<sets.length; i++) {
if (!neg[i] && i!=smallestIndex) finalSet = finalSet.intersection(sets[i]);
}
return finalSet.getTopFilter();
}
// query must be positive
protected DocSet getDocSetNC(Query query, DocSet filter, DocsEnumState deState) throws IOException {
if (filter != null) return getDocSetNC(query, filter, null);

View File

@ -0,0 +1,38 @@
/**
* 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.
*/
package org.apache.solr.search;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.request.SolrQueryRequest;
public class SpatialBoxQParserPlugin extends SpatialFilterQParserPlugin {
public static String NAME = "bbox";
@Override
public QParser createParser(String qstr, SolrParams localParams,
SolrParams params, SolrQueryRequest req) {
return new SpatialFilterQParser(qstr, localParams, params, req, true);
}
public void init(NamedList args) {
}
}

View File

@ -27,7 +27,6 @@ import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.params.SpatialParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.schema.SpatialQueryable;
@ -54,10 +53,11 @@ import org.apache.solr.schema.SpatialQueryable;
*
*/
public class SpatialFilterQParser extends QParser {
boolean bbox; // do bounding box only
public SpatialFilterQParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
public SpatialFilterQParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req, boolean bbox) {
super(qstr, localParams, params, req);
this.bbox = bbox;
}
@ -98,6 +98,7 @@ public class SpatialFilterQParser extends QParser {
if (type instanceof SpatialQueryable) {
double radius = localParams.getDouble(SpatialParams.SPHERE_RADIUS, DistanceUtils.EARTH_MEAN_RADIUS_KM);
SpatialOptions opts = new SpatialOptions(pointStr, dist, sf, measStr, radius, DistanceUnits.KILOMETERS);
opts.bbox = bbox;
result = ((SpatialQueryable)type).createSpatialQuery(this, opts);
} else {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "The field " + fields[0]

View File

@ -27,12 +27,11 @@ import org.apache.solr.request.SolrQueryRequest;
public class SpatialFilterQParserPlugin extends QParserPlugin {
public static String NAME = "sfilt";
@Override
public QParser createParser(String qstr, SolrParams localParams,
SolrParams params, SolrQueryRequest req) {
return new SpatialFilterQParser(qstr, localParams, params, req);
return new SpatialFilterQParser(qstr, localParams, params, req, false);
}
public void init(NamedList args) {
@ -40,3 +39,4 @@ public class SpatialFilterQParserPlugin extends QParserPlugin {
}
}

View File

@ -32,6 +32,11 @@ public class SpatialOptions {
public double radius;
public DistanceUnits units;
/** Just do a "bounding box" - or any other quicker method / shape that
* still encompasses all of the points of interest, but may also encompass
* points outside.
*/
public boolean bbox;
public SpatialOptions() {
}

View File

@ -96,7 +96,7 @@ public class SpatialFilterTest extends SolrTestCaseJ4 {
//Because we are generating a box based on the west/east longitudes and the south/north latitudes, which then
//translates to a range query, which is slightly more inclusive. Thus, even though 0.0 is 15.725 kms away,
//it will be included, b/c of the box calculation.
checkHits(fieldName, "0.1,0.1", 15, 2, 5, 6);
checkHits(fieldName, false, "0.1,0.1", 15, 2, 5, 6);
//try some more
clearIndex();
assertU(adoc("id", "14", fieldName, "0,5"));
@ -108,15 +108,23 @@ public class SpatialFilterTest extends SolrTestCaseJ4 {
checkHits(fieldName, "0,0", 1000, 1, 14);
checkHits(fieldName, "0,0", 2000, 2, 14, 15);
checkHits(fieldName, "0,0", 3000, 3, 14, 15, 16);
checkHits(fieldName, false, "0,0", 3000, 3, 14, 15, 16);
checkHits(fieldName, "0,0", 3001, 3, 14, 15, 16);
checkHits(fieldName, "0,0", 3000.1, 3, 14, 15, 16);
//really fine grained distance and reflects some of the vagaries of how we are calculating the box
checkHits(fieldName, "43.517030,-96.789603", 109, 0);
checkHits(fieldName, "43.517030,-96.789603", 110, 1, 17);
// falls outside of the real distance, but inside the bounding box
checkHits(fieldName, true, "43.517030,-96.789603", 110, 0);
checkHits(fieldName, false, "43.517030,-96.789603", 110, 1, 17);
}
private void checkHits(String fieldName, String pt, double distance, int count, int ... docIds) {
checkHits(fieldName, true, pt, distance, count, docIds);
}
private void checkHits(String fieldName, boolean exact, String pt, double distance, int count, int ... docIds) {
String [] tests = new String[docIds != null && docIds.length > 0 ? docIds.length + 1 : 1];
tests[0] = "*[count(//doc)=" + count + "]";
if (docIds != null && docIds.length > 0) {
@ -125,9 +133,12 @@ public class SpatialFilterTest extends SolrTestCaseJ4 {
tests[i++] = "//result/doc/int[@name='id'][.='" + docId + "']";
}
}
assertQ(req("fl", "id", "q","*:*", "rows", "1000", "fq", "{!sfilt fl=" +fieldName +"}",
"pt", pt, "d", String.valueOf(distance)),
tests);//
String method = exact ? "sfilt" : "bbox";
assertQ(req("fl", "id", "q","*:*", "rows", "1000", "fq", "{!"+method+" fl=" +fieldName +"}",
"pt", pt, "d", String.valueOf(distance)),
tests);
}