mirror of https://github.com/apache/lucene.git
LUCENE-9238: Add new XYPointField, queries and sorting capabilities (#1272)
New XYPointField field and Queries for indexing, searching and sorting cartesian points.
This commit is contained in:
parent
cb68d7d2c5
commit
88dd1c3f3d
|
@ -121,6 +121,9 @@ New Features
|
|||
|
||||
* LUCENE-8707: Add LatLonShape and XYShape distance query. (Ignacio Vera)
|
||||
|
||||
* LUCENE-9238: New XYPointField field and Queries for indexing, searching and sorting
|
||||
cartesian points. (Ignacio Vera)
|
||||
|
||||
Improvements
|
||||
---------------------
|
||||
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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.document;
|
||||
|
||||
import org.apache.lucene.geo.XYCircle;
|
||||
import org.apache.lucene.geo.XYEncodingUtils;
|
||||
import org.apache.lucene.geo.XYPolygon;
|
||||
import org.apache.lucene.geo.XYRectangle;
|
||||
import org.apache.lucene.index.DocValuesType;
|
||||
import org.apache.lucene.index.FieldInfo;
|
||||
import org.apache.lucene.search.FieldDoc;
|
||||
import org.apache.lucene.search.IndexOrDocValuesQuery;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.search.SortField;
|
||||
|
||||
|
||||
/**
|
||||
* An per-document location field.
|
||||
* <p>
|
||||
* Sorting by distance is efficient. Multiple values for the same field in one document
|
||||
* is allowed.
|
||||
* <p>
|
||||
* This field defines static factory methods for common operations:
|
||||
* <ul>
|
||||
* <li>{@link #newDistanceSort newDistanceSort()} for ordering documents by distance from a specified location.
|
||||
* </ul>
|
||||
* <p>
|
||||
* If you also need query operations, you should add a separate {@link XYPointField} instance.
|
||||
* If you also need to store the value, you should add a separate {@link StoredField} instance.
|
||||
*
|
||||
* @see XYPointField
|
||||
*/
|
||||
public class XYDocValuesField extends Field {
|
||||
|
||||
/**
|
||||
* Type for a XYDocValuesField
|
||||
* <p>
|
||||
* Each value stores a 64-bit long where the upper 32 bits are the encoded x value,
|
||||
* and the lower 32 bits are the encoded y value.
|
||||
* @see org.apache.lucene.geo.XYEncodingUtils#decode(int)
|
||||
*/
|
||||
public static final FieldType TYPE = new FieldType();
|
||||
static {
|
||||
TYPE.setDocValuesType(DocValuesType.SORTED_NUMERIC);
|
||||
TYPE.freeze();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new XYDocValuesField with the specified x and y
|
||||
* @param name field name
|
||||
* @param x x value.
|
||||
* @param y y values.
|
||||
* @throws IllegalArgumentException if the field name is null or x or y are infinite or NaN.
|
||||
*/
|
||||
public XYDocValuesField(String name, float x, float y) {
|
||||
super(name, TYPE);
|
||||
setLocationValue(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the values of this field
|
||||
* @param x x value.
|
||||
* @param y y value.
|
||||
* @throws IllegalArgumentException if x or y are infinite or NaN.
|
||||
*/
|
||||
public void setLocationValue(float x, float y) {
|
||||
int xEncoded = XYEncodingUtils.encode(x);
|
||||
int yEncoded = XYEncodingUtils.encode(y);
|
||||
fieldsData = Long.valueOf((((long) xEncoded) << 32) | (yEncoded & 0xFFFFFFFFL));
|
||||
}
|
||||
|
||||
/** helper: checks a fieldinfo and throws exception if its definitely not a XYDocValuesField */
|
||||
static void checkCompatible(FieldInfo fieldInfo) {
|
||||
// dv properties could be "unset", if you e.g. used only StoredField with this same name in the segment.
|
||||
if (fieldInfo.getDocValuesType() != DocValuesType.NONE && fieldInfo.getDocValuesType() != TYPE.docValuesType()) {
|
||||
throw new IllegalArgumentException("field=\"" + fieldInfo.name + "\" was indexed with docValuesType=" + fieldInfo.getDocValuesType() +
|
||||
" but this type has docValuesType=" + TYPE.docValuesType() +
|
||||
", is the field really a XYDocValuesField?");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append(getClass().getSimpleName());
|
||||
result.append(" <");
|
||||
result.append(name);
|
||||
result.append(':');
|
||||
|
||||
long currentValue = (Long)fieldsData;
|
||||
result.append(XYEncodingUtils.decode((int)(currentValue >> 32)));
|
||||
result.append(',');
|
||||
result.append(XYEncodingUtils.decode((int)(currentValue & 0xFFFFFFFF)));
|
||||
|
||||
result.append('>');
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SortField for sorting by distance from a location.
|
||||
* <p>
|
||||
* This sort orders documents by ascending distance from the location. The value returned in {@link FieldDoc} for
|
||||
* the hits contains a Double instance with the distance in meters.
|
||||
* <p>
|
||||
* If a document is missing the field, then by default it is treated as having {@link Double#POSITIVE_INFINITY} distance
|
||||
* (missing values sort last).
|
||||
* <p>
|
||||
* If a document contains multiple values for the field, the <i>closest</i> distance to the location is used.
|
||||
*
|
||||
* @param field field name. must not be null.
|
||||
* @param x x at the center.
|
||||
* @param y y at the center.
|
||||
* @return SortField ordering documents by distance
|
||||
* @throws IllegalArgumentException if {@code field} is null or location has invalid coordinates.
|
||||
*/
|
||||
public static SortField newDistanceSort(String field, float x, float y) {
|
||||
return new XYPointSortField(field, x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query for matching a bounding box using doc values.
|
||||
* This query is usually slow as it does not use an index structure and needs
|
||||
* to verify documents one-by-one in order to know whether they match. It is
|
||||
* best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
|
||||
* {@link XYPointField#newBoxQuery}.
|
||||
*/
|
||||
public static Query newSlowBoxQuery(String field, float minX, float maxX, float minY, float maxY) {
|
||||
XYRectangle rectangle = new XYRectangle(minX, maxX, minY, maxY);
|
||||
return new XYDocValuesPointInGeometryQuery(field, rectangle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query for matching points within the specified distance of the supplied location.
|
||||
* This query is usually slow as it does not use an index structure and needs
|
||||
* to verify documents one-by-one in order to know whether they match. It is
|
||||
* best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
|
||||
* {@link XYPointField#newDistanceQuery}.
|
||||
* @param field field name. must not be null.
|
||||
* @param x x at the center.
|
||||
* @param y y at the center: must be within standard +/-180 coordinate bounds.
|
||||
* @param radius maximum distance from the center in cartesian distance: must be non-negative and finite.
|
||||
* @return query matching points within this distance
|
||||
* @throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or radius is invalid.
|
||||
*/
|
||||
public static Query newSlowDistanceQuery(String field, float x, float y, float radius) {
|
||||
XYCircle circle = new XYCircle(x, y, radius);
|
||||
return new XYDocValuesPointInGeometryQuery(field, circle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query for matching points within the supplied polygons.
|
||||
* This query is usually slow as it does not use an index structure and needs
|
||||
* to verify documents one-by-one in order to know whether they match. It is
|
||||
* best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
|
||||
* {@link XYPointField#newPolygonQuery(String, XYPolygon...)}.
|
||||
* @param field field name. must not be null.
|
||||
* @param polygons array of polygons. must not be null or empty.
|
||||
* @return query matching points within the given polygons.
|
||||
* @throws IllegalArgumentException if {@code field} is null or polygons is empty or contain a null polygon.
|
||||
*/
|
||||
public static Query newSlowPolygonQuery(String field, XYPolygon... polygons) {
|
||||
return new XYDocValuesPointInGeometryQuery(field, polygons);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* 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.document;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import org.apache.lucene.geo.Component2D;
|
||||
import org.apache.lucene.geo.XYEncodingUtils;
|
||||
import org.apache.lucene.geo.XYGeometry;
|
||||
import org.apache.lucene.index.DocValues;
|
||||
import org.apache.lucene.index.LeafReaderContext;
|
||||
import org.apache.lucene.index.SortedNumericDocValues;
|
||||
import org.apache.lucene.search.ConstantScoreScorer;
|
||||
import org.apache.lucene.search.ConstantScoreWeight;
|
||||
import org.apache.lucene.search.IndexSearcher;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.search.QueryVisitor;
|
||||
import org.apache.lucene.search.ScoreMode;
|
||||
import org.apache.lucene.search.Scorer;
|
||||
import org.apache.lucene.search.TwoPhaseIterator;
|
||||
import org.apache.lucene.search.Weight;
|
||||
|
||||
/** XYGeometry query for {@link XYDocValuesField}. */
|
||||
public class XYDocValuesPointInGeometryQuery extends Query {
|
||||
|
||||
private final String field;
|
||||
private final XYGeometry[] geometries;
|
||||
|
||||
|
||||
XYDocValuesPointInGeometryQuery(String field, XYGeometry... geometries) {
|
||||
if (field == null) {
|
||||
throw new IllegalArgumentException("field must not be null");
|
||||
}
|
||||
if (geometries == null) {
|
||||
throw new IllegalArgumentException("geometries must not be null");
|
||||
}
|
||||
if (geometries.length == 0) {
|
||||
throw new IllegalArgumentException("geometries must not be empty");
|
||||
}
|
||||
for (int i = 0; i < geometries.length; i++) {
|
||||
if (geometries[i] == null) {
|
||||
throw new IllegalArgumentException("geometries[" + i + "] must not be null");
|
||||
}
|
||||
}
|
||||
this.field = field;
|
||||
this.geometries = geometries.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String field) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (!this.field.equals(field)) {
|
||||
sb.append(this.field);
|
||||
sb.append(':');
|
||||
}
|
||||
sb.append("geometries(").append(Arrays.toString(geometries));
|
||||
return sb.append(")").toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (sameClassAs(obj) == false) {
|
||||
return false;
|
||||
}
|
||||
XYDocValuesPointInGeometryQuery other = (XYDocValuesPointInGeometryQuery) obj;
|
||||
return field.equals(other.field) &&
|
||||
Arrays.equals(geometries, other.geometries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int h = classHash();
|
||||
h = 31 * h + field.hashCode();
|
||||
h = 31 * h + Arrays.hashCode(geometries);
|
||||
return h;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(QueryVisitor visitor) {
|
||||
if (visitor.acceptField(field)) {
|
||||
visitor.visitLeaf(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
|
||||
|
||||
return new ConstantScoreWeight(this, boost) {
|
||||
|
||||
final Component2D component2D = XYGeometry.create(geometries);
|
||||
|
||||
@Override
|
||||
public Scorer scorer(LeafReaderContext context) throws IOException {
|
||||
final SortedNumericDocValues values = context.reader().getSortedNumericDocValues(field);
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final TwoPhaseIterator iterator = new TwoPhaseIterator(values) {
|
||||
|
||||
@Override
|
||||
public boolean matches() throws IOException {
|
||||
for (int i = 0, count = values.docValueCount(); i < count; ++i) {
|
||||
final long value = values.nextValue();
|
||||
final double x = XYEncodingUtils.decode((int) (value >>> 32));
|
||||
final double y = XYEncodingUtils.decode((int) (value & 0xFFFFFFFF));
|
||||
if (component2D.contains(x, y)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float matchCost() {
|
||||
return 1000f; // TODO: what should it be?
|
||||
}
|
||||
};
|
||||
return new ConstantScoreScorer(this, boost, scoreMode, iterator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCacheable(LeafReaderContext ctx) {
|
||||
return DocValues.isCacheable(ctx, field);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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.document;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.lucene.geo.XYEncodingUtils;
|
||||
import org.apache.lucene.index.DocValues;
|
||||
import org.apache.lucene.index.FieldInfo;
|
||||
import org.apache.lucene.index.LeafReader;
|
||||
import org.apache.lucene.index.LeafReaderContext;
|
||||
import org.apache.lucene.index.SortedNumericDocValues;
|
||||
import org.apache.lucene.search.FieldComparator;
|
||||
import org.apache.lucene.search.LeafFieldComparator;
|
||||
import org.apache.lucene.search.Scorable;
|
||||
import org.apache.lucene.util.ArrayUtil;
|
||||
|
||||
/**
|
||||
* Compares documents by distance from an origin point
|
||||
* <p>
|
||||
* When the least competitive item on the priority queue changes (setBottom), we recompute
|
||||
* a bounding box representing competitive distance to the top-N. Then in compareBottom, we can
|
||||
* quickly reject hits based on bounding box alone without computing distance for every element.
|
||||
*/
|
||||
class XYPointDistanceComparator extends FieldComparator<Double> implements LeafFieldComparator {
|
||||
final String field;
|
||||
final double x;
|
||||
final double y;
|
||||
|
||||
// distances needs to be calculated with square root to
|
||||
// avoid numerical issues (square distances are different but
|
||||
// actual distances are equal)
|
||||
final double[] values;
|
||||
double bottom;
|
||||
double topValue;
|
||||
SortedNumericDocValues currentDocs;
|
||||
|
||||
// current bounding box(es) for the bottom distance on the PQ.
|
||||
// these are pre-encoded with XYPoint's encoding and
|
||||
// used to exclude uncompetitive hits faster.
|
||||
int minX = Integer.MIN_VALUE;
|
||||
int maxX = Integer.MAX_VALUE;
|
||||
int minY = Integer.MIN_VALUE;
|
||||
int maxY = Integer.MAX_VALUE;
|
||||
|
||||
// the number of times setBottom has been called (adversary protection)
|
||||
int setBottomCounter = 0;
|
||||
|
||||
private long[] currentValues = new long[4];
|
||||
private int valuesDocID = -1;
|
||||
|
||||
public XYPointDistanceComparator(String field, float x, float y, int numHits) {
|
||||
this.field = field;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.values = new double[numHits];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScorer(Scorable scorer) {}
|
||||
|
||||
@Override
|
||||
public int compare(int slot1, int slot2) {
|
||||
return Double.compare(values[slot1], values[slot2]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBottom(int slot) {
|
||||
bottom = values[slot];
|
||||
// make bounding box(es) to exclude non-competitive hits, but start
|
||||
// sampling if we get called way too much: don't make gobs of bounding
|
||||
// boxes if comparator hits a worst case order (e.g. backwards distance order)
|
||||
if (setBottomCounter < 1024 || (setBottomCounter & 0x3F) == 0x3F) {
|
||||
|
||||
// pre-encode our box to our integer encoding, so we don't have to decode
|
||||
// to double values for uncompetitive hits. This has some cost!
|
||||
this.minX = XYEncodingUtils.encode((float) Math.max(-Float.MAX_VALUE, x - bottom));
|
||||
this.maxX = XYEncodingUtils.encode((float) Math.min(Float.MAX_VALUE, x + bottom));
|
||||
this.minY = XYEncodingUtils.encode((float) Math.max(-Float.MAX_VALUE, y - bottom));
|
||||
this.maxY = XYEncodingUtils.encode((float) Math.min(Float.MAX_VALUE, y + bottom));
|
||||
}
|
||||
setBottomCounter++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTopValue(Double value) {
|
||||
topValue = value.doubleValue();
|
||||
}
|
||||
|
||||
private void setValues() throws IOException {
|
||||
if (valuesDocID != currentDocs.docID()) {
|
||||
assert valuesDocID < currentDocs.docID(): " valuesDocID=" + valuesDocID + " vs " + currentDocs.docID();
|
||||
valuesDocID = currentDocs.docID();
|
||||
int count = currentDocs.docValueCount();
|
||||
if (count > currentValues.length) {
|
||||
currentValues = new long[ArrayUtil.oversize(count, Long.BYTES)];
|
||||
}
|
||||
for(int i=0;i<count;i++) {
|
||||
currentValues[i] = currentDocs.nextValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareBottom(int doc) throws IOException {
|
||||
if (doc > currentDocs.docID()) {
|
||||
currentDocs.advance(doc);
|
||||
}
|
||||
if (doc < currentDocs.docID()) {
|
||||
return Double.compare(bottom, Double.POSITIVE_INFINITY);
|
||||
}
|
||||
|
||||
setValues();
|
||||
|
||||
int numValues = currentDocs.docValueCount();
|
||||
|
||||
int cmp = -1;
|
||||
for (int i = 0; i < numValues; i++) {
|
||||
long encoded = currentValues[i];
|
||||
|
||||
// test bounding box
|
||||
int xBits = (int)(encoded >> 32);
|
||||
if (xBits < minX || xBits > maxX) {
|
||||
continue;
|
||||
}
|
||||
int yBits = (int)(encoded & 0xFFFFFFFF);
|
||||
if (yBits < minY || yBits > maxY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// only compute actual distance if its inside "competitive bounding box"
|
||||
double docX = XYEncodingUtils.decode(xBits);
|
||||
double docY = XYEncodingUtils.decode(yBits);
|
||||
final double diffX = x - docX;
|
||||
final double diffY = y - docY;
|
||||
double distance = Math.sqrt(diffX * diffX + diffY * diffY);
|
||||
cmp = Math.max(cmp, Double.compare(bottom, distance));
|
||||
// once we compete in the PQ, no need to continue.
|
||||
if (cmp > 0) {
|
||||
return cmp;
|
||||
}
|
||||
}
|
||||
return cmp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(int slot, int doc) throws IOException {
|
||||
values[slot] = sortKey(doc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LeafFieldComparator getLeafComparator(LeafReaderContext context) throws IOException {
|
||||
LeafReader reader = context.reader();
|
||||
FieldInfo info = reader.getFieldInfos().fieldInfo(field);
|
||||
if (info != null) {
|
||||
XYDocValuesField.checkCompatible(info);
|
||||
}
|
||||
currentDocs = DocValues.getSortedNumeric(reader, field);
|
||||
valuesDocID = -1;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double value(int slot) {
|
||||
return values[slot];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTop(int doc) throws IOException {
|
||||
return Double.compare(topValue, sortKey(doc));
|
||||
}
|
||||
|
||||
double sortKey(int doc) throws IOException {
|
||||
if (doc > currentDocs.docID()) {
|
||||
currentDocs.advance(doc);
|
||||
}
|
||||
double minValue = Double.POSITIVE_INFINITY;
|
||||
if (doc == currentDocs.docID()) {
|
||||
setValues();
|
||||
int numValues = currentDocs.docValueCount();
|
||||
for (int i = 0; i < numValues; i++) {
|
||||
long encoded = currentValues[i];
|
||||
double docX = XYEncodingUtils.decode((int)(encoded >> 32));
|
||||
double docY = XYEncodingUtils.decode((int)(encoded & 0xFFFFFFFF));
|
||||
final double diffX = x - docX;
|
||||
final double diffY = y - docY;
|
||||
double distance = Math.sqrt(diffX * diffX + diffY * diffY);
|
||||
minValue = Math.min(minValue, distance);
|
||||
}
|
||||
}
|
||||
return minValue;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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.document;
|
||||
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.geo.XYCircle;
|
||||
import org.apache.lucene.geo.XYEncodingUtils;
|
||||
import org.apache.lucene.geo.XYPolygon;
|
||||
import org.apache.lucene.geo.XYRectangle;
|
||||
import org.apache.lucene.index.FieldInfo;
|
||||
import org.apache.lucene.index.PointValues;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.util.BytesRef;
|
||||
import org.apache.lucene.util.NumericUtils;
|
||||
|
||||
|
||||
/**
|
||||
* An indexed XY position field.
|
||||
* <p>
|
||||
* Finding all documents within a range at search time is
|
||||
* efficient. Multiple values for the same field in one document
|
||||
* is allowed.
|
||||
* <p>
|
||||
* This field defines static factory methods for common operations:
|
||||
* <ul>
|
||||
* <li>{@link #newBoxQuery newBoxQuery()} for matching points within a bounding box.
|
||||
* <li>{@link #newDistanceQuery newDistanceQuery()} for matching points within a specified distance.
|
||||
* <li>{@link #newPolygonQuery newPolygonQuery()} for matching points within an arbitrary polygon.
|
||||
* </ul>
|
||||
* <p>
|
||||
* If you also need per-document operations such as sort by distance, add a separate {@link XYDocValuesField} instance.
|
||||
* If you also need to store the value, you should add a separate {@link StoredField} instance.
|
||||
*
|
||||
* @see PointValues
|
||||
* @see XYDocValuesField
|
||||
*/
|
||||
|
||||
public class XYPointField extends Field {
|
||||
/** XYPoint is encoded as integer values so number of bytes is 4 */
|
||||
public static final int BYTES = Integer.BYTES;
|
||||
/**
|
||||
* Type for an indexed XYPoint
|
||||
* <p>
|
||||
* Each point stores two dimensions with 4 bytes per dimension.
|
||||
*/
|
||||
public static final FieldType TYPE = new FieldType();
|
||||
static {
|
||||
TYPE.setDimensions(2, Integer.BYTES);
|
||||
TYPE.freeze();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the values of this field
|
||||
* @param x x value.
|
||||
* @param y y value.
|
||||
*/
|
||||
public void setLocationValue(float x, float y) {
|
||||
final byte[] bytes;
|
||||
if (fieldsData == null) {
|
||||
bytes = new byte[8];
|
||||
fieldsData = new BytesRef(bytes);
|
||||
} else {
|
||||
bytes = ((BytesRef) fieldsData).bytes;
|
||||
}
|
||||
int xEncoded = XYEncodingUtils.encode(x);
|
||||
int yEncoded = XYEncodingUtils.encode(y);
|
||||
NumericUtils.intToSortableBytes(xEncoded, bytes, 0);
|
||||
NumericUtils.intToSortableBytes(yEncoded, bytes, Integer.BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new XYPoint with the specified x and y
|
||||
* @param name field name
|
||||
* @param x x value.
|
||||
* @param y y value.
|
||||
*/
|
||||
public XYPointField(String name, float x, float y) {
|
||||
super(name, TYPE);
|
||||
setLocationValue(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append(getClass().getSimpleName());
|
||||
result.append(" <");
|
||||
result.append(name);
|
||||
result.append(':');
|
||||
|
||||
byte bytes[] = ((BytesRef) fieldsData).bytes;
|
||||
result.append(XYEncodingUtils.decode(bytes, 0));
|
||||
result.append(',');
|
||||
result.append(XYEncodingUtils.decode(bytes, Integer.BYTES));
|
||||
|
||||
result.append('>');
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
|
||||
/** helper: checks a fieldinfo and throws exception if its definitely not a XYPoint */
|
||||
static void checkCompatible(FieldInfo fieldInfo) {
|
||||
// point/dv properties could be "unset", if you e.g. used only StoredField with this same name in the segment.
|
||||
if (fieldInfo.getPointDimensionCount() != 0 && fieldInfo.getPointDimensionCount() != TYPE.pointDimensionCount()) {
|
||||
throw new IllegalArgumentException("field=\"" + fieldInfo.name + "\" was indexed with numDims=" + fieldInfo.getPointDimensionCount() +
|
||||
" but this point type has numDims=" + TYPE.pointDimensionCount() +
|
||||
", is the field really a XYPoint?");
|
||||
}
|
||||
if (fieldInfo.getPointNumBytes() != 0 && fieldInfo.getPointNumBytes() != TYPE.pointNumBytes()) {
|
||||
throw new IllegalArgumentException("field=\"" + fieldInfo.name + "\" was indexed with bytesPerDim=" + fieldInfo.getPointNumBytes() +
|
||||
" but this point type has bytesPerDim=" + TYPE.pointNumBytes() +
|
||||
", is the field really a XYPoint?");
|
||||
}
|
||||
}
|
||||
|
||||
// static methods for generating queries
|
||||
|
||||
/**
|
||||
* Create a query for matching a bounding box.
|
||||
* <p>
|
||||
* @param field field name. must not be null.
|
||||
* @param minX x lower bound.
|
||||
* @param maxX x upper bound.
|
||||
* @param minY y lower bound.
|
||||
* @param maxY y upper bound.
|
||||
* @return query matching points within this box
|
||||
* @throws IllegalArgumentException if {@code field} is null, or the box has invalid coordinates.
|
||||
*/
|
||||
public static Query newBoxQuery(String field, float minX, float maxX, float minY, float maxY) {
|
||||
XYRectangle rectangle = new XYRectangle(minX, maxX, minY, maxY);
|
||||
return new XYPointInGeometryQuery(field, rectangle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query for matching points within the specified distance of the supplied location.
|
||||
* @param field field name. must not be null.
|
||||
* @param x x at the center.
|
||||
* @param y y at the center.
|
||||
* @param radius maximum distance from the center in cartesian units: must be non-negative and finite.
|
||||
* @return query matching points within this distance
|
||||
* @throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or radius is invalid.
|
||||
*/
|
||||
public static Query newDistanceQuery(String field, float x, float y, float radius) {
|
||||
XYCircle circle = new XYCircle(x, y, radius);
|
||||
return new XYPointInGeometryQuery(field, circle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query for matching one or more polygons.
|
||||
* @param field field name. must not be null.
|
||||
* @param polygons array of polygons. must not be null or empty
|
||||
* @return query matching points within this polygon
|
||||
* @throws IllegalArgumentException if {@code field} is null, {@code polygons} is null or empty
|
||||
* @see Polygon
|
||||
*/
|
||||
public static Query newPolygonQuery(String field, XYPolygon... polygons) {
|
||||
return new XYPointInGeometryQuery(field, polygons);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* 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.document;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.lucene.geo.Component2D;
|
||||
import org.apache.lucene.geo.XYEncodingUtils;
|
||||
import org.apache.lucene.geo.XYGeometry;
|
||||
import org.apache.lucene.index.FieldInfo;
|
||||
import org.apache.lucene.index.LeafReader;
|
||||
import org.apache.lucene.index.LeafReaderContext;
|
||||
import org.apache.lucene.index.PointValues;
|
||||
import org.apache.lucene.index.PointValues.IntersectVisitor;
|
||||
import org.apache.lucene.index.PointValues.Relation;
|
||||
import org.apache.lucene.search.ConstantScoreScorer;
|
||||
import org.apache.lucene.search.ConstantScoreWeight;
|
||||
import org.apache.lucene.search.DocIdSetIterator;
|
||||
import org.apache.lucene.search.IndexSearcher;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.search.QueryVisitor;
|
||||
import org.apache.lucene.search.ScoreMode;
|
||||
import org.apache.lucene.search.Scorer;
|
||||
import org.apache.lucene.search.ScorerSupplier;
|
||||
import org.apache.lucene.search.Weight;
|
||||
import org.apache.lucene.util.DocIdSetBuilder;
|
||||
|
||||
/** Finds all previously indexed points that fall within the specified XY geometries.
|
||||
*
|
||||
* <p>The field must be indexed with using {@link XYPointField} added per document.
|
||||
*
|
||||
* @lucene.experimental */
|
||||
|
||||
final class XYPointInGeometryQuery extends Query {
|
||||
final String field;
|
||||
final XYGeometry[] xyGeometries;
|
||||
|
||||
XYPointInGeometryQuery(String field, XYGeometry... xyGeometries) {
|
||||
if (field == null) {
|
||||
throw new IllegalArgumentException("field must not be null");
|
||||
}
|
||||
if (xyGeometries == null) {
|
||||
throw new IllegalArgumentException("geometries must not be null");
|
||||
}
|
||||
if (xyGeometries.length == 0) {
|
||||
throw new IllegalArgumentException("geometries must not be empty");
|
||||
}
|
||||
this.field = field;
|
||||
this.xyGeometries = xyGeometries.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(QueryVisitor visitor) {
|
||||
if (visitor.acceptField(field)) {
|
||||
visitor.visitLeaf(this);
|
||||
}
|
||||
}
|
||||
|
||||
private IntersectVisitor getIntersectVisitor(DocIdSetBuilder result, Component2D tree) {
|
||||
return new IntersectVisitor() {
|
||||
DocIdSetBuilder.BulkAdder adder;
|
||||
|
||||
@Override
|
||||
public void grow(int count) {
|
||||
adder = result.grow(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(int docID) {
|
||||
adder.add(docID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(int docID, byte[] packedValue) {
|
||||
double x = XYEncodingUtils.decode(packedValue, 0);
|
||||
double y = XYEncodingUtils.decode(packedValue, Integer.BYTES);
|
||||
if (tree.contains(x, y)) {
|
||||
visit(docID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(DocIdSetIterator iterator, byte[] packedValue) throws IOException {
|
||||
double x = XYEncodingUtils.decode(packedValue, 0);
|
||||
double y = XYEncodingUtils.decode(packedValue, Integer.BYTES);
|
||||
if (tree.contains(x, y)) {
|
||||
int docID;
|
||||
while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
|
||||
visit(docID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
|
||||
double cellMinX = XYEncodingUtils.decode(minPackedValue, 0);
|
||||
double cellMinY = XYEncodingUtils.decode(minPackedValue, Integer.BYTES);
|
||||
double cellMaxX = XYEncodingUtils.decode(maxPackedValue, 0);
|
||||
double cellMaxY = XYEncodingUtils.decode(maxPackedValue, Integer.BYTES);
|
||||
return tree.relate(cellMinX, cellMaxX, cellMinY, cellMaxY);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
|
||||
|
||||
final Component2D tree = XYGeometry.create(xyGeometries);
|
||||
|
||||
return new ConstantScoreWeight(this, boost) {
|
||||
|
||||
@Override
|
||||
public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
|
||||
LeafReader reader = context.reader();
|
||||
PointValues values = reader.getPointValues(field);
|
||||
if (values == null) {
|
||||
// No docs in this segment had any points fields
|
||||
return null;
|
||||
}
|
||||
FieldInfo fieldInfo = reader.getFieldInfos().fieldInfo(field);
|
||||
if (fieldInfo == null) {
|
||||
// No docs in this segment indexed this field at all
|
||||
return null;
|
||||
}
|
||||
XYPointField.checkCompatible(fieldInfo);
|
||||
final Weight weight = this;
|
||||
|
||||
return new ScorerSupplier() {
|
||||
|
||||
long cost = -1;
|
||||
DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field);
|
||||
final IntersectVisitor visitor = getIntersectVisitor(result, tree);
|
||||
|
||||
@Override
|
||||
public Scorer get(long leadCost) throws IOException {
|
||||
values.intersect(visitor);
|
||||
return new ConstantScoreScorer(weight, score(), scoreMode, result.build().iterator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long cost() {
|
||||
if (cost == -1) {
|
||||
// Computing the cost may be expensive, so only do it if necessary
|
||||
cost = values.estimateDocCount(visitor);
|
||||
assert cost >= 0;
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scorer scorer(LeafReaderContext context) throws IOException {
|
||||
ScorerSupplier scorerSupplier = scorerSupplier(context);
|
||||
if (scorerSupplier == null) {
|
||||
return null;
|
||||
}
|
||||
return scorerSupplier.get(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCacheable(LeafReaderContext ctx) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/** Returns the query field */
|
||||
public String getField() {
|
||||
return field;
|
||||
}
|
||||
|
||||
/** Returns a copy of the internal geometries array */
|
||||
public XYGeometry[] getGeometries() {
|
||||
return xyGeometries.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = classHash();
|
||||
result = prime * result + field.hashCode();
|
||||
result = prime * result + Arrays.hashCode(xyGeometries);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return sameClassAs(other) &&
|
||||
equalsTo(getClass().cast(other));
|
||||
}
|
||||
|
||||
private boolean equalsTo(XYPointInGeometryQuery other) {
|
||||
return field.equals(other.field) &&
|
||||
Arrays.equals(xyGeometries, other.xyGeometries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String field) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(getClass().getSimpleName());
|
||||
sb.append(':');
|
||||
if (this.field.equals(field) == false) {
|
||||
sb.append(" field=");
|
||||
sb.append(this.field);
|
||||
sb.append(':');
|
||||
}
|
||||
sb.append(Arrays.toString(xyGeometries));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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.document;
|
||||
|
||||
import org.apache.lucene.search.FieldComparator;
|
||||
import org.apache.lucene.search.SortField;
|
||||
|
||||
/**
|
||||
* Sorts by distance from an origin location.
|
||||
*/
|
||||
final class XYPointSortField extends SortField {
|
||||
final float x;
|
||||
final float y;
|
||||
|
||||
XYPointSortField(String field, float x, float y) {
|
||||
super(field, Type.CUSTOM);
|
||||
if (field == null) {
|
||||
throw new IllegalArgumentException("field must not be null");
|
||||
}
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
setMissingValue(Double.POSITIVE_INFINITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FieldComparator<?> getComparator(int numHits, int sortPos) {
|
||||
return new XYPointDistanceComparator(getField(), x, y, numHits);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getMissingValue() {
|
||||
return (Double) super.getMissingValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMissingValue(Object missingValue) {
|
||||
if (Double.valueOf(Double.POSITIVE_INFINITY).equals(missingValue) == false) {
|
||||
throw new IllegalArgumentException("Missing value can only be Double.POSITIVE_INFINITY (missing values last), but got " + missingValue);
|
||||
}
|
||||
this.missingValue = missingValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = super.hashCode();
|
||||
long temp;
|
||||
temp = Float.floatToIntBits(x);
|
||||
result = prime * result + (int) (temp ^ (temp >>> 32));
|
||||
temp = Float.floatToIntBits(y);
|
||||
result = prime * result + (int) (temp ^ (temp >>> 32));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!super.equals(obj)) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
XYPointSortField other = (XYPointSortField) obj;
|
||||
if (x != other.x || y != other.y) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("<distance:");
|
||||
builder.append('"');
|
||||
builder.append(getField());
|
||||
builder.append('"');
|
||||
builder.append(" x=");
|
||||
builder.append(x);
|
||||
builder.append(" y=");
|
||||
builder.append(y);
|
||||
if (Double.POSITIVE_INFINITY != getMissingValue()) {
|
||||
builder.append(" missingValue=").append(getMissingValue());
|
||||
}
|
||||
builder.append('>');
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.search;
|
||||
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.document.XYDocValuesField;
|
||||
import org.apache.lucene.geo.BaseXYPointTestCase;
|
||||
import org.apache.lucene.geo.XYPolygon;
|
||||
|
||||
public class TestXYDocValuesQueries extends BaseXYPointTestCase {
|
||||
|
||||
@Override
|
||||
protected void addPointToDoc(String field, Document doc, float x, float y) {
|
||||
doc.add(new XYDocValuesField(field, x, y));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Query newRectQuery(String field, float minX, float maxX, float minY, float maxY) {
|
||||
return XYDocValuesField.newSlowBoxQuery(field, minX, maxX, minY, maxY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Query newDistanceQuery(String field, float centerX, float centerY, float radius) {
|
||||
return XYDocValuesField.newSlowDistanceQuery(field, centerX, centerY, radius);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Query newPolygonQuery(String field, XYPolygon... polygons) {
|
||||
return XYDocValuesField.newSlowPolygonQuery(field, polygons);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
/*
|
||||
* 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.search;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.document.NumericDocValuesField;
|
||||
import org.apache.lucene.document.StoredField;
|
||||
import org.apache.lucene.document.XYDocValuesField;
|
||||
import org.apache.lucene.geo.ShapeTestUtil;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
import org.apache.lucene.index.IndexWriterConfig;
|
||||
import org.apache.lucene.index.RandomIndexWriter;
|
||||
import org.apache.lucene.index.SerialMergeScheduler;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
import org.apache.lucene.util.TestUtil;
|
||||
|
||||
/** Simple tests for {@link XYDocValuesField#newDistanceSort} */
|
||||
public class TestXYPointDistanceSort extends LuceneTestCase {
|
||||
|
||||
private double cartesianDistance(double x1, double y1, double x2, double y2) {
|
||||
final double diffX = x1 - x2;
|
||||
final double diffY = y1 - y2;
|
||||
return Math.sqrt(diffX * diffX + diffY * diffY);
|
||||
}
|
||||
|
||||
/** Add three points and sort by distance */
|
||||
public void testDistanceSort() throws Exception {
|
||||
Directory dir = newDirectory();
|
||||
RandomIndexWriter iw = new RandomIndexWriter(random(), dir);
|
||||
|
||||
// add some docs
|
||||
Document doc = new Document();
|
||||
doc.add(new XYDocValuesField("location", 40.759011f, -73.9844722f));
|
||||
iw.addDocument(doc);
|
||||
double d1 = cartesianDistance(40.759011f, -73.9844722f, 40.7143528f, -74.0059731f);
|
||||
|
||||
doc = new Document();
|
||||
doc.add(new XYDocValuesField("location", 40.718266f, -74.007819f));
|
||||
iw.addDocument(doc);
|
||||
double d2 = cartesianDistance(40.718266f, -74.007819f, 40.7143528f, -74.0059731f);
|
||||
|
||||
doc = new Document();
|
||||
doc.add(new XYDocValuesField("location", 40.7051157f, -74.0088305f));
|
||||
iw.addDocument(doc);
|
||||
double d3 = cartesianDistance(40.7051157f, -74.0088305f, 40.7143528f, -74.0059731f);
|
||||
|
||||
IndexReader reader = iw.getReader();
|
||||
IndexSearcher searcher = newSearcher(reader);
|
||||
iw.close();
|
||||
|
||||
Sort sort = new Sort(XYDocValuesField.newDistanceSort("location", 40.7143528f, -74.0059731f));
|
||||
TopDocs td = searcher.search(new MatchAllDocsQuery(), 3, sort);
|
||||
|
||||
FieldDoc d = (FieldDoc) td.scoreDocs[0];
|
||||
assertEquals(d2, (Double)d.fields[0], 0.0D);
|
||||
|
||||
d = (FieldDoc) td.scoreDocs[1];
|
||||
assertEquals(d3, (Double)d.fields[0], 0.0D);
|
||||
|
||||
d = (FieldDoc) td.scoreDocs[2];
|
||||
assertEquals(d1, (Double)d.fields[0], 0.0D);
|
||||
|
||||
reader.close();
|
||||
dir.close();
|
||||
}
|
||||
|
||||
/** Add two points (one doc missing) and sort by distance */
|
||||
public void testMissingLast() throws Exception {
|
||||
Directory dir = newDirectory();
|
||||
RandomIndexWriter iw = new RandomIndexWriter(random(), dir);
|
||||
|
||||
// missing
|
||||
Document doc = new Document();
|
||||
iw.addDocument(doc);
|
||||
|
||||
doc = new Document();
|
||||
doc.add(new XYDocValuesField("location", 40.718266f, -74.007819f));
|
||||
iw.addDocument(doc);
|
||||
double d2 = cartesianDistance(40.718266f, -74.007819f, 40.7143528f, -74.0059731f);
|
||||
|
||||
doc = new Document();
|
||||
doc.add(new XYDocValuesField("location", 40.7051157f, -74.0088305f));
|
||||
iw.addDocument(doc);
|
||||
double d3 = cartesianDistance(40.7051157f, -74.0088305f, 40.7143528f, -74.0059731f);
|
||||
|
||||
|
||||
IndexReader reader = iw.getReader();
|
||||
IndexSearcher searcher = newSearcher(reader);
|
||||
iw.close();
|
||||
|
||||
Sort sort = new Sort(XYDocValuesField.newDistanceSort("location", 40.7143528f, -74.0059731f));
|
||||
TopDocs td = searcher.search(new MatchAllDocsQuery(), 3, sort);
|
||||
|
||||
FieldDoc d = (FieldDoc) td.scoreDocs[0];
|
||||
assertEquals(d2, (Double)d.fields[0], 0.0D);
|
||||
|
||||
d = (FieldDoc) td.scoreDocs[1];
|
||||
assertEquals(d3, (Double)d.fields[0], 0.0D);
|
||||
|
||||
d = (FieldDoc) td.scoreDocs[2];
|
||||
assertEquals(Double.POSITIVE_INFINITY, (Double)d.fields[0], 0.0D);
|
||||
|
||||
reader.close();
|
||||
dir.close();
|
||||
}
|
||||
|
||||
/** Run a few iterations with just 10 docs, hopefully easy to debug */
|
||||
public void testRandom() throws Exception {
|
||||
for (int iters = 0; iters < 100; iters++) {
|
||||
doRandomTest(10, 100);
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs with thousands of docs */
|
||||
@Nightly
|
||||
public void testRandomHuge() throws Exception {
|
||||
for (int iters = 0; iters < 10; iters++) {
|
||||
doRandomTest(2000, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// result class used for testing. holds an id+distance.
|
||||
// we sort these with Arrays.sort and compare with lucene's results
|
||||
static class Result implements Comparable<Result> {
|
||||
int id;
|
||||
double distance;
|
||||
|
||||
Result(int id, double distance) {
|
||||
this.id = id;
|
||||
this.distance = distance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Result o) {
|
||||
int cmp = Double.compare(distance, o.distance);
|
||||
if (cmp == 0) {
|
||||
return Integer.compare(id, o.id);
|
||||
}
|
||||
return cmp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
long temp;
|
||||
temp = Double.doubleToLongBits(distance);
|
||||
result = prime * result + (int) (temp ^ (temp >>> 32));
|
||||
result = prime * result + id;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
Result other = (Result) obj;
|
||||
if (Double.doubleToLongBits(distance) != Double.doubleToLongBits(other.distance)) return false;
|
||||
if (id != other.id) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Result [id=" + id + ", distance=" + distance + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private void doRandomTest(int numDocs, int numQueries) throws IOException {
|
||||
Directory dir = newDirectory();
|
||||
IndexWriterConfig iwc = newIndexWriterConfig();
|
||||
// else seeds may not to reproduce:
|
||||
iwc.setMergeScheduler(new SerialMergeScheduler());
|
||||
RandomIndexWriter writer = new RandomIndexWriter(random(), dir, iwc);
|
||||
|
||||
for (int i = 0; i < numDocs; i++) {
|
||||
Document doc = new Document();
|
||||
doc.add(new StoredField("id", i));
|
||||
doc.add(new NumericDocValuesField("id", i));
|
||||
if (random().nextInt(10) > 7) {
|
||||
float x = ShapeTestUtil.nextFloat(random());
|
||||
float y = ShapeTestUtil.nextFloat(random());
|
||||
|
||||
doc.add(new XYDocValuesField("field", x, y));
|
||||
doc.add(new StoredField("x", x));
|
||||
doc.add(new StoredField("y", y));
|
||||
} // otherwise "missing"
|
||||
writer.addDocument(doc);
|
||||
}
|
||||
IndexReader reader = writer.getReader();
|
||||
IndexSearcher searcher = newSearcher(reader);
|
||||
|
||||
for (int i = 0; i < numQueries; i++) {
|
||||
float x = ShapeTestUtil.nextFloat(random());
|
||||
float y = ShapeTestUtil.nextFloat(random());
|
||||
double missingValue = Double.POSITIVE_INFINITY;
|
||||
|
||||
Result expected[] = new Result[reader.maxDoc()];
|
||||
|
||||
for (int doc = 0; doc < reader.maxDoc(); doc++) {
|
||||
Document targetDoc = reader.document(doc);
|
||||
final double distance;
|
||||
if (targetDoc.getField("x") == null) {
|
||||
distance = missingValue; // missing
|
||||
} else {
|
||||
double docX = targetDoc.getField("x").numericValue().floatValue();
|
||||
double docY = targetDoc.getField("y").numericValue().floatValue();
|
||||
distance = cartesianDistance(x, y, docX, docY);
|
||||
}
|
||||
int id = targetDoc.getField("id").numericValue().intValue();
|
||||
expected[doc] = new Result(id, distance);
|
||||
}
|
||||
|
||||
Arrays.sort(expected);
|
||||
|
||||
// randomize the topN a bit
|
||||
int topN = TestUtil.nextInt(random(), 1, reader.maxDoc());
|
||||
// sort by distance, then ID
|
||||
SortField distanceSort = XYDocValuesField.newDistanceSort("field", x, y);
|
||||
distanceSort.setMissingValue(missingValue);
|
||||
Sort sort = new Sort(distanceSort,
|
||||
new SortField("id", SortField.Type.INT));
|
||||
|
||||
TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), topN, sort);
|
||||
for (int resultNumber = 0; resultNumber < topN; resultNumber++) {
|
||||
FieldDoc fieldDoc = (FieldDoc) topDocs.scoreDocs[resultNumber];
|
||||
Result actual = new Result((Integer) fieldDoc.fields[1], (Double) fieldDoc.fields[0]);
|
||||
assertEquals(expected[resultNumber], actual);
|
||||
}
|
||||
|
||||
// get page2 with searchAfter()
|
||||
if (topN < reader.maxDoc()) {
|
||||
int page2 = TestUtil.nextInt(random(), 1, reader.maxDoc() - topN);
|
||||
TopDocs topDocs2 = searcher.searchAfter(topDocs.scoreDocs[topN - 1], new MatchAllDocsQuery(), page2, sort);
|
||||
for (int resultNumber = 0; resultNumber < page2; resultNumber++) {
|
||||
FieldDoc fieldDoc = (FieldDoc) topDocs2.scoreDocs[resultNumber];
|
||||
Result actual = new Result((Integer) fieldDoc.fields[1], (Double) fieldDoc.fields[0]);
|
||||
assertEquals(expected[topN + resultNumber], actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.close();
|
||||
writer.close();
|
||||
dir.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.search;
|
||||
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.document.XYPointField;
|
||||
import org.apache.lucene.geo.BaseXYPointTestCase;
|
||||
import org.apache.lucene.geo.XYPolygon;
|
||||
|
||||
public class TestXYPointQueries extends BaseXYPointTestCase {
|
||||
|
||||
@Override
|
||||
protected void addPointToDoc(String field, Document doc, float x, float y) {
|
||||
doc.add(new XYPointField(field, x, y));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Query newRectQuery(String field, float minX, float maxX, float minY, float maxY) {
|
||||
return XYPointField.newBoxQuery(field, minX, maxX, minY, maxY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Query newDistanceQuery(String field, float centerX, float centerY, float radius) {
|
||||
return XYPointField.newDistanceQuery(field, centerX, centerY, radius);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Query newPolygonQuery(String field, XYPolygon... polygons) {
|
||||
return XYPointField.newPolygonQuery(field, polygons);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -21,7 +21,6 @@ import java.util.Random;
|
|||
|
||||
import com.carrotsearch.randomizedtesting.RandomizedContext;
|
||||
import com.carrotsearch.randomizedtesting.generators.BiasedNumbers;
|
||||
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
import org.apache.lucene.util.TestUtil;
|
||||
|
||||
|
@ -42,7 +41,7 @@ public class ShapeTestUtil {
|
|||
try {
|
||||
return createRegularPolygon(nextFloat(random), nextFloat(random), radius, gons);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
// we tried to cross dateline or pole ... try again
|
||||
// something went wrong, try again
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -136,9 +135,7 @@ public class ShapeTestUtil {
|
|||
}
|
||||
|
||||
private static XYPolygon surpriseMePolygon(Random random) {
|
||||
// repeat until we get a poly that doesn't cross dateline:
|
||||
while (true) {
|
||||
//System.out.println("\nPOLY ITER");
|
||||
float centerX = nextFloat(random);
|
||||
float centerY = nextFloat(random);
|
||||
double radius = 0.1 + 20 * random.nextDouble();
|
||||
|
@ -149,7 +146,6 @@ public class ShapeTestUtil {
|
|||
double angle = 0.0;
|
||||
while (true) {
|
||||
angle += random.nextDouble()*40.0;
|
||||
//System.out.println(" angle " + angle);
|
||||
if (angle > 360) {
|
||||
break;
|
||||
}
|
||||
|
@ -159,14 +155,11 @@ public class ShapeTestUtil {
|
|||
|
||||
len = StrictMath.min(len, StrictMath.min(maxX, maxY));
|
||||
|
||||
//System.out.println(" len=" + len);
|
||||
float x = (float)(centerX + len * Math.cos(Math.toRadians(angle)));
|
||||
float y = (float)(centerY + len * Math.sin(Math.toRadians(angle)));
|
||||
|
||||
xList.add(x);
|
||||
yList.add(y);
|
||||
|
||||
//System.out.println(" lat=" + lats.get(lats.size()-1) + " lon=" + lons.get(lons.size()-1));
|
||||
}
|
||||
|
||||
// close it
|
||||
|
@ -222,4 +215,67 @@ public class ShapeTestUtil {
|
|||
private static Random random() {
|
||||
return RandomizedContext.current().getRandom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple slow point in polygon check (for testing)
|
||||
*/
|
||||
// direct port of PNPOLY C code (https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html)
|
||||
// this allows us to improve the code yet still ensure we have its properties
|
||||
// it is under the BSD license (https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html#License%20to%20Use)
|
||||
//
|
||||
// Copyright (c) 1970-2003, Wm. Randolph Franklin
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
||||
// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimers.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright
|
||||
// notice in the documentation and/or other materials provided with
|
||||
// the distribution.
|
||||
// 3. The name of W. Randolph Franklin may not be used to endorse or
|
||||
// promote products derived from this Software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
public static boolean containsSlowly(XYPolygon polygon, double x, double y) {
|
||||
if (polygon.getHoles().length > 0) {
|
||||
throw new UnsupportedOperationException("this testing method does not support holes");
|
||||
}
|
||||
double polyXs[] = XYEncodingUtils.floatArrayToDoubleArray(polygon.getPolyX());
|
||||
double polyYs[] =XYEncodingUtils.floatArrayToDoubleArray(polygon.getPolyY());
|
||||
// bounding box check required due to rounding errors (we don't solve that problem)
|
||||
if (x < polygon.minX || x > polygon.maxX || y < polygon.minY || y > polygon.maxY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean c = false;
|
||||
int i, j;
|
||||
int nvert = polyYs.length;
|
||||
double verty[] = polyYs;
|
||||
double vertx[] = polyXs;
|
||||
double testy = y;
|
||||
double testx = x;
|
||||
for (i = 0, j = 1; j < nvert; ++i, ++j) {
|
||||
if (testy == verty[j] && testy == verty[i] ||
|
||||
((testy <= verty[j] && testy >= verty[i]) != (testy >= verty[j] && testy <= verty[i]))) {
|
||||
if ((testx == vertx[j] && testx == vertx[i]) ||
|
||||
((testx <= vertx[j] && testx >= vertx[i]) != (testx >= vertx[j] && testx <= vertx[i]) &&
|
||||
GeoUtils.orient(vertx[i], verty[i], vertx[j], verty[j], testx, testy) == 0)) {
|
||||
// return true if point is on boundary
|
||||
return true;
|
||||
} else if ( ((verty[i] > testy) != (verty[j] > testy)) &&
|
||||
(testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) ) {
|
||||
c = !c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue