LUCENE-4419: Test indexing non-point shapes; and add SpatialOperation.evaluate(s1,s2)

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1418007 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
David Wayne Smiley 2012-12-06 17:19:23 +00:00
parent 4eb1c502c9
commit e4071374f4
5 changed files with 327 additions and 54 deletions

View File

@ -17,6 +17,9 @@ package org.apache.lucene.spatial.query;
* limitations under the License. * limitations under the License.
*/ */
import com.spatial4j.core.shape.Shape;
import com.spatial4j.core.shape.SpatialRelation;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -25,14 +28,16 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
/** /**
* A clause that compares a stored geometry to a supplied geometry. * A clause that compares a stored geometry to a supplied geometry. For more
* explanation of each operation, consider looking at the source implementation
* of {@link #evaluate(com.spatial4j.core.shape.Shape, com.spatial4j.core.shape.Shape)}.
* *
* @see <a href="http://edndoc.esri.com/arcsde/9.1/general_topics/understand_spatial_relations.htm"> * @see <a href="http://edndoc.esri.com/arcsde/9.1/general_topics/understand_spatial_relations.htm">
* ESRIs docs on spatial relations</a> * ESRIs docs on spatial relations</a>
* *
* @lucene.experimental * @lucene.experimental
*/ */
public class SpatialOperation implements Serializable { public abstract class SpatialOperation implements Serializable {
// Private registry // Private registry
private static final Map<String, SpatialOperation> registry = new HashMap<String, SpatialOperation>(); private static final Map<String, SpatialOperation> registry = new HashMap<String, SpatialOperation>();
private static final List<SpatialOperation> list = new ArrayList<SpatialOperation>(); private static final List<SpatialOperation> list = new ArrayList<SpatialOperation>();
@ -40,15 +45,55 @@ public class SpatialOperation implements Serializable {
// Geometry Operations // Geometry Operations
/** Bounding box of the *indexed* shape. */ /** Bounding box of the *indexed* shape. */
public static final SpatialOperation BBoxIntersects = new SpatialOperation("BBoxIntersects", true, false, false); public static final SpatialOperation BBoxIntersects = new SpatialOperation("BBoxIntersects", true, false, false) {
@Override
public boolean evaluate(Shape indexedShape, Shape queryShape) {
return indexedShape.getBoundingBox().relate(queryShape).intersects();
}
};
/** Bounding box of the *indexed* shape. */ /** Bounding box of the *indexed* shape. */
public static final SpatialOperation BBoxWithin = new SpatialOperation("BBoxWithin", true, false, false); public static final SpatialOperation BBoxWithin = new SpatialOperation("BBoxWithin", true, false, false) {
public static final SpatialOperation Contains = new SpatialOperation("Contains", true, true, false); @Override
public static final SpatialOperation Intersects = new SpatialOperation("Intersects", true, false, false); public boolean evaluate(Shape indexedShape, Shape queryShape) {
public static final SpatialOperation IsEqualTo = new SpatialOperation("IsEqualTo", false, false, false); return indexedShape.getBoundingBox().relate(queryShape) == SpatialRelation.WITHIN;
public static final SpatialOperation IsDisjointTo = new SpatialOperation("IsDisjointTo", false, false, false); }
public static final SpatialOperation IsWithin = new SpatialOperation("IsWithin", true, false, true); };
public static final SpatialOperation Overlaps = new SpatialOperation("Overlaps", true, false, true); public static final SpatialOperation Contains = new SpatialOperation("Contains", true, true, false) {
@Override
public boolean evaluate(Shape indexedShape, Shape queryShape) {
return indexedShape.hasArea() && indexedShape.relate(queryShape) == SpatialRelation.CONTAINS;
}
};
public static final SpatialOperation Intersects = new SpatialOperation("Intersects", true, false, false) {
@Override
public boolean evaluate(Shape indexedShape, Shape queryShape) {
return indexedShape.relate(queryShape).intersects();
}
};
public static final SpatialOperation IsEqualTo = new SpatialOperation("IsEqualTo", false, false, false) {
@Override
public boolean evaluate(Shape indexedShape, Shape queryShape) {
return indexedShape.equals(queryShape);
}
};
public static final SpatialOperation IsDisjointTo = new SpatialOperation("IsDisjointTo", false, false, false) {
@Override
public boolean evaluate(Shape indexedShape, Shape queryShape) {
return ! indexedShape.relate(queryShape).intersects();
}
};
public static final SpatialOperation IsWithin = new SpatialOperation("IsWithin", true, false, true) {
@Override
public boolean evaluate(Shape indexedShape, Shape queryShape) {
return queryShape.hasArea() && indexedShape.relate(queryShape) == SpatialRelation.WITHIN;
}
};
public static final SpatialOperation Overlaps = new SpatialOperation("Overlaps", true, false, true) {
@Override
public boolean evaluate(Shape indexedShape, Shape queryShape) {
return queryShape.hasArea() && indexedShape.relate(queryShape).intersects();
}
};
// Member variables // Member variables
private final boolean scoreIsMeaningful; private final boolean scoreIsMeaningful;
@ -90,6 +135,11 @@ public class SpatialOperation implements Serializable {
return false; return false;
} }
/**
* Returns whether the relationship between indexedShape and queryShape is
* satisfied by this operation.
*/
public abstract boolean evaluate(Shape indexedShape, Shape queryShape);
// ================================================= Getters / Setters ============================================= // ================================================= Getters / Setters =============================================

View File

@ -1,3 +1,5 @@
package org.apache.lucene.spatial;
/* /*
* Licensed to the Apache Software Foundation (ASF) under one or more * Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with * contributor license agreements. See the NOTICE file distributed with
@ -15,8 +17,9 @@
* limitations under the License. * limitations under the License.
*/ */
package org.apache.lucene.spatial; import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.shape.Point;
import com.spatial4j.core.shape.Rectangle;
import org.apache.lucene.document.Document; import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.RandomIndexWriter; import org.apache.lucene.index.RandomIndexWriter;
@ -35,6 +38,10 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomGaussian;
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomIntBetween;
/** A base test class for spatial lucene. It's mostly Lucene generic. */
public abstract class SpatialTestCase extends LuceneTestCase { public abstract class SpatialTestCase extends LuceneTestCase {
private DirectoryReader indexReader; private DirectoryReader indexReader;
@ -42,6 +49,8 @@ public abstract class SpatialTestCase extends LuceneTestCase {
private Directory directory; private Directory directory;
protected IndexSearcher indexSearcher; protected IndexSearcher indexSearcher;
protected SpatialContext ctx;//subclass must initialize
@Override @Override
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
@ -100,6 +109,63 @@ public abstract class SpatialTestCase extends LuceneTestCase {
} }
} }
protected Point randomPoint() {
final Rectangle WB = ctx.getWorldBounds();
return ctx.makePoint(
randomIntBetween((int) WB.getMinX(), (int) WB.getMaxX()),
randomIntBetween((int) WB.getMinY(), (int) WB.getMaxY()));
}
protected Rectangle randomRectangle() {
final Rectangle WB = ctx.getWorldBounds();
int rW = (int) randomGaussianMeanMax(10, WB.getWidth());
double xMin = randomIntBetween((int) WB.getMinX(), (int) WB.getMaxX() - rW);
double xMax = xMin + rW;
int yH = (int) randomGaussianMeanMax(Math.min(rW, WB.getHeight()), WB.getHeight());
double yMin = randomIntBetween((int) WB.getMinY(), (int) WB.getMaxY() - yH);
double yMax = yMin + yH;
return ctx.makeRectangle(xMin, xMax, yMin, yMax);
}
private double randomGaussianMinMeanMax(double min, double mean, double max) {
assert mean > min;
return randomGaussianMeanMax(mean - min, max - min) + min;
}
/**
* Within one standard deviation (68% of the time) the result is "close" to
* mean. By "close": when greater than mean, it's the lesser of 2*mean or half
* way to max, when lesser than mean, it's the greater of max-2*mean or half
* way to 0. The other 32% of the time it's in the rest of the range, touching
* either 0 or max but never exceeding.
*/
private double randomGaussianMeanMax(double mean, double max) {
// DWS: I verified the results empirically
assert mean <= max && mean >= 0;
double g = randomGaussian();
double mean2 = mean;
double flip = 1;
if (g < 0) {
mean2 = max - mean;
flip = -1;
g *= -1;
}
// pivot is the distance from mean2 towards max where the boundary of
// 1 standard deviation alters the calculation
double pivotMax = max - mean2;
double pivot = Math.min(mean2, pivotMax / 2);//from 0 to max-mean2
assert pivot >= 0 && pivotMax >= pivot && g >= 0;
double pivotResult;
if (g <= 1)
pivotResult = pivot * g;
else
pivotResult = Math.min(pivotMax, (g - 1) * (pivotMax - pivot) + pivot);
return mean + flip * pivotResult;
}
// ================================================= Inner Classes ================================================= // ================================================= Inner Classes =================================================
protected static class SearchResults { protected static class SearchResults {
@ -116,7 +182,7 @@ public abstract class SpatialTestCase extends LuceneTestCase {
StringBuilder str = new StringBuilder(); StringBuilder str = new StringBuilder();
str.append("found: ").append(numFound).append('['); str.append("found: ").append(numFound).append('[');
for(SearchResult r : results) { for(SearchResult r : results) {
String id = r.document.get("id"); String id = r.getId();
str.append(id).append(", "); str.append(id).append(", ");
} }
str.append(']'); str.append(']');
@ -139,6 +205,10 @@ public abstract class SpatialTestCase extends LuceneTestCase {
this.document = storedDocument; this.document = storedDocument;
} }
public String getId() {
return document.get("id");
}
@Override @Override
public String toString() { public String toString() {
return "["+score+"="+document+"]"; return "["+score+"="+document+"]";

View File

@ -82,6 +82,8 @@ public class SpatialTestQuery {
@Override @Override
public String toString() { public String toString() {
if (line != null)
return line; return line;
return args.toString()+" "+ids;
} }
} }

View File

@ -31,8 +31,9 @@ import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.CheckHits; import org.apache.lucene.search.CheckHits;
import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.TopDocs;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialArgsParser; import org.apache.lucene.spatial.query.SpatialArgsParser;
import org.junit.Assert; import org.apache.lucene.spatial.query.SpatialOperation;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
@ -42,6 +43,7 @@ import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -64,7 +66,6 @@ public abstract class StrategyTestCase extends SpatialTestCase {
protected final SpatialArgsParser argsParser = new SpatialArgsParser(); protected final SpatialArgsParser argsParser = new SpatialArgsParser();
protected SpatialStrategy strategy; protected SpatialStrategy strategy;
protected SpatialContext ctx;
protected boolean storeShape = true; protected boolean storeShape = true;
protected void executeQueries(SpatialMatchConcern concern, String... testQueryFile) throws IOException { protected void executeQueries(SpatialMatchConcern concern, String... testQueryFile) throws IOException {
@ -131,9 +132,13 @@ public abstract class StrategyTestCase extends SpatialTestCase {
SpatialMatchConcern concern) { SpatialMatchConcern concern) {
while (queries.hasNext()) { while (queries.hasNext()) {
SpatialTestQuery q = queries.next(); SpatialTestQuery q = queries.next();
runTestQuery(concern, q);
}
}
String msg = q.line; //"Query: " + q.args.toString(ctx); public void runTestQuery(SpatialMatchConcern concern, SpatialTestQuery q) {
SearchResults got = executeQuery(strategy.makeQuery(q.args), 100); String msg = q.toString(); //"Query: " + q.args.toString(ctx);
SearchResults got = executeQuery(strategy.makeQuery(q.args), Math.max(100, q.ids.size()+1));
if (storeShape && got.numFound > 0) { if (storeShape && got.numFound > 0) {
//check stored value is there & parses //check stored value is there & parses
assertNotNull(ctx.readShape(got.results.get(0).document.get(strategy.getFieldName()))); assertNotNull(ctx.readShape(got.results.get(0).document.get(strategy.getFieldName())));
@ -143,13 +148,13 @@ public abstract class StrategyTestCase extends SpatialTestCase {
for (SearchResult r : got.results) { for (SearchResult r : got.results) {
String id = r.document.get("id"); String id = r.document.get("id");
if (!ids.hasNext()) { if (!ids.hasNext()) {
Assert.fail(msg + " :: Did not get enough results. Expect" + q.ids+", got: "+got.toDebugString()); fail(msg + " :: Did not get enough results. Expect" + q.ids + ", got: " + got.toDebugString());
} }
Assert.assertEquals( "out of order: " + msg, ids.next(), id); assertEquals("out of order: " + msg, ids.next(), id);
} }
if (ids.hasNext()) { if (ids.hasNext()) {
Assert.fail(msg + " :: expect more results then we got: " + ids.next()); fail(msg + " :: expect more results then we got: " + ids.next());
} }
} else { } else {
// We are looking at how the results overlap // We are looking at how the results overlap
@ -160,11 +165,10 @@ public abstract class StrategyTestCase extends SpatialTestCase {
} }
for (String s : q.ids) { for (String s : q.ids) {
if (!found.contains(s)) { if (!found.contains(s)) {
Assert.fail( "Results are mising id: "+s + " :: " + found ); fail("Results are mising id: " + s + " :: " + found);
} }
} }
} } else {
else {
List<String> found = new ArrayList<String>(); List<String> found = new ArrayList<String>();
for (SearchResult r : got.results) { for (SearchResult r : got.results) {
found.add(r.document.get("id")); found.add(r.document.get("id"));
@ -173,8 +177,7 @@ public abstract class StrategyTestCase extends SpatialTestCase {
// sort both so that the order is not important // sort both so that the order is not important
Collections.sort(q.ids); Collections.sort(q.ids);
Collections.sort(found); Collections.sort(found);
Assert.assertEquals(msg, q.ids.toString(), found.toString()); assertEquals(msg, q.ids.toString(), found.toString());
}
} }
} }
} }
@ -221,4 +224,19 @@ public abstract class StrategyTestCase extends SpatialTestCase {
CheckHits.checkExplanations(q, "", indexSearcher); CheckHits.checkExplanations(q, "", indexSearcher);
} }
protected void assertOperation(Map<String,Shape> indexedDocs,
SpatialOperation operation, Shape queryShape) {
//Generate truth via brute force
Set<String> expectedIds = new HashSet<String>();
for (Map.Entry<String, Shape> stringShapeEntry : indexedDocs.entrySet()) {
if (operation.evaluate(stringShapeEntry.getValue(), queryShape))
expectedIds.add(stringShapeEntry.getKey());
}
SpatialTestQuery testQuery = new SpatialTestQuery();
testQuery.args = new SpatialArgs(operation, queryShape);
testQuery.ids = new ArrayList<String>(expectedIds);
runTestQuery(SpatialMatchConcern.FILTER, testQuery);
}
} }

View File

@ -0,0 +1,133 @@
package org.apache.lucene.spatial.prefix;
/*
* 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.carrotsearch.randomizedtesting.annotations.Repeat;
import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.shape.Rectangle;
import com.spatial4j.core.shape.Shape;
import com.spatial4j.core.shape.impl.RectangleImpl;
import org.apache.lucene.search.Query;
import org.apache.lucene.spatial.StrategyTestCase;
import org.apache.lucene.spatial.prefix.tree.Node;
import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree;
import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.junit.Test;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomInt;
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomIntBetween;
public class SpatialOpRecursivePrefixTreeTest extends StrategyTestCase {
private SpatialPrefixTree grid;
@Test
@Repeat(iterations = 20)
public void testIntersects() throws IOException {
//non-geospatial makes this test a little easier
this.ctx = new SpatialContext(false, null, new RectangleImpl(0, 256, -128, 128, null));
//A fairly shallow grid, and default 2.5% distErrPct
this.grid = new QuadPrefixTree(ctx, randomIntBetween(1, 8));
this.strategy = new RecursivePrefixTreeStrategy(grid, getClass().getSimpleName());
//((PrefixTreeStrategy) strategy).setDistErrPct(0);//fully precise to grid
deleteAll();
Map<String, Shape> indexedShapes = new LinkedHashMap<String, Shape>();
Map<String, Rectangle> indexedGriddedShapes = new LinkedHashMap<String, Rectangle>();
final int numIndexedShapes = randomIntBetween(1, 6);
for (int i = 1; i <= numIndexedShapes; i++) {
String id = "" + i;
Shape indexShape = randomRectangle();
Rectangle gridShape = gridSnapp(indexShape);
indexedShapes.put(id, indexShape);
indexedGriddedShapes.put(id, gridShape);
adoc(id, indexShape);
}
commit();
final int numQueryShapes = atLeast(10);
for (int i = 0; i < numQueryShapes; i++) {
int scanLevel = randomInt(grid.getMaxLevels());
((RecursivePrefixTreeStrategy) strategy).setPrefixGridScanLevel(scanLevel);
Rectangle queryShape = randomRectangle();
Rectangle queryGridShape = gridSnapp(queryShape);
//Generate truth via brute force
final SpatialOperation operation = SpatialOperation.Intersects;
Set<String> expectedIds = new TreeSet<String>();
Set<String> optionalIds = new TreeSet<String>();
for (String id : indexedShapes.keySet()) {
Shape indexShape = indexedShapes.get(id);
Rectangle indexGridShape = indexedGriddedShapes.get(id);
if (operation.evaluate(indexShape, queryShape))
expectedIds.add(id);
else if (operation.evaluate(indexGridShape, queryGridShape))
optionalIds.add(id);
}
//Search and verify results
Query query = strategy.makeQuery(new SpatialArgs(operation, queryShape));
SearchResults got = executeQuery(query, 100);
Set<String> remainingExpectedIds = new TreeSet<String>(expectedIds);
String msg = queryShape.toString()+" Expect: "+expectedIds+" Opt: "+optionalIds;
for (SearchResult result : got.results) {
String id = result.getId();
Object removed = remainingExpectedIds.remove(id);
if (removed == null) {
assertTrue("Shouldn't match " + id + " in "+msg, optionalIds.contains(id));
}
}
assertTrue("Didn't match " + remainingExpectedIds + " in " + msg, remainingExpectedIds.isEmpty());
}
}
protected Rectangle gridSnapp(Shape snapMe) {
//The next 4 lines mimic PrefixTreeStrategy.createIndexableFields()
double distErrPct = ((PrefixTreeStrategy) strategy).getDistErrPct();
double distErr = SpatialArgs.calcDistanceFromErrPct(snapMe, distErrPct, ctx);
int detailLevel = grid.getLevelForDistance(distErr);
List<Node> cells = grid.getNodes(snapMe, detailLevel, false);
//calc bounding box of cells.
double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY, maxY = Double.NEGATIVE_INFINITY;
for (Node cell : cells) {
assert cell.getLevel() <= detailLevel;
Rectangle cellR = cell.getShape().getBoundingBox();
minX = Math.min(minX, cellR.getMinX());
maxX = Math.max(maxX, cellR.getMaxX());
minY = Math.min(minY, cellR.getMinY());
maxY = Math.max(maxY, cellR.getMaxY());
}
return ctx.makeRectangle(minX, maxX, minY, maxY);
}
}