mirror of https://github.com/apache/lucene.git
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:
parent
ba3292798f
commit
06ef8111bd
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -100,4 +100,8 @@ final class GeoPointDistanceQueryImpl extends GeoPointInBBoxQueryImpl {
|
|||
result = 31 * result + query.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
public double getRadius() {
|
||||
return query.getRadius();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
query = new GeoPointDistanceQuery(FIELD_NAME, centerLon, centerLat, radius);
|
||||
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,7 +600,12 @@ public class TestGeoPointQuery extends LuceneTestCase {
|
|||
lonB = GeoUtils.mortonUnhashLon(hashedPtB);
|
||||
latB = GeoUtils.mortonUnhashLat(hashedPtB);
|
||||
|
||||
return (SloppyMath.haversin(latA, lonA, latB, lonB)*1000.0 <= radius);
|
||||
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) {
|
||||
|
|
Loading…
Reference in New Issue