LUCENE-6778: add GeoPointDistanceRangeQuery

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1709476 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Michael McCandless 2015-10-19 20:40:57 +00:00
parent ba3292798f
commit 06ef8111bd
6 changed files with 164 additions and 15 deletions

View File

@ -94,6 +94,10 @@ New Features
* LUCENE-6844: PayloadScoreQuery can include or exclude underlying span scores
from its score calculations (Bill Bell, Alan Woodward)
* LUCENE-6778: Add GeoPointDistanceRangeQuery, to search for points
within a "ring" (beyond a minimum distance and below a maximum
distance) (Nick Knize via Mike McCandless)
API Changes
* LUCENE-6590: Query.setBoost(), Query.getBoost() and Query.clone() are gone.

View File

@ -39,7 +39,7 @@ import org.apache.lucene.util.GeoUtils;
*
* @lucene.experimental
*/
public final class GeoPointDistanceQuery extends GeoPointInBBoxQuery {
public class GeoPointDistanceQuery extends GeoPointInBBoxQuery {
protected final double centerLon;
protected final double centerLat;
protected final double radius;

View File

@ -100,4 +100,8 @@ final class GeoPointDistanceQueryImpl extends GeoPointInBBoxQueryImpl {
result = 31 * result + query.hashCode();
return result;
}
public double getRadius() {
return query.getRadius();
}
}

View File

@ -0,0 +1,114 @@
package org.apache.lucene.search;
/*
* 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.util.List;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.util.GeoProjectionUtils;
/** Implements a point distance range query on a GeoPoint field. This is based on
* {@code org.apache.lucene.search.GeoPointDistanceQuery} and is implemented using a
* {@code org.apache.lucene.search.BooleanClause.MUST_NOT} clause to exclude any points that fall within
* minRadius from the provided point.
*
* @lucene.experimental
*/
public final class GeoPointDistanceRangeQuery extends GeoPointDistanceQuery {
protected final double minRadius;
public GeoPointDistanceRangeQuery(final String field, final double centerLon, final double centerLat,
final double minRadius, final double maxRadius) {
super(field, centerLon, centerLat, maxRadius);
this.minRadius = minRadius;
}
@Override
public Query rewrite(IndexReader reader) {
Query q = super.rewrite(reader);
if (minRadius == 0.0) {
return q;
}
final double radius;
if (q instanceof BooleanQuery) {
final List<BooleanClause> clauses = ((BooleanQuery)q).clauses();
assert clauses.size() > 0;
radius = ((GeoPointDistanceQueryImpl)(clauses.get(0).getQuery())).getRadius();
} else {
radius = ((GeoPointDistanceQueryImpl)q).getRadius();
}
// add an exclusion query
BooleanQuery.Builder bqb = new BooleanQuery.Builder();
// create a new exclusion query
GeoPointDistanceQuery exclude = new GeoPointDistanceQuery(field, centerLon, centerLat, minRadius);
// full map search
if (radius >= GeoProjectionUtils.SEMIMINOR_AXIS) {
bqb.add(new BooleanClause(new GeoPointInBBoxQuery(this.field, -180.0, -90.0, 180.0, 90.0), BooleanClause.Occur.MUST));
} else {
bqb.add(new BooleanClause(q, BooleanClause.Occur.MUST));
}
bqb.add(new BooleanClause(exclude, BooleanClause.Occur.MUST_NOT));
return bqb.build();
}
@Override
public String toString(String field) {
final StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(':');
if (!this.field.equals(field)) {
sb.append(" field=");
sb.append(this.field);
sb.append(':');
}
return sb.append( " Center: [")
.append(centerLon)
.append(',')
.append(centerLat)
.append(']')
.append(" From Distance: ")
.append(minRadius)
.append(" m")
.append(" To Distance: ")
.append(radius)
.append(" m")
.append(" Lower Left: [")
.append(minLon)
.append(',')
.append(minLat)
.append(']')
.append(" Upper Right: [")
.append(maxLon)
.append(',')
.append(maxLat)
.append("]")
.toString();
}
public double getMinRadiusMeters() {
return this.minRadius;
}
public double getMaxRadiusMeters() {
return this.radius;
}
}

View File

@ -24,10 +24,10 @@ package org.apache.lucene.util;
*/
public class GeoProjectionUtils {
// WGS84 earth-ellipsoid major (a) minor (b) radius, (f) flattening and eccentricity (e)
static final double SEMIMAJOR_AXIS = 6_378_137; // [m]
static final double FLATTENING = 1.0/298.257223563;
static final double SEMIMINOR_AXIS = SEMIMAJOR_AXIS * (1.0 - FLATTENING); //6_356_752.31420; // [m]
static final double ECCENTRICITY = StrictMath.sqrt((2.0 - FLATTENING) * FLATTENING);
public static final double SEMIMAJOR_AXIS = 6_378_137; // [m]
public static final double FLATTENING = 1.0/298.257223563;
public static final double SEMIMINOR_AXIS = SEMIMAJOR_AXIS * (1.0 - FLATTENING); //6_356_752.31420; // [m]
public static final double ECCENTRICITY = StrictMath.sqrt((2.0 - FLATTENING) * FLATTENING);
static final double PI_OVER_2 = StrictMath.PI / 2.0D;
static final double SEMIMAJOR_AXIS2 = SEMIMAJOR_AXIS * SEMIMAJOR_AXIS;
static final double SEMIMINOR_AXIS2 = SEMIMINOR_AXIS * SEMIMINOR_AXIS;

View File

@ -42,6 +42,7 @@ import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.FixedBitSet;
import org.apache.lucene.util.GeoDistanceUtils;
import org.apache.lucene.util.GeoProjectionUtils;
import org.apache.lucene.util.GeoUtils;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.LuceneTestCase;
@ -142,6 +143,12 @@ public class TestGeoPointQuery extends LuceneTestCase {
return searcher.search(q, limit);
}
private TopDocs geoDistanceRangeQuery(double lon, double lat, double minRadius, double maxRadius, int limit)
throws Exception {
GeoPointDistanceRangeQuery q = new GeoPointDistanceRangeQuery(FIELD_NAME, lon, lat, minRadius, maxRadius);
return searcher.search(q, limit);
}
@Test
public void testBBoxQuery() throws Exception {
TopDocs td = bboxQuery(-96.7772, 32.778650, -96.77690000, 32.778950, 5);
@ -247,6 +254,12 @@ public class TestGeoPointQuery extends LuceneTestCase {
throw new Exception("GeoDistanceQuery should not accept invalid lat/lon as origin");
}
@Test
public void testMaxDistanceRangeQuery() throws Exception {
TopDocs td = geoDistanceRangeQuery(0.0, 0.0, 10000, GeoProjectionUtils.SEMIMINOR_AXIS, 20);
assertEquals("GeoDistanceRangeQuery failed", 24, td.totalHits);
}
public void testRandomTiny() throws Exception {
// Make sure single-leaf-node case is OK:
doTestRandom(10);
@ -434,7 +447,6 @@ public class TestGeoPointQuery extends LuceneTestCase {
}
};
} else if (random().nextBoolean()) {
// generate a random bounding box
GeoBoundingBox bbox = randomBBox();
@ -442,13 +454,22 @@ public class TestGeoPointQuery extends LuceneTestCase {
double centerLon = bbox.minLon + ((bbox.maxLon - bbox.minLon)/2.0);
// radius (in meters) as a function of the random generated bbox
final double radius = GeoDistanceUtils.vincentyDistance(centerLon, centerLat, centerLon, bbox.minLat);
//final double radius = SloppyMath.haversin(centerLat, centerLon, bbox.minLat, centerLon)*1000;
final double radius = random().nextDouble() * (0.05 * GeoProjectionUtils.SEMIMINOR_AXIS);
// randomly test range queries
final boolean rangeQuery = random().nextBoolean();
final double radiusMax = (rangeQuery) ? radius + random().nextDouble() * (0.05 * GeoProjectionUtils.SEMIMINOR_AXIS) : 0;
if (VERBOSE) {
System.out.println("\t radius = " + radius);
System.out.println("\t radius = " + radius + ((rangeQuery) ? " : " + radiusMax : ""));
}
// query using the centroid of the bounding box
if (rangeQuery) {
query = new GeoPointDistanceRangeQuery(FIELD_NAME, centerLon, centerLat, radius, radiusMax);
} else {
query = new GeoPointDistanceQuery(FIELD_NAME, centerLon, centerLat, radius);
}
verifyHits = new VerifyHits() {
@Override
@ -456,14 +477,14 @@ public class TestGeoPointQuery extends LuceneTestCase {
if (Double.isNaN(pointLat) || Double.isNaN(pointLon)) {
return null;
}
if (radiusQueryCanBeWrong(centerLat, centerLon, pointLon, pointLat, radius)) {
if (radiusQueryCanBeWrong(centerLat, centerLon, pointLon, pointLat, radius)
|| (rangeQuery && radiusQueryCanBeWrong(centerLat, centerLon, pointLon, pointLat, radiusMax))) {
return null;
} else {
return distanceContainsPt(centerLon, centerLat, pointLon, pointLat, radius);
return distanceContainsPt(centerLon, centerLat, pointLon, pointLat, radius, (rangeQuery) ? radiusMax : 0);
}
}
};
} else {
GeoBoundingBox bbox = randomBBox();
@ -570,7 +591,8 @@ public class TestGeoPointQuery extends LuceneTestCase {
protected abstract Boolean shouldMatch(double lat, double lon);
}
private static boolean distanceContainsPt(double lonA, double latA, double lonB, double latB, final double radius) {
private static boolean distanceContainsPt(double lonA, double latA, double lonB, double latB, final double radius,
final double maxRadius) {
final long hashedPtA = GeoUtils.mortonHash(lonA, latA);
lonA = GeoUtils.mortonUnhashLon(hashedPtA);
latA = GeoUtils.mortonUnhashLat(hashedPtA);
@ -578,9 +600,14 @@ public class TestGeoPointQuery extends LuceneTestCase {
lonB = GeoUtils.mortonUnhashLon(hashedPtB);
latB = GeoUtils.mortonUnhashLat(hashedPtB);
if (maxRadius == 0) {
return (SloppyMath.haversin(latA, lonA, latB, lonB)*1000.0 <= radius);
}
return SloppyMath.haversin(latA, lonA, latB, lonB)*1000.0 >= radius
&& SloppyMath.haversin(latA, lonA, latB, lonB)*1000.0 <= maxRadius;
}
private static boolean rectContainsPointEnc(GeoBoundingBox bbox, double pointLat, double pointLon) {
// We should never see a deleted doc here?
assert Double.isNaN(pointLat) == false;