diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 432e1d2c144..6d663552433 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -38,6 +38,9 @@ API Changes New Features +* LUCENE-7381: Add point based DoubleRangeField and RangeFieldQuery for + indexing and querying on Ranges up to 4 dimensions (Nick Knize) + * LUCENE-7302: IndexWriter methods that change the index now return a long "sequence number" indicating the effective equivalent single-threaded execution order (Mike McCandless) diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/DoubleRangeField.java b/lucene/sandbox/src/java/org/apache/lucene/document/DoubleRangeField.java new file mode 100644 index 00000000000..2af8697f388 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/DoubleRangeField.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 Double 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 double 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 Double.NEGATIVE_INFINITY} and {@code Double.POSITIVE_INFINITY}. + * + *

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

+ */ +public class DoubleRangeField extends Field { + /** stores double values so number of bytes is 8 */ + public static final int BYTES = Double.BYTES; + + /** + * Create a new DoubleRangeField 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 DoubleRangeField(String name, final double[] min, final double[] 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("DoubleRangeField 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 Double.NEGATIVE_INFINITY}) + * @param max array of max values. (accepts {@code Double.POSITIVE_INFINITY}) + * @throws IllegalArgumentException if {@code min} or {@code max} is invalid + */ + public void setRangeValues(double[] min, double[] 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 double[] min, final double[] 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("DoubleRangeField does not support greater than 4 dimensions"); + } + } + + /** + * Encodes the min, max ranges into a byte array + */ + private static byte[] encode(double[] min, double[] 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(double[] min, double[] 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(double val, byte[] bytes, int offset) { + NumericUtils.longToSortableBytes(NumericUtils.doubleToSortableLong(val), bytes, offset); + } + + /** + * Get the min value for the given dimension + * @param dimension the dimension, always positive + * @return the decoded min value + */ + public double 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 double 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 double decodeMin(byte[] b, int dimension) { + int offset = dimension*BYTES; + return NumericUtils.sortableLongToDouble(NumericUtils.sortableBytesToLong(b, offset)); + } + + /** decodes the max value (for the defined dimension) from the encoded input byte array */ + static double decodeMax(byte[] b, int dimension) { + int offset = b.length/2 + dimension*BYTES; + return NumericUtils.sortableLongToDouble(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 Double.NEGATIVE_INFINITY}) + * @param max array of max values. (accepts {@code Double.POSITIVE_INFINITY}) + * @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 double[] min, final double[] max) { + return new RangeFieldQuery(field, encode(min, max), min.length, QueryType.INTERSECTS) { + @Override + protected String toString(byte[] ranges, int dimension) { + return DoubleRangeField.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 Double.MIN_VALUE}) + * @param max array of max values. (accepts {@code Double.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 double[] min, final double[] max) { + return new RangeFieldQuery(field, encode(min, max), min.length, QueryType.CONTAINS) { + @Override + protected String toString(byte[] ranges, int dimension) { + return DoubleRangeField.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 Double.MIN_VALUE}) + * @param max array of max values. (accepts {@code Double.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 double[] min, final double[] max) { + checkArgs(min, max); + return new RangeFieldQuery(field, encode(min, max), min.length, QueryType.WITHIN) { + @Override + protected String toString(byte[] ranges, int dimension) { + return DoubleRangeField.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 "[" + Double.toString(decodeMin(ranges, dimension)) + " : " + + Double.toString(decodeMax(ranges, dimension)) + "]"; + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/RangeFieldQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/RangeFieldQuery.java new file mode 100644 index 00000000000..36de9b2581f --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/RangeFieldQuery.java @@ -0,0 +1,313 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.lucene.document; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.PointValues; +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.index.PointValues.IntersectVisitor; +import org.apache.lucene.search.ConstantScoreScorer; +import org.apache.lucene.search.ConstantScoreWeight; +import org.apache.lucene.search.DocIdSet; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Weight; +import org.apache.lucene.util.DocIdSetBuilder; +import org.apache.lucene.util.StringHelper; + +/** + * Query class for searching {@code RangeField} types by a defined {@link Relation}. + */ +abstract class RangeFieldQuery extends Query { + /** field name */ + final String field; + /** query relation + * intersects: {@code CELL_CROSSES_QUERY}, + * contains: {@code CELL_CONTAINS_QUERY}, + * within: {@code CELL_WITHIN_QUERY} */ + final QueryType queryType; + /** number of dimensions - max 4 */ + final int numDims; + /** ranges encoded as a sortable byte array */ + final byte[] ranges; + /** number of bytes per dimension */ + final int bytesPerDim; + + /** Used by {@code RangeFieldQuery} to check how each internal or leaf node relates to the query. */ + enum QueryType { + /** Use this for intersects queries. */ + INTERSECTS, + /** Use this for within queries. */ + WITHIN, + /** Use this for contains */ + CONTAINS + } + + /** + * Create a query for searching indexed ranges that match the provided relation. + * @param field field name. must not be null. + * @param ranges encoded range values; this is done by the {@code RangeField} implementation + * @param queryType the query relation + */ + RangeFieldQuery(String field, final byte[] ranges, final int numDims, final QueryType queryType) { + checkArgs(field, ranges, numDims); + if (queryType == null) { + throw new IllegalArgumentException("Query type cannot be null"); + } + this.field = field; + this.queryType = queryType; + this.numDims = numDims; + this.ranges = ranges; + this.bytesPerDim = ranges.length / (2*numDims); + } + + /** check input arguments */ + private static void checkArgs(String field, final byte[] ranges, final int numDims) { + if (field == null) { + throw new IllegalArgumentException("field must not be null"); + } + if (numDims > 4) { + throw new IllegalArgumentException("dimension size cannot be greater than 4"); + } + if (ranges == null || ranges.length == 0) { + throw new IllegalArgumentException("encoded ranges cannot be null or empty"); + } + } + + /** Check indexed field info against the provided query data. */ + private void checkFieldInfo(FieldInfo fieldInfo) { + if (fieldInfo.getPointDimensionCount()/2 != numDims) { + throw new IllegalArgumentException("field=\"" + field + "\" was indexed with numDims=" + + fieldInfo.getPointDimensionCount()/2 + " but this query has numDims=" + numDims); + } + } + + @Override + public final Weight createWeight(IndexSearcher searcher, boolean needsScores, float boost) throws IOException { + return new ConstantScoreWeight(this, boost) { + final RangeFieldComparator comparator = new RangeFieldComparator(); + private DocIdSet buildMatchingDocIdSet(LeafReader reader, PointValues values) throws IOException { + DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field); + values.intersect(field, + new IntersectVisitor() { + DocIdSetBuilder.BulkAdder adder; + @Override + public void grow(int count) { + adder = result.grow(count); + } + @Override + public void visit(int docID) throws IOException { + adder.add(docID); + } + @Override + public void visit(int docID, byte[] leaf) throws IOException { + // add the document iff: + if (// target is within cell and queryType is INTERSECTS or CONTAINS: + (comparator.isWithin(leaf) && queryType != QueryType.WITHIN) + // target contains cell and queryType is INTERSECTS or WITHIN: + || (comparator.contains(leaf) && queryType != QueryType.CONTAINS) + // target is not disjoint (crosses) and queryType is INTERSECTS + || (comparator.isDisjoint(leaf) == false && queryType == QueryType.INTERSECTS)) { + adder.add(docID); + } + } + @Override + public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) { + byte[] node = getInternalRange(minPackedValue, maxPackedValue); + // compute range relation for BKD traversal + if (comparator.isDisjoint(node)) { + return Relation.CELL_OUTSIDE_QUERY; + } else if (comparator.contains(node)) { + // target contains cell; add iff queryType is not a CONTAINS query: + return (queryType == QueryType.CONTAINS) ? Relation.CELL_OUTSIDE_QUERY : Relation.CELL_INSIDE_QUERY; + } else if (comparator.isWithin(node)) { + // target within cell; continue traversing: + return Relation.CELL_CROSSES_QUERY; + } + // target intersects cell; continue traversing: + return Relation.CELL_CROSSES_QUERY; + } + }); + return result.build(); + } + + @Override + public Scorer scorer(LeafReaderContext context) throws IOException { + LeafReader reader = context.reader(); + PointValues values = reader.getPointValues(); + if (values == null) { + // no docs in this segment indexed any ranges + return null; + } + FieldInfo fieldInfo = reader.getFieldInfos().fieldInfo(field); + if (fieldInfo == null) { + // no docs in this segment indexed this field + } + checkFieldInfo(fieldInfo); + boolean allDocsMatch = true; + if (values.getDocCount(field) == reader.maxDoc()) { + // if query crosses, docs need to be further scrutinized + byte[] range = getInternalRange(values.getMinPackedValue(field), values.getMaxPackedValue(field)); + // if the internal node is not contained by the query, all docs do not match + if (((comparator.contains(range) && queryType == QueryType.CONTAINS)) == false) { + allDocsMatch = false; + } + } else { + allDocsMatch = false; + } + + DocIdSetIterator iterator = allDocsMatch == true ? + DocIdSetIterator.all(reader.maxDoc()) : buildMatchingDocIdSet(reader, values).iterator(); + return new ConstantScoreScorer(this, score(), iterator); + } + + /** get an encoded byte representation of the internal node; this is + * the lower half of the min array and the upper half of the max array */ + private byte[] getInternalRange(byte[] min, byte[] max) { + byte[] range = new byte[min.length]; + final int dimSize = numDims * bytesPerDim; + System.arraycopy(min, 0, range, 0, dimSize); + System.arraycopy(max, dimSize, range, dimSize, dimSize); + return range; + } + }; + } + + /** + * RangeFieldComparator class provides the core comparison logic for accepting or rejecting indexed + * {@code RangeField} types based on the defined query range and relation. + */ + class RangeFieldComparator { + /** check if the query is outside the candidate range */ + private boolean isDisjoint(final byte[] range) { + for (int d=0; d 0 || compareMaxMin(range, d) < 0) { + return true; + } + } + return false; + } + + /** check if query is within candidate range */ + private boolean isWithin(final byte[] range) { + for (int d=0; d 0) { + return false; + } + } + return true; + } + + /** check if query contains candidate range */ + private boolean contains(final byte[] range) { + for (int d=0; d 0 || compareMaxMax(range, d) < 0) { + return false; + } + } + return true; + } + + /** compare the encoded min value (for the defined query dimension) with the encoded min value in the byte array */ + private int compareMinMin(byte[] b, int dimension) { + // convert dimension to offset: + dimension *= bytesPerDim; + return StringHelper.compare(bytesPerDim, ranges, dimension, b, dimension); + } + + /** compare the encoded min value (for the defined query dimension) with the encoded max value in the byte array */ + private int compareMinMax(byte[] b, int dimension) { + // convert dimension to offset: + dimension *= bytesPerDim; + return StringHelper.compare(bytesPerDim, ranges, dimension, b, numDims * bytesPerDim + dimension); + } + + /** compare the encoded max value (for the defined query dimension) with the encoded min value in the byte array */ + private int compareMaxMin(byte[] b, int dimension) { + // convert dimension to offset: + dimension *= bytesPerDim; + return StringHelper.compare(bytesPerDim, ranges, numDims * bytesPerDim + dimension, b, dimension); + } + + /** compare the encoded max value (for the defined query dimension) with the encoded max value in the byte array */ + private int compareMaxMax(byte[] b, int dimension) { + // convert dimension to max offset: + dimension = numDims * bytesPerDim + dimension * bytesPerDim; + return StringHelper.compare(bytesPerDim, ranges, dimension, b, dimension); + } + } + + @Override + public int hashCode() { + int hash = classHash(); + hash = 31 * hash + field.hashCode(); + hash = 31 * hash + numDims; + hash = 31 * hash + queryType.hashCode(); + hash = 31 * hash + Arrays.hashCode(ranges); + + return hash; + } + + @Override + public final boolean equals(Object o) { + return sameClassAs(o) && + equalsTo(getClass().cast(o)); + } + + protected boolean equalsTo(RangeFieldQuery other) { + return Objects.equals(field, other.field) && + numDims == other.numDims && + Arrays.equals(ranges, other.ranges) && + other.queryType == queryType; + } + + @Override + public String toString(String field) { + StringBuilder sb = new StringBuilder(); + if (this.field.equals(field) == false) { + sb.append(this.field); + sb.append(':'); + } + sb.append("'); + + return sb.toString(); + } + + /** + * Returns a string of a single value in a human-readable format for debugging. + * This is used by {@link #toString()}. + * + * @param dimension dimension of the particular value + * @param ranges encoded ranges, never null + * @return human readable value for debugging + */ + protected abstract String toString(byte[] ranges, int dimension); +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestDoubleRangeField.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestDoubleRangeField.java new file mode 100644 index 00000000000..188aab65caf --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestDoubleRangeField.java @@ -0,0 +1,67 @@ +/* + * 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.util.LuceneTestCase; + +/** + * Random testing for RangeField type. + **/ +public class TestDoubleRangeField extends LuceneTestCase { + private static final String FIELD_NAME = "rangeField"; + + /** test illegal NaN range values */ + public void testIllegalNaNValues() { + Document doc = new Document(); + IllegalArgumentException expected; + + expected = expectThrows(IllegalArgumentException.class, () -> + doc.add(new DoubleRangeField(FIELD_NAME, new double[] {Double.NaN}, new double[] {5}))); + assertTrue(expected.getMessage().contains("invalid min value")); + + expected = expectThrows(IllegalArgumentException.class, () -> + doc.add(new DoubleRangeField(FIELD_NAME, new double[] {5}, new double[] {Double.NaN}))); + assertTrue(expected.getMessage().contains("invalid max value")); + } + + /** min/max array sizes must agree */ + public void testUnevenArrays() { + Document doc = new Document(); + IllegalArgumentException expected; + expected = expectThrows(IllegalArgumentException.class, () -> + doc.add(new DoubleRangeField(FIELD_NAME, new double[] {5, 6}, new double[] {5}))); + assertTrue(expected.getMessage().contains("min/max ranges must agree")); + } + + /** dimensions greater than 4 not supported */ + public void testOversizeDimensions() { + Document doc = new Document(); + IllegalArgumentException expected; + expected = expectThrows(IllegalArgumentException.class, () -> + doc.add(new DoubleRangeField(FIELD_NAME, new double[] {1, 2, 3, 4, 5}, new double[] {5}))); + assertTrue(expected.getMessage().contains("does not support greater than 4 dimensions")); + } + + /** min cannot be greater than max */ + public void testMinGreaterThanMax() { + Document doc = new Document(); + IllegalArgumentException expected; + expected = expectThrows(IllegalArgumentException.class, () -> + doc.add(new DoubleRangeField(FIELD_NAME, new double[] {3, 4}, new double[] {1, 2}))); + assertTrue(expected.getMessage().contains("is greater than max value")); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/search/BaseRangeFieldQueryTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/search/BaseRangeFieldQueryTestCase.java new file mode 100644 index 00000000000..dadb5886340 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/search/BaseRangeFieldQueryTestCase.java @@ -0,0 +1,403 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.lucene.search; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.NumericDocValuesField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.MultiDocValues; +import org.apache.lucene.index.MultiFields; +import org.apache.lucene.index.NumericDocValues; +import org.apache.lucene.index.SerialMergeScheduler; +import org.apache.lucene.index.Term; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.FixedBitSet; +import org.apache.lucene.util.IOUtils; +import org.apache.lucene.util.LuceneTestCase; + +/** + * Abstract class to do basic tests for a RangeField query. + */ +public abstract class BaseRangeFieldQueryTestCase extends LuceneTestCase { + protected abstract Field newRangeField(double[] min, double[] max); + + protected abstract Query newIntersectsQuery(double[] min, double[] max); + + protected abstract Query newContainsQuery(double[] min, double[] max); + + protected abstract Query newWithinQuery(double[] min, double[] max); + + protected int dimension() { + return random().nextInt(4) + 1; + } + + public void testRandomTiny() throws Exception { + // Make sure single-leaf-node case is OK: + doTestRandom(10, false); + } + + public void testRandomMedium() throws Exception { + doTestRandom(10000, false); + } + + @Nightly + public void testRandomBig() throws Exception { + doTestRandom(200000, false); + } + + public void testMultiValued() throws Exception { + doTestRandom(10000, true); + } + + private void doTestRandom(int count, boolean multiValued) throws Exception { + int numDocs = atLeast(count); + int dimensions = dimension(); + + if (VERBOSE) { + System.out.println("TEST: numDocs=" + numDocs); + } + + Box[][] boxes = new Box[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: + while (true) { + oldID = random().nextInt(id); + if (Double.isNaN(boxes[oldID][0].min[0]) == false) { + break; + } else if (++i > id) { + continue nextdoc; + } + } + + 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) { + dir = newFSDirectory(createTempDir(getClass().getSimpleName())); + } else { + dir = newDirectory(); + } + + Set deleted = new HashSet<>(); + IndexWriter w = new IndexWriter(dir, iwc); + for (int id=0; id < boxes.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 0 && random().nextInt(100) == 1) { + int idToDelete = random().nextInt(id); + w.deleteDocuments(new Term("id", ""+idToDelete)); + deleted.add(idToDelete); + if (VERBOSE) { + System.out.println(" delete id=" + idToDelete); + } + } + } + + if (random().nextBoolean()) { + w.forceMerge(1); + } + final IndexReader r = DirectoryReader.open(w); + w.close(); + IndexSearcher s = newSearcher(r); + + int dimensions = boxes[0][0].min.length; + int iters = atLeast(25); + NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id"); + Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader()); + int maxDoc = s.getIndexReader().maxDoc(); + + for (int iter=0; iter 1) ? "es=" : "=" ) + boxes[id][0]); + for (int n=1; n 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 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) { + 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) { + 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