LUCENE-5714, LUCENE-5779: Enhance BBoxStrategy & Overlap similarity. Configurable docValues / index usage.

Add new ShapeAreaValueSource.

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1608793 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
David Wayne Smiley 2014-07-08 14:15:35 +00:00
parent beeeba528f
commit 7e0aff8dab
12 changed files with 972 additions and 568 deletions

View File

@ -24,9 +24,6 @@ New Features
implemented in the spatial module as DateRangePrefixTree used with
NumberRangePrefixTreeStrategy. (David Smiley)
* LUCENE-4175: Index and search rectangles with spatial BBoxSpatialStrategy.
Sort documents by relative overlap of query areas. (Ryan McKinley)
API Changes
* LUCENE-4535: oal.util.FilterIterator is now an internal API.
@ -103,6 +100,10 @@ New Features
* LUCENE-5801: Added (back) OrdinalMappingAtomicReader for merging search
indexes that contain category ordinals from separate taxonomy indexes.
(Nicola Buso via Shai Erera)
* LUCENE-4175, LUCENE-5714, LUCENE-5779: Index and search rectangles with spatial
BBoxSpatialStrategy using most predicates. Sort documents by relative overlap
of query areas or just by indexed shape area. (Ryan McKinley, David Smiley)
API Changes

View File

@ -1,217 +0,0 @@
/*
* 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.lucene.spatial.bbox;
import org.apache.lucene.search.Explanation;
import com.spatial4j.core.shape.Rectangle;
/**
* The algorithm is implemented as envelope on envelope overlays rather than
* complex polygon on complex polygon overlays.
* <p/>
* <p/>
* Spatial relevance scoring algorithm:
* <p/>
* <br/> queryArea = the area of the input query envelope
* <br/> targetArea = the area of the target envelope (per Lucene document)
* <br/> intersectionArea = the area of the intersection for the query/target envelopes
* <br/> queryPower = the weighting power associated with the query envelope (default = 1.0)
* <br/> targetPower = the weighting power associated with the target envelope (default = 1.0)
* <p/>
* <br/> queryRatio = intersectionArea / queryArea;
* <br/> targetRatio = intersectionArea / targetArea;
* <br/> queryFactor = Math.pow(queryRatio,queryPower);
* <br/> targetFactor = Math.pow(targetRatio,targetPower);
* <br/> score = queryFactor * targetFactor;
* <p/>
* Based on Geoportal's
* <a href="http://geoportal.svn.sourceforge.net/svnroot/geoportal/Geoportal/trunk/src/com/esri/gpt/catalog/lucene/SpatialRankingValueSource.java">
* SpatialRankingValueSource</a>.
*
* @lucene.experimental
*/
public class AreaSimilarity implements BBoxSimilarity {
/**
* Properties associated with the query envelope
*/
private final Rectangle queryExtent;
private final double queryArea;
private final double targetPower;
private final double queryPower;
public AreaSimilarity(Rectangle queryExtent, double queryPower, double targetPower) {
this.queryExtent = queryExtent;
this.queryArea = queryExtent.getArea(null);
this.queryPower = queryPower;
this.targetPower = targetPower;
// if (this.qryMinX > queryExtent.getMaxX()) {
// this.qryCrossedDateline = true;
// this.qryArea = Math.abs(qryMaxX + 360.0 - qryMinX) * Math.abs(qryMaxY - qryMinY);
// } else {
// this.qryArea = Math.abs(qryMaxX - qryMinX) * Math.abs(qryMaxY - qryMinY);
// }
}
public AreaSimilarity(Rectangle queryExtent) {
this(queryExtent, 2.0, 0.5);
}
public String getDelimiterQueryParameters() {
return queryExtent.toString() + ";" + queryPower + ";" + targetPower;
}
@Override
public double score(Rectangle target, Explanation exp) {
if (target == null || queryArea <= 0) {
return 0;
}
double targetArea = target.getArea(null);
if (targetArea <= 0) {
return 0;
}
double score = 0;
double top = Math.min(queryExtent.getMaxY(), target.getMaxY());
double bottom = Math.max(queryExtent.getMinY(), target.getMinY());
double height = top - bottom;
double width = 0;
// queries that cross the date line
if (queryExtent.getCrossesDateLine()) {
// documents that cross the date line
if (target.getCrossesDateLine()) {
double left = Math.max(queryExtent.getMinX(), target.getMinX());
double right = Math.min(queryExtent.getMaxX(), target.getMaxX());
width = right + 360.0 - left;
} else {
double qryWestLeft = Math.max(queryExtent.getMinX(), target.getMaxX());
double qryWestRight = Math.min(target.getMaxX(), 180.0);
double qryWestWidth = qryWestRight - qryWestLeft;
if (qryWestWidth > 0) {
width = qryWestWidth;
} else {
double qryEastLeft = Math.max(target.getMaxX(), -180.0);
double qryEastRight = Math.min(queryExtent.getMaxX(), target.getMaxX());
double qryEastWidth = qryEastRight - qryEastLeft;
if (qryEastWidth > 0) {
width = qryEastWidth;
}
}
}
} else { // queries that do not cross the date line
if (target.getCrossesDateLine()) {
double tgtWestLeft = Math.max(queryExtent.getMinX(), target.getMinX());
double tgtWestRight = Math.min(queryExtent.getMaxX(), 180.0);
double tgtWestWidth = tgtWestRight - tgtWestLeft;
if (tgtWestWidth > 0) {
width = tgtWestWidth;
} else {
double tgtEastLeft = Math.max(queryExtent.getMinX(), -180.0);
double tgtEastRight = Math.min(queryExtent.getMaxX(), target.getMaxX());
double tgtEastWidth = tgtEastRight - tgtEastLeft;
if (tgtEastWidth > 0) {
width = tgtEastWidth;
}
}
} else {
double left = Math.max(queryExtent.getMinX(), target.getMinX());
double right = Math.min(queryExtent.getMaxX(), target.getMaxX());
width = right - left;
}
}
// calculate the score
if ((width > 0) && (height > 0)) {
double intersectionArea = width * height;
double queryRatio = intersectionArea / queryArea;
double targetRatio = intersectionArea / targetArea;
double queryFactor = Math.pow(queryRatio, queryPower);
double targetFactor = Math.pow(targetRatio, targetPower);
score = queryFactor * targetFactor * 10000.0;
if (exp!=null) {
// StringBuilder sb = new StringBuilder();
// sb.append("\nscore=").append(score);
// sb.append("\n query=").append();
// sb.append("\n target=").append(target.toString());
// sb.append("\n intersectionArea=").append(intersectionArea);
//
// sb.append(" queryArea=").append(queryArea).append(" targetArea=").append(targetArea);
// sb.append("\n queryRatio=").append(queryRatio).append(" targetRatio=").append(targetRatio);
// sb.append("\n queryFactor=").append(queryFactor).append(" targetFactor=").append(targetFactor);
// sb.append(" (queryPower=").append(queryPower).append(" targetPower=").append(targetPower).append(")");
exp.setValue((float)score);
exp.setDescription(this.getClass().getSimpleName());
Explanation e = null;
exp.addDetail( e = new Explanation((float)intersectionArea, "IntersectionArea") );
e.addDetail(new Explanation((float)width, "width; Query: "+queryExtent.toString()));
e.addDetail(new Explanation((float)height, "height; Target: "+target.toString()));
exp.addDetail( e = new Explanation((float)queryFactor, "Query") );
e.addDetail(new Explanation((float)queryArea, "area"));
e.addDetail(new Explanation((float)queryRatio, "ratio"));
e.addDetail(new Explanation((float)queryPower, "power"));
exp.addDetail( e = new Explanation((float)targetFactor, "Target") );
e.addDetail(new Explanation((float)targetArea, "area"));
e.addDetail(new Explanation((float)targetRatio, "ratio"));
e.addDetail(new Explanation((float)targetPower, "power"));
}
}
else if(exp !=null) {
exp.setValue(0);
exp.setDescription("Shape does not intersect");
}
return score;
}
/**
* Determines if this ValueSource is equal to another.
*
* @param o the ValueSource to compare
* @return <code>true</code> if the two objects are based upon the same query envelope
*/
@Override
public boolean equals(Object o) {
if (o.getClass() != AreaSimilarity.class)
return false;
AreaSimilarity other = (AreaSimilarity) o;
return getDelimiterQueryParameters().equals(other.getDelimiterQueryParameters());
}
/**
* Returns the ValueSource hash code.
*
* @return the hash code
*/
@Override
public int hashCode() {
return getDelimiterQueryParameters().hashCode();
}
}

View File

