LUCENE-7388: Adds IntRangeField, FloatRangeField, LongRangeField along with supporting queries and tests

This commit is contained in:
Nicholas Knize 2016-07-21 14:32:05 -05:00
parent fc1adb4053
commit 249780cf60
9 changed files with 1723 additions and 174 deletions

View File

@ -38,6 +38,9 @@ API Changes
New Features New Features
* LUCENE-7388: Add point based IntRangeField, FloatRangeField, LongRangeField along with
supporting queries and tests (Nick Knize)
* LUCENE-7381: Add point based DoubleRangeField and RangeFieldQuery for * LUCENE-7381: Add point based DoubleRangeField and RangeFieldQuery for
indexing and querying on Ranges up to 4 dimensions (Nick Knize) indexing and querying on Ranges up to 4 dimensions (Nick Knize)

View File

@ -0,0 +1,262 @@
/*
* 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.document.RangeFieldQuery.QueryType;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.NumericUtils;
/**
* An indexed Float Range field.
* <p>
* This field indexes dimensional ranges defined as min/max pairs. It supports
* up to a maximum of 4 dimensions (indexed as 8 numeric values). With 1 dimension representing a single float range,
* 2 dimensions representing a bounding box, 3 dimensions a bounding cube, and 4 dimensions a tesseract.
* <p>
* Multiple values for the same field in one document is supported, and open ended ranges can be defined using
* {@code Float.NEGATIVE_INFINITY} and {@code Float.POSITIVE_INFINITY}.
*
* <p>
* This field defines the following static factory methods for common search operations over float ranges:
* <ul>
* <li>{@link #newIntersectsQuery newIntersectsQuery()} matches ranges that intersect the defined search range.
* <li>{@link #newWithinQuery newWithinQuery()} matches ranges that are within the defined search range.
* <li>{@link #newContainsQuery newContainsQuery()} matches ranges that contain the defined search range.
* </ul>
*/
public class FloatRangeField extends Field {
/** stores float values so number of bytes is 4 */
public static final int BYTES = Float.BYTES;
/**
* Create a new FloatRangeField type, from min/max parallel arrays
*
* @param name field name. must not be null.
* @param min range min values; each entry is the min value for the dimension
* @param max range max values; each entry is the max value for the dimension
*/
public FloatRangeField(String name, final float[] min, final float[] max) {
super(name, getType(min.length));
setRangeValues(min, max);
}
/** set the field type */
private static FieldType getType(int dimensions) {
if (dimensions > 4) {
throw new IllegalArgumentException("FloatRangeField does not support greater than 4 dimensions");
}
FieldType ft = new FieldType();
// dimensions is set as 2*dimension size (min/max per dimension)
ft.setDimensions(dimensions*2, BYTES);
ft.freeze();
return ft;
}
/**
* Changes the values of the field.
* @param min array of min values. (accepts {@code Float.NEGATIVE_INFINITY})
* @param max array of max values. (accepts {@code Float.POSITIVE_INFINITY})
* @throws IllegalArgumentException if {@code min} or {@code max} is invalid
*/
public void setRangeValues(float[] min, float[] max) {
checkArgs(min, max);
if (min.length*2 != type.pointDimensionCount() || max.length*2 != type.pointDimensionCount()) {
throw new IllegalArgumentException("field (name=" + name + ") uses " + type.pointDimensionCount()/2
+ " dimensions; cannot change to (incoming) " + min.length + " dimensions");
}
final byte[] bytes;
if (fieldsData == null) {
bytes = new byte[BYTES*2*min.length];
fieldsData = new BytesRef(bytes);
} else {
bytes = ((BytesRef)fieldsData).bytes;
}
verifyAndEncode(min, max, bytes);
}
/** validate the arguments */
private static void checkArgs(final float[] min, final float[] max) {
if (min == null || max == null || min.length == 0 || max.length == 0) {
throw new IllegalArgumentException("min/max range values cannot be null or empty");
}
if (min.length != max.length) {
throw new IllegalArgumentException("min/max ranges must agree");
}
if (min.length > 4) {
throw new IllegalArgumentException("FloatRangeField does not support greater than 4 dimensions");
}
}
/**
* Encodes the min, max ranges into a byte array
*/
private static byte[] encode(float[] min, float[] max) {
checkArgs(min, max);
byte[] b = new byte[BYTES*2*min.length];
verifyAndEncode(min, max, b);
return b;
}
/**
* encode the ranges into a sortable byte array ({@code Float.NaN} not allowed)
* <p>
* example for 4 dimensions (8 bytes per dimension value):
* minD1 ... minD4 | maxD1 ... maxD4
*/
static void verifyAndEncode(float[] min, float[] max, byte[] bytes) {
for (int d=0,i=0,j=min.length*BYTES; d<min.length; ++d, i+=BYTES, j+=BYTES) {
if (Double.isNaN(min[d])) {
throw new IllegalArgumentException("invalid min value (" + Float.NaN + ")" + " in FloatRangeField");
}
if (Double.isNaN(max[d])) {
throw new IllegalArgumentException("invalid max value (" + Float.NaN + ")" + " in FloatRangeField");
}
if (min[d] > max[d]) {
throw new IllegalArgumentException("min value (" + min[d] + ") is greater than max value (" + max[d] + ")");
}
encode(min[d], bytes, i);
encode(max[d], bytes, j);
}
}
/** encode the given value into the byte array at the defined offset */
private static void encode(float val, byte[] bytes, int offset) {
NumericUtils.intToSortableBytes(NumericUtils.floatToSortableInt(val), bytes, offset);
}
/**
* Get the min value for the given dimension
* @param dimension the dimension, always positive
* @return the decoded min value
*/
public float getMin(int dimension) {
if (dimension < 0 || dimension >= type.pointDimensionCount()/2) {
throw new IllegalArgumentException("dimension request (" + dimension +
") out of bounds for field (name=" + name + " dimensions=" + type.pointDimensionCount()/2 + "). ");
}
return decodeMin(((BytesRef)fieldsData).bytes, dimension);
}
/**
* Get the max value for the given dimension
* @param dimension the dimension, always positive
* @return the decoded max value
*/
public float getMax(int dimension) {
if (dimension < 0 || dimension >= type.pointDimensionCount()/2) {
throw new IllegalArgumentException("dimension request (" + dimension +
") out of bounds for field (name=" + name + " dimensions=" + type.pointDimensionCount()/2 + "). ");
}
return decodeMax(((BytesRef)fieldsData).bytes, dimension);
}
/** decodes the min value (for the defined dimension) from the encoded input byte array */
static float decodeMin(byte[] b, int dimension) {
int offset = dimension*BYTES;
return NumericUtils.sortableIntToFloat(NumericUtils.sortableBytesToInt(b, offset));
}
/** decodes the max value (for the defined dimension) from the encoded input byte array */
static float decodeMax(byte[] b, int dimension) {
int offset = b.length/2 + dimension*BYTES;
return NumericUtils.sortableIntToFloat(NumericUtils.sortableBytesToInt(b, offset));
}
/**
* Create a query for matching indexed ranges that intersect the defined range.
* @param field field name. must not be null.
* @param min array of min values. (accepts {@code Float.NEGATIVE_INFINITY})
* @param max array of max values. (accepts {@code Float.MAX_VALUE})
* @return query for matching intersecting ranges (overlap, within, or contains)
* @throws IllegalArgumentException if {@code field} is null, {@code min} or {@code max} is invalid
*/
public static Query newIntersectsQuery(String field, final float[] min, final float[] max) {
return new RangeFieldQuery(field, encode(min, max), min.length, QueryType.INTERSECTS) {
@Override
protected String toString(byte[] ranges, int dimension) {
return FloatRangeField.toString(ranges, dimension);
}
};
}
/**
* Create a query for matching indexed float ranges that contain the defined range.
* @param field field name. must not be null.
* @param min array of min values. (accepts {@code Float.NEGATIVE_INFINITY})
* @param max array of max values. (accepts {@code Float.POSITIVE_INFINITY})
* @return query for matching ranges that contain the defined range
* @throws IllegalArgumentException if {@code field} is null, {@code min} or {@code max} is invalid
*/
public static Query newContainsQuery(String field, final float[] min, final float[] max) {
return new RangeFieldQuery(field, encode(min, max), min.length, QueryType.CONTAINS) {
@Override
protected String toString(byte[] ranges, int dimension) {
return FloatRangeField.toString(ranges, dimension);
}
};
}
/**
* Create a query for matching indexed ranges that are within the defined range.
* @param field field name. must not be null.
* @param min array of min values. (accepts {@code Float.NEGATIVE_INFINITY})
* @param max array of max values. (accepts {@code Float.POSITIVE_INFINITY})
* @return query for matching ranges within the defined range
* @throws IllegalArgumentException if {@code field} is null, {@code min} or {@code max} is invalid
*/
public static Query newWithinQuery(String field, final float[] min, final float[] max) {
checkArgs(min, max);
return new RangeFieldQuery(field, encode(min, max), min.length, QueryType.WITHIN) {
@Override
protected String toString(byte[] ranges, int dimension) {
return FloatRangeField.toString(ranges, dimension);
}
};
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" <");
sb.append(name);
sb.append(':');
byte[] b = ((BytesRef)fieldsData).bytes;
toString(b, 0);
for (int d=1; d<type.pointDimensionCount(); ++d) {
sb.append(' ');
toString(b, d);
}
sb.append('>');
return sb.toString();
}
/**
* Returns the String representation for the range at the given dimension
* @param ranges the encoded ranges, never null
* @param dimension the dimension of interest
* @return The string representation for the range at the provided dimension
*/
private static String toString(byte[] ranges, int dimension) {
return "[" + Float.toString(decodeMin(ranges, dimension)) + " : "
+ Float.toString(decodeMax(ranges, dimension)) + "]";
}
}

