mirror of https://github.com/apache/lucene.git
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:
parent
21ca796da8
commit
c1e2880e4a
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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} */
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue