DATACOUCH-176 - Geo false positive autoelimination

So far with geo queries a bounding box approximation was performed on Circle and Polygon queries (since Couchbase internally only supports Bounding Box queries).

It was confusing since in previous iteration it was up to the user to remove these false positives "after the fact" in code calling the repository.

This change allows for the framework to retain enough information to perform false positives elimination automatically and transparently for the user.
This commit is contained in:
Simon Baslé
2015-11-17 16:50:18 -05:00
parent b78ed124ec
commit aa3f9c380b
7 changed files with 199 additions and 110 deletions

View File

@@ -16,14 +16,26 @@
package org.springframework.data.couchbase.repository.query;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.List;
import com.couchbase.client.java.view.SpatialViewQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.core.query.Dimensional;
import org.springframework.data.couchbase.repository.query.support.GeoUtils;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
/**
@@ -64,17 +76,20 @@ public class SpatialViewBasedQuery implements RepositoryQuery {
SpatialViewQueryCreator creator = new SpatialViewQueryCreator(dimensions,
tree, new ParametersParameterAccessor(method.getParameters(), runtimeParams),
baseSpatialQuery, operations.getConverter());
SpatialViewQuery finalQuery = creator.createQuery();
SpatialViewQueryCreator.SpatialViewQueryWrapper finalQuery = creator.createQuery();
//execute the spatial query
return execute(finalQuery);
}
protected Object execute(SpatialViewQuery query) {
protected Object execute(SpatialViewQueryCreator.SpatialViewQueryWrapper query) {
if (LOG.isDebugEnabled()) {
LOG.debug("Executing spatial view query: " + query.toString());
LOG.debug("Executing spatial view query: " + query.getQuery().toString());
}
return operations.findBySpatialView(query, method.getEntityInformation().getJavaType());
return query.eliminate(
operations.findBySpatialView(query.getQuery(), method.getEntityInformation().getJavaType())
);
}
@Override

View File

@@ -16,18 +16,30 @@
package org.springframework.data.couchbase.repository.query;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.view.SpatialViewQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.query.Dimensional;
import org.springframework.data.couchbase.repository.query.support.AwtPointInShapeEvaluator;
import org.springframework.data.couchbase.repository.query.support.GeoUtils;
import org.springframework.data.couchbase.repository.query.support.PointInShapeEvaluator;
import org.springframework.data.domain.Sort;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Polygon;
import org.springframework.data.geo.Shape;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
@@ -58,7 +70,9 @@ import org.springframework.data.repository.query.parser.PartTree;
* </p>
* Additionally, {@link PartTree#isLimiting()} will trigger usage of {@link SpatialViewQuery#limit(int) limit}.
*/
public class SpatialViewQueryCreator extends AbstractQueryCreator<SpatialViewQuery, SpatialViewQuery> {
public class SpatialViewQueryCreator extends AbstractQueryCreator<SpatialViewQueryCreator.SpatialViewQueryWrapper, SpatialViewQuery> {
private static final Logger LOGGER = LoggerFactory.getLogger(SpatialViewQueryCreator.class);
private final SpatialViewQuery query;
private final PartTree tree;
@@ -67,6 +81,7 @@ public class SpatialViewQueryCreator extends AbstractQueryCreator<SpatialViewQue
private final int dimensions;
private final JsonArray startRange;
private final JsonArray endRange;
private final List<AbstractFalsePositiveEvaluator> evaluators;
public SpatialViewQueryCreator(int dimensions, PartTree tree, ParameterAccessor parameters, SpatialViewQuery query,
CouchbaseConverter converter) {
@@ -77,6 +92,7 @@ public class SpatialViewQueryCreator extends AbstractQueryCreator<SpatialViewQue
this.dimensions = dimensions;
this.startRange = JsonArray.create();
this.endRange = JsonArray.create();
this.evaluators = new ArrayList<AbstractFalsePositiveEvaluator>();
}
@Override
@@ -85,10 +101,10 @@ public class SpatialViewQueryCreator extends AbstractQueryCreator<SpatialViewQue
switch (part.getType()) {
case WITHIN:
applyWithin(startRange, endRange, iterator);
applyWithin(startRange, endRange, iterator, evaluators, part.getProperty());
break;
case NEAR:
applyNear(startRange, endRange, iterator);
applyNear(startRange, endRange, iterator, evaluators, part.getProperty());
break;
case GREATER_THAN:
case GREATER_THAN_EQUAL:
@@ -125,20 +141,28 @@ public class SpatialViewQueryCreator extends AbstractQueryCreator<SpatialViewQue
}
}
private static void applyWithin(JsonArray startRange, JsonArray endRange, Iterator<Object> iterator) {
private static void applyWithin(JsonArray startRange, JsonArray endRange, Iterator<Object> iterator,
List<AbstractFalsePositiveEvaluator> evaluators, PropertyPath path) {
if (!iterator.hasNext()) {
throw new IllegalArgumentException("Not enough parameters for within");
}
Object next = iterator.next();
if (next instanceof Shape) {
GeoUtils.convertShapeTo2DRanges(startRange, endRange, (Shape) next);
if (next instanceof Circle) {
evaluators.add(new CircleFalsePositiveEvaluator(path, (Circle) next));
GeoUtils.convertShapeTo2DRanges(startRange, endRange, (Circle) next);
} else if (next instanceof Polygon) {
evaluators.add(new PolygonFalsePositiveEvaluator(path, (Polygon) next));
GeoUtils.convertShapeTo2DRanges(startRange, endRange, (Polygon) next);
} else if (next instanceof Box) {
GeoUtils.convertShapeTo2DRanges(startRange, endRange, (Box) next);
} else if (next instanceof Point) {
//expect another point for the other corner of the bounding box
Point northwest = (Point) next;
Point southeast = checkedNext(iterator, Point.class, "Cannot compute a bounding box for within, 2 Point needed");
GeoUtils.convertPointsTo2DRanges(startRange, endRange, true, northwest, southeast);
} else if (next instanceof Point[]) {
evaluators.add(new PointArrayFalsePositiveEvaluator(path, (Point[]) next));
GeoUtils.convertPointsTo2DRanges(startRange, endRange, false, (Point[]) next);
} else if (next instanceof JsonArray) { //discouraged, leaks Couchbase classes into signatures
JsonArray first = (JsonArray) next;
@@ -151,14 +175,12 @@ public class SpatialViewQueryCreator extends AbstractQueryCreator<SpatialViewQue
endRange.add(o);
}
} else {
//TODO Collection<Point>: same as polygon
//TODO Collection<Double/Number>: one collection per range
//TODO Number[]: one array per range
throw new IllegalArgumentException("Unsupported parameter type for within: " + next.getClass());
}
}
private static void applyNear(JsonArray startRange, JsonArray endRange, Iterator<Object> iterator) {
private static void applyNear(JsonArray startRange, JsonArray endRange, Iterator<Object> iterator,
List<AbstractFalsePositiveEvaluator> evaluators, PropertyPath path) {
if (!iterator.hasNext()) {
throw new IllegalArgumentException("Not enough parameters for near");
}
@@ -166,6 +188,8 @@ public class SpatialViewQueryCreator extends AbstractQueryCreator<SpatialViewQue
Point near = checkedNext(iterator, Point.class, "Near queries need a Point as first argument");
Distance distance = checkedNext(iterator, Distance.class, "Near queries need a maximum Distance as second argument");
evaluators.add(new CircleFalsePositiveEvaluator(path, new Circle(near, distance)));
double[] boundingBox = GeoUtils.getBoundingBoxForNear(near, distance);
startRange.add(boundingBox[0]).add(boundingBox[1]);
@@ -215,7 +239,7 @@ public class SpatialViewQueryCreator extends AbstractQueryCreator<SpatialViewQue
}
@Override
protected SpatialViewQuery complete(SpatialViewQuery criteria, Sort sort) {
protected SpatialViewQueryWrapper complete(SpatialViewQuery criteria, Sort sort) {
if (sort != null) {
throw new IllegalArgumentException("Sort is not supported on Spatial View queries");
}
@@ -225,11 +249,113 @@ public class SpatialViewQueryCreator extends AbstractQueryCreator<SpatialViewQue
}
if (startRange.isEmpty() && endRange.isEmpty()) {
return query;
return new SpatialViewQueryWrapper(query, evaluators);
}
completeRangeIfNeeded(startRange, dimensions);
completeRangeIfNeeded(endRange, dimensions);
return query.range(startRange, endRange);
return new SpatialViewQueryWrapper(query.range(startRange, endRange), evaluators);
}
}
public static abstract class AbstractFalsePositiveEvaluator {
protected static final PointInShapeEvaluator POINT_IN_SHAPE = new AwtPointInShapeEvaluator();
protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractFalsePositiveEvaluator.class);
protected final PropertyPath propertyPath;
protected AbstractFalsePositiveEvaluator(PropertyPath path) {
this.propertyPath = path;
}
public boolean evaluate(Object original, BeanWrapper bean) {
Object value = bean.getPropertyValue(propertyPath.toDotPath());//TODO use the aliases?
if (value instanceof Point) {
return evaluateCriteria((Point) value);
} else if (value == null) {
LOGGER.trace("Cannot find a Point (was null) for attribute {}, object is {}", propertyPath.toDotPath(), original);
return false;
} else {
LOGGER.trace("Cannot find a Point (was {}) for attribute {}, object is {}", value.getClass().getName(),
propertyPath.toDotPath(), original);
return false;
}
}
protected abstract boolean evaluateCriteria(Point p);
}
public static class CircleFalsePositiveEvaluator extends AbstractFalsePositiveEvaluator {
private final Circle criteria;
public CircleFalsePositiveEvaluator(PropertyPath path, Circle criteria) {
super(path);
this.criteria = criteria;
}
protected boolean evaluateCriteria(Point p) {
return POINT_IN_SHAPE.pointInCircle(p, criteria);
}
}
public static class PolygonFalsePositiveEvaluator extends AbstractFalsePositiveEvaluator {
private final Polygon criteria;
public PolygonFalsePositiveEvaluator(PropertyPath path, Polygon criteria) {
super(path);
this.criteria = criteria;
}
@Override
protected boolean evaluateCriteria(Point p) {
return POINT_IN_SHAPE.pointInPolygon(p, criteria);
}
}
public static final class PointArrayFalsePositiveEvaluator extends AbstractFalsePositiveEvaluator {
private final Point[] criteria;
public PointArrayFalsePositiveEvaluator(PropertyPath path, Point[] criteria) {
super(path);
this.criteria = criteria;
}
@Override
protected boolean evaluateCriteria(Point p) {
return POINT_IN_SHAPE.pointInPolygon(p, criteria);
}
}
public static class SpatialViewQueryWrapper {
private SpatialViewQuery query;
private List<AbstractFalsePositiveEvaluator> eliminators;
public SpatialViewQueryWrapper(SpatialViewQuery query, List<AbstractFalsePositiveEvaluator> eliminators) {
this.query = query;
this.eliminators = eliminators;
}
public SpatialViewQuery getQuery() {
return query;
}
public <T> List<T> eliminate(List<T> objects) {
List<T> result = new ArrayList<T>(objects.size());
for (T object : objects) {
BeanWrapper bean = new BeanWrapperImpl(object);
boolean pass = true;
for (AbstractFalsePositiveEvaluator eliminator : eliminators) {
pass = pass && eliminator.evaluate(object, bean);
}
if (pass) {
result.add(object);
} else {
LOGGER.trace("Object {} was a false positive in geo query", object);
}
}
return result;
}
}
}