View File

@ -0,0 +1,262 @@
/*
* 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.document.RangeFieldQuery.QueryType;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.NumericUtils;
/**
* An indexed Integer Range field.
* <p>
* This field indexes dimensional ranges defined as min/max pairs. It supports
* up to a maximum of 4 dimensions (indexed as 8 numeric values). With 1 dimension representing a single integer range,
* 2 dimensions representing a bounding box, 3 dimensions a bounding cube, and 4 dimensions a tesseract.
* <p>
* Multiple values for the same field in one document is supported, and open ended ranges can be defined using
* {@code Integer.MIN_VALUE} and {@code Integer.MAX_VALUE}.
*
* <p>
* This field defines the following static factory methods for common search operations over integer ranges:
* <ul>
* <li>{@link #newIntersectsQuery newIntersectsQuery()} matches ranges that intersect the defined search range.
* <li>{@link #newWithinQuery newWithinQuery()} matches ranges that are within the defined search range.
* <li>{@link #newContainsQuery newContainsQuery()} matches ranges that contain the defined search range.
* </ul>
*/
public class IntRangeField extends Field {
/** stores integer values so number of bytes is 4 */
public static final int BYTES = Integer.BYTES;
/**
* Create a new IntRangeField type, from min/max parallel arrays
*
* @param name field name. must not be null.
* @param min range min values; each entry is the min value for the dimension
* @param max range max values; each entry is the max value for the dimension
*/
public IntRangeField(String name, final int[] min, final int[] max) {
super(name, getType(min.length));
setRangeValues(min, max);
}
/** set the field type */
private static FieldType getType(int dimensions) {
if (dimensions > 4) {
throw new IllegalArgumentException("IntRangeField does not support greater than 4 dimensions");
}
FieldType ft = new FieldType();
// dimensions is set as 2*dimension size (min/max per dimension)
ft.setDimensions(dimensions*2, BYTES);
ft.freeze();
return ft;
}
/**
* Changes the values of the field.
* @param min array of min values. (accepts {@code Integer.NEGATIVE_INFINITY})
* @param max array of max values. (accepts {@code Integer.POSITIVE_INFINITY})
* @throws IllegalArgumentException if {@code min} or {@code max} is invalid
*/
public void setRangeValues(int[] min, int[] max) {
checkArgs(min, max);
if (min.length*2 != type.pointDimensionCount() || max.length*2 != type.pointDimensionCount()) {
throw new IllegalArgumentException("field (name=" + name + ") uses " + type.pointDimensionCount()/2
+ " dimensions; cannot change to (incoming) " + min.length + " dimensions");
}
final byte[] bytes;
if (fieldsData == null) {
bytes = new byte[BYTES*2*min.length];
fieldsData = new BytesRef(bytes);
} else {
bytes = ((BytesRef)fieldsData).bytes;
}
verifyAndEncode(min, max, bytes);
}
/** validate the arguments */
private static void checkArgs(final int[] min, final int[] max) {
if (min == null || max == null || min.length == 0 || max.length == 0) {
throw new IllegalArgumentException("min/max range values cannot be null or empty");
}
if (min.length != max.length) {
throw new IllegalArgumentException("min/max ranges must agree");
}
if (min.length > 4) {
throw new IllegalArgumentException("IntRangeField does not support greater than 4 dimensions");
}
}
/**
* Encodes the min, max ranges into a byte array
*/
private static byte[] encode(int[] min, int[] max) {
checkArgs(min, max);
byte[] b = new byte[BYTES*2*min.length];
verifyAndEncode(min, max, b);
return b;
}
/**
* encode the ranges into a sortable byte array ({@code Double.NaN} not allowed)
* <p>
* example for 4 dimensions (8 bytes per dimension value):
* minD1 ... minD4 | maxD1 ... maxD4
*/
static void verifyAndEncode(int[] min, int[] max, byte[] bytes) {
for (int d=0,i=0,j=min.length*BYTES; d<min.length; ++d, i+=BYTES, j+=BYTES) {
if (Double.isNaN(min[d])) {
throw new IllegalArgumentException("invalid min value (" + Double.NaN + ")" + " in IntRangeField");
}
if (Double.isNaN(max[d])) {
throw new IllegalArgumentException("invalid max value (" + Double.NaN + ")" + " in IntRangeField");
}
if (min[d] > max[d]) {
throw new IllegalArgumentException("min value (" + min[d] + ") is greater than max value (" + max[d] + ")");
}
encode(min[d], bytes, i);
encode(max[d], bytes, j);
}
}
/** encode the given value into the byte array at the defined offset */
private static void encode(int val, byte[] bytes, int offset) {
NumericUtils.intToSortableBytes(val, bytes, offset);
}
/**
* Get the min value for the given dimension
* @param dimension the dimension, always positive
* @return the decoded min value
*/
public int getMin(int dimension) {
if (dimension < 0 || dimension >= type.pointDimensionCount()/2) {
throw new IllegalArgumentException("dimension request (" + dimension +
") out of bounds for field (name=" + name + " dimensions=" + type.pointDimensionCount()/2 + "). ");
}
return decodeMin(((BytesRef)fieldsData).bytes, dimension);
}
/**
* Get the max value for the given dimension
* @param dimension the dimension, always positive
* @return the decoded max value
*/
public int getMax(int dimension) {
if (dimension < 0 || dimension >= type.pointDimensionCount()/2) {
throw new IllegalArgumentException("dimension request (" + dimension +
") out of bounds for field (name=" + name + " dimensions=" + type.pointDimensionCount()/2 + "). ");
}
return decodeMax(((BytesRef)fieldsData).bytes, dimension);
}
/** decodes the min value (for the defined dimension) from the encoded input byte array */
static int decodeMin(byte[] b, int dimension) {
int offset = dimension*BYTES;
return NumericUtils.sortableBytesToInt(b, offset);
}
/** decodes the max value (for the defined dimension) from the encoded input byte array */
static int decodeMax(byte[] b, int dimension) {
int offset = b.length/2 + dimension*BYTES;
return NumericUtils.sortableBytesToInt(b, offset);
}
/**
* Create a query for matching indexed ranges that intersect the defined range.
* @param field field name. must not be null.
* @param min array of min values. (accepts {@code Integer.MIN_VALUE})
* @param max array of max values. (accepts {@code Integer.MAX_VALUE})
* @return query for matching intersecting ranges (overlap, within, or contains)
* @throws IllegalArgumentException if {@code field} is null, {@code min} or {@code max} is invalid
*/
public static Query newIntersectsQuery(String field, final int[] min, final int[] max) {
return new RangeFieldQuery(field, encode(min, max), min.length, QueryType.INTERSECTS) {
@Override
protected String toString(byte[] ranges, int dimension) {
return IntRangeField.toString(ranges, dimension);
}
};
}
/**
* Create a query for matching indexed ranges that contain the defined range.
* @param field field name. must not be null.
* @param min array of min values. (accepts {@code Integer.MIN_VALUE})
* @param max array of max values. (accepts {@code Integer.MAX_VALUE})
* @return query for matching ranges that contain the defined range
* @throws IllegalArgumentException if {@code field} is null, {@code min} or {@code max} is invalid
*/
public static Query newContainsQuery(String field, final int[] min, final int[] max) {
return new RangeFieldQuery(field, encode(min, max), min.length, QueryType.CONTAINS) {
@Override
protected String toString(byte[] ranges, int dimension) {
return IntRangeField.toString(ranges, dimension);
}
};
}
/**
* Create a query for matching indexed ranges that are within the defined range.
* @param field field name. must not be null.
* @param min array of min values. (accepts {@code Integer.MIN_VALUE})
* @param max array of max values. (accepts {@code Integer.MAX_VALUE})
* @return query for matching ranges within the defined range
* @throws IllegalArgumentException if {@code field} is null, {@code min} or {@code max} is invalid
*/
public static Query newWithinQuery(String field, final int[] min, final int[] max) {
checkArgs(min, max);
return new RangeFieldQuery(field, encode(min, max), min.length, QueryType.WITHIN) {
@Override
protected String toString(byte[] ranges, int dimension) {
return IntRangeField.toString(ranges, dimension);
}
};
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" <");
sb.append(name);
sb.append(':');
byte[] b = ((BytesRef)fieldsData).bytes;
toString(b, 0);
for (int d=1; d<type.pointDimensionCount(); ++d) {
sb.append(' ');
toString(b, d);
}
sb.append('>');
return sb.toString();
}
/**
* Returns the String representation for the range at the given dimension
* @param ranges the encoded ranges, never null
* @param dimension the dimension of interest
* @return The string representation for the range at the provided dimension
*/
private static String toString(byte[] ranges, int dimension) {
return "[" + Integer.toString(decodeMin(ranges, dimension)) + " : "
+ Integer.toString(decodeMax(ranges, dimension)) + "]";
}
}

