diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index d3b2db6b6b1..3f7f7c3f6f3 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -38,6 +38,9 @@ API Changes 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 indexing and querying on Ranges up to 4 dimensions (Nick Knize) diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/FloatRangeField.java b/lucene/sandbox/src/java/org/apache/lucene/document/FloatRangeField.java new file mode 100644 index 00000000000..e138ae2057d --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/FloatRangeField.java @@ -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. + *

+ * 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. + *

+ * 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}. + * + *

+ * This field defines the following static factory methods for common search operations over float ranges: + *

+ */ +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) + *

+ * 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 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'); + + 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)) + "]"; + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/IntRangeField.java b/lucene/sandbox/src/java/org/apache/lucene/document/IntRangeField.java new file mode 100644 index 00000000000..c0ce61d85e3 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/IntRangeField.java @@ -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. + *

+ * 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. + *

+ * 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}. + * + *

+ * This field defines the following static factory methods for common search operations over integer ranges: + *

+ */ +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) + *

+ * 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 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'); + + 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)) + "]"; + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LongRangeField.java b/lucene/sandbox/src/java/org/apache/lucene/document/LongRangeField.java new file mode 100644 index 00000000000..b9298b9d8d3 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LongRangeField.java @@ -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. + *

+ * 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. + *

+ * 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}. + * + *

+ * This field defines the following static factory methods for common search operations over long ranges: + *

+ */ +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) + *

+ * 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 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'); + + 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)) + "]"; + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/search/BaseRangeFieldQueryTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/search/BaseRangeFieldQueryTestCase.java index d9cb830c120..9d293305c70 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/search/BaseRangeFieldQueryTestCase.java +++ b/lucene/sandbox/src/test/org/apache/lucene/search/BaseRangeFieldQueryTestCase.java @@ -17,7 +17,6 @@ package org.apache.lucene.search; import java.io.IOException; -import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -41,16 +40,18 @@ import org.apache.lucene.util.IOUtils; 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 { - 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() { return random().nextInt(4) + 1; @@ -82,18 +83,18 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase { System.out.println("TEST: numDocs=" + numDocs); } - Box[][] boxes = new Box[numDocs][]; + Range[][] ranges = new Range[numDocs][]; boolean haveRealDoc = true; nextdoc: for (int id=0; id 0 && x < 9 && haveRealDoc) { int oldID; int i=0; - // don't step on missing boxes: + // don't step on missing ranges: while (true) { oldID = random().nextInt(id); - if (Double.isNaN(boxes[oldID][0].min[0]) == false) { + if (ranges[oldID][0].isMissing == false) { break; } else if (++i > id) { continue nextdoc; @@ -125,11 +126,11 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase { if (x == dimensions*2) { // Fully identical box (use first box in case current is multivalued but old is not) for (int d=0; d 50000) { + if (ranges.length > 50000) { dir = newFSDirectory(createTempDir(getClass().getSimpleName())); } else { dir = newDirectory(); @@ -173,13 +174,13 @@ public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase { Set deleted = new HashSet<>(); 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(); doc.add(newStringField("id", ""+id, Field.Store.NO)); doc.add(new NumericDocValuesField("id", id)); - if (Double.isNaN(boxes[id][0].min[0]) == false) { - for (int n=0; n 1) ? "es=" : "=" ) + boxes[id][0]); - for (int n=1; n 1) ? "es=" : "=" ) + ranges[id][0]); + for (int n=1; n other.max[d] || this.max[d] < other.min[d]) { - // disjoint: - return null; - } - } - - // check within - boolean within = true; - for (int d=0; d= other.min[d] && this.max[d] <= other.max[d]) == false) { - // not within: - within = false; - break; - } - } - if (within == true) { + protected QueryType relate(Range other) { + if (isDisjoint(other)) { + // if disjoint; return null: + return null; + } else if (isWithin(other)) { return QueryType.WITHIN; - } - - // check contains - boolean contains = true; - for (int d=0; d= other.max[d]) == false) { - // not contains: - contains = false; - break; - } - } - if (contains == true) { + } else if (contains(other)) { return QueryType.CONTAINS; } 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 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 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 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= 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= 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 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 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 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= 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= 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 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 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 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= 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= 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 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 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 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= 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= 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