| package org.apache.solr.schema; |
| /* |
| * 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 java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import com.spatial4j.core.shape.Point; |
| |
| import org.apache.lucene.document.FieldType; |
| import org.apache.lucene.index.AtomicReaderContext; |
| import org.apache.lucene.index.IndexReader; |
| import org.apache.lucene.index.StorableField; |
| import org.apache.lucene.queries.function.FunctionValues; |
| import org.apache.lucene.queries.function.ValueSource; |
| import org.apache.lucene.queries.function.valuesource.VectorValueSource; |
| import org.apache.lucene.search.LeafCollector; |
| import org.apache.lucene.search.BooleanClause; |
| import org.apache.lucene.search.BooleanQuery; |
| import org.apache.lucene.search.ComplexExplanation; |
| import org.apache.lucene.search.Explanation; |
| import org.apache.lucene.search.IndexSearcher; |
| import org.apache.lucene.search.Query; |
| import org.apache.lucene.search.Scorer; |
| import org.apache.lucene.search.SortField; |
| import org.apache.lucene.search.Weight; |
| import org.apache.lucene.util.Bits; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.response.TextResponseWriter; |
| import org.apache.solr.search.DelegatingCollector; |
| import org.apache.solr.search.ExtendedQueryBase; |
| import org.apache.solr.search.PostFilter; |
| import org.apache.solr.search.QParser; |
| import org.apache.solr.search.SpatialOptions; |
| |
| import com.spatial4j.core.context.SpatialContext; |
| import com.spatial4j.core.distance.DistanceUtils; |
| import com.spatial4j.core.shape.Rectangle; |
| |
| import org.apache.solr.util.SpatialUtils; |
| |
| |
| /** |
| * Represents a Latitude/Longitude as a 2 dimensional point. Latitude is <b>always</b> specified first. |
| */ |
| public class LatLonType extends AbstractSubTypeFieldType implements SpatialQueryable { |
| protected static final int LAT = 0; |
| protected static final int LON = 1; |
| |
| @Override |
| protected void init(IndexSchema schema, Map<String, String> args) { |
| super.init(schema, args); |
| //TODO: refactor this, as we are creating the suffix cache twice, since the super.init does it too |
| createSuffixCache(3);//we need three extra fields: one for the storage field, two for the lat/lon |
| } |
| |
| @Override |
| public List<StorableField> createFields(SchemaField field, Object value, float boost) { |
| String externalVal = value.toString(); |
| //we could have 3 fields (two for the lat & lon, one for storage) |
| List<StorableField> f = new ArrayList<>(3); |
| if (field.indexed()) { |
| Point point = SpatialUtils.parsePointSolrException(externalVal, SpatialContext.GEO); |
| //latitude |
| SchemaField subLatSF = subField(field, LAT, schema); |
| f.add(subLatSF.createField(String.valueOf(point.getY()), subLatSF.indexed() && !subLatSF.omitNorms() ? boost : 1f)); |
| //longitude |
| SchemaField subLonSF = subField(field, LON, schema); |
| f.add(subLonSF.createField(String.valueOf(point.getX()), subLonSF.indexed() && !subLonSF.omitNorms() ? boost : 1f)); |
| } |
| |
| if (field.stored()) { |
| FieldType customType = new FieldType(); |
| customType.setStored(true); |
| f.add(createField(field.getName(), externalVal, customType, 1f)); |
| } |
| return f; |
| } |
| |
| |
| @Override |
| public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) { |
| Point p1 = SpatialUtils.parsePointSolrException(part1, SpatialContext.GEO); |
| Point p2 = SpatialUtils.parsePointSolrException(part2, SpatialContext.GEO); |
| |
| SchemaField latSF = subField(field, LAT, parser.getReq().getSchema()); |
| SchemaField lonSF = subField(field, LON, parser.getReq().getSchema()); |
| BooleanQuery result = new BooleanQuery(true); |
| // points must currently be ordered... should we support specifying any two opposite corner points? |
| result.add(latSF.getType().getRangeQuery(parser, latSF, |
| Double.toString(p1.getY()), Double.toString(p2.getY()), minInclusive, maxInclusive), BooleanClause.Occur.MUST); |
| result.add(lonSF.getType().getRangeQuery(parser, lonSF, |
| Double.toString(p1.getX()), Double.toString(p2.getX()), minInclusive, maxInclusive), BooleanClause.Occur.MUST); |
| return result; |
| } |
| |
| @Override |
| public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) { |
| Point p1 = SpatialUtils.parsePointSolrException(externalVal, SpatialContext.GEO); |
| |
| SchemaField latSF = subField(field, LAT, parser.getReq().getSchema()); |
| SchemaField lonSF = subField(field, LON, parser.getReq().getSchema()); |
| BooleanQuery result = new BooleanQuery(true); |
| result.add(latSF.getType().getFieldQuery(parser, latSF, |
| Double.toString(p1.getY())), BooleanClause.Occur.MUST); |
| result.add(lonSF.getType().getFieldQuery(parser, lonSF, |
| Double.toString(p1.getX())), BooleanClause.Occur.MUST); |
| return result; |
| } |
| |
| |
| |
| @Override |
| public Query createSpatialQuery(QParser parser, SpatialOptions options) { |
| Point point = SpatialUtils.parsePointSolrException(options.pointStr, SpatialContext.GEO); |
| |
| // lat & lon in degrees |
| double latCenter = point.getY(); |
| double lonCenter = point.getX(); |
| |
| double distDeg = DistanceUtils.dist2Degrees(options.distance, options.radius); |
| Rectangle bbox = DistanceUtils.calcBoxByDistFromPtDEG(latCenter, lonCenter, distDeg, SpatialContext.GEO, null); |
| double latMin = bbox.getMinY(); |
| double latMax = bbox.getMaxY(); |
| double lonMin, lonMax, lon2Min, lon2Max; |
| if (bbox.getCrossesDateLine()) { |
| lonMin = -180; |
| lonMax = bbox.getMaxX(); |
| lon2Min = bbox.getMinX(); |
| lon2Max = 180; |
| } else { |
| lonMin = bbox.getMinX(); |
| lonMax = bbox.getMaxX(); |
| lon2Min = -180; |
| lon2Max = 180; |
| } |
| |
| IndexSchema schema = parser.getReq().getSchema(); |
| |
| // Now that we've figured out the ranges, build them! |
| SchemaField latSF = subField(options.field, LAT, schema); |
| SchemaField lonSF = subField(options.field, LON, schema); |
| |
| SpatialDistanceQuery spatial = new SpatialDistanceQuery(); |
| |
| |
| if (options.bbox) { |
| BooleanQuery result = new BooleanQuery(); |
| |
| Query latRange = latSF.getType().getRangeQuery(parser, latSF, |
| String.valueOf(latMin), |
| String.valueOf(latMax), |
| true, true); |
| result.add(latRange, BooleanClause.Occur.MUST); |
| |
| if (lonMin != -180 || lonMax != 180) { |
| Query lonRange = lonSF.getType().getRangeQuery(parser, lonSF, |
| 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 = lonSF.getType().getRangeQuery(parser, lonSF, |
| String.valueOf(lon2Min), |
| String.valueOf(lon2Max), |
| true, true); |
| bothLons.add(lonRange, BooleanClause.Occur.SHOULD); |
| |
| lonRange = bothLons; |
| } |
| |
| result.add(lonRange, BooleanClause.Occur.MUST); |
| } |
| |
| spatial.bboxQuery = result; |
| } |
| |
| |
| spatial.origField = options.field.getName(); |
| spatial.latSource = latSF.getType().getValueSource(latSF, parser); |
| spatial.lonSource = lonSF.getType().getValueSource(lonSF, 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; |
| |
| spatial.latCenter = latCenter; |
| spatial.lonCenter = lonCenter; |
| spatial.dist = options.distance; |
| spatial.planetRadius = options.radius; |
| |
| spatial.calcDist = !options.bbox; |
| |
| return spatial; |
| } |
| |
| @Override |
| public ValueSource getValueSource(SchemaField field, QParser parser) { |
| ArrayList<ValueSource> vs = new ArrayList<>(2); |
| for (int i = 0; i < 2; i++) { |
| SchemaField sub = subField(field, i, parser.getReq().getSchema()); |
| vs.add(sub.getType().getValueSource(sub, parser)); |
| } |
| return new LatLonValueSource(field, vs); |
| } |
| |
| @Override |
| public boolean isPolyField() { |
| return true; |
| } |
| |
| @Override |
| public void write(TextResponseWriter writer, String name, StorableField f) throws IOException { |
| writer.writeStr(name, f.stringValue(), true); |
| } |
| |
| @Override |
| public SortField getSortField(SchemaField field, boolean top) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Sorting not supported on LatLonType " + field.getName()); |
| } |
| |
| |
| |
| //It never makes sense to create a single field, so make it impossible to happen |
| |
| @Override |
| public StorableField createField(SchemaField field, Object value, float boost) { |
| throw new UnsupportedOperationException("LatLonType uses multiple fields. field=" + field.getName()); |
| } |
| |
| } |
| |
| class LatLonValueSource extends VectorValueSource { |
| private final SchemaField sf; |
| |
| public LatLonValueSource(SchemaField sf, List<ValueSource> sources) { |
| super(sources); |
| this.sf = sf; |
| } |
| |
| @Override |
| public String name() { |
| return "latlon"; |
| } |
| |
| @Override |
| public String description() { |
| return name() + "(" + sf.getName() + ")"; |
| } |
| } |
| |
| |
| |
| |
| //////////////////////////////////////////////////////////////////////////////////////////// |
| // TODO: recast as a value source that doesn't have to match all docs |
| |
| class SpatialDistanceQuery extends ExtendedQueryBase implements PostFilter { |
| String origField; |
| ValueSource latSource; |
| ValueSource lonSource; |
| double lonMin, lonMax, lon2Min, lon2Max, latMin, latMax; |
| boolean lon2; |
| |
| boolean calcDist; // actually calculate the distance with haversine |
| Query bboxQuery; |
| |
| double latCenter; |
| double lonCenter; |
| double dist; |
| double planetRadius; |
| |
| |
| @Override |
| public Query rewrite(IndexReader reader) throws IOException { |
| return bboxQuery != null ? bboxQuery.rewrite(reader) : this; |
| } |
| |
| @Override |
| public void extractTerms(Set terms) {} |
| |
| |
| protected class SpatialWeight extends Weight { |
| protected IndexSearcher searcher; |
| protected float queryNorm; |
| protected float queryWeight; |
| protected Map latContext; |
| protected Map lonContext; |
| |
| public SpatialWeight(IndexSearcher searcher) throws IOException { |
| this.searcher = searcher; |
| this.latContext = ValueSource.newContext(searcher); |
| this.lonContext = ValueSource.newContext(searcher); |
| latSource.createWeight(latContext, searcher); |
| lonSource.createWeight(lonContext, searcher); |
| } |
| |
| @Override |
| public Query getQuery() { |
| return SpatialDistanceQuery.this; |
| } |
| |
| @Override |
| public float getValueForNormalization() throws IOException { |
| queryWeight = getBoost(); |
| return queryWeight * queryWeight; |
| } |
| |
| @Override |
| public void normalize(float norm, float topLevelBoost) { |
| this.queryNorm = norm * topLevelBoost; |
| queryWeight *= this.queryNorm; |
| } |
| |
| @Override |
| public Scorer scorer(AtomicReaderContext context, Bits acceptDocs) throws IOException { |
| return new SpatialScorer(context, acceptDocs, this, queryWeight); |
| } |
| |
| @Override |
| public Explanation explain(AtomicReaderContext context, int doc) throws IOException { |
| return ((SpatialScorer)scorer(context, context.reader().getLiveDocs())).explain(doc); |
| } |
| } |
| |
| protected class SpatialScorer extends Scorer { |
| final IndexReader reader; |
| final SpatialWeight weight; |
| final int maxDoc; |
| final float qWeight; |
| int doc=-1; |
| final FunctionValues latVals; |
| final FunctionValues lonVals; |
| final Bits acceptDocs; |
| |
| |
| 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(AtomicReaderContext readerContext, Bits acceptDocs, SpatialWeight w, float qWeight) throws IOException { |
| super(w); |
| this.weight = w; |
| this.qWeight = qWeight; |
| this.reader = readerContext.reader(); |
| this.maxDoc = reader.maxDoc(); |
| this.acceptDocs = acceptDocs; |
| latVals = latSource.getValues(weight.latContext, readerContext); |
| lonVals = lonSource.getValues(weight.lonContext, readerContext); |
| |
| 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 (acceptDocs != null && !acceptDocs.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); |
| } |
| |
| @Override |
| public int freq() throws IOException { |
| return 1; |
| } |
| |
| @Override |
| public long cost() { |
| return maxDoc; |
| } |
| |
| 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 DelegatingCollector getFilterCollector(IndexSearcher searcher) { |
| try { |
| return new SpatialCollector(new SpatialWeight(searcher)); |
| } catch (IOException e) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e); |
| } |
| } |
| |
| |
| class SpatialCollector extends DelegatingCollector { |
| final SpatialWeight weight; |
| SpatialScorer spatialScorer; |
| int maxdoc; |
| |
| public SpatialCollector(SpatialWeight weight) { |
| this.weight = weight; |
| } |
| |
| @Override |
| public void collect(int doc) throws IOException { |
| spatialScorer.doc = doc; |
| if (spatialScorer.match()) leafDelegate.collect(doc); |
| } |
| |
| @Override |
| protected void doSetNextReader(AtomicReaderContext context) throws IOException { |
| super.doSetNextReader(context); |
| maxdoc = context.reader().maxDoc(); |
| spatialScorer = new SpatialScorer(context, null, weight, 1.0f); |
| } |
| } |
| |
| |
| @Override |
| public Weight createWeight(IndexSearcher searcher) throws IOException { |
| // if we were supposed to use bboxQuery, then we should have been rewritten using that query |
| assert bboxQuery == null; |
| return new SpatialWeight(searcher); |
| } |
| |
| |
| /** Prints a user-readable version of this query. */ |
| @Override |
| public String toString(String field) |
| { |
| float boost = getBoost(); |
| return super.getOptions() + (boost!=1.0?"(":"") + |
| (calcDist ? "geofilt" : "bbox") + "(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 |
| // + (bboxQuery == null ? "" : ",bboxQuery="+bboxQuery) |
| +")" |
| + (boost==1.0 ? "" : ")^"+boost); |
| } |
| |
| |
| /** Returns true if <code>o</code> is equal to this. */ |
| @Override |
| public boolean equals(Object o) { |
| if (!super.equals(o)) 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 unique |
| long hash = Double.doubleToLongBits(latCenter); |
| hash = hash * 31 + Double.doubleToLongBits(lonMin); |
| hash = hash * 31 + (long)super.hashCode(); |
| return (int)(hash >> 32 + hash); |
| } |
| |
| } |
| |
| |