View File

@ -0,0 +1,260 @@
/*
* 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.document.RangeFieldQuery.QueryType;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.NumericUtils;
/**
* An indexed Long Range field.
* <p>
* This field indexes dimensional ranges defined as min/max pairs. It supports
* up to a maximum of 4 dimensions (indexed as 8 numeric values). With 1 dimension representing a single long range,
* 2 dimensions representing a bounding box, 3 dimensions a bounding cube, and 4 dimensions a tesseract.
* <p>
* Multiple values for the same field in one document is supported, and open ended ranges can be defined using
* {@code Long.MIN_VALUE} and {@code Long.MAX_VALUE}.
*
* <p>
* This field defines the following static factory methods for common search operations over long ranges:
* <ul>
* <li>{@link #newIntersectsQuery newIntersectsQuery()} matches ranges that intersect the defined search range.
* <li>{@link #newWithinQuery newWithinQuery()} matches ranges that are within the defined search range.
* <li>{@link #newContainsQuery newContainsQuery()} matches ranges that contain the defined search range.
* </ul>
*/
public class LongRangeField extends Field {
/** stores long values so number of bytes is 8 */
public static final int BYTES = Long.BYTES;
/**
* Create a new LongRangeField type, from min/max parallel arrays
*
* @param name field name. must not be null.
* @param min range min values; each entry is the min value for the dimension
* @param max range max values; each entry is the max value for the dimension
*/
public LongRangeField(String name, final long[] min, final long[] max) {
super(name, getType(min.length));
setRangeValues(min, max);
}
/** set the field type */
private static FieldType getType(int dimensions) {
if (dimensions > 4) {
throw new IllegalArgumentException("LongRangeField does not support greater than 4 dimensions");
}
FieldType ft = new FieldType();
// dimensions is set as 2*dimension size (min/max per dimension)
ft.setDimensions(dimensions*2, BYTES);
ft.freeze();
return ft;
}
/**
* Changes the values of the field.
* @param min array of min values. (accepts {@code Long.MIN_VALUE})
* @param max array of max values. (accepts {@code Long.MAX_VALUE})
* @throws IllegalArgumentException if {@code min} or {@code max} is invalid
*/
public void setRangeValues(long[] min, long[] max) {
checkArgs(min, max);
if (min.length*2 != type.pointDimensionCount() || max.length*2 != type.pointDimensionCount()) {
throw new IllegalArgumentException("field (name=" + name + ") uses " + type.pointDimensionCount()/2
+ " dimensions; cannot change to (incoming) " + min.length + " dimensions");
}
final byte[] bytes;
if (fieldsData == null) {
bytes = new byte[BYTES*2*min.length];
fieldsData = new BytesRef(bytes);
} else {
bytes = ((BytesRef)fieldsData).bytes;
}
verifyAndEncode(min, max, bytes);
}
/** validate the arguments */
private static void checkArgs(final long[] min, final long[] max) {
if (min == null || max == null || min.length == 0 || max.length == 0) {
throw new IllegalArgumentException("min/max range values cannot be null or empty");
}
if (min.length != max.length) {
throw new IllegalArgumentException("min/max ranges must agree");
}
if (min.length > 4) {
throw new IllegalArgumentException("LongRangeField does not support greater than 4 dimensions");
}
}
/** Encodes the min, max ranges into a byte array */
private static byte[] encode(long[] min, long[] max) {
checkArgs(min, max);
byte[] b = new byte[BYTES*2*min.length];
verifyAndEncode(min, max, b);
return b;
}
/**
* encode the ranges into a sortable byte array ({@code Double.NaN} not allowed)
* <p>
* example for 4 dimensions (8 bytes per dimension value):
* minD1 ... minD4 | maxD1 ... maxD4
*/
static void verifyAndEncode(long[] min, long[] max, byte[] bytes) {
for (int d=0,i=0,j=min.length*BYTES; d<min.length; ++d, i+=BYTES, j+=BYTES) {
if (Double.isNaN(min[d])) {
throw new IllegalArgumentException("invalid min value (" + Double.NaN + ")" + " in IntRangeField");
}
if (Double.isNaN(max[d])) {
throw new IllegalArgumentException("invalid max value (" + Double.NaN + ")" + " in IntRangeField");
}
if (min[d] > max[d]) {
throw new IllegalArgumentException("min value (" + min[d] + ") is greater than max value (" + max[d] + ")");
}
encode(min[d], bytes, i);
encode(max[d], bytes, j);
}
}
/** encode the given value into the byte array at the defined offset */
private static void encode(long val, byte[] bytes, int offset) {
NumericUtils.longToSortableBytes(val, bytes, offset);
}
/**
* Get the min value for the given dimension
* @param dimension the dimension, always positive
* @return the decoded min value
*/
public long getMin(int dimension) {
if (dimension < 0 || dimension >= type.pointDimensionCount()/2) {
throw new IllegalArgumentException("dimension request (" + dimension +
") out of bounds for field (name=" + name + " dimensions=" + type.pointDimensionCount()/2 + "). ");
}
return decodeMin(((BytesRef)fieldsData).bytes, dimension);
}
/**
* Get the max value for the given dimension
* @param dimension the dimension, always positive
* @return the decoded max value
*/
public long getMax(int dimension) {
if (dimension < 0 || dimension >= type.pointDimensionCount()/2) {
throw new IllegalArgumentException("dimension request (" + dimension +
") out of bounds for field (name=" + name + " dimensions=" + type.pointDimensionCount()/2 + "). ");
}
return decodeMax(((BytesRef)fieldsData).bytes, dimension);
}
/** decodes the min value (for the defined dimension) from the encoded input byte array */
static long decodeMin(byte[] b, int dimension) {
int offset = dimension*BYTES;
return NumericUtils.sortableBytesToLong(b, offset);
}
/** decodes the max value (for the defined dimension) from the encoded input byte array */
static long decodeMax(byte[] b, int dimension) {
int offset = b.length/2 + dimension*BYTES;
return NumericUtils.sortableBytesToLong(b, offset);
}
/**
* Create a query for matching indexed ranges that intersect the defined range.
* @param field field name. must not be null.
* @param min array of min values. (accepts {@code Long.MIN_VALUE})
* @param max array of max values. (accepts {@code Long.MAX_VALUE})
* @return query for matching intersecting ranges (overlap, within, or contains)
* @throws IllegalArgumentException if {@code field} is null, {@code min} or {@code max} is invalid
*/
public static Query newIntersectsQuery(String field, final long[] min, final long[] max) {
return new RangeFieldQuery(field, encode(min, max), min.length, QueryType.INTERSECTS) {
@Override
protected String toString(byte[] ranges, int dimension) {
return LongRangeField.toString(ranges, dimension);
}
};
}
/**
* Create a query for matching indexed ranges that contain the defined range.
* @param field field name. must not be null.
* @param min array of min values. (accepts {@code Long.MIN_VALUE})
* @param max array of max values. (accepts {@code Long.MAX_VALUE})
* @return query for matching ranges that contain the defined range
* @throws IllegalArgumentException if {@code field} is null, {@code min} or {@code max} is invalid
*/
public static Query newContainsQuery(String field, final long[] min, final long[] max) {
return new RangeFieldQuery(field, encode(min, max), min.length, QueryType.CONTAINS) {
@Override
protected String toString(byte[] ranges, int dimension) {
return LongRangeField.toString(ranges, dimension);
}
};
}
/**
* Create a query for matching indexed ranges that are within the defined range.
* @param field field name. must not be null.
* @param min array of min values. (accepts {@code Long.MIN_VALUE})
* @param max array of max values. (accepts {@code Long.MAX_VALUE})
* @return query for matching ranges within the defined range
* @throws IllegalArgumentException if {@code field} is null, {@code min} or {@code max} is invalid
*/
public static Query newWithinQuery(String field, final long[] min, final long[] max) {
checkArgs(min, max);
return new RangeFieldQuery(field, encode(min, max), min.length, QueryType.WITHIN) {
@Override
protected String toString(byte[] ranges, int dimension) {
return LongRangeField.toString(ranges, dimension);
}
};
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" <");
sb.append(name);
sb.append(':');
byte[] b = ((BytesRef)fieldsData).bytes;
toString(b, 0);
for (int d=1; d<type.pointDimensionCount(); ++d) {
sb.append(' ');
toString(b, d);
}
sb.append('>');
return sb.toString();
}
/**
* Returns the String representation for the range at the given dimension
* @param ranges the encoded ranges, never null
* @param dimension the dimension of interest
* @return The string representation for the range at the provided dimension
*/
private static String toString(byte[] ranges, int dimension) {
return "[" + Long.toString(decodeMin(ranges, dimension)) + " : "
+ Long.toString(decodeMax(ranges, dimension)) + "]";
}
}