@ -0,0 +1,244 @@
/*
* 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.lucene.spatial.bbox;
import com.spatial4j.core.shape.Rectangle;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.Explanation;
/**
* The algorithm is implemented as envelope on envelope (rect on rect) overlays rather than
* complex polygon on complex polygon overlays.
* <p/>
* <p/>
* Spatial relevance scoring algorithm:
* <DL>
* <DT>queryArea</DT> <DD>the area of the input query envelope</DD>
* <DT>targetArea</DT> <DD>the area of the target envelope (per Lucene document)</DD>
* <DT>intersectionArea</DT> <DD>the area of the intersection between the query and target envelopes</DD>
* <DT>queryTargetProportion</DT> <DD>A 0-1 factor that divides the score proportion between query and target.
* 0.5 is evenly.</DD>
*
* <DT>queryRatio</DT> <DD>intersectionArea / queryArea; (see note)</DD>
* <DT>targetRatio</DT> <DD>intersectionArea / targetArea; (see note)</DD>
* <DT>queryFactor</DT> <DD>queryRatio * queryTargetProportion;</DD>
* <DT>targetFactor</DT> <DD>targetRatio * (1 - queryTargetProportion);</DD>
* <DT>score</DT> <DD>queryFactor + targetFactor;</DD>
* </DL>
* Additionally, note that an optional minimum side length {@code minSideLength} may be used whenever an
* area is calculated (queryArea, targetArea, intersectionArea). This allows for points or horizontal/vertical lines
* to be used as the query shape and in such case the descending order should have smallest boxes up front. Without
* this, a point or line query shape typically scores everything with the same value since there is 0 area.
* <p />
* Note: The actual computation of queryRatio and targetRatio is more complicated so that it considers
* points and lines. Lines have the ratio of overlap, and points are either 1.0 or 0.0 depending on whether
* it intersects or not.
* <p />
* Originally based on Geoportal's
* <a href="http://geoportal.svn.sourceforge.net/svnroot/geoportal/Geoportal/trunk/src/com/esri/gpt/catalog/lucene/SpatialRankingValueSource.java">
* SpatialRankingValueSource</a> but modified quite a bit. GeoPortal's algorithm will yield a score of 0
* if either a line or point is compared, and it's doesn't output a 0-1 normalized score (it multiplies the factors),
* and it doesn't support minSideLength, and it had dateline bugs.
*
* @lucene.experimental
*/
public class BBoxOverlapRatioValueSource extends BBoxSimilarityValueSource {
private final boolean isGeo;//-180/+180 degrees (not part of identity; attached to parent strategy/field)
private final Rectangle queryExtent;
private final double queryArea;//not part of identity
private final double minSideLength;
private final double queryTargetProportion;
//TODO option to compute geodetic area
/**
*
* @param rectValueSource mandatory; source of rectangles
* @param isGeo True if ctx.isGeo() and thus dateline issues should be attended to
* @param queryExtent mandatory; the query rectangle
* @param queryTargetProportion see class javadocs. Between 0 and 1.
* @param minSideLength see class javadocs. 0.0 will effectively disable.
*/
public BBoxOverlapRatioValueSource(ValueSource rectValueSource, boolean isGeo, Rectangle queryExtent,
double queryTargetProportion, double minSideLength) {
super(rectValueSource);
this.isGeo = isGeo;
this.minSideLength = minSideLength;
this.queryExtent = queryExtent;
this.queryArea = calcArea(queryExtent.getWidth(), queryExtent.getHeight());
assert queryArea >= 0;
this.queryTargetProportion = queryTargetProportion;
if (queryTargetProportion < 0 || queryTargetProportion > 1.0)
throw new IllegalArgumentException("queryTargetProportion must be >= 0 and <= 1");
}
/** Construct with 75% weighting towards target (roughly GeoPortal's default), geo degrees assumed, no
* minimum side length. */
public BBoxOverlapRatioValueSource(ValueSource rectValueSource, Rectangle queryExtent) {
this(rectValueSource, true, queryExtent, 0.25, 0.0);
}
@Override
public boolean equals(Object o) {
if (!super.equals(o)) return false;
BBoxOverlapRatioValueSource that = (BBoxOverlapRatioValueSource) o;
if (Double.compare(that.minSideLength, minSideLength) != 0) return false;
if (Double.compare(that.queryTargetProportion, queryTargetProportion) != 0) return false;
if (!queryExtent.equals(that.queryExtent)) return false;
return true;
}
@Override
public int hashCode() {
int result = super.hashCode();
long temp;
result = 31 * result + queryExtent.hashCode();
temp = Double.doubleToLongBits(minSideLength);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(queryTargetProportion);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
@Override
protected String similarityDescription() {
return queryExtent.toString() + "," + queryTargetProportion;
}
@Override
protected double score(Rectangle target, Explanation exp) {
// calculate "height": the intersection height between two boxes.
double top = Math.min(queryExtent.getMaxY(), target.getMaxY());
double bottom = Math.max(queryExtent.getMinY(), target.getMinY());
double height = top - bottom;
if (height < 0)
return 0;//no intersection
// calculate "width": the intersection width between two boxes.
double width = 0;
{
Rectangle a = queryExtent;
Rectangle b = target;
if (a.getCrossesDateLine() == b.getCrossesDateLine()) {
//both either cross or don't
double left = Math.max(a.getMinX(), b.getMinX());
double right = Math.min(a.getMaxX(), b.getMaxX());
if (!a.getCrossesDateLine()) {//both don't
if (left <= right) {
width = right - left;
} else if (isGeo && (Math.abs(a.getMinX()) == 180 || Math.abs(a.getMaxX()) == 180)
&& (Math.abs(b.getMinX()) == 180 || Math.abs(b.getMaxX()) == 180)) {
width = 0;//both adjacent to dateline
} else {
return 0;//no intersection
}
} else {//both cross
width = right - left + 360;
}
} else {
if (!a.getCrossesDateLine()) {//then flip
a = target;
b = queryExtent;
}
//a crosses, b doesn't
double qryWestLeft = Math.max(a.getMinX(), b.getMinX());
double qryWestRight = b.getMaxX();
if (qryWestLeft < qryWestRight)
width += qryWestRight - qryWestLeft;
double qryEastLeft = b.getMinX();
double qryEastRight = Math.min(a.getMaxX(), b.getMaxX());
if (qryEastLeft < qryEastRight)
width += qryEastRight - qryEastLeft;
if (qryWestLeft > qryWestRight && qryEastLeft > qryEastRight)
return 0;//no intersection
}
}
// calculate queryRatio and targetRatio
double intersectionArea = calcArea(width, height);
double queryRatio;
if (queryArea > 0) {
queryRatio = intersectionArea / queryArea;
} else if (queryExtent.getHeight() > 0) {//vert line
queryRatio = height / queryExtent.getHeight();
} else if (queryExtent.getWidth() > 0) {//horiz line
queryRatio = width / queryExtent.getWidth();
} else {
queryRatio = queryExtent.relate(target).intersects() ? 1 : 0;//could be optimized
}
double targetArea = calcArea(target.getWidth(), target.getHeight());
assert targetArea >= 0;
double targetRatio;
if (targetArea > 0) {
targetRatio = intersectionArea / targetArea;
} else if (target.getHeight() > 0) {//vert line
targetRatio = height / target.getHeight();
} else if (target.getWidth() > 0) {//horiz line
targetRatio = width / target.getWidth();
} else {
targetRatio = target.relate(queryExtent).intersects() ? 1 : 0;//could be optimized
}
assert queryRatio >= 0 && queryRatio <= 1 : queryRatio;
assert targetRatio >= 0 && targetRatio <= 1 : targetRatio;
// combine ratios into a score
double queryFactor = queryRatio * queryTargetProportion;
double targetFactor = targetRatio * (1.0 - queryTargetProportion);
double score = queryFactor + targetFactor;
if (exp!=null) {
exp.setValue((float)score);
exp.setDescription(this.getClass().getSimpleName()+": queryFactor + targetFactor");
Explanation e;//tmp
String minSideDesc = minSideLength > 0.0 ? " (minSide="+minSideLength+")" : "";
exp.addDetail( e = new Explanation((float)intersectionArea, "IntersectionArea" + minSideDesc));
e.addDetail(new Explanation((float)width, "width"));
e.addDetail(new Explanation((float)height, "height"));
e.addDetail(new Explanation((float)queryTargetProportion, "queryTargetProportion"));
exp.addDetail( e = new Explanation((float)queryFactor, "queryFactor"));
e.addDetail(new Explanation((float)queryRatio, "ratio"));
e.addDetail(new Explanation((float)queryArea, "area of " + queryExtent + minSideDesc));
exp.addDetail( e = new Explanation((float)targetFactor, "targetFactor"));
e.addDetail(new Explanation((float)targetRatio, "ratio"));
e.addDetail(new Explanation((float)targetArea, "area of " + target + minSideDesc));
}
return score;
}
/** Calculates the area while applying the minimum side length. */
private double calcArea(double width, double height) {
return Math.max(minSideLength, width) * Math.max(minSideLength, height);
}
}

View File

@ -1,31 +0,0 @@
/*
* 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.lucene.spatial.bbox;
import org.apache.lucene.search.Explanation;
import com.spatial4j.core.shape.Rectangle;
/**
* Abstraction of the calculation used to determine how similar two Bounding Boxes are.
*
* @lucene.experimental
*/
public interface BBoxSimilarity {
public double score(Rectangle extent, Explanation exp);
}

View File

@ -18,119 +18,99 @@ package org.apache.lucene.spatial.bbox;
*/
import com.spatial4j.core.shape.Rectangle;
import org.apache.lucene.index.AtomicReader;
import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.NumericDocValues;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.docvalues.DoubleDocValues;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.util.Bits;
import org.apache.lucene.search.IndexSearcher;
import java.io.IOException;
import java.util.Map;
/**
* An implementation of the Lucene ValueSource model to support spatial relevance ranking.
* A base class for calculating a spatial relevance rank per document from a provided
* {@link ValueSource} in which {@link FunctionValues#objectVal(int)} returns a {@link
* com.spatial4j.core.shape.Rectangle}.
* <p/>
* Implementers: remember to implement equals & hashCode if you have
* fields!
*
* @lucene.experimental
*/
public class BBoxSimilarityValueSource extends ValueSource {
public abstract class BBoxSimilarityValueSource extends ValueSource {
private final BBoxStrategy strategy;
private final BBoxSimilarity similarity;
private final ValueSource bboxValueSource;
public BBoxSimilarityValueSource(BBoxStrategy strategy, BBoxSimilarity similarity) {
this.strategy = strategy;
this.similarity = similarity;
public BBoxSimilarityValueSource(ValueSource bboxValueSource) {
this.bboxValueSource = bboxValueSource;
}
@Override
public void createWeight(Map context, IndexSearcher searcher) throws IOException {
bboxValueSource.createWeight(context, searcher);
}
/**
* Returns the ValueSource description.
*
* @return the description
*/
@Override
public String description() {
return "BBoxSimilarityValueSource(" + similarity + ")";
return getClass().getSimpleName()+"(" + bboxValueSource.description() + "," + similarityDescription() + ")";
}
/** A comma-separated list of configurable items of the subclass to put into {@link #description()}. */
protected abstract String similarityDescription();
/**
* Returns the DocValues used by the function query.
*
* @param readerContext the AtomicReaderContext which holds an AtomicReader
* @return the values
*/
@Override
public FunctionValues getValues(Map context, AtomicReaderContext readerContext) throws IOException {
AtomicReader reader = readerContext.reader();
final NumericDocValues minX = DocValues.getNumeric(reader, strategy.field_minX);
final NumericDocValues minY = DocValues.getNumeric(reader, strategy.field_minY);
final NumericDocValues maxX = DocValues.getNumeric(reader, strategy.field_maxX);
final NumericDocValues maxY = DocValues.getNumeric(reader, strategy.field_maxY);
final Bits validMinX = DocValues.getDocsWithField(reader, strategy.field_minX);
final Bits validMaxX = DocValues.getDocsWithField(reader, strategy.field_maxX);
final FunctionValues shapeValues = bboxValueSource.getValues(context, readerContext);
return new FunctionValues() {
//reused
Rectangle rect = strategy.getSpatialContext().makeRectangle(0,0,0,0);
return new DoubleDocValues(this) {
@Override
public double doubleVal(int doc) {
//? limit to Rect or call getBoundingBox()? latter would encourage bad practice
final Rectangle rect = (Rectangle) shapeValues.objectVal(doc);
return rect==null ? 0 : score(rect, null);
}
@Override
public float floatVal(int doc) {
double minXVal = Double.longBitsToDouble(minX.get(doc));
double maxXVal = Double.longBitsToDouble(maxX.get(doc));
// make sure it has minX and area
if ((minXVal != 0 || validMinX.get(doc)) && (maxXVal != 0 || validMaxX.get(doc))) {
rect.reset(
minXVal, maxXVal,
Double.longBitsToDouble(minY.get(doc)), Double.longBitsToDouble(maxY.get(doc)));
return (float) similarity.score(rect, null);
} else {
return (float) similarity.score(null, null);
}
public boolean exists(int doc) {
return shapeValues.exists(doc);
}
@Override
public Explanation explain(int doc) {
// make sure it has minX and area
if (validMinX.get(doc) && validMaxX.get(doc)) {
rect.reset(
Double.longBitsToDouble(minX.get(doc)), Double.longBitsToDouble(maxX.get(doc)),
Double.longBitsToDouble(minY.get(doc)), Double.longBitsToDouble(maxY.get(doc)));
Explanation exp = new Explanation();
similarity.score(rect, exp);
return exp;
}
return new Explanation(0, "No BBox");
}
@Override
public String toString(int doc) {
return description() + "=" + floatVal(doc);
final Rectangle rect = (Rectangle) shapeValues.objectVal(doc);
if (rect == null)
return new Explanation(0, "no rect");
Explanation exp = new Explanation();
score(rect, exp);
return exp;
}
};
}
/**
* Determines if this ValueSource is equal to another.
*
* @param o the ValueSource to compare
* @return <code>true</code> if the two objects are based upon the same query envelope
* Return a relevancy score. If {@code exp} is provided then diagnostic information is added.
* @param rect The indexed rectangle; not null.
* @param exp Optional diagnostic holder.
* @return a score.
*/
protected abstract double score(Rectangle rect, Explanation exp);
@Override
public boolean equals(Object o) {
if (o.getClass() != BBoxSimilarityValueSource.class) {
return false;
}
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;//same class
BBoxSimilarityValueSource other = (BBoxSimilarityValueSource) o;
return similarity.equals(other.similarity);
BBoxSimilarityValueSource that = (BBoxSimilarityValueSource) o;
if (!bboxValueSource.equals(that.bboxValueSource)) return false;
return true;
}
@Override
public int hashCode() {
return BBoxSimilarityValueSource.class.hashCode() + similarity.hashCode();
return bboxValueSource.hashCode();
}
}

View File

@ -26,8 +26,8 @@ import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.AtomicReader;
import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.Term;
import org.apache.lucene.queries.function.FunctionQuery;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
@ -41,6 +41,9 @@ import org.apache.lucene.spatial.SpatialStrategy;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.spatial.query.UnsupportedSpatialOperation;
import org.apache.lucene.spatial.util.DistanceToShapeValueSource;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.NumericUtils;
/**
@ -62,7 +65,7 @@ import org.apache.lucene.spatial.query.UnsupportedSpatialOperation;
* and a boolean to mark a dateline cross. Depending on the particular {@link
* SpatialOperation}s, there is a variety of {@link NumericRangeQuery}s to be
* done.
* The {@link #makeBBoxAreaSimilarityValueSource(com.spatial4j.core.shape.Rectangle)}
* The {@link #makeOverlapRatioValueSource(com.spatial4j.core.shape.Rectangle, double)}
* works by calculating the query bbox overlap percentage against the indexed
* shape overlap percentage. The indexed shape's coordinates are retrieved from
* {@link AtomicReader#getNumericDocValues}
@ -77,20 +80,22 @@ public class BBoxStrategy extends SpatialStrategy {
public static final String SUFFIX_MAXY = "__maxY";
public static final String SUFFIX_XDL = "__xdl";
private static BytesRef T_BYTES = new BytesRef("T");//same as Solr BoolField
private static BytesRef F_BYTES = new BytesRef("F");//same as Solr BoolField
/*
* The Bounding Box gets stored as four fields for x/y min/max and a flag
* that says if the box crosses the dateline (xdl).
*/
public final String field_bbox;
public final String field_minX;
public final String field_minY;
public final String field_maxX;
public final String field_maxY;
public final String field_xdl; // crosses dateline
protected final String field_bbox;
protected final String field_minX;
protected final String field_minY;
protected final String field_maxX;
protected final String field_maxY;
protected final String field_xdl; // crosses dateline
public double queryPower = 1.0;
public double targetPower = 1.0f;
public int precisionStep = 8; // same as solr default
protected FieldType fieldType;//for the 4 numbers
protected FieldType xdlFieldType;
public BBoxStrategy(SpatialContext ctx, String fieldNamePrefix) {
super(ctx, fieldNamePrefix);
@ -100,12 +105,32 @@ public class BBoxStrategy extends SpatialStrategy {
field_minY = fieldNamePrefix + SUFFIX_MINY;
field_maxY = fieldNamePrefix + SUFFIX_MAXY;
field_xdl = fieldNamePrefix + SUFFIX_XDL;
FieldType fieldType = new FieldType(DoubleField.TYPE_NOT_STORED);
fieldType.setNumericPrecisionStep(8);//Solr's default
fieldType.setDocValueType(FieldInfo.DocValuesType.NUMERIC);
setFieldType(fieldType);
}
public void setPrecisionStep( int p ) {
precisionStep = p;
if (precisionStep<=0 || precisionStep>=64)
precisionStep=Integer.MAX_VALUE;
private int getPrecisionStep() {
return fieldType.numericPrecisionStep();
}
public FieldType getFieldType() {
return fieldType;
}
public void setFieldType(FieldType fieldType) {
fieldType.freeze();
this.fieldType = fieldType;
//only double's supported right now
if (fieldType.numericType() != FieldType.NumericType.DOUBLE)
throw new IllegalArgumentException("BBoxStrategy only supports doubles at this time.");
//for xdlFieldType, copy some similar options. Don't do docValues since it isn't needed here.
xdlFieldType = new FieldType(StringField.TYPE_NOT_STORED);
xdlFieldType.setStored(fieldType.stored());
xdlFieldType.setIndexed(fieldType.indexed());
xdlFieldType.freeze();
}
//---------------------------------
@ -119,33 +144,70 @@ public class BBoxStrategy extends SpatialStrategy {
throw new UnsupportedOperationException("Can only index a Rectangle, not " + shape);
}
/** Field subclass circumventing Field limitations. This one instance can optionally index, store,
* or have DocValues.
*/
private static class ComboField extends Field {
private ComboField(String name, Object value, FieldType type) {
super(name, type);//this expert constructor allows us to have a field that has docValues & indexed
super.fieldsData = value;
}
//Is this a hack? We assume that numericValue() is only called for DocValues purposes.
@Override
public Number numericValue() {
//Numeric DocValues only supports Long,
final Number number = super.numericValue();
if (number == null)
return null;
if (fieldType().numericType() == FieldType.NumericType.DOUBLE)
return Double.doubleToLongBits(number.doubleValue());
if (fieldType().numericType() == FieldType.NumericType.FLOAT)
return Float.floatToIntBits(number.floatValue());
return number.longValue();
}
}
public Field[] createIndexableFields(Rectangle bbox) {
FieldType doubleFieldType = new FieldType(DoubleField.TYPE_NOT_STORED);
doubleFieldType.setNumericPrecisionStep(precisionStep);
Field[] fields = new Field[5];
fields[0] = new DoubleField(field_minX, bbox.getMinX(), doubleFieldType);
fields[1] = new DoubleField(field_maxX, bbox.getMaxX(), doubleFieldType);
fields[2] = new DoubleField(field_minY, bbox.getMinY(), doubleFieldType);
fields[3] = new DoubleField(field_maxY, bbox.getMaxY(), doubleFieldType);
fields[4] = new Field( field_xdl, bbox.getCrossesDateLine()?"T":"F", StringField.TYPE_NOT_STORED);
fields[0] = new ComboField(field_minX, bbox.getMinX(), fieldType);
fields[1] = new ComboField(field_maxX, bbox.getMaxX(), fieldType);
fields[2] = new ComboField(field_minY, bbox.getMinY(), fieldType);
fields[3] = new ComboField(field_maxY, bbox.getMaxY(), fieldType);
fields[4] = new ComboField(field_xdl, bbox.getCrossesDateLine()?"T":"F", xdlFieldType);
return fields;
}
//---------------------------------
// Query Builder
// Value Source / Relevancy
//---------------------------------
/**
* Provides access to each rectangle per document as a ValueSource in which
* {@link org.apache.lucene.queries.function.FunctionValues#objectVal(int)} returns a {@link
* Shape}.
*/ //TODO raise to SpatialStrategy
public ValueSource makeShapeValueSource() {
return new BBoxValueSource(this);
}
@Override
public ValueSource makeDistanceValueSource(Point queryPoint, double multiplier) {
return new BBoxSimilarityValueSource(
this, new DistanceSimilarity(this.getSpatialContext(), queryPoint, multiplier));
//TODO if makeShapeValueSource gets lifted to the top; this could become a generic impl.
return new DistanceToShapeValueSource(makeShapeValueSource(), queryPoint, multiplier, ctx);
}
public ValueSource makeBBoxAreaSimilarityValueSource(Rectangle queryBox) {
return new BBoxSimilarityValueSource(
this, new AreaSimilarity(queryBox, queryPower, targetPower));
/** Returns a similarity based on {@link BBoxOverlapRatioValueSource}. This is just a
* convenience method. */
public ValueSource makeOverlapRatioValueSource(Rectangle queryBox, double queryTargetProportion) {
return new BBoxOverlapRatioValueSource(
makeShapeValueSource(), ctx.isGeo(), queryBox, queryTargetProportion, 0.0);
}
//---------------------------------
// Query / Filter Building
//---------------------------------
@Override
public Filter makeFilter(SpatialArgs args) {
return new QueryWrapperFilter(makeSpatialQuery(args));
@ -156,17 +218,10 @@ public class BBoxStrategy extends SpatialStrategy {
return new ConstantScoreQuery(makeSpatialQuery(args));
}
public Query makeQueryWithValueSource(SpatialArgs args, ValueSource valueSource) {
BooleanQuery bq = new BooleanQuery();
Query spatial = makeSpatialQuery(args);
bq.add(new ConstantScoreQuery(spatial), BooleanClause.Occur.MUST);
// This part does the scoring
Query spatialRankingQuery = new FunctionQuery(valueSource);
bq.add(spatialRankingQuery, BooleanClause.Occur.MUST);
return bq;
}
// Utility on SpatialStrategy?
// public Query makeQueryWithValueSource(SpatialArgs args, ValueSource valueSource) {
// return new FilteredQuery(new FunctionQuery(valueSource), makeFilter(args));
// }
private Query makeSpatialQuery(SpatialArgs args) {
Shape shape = args.getShape();
@ -192,11 +247,6 @@ public class BBoxStrategy extends SpatialStrategy {
return spatial;
}
//-------------------------------------------------------------------------------
//
//-------------------------------------------------------------------------------
/**
* Constructs a query to retrieve documents that fully contain the input envelope.
*
@ -209,8 +259,8 @@ public class BBoxStrategy extends SpatialStrategy {
// Y conditions
// docMinY <= queryExtent.getMinY() AND docMaxY >= queryExtent.getMaxY()
Query qMinY = NumericRangeQuery.newDoubleRange(field_minY, precisionStep, null, bbox.getMinY(), false, true);
Query qMaxY = NumericRangeQuery.newDoubleRange(field_maxY, precisionStep, bbox.getMaxY(), null, true, false);
Query qMinY = NumericRangeQuery.newDoubleRange(field_minY, getPrecisionStep(), null, bbox.getMinY(), false, true);
Query qMaxY = NumericRangeQuery.newDoubleRange(field_maxY, getPrecisionStep(), bbox.getMaxY(), null, true, false);
Query yConditions = this.makeQuery(BooleanClause.Occur.MUST, qMinY, qMaxY);
// X conditions
@ -222,25 +272,35 @@ public class BBoxStrategy extends SpatialStrategy {
// X Conditions for documents that do not cross the date line,
// documents that contain the min X and max X of the query envelope,
// docMinX <= queryExtent.getMinX() AND docMaxX >= queryExtent.getMaxX()
Query qMinX = NumericRangeQuery.newDoubleRange(field_minX, precisionStep, null, bbox.getMinX(), false, true);
Query qMaxX = NumericRangeQuery.newDoubleRange(field_maxX, precisionStep, bbox.getMaxX(), null, true, false);
Query qMinX = NumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), null, bbox.getMinX(), false, true);
Query qMaxX = NumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), bbox.getMaxX(), null, true, false);
Query qMinMax = this.makeQuery(BooleanClause.Occur.MUST, qMinX, qMaxX);
Query qNonXDL = this.makeXDL(false, qMinMax);
// X Conditions for documents that cross the date line,
// the left portion of the document contains the min X of the query
// OR the right portion of the document contains the max X of the query,
// docMinXLeft <= queryExtent.getMinX() OR docMaxXRight >= queryExtent.getMaxX()
Query qXDLLeft = NumericRangeQuery.newDoubleRange(field_minX, precisionStep, null, bbox.getMinX(), false, true);
Query qXDLRight = NumericRangeQuery.newDoubleRange(field_maxX, precisionStep, bbox.getMaxX(), null, true, false);
Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.SHOULD, qXDLLeft, qXDLRight);
Query qXDL = this.makeXDL(true, qXDLLeftRight);
if (!ctx.isGeo()) {
xConditions = qNonXDL;
} else {
// X Conditions for documents that cross the date line,
// the left portion of the document contains the min X of the query
// OR the right portion of the document contains the max X of the query,
// docMinXLeft <= queryExtent.getMinX() OR docMaxXRight >= queryExtent.getMaxX()
Query qXDLLeft = NumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), null, bbox.getMinX(), false, true);
Query qXDLRight = NumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), bbox.getMaxX(), null, true, false);
Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.SHOULD, qXDLLeft, qXDLRight);
Query qXDL = this.makeXDL(true, qXDLLeftRight);
// apply the non-XDL and XDL conditions
xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
Query qEdgeDL = null;
if (bbox.getMinX() == bbox.getMaxX() && Math.abs(bbox.getMinX()) == 180) {
double edge = bbox.getMinX() * -1;//opposite dateline edge
qEdgeDL = makeQuery(BooleanClause.Occur.SHOULD,
makeNumberTermQuery(field_minX, edge), makeNumberTermQuery(field_maxX, edge));
}
// queries that cross the date line
// apply the non-XDL and XDL conditions
xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL, qEdgeDL);
}
} else {
// queries that cross the date line
// No need to search for documents that do not cross the date line
@ -248,11 +308,14 @@ public class BBoxStrategy extends SpatialStrategy {
// the left portion of the document contains the min X of the query
// AND the right portion of the document contains the max X of the query,
// docMinXLeft <= queryExtent.getMinX() AND docMaxXRight >= queryExtent.getMaxX()
Query qXDLLeft = NumericRangeQuery.newDoubleRange(field_minX, precisionStep, null, bbox.getMinX(), false, true);
Query qXDLRight = NumericRangeQuery.newDoubleRange(field_maxX, precisionStep, bbox.getMaxX(), null, true, false);
Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qXDLLeft, qXDLRight);
Query qXDLLeft = NumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), null, bbox.getMinX(), false, true);
Query qXDLRight = NumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), bbox.getMaxX(), null, true, false);
Query qXDLLeftRight = this.makeXDL(true, this.makeQuery(BooleanClause.Occur.MUST, qXDLLeft, qXDLRight));
xConditions = this.makeXDL(true, qXDLLeftRight);
Query qWorld = makeQuery(BooleanClause.Occur.MUST,
makeNumberTermQuery(field_minX, -180), makeNumberTermQuery(field_maxX, 180));
xConditions = makeQuery(BooleanClause.Occur.SHOULD, qXDLLeftRight, qWorld);
}
// both X and Y conditions must occur
@ -271,8 +334,8 @@ public class BBoxStrategy extends SpatialStrategy {
// Y conditions
// docMinY > queryExtent.getMaxY() OR docMaxY < queryExtent.getMinY()
Query qMinY = NumericRangeQuery.newDoubleRange(field_minY, precisionStep, bbox.getMaxY(), null, false, false);
Query qMaxY = NumericRangeQuery.newDoubleRange(field_maxY, precisionStep, null, bbox.getMinY(), false, false);
Query qMinY = NumericRangeQuery.newDoubleRange(field_minY, getPrecisionStep(), bbox.getMaxY(), null, false, false);
Query qMaxY = NumericRangeQuery.newDoubleRange(field_maxY, getPrecisionStep(), null, bbox.getMinY(), false, false);
Query yConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qMinY, qMaxY);
// X conditions
@ -283,26 +346,42 @@ public class BBoxStrategy extends SpatialStrategy {
// X Conditions for documents that do not cross the date line,
// docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX()
Query qMinX = NumericRangeQuery.newDoubleRange(field_minX, precisionStep, bbox.getMaxX(), null, false, false);
Query qMaxX = NumericRangeQuery.newDoubleRange(field_maxX, precisionStep, null, bbox.getMinX(), false, false);
Query qMinX = NumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMaxX(), null, false, false);
if (bbox.getMinX() == -180.0 && ctx.isGeo()) {//touches dateline; -180 == 180
BooleanQuery bq = new BooleanQuery();
bq.add(qMinX, BooleanClause.Occur.MUST);
bq.add(makeNumberTermQuery(field_maxX, 180.0), BooleanClause.Occur.MUST_NOT);
qMinX = bq;
}
Query qMaxX = NumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMinX(), false, false);
if (bbox.getMaxX() == 180.0 && ctx.isGeo()) {//touches dateline; -180 == 180
BooleanQuery bq = new BooleanQuery();
bq.add(qMaxX, BooleanClause.Occur.MUST);
bq.add(makeNumberTermQuery(field_minX, -180.0), BooleanClause.Occur.MUST_NOT);
qMaxX = bq;
}
Query qMinMax = this.makeQuery(BooleanClause.Occur.SHOULD, qMinX, qMaxX);
Query qNonXDL = this.makeXDL(false, qMinMax);
// X Conditions for documents that cross the date line,
// both the left and right portions of the document must be disjoint to the query
// (docMinXLeft > queryExtent.getMaxX() OR docMaxXLeft < queryExtent.getMinX()) AND
// (docMinXRight > queryExtent.getMaxX() OR docMaxXRight < queryExtent.getMinX())
// where: docMaxXLeft = 180.0, docMinXRight = -180.0
// (docMaxXLeft < queryExtent.getMinX()) equates to (180.0 < queryExtent.getMinX()) and is ignored
// (docMinXRight > queryExtent.getMaxX()) equates to (-180.0 > queryExtent.getMaxX()) and is ignored
Query qMinXLeft = NumericRangeQuery.newDoubleRange(field_minX, precisionStep, bbox.getMaxX(), null, false, false);
Query qMaxXRight = NumericRangeQuery.newDoubleRange(field_maxX, precisionStep, null, bbox.getMinX(), false, false);
Query qLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qMinXLeft, qMaxXRight);
Query qXDL = this.makeXDL(true, qLeftRight);
if (!ctx.isGeo()) {
xConditions = qNonXDL;
} else {
// X Conditions for documents that cross the date line,
// apply the non-XDL and XDL conditions
xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
// both the left and right portions of the document must be disjoint to the query
// (docMinXLeft > queryExtent.getMaxX() OR docMaxXLeft < queryExtent.getMinX()) AND
// (docMinXRight > queryExtent.getMaxX() OR docMaxXRight < queryExtent.getMinX())
// where: docMaxXLeft = 180.0, docMinXRight = -180.0
// (docMaxXLeft < queryExtent.getMinX()) equates to (180.0 < queryExtent.getMinX()) and is ignored
// (docMinXRight > queryExtent.getMaxX()) equates to (-180.0 > queryExtent.getMaxX()) and is ignored
Query qMinXLeft = NumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMaxX(), null, false, false);
Query qMaxXRight = NumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMinX(), false, false);
Query qLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qMinXLeft, qMaxXRight);
Query qXDL = this.makeXDL(true, qLeftRight);
// apply the non-XDL and XDL conditions
xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
}
// queries that cross the date line
} else {
@ -310,10 +389,10 @@ public class BBoxStrategy extends SpatialStrategy {
// the document must be disjoint to both the left and right query portions
// (docMinX > queryExtent.getMaxX()Left OR docMaxX < queryExtent.getMinX()) AND (docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX()Left)
// where: queryExtent.getMaxX()Left = 180.0, queryExtent.getMinX()Left = -180.0
Query qMinXLeft = NumericRangeQuery.newDoubleRange(field_minX, precisionStep, 180.0, null, false, false);
Query qMaxXLeft = NumericRangeQuery.newDoubleRange(field_maxX, precisionStep, null, bbox.getMinX(), false, false);
Query qMinXRight = NumericRangeQuery.newDoubleRange(field_minX, precisionStep, bbox.getMaxX(), null, false, false);
Query qMaxXRight = NumericRangeQuery.newDoubleRange(field_maxX, precisionStep, null, -180.0, false, false);
Query qMinXLeft = NumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), 180.0, null, false, false);
Query qMaxXLeft = NumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMinX(), false, false);
Query qMinXRight = NumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMaxX(), null, false, false);
Query qMaxXRight = NumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, -180.0, false, false);
Query qLeft = this.makeQuery(BooleanClause.Occur.SHOULD, qMinXLeft, qMaxXLeft);
Query qRight = this.makeQuery(BooleanClause.Occur.SHOULD, qMinXRight, qMaxXRight);
Query qLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qLeft, qRight);
@ -335,16 +414,11 @@ public class BBoxStrategy extends SpatialStrategy {
Query makeEquals(Rectangle bbox) {
// docMinX = queryExtent.getMinX() AND docMinY = queryExtent.getMinY() AND docMaxX = queryExtent.getMaxX() AND docMaxY = queryExtent.getMaxY()
Query qMinX = NumericRangeQuery.newDoubleRange(field_minX, precisionStep, bbox.getMinX(), bbox.getMinX(), true, true);
Query qMinY = NumericRangeQuery.newDoubleRange(field_minY, precisionStep, bbox.getMinY(), bbox.getMinY(), true, true);
Query qMaxX = NumericRangeQuery.newDoubleRange(field_maxX, precisionStep, bbox.getMaxX(), bbox.getMaxX(), true, true);
Query qMaxY = NumericRangeQuery.newDoubleRange(field_maxY, precisionStep, bbox.getMaxY(), bbox.getMaxY(), true, true);
BooleanQuery bq = new BooleanQuery();
bq.add(qMinX, BooleanClause.Occur.MUST);
bq.add(qMinY, BooleanClause.Occur.MUST);
bq.add(qMaxX, BooleanClause.Occur.MUST);
bq.add(qMaxY, BooleanClause.Occur.MUST);
return bq;
Query qMinX = makeNumberTermQuery(field_minX, bbox.getMinX());
Query qMinY = makeNumberTermQuery(field_minY, bbox.getMinY());
Query qMaxX = makeNumberTermQuery(field_maxX, bbox.getMaxX());
Query qMaxY = makeNumberTermQuery(field_maxY, bbox.getMaxY());
return makeQuery(BooleanClause.Occur.MUST, qMinX, qMinY, qMaxX, qMaxY);
}
/**
@ -361,12 +435,18 @@ public class BBoxStrategy extends SpatialStrategy {
// to get around it we add all documents as a SHOULD
// there must be an envelope, it must not be disjoint
Query qDisjoint = makeDisjoint(bbox);
Query qIsNonXDL = this.makeXDL(false);
Query qIsXDL = this.makeXDL(true);
Query qHasEnv = this.makeQuery(BooleanClause.Occur.SHOULD, qIsNonXDL, qIsXDL);
Query qHasEnv;
if (ctx.isGeo()) {
Query qIsNonXDL = this.makeXDL(false);
Query qIsXDL = ctx.isGeo() ? this.makeXDL(true) : null;
qHasEnv = this.makeQuery(BooleanClause.Occur.SHOULD, qIsNonXDL, qIsXDL);
} else {
qHasEnv = this.makeXDL(false);
}
BooleanQuery qNotDisjoint = new BooleanQuery();
qNotDisjoint.add(qHasEnv, BooleanClause.Occur.MUST);
Query qDisjoint = makeDisjoint(bbox);
qNotDisjoint.add(qDisjoint, BooleanClause.Occur.MUST_NOT);
//Query qDisjoint = makeDisjoint();
@ -386,7 +466,8 @@ public class BBoxStrategy extends SpatialStrategy {
BooleanQuery makeQuery(BooleanClause.Occur occur, Query... queries) {
BooleanQuery bq = new BooleanQuery();
for (Query query : queries) {
bq.add(query, occur);
if (query != null)
bq.add(query, occur);
}
return bq;
}
@ -403,40 +484,38 @@ public class BBoxStrategy extends SpatialStrategy {
// Y conditions
// docMinY >= queryExtent.getMinY() AND docMaxY <= queryExtent.getMaxY()
Query qMinY = NumericRangeQuery.newDoubleRange(field_minY, precisionStep, bbox.getMinY(), null, true, false);
Query qMaxY = NumericRangeQuery.newDoubleRange(field_maxY, precisionStep, null, bbox.getMaxY(), false, true);
Query qMinY = NumericRangeQuery.newDoubleRange(field_minY, getPrecisionStep(), bbox.getMinY(), null, true, false);
Query qMaxY = NumericRangeQuery.newDoubleRange(field_maxY, getPrecisionStep(), null, bbox.getMaxY(), false, true);
Query yConditions = this.makeQuery(BooleanClause.Occur.MUST, qMinY, qMaxY);
// X conditions
Query xConditions;
// X Conditions for documents that cross the date line,
// the left portion of the document must be within the left portion of the query,
// AND the right portion of the document must be within the right portion of the query
// docMinXLeft >= queryExtent.getMinX() AND docMaxXLeft <= 180.0
// AND docMinXRight >= -180.0 AND docMaxXRight <= queryExtent.getMaxX()
Query qXDLLeft = NumericRangeQuery.newDoubleRange(field_minX, precisionStep, bbox.getMinX(), null, true, false);
Query qXDLRight = NumericRangeQuery.newDoubleRange(field_maxX, precisionStep, null, bbox.getMaxX(), false, true);
Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qXDLLeft, qXDLRight);
Query qXDL = this.makeXDL(true, qXDLLeftRight);
if (ctx.isGeo() && bbox.getMinX() == -180.0 && bbox.getMaxX() == 180.0) {
//if query world-wraps, only the y condition matters
return yConditions;
// queries that do not cross the date line
if (!bbox.getCrossesDateLine()) {
} else if (!bbox.getCrossesDateLine()) {
// queries that do not cross the date line
// X Conditions for documents that do not cross the date line,
// docMinX >= queryExtent.getMinX() AND docMaxX <= queryExtent.getMaxX()
Query qMinX = NumericRangeQuery.newDoubleRange(field_minX, precisionStep, bbox.getMinX(), null, true, false);
Query qMaxX = NumericRangeQuery.newDoubleRange(field_maxX, precisionStep, null, bbox.getMaxX(), false, true);
Query qMinX = NumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMinX(), null, true, false);
Query qMaxX = NumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMaxX(), false, true);
Query qMinMax = this.makeQuery(BooleanClause.Occur.MUST, qMinX, qMaxX);
Query qNonXDL = this.makeXDL(false, qMinMax);
// apply the non-XDL or XDL X conditions
if ((bbox.getMinX() <= -180.0) && bbox.getMaxX() >= 180.0) {
xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
} else {
xConditions = qNonXDL;
double edge = 0;//none, otherwise opposite dateline of query
if (bbox.getMinX() == -180.0)
edge = 180;
else if (bbox.getMaxX() == 180.0)
edge = -180;
if (edge != 0 && ctx.isGeo()) {
Query edgeQ = makeQuery(BooleanClause.Occur.MUST,
makeNumberTermQuery(field_minX, edge), makeNumberTermQuery(field_maxX, edge));
qMinMax = makeQuery(BooleanClause.Occur.SHOULD, qMinMax, edgeQ);
}
xConditions = this.makeXDL(false, qMinMax);
// queries that cross the date line
} else {
@ -444,14 +523,14 @@ public class BBoxStrategy extends SpatialStrategy {
// the document should be within the left portion of the query
// docMinX >= queryExtent.getMinX() AND docMaxX <= 180.0
Query qMinXLeft = NumericRangeQuery.newDoubleRange(field_minX, precisionStep, bbox.getMinX(), null, true, false);
Query qMaxXLeft = NumericRangeQuery.newDoubleRange(field_maxX, precisionStep, null, 180.0, false, true);
Query qMinXLeft = NumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMinX(), null, true, false);
Query qMaxXLeft = NumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, 180.0, false, true);
Query qLeft = this.makeQuery(BooleanClause.Occur.MUST, qMinXLeft, qMaxXLeft);
// the document should be within the right portion of the query
// docMinX >= -180.0 AND docMaxX <= queryExtent.getMaxX()
Query qMinXRight = NumericRangeQuery.newDoubleRange(field_minX, precisionStep, -180.0, null, true, false);
Query qMaxXRight = NumericRangeQuery.newDoubleRange(field_maxX, precisionStep, null, bbox.getMaxX(), false, true);
Query qMinXRight = NumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), -180.0, null, true, false);
Query qMaxXRight = NumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMaxX(), false, true);
Query qRight = this.makeQuery(BooleanClause.Occur.MUST, qMinXRight, qMaxXRight);
// either left or right conditions should occur,
@ -459,6 +538,16 @@ public class BBoxStrategy extends SpatialStrategy {
Query qLeftRight = this.makeQuery(BooleanClause.Occur.SHOULD, qLeft, qRight);
Query qNonXDL = this.makeXDL(false, qLeftRight);
// X Conditions for documents that cross the date line,
// the left portion of the document must be within the left portion of the query,
// AND the right portion of the document must be within the right portion of the query
// docMinXLeft >= queryExtent.getMinX() AND docMaxXLeft <= 180.0
// AND docMinXRight >= -180.0 AND docMaxXRight <= queryExtent.getMaxX()
Query qXDLLeft = NumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMinX(), null, true, false);
Query qXDLRight = NumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMaxX(), false, true);
Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qXDLLeft, qXDLRight);
Query qXDL = this.makeXDL(true, qXDLLeftRight);
// apply the non-XDL and XDL conditions
xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
}
@ -470,11 +559,10 @@ public class BBoxStrategy extends SpatialStrategy {
/**
* Constructs a query to retrieve documents that do or do not cross the date line.
*
*
* @param crossedDateLine <code>true</true> for documents that cross the date line
* @return the query
*/
Query makeXDL(boolean crossedDateLine) {
private Query makeXDL(boolean crossedDateLine) {
// The 'T' and 'F' values match solr fields
return new TermQuery(new Term(field_xdl, crossedDateLine ? "T" : "F"));
}
@ -487,12 +575,23 @@ public class BBoxStrategy extends SpatialStrategy {
* @param query the spatial query
* @return the query
*/
Query makeXDL(boolean crossedDateLine, Query query) {
private Query makeXDL(boolean crossedDateLine, Query query) {
if (!ctx.isGeo()) {
assert !crossedDateLine;
return query;
}
BooleanQuery bq = new BooleanQuery();
bq.add(this.makeXDL(crossedDateLine), BooleanClause.Occur.MUST);
bq.add(query, BooleanClause.Occur.MUST);
return bq;
}
private Query makeNumberTermQuery(String field, double number) {
BytesRef bytes = new BytesRef();
NumericUtils.longToPrefixCodedBytes(NumericUtils.doubleToSortableLong(number), 0, bytes);
return new TermQuery(new Term(field, bytes));
}
}

View File

@ -0,0 +1,116 @@
package org.apache.lucene.spatial.bbox;
/*
* 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 com.spatial4j.core.shape.Rectangle;
import org.apache.lucene.index.AtomicReader;
import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.NumericDocValues;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.util.Bits;
import java.io.IOException;
import java.util.Map;
/**
* A ValueSource in which the indexed Rectangle is returned from
* {@link org.apache.lucene.queries.function.FunctionValues#objectVal(int)}.
*
* @lucene.internal
*/
class BBoxValueSource extends ValueSource {
private final BBoxStrategy strategy;
public BBoxValueSource(BBoxStrategy strategy) {
this.strategy = strategy;
}
@Override
public String description() {
return "bboxShape(" + strategy.getFieldName() + ")";
}
@Override
public FunctionValues getValues(Map context, AtomicReaderContext readerContext) throws IOException {
AtomicReader reader = readerContext.reader();
final NumericDocValues minX = DocValues.getNumeric(reader, strategy.field_minX);
final NumericDocValues minY = DocValues.getNumeric(reader, strategy.field_minY);
final NumericDocValues maxX = DocValues.getNumeric(reader, strategy.field_maxX);
final NumericDocValues maxY = DocValues.getNumeric(reader, strategy.field_maxY);
final Bits validBits = DocValues.getDocsWithField(reader, strategy.field_minX);//could have chosen any field
//reused
final Rectangle rect = strategy.getSpatialContext().makeRectangle(0,0,0,0);
return new FunctionValues() {
@Override
public Object objectVal(int doc) {
if (!validBits.get(doc)) {
return null;
} else {
rect.reset(
Double.longBitsToDouble(minX.get(doc)), Double.longBitsToDouble(maxX.get(doc)),
Double.longBitsToDouble(minY.get(doc)), Double.longBitsToDouble(maxY.get(doc)));
return rect;
}
}
@Override
public String strVal(int doc) {//TODO support WKT output once Spatial4j does
Object v = objectVal(doc);
return v == null ? null : v.toString();
}
@Override
public boolean exists(int doc) {
return validBits.get(doc);
}
@Override
public Explanation explain(int doc) {
return new Explanation(Float.NaN, toString(doc));
}
@Override
public String toString(int doc) {
return description() + '=' + strVal(doc);
}
};
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BBoxValueSource that = (BBoxValueSource) o;
if (!strategy.equals(that.strategy)) return false;
return true;
}
@Override
public int hashCode() {
return strategy.hashCode();
}
}

View File

@ -1,89 +0,0 @@
package org.apache.lucene.spatial.bbox;
/*
* 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 com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.distance.DistanceCalculator;
import com.spatial4j.core.shape.Point;
import com.spatial4j.core.shape.Rectangle;
import org.apache.lucene.search.Explanation;
/**
* Returns the distance between the center of the indexed rectangle and the
* query shape.
* @lucene.experimental
*/
public class DistanceSimilarity implements BBoxSimilarity {
private final Point queryPoint;
private final double multiplier;
private final DistanceCalculator distCalc;
private final double nullValue;
public DistanceSimilarity(SpatialContext ctx, Point queryPoint, double multiplier) {
this.queryPoint = queryPoint;
this.multiplier = multiplier;
this.distCalc = ctx.getDistCalc();
this.nullValue = (ctx.isGeo() ? 180 * multiplier : Double.MAX_VALUE);
}
@Override
public double score(Rectangle indexRect, Explanation exp) {
double score;
if (indexRect == null) {
score = nullValue;
} else {
score = distCalc.distance(queryPoint, indexRect.getCenter()) * multiplier;
}
if (exp != null) {
exp.setValue((float)score);
exp.setDescription(this.getClass().getSimpleName());
exp.addDetail(new Explanation(-1f, "" + queryPoint));
exp.addDetail(new Explanation(-1f,""+indexRect));
exp.addDetail(new Explanation((float)multiplier,"multiplier"));
}
return score;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DistanceSimilarity that = (DistanceSimilarity) o;
if (Double.compare(that.multiplier, multiplier) != 0) return false;
if (Double.compare(that.nullValue, nullValue) != 0) return false;
if (!distCalc.equals(that.distCalc)) return false;
if (!queryPoint.equals(that.queryPoint)) return false;
return true;
}
@Override
public int hashCode() {
int result;
long temp;
result = queryPoint.hashCode();
temp = Double.doubleToLongBits(multiplier);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + distCalc.hashCode();
temp = Double.doubleToLongBits(nullValue);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
}

View File

@ -0,0 +1,110 @@
package org.apache.lucene.spatial.util;
/*
* 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 com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.shape.Shape;
import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.docvalues.DoubleDocValues;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.IndexSearcher;
import java.io.IOException;
import java.util.Map;
/**
* The area of a Shape retrieved from a ValueSource via
* {@link org.apache.lucene.queries.function.FunctionValues#objectVal(int)}.
*
* @see Shape#getArea(com.spatial4j.core.context.SpatialContext)
*
* @lucene.experimental
*/
public class ShapeAreaValueSource extends ValueSource {
private final ValueSource shapeValueSource;
private final SpatialContext ctx;//not part of identity; should be associated with shapeValueSource indirectly
private final boolean geoArea;
public ShapeAreaValueSource(ValueSource shapeValueSource, SpatialContext ctx, boolean geoArea) {
this.shapeValueSource = shapeValueSource;
this.ctx = ctx;
this.geoArea = geoArea;
}
@Override
public String description() {
return "area(" + shapeValueSource.description() + ",geo=" + geoArea + ")";
}
@Override
public void createWeight(Map context, IndexSearcher searcher) throws IOException {
shapeValueSource.createWeight(context, searcher);
}
@Override
public FunctionValues getValues(Map context, AtomicReaderContext readerContext) throws IOException {
final FunctionValues shapeValues = shapeValueSource.getValues(context, readerContext);
return new DoubleDocValues(this) {
@Override
public double doubleVal(int doc) {
Shape shape = (Shape) shapeValues.objectVal(doc);
if (shape == null || shape.isEmpty())
return 0;//or NaN?
//This part of Spatial4j API is kinda weird. Passing null means 2D area, otherwise geo
// assuming ctx.isGeo()
return shape.getArea( geoArea ? ctx : null );
}
@Override
public boolean exists(int doc) {
return shapeValues.exists(doc);
}
@Override
public Explanation explain(int doc) {
Explanation exp = super.explain(doc);
exp.addDetail(shapeValues.explain(doc));
return exp;
}
};
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ShapeAreaValueSource that = (ShapeAreaValueSource) o;
if (geoArea != that.geoArea) return false;
if (!shapeValueSource.equals(that.shapeValueSource)) return false;
return true;
}
@Override
public int hashCode() {
int result = shapeValueSource.hashCode();
result = 31 * result + (geoArea ? 1 : 0);
return result;
}
}

View File

@ -23,6 +23,7 @@ import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.shape.Point;
import com.spatial4j.core.shape.Shape;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.spatial.bbox.BBoxStrategy;
import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
import org.apache.lucene.spatial.prefix.TermQueryPrefixTreeStrategy;
@ -90,9 +91,20 @@ public class DistanceStrategyTest extends StrategyTestCase {
this.strategy = strategy;
}
@Override
public void setUp() throws Exception {
super.setUp();
if (strategy instanceof BBoxStrategy && random().nextBoolean()) {//disable indexing sometimes
BBoxStrategy bboxStrategy = (BBoxStrategy)strategy;
final FieldType fieldType = new FieldType(bboxStrategy.getFieldType());
fieldType.setIndexed(false);
bboxStrategy.setFieldType(fieldType);
}
}
@Override
protected boolean needsDocValues() {
return (strategy instanceof SerializedDVStrategy);
return true;
}
@Test

View File

@ -70,10 +70,6 @@ public abstract class SpatialTestCase extends LuceneTestCase {
super.setUp();
// TODO: change this module to index docvalues instead of uninverting
uninvertMap.clear();
uninvertMap.put("bbox__minX", Type.DOUBLE);
uninvertMap.put("bbox__maxX", Type.DOUBLE);
uninvertMap.put("bbox__minY", Type.DOUBLE);
uninvertMap.put("bbox__maxY", Type.DOUBLE);
uninvertMap.put("pointvector__x", Type.DOUBLE);
uninvertMap.put("pointvector__y", Type.DOUBLE);

View File

@ -17,52 +17,235 @@ package org.apache.lucene.spatial.bbox;
* limitations under the License.
*/
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.context.SpatialContextFactory;
import com.spatial4j.core.distance.DistanceUtils;
import com.spatial4j.core.shape.Rectangle;
import com.spatial4j.core.shape.Shape;
import com.spatial4j.core.shape.impl.RectangleImpl;
import org.apache.lucene.spatial.SpatialMatchConcern;
import org.apache.lucene.spatial.StrategyTestCase;
import org.junit.Before;
import org.apache.lucene.spatial.prefix.RandomSpatialOpStrategyTestCase;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.spatial.util.ShapeAreaValueSource;
import org.junit.Ignore;
import org.junit.Test;
import java.io.IOException;
public class TestBBoxStrategy extends StrategyTestCase {
public class TestBBoxStrategy extends RandomSpatialOpStrategyTestCase {
@Before
@Override
public void setUp() throws Exception {
super.setUp();
protected Shape randomIndexedShape() {
Rectangle world = ctx.getWorldBounds();
if (random().nextInt(10) == 0) // increased chance of getting one of these
return world;
int worldWidth = (int) Math.round(world.getWidth());
int deltaLeft = nextIntInclusive(worldWidth);
int deltaRight = nextIntInclusive(worldWidth - deltaLeft);
int worldHeight = (int) Math.round(world.getHeight());
int deltaTop = nextIntInclusive(worldHeight);
int deltaBottom = nextIntInclusive(worldHeight - deltaTop);
if (ctx.isGeo() && (deltaLeft != 0 || deltaRight != 0)) {
//if geo & doesn't world-wrap, we shift randomly to potentially cross dateline
int shift = nextIntInclusive(360);
return ctx.makeRectangle(
DistanceUtils.normLonDEG(world.getMinX() + deltaLeft + shift),
DistanceUtils.normLonDEG(world.getMaxX() - deltaRight + shift),
world.getMinY() + deltaBottom, world.getMaxY() - deltaTop);
} else {
return ctx.makeRectangle(
world.getMinX() + deltaLeft, world.getMaxX() - deltaRight,
world.getMinY() + deltaBottom, world.getMaxY() - deltaTop);
}
}
/** next int, inclusive, rounds to multiple of 10 if given evenly divisible. */
private int nextIntInclusive(int toInc) {
final int DIVIS = 10;
if (toInc % DIVIS == 0) {
return random().nextInt(toInc/DIVIS + 1) * DIVIS;
} else {
return random().nextInt(toInc + 1);
}
}
@Override
protected Shape randomQueryShape() {
return randomIndexedShape();
}
@Test
@Repeat(iterations = 20)
public void testOperations() throws IOException {
//setup
if (random().nextInt(4) > 0) {//75% of the time choose geo (more interesting to test)
this.ctx = SpatialContext.GEO;
} else {
SpatialContextFactory factory = new SpatialContextFactory();
factory.geo = false;
factory.worldBounds = new RectangleImpl(-300, 300, -100, 100, null);
this.ctx = factory.newSpatialContext();
}
this.strategy = new BBoxStrategy(ctx, "bbox");
for (SpatialOperation operation : SpatialOperation.values()) {
if (operation == SpatialOperation.Overlaps)
continue;//unsupported
testOperationRandomShapes(operation);
deleteAll();
commit();
}
}
@Test
public void testIntersectsBugDatelineEdge() throws IOException {
setupGeo();
testOperation(
ctx.makeRectangle(160, 180, -10, 10),
SpatialOperation.Intersects,
ctx.makeRectangle(-180, -160, -10, 10), true);
}
@Test
public void testIntersectsWorldDatelineEdge() throws IOException {
setupGeo();
testOperation(
ctx.makeRectangle(-180, 180, -10, 10),
SpatialOperation.Intersects,
ctx.makeRectangle(180, 180, -10, 10), true);
}
@Test
public void testWithinBugDatelineEdge() throws IOException {
setupGeo();
testOperation(
ctx.makeRectangle(180, 180, -10, 10),
SpatialOperation.IsWithin,
ctx.makeRectangle(-180, -100, -10, 10), true);
}
@Test
public void testContainsBugDatelineEdge() throws IOException {
setupGeo();
testOperation(
ctx.makeRectangle(-180, -150, -10, 10),
SpatialOperation.Contains,
ctx.makeRectangle(180, 180, -10, 10), true);
}
@Test
public void testWorldContainsXDL() throws IOException {
setupGeo();
testOperation(
ctx.makeRectangle(-180, 180, -10, 10),
SpatialOperation.Contains,
ctx.makeRectangle(170, -170, -10, 10), true);
}
private void setupGeo() {
this.ctx = SpatialContext.GEO;
this.strategy = new BBoxStrategy(ctx, "bbox");
}
// OLD STATIC TESTS (worthless?)
@Test @Ignore("Overlaps not supported")
public void testBasicOperaions() throws IOException {
setupGeo();
getAddAndVerifyIndexedDocuments(DATA_SIMPLE_BBOX);
executeQueries(SpatialMatchConcern.EXACT, QTEST_Simple_Queries_BBox);
}
@Test
public void testStatesBBox() throws IOException {
setupGeo();
getAddAndVerifyIndexedDocuments(DATA_STATES_BBOX);
executeQueries(SpatialMatchConcern.FILTER, QTEST_States_IsWithin_BBox);
executeQueries(SpatialMatchConcern.FILTER, QTEST_States_Intersects_BBox);
}
@Test
public void testCitiesIntersectsBBox() throws IOException {
setupGeo();
getAddAndVerifyIndexedDocuments(DATA_WORLD_CITIES_POINTS);
executeQueries(SpatialMatchConcern.FILTER, QTEST_Cities_Intersects_BBox);
}
/* Convert DATA_WORLD_CITIES_POINTS to bbox */
@Override
protected Shape convertShapeFromGetDocuments(Shape shape) {
return shape.getBoundingBox();
}
@Test @Ignore("Overlaps not supported")
public void testBasicOperaions() throws IOException {
getAddAndVerifyIndexedDocuments(DATA_SIMPLE_BBOX);
executeQueries(SpatialMatchConcern.EXACT, QTEST_Simple_Queries_BBox);
}
@Test
public void testStatesBBox() throws IOException {
getAddAndVerifyIndexedDocuments(DATA_STATES_BBOX);
executeQueries(SpatialMatchConcern.FILTER, QTEST_States_IsWithin_BBox);
executeQueries(SpatialMatchConcern.FILTER, QTEST_States_Intersects_BBox);
public void testOverlapRatio() throws IOException {
setupGeo();
//Simply assert null shape results in 0
adoc("999", (Shape) null);
commit();
BBoxStrategy bboxStrategy = (BBoxStrategy) strategy;
checkValueSource(bboxStrategy.makeOverlapRatioValueSource(randomRectangle(), 0.0), new float[]{0f}, 0f);
//we test raw BBoxOverlapRatioValueSource without actual indexing
for (int SHIFT = 0; SHIFT < 360; SHIFT += 10) {
Rectangle queryBox = shiftedRect(0, 40, -20, 20, SHIFT);//40x40, 1600 area
final boolean MSL = random().nextBoolean();
final double minSideLength = MSL ? 0.1 : 0.0;
BBoxOverlapRatioValueSource sim = new BBoxOverlapRatioValueSource(null, true, queryBox, 0.5, minSideLength);
int nudge = SHIFT == 0 ? 0 : random().nextInt(3) * 10 - 10;//-10, 0, or 10. Keep 0 on first round.
final double EPS = 0.0000001;
assertEquals("within", (200d/1600d * 0.5) + (0.5), sim.score(shiftedRect(10, 30, 0, 10, SHIFT + nudge), null), EPS);
assertEquals("in25%", 0.25, sim.score(shiftedRect(30, 70, -20, 20, SHIFT), null), EPS);
assertEquals("wrap", 0.2794117, sim.score(shiftedRect(30, 10, -20, 20, SHIFT + nudge), null), EPS);
assertEquals("no intersection H", 0.0, sim.score(shiftedRect(-10, -10, -20, 20, SHIFT), null), EPS);
assertEquals("no intersection V", 0.0, sim.score(shiftedRect(0, 20, -30, -30, SHIFT), null), EPS);
assertEquals("point", 0.5 + (MSL?(0.1*0.1/1600.0/2.0):0), sim.score(shiftedRect(0, 0, 0, 0, SHIFT), null), EPS);
assertEquals("line 25% intersection", 0.25/2 + (MSL?(10.0*0.1/1600.0/2.0):0.0), sim.score(shiftedRect(-30, 10, 0, 0, SHIFT), null), EPS);
//test with point query
sim = new BBoxOverlapRatioValueSource(null, true, shiftedRect(0, 0, 0, 0, SHIFT), 0.5, minSideLength);
assertEquals("same", 1.0, sim.score(shiftedRect(0, 0, 0, 0, SHIFT), null), EPS);
assertEquals("contains", 0.5 + (MSL?(0.1*0.1/(30*10)/2.0):0.0), sim.score(shiftedRect(0, 30, 0, 10, SHIFT), null), EPS);
//test with line query (vertical this time)
sim = new BBoxOverlapRatioValueSource(null, true, shiftedRect(0, 0, 20, 40, SHIFT), 0.5, minSideLength);
assertEquals("line 50%", 0.5, sim.score(shiftedRect(0, 0, 10, 30, SHIFT), null), EPS);
assertEquals("point", 0.5 + (MSL?(0.1*0.1/(20*0.1)/2.0):0.0), sim.score(shiftedRect(0, 0, 30, 30, SHIFT), null), EPS);
}
}
@Test
public void testCitiesIntersectsBBox() throws IOException {
getAddAndVerifyIndexedDocuments(DATA_WORLD_CITIES_POINTS);
executeQueries(SpatialMatchConcern.FILTER, QTEST_Cities_Intersects_BBox);
private Rectangle shiftedRect(double minX, double maxX, double minY, double maxY, int xShift) {
return ctx.makeRectangle(
DistanceUtils.normLonDEG(minX + xShift),
DistanceUtils.normLonDEG(maxX + xShift),
minY, maxY);
}
public void testAreaValueSource() throws IOException {
setupGeo();
adoc("100", ctx.makeRectangle(0, 20, 40, 80));
adoc("999", (Shape) null);
commit();
BBoxStrategy bboxStrategy = (BBoxStrategy) strategy;
checkValueSource(new ShapeAreaValueSource(bboxStrategy.makeShapeValueSource(), ctx, false),
new float[]{800f, 0f}, 0f);
checkValueSource(new ShapeAreaValueSource(bboxStrategy.makeShapeValueSource(), ctx, true),//geo
new float[]{391.93f, 0f}, 0.01f);
}
}