/* * Hibernate Search, full-text search for your domain model * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.search.spatial.impl; import static org.hibernate.search.spatial.impl.CoordinateHelper.coordinate; import java.io.IOException; import java.util.Objects; import org.apache.lucene.index.DocValues; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.NumericDocValues; import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.Scorer; import org.apache.lucene.search.TwoPhaseIterator; import org.apache.lucene.search.Weight; import org.apache.lucene.util.Bits; import org.hibernate.search.spatial.Coordinates; import org.hibernate.search.spatial.SpatialFieldBridgeByRange; /** * Lucene distance Query for documents which have been indexed with {@link SpatialFieldBridgeByRange} * Use double lat,long field in the index from a Coordinates field declaration * * @author Nicolas Helleringer * @see org.hibernate.search.spatial.SpatialFieldBridgeByHash * @see org.hibernate.search.spatial.SpatialFieldBridgeByRange * @see org.hibernate.search.spatial.Coordinates */ public final class DistanceQuery extends Query { private final Query approximationQuery; private final Point center; private final double radius; private final String coordinatesField; private final String latitudeField; private final String longitudeField; /** * Construct a distance query to match document distant at most of radius from center Point * * @param approximationQuery an approximation for this distance query * (i.e. a query that produces no false-negatives, but may produce false-positives), or {@code null}. * If non-null, only documents returned by the approximation query will be considered, * which will enhance performance. * @param centerCoordinates center of the search perimeter * @param radius radius of the search perimeter * @param coordinatesField name of the field implementing Coordinates * @see org.hibernate.search.spatial.Coordinates */ public DistanceQuery(Query approximationQuery, Coordinates centerCoordinates, double radius, String coordinatesField) { this( approximationQuery, centerCoordinates, radius, coordinatesField, null, null ); } /** * Construct a distance query to match document distant at most of radius from center Point * * @param approximationQuery an approximation for this distance query * (i.e. a query that produces no false-negatives, but may produce false-positives), or {@code null}. * If non-null, only documents returned by the approximation query will be considered, * which will enhance performance. * @param centerCoordinates center of the search perimeter * @param radius radius of the search perimeter * @param latitudeField name of the field hosting latitude * @param longitudeField name of the field hosting longitude * @see org.hibernate.search.spatial.Coordinates */ public DistanceQuery(Query approximationQuery, Coordinates centerCoordinates, double radius, String latitudeField, String longitudeField) { this( approximationQuery, centerCoordinates, radius, null, latitudeField, longitudeField ); } private DistanceQuery(Query approximationQuery, Coordinates centerCoordinates, double radius, String coordinatesField, String latitudeField, String longitudeField) { if ( approximationQuery == null ) { this.approximationQuery = new MatchAllDocsQuery(); } else { this.approximationQuery = approximationQuery; } this.center = Point.fromCoordinates( centerCoordinates ); this.radius = radius; this.coordinatesField = coordinatesField; this.latitudeField = latitudeField; this.longitudeField = longitudeField; } @Override public Query rewrite(IndexReader reader) throws IOException { Query superRewritten = super.rewrite( reader ); if ( superRewritten != this ) { return superRewritten; } Query rewrittenApproximationQuery = approximationQuery.rewrite( reader ); if ( rewrittenApproximationQuery != approximationQuery ) { DistanceQuery clone = new DistanceQuery( rewrittenApproximationQuery, this.center, this.radius, this.coordinatesField, this.latitudeField, this.longitudeField ); return clone; } return this; } @Override public Weight createWeight(IndexSearcher searcher, boolean needsScores) throws IOException { Weight approximationWeight = approximationQuery.createWeight( searcher, needsScores ); return new ConstantScoreWeight( this ) { @Override public Scorer scorer(LeafReaderContext context) throws IOException { Scorer approximationScorer = approximationWeight.scorer( context ); if ( approximationScorer == null ) { // No result return null; } DocIdSetIterator approximation = approximationScorer.iterator(); TwoPhaseIterator iterator = createDocIdSetIterator( approximation, context ); return new ConstantScoreScorer( this, score(), iterator ); } }; } /** * Returns a {@link TwoPhaseIterator} that will first check the {@link #approximationQuery} (if any), * and will only match documents whose coordinates are within distance(radius) of the center of the search. * * @param approximation an approximation of matching documents. * @param context the {@link LeafReaderContext} for which to return the {LeafReaderContext}. * * @return a {@link TwoPhaseIterator} with the matching document ids */ private TwoPhaseIterator createDocIdSetIterator(DocIdSetIterator approximation, LeafReaderContext context) throws IOException { return new TwoPhaseIterator( approximation ) { private Bits docsWithLatitude; private Bits docsWithLongitude; private NumericDocValues latitudeValues; private NumericDocValues longitudeValues; private void lazyInit() throws IOException { if ( docsWithLatitude != null ) { return; } LeafReader atomicReader = context.reader(); this.docsWithLatitude = DocValues.getDocsWithField( atomicReader, getLatitudeField() ); this.docsWithLongitude = DocValues.getDocsWithField( atomicReader, getLongitudeField() ); this.latitudeValues = DocValues.getNumeric( atomicReader, getLatitudeField() ); this.longitudeValues = DocValues.getNumeric( atomicReader, getLongitudeField() ); } @Override public boolean matches() throws IOException { lazyInit(); int docID = approximation().docID(); if ( docsWithLatitude.get( docID ) && docsWithLongitude.get( docID ) ) { double lat = coordinate( latitudeValues, docID ); double lon = coordinate( longitudeValues, docID ); if ( center.getDistanceTo( lat, lon ) <= radius ) { return true; } } return false; } @Override public float matchCost() { /* * I honestly have no idea how many "simple operations" we're performing here. * I suppose sines and cosines are very low-level, probably assembly instructions * on most architectures. * Some Lucene implementations seem to use 100 as a default, so let's do the same. */ return 100; } }; } public String getCoordinatesField() { if ( coordinatesField != null ) { return coordinatesField; } else { return SpatialHelper.stripSpatialFieldSuffix( latitudeField ); } } public double getRadius() { return radius; } public Point getCenter() { return center; } public Query getApproximationQuery() { return approximationQuery; } private String getLatitudeField() { if ( latitudeField != null ) { return latitudeField; } else { return SpatialHelper.formatLatitude( coordinatesField ); } } private String getLongitudeField() { if ( longitudeField != null ) { return longitudeField; } else { return SpatialHelper.formatLongitude( coordinatesField ); } } @Override public int hashCode() { int hashCode = 31 * super.hashCode() + approximationQuery.hashCode(); hashCode = 31 * hashCode + center.hashCode(); hashCode = 31 * hashCode + Double.hashCode( radius ); hashCode = 31 * hashCode + Objects.hashCode( coordinatesField ); hashCode = 31 * hashCode + Objects.hashCode( latitudeField ); hashCode = 31 * hashCode + Objects.hashCode( longitudeField ); return hashCode; } @Override public boolean equals(Object obj) { if ( obj == this ) { return true; } if ( obj instanceof DistanceQuery ) { DistanceQuery other = (DistanceQuery) obj; return Float.floatToIntBits( getBoost() ) == Float.floatToIntBits( other.getBoost() ) && approximationQuery.equals( other.approximationQuery ) && center.equals( other.center ) && radius == other.radius && Objects.equals( coordinatesField, other.coordinatesField ) && Objects.equals( latitudeField, other.latitudeField ) && Objects.equals( longitudeField, other.longitudeField ); } return false; } @Override public String toString(String field) { final StringBuilder sb = new StringBuilder(); sb.append( "DistanceQuery" ); sb.append( "{approximationQuery=" ).append( approximationQuery ); sb.append( ", center=" ).append( center ); sb.append( ", radius=" ).append( radius ); if ( coordinatesField != null ) { sb.append( ", coordinatesField='" ).append( coordinatesField ).append( '\'' ); } else { sb.append( ", latitudeField=" ).append( latitudeField ); sb.append( ", longitudeField=" ).append( longitudeField ).append( '\'' ); } sb.append( '}' ); return sb.toString(); } }