View File

@ -17,7 +17,6 @@
package org.apache.lucene.search; package org.apache.lucene.search;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -41,16 +40,18 @@ import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.LuceneTestCase; import org.apache.lucene.util.LuceneTestCase;
/** /**
* Abstract class to do basic tests for a RangeField query. * Abstract class to do basic tests for a RangeField query. Testing rigor inspired by {@code BaseGeoPointTestCase}
*/ */
public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase { public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase {
protected abstract Field newRangeField(double[] min, double[] max); protected abstract Field newRangeField(Range box);
protected abstract Query newIntersectsQuery(double[] min, double[] max); protected abstract Query newIntersectsQuery(Range box);
protected abstract Query newContainsQuery(double[] min, double[] max); protected abstract Query newContainsQuery(Range box);
protected abstract Query newWithinQuery(double[] min, double[] max); protected abstract Query newWithinQuery(Range box);
protected abstract Range nextRange(int dimensions);
protected int dimension() { protected int dimension() {
return random().nextInt(4) + 1; return random().nextInt(4) + 1;
@ -82,18 +83,18 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase {
System.out.println("TEST: numDocs=" + numDocs); System.out.println("TEST: numDocs=" + numDocs);
} }
Box[][] boxes = new Box[numDocs][]; Range[][] ranges = new Range[numDocs][];
boolean haveRealDoc = true; boolean haveRealDoc = true;
nextdoc: for (int id=0; id<numDocs; ++id) { nextdoc: for (int id=0; id<numDocs; ++id) {
int x = random().nextInt(20); int x = random().nextInt(20);
if (boxes[id] == null) { if (ranges[id] == null) {
boxes[id] = new Box[] {nextBox(dimensions)}; ranges[id] = new Range[] {nextRange(dimensions)};
} }
if (x == 17) { if (x == 17) {
// dome docs don't have a box: // dome docs don't have a box:
boxes[id][0].min[0] = Double.NaN; ranges[id][0].isMissing = true;
if (VERBOSE) { if (VERBOSE) {
System.out.println(" id=" + id + " is missing"); System.out.println(" id=" + id + " is missing");
} }
@ -103,19 +104,19 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase {
if (multiValued == true && random().nextBoolean()) { if (multiValued == true && random().nextBoolean()) {
// randomly add multi valued documents (up to 2 fields) // randomly add multi valued documents (up to 2 fields)
int n = random().nextInt(2) + 1; int n = random().nextInt(2) + 1;
boxes[id] = new Box[n]; ranges[id] = new Range[n];
for (int i=0; i<n; ++i) { for (int i=0; i<n; ++i) {
boxes[id][i] = nextBox(dimensions); ranges[id][i] = nextRange(dimensions);
} }
} }
if (id > 0 && x < 9 && haveRealDoc) { if (id > 0 && x < 9 && haveRealDoc) {
int oldID; int oldID;
int i=0; int i=0;
// don't step on missing boxes: // don't step on missing ranges:
while (true) { while (true) {
oldID = random().nextInt(id); oldID = random().nextInt(id);
if (Double.isNaN(boxes[oldID][0].min[0]) == false) { if (ranges[oldID][0].isMissing == false) {
break; break;
} else if (++i > id) { } else if (++i > id) {
continue nextdoc; continue nextdoc;
@ -125,11 +126,11 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase {
if (x == dimensions*2) { if (x == dimensions*2) {
// Fully identical box (use first box in case current is multivalued but old is not) // Fully identical box (use first box in case current is multivalued but old is not)
for (int d=0; d<dimensions; ++d) { for (int d=0; d<dimensions; ++d) {
boxes[id][0].min[d] = boxes[oldID][0].min[d]; ranges[id][0].setMin(d, ranges[oldID][0].getMin(d));
boxes[id][0].max[d] = boxes[oldID][0].max[d]; ranges[id][0].setMax(d, ranges[oldID][0].getMax(d));
} }
if (VERBOSE) { if (VERBOSE) {
System.out.println(" id=" + id + " box=" + boxes[id] + " (same box as doc=" + oldID + ")"); System.out.println(" id=" + id + " box=" + ranges[id] + " (same box as doc=" + oldID + ")");
} }
} else { } else {
for (int m = 0, even = dimensions % 2; m < dimensions * 2; ++m) { for (int m = 0, even = dimensions % 2; m < dimensions * 2; ++m) {
@ -137,14 +138,14 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase {
int d = (int)Math.floor(m/2); int d = (int)Math.floor(m/2);
// current could be multivalue but old may not be, so use first box // current could be multivalue but old may not be, so use first box
if (even == 0) { if (even == 0) {
boxes[id][0].setVal(d, boxes[oldID][0].min[d]); ranges[id][0].setMin(d, ranges[oldID][0].getMin(d));
if (VERBOSE) { if (VERBOSE) {
System.out.println(" id=" + id + " box=" + boxes[id] + " (same min[" + d + "] as doc=" + oldID + ")"); System.out.println(" id=" + id + " box=" + ranges[id] + " (same min[" + d + "] as doc=" + oldID + ")");
} }
} else { } else {
boxes[id][0].setVal(d, boxes[oldID][0].max[d]); ranges[id][0].setMax(d, ranges[oldID][0].getMax(d));
if (VERBOSE) { if (VERBOSE) {
System.out.println(" id=" + id + " box=" + boxes[id] + " (same max[" + d + "] as doc=" + oldID + ")"); System.out.println(" id=" + id + " box=" + ranges[id] + " (same max[" + d + "] as doc=" + oldID + ")");
} }
} }
} }
@ -152,20 +153,20 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase {
} }
} }
} }
verify(boxes); verify(ranges);
} }
private void verify(Box[][] boxes) throws Exception { private void verify(Range[][] ranges) throws Exception {
IndexWriterConfig iwc = newIndexWriterConfig(); IndexWriterConfig iwc = newIndexWriterConfig();
// Else seeds may not reproduce: // Else seeds may not reproduce:
iwc.setMergeScheduler(new SerialMergeScheduler()); iwc.setMergeScheduler(new SerialMergeScheduler());
// Else we can get O(N^2) merging // Else we can get O(N^2) merging
int mbd = iwc.getMaxBufferedDocs(); int mbd = iwc.getMaxBufferedDocs();
if (mbd != -1 && mbd < boxes.length/100) { if (mbd != -1 && mbd < ranges.length/100) {
iwc.setMaxBufferedDocs(boxes.length/100); iwc.setMaxBufferedDocs(ranges.length/100);
} }
Directory dir; Directory dir;
if (boxes.length > 50000) { if (ranges.length > 50000) {
dir = newFSDirectory(createTempDir(getClass().getSimpleName())); dir = newFSDirectory(createTempDir(getClass().getSimpleName()));
} else { } else {
dir = newDirectory(); dir = newDirectory();
@ -173,13 +174,13 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase {
Set<Integer> deleted = new HashSet<>(); Set<Integer> deleted = new HashSet<>();
IndexWriter w = new IndexWriter(dir, iwc); IndexWriter w = new IndexWriter(dir, iwc);
for (int id=0; id < boxes.length; ++id) { for (int id=0; id < ranges.length; ++id) {
Document doc = new Document(); Document doc = new Document();
doc.add(newStringField("id", ""+id, Field.Store.NO)); doc.add(newStringField("id", ""+id, Field.Store.NO));
doc.add(new NumericDocValuesField("id", id)); doc.add(new NumericDocValuesField("id", id));
if (Double.isNaN(boxes[id][0].min[0]) == false) { if (ranges[id][0].isMissing == false) {
for (int n=0; n<boxes[id].length; ++n) { for (int n=0; n<ranges[id].length; ++n) {
doc.add(newRangeField(boxes[id][n].min, boxes[id][n].max)); doc.add(newRangeField(ranges[id][n]));
} }
} }
w.addDocument(doc); w.addDocument(doc);
@ -200,7 +201,7 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase {
w.close(); w.close();
IndexSearcher s = newSearcher(r); IndexSearcher s = newSearcher(r);
int dimensions = boxes[0][0].min.length; int dimensions = ranges[0][0].numDimensions();
int iters = atLeast(25); int iters = atLeast(25);
NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id"); NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id");
Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader()); Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader());
@ -211,20 +212,20 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase {
System.out.println("\nTEST: iter=" + iter + " s=" + s); System.out.println("\nTEST: iter=" + iter + " s=" + s);
} }
// occasionally test open ended bounding boxes // occasionally test open ended bounding ranges
Box queryBox = nextBox(dimensions); Range queryRange = nextRange(dimensions);
int rv = random().nextInt(3); int rv = random().nextInt(3);
Query query; Query query;
Box.QueryType queryType; Range.QueryType queryType;
if (rv == 0) { if (rv == 0) {
queryType = Box.QueryType.INTERSECTS; queryType = Range.QueryType.INTERSECTS;
query = newIntersectsQuery(queryBox.min, queryBox.max); query = newIntersectsQuery(queryRange);
} else if (rv == 1) { } else if (rv == 1) {
queryType = Box.QueryType.CONTAINS; queryType = Range.QueryType.CONTAINS;
query = newContainsQuery(queryBox.min, queryBox.max); query = newContainsQuery(queryRange);
} else { } else {
queryType = Box.QueryType.WITHIN; queryType = Range.QueryType.WITHIN;
query = newWithinQuery(queryBox.min, queryBox.max); query = newWithinQuery(queryRange);
} }
if (VERBOSE) { if (VERBOSE) {
@ -255,25 +256,25 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase {
if (liveDocs != null && liveDocs.get(docID) == false) { if (liveDocs != null && liveDocs.get(docID) == false) {
// document is deleted // document is deleted
expected = false; expected = false;
} else if (Double.isNaN(boxes[id][0].min[0])) { } else if (ranges[id][0].isMissing) {
expected = false; expected = false;
} else { } else {
expected = expectedResult(queryBox, boxes[id], queryType); expected = expectedResult(queryRange, ranges[id], queryType);
} }
if (hits.get(docID) != expected) { if (hits.get(docID) != expected) {
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
b.append("FAIL (iter " + iter + "): "); b.append("FAIL (iter " + iter + "): ");
if (expected == true) { if (expected == true) {
b.append("id=" + id + (boxes[id].length > 1 ? " (MultiValue) " : " ") + "should match but did not\n"); b.append("id=" + id + (ranges[id].length > 1 ? " (MultiValue) " : " ") + "should match but did not\n");
} else { } else {
b.append("id=" + id + " should not match but did\n"); b.append("id=" + id + " should not match but did\n");
} }
b.append(" queryBox=" + queryBox + "\n"); b.append(" queryRange=" + queryRange + "\n");
b.append(" box" + ((boxes[id].length > 1) ? "es=" : "=" ) + boxes[id][0]); b.append(" box" + ((ranges[id].length > 1) ? "es=" : "=" ) + ranges[id][0]);
for (int n=1; n<boxes[id].length; ++n) { for (int n=1; n<ranges[id].length; ++n) {
b.append(", "); b.append(", ");
b.append(boxes[id][n]); b.append(ranges[id][n]);
} }
b.append("\n queryType=" + queryType + "\n"); b.append("\n queryType=" + queryType + "\n");
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false)); b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
@ -284,144 +285,51 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase {
IOUtils.close(r, dir); IOUtils.close(r, dir);
} }
protected boolean expectedResult(Box queryBox, Box[] box, Box.QueryType queryType) { protected boolean expectedResult(Range queryRange, Range[] range, Range.QueryType queryType) {
for (int i=0; i<box.length; ++i) { for (int i=0; i<range.length; ++i) {
if (expectedBBoxQueryResult(queryBox, box[i], queryType) == true) { if (expectedBBoxQueryResult(queryRange, range[i], queryType) == true) {
return true; return true;
} }
} }
return false; return false;
} }
protected boolean expectedBBoxQueryResult(Box queryBox, Box box, Box.QueryType queryType) { protected boolean expectedBBoxQueryResult(Range queryRange, Range range, Range.QueryType queryType) {
if (box.equals(queryBox)) { if (queryRange.isEqual(range)) {
return true; return true;
} }
Box.QueryType relation = box.relate(queryBox); Range.QueryType relation = range.relate(queryRange);
if (queryType == Box.QueryType.INTERSECTS) { if (queryType == Range.QueryType.INTERSECTS) {
return relation != null; return relation != null;
} }
return relation == queryType; return relation == queryType;
} }
protected double nextDoubleInternal() { abstract static class Range {
if (rarely()) { protected boolean isMissing = false;
return random().nextBoolean() ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY;
}
double max = 100 / 2;
return (max + max) * random().nextDouble() - max;
}
protected Box nextBox(int dimensions) {
double[] min = new double[dimensions];
double[] max = new double[dimensions];
for (int d=0; d<dimensions; ++d) {
min[d] = nextDoubleInternal();
max[d] = nextDoubleInternal();
}
return new Box(min, max);
}
protected static class Box {
double[] min;
double[] max;
enum QueryType { INTERSECTS, WITHIN, CONTAINS } enum QueryType { INTERSECTS, WITHIN, CONTAINS }
Box(double[] min, double[] max) { protected abstract int numDimensions();
assert min != null && max != null && min.length > 0 && max.length > 0 protected abstract Object getMin(int dim);
: "test box: min/max cannot be null or empty"; protected abstract void setMin(int dim, Object val);
assert min.length == max.length : "test box: min/max length do not agree"; protected abstract Object getMax(int dim);
this.min = new double[min.length]; protected abstract void setMax(int dim, Object val);
this.max = new double[max.length]; protected abstract boolean isEqual(Range other);
for (int d=0; d<min.length; ++d) { protected abstract boolean isDisjoint(Range other);
this.min[d] = Math.min(min[d], max[d]); protected abstract boolean isWithin(Range other);
this.max[d] = Math.max(min[d], max[d]); protected abstract boolean contains(Range other);
}
}
protected void setVal(int dimension, double val) { protected QueryType relate(Range other) {
if (val <= min[dimension]) { if (isDisjoint(other)) {
min[dimension] = val; // if disjoint; return null:
} else { return null;
max[dimension] = val; } else if (isWithin(other)) {
}
}
@Override
public boolean equals(Object o) {
return o != null
&& getClass() == o.getClass()
&& equalTo(getClass().cast(o));
}
private boolean equalTo(Box o) {
return Arrays.equals(min, o.min)
&& Arrays.equals(max, o.max);
}
@Override
public int hashCode() {
int result = Arrays.hashCode(min);
result = 31 * result + Arrays.hashCode(max);
return result;
}
QueryType relate(Box other) {
// check disjoint
for (int d=0; d<this.min.length; ++d) {
if (this.min[d] > other.max[d] || this.max[d] < other.min[d]) {
// disjoint:
return null;
}
}
// check within
boolean within = true;
for (int d=0; d<this.min.length; ++d) {
if ((this.min[d] >= other.min[d] && this.max[d] <= other.max[d]) == false) {
// not within:
within = false;
break;
}
}
if (within == true) {
return QueryType.WITHIN; return QueryType.WITHIN;
} } else if (contains(other)) {
// check contains
boolean contains = true;
for (int d=0; d<this.min.length; ++d) {
if ((this.min[d] <= other.min[d] && this.max[d] >= other.max[d]) == false) {
// not contains:
contains = false;
break;
}
}
if (contains == true) {
return QueryType.CONTAINS; return QueryType.CONTAINS;
} }
return QueryType.INTERSECTS; return QueryType.INTERSECTS;
} }
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append("Box(");
b.append(min[0]);
b.append(" TO ");
b.append(max[0]);
for (int d=1; d<min.length; ++d) {
b.append(", ");
b.append(min[d]);
b.append(" TO ");
b.append(max[d]);
}
b.append(")");
return b.toString();
}
} }
} }

View File

@ -16,6 +16,8 @@
*/ */
package org.apache.lucene.search; package org.apache.lucene.search;
import java.util.Arrays;
import org.apache.lucene.document.Document; import org.apache.lucene.document.Document;
import org.apache.lucene.document.DoubleRangeField; import org.apache.lucene.document.DoubleRangeField;
import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexReader;
@ -23,25 +25,50 @@ import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.store.Directory; import org.apache.lucene.store.Directory;
/** /**
* Random testing for RangeFieldQueries. Testing rigor inspired by {@code BaseGeoPointTestCase} * Random testing for RangeFieldQueries.
*/ */
public class TestDoubleRangeFieldQueries extends BaseRangeFieldQueryTestCase { public class TestDoubleRangeFieldQueries extends BaseRangeFieldQueryTestCase {
private static final String FIELD_NAME = "rangeField"; private static final String FIELD_NAME = "doubleRangeField";
protected DoubleRangeField newRangeField(double[] min, double[] max) { private double nextDoubleInternal() {
return new DoubleRangeField(FIELD_NAME, min, max); if (rarely()) {
return random().nextBoolean() ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY;
}
double max = Double.MAX_VALUE / 2;
return (max + max) * random().nextDouble() - max;
} }
protected Query newIntersectsQuery(double[] min, double[] max) { @Override
return DoubleRangeField.newIntersectsQuery(FIELD_NAME, min, max); protected Range nextRange(int dimensions) {
double[] min = new double[dimensions];
double[] max = new double[dimensions];
for (int d=0; d<dimensions; ++d) {
min[d] = nextDoubleInternal();
max[d] = nextDoubleInternal();
}
return new DoubleRange(min, max);
} }
protected Query newContainsQuery(double[] min, double[] max) { @Override
return DoubleRangeField.newContainsQuery(FIELD_NAME, min, max); protected DoubleRangeField newRangeField(Range r) {
return new DoubleRangeField(FIELD_NAME, ((DoubleRange)r).min, ((DoubleRange)r).max);
} }
protected Query newWithinQuery(double[] min, double[] max) { @Override
return DoubleRangeField.newWithinQuery(FIELD_NAME, min, max); protected Query newIntersectsQuery(Range r) {
return DoubleRangeField.newIntersectsQuery(FIELD_NAME, ((DoubleRange)r).min, ((DoubleRange)r).max);
}
@Override
protected Query newContainsQuery(Range r) {
return DoubleRangeField.newContainsQuery(FIELD_NAME, ((DoubleRange)r).min, ((DoubleRange)r).max);
}
@Override
protected Query newWithinQuery(Range r) {
return DoubleRangeField.newWithinQuery(FIELD_NAME, ((DoubleRange)r).min, ((DoubleRange)r).max);
} }
/** Basic test */ /** Basic test */
@ -103,4 +130,111 @@ public class TestDoubleRangeFieldQueries extends BaseRangeFieldQueryTestCase {
writer.close(); writer.close();
dir.close(); dir.close();
} }
/** DoubleRange test class implementation - use to validate DoubleRangeField */
private class DoubleRange extends Range {
double[] min;
double[] max;
DoubleRange(double[] min, double[] max) {
assert min != null && max != null && min.length > 0 && max.length > 0
: "test box: min/max cannot be null or empty";
assert min.length == max.length : "test box: min/max length do not agree";
this.min = new double[min.length];
this.max = new double[max.length];
for (int d=0; d<min.length; ++d) {
if (min[d] > max[d]) {
// swap if max < min:
double temp = min[d];
min[d] = max[d];
max[d] = temp;
}
}
}
@Override
protected int numDimensions() {
return min.length;
}
@Override
protected Double getMin(int dim) {
return min[dim];
}
@Override
protected void setMin(int dim, Object val) {
min[dim] = (Double)val;
}
@Override
protected Double getMax(int dim) {
return max[dim];
}
@Override
protected void setMax(int dim, Object val) {
max[dim] = (Double)val;
}
@Override
protected boolean isEqual(Range other) {
DoubleRange o = (DoubleRange)other;
return Arrays.equals(min, o.min) && Arrays.equals(max, o.max);
}
@Override
protected boolean isDisjoint(Range o) {
DoubleRange other = (DoubleRange)o;
for (int d=0; d<this.min.length; ++d) {
if (this.min[d] > other.max[d] || this.max[d] < other.min[d]) {
// disjoint:
return true;
}
}
return false;
}
@Override
protected boolean isWithin(Range o) {
DoubleRange other = (DoubleRange)o;
for (int d=0; d<this.min.length; ++d) {
if ((this.min[d] >= other.min[d] && this.max[d] <= other.max[d]) == false) {
// not within:
return false;
}
}
return true;
}
@Override
protected boolean contains(Range o) {
DoubleRange other = (DoubleRange) o;
for (int d=0; d<this.min.length; ++d) {
if ((this.min[d] <= other.min[d] && this.max[d] >= other.max[d]) == false) {
// not contains:
return false;
}
}
return true;
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append("Box(");
b.append(min[0]);
b.append(" TO ");
b.append(max[0]);
for (int d=1; d<min.length; ++d) {
b.append(", ");
b.append(min[d]);
b.append(" TO ");
b.append(max[d]);
}
b.append(")");
return b.toString();
}
}
} }

View File

@ -0,0 +1,240 @@
/*
* 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.util.Arrays;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.FloatRangeField;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.store.Directory;
/**
* Random testing for FloatRangeField Queries.
*/
public class TestFloatRangeFieldQueries extends BaseRangeFieldQueryTestCase {
private static final String FIELD_NAME = "floatRangeField";
private float nextFloatInternal() {
if (rarely()) {
return random().nextBoolean() ? Float.NEGATIVE_INFINITY : Float.POSITIVE_INFINITY;
}
float max = Float.MAX_VALUE / 2;
return (max + max) * random().nextFloat() - max;
}
@Override
protected Range nextRange(int dimensions) {
float[] min = new float[dimensions];
float[] max = new float[dimensions];
for (int d=0; d<dimensions; ++d) {
min[d] = nextFloatInternal();
max[d] = nextFloatInternal();
}
return new FloatRange(min, max);
}
@Override
protected FloatRangeField newRangeField(Range r) {
return new FloatRangeField(FIELD_NAME, ((FloatRange)r).min, ((FloatRange)r).max);
}
@Override
protected Query newIntersectsQuery(Range r) {
return FloatRangeField.newIntersectsQuery(FIELD_NAME, ((FloatRange)r).min, ((FloatRange)r).max);
}
@Override
protected Query newContainsQuery(Range r) {
return FloatRangeField.newContainsQuery(FIELD_NAME, ((FloatRange)r).min, ((FloatRange)r).max);
}
@Override
protected Query newWithinQuery(Range r) {
return FloatRangeField.newWithinQuery(FIELD_NAME, ((FloatRange)r).min, ((FloatRange)r).max);
}
/** Basic test */
public void testBasics() throws Exception {
Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
// intersects (within)
Document document = new Document();
document.add(new FloatRangeField(FIELD_NAME, new float[] {-10.0f, -10.0f}, new float[] {9.1f, 10.1f}));
writer.addDocument(document);
// intersects (crosses)
document = new Document();
document.add(new FloatRangeField(FIELD_NAME, new float[] {10.0f, -10.0f}, new float[] {20.0f, 10.0f}));
writer.addDocument(document);
// intersects (contains)
document = new Document();
document.add(new FloatRangeField(FIELD_NAME, new float[] {-20.0f, -20.0f}, new float[] {30.0f, 30.1f}));
writer.addDocument(document);
// intersects (crosses)
document = new Document();
document.add(new FloatRangeField(FIELD_NAME, new float[] {-11.1f, -11.2f}, new float[] {1.23f, 11.5f}));
writer.addDocument(document);
// intersects (crosses)
document = new Document();
document.add(new FloatRangeField(FIELD_NAME, new float[] {12.33f, 1.2f}, new float[] {15.1f, 29.9f}));
writer.addDocument(document);
// disjoint
document = new Document();
document.add(new FloatRangeField(FIELD_NAME, new float[] {-122.33f, 1.2f}, new float[] {-115.1f, 29.9f}));
writer.addDocument(document);
// intersects (crosses)
document = new Document();
document.add(new FloatRangeField(FIELD_NAME, new float[] {Float.NEGATIVE_INFINITY, 1.2f}, new float[] {-11.0f, 29.9f}));
writer.addDocument(document);
// equal (within, contains, intersects)
document = new Document();
document.add(new FloatRangeField(FIELD_NAME, new float[] {-11f, -15f}, new float[] {15f, 20f}));
writer.addDocument(document);
// search
IndexReader reader = writer.getReader();
IndexSearcher searcher = newSearcher(reader);
assertEquals(7, searcher.count(FloatRangeField.newIntersectsQuery(FIELD_NAME,
new float[] {-11.0f, -15.0f}, new float[] {15.0f, 20.0f})));
assertEquals(2, searcher.count(FloatRangeField.newWithinQuery(FIELD_NAME,
new float[] {-11.0f, -15.0f}, new float[] {15.0f, 20.0f})));
assertEquals(2, searcher.count(FloatRangeField.newContainsQuery(FIELD_NAME,
new float[] {-11.0f, -15.0f}, new float[] {15.0f, 20.0f})));
reader.close();
writer.close();
dir.close();
}
/** FloatRange test class implementation - use to validate FloatRangeField */
private class FloatRange extends Range {
float[] min;
float[] max;
FloatRange(float[] min, float[] max) {
assert min != null && max != null && min.length > 0 && max.length > 0
: "test box: min/max cannot be null or empty";
assert min.length == max.length : "test box: min/max length do not agree";
this.min = new float[min.length];
this.max = new float[max.length];
for (int d=0; d<min.length; ++d) {
if (min[d] > max[d]) {
// swap if max < min:
float temp = min[d];
min[d] = max[d];
max[d] = temp;
}
}
}
@Override
protected int numDimensions() {
return min.length;
}
@Override
protected Float getMin(int dim) {
return min[dim];
}
@Override
protected void setMin(int dim, Object val) {
min[dim] = (Float)val;
}
@Override
protected Float getMax(int dim) {
return max[dim];
}
@Override
protected void setMax(int dim, Object val) {
max[dim] = (Float)val;
}
@Override
protected boolean isEqual(Range other) {
FloatRange o = (FloatRange)other;
return Arrays.equals(min, o.min) && Arrays.equals(max, o.max);
}
@Override
protected boolean isDisjoint(Range o) {
FloatRange other = (FloatRange)o;
for (int d=0; d<this.min.length; ++d) {
if (this.min[d] > other.max[d] || this.max[d] < other.min[d]) {
// disjoint:
return true;
}
}
return false;
}
@Override
protected boolean isWithin(Range o) {
FloatRange other = (FloatRange)o;
for (int d=0; d<this.min.length; ++d) {
if ((this.min[d] >= other.min[d] && this.max[d] <= other.max[d]) == false) {
// not within:
return false;
}
}
return true;
}
@Override
protected boolean contains(Range o) {
FloatRange other = (FloatRange) o;
for (int d=0; d<this.min.length; ++d) {
if ((this.min[d] <= other.min[d] && this.max[d] >= other.max[d]) == false) {
// not contains:
return false;
}
}
return true;
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append("Box(");
b.append(min[0]);
b.append(" TO ");
b.append(max[0]);
for (int d=1; d<min.length; ++d) {
b.append(", ");
b.append(min[d]);
b.append(" TO ");
b.append(max[d]);
}
b.append(")");
return b.toString();
}
}
}

View File

@ -0,0 +1,240 @@
/*
* 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.util.Arrays;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.IntRangeField;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.store.Directory;
/**
* Random testing for IntRangeField Queries.
*/
public class TestIntRangeFieldQueries extends BaseRangeFieldQueryTestCase {
private static final String FIELD_NAME = "intRangeField";
private int nextIntInternal() {
if (rarely()) {
return random().nextBoolean() ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
int max = Integer.MAX_VALUE / 2;
return (max + max) * random().nextInt() - max;
}
@Override
protected Range nextRange(int dimensions) {
int[] min = new int[dimensions];
int[] max = new int[dimensions];
for (int d=0; d<dimensions; ++d) {
min[d] = nextIntInternal();
max[d] = nextIntInternal();
}
return new IntRange(min, max);
}
@Override
protected IntRangeField newRangeField(Range r) {
return new IntRangeField(FIELD_NAME, ((IntRange)r).min, ((IntRange)r).max);
}
@Override
protected Query newIntersectsQuery(Range r) {
return IntRangeField.newIntersectsQuery(FIELD_NAME, ((IntRange)r).min, ((IntRange)r).max);
}
@Override
protected Query newContainsQuery(Range r) {
return IntRangeField.newContainsQuery(FIELD_NAME, ((IntRange)r).min, ((IntRange)r).max);
}
@Override
protected Query newWithinQuery(Range r) {
return IntRangeField.newWithinQuery(FIELD_NAME, ((IntRange)r).min, ((IntRange)r).max);
}
/** Basic test */
public void testBasics() throws Exception {
Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
// intersects (within)
Document document = new Document();
document.add(new IntRangeField(FIELD_NAME, new int[] {-10, -10}, new int[] {9, 10}));
writer.addDocument(document);
// intersects (crosses)
document = new Document();
document.add(new IntRangeField(FIELD_NAME, new int[] {10, -10}, new int[] {20, 10}));
writer.addDocument(document);
// intersects (contains)
document = new Document();
document.add(new IntRangeField(FIELD_NAME, new int[] {-20, -20}, new int[] {30, 30}));
writer.addDocument(document);
// intersects (within)
document = new Document();
document.add(new IntRangeField(FIELD_NAME, new int[] {-11, -11}, new int[] {1, 11}));
writer.addDocument(document);
// intersects (crosses)
document = new Document();
document.add(new IntRangeField(FIELD_NAME, new int[] {12, 1}, new int[] {15, 29}));
writer.addDocument(document);
// disjoint
document = new Document();
document.add(new IntRangeField(FIELD_NAME, new int[] {-122, 1}, new int[] {-115, 29}));
writer.addDocument(document);
// intersects (crosses)
document = new Document();
document.add(new IntRangeField(FIELD_NAME, new int[] {Integer.MIN_VALUE, 1}, new int[] {-11, 29}));
writer.addDocument(document);
// equal (within, contains, intersects)
document = new Document();
document.add(new IntRangeField(FIELD_NAME, new int[] {-11, -15}, new int[] {15, 20}));
writer.addDocument(document);
// search
IndexReader reader = writer.getReader();
IndexSearcher searcher = newSearcher(reader);
assertEquals(7, searcher.count(IntRangeField.newIntersectsQuery(FIELD_NAME,
new int[] {-11, -15}, new int[] {15, 20})));
assertEquals(3, searcher.count(IntRangeField.newWithinQuery(FIELD_NAME,
new int[] {-11, -15}, new int[] {15, 20})));
assertEquals(2, searcher.count(IntRangeField.newContainsQuery(FIELD_NAME,
new int[] {-11, -15}, new int[] {15, 20})));
reader.close();
writer.close();
dir.close();
}
/** IntRange test class implementation - use to validate IntRangeField */
private class IntRange extends Range {
int[] min;
int[] max;
IntRange(int[] min, int[] max) {
assert min != null && max != null && min.length > 0 && max.length > 0
: "test box: min/max cannot be null or empty";
assert min.length == max.length : "test box: min/max length do not agree";
this.min = new int[min.length];
this.max = new int[max.length];
for (int d=0; d<min.length; ++d) {
if (min[d] > max[d]) {
// swap if max < min:
int temp = min[d];
min[d] = max[d];
max[d] = temp;
}
}
}
@Override
protected int numDimensions() {
return min.length;
}
@Override
protected Integer getMin(int dim) {
return min[dim];
}
@Override
protected void setMin(int dim, Object val) {
min[dim] = (Integer)val;
}
@Override
protected Integer getMax(int dim) {
return max[dim];
}
@Override
protected void setMax(int dim, Object val) {
max[dim] = (Integer)val;
}
@Override
protected boolean isEqual(Range other) {
IntRange o = (IntRange)other;
return Arrays.equals(min, o.min) && Arrays.equals(max, o.max);
}
@Override
protected boolean isDisjoint(Range o) {
IntRange other = (IntRange)o;
for (int d=0; d<this.min.length; ++d) {
if (this.min[d] > other.max[d] || this.max[d] < other.min[d]) {
// disjoint:
return true;
}
}
return false;
}
@Override
protected boolean isWithin(Range o) {
IntRange other = (IntRange)o;
for (int d=0; d<this.min.length; ++d) {
if ((this.min[d] >= other.min[d] && this.max[d] <= other.max[d]) == false) {
// not within:
return false;
}
}
return true;
}
@Override
protected boolean contains(Range o) {
IntRange other = (IntRange) o;
for (int d=0; d<this.min.length; ++d) {
if ((this.min[d] <= other.min[d] && this.max[d] >= other.max[d]) == false) {
// not contains:
return false;
}
}
return true;
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append("Box(");
b.append(min[0]);
b.append(" TO ");
b.append(max[0]);
for (int d=1; d<min.length; ++d) {
b.append(", ");
b.append(min[d]);
b.append(" TO ");
b.append(max[d]);
}
b.append(")");
return b.toString();
}
}
}

View File

@ -0,0 +1,240 @@
/*
* 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.util.Arrays;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.LongRangeField;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.store.Directory;
/**
* Random testing for LongRangeField Queries.
*/
public class TestLongRangeFieldQueries extends BaseRangeFieldQueryTestCase {
private static final String FIELD_NAME = "longRangeField";
private long nextLongInternal() {
if (rarely()) {
return random().nextBoolean() ? Long.MAX_VALUE : Long.MIN_VALUE;
}
long max = Long.MAX_VALUE / 2;
return (max + max) * random().nextLong() - max;
}
@Override
protected Range nextRange(int dimensions) {
long[] min = new long[dimensions];
long[] max = new long[dimensions];
for (int d=0; d<dimensions; ++d) {
min[d] = nextLongInternal();
max[d] = nextLongInternal();
}
return new LongRange(min, max);
}
@Override
protected LongRangeField newRangeField(Range r) {
return new LongRangeField(FIELD_NAME, ((LongRange)r).min, ((LongRange)r).max);
}
@Override
protected Query newIntersectsQuery(Range r) {
return LongRangeField.newIntersectsQuery(FIELD_NAME, ((LongRange)r).min, ((LongRange)r).max);
}
@Override
protected Query newContainsQuery(Range r) {
return LongRangeField.newContainsQuery(FIELD_NAME, ((LongRange)r).min, ((LongRange)r).max);
}
@Override
protected Query newWithinQuery(Range r) {
return LongRangeField.newWithinQuery(FIELD_NAME, ((LongRange)r).min, ((LongRange)r).max);
}
/** Basic test */
public void testBasics() throws Exception {
Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
// intersects (within)
Document document = new Document();
document.add(new LongRangeField(FIELD_NAME, new long[] {-10, -10}, new long[] {9, 10}));
writer.addDocument(document);
// intersects (crosses)
document = new Document();
document.add(new LongRangeField(FIELD_NAME, new long[] {10, -10}, new long[] {20, 10}));
writer.addDocument(document);
// intersects (contains)
document = new Document();
document.add(new LongRangeField(FIELD_NAME, new long[] {-20, -20}, new long[] {30, 30}));
writer.addDocument(document);
// intersects (within)
document = new Document();
document.add(new LongRangeField(FIELD_NAME, new long[] {-11, -11}, new long[] {1, 11}));
writer.addDocument(document);
// intersects (crosses)
document = new Document();
document.add(new LongRangeField(FIELD_NAME, new long[] {12, 1}, new long[] {15, 29}));
writer.addDocument(document);
// disjoint
document = new Document();
document.add(new LongRangeField(FIELD_NAME, new long[] {-122, 1}, new long[] {-115, 29}));
writer.addDocument(document);
// intersects (crosses)
document = new Document();
document.add(new LongRangeField(FIELD_NAME, new long[] {Long.MIN_VALUE, 1}, new long[] {-11, 29}));
writer.addDocument(document);
// equal (within, contains, intersects)
document = new Document();
document.add(new LongRangeField(FIELD_NAME, new long[] {-11, -15}, new long[] {15, 20}));
writer.addDocument(document);
// search
IndexReader reader = writer.getReader();
IndexSearcher searcher = newSearcher(reader);
assertEquals(7, searcher.count(LongRangeField.newIntersectsQuery(FIELD_NAME,
new long[] {-11, -15}, new long[] {15, 20})));
assertEquals(3, searcher.count(LongRangeField.newWithinQuery(FIELD_NAME,
new long[] {-11, -15}, new long[] {15, 20})));
assertEquals(2, searcher.count(LongRangeField.newContainsQuery(FIELD_NAME,
new long[] {-11, -15}, new long[] {15, 20})));
reader.close();
writer.close();
dir.close();
}
/** LongRange test class implementation - use to validate LongRangeField */
private class LongRange extends Range {
long[] min;
long[] max;
LongRange(long[] min, long[] max) {
assert min != null && max != null && min.length > 0 && max.length > 0
: "test box: min/max cannot be null or empty";
assert min.length == max.length : "test box: min/max length do not agree";
this.min = new long[min.length];
this.max = new long[max.length];
for (int d=0; d<min.length; ++d) {
if (min[d] > max[d]) {
// swap if max < min:
long temp = min[d];
min[d] = max[d];
max[d] = temp;
}
}
}
@Override
protected int numDimensions() {
return min.length;
}
@Override
protected Long getMin(int dim) {
return min[dim];
}
@Override
protected void setMin(int dim, Object val) {
min[dim] = (Long)val;
}
@Override
protected Long getMax(int dim) {
return max[dim];
}
@Override
protected void setMax(int dim, Object val) {
max[dim] = (Long)val;
}
@Override
protected boolean isEqual(Range other) {
LongRange o = (LongRange)other;
return Arrays.equals(min, o.min) && Arrays.equals(max, o.max);
}
@Override
protected boolean isDisjoint(Range o) {
LongRange other = (LongRange)o;
for (int d=0; d<this.min.length; ++d) {
if (this.min[d] > other.max[d] || this.max[d] < other.min[d]) {
// disjoint:
return true;
}
}
return false;
}
@Override
protected boolean isWithin(Range o) {
LongRange other = (LongRange)o;
for (int d=0; d<this.min.length; ++d) {
if ((this.min[d] >= other.min[d] && this.max[d] <= other.max[d]) == false) {
// not within:
return false;
}
}
return true;
}
@Override
protected boolean contains(Range o) {
LongRange other = (LongRange) o;
for (int d=0; d<this.min.length; ++d) {
if ((this.min[d] <= other.min[d] && this.max[d] >= other.max[d]) == false) {
// not contains:
return false;
}
}
return true;
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append("Box(");
b.append(min[0]);
b.append(" TO ");
b.append(max[0]);
for (int d=1; d<min.length; ++d) {
b.append(", ");
b.append(min[d]);
b.append(" TO ");
b.append(max[d]);
}
b.append(")");
return b.toString();
}
}
}