View File

@@ -34,8 +34,6 @@ import org.springframework.data.geo.Shape;
*/
public class GeoUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(GeoUtils.class);
/**
* Computes the bounding box approximation for a "near" query (max distance from a point of origin).
*
@@ -46,7 +44,6 @@ public class GeoUtils {
*/
public static double[] getBoundingBoxForNear(Point origin, Distance distance) {
if (origin == null || distance == null) throw new NullPointerException("Origin and distance required");
warnBoundingBoxApproximation("near a Point (within Distance)");
//since maxDistance COULD be negative, we have to make sure we have correct min/max
double maxDistance = Math.abs(distance.getNormalizedValue());
@@ -90,7 +87,6 @@ public class GeoUtils {
endRange.add(points[1].getX()).add(points[1].getY());
} else {
//this is like a polygon, find the bounding box
warnBoundingBoxApproximation("within a sequence of Points (polygon)");
//find the lowest and highest X and Y to get the bounding box
double xMin = Double.POSITIVE_INFINITY;
double yMin = Double.POSITIVE_INFINITY;
@@ -128,7 +124,6 @@ public class GeoUtils {
.add(box.getSecond().getX())
.add(box.getSecond().getY());
} else if (shape instanceof Polygon) {
warnBoundingBoxApproximation("within a Polygon");
//find the lowest and highest X and Y to get the bounding box
double xMin = Double.POSITIVE_INFINITY;
double yMin = Double.POSITIVE_INFINITY;
@@ -146,7 +141,6 @@ public class GeoUtils {
startRange.add(xMin).add(yMin);
endRange.add(xMax).add(yMax);
} else if (shape instanceof Circle) {
warnBoundingBoxApproximation("within a Circle");
//here the bounding box is the box that contains the circle
Circle circle = (Circle) shape;
Point center = circle.getCenter();
@@ -163,11 +157,4 @@ public class GeoUtils {
throw new IllegalArgumentException("Unsupported shape " + shape.getClass().getName());
}
}
private static void warnBoundingBoxApproximation(String kind) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Spatial View queries " + kind + " are made using a bounding box approximation," +
"you probably want to remove false positives by applying a PointInShapeEvaluator");
}
}
}