diff --git a/lucene/core/src/java/org/apache/lucene/document/DoublePoint.java b/lucene/core/src/java/org/apache/lucene/document/DoublePoint.java index e442bec5251..29a4211a9f0 100644 --- a/lucene/core/src/java/org/apache/lucene/document/DoublePoint.java +++ b/lucene/core/src/java/org/apache/lucene/document/DoublePoint.java @@ -44,7 +44,6 @@ import org.apache.lucene.util.NumericUtils; * @see PointValues */ public final class DoublePoint extends Field { - /** * Return the least double that compares greater than {@code d} consistently * with {@link Double#compare}. The only difference with @@ -106,7 +105,13 @@ public final class DoublePoint extends Field { return decodeDimension(bytes.bytes, bytes.offset); } - private static BytesRef pack(double... point) { + /** + * Pack a double point into a BytesRef + * + * @param point double[] value + * @throws IllegalArgumentException is the value is null or of zero length + */ + public static BytesRef pack(double... point) { if (point == null) { throw new IllegalArgumentException("point must not be null"); } diff --git a/lucene/core/src/java/org/apache/lucene/document/FloatPoint.java b/lucene/core/src/java/org/apache/lucene/document/FloatPoint.java index b6d508ffe61..fea8db574f1 100644 --- a/lucene/core/src/java/org/apache/lucene/document/FloatPoint.java +++ b/lucene/core/src/java/org/apache/lucene/document/FloatPoint.java @@ -44,7 +44,6 @@ import org.apache.lucene.util.NumericUtils; * @see PointValues */ public final class FloatPoint extends Field { - /** * Return the least float that compares greater than {@code f} consistently * with {@link Float#compare}. The only difference with @@ -106,7 +105,13 @@ public final class FloatPoint extends Field { return decodeDimension(bytes.bytes, bytes.offset); } - private static BytesRef pack(float... point) { + /** + * Pack a float point into a BytesRef + * + * @param point float[] value + * @throws IllegalArgumentException is the value is null or of zero length + */ + public static BytesRef pack(float... point) { if (point == null) { throw new IllegalArgumentException("point must not be null"); } diff --git a/lucene/core/src/java/org/apache/lucene/document/IntPoint.java b/lucene/core/src/java/org/apache/lucene/document/IntPoint.java index da4f391cf33..671a900fa66 100644 --- a/lucene/core/src/java/org/apache/lucene/document/IntPoint.java +++ b/lucene/core/src/java/org/apache/lucene/document/IntPoint.java @@ -44,7 +44,6 @@ import org.apache.lucene.util.NumericUtils; * @see PointValues */ public final class IntPoint extends Field { - private static FieldType getType(int numDims) { FieldType type = new FieldType(); type.setDimensions(numDims, Integer.BYTES); @@ -80,7 +79,13 @@ public final class IntPoint extends Field { return decodeDimension(bytes.bytes, bytes.offset); } - private static BytesRef pack(int... point) { + /** + * Pack an integer point into a BytesRef + * + * @param point int[] value + * @throws IllegalArgumentException is the value is null or of zero length + */ + public static BytesRef pack(int... point) { if (point == null) { throw new IllegalArgumentException("point must not be null"); } diff --git a/lucene/core/src/java/org/apache/lucene/document/LongPoint.java b/lucene/core/src/java/org/apache/lucene/document/LongPoint.java index 835a37ad37d..28a6a59a3fe 100644 --- a/lucene/core/src/java/org/apache/lucene/document/LongPoint.java +++ b/lucene/core/src/java/org/apache/lucene/document/LongPoint.java @@ -47,7 +47,6 @@ import org.apache.lucene.util.NumericUtils; * @see PointValues */ public final class LongPoint extends Field { - private static FieldType getType(int numDims) { FieldType type = new FieldType(); type.setDimensions(numDims, Long.BYTES); @@ -83,7 +82,13 @@ public final class LongPoint extends Field { return decodeDimension(bytes.bytes, bytes.offset); } - private static BytesRef pack(long... point) { + /** + * Pack a long point into a BytesRef + * + * @param point long[] value + * @throws IllegalArgumentException is the value is null or of zero length + */ + public static BytesRef pack(long... point) { if (point == null) { throw new IllegalArgumentException("point must not be null"); } diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/DoublePointMultiRangeBuilder.java b/lucene/sandbox/src/java/org/apache/lucene/document/DoublePointMultiRangeBuilder.java new file mode 100644 index 00000000000..314005a00e8 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/DoublePointMultiRangeBuilder.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.lucene.document; + +import org.apache.lucene.search.MultiRangeQuery; + +import static org.apache.lucene.document.DoublePoint.decodeDimension; +import static org.apache.lucene.document.DoublePoint.pack; + +/** Builder for multi range queries for DoublePoints */ +public class DoublePointMultiRangeBuilder extends MultiRangeQuery.Builder { + public DoublePointMultiRangeBuilder(String field, int numDims) { + super(field, Double.BYTES, numDims); + } + + @Override + public MultiRangeQuery build() { + return new MultiRangeQuery(field, numDims, bytesPerDim, clauses) { + @Override + protected String toString(int dimension, byte[] value) { + return Double.toString(decodeDimension(value, 0)); + } + }; + } + + public void add(double[] lowerValue, double[] upperValue) { + if (upperValue.length != numDims || lowerValue.length != numDims) { + throw new IllegalArgumentException("Passed in range does not conform to specified dimensions"); + } + + for (int i = 0; i < numDims; i++) { + if (upperValue[i] < lowerValue[i]) { + throw new IllegalArgumentException("Upper value of range should be greater than lower value of range"); + } + } + add(pack(lowerValue).bytes, pack(upperValue).bytes); + } +} + diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/FloatPointMultiRangeBuilder.java b/lucene/sandbox/src/java/org/apache/lucene/document/FloatPointMultiRangeBuilder.java new file mode 100644 index 00000000000..0f590b0d407 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/FloatPointMultiRangeBuilder.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.lucene.document; + +import org.apache.lucene.search.MultiRangeQuery; + +import static org.apache.lucene.document.FloatPoint.decodeDimension; +import static org.apache.lucene.document.FloatPoint.pack; + +/** + * Builder for multi range queries for FloatPoints + */ +public class FloatPointMultiRangeBuilder extends MultiRangeQuery.Builder { + public FloatPointMultiRangeBuilder(String field, int numDims) { + super(field, Float.BYTES, numDims); + } + + @Override + public MultiRangeQuery build() { + return new MultiRangeQuery(field, numDims, bytesPerDim, clauses) { + @Override + protected String toString(int dimension, byte[] value) { + return Float.toString(decodeDimension(value, 0)); + } + }; + } + + public void add(float[] lowerValue, float[] upperValue) { + if (upperValue.length != numDims || lowerValue.length != numDims) { + throw new IllegalArgumentException("Passed in range does not conform to specified dimensions"); + } + + for (int i = 0; i < numDims; i++) { + if (upperValue[i] < lowerValue[i]) { + throw new IllegalArgumentException("Upper value of range should be greater than lower value of range"); + } + } + add(pack(lowerValue).bytes, pack(upperValue).bytes); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/IntPointMultiRangeBuilder.java b/lucene/sandbox/src/java/org/apache/lucene/document/IntPointMultiRangeBuilder.java new file mode 100644 index 00000000000..ce2ab31af37 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/IntPointMultiRangeBuilder.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.lucene.document; + +import org.apache.lucene.search.MultiRangeQuery; + +import static org.apache.lucene.document.IntPoint.decodeDimension; +import static org.apache.lucene.document.IntPoint.pack; + +/** Builder for multi range queries for IntPoints */ +public class IntPointMultiRangeBuilder extends MultiRangeQuery.Builder { + public IntPointMultiRangeBuilder(String field, int numDims) { + super(field, Integer.BYTES, numDims); + } + + @Override + public MultiRangeQuery build() { + return new MultiRangeQuery(field, numDims, bytesPerDim, clauses) { + @Override + protected String toString(int dimension, byte[] value) { + return Integer.toString(decodeDimension(value, 0)); + } + }; + } + + public void add(int[] lowerValue, int[] upperValue) { + if (upperValue.length != numDims || lowerValue.length != numDims) { + throw new IllegalArgumentException("Passed in range does not conform to specified dimensions"); + } + + for (int i = 0; i < numDims; i++) { + if (upperValue[i] < lowerValue[i]) { + throw new IllegalArgumentException("Upper value of range should be greater than lower value of range"); + } + } + add(pack(lowerValue).bytes, pack(upperValue).bytes); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LongPointMultiRangeBuilder.java b/lucene/sandbox/src/java/org/apache/lucene/document/LongPointMultiRangeBuilder.java new file mode 100644 index 00000000000..46881ddf8bc --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LongPointMultiRangeBuilder.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.lucene.document; + +import org.apache.lucene.search.MultiRangeQuery; + +import static org.apache.lucene.document.LongPoint.decodeDimension; +import static org.apache.lucene.document.LongPoint.pack; + +/** Builder for multi range queries for LongPoints */ +public class LongPointMultiRangeBuilder extends MultiRangeQuery.Builder { + public LongPointMultiRangeBuilder(String field, int numDims) { + super(field, Long.BYTES, numDims); + } + + @Override + public MultiRangeQuery build() { + return new MultiRangeQuery(field, numDims, bytesPerDim, clauses) { + @Override + protected String toString(int dimension, byte[] value) { + return Long.toString(decodeDimension(value, 0)); + } + }; + } + + public void add(long[] lowerValue, long[] upperValue) { + if (upperValue.length != numDims || lowerValue.length != numDims) { + throw new IllegalArgumentException("Passed in range does not conform to specified dimensions"); + } + + for (int i = 0; i < numDims; i++) { + if (upperValue[i] < lowerValue[i]) { + throw new IllegalArgumentException("Upper value of range should be greater than lower value of range"); + } + } + add(pack(lowerValue).bytes, pack(upperValue).bytes); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/MultiRangeQuery.java b/lucene/sandbox/src/java/org/apache/lucene/search/MultiRangeQuery.java new file mode 100644 index 00000000000..824c04b90ef --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/search/MultiRangeQuery.java @@ -0,0 +1,385 @@ +/* + * 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.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.PointValues; +import org.apache.lucene.util.ArrayUtil; +import org.apache.lucene.util.DocIdSetBuilder; + +/** + * Abstract class for range queries involving multiple ranges against physical points such as {@code IntPoints} + * All ranges are logically ORed together + * @lucene.experimental + */ +public abstract class MultiRangeQuery extends Query { + /** + * Representation of a single clause in a MultiRangeQuery + */ + public static class RangeClause { + byte[] lowerValue; + byte[] upperValue; + + public RangeClause(byte[] lowerValue, byte[] upperValue) { + this.lowerValue = lowerValue; + this.upperValue = upperValue; + } + } + + /** A builder for multirange queries. */ + public static abstract class Builder { + + protected final String field; + protected final int bytesPerDim; + protected final int numDims; + protected final List clauses = new ArrayList<>(); + + /** Sole constructor. */ + public Builder(String field, int bytesPerDim, int numDims) { + if (field == null) { + throw new IllegalArgumentException("field should not be null"); + } + if (bytesPerDim <= 0) { + throw new IllegalArgumentException("bytesPerDim should be a valid value"); + } + if (numDims <= 0) { + throw new IllegalArgumentException("numDims should be a valid value"); + } + + this.field = field; + this.bytesPerDim = bytesPerDim; + this.numDims = numDims; + } + + /** + * Add a new clause to this {@link Builder}. + */ + public Builder add(RangeClause clause) { + clauses.add(clause); + return this; + } + + /** + * Add a new clause to this {@link Builder}. + */ + public Builder add(byte[] lowerValue, byte[] upperValue) { + checkArgs(lowerValue, upperValue); + return add(new RangeClause(lowerValue, upperValue)); + } + + /** Create a new {@link MultiRangeQuery} based on the parameters that have + * been set on this builder. */ + public abstract MultiRangeQuery build(); + + /** + * Check preconditions for all factory methods + * @throws IllegalArgumentException if {@code field}, {@code lowerPoint} or {@code upperPoint} are null. + */ + private void checkArgs(Object lowerPoint, Object upperPoint) { + if (lowerPoint == null) { + throw new IllegalArgumentException("lowerPoint must not be null"); + } + if (upperPoint == null) { + throw new IllegalArgumentException("upperPoint must not be null"); + } + } + } + + final String field; + final int numDims; + final int bytesPerDim; + final List rangeClauses; + /** + * Expert: create a multidimensional range query with multiple connected ranges + * + * @param field field name. must not be {@code null}. + * @param rangeClauses Range Clauses for this query + * @param numDims number of dimensions. + */ + protected MultiRangeQuery(String field, int numDims, int bytesPerDim, List rangeClauses) { + this.field = field; + this.numDims = numDims; + this.bytesPerDim = bytesPerDim; + this.rangeClauses = rangeClauses; + } + + @Override + public void visit(QueryVisitor visitor) { + if (visitor.acceptField(field)) { + visitor.visitLeaf(this); + } + } + + @Override + public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException { + + // We don't use RandomAccessWeight here: it's no good to approximate with "match all docs". + // This is an inverted structure and should be used in the first pass: + + return new ConstantScoreWeight(this, boost) { + + private PointValues.IntersectVisitor getIntersectVisitor(DocIdSetBuilder result) { + return new PointValues.IntersectVisitor() { + + DocIdSetBuilder.BulkAdder adder; + + @Override + public void grow(int count) { + adder = result.grow(count); + } + + @Override + public void visit(int docID) { + adder.add(docID); + } + + @Override + public void visit(int docID, byte[] packedValue) { + // If a single OR clause has the value in range, the entire query accepts the value + for (RangeClause rangeClause : rangeClauses) { + for (int dim = 0; dim < numDims; dim++) { + int offset = dim * bytesPerDim; + if ((Arrays.compareUnsigned(packedValue, offset, offset + bytesPerDim, rangeClause.lowerValue, offset, offset + bytesPerDim) >= 0) && + (Arrays.compareUnsigned(packedValue, offset, offset + bytesPerDim, rangeClause.upperValue, offset, offset + bytesPerDim) <= 0)) { + // Doc is in-bounds. Add and short circuit + adder.add(docID); + return; + } + // Iterate till we have any OR clauses remaining + } + } + } + + @Override + public PointValues.Relation compare(byte[] minPackedValue, byte[] maxPackedValue) { + + boolean crosses = false; + + /** + * CROSSES and INSIDE take priority over OUTSIDE. How we calculate the position is: + * 1) If any range sees the point as inside, return INSIDE. + * 2) If no range sees the point as inside and atleast one range sees the point as CROSSES, return CROSSES + * 3) If none of the above, return OUTSIDE + */ + for (RangeClause rangeClause : rangeClauses) { + for (int dim = 0; dim < numDims; dim++) { + int offset = dim * bytesPerDim; + + if ((Arrays.compareUnsigned(minPackedValue, offset, offset + bytesPerDim, rangeClause.lowerValue, offset, offset + bytesPerDim) >= 0) && + (Arrays.compareUnsigned(maxPackedValue, offset, offset + bytesPerDim, rangeClause.upperValue, offset, offset + bytesPerDim) <= 0)) { + return PointValues.Relation.CELL_INSIDE_QUERY; + } + + crosses |= Arrays.compareUnsigned(minPackedValue, offset, offset + bytesPerDim, rangeClause.lowerValue, offset, offset + bytesPerDim) < 0 || + Arrays.compareUnsigned(maxPackedValue, offset, offset + bytesPerDim, rangeClause.upperValue, offset, offset + bytesPerDim) > 0; + } + } + + if (crosses) { + return PointValues.Relation.CELL_CROSSES_QUERY; + } else { + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + } + }; + } + + @Override + public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException { + LeafReader reader = context.reader(); + + PointValues values = reader.getPointValues(field); + if (values == null) { + // No docs in this segment/field indexed any points + return null; + } + + if (values.getNumIndexDimensions() != numDims) { + throw new IllegalArgumentException("field=\"" + field + "\" was indexed with numIndexDimensions=" + values.getNumIndexDimensions() + " but this query has numDims=" + numDims); + } + if (bytesPerDim != values.getBytesPerDimension()) { + throw new IllegalArgumentException("field=\"" + field + "\" was indexed with bytesPerDim=" + values.getBytesPerDimension() + " but this query has bytesPerDim=" + bytesPerDim); + } + + boolean allDocsMatch; + if (values.getDocCount() == reader.maxDoc()) { + final byte[] fieldPackedLower = values.getMinPackedValue(); + final byte[] fieldPackedUpper = values.getMaxPackedValue(); + allDocsMatch = true; + for (RangeClause rangeClause : rangeClauses) { + for (int i = 0; i < numDims; ++i) { + int offset = i * bytesPerDim; + if (Arrays.compareUnsigned(rangeClause.lowerValue, offset, offset + bytesPerDim, fieldPackedLower, offset, offset + bytesPerDim) > 0 + || Arrays.compareUnsigned(rangeClause.upperValue, offset, offset + bytesPerDim, fieldPackedUpper, offset, offset + bytesPerDim) < 0) { + allDocsMatch = false; + break; + } + } + } + } else { + allDocsMatch = false; + } + + final Weight weight = this; + if (allDocsMatch) { + // all docs have a value and all points are within bounds, so everything matches + return new ScorerSupplier() { + @Override + public Scorer get(long leadCost) { + return new ConstantScoreScorer(weight, score(), scoreMode, DocIdSetIterator.all(reader.maxDoc())); + } + + @Override + public long cost() { + return reader.maxDoc(); + } + }; + } else { + return new ScorerSupplier() { + + final DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field); + final PointValues.IntersectVisitor visitor = getIntersectVisitor(result); + long cost = -1; + + @Override + public Scorer get(long leadCost) throws IOException { + values.intersect(visitor); + DocIdSetIterator iterator = result.build().iterator(); + return new ConstantScoreScorer(weight, score(), scoreMode, iterator); + } + + @Override + public long cost() { + if (cost == -1) { + // Computing the cost may be expensive, so only do it if necessary + cost = values.estimatePointCount(visitor) * rangeClauses.size(); + assert cost >= 0; + } + return cost; + } + }; + } + } + + @Override + public Scorer scorer(LeafReaderContext context) throws IOException { + ScorerSupplier scorerSupplier = scorerSupplier(context); + if (scorerSupplier == null) { + return null; + } + return scorerSupplier.get(Long.MAX_VALUE); + } + + @Override + public boolean isCacheable(LeafReaderContext ctx) { + return true; + } + + }; + } + + public String getField() { + return field; + } + + public int getNumDims() { + return numDims; + } + + public int getBytesPerDim() { + return bytesPerDim; + } + + @Override + public final int hashCode() { + int hash = classHash(); + hash = 31 * hash + field.hashCode(); + for (RangeClause rangeClause : rangeClauses) { + hash = 31 * hash + Arrays.hashCode(rangeClause.lowerValue); + hash = 31 * hash + Arrays.hashCode(rangeClause.lowerValue); + } + hash = 31 * hash + numDims; + hash = 31 * hash + Objects.hashCode(bytesPerDim); + return hash; + } + + @Override + public final boolean equals(Object o) { + return sameClassAs(o) && + equalsTo(getClass().cast(o)); + } + + private boolean equalsTo(MultiRangeQuery other) { + return Objects.equals(field, other.field) && + numDims == other.numDims && + bytesPerDim == other.bytesPerDim && + rangeClauses.equals(other.rangeClauses); + } + + @Override + public final String toString(String field) { + final StringBuilder sb = new StringBuilder(); + if (this.field.equals(field) == false) { + sb.append(this.field); + sb.append(':'); + } + + int count = 0; + // print ourselves as "range per dimension per value" + for (RangeClause rangeClause : rangeClauses) { + if (count > 0) { + sb.append(','); + } + sb.append('{'); + for (int i = 0; i < numDims; i++) { + if (i > 0) { + sb.append(','); + } + + int startOffset = bytesPerDim * i; + + sb.append('['); + sb.append(toString(i, ArrayUtil.copyOfSubArray(rangeClause.lowerValue, startOffset, startOffset + bytesPerDim))); + sb.append(" TO "); + sb.append(toString(i, ArrayUtil.copyOfSubArray(rangeClause.upperValue, startOffset, startOffset + bytesPerDim))); + sb.append(']'); + } + sb.append('}'); + ++count; + } + + 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 value single value, never null + * @return human readable value for debugging + */ + protected abstract String toString(int dimension, byte[] value); +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/search/TestMultiRangeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/search/TestMultiRangeQueries.java new file mode 100644 index 00000000000..d865a2e084c --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/search/TestMultiRangeQueries.java @@ -0,0 +1,590 @@ +/* + * 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 org.apache.lucene.document.Document; +import org.apache.lucene.document.DoublePoint; +import org.apache.lucene.document.DoublePointMultiRangeBuilder; +import org.apache.lucene.document.FloatPoint; +import org.apache.lucene.document.FloatPointMultiRangeBuilder; +import org.apache.lucene.document.IntPoint; +import org.apache.lucene.document.IntPointMultiRangeBuilder; +import org.apache.lucene.document.LongPoint; +import org.apache.lucene.document.LongPointMultiRangeBuilder; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.LuceneTestCase; +import org.apache.lucene.util.TestUtil; + +public class TestMultiRangeQueries extends LuceneTestCase { + + public void testDoubleRandomMultiRangeQuery() throws IOException { + final int numDims = TestUtil.nextInt(random(), 1, 3); + final int numVals = TestUtil.nextInt(random(), 3, 8); + Directory dir = newDirectory(); + RandomIndexWriter w = new RandomIndexWriter(random(), dir); + Document doc = new Document(); + double[] value = new double[numDims]; + for (int i = 0; i < numDims; ++i) { + value[i] = TestUtil.nextInt(random(), 1, 10); + } + doc.add(new DoublePoint("point", value)); + w.addDocument(doc); + IndexReader reader = w.getReader(); + IndexSearcher searcher = new IndexSearcher(reader); + searcher.setQueryCache(null); + DoublePointMultiRangeBuilder builder = new DoublePointMultiRangeBuilder("point", numDims); + for (int j = 0;j < numVals; j++) { + double[] lowerBound = new double[numDims]; + double[] upperBound = new double[numDims]; + for (int i = 0; i < numDims; ++i) { + lowerBound[i] = value[i] - random().nextInt(1); + upperBound[i] = value[i] + random().nextInt(1); + } + builder.add(lowerBound, upperBound); + } + + Query query = builder.build(); + searcher.search(query, Integer.MAX_VALUE); + + reader.close(); + w.close(); + dir.close(); + } + + public void testDoublePointMultiRangeQuery() throws IOException { + Directory dir = newDirectory(); + RandomIndexWriter iw = new RandomIndexWriter(random(), dir); + double[] firstPoint = {112.4, 296.2, 512.7}; + double[] secondPoint = {219.3, 514.7, 624.2}; + + Document doc = new Document(); + doc.add(new DoublePoint("point", firstPoint)); + iw.addDocument(doc); + iw.commit(); + + doc = new Document(); + doc.add(new DoublePoint("point", secondPoint)); + iw.addDocument(doc); + iw.commit(); + + // One range matches + double[] firstLowerRange= {111.3, 294.2, 502.8}; + double[] firstUpperRange = {117.3, 301.4, 514.5}; + + double[] secondLowerRange = {15.3, 4.5, 415.7}; + double[] secondUpperRange = {200.2, 402.4, 583.6}; + + DoublePointMultiRangeBuilder builder = new DoublePointMultiRangeBuilder("point", 3); + + builder.add(firstLowerRange, firstUpperRange); + builder.add(secondLowerRange, secondUpperRange); + + Query query = builder.build(); + + IndexReader reader = iw.getReader(); + final IndexSearcher searcher = newSearcher(reader); + iw.close(); + + assertEquals(searcher.count(query), 1); + + // Both ranges match + double[] firstMatchingLowerRange= {111.3, 294.2, 502.4}; + double[] firstMatchingUpperRange = {117.6, 301.8, 514.2}; + + double[] secondMatchingLowerRange = {212.4, 512.3, 415.7}; + double[] secondMatchingUpperRange = {228.3, 538.7, 647.1}; + + DoublePointMultiRangeBuilder builder2 = new DoublePointMultiRangeBuilder("point", 3); + + builder2.add(firstMatchingLowerRange, firstMatchingUpperRange); + builder2.add(secondMatchingLowerRange, secondMatchingUpperRange); + + query = builder2.build(); + + assertEquals(searcher.count(query), 2); + + // None match + double[] nonMatchingFirstRangeLower = {1.3, 3.5, 2.7}; + double[] nonMatchingFirstRangeUpper = {5.2, 8.3, 7.8}; + + double[] nonMatchingSecondRangeLower = {11246.3, 19388.7, 21248.4}; + double[] nonMatchingSecondRangeUpper = {13242.9, 20214.2, 23236.5}; + DoublePointMultiRangeBuilder builder3 = new DoublePointMultiRangeBuilder("point", 3); + + builder3.add(nonMatchingFirstRangeLower, nonMatchingFirstRangeUpper); + builder3.add(nonMatchingSecondRangeLower, nonMatchingSecondRangeUpper); + + query = builder3.build(); + + assertEquals(searcher.count(query), 0); + + // Lower point is equal to a point + double[] firstEqualLowerRange= {112.4, 296.2, 512.7}; + double[] firstEqualUpperRange = {117.6, 301.8, 514.2}; + + double[] secondEqualLowerRange = {219.3, 514.7, 624.2}; + double[] secondEqualUpperRange = {228.3, 538.7, 647.1}; + + DoublePointMultiRangeBuilder builder4 = new DoublePointMultiRangeBuilder("point", 3); + + builder4.add(firstEqualLowerRange, firstEqualUpperRange); + builder4.add(secondEqualLowerRange, secondEqualUpperRange); + + query = builder4.build(); + + assertEquals(searcher.count(query), 2); + + reader.close(); + dir.close(); + } + + public void testLongRandomMultiRangeQuery() throws IOException { + final int numDims = TestUtil.nextInt(random(), 1, 3); + final int numVals = TestUtil.nextInt(random(), 3, 8); + Directory dir = newDirectory(); + RandomIndexWriter w = new RandomIndexWriter(random(), dir); + Document doc = new Document(); + long[] value = new long[numDims]; + for (int i = 0; i < numDims; ++i) { + value[i] = TestUtil.nextLong(random(), 1, 10); + } + doc.add(new LongPoint("point", value)); + w.addDocument(doc); + IndexReader reader = w.getReader(); + IndexSearcher searcher = new IndexSearcher(reader); + searcher.setQueryCache(null); + LongPointMultiRangeBuilder builder = new LongPointMultiRangeBuilder("point", numDims); + for (int j = 0;j < numVals; j++) { + long[] lowerBound = new long[numDims]; + long[] upperBound = new long[numDims]; + for (int i = 0; i < numDims; ++i) { + lowerBound[i] = value[i] - random().nextInt(1); + upperBound[i] = value[i] + random().nextInt(1); + } + builder.add(lowerBound, upperBound); + } + + Query query = builder.build(); + searcher.search(query, Integer.MAX_VALUE); + + reader.close(); + w.close(); + dir.close(); + } + + public void testLongPointMultiRangeQuery() throws IOException { + Directory dir = newDirectory(); + RandomIndexWriter iw = new RandomIndexWriter(random(), dir); + long[] firstPoint = {112, 296, 512}; + long[] secondPoint = {219, 514, 624}; + + Document doc = new Document(); + doc.add(new LongPoint("point", firstPoint)); + iw.addDocument(doc); + iw.commit(); + + doc = new Document(); + doc.add(new LongPoint("point", secondPoint)); + iw.addDocument(doc); + iw.commit(); + + // One range matches + long[] firstLowerRange= {111, 294, 502}; + long[] firstUpperRange = {117, 301, 514}; + + long[] secondLowerRange = {15, 4, 415}; + long[] secondUpperRange = {200, 402, 583}; + + LongPointMultiRangeBuilder builder = new LongPointMultiRangeBuilder("point", 3); + + builder.add(firstLowerRange, firstUpperRange); + builder.add(secondLowerRange, secondUpperRange); + + Query query = builder.build(); + + IndexReader reader = iw.getReader(); + final IndexSearcher searcher = newSearcher(reader); + iw.close(); + + assertEquals(searcher.count(query), 1); + + // Both ranges match + long[] firstMatchingLowerRange= {111, 294, 502}; + long[] firstMatchingUpperRange = {117, 301, 514}; + + long[] secondMatchingLowerRange = {212, 512, 415}; + long[] secondMatchingUpperRange = {228, 538, 647}; + + + LongPointMultiRangeBuilder builder2 = new LongPointMultiRangeBuilder("point", 3); + + builder2.add(firstMatchingLowerRange, firstMatchingUpperRange); + builder2.add(secondMatchingLowerRange, secondMatchingUpperRange); + + query = builder2.build(); + + assertEquals(searcher.count(query), 2); + + // None match + long[] nonMatchingFirstRangeLower = {1, 3, 2}; + long[] nonMatchingFirstRangeUpper = {5, 8, 7}; + + long[] nonMatchingSecondRangeLower = {11246, 19388, 21248}; + long[] nonMatchingSecondRangeUpper = {13242, 20214, 23236}; + LongPointMultiRangeBuilder builder3 = new LongPointMultiRangeBuilder("point", 3); + + builder3.add(nonMatchingFirstRangeLower, nonMatchingFirstRangeUpper); + builder3.add(nonMatchingSecondRangeLower, nonMatchingSecondRangeUpper); + + query = builder3.build(); + + assertEquals(searcher.count(query), 0); + + // Lower point is equal to a point + long[] firstEqualsLowerPoint= {112, 296, 512}; + long[] firstEqualsUpperPoint = {219, 514, 624}; + + long[] secondEqualsLowerPoint = {11246, 19388, 21248}; + long[] secondEqualsUpperPoint = {13242, 20214, 23236}; + + LongPointMultiRangeBuilder builder4 = new LongPointMultiRangeBuilder("point", 3); + + builder4.add(firstEqualsLowerPoint, firstEqualsUpperPoint); + builder4.add(secondEqualsLowerPoint, secondEqualsUpperPoint); + + query = builder4.build(); + + assertEquals(searcher.count(query), 2); + + reader.close(); + dir.close(); + } + + public void testFloatRandomMultiRangeQuery() throws IOException { + final int numDims = TestUtil.nextInt(random(), 1, 3); + final int numVals = TestUtil.nextInt(random(), 3, 8); + Directory dir = newDirectory(); + RandomIndexWriter w = new RandomIndexWriter(random(), dir); + Document doc = new Document(); + float[] value = new float[numDims]; + for (int i = 0; i < numDims; ++i) { + value[i] = TestUtil.nextInt(random(), 1, 10); + } + doc.add(new FloatPoint("point", value)); + w.addDocument(doc); + IndexReader reader = w.getReader(); + IndexSearcher searcher = new IndexSearcher(reader); + searcher.setQueryCache(null); + FloatPointMultiRangeBuilder builder = new FloatPointMultiRangeBuilder("point", numDims); + for (int j = 0;j < numVals; j++) { + float[] lowerBound = new float[numDims]; + float[] upperBound = new float[numDims]; + for (int i = 0; i < numDims; ++i) { + lowerBound[i] = value[i] - random().nextInt(1); + upperBound[i] = value[i] + random().nextInt(1); + } + builder.add(lowerBound, upperBound); + } + + Query query = builder.build(); + searcher.search(query, Integer.MAX_VALUE); + + reader.close(); + w.close(); + dir.close(); + } + + public void testFloatPointMultiRangeQuery() throws IOException { + Directory dir = newDirectory(); + RandomIndexWriter iw = new RandomIndexWriter(random(), dir); + float[] firstPoint = {112.4f, 296.3f, 512.1f}; + float[] secondPoint = {219.7f, 514.2f, 624.6f}; + + Document doc = new Document(); + doc.add(new FloatPoint("point", firstPoint)); + iw.addDocument(doc); + iw.commit(); + + doc = new Document(); + doc.add(new FloatPoint("point", secondPoint)); + iw.addDocument(doc); + iw.commit(); + + // One range matches + float[] firstLowerRange= {111.3f, 294.7f, 502.1f}; + float[] firstUpperRange = {117.2f, 301.6f, 514.3f}; + + float[] secondLowerRange = {15.2f, 4.3f, 415.2f}; + float[] secondUpperRange = {200.6f, 402.3f, 583.8f}; + + FloatPointMultiRangeBuilder builder = new FloatPointMultiRangeBuilder("point", 3); + + builder.add(firstLowerRange, firstUpperRange); + builder.add(secondLowerRange, secondUpperRange); + + Query query = builder.build(); + + IndexReader reader = iw.getReader(); + final IndexSearcher searcher = newSearcher(reader); + iw.close(); + + assertEquals(searcher.count(query), 1); + + // Both ranges match + float[] firstMatchingLowerRange= {111f, 294f, 502f}; + float[] firstMatchingUpperRange = {117f, 301f, 514f}; + + float[] secondMatchingLowerRange = {212f, 512f, 415f}; + float[] secondMatchingUpperRange = {228f, 538f, 647f}; + + FloatPointMultiRangeBuilder builder2 = new FloatPointMultiRangeBuilder("point", 3); + + builder2.add(firstMatchingLowerRange, firstMatchingUpperRange); + builder2.add(secondMatchingLowerRange, secondMatchingUpperRange); + + query = builder2.build(); + + assertEquals(searcher.count(query), 2); + + // None Match + float[] nonMatchingFirstRangeLower = {1.4f, 3.3f, 2.7f}; + float[] nonMatchingFirstRangeUpper = {5.4f, 8.2f, 7.3f}; + + float[] nonMatchingSecondRangeLower = {11246.2f, 19388.6f, 21248.3f}; + float[] nonMatchingSecondRangeUpper = {13242.4f, 20214.7f, 23236.3f}; + FloatPointMultiRangeBuilder builder3 = new FloatPointMultiRangeBuilder("point", 3); + + builder3.add(nonMatchingFirstRangeLower, nonMatchingFirstRangeUpper); + builder3.add(nonMatchingSecondRangeLower, nonMatchingSecondRangeUpper); + + query = builder3.build(); + + assertEquals(searcher.count(query), 0); + + // Lower point is equal to a point + float[] firstEqualsLowerPoint= {112.4f, 296.3f, 512.1f}; + float[] firstEqualsUpperPoint = {117.3f, 299.4f, 519.3f}; + + float[] secondEqualsLowerPoint = {219.7f, 514.2f, 624.6f}; + float[] secondEqualsUpperPoint = {13242.4f, 20214.7f, 23236.3f}; + + FloatPointMultiRangeBuilder builder4 = new FloatPointMultiRangeBuilder("point", 3); + + builder4.add(firstEqualsLowerPoint, firstEqualsUpperPoint); + builder4.add(secondEqualsLowerPoint, secondEqualsUpperPoint); + + query = builder4.build(); + + assertEquals(searcher.count(query), 2); + + reader.close(); + dir.close(); + } + + public void testIntRandomMultiRangeQuery() throws IOException { + final int numDims = TestUtil.nextInt(random(), 1, 3); + final int numVals = TestUtil.nextInt(random(), 3, 8); + Directory dir = newDirectory(); + RandomIndexWriter w = new RandomIndexWriter(random(), dir); + Document doc = new Document(); + int[] value = new int[numDims]; + for (int i = 0; i < numDims; ++i) { + value[i] = TestUtil.nextInt(random(), 1, 10); + } + doc.add(new IntPoint("point", value)); + w.addDocument(doc); + IndexReader reader = w.getReader(); + IndexSearcher searcher = new IndexSearcher(reader); + searcher.setQueryCache(null); + IntPointMultiRangeBuilder builder = new IntPointMultiRangeBuilder("point", numDims); + for (int j = 0;j < numVals; j++) { + int[] lowerBound = new int[numDims]; + int[] upperBound = new int[numDims]; + for (int i = 0; i < numDims; ++i) { + lowerBound[i] = value[i] - random().nextInt(1); + upperBound[i] = value[i] + random().nextInt(1); + } + builder.add(lowerBound, upperBound); + } + + Query query = builder.build(); + searcher.search(query, Integer.MAX_VALUE); + + reader.close(); + w.close(); + dir.close(); + } + + public void testIntPointMultiRangeQuery() throws IOException { + Directory dir = newDirectory(); + RandomIndexWriter iw = new RandomIndexWriter(random(), dir); + int[] firstPoint = {112, 296, 512}; + int[] secondPoint = {219, 514, 624}; + + Document doc = new Document(); + doc.add(new IntPoint("point", firstPoint)); + iw.addDocument(doc); + iw.commit(); + + doc = new Document(); + doc.add(new IntPoint("point", secondPoint)); + iw.addDocument(doc); + iw.commit(); + + // One range matches + int[] firstLowerRange= {111, 294, 502}; + int[] firstUpperRange = {117, 301, 514}; + + int[] secondLowerRange = {15, 4, 415}; + int[] secondUpperRange = {200, 402, 583}; + + IntPointMultiRangeBuilder builder = new IntPointMultiRangeBuilder("point", 3); + + builder.add(firstLowerRange, firstUpperRange); + builder.add(secondLowerRange, secondUpperRange); + + Query query = builder.build(); + + IndexReader reader = iw.getReader(); + final IndexSearcher searcher = newSearcher(reader); + iw.close(); + + assertEquals(searcher.count(query), 1); + + // Both ranges match + int[] firstMatchingLowerRange= {111, 294, 502}; + int[] firstMatchingUpperRange = {117, 301, 514}; + + int[] secondMatchingLowerRange = {212, 512, 415}; + int[] secondMatchingUpperRange = {228, 538, 647}; + + + IntPointMultiRangeBuilder builder2 = new IntPointMultiRangeBuilder("point", 3); + + builder2.add(firstMatchingLowerRange, firstMatchingUpperRange); + builder2.add(secondMatchingLowerRange, secondMatchingUpperRange); + + query = builder2.build(); + + assertEquals(searcher.count(query), 2); + + // None match + int[] nonMatchingFirstRangeLower = {1, 3, 2}; + int[] nonMatchingFirstRangeUpper = {5, 8, 7}; + + int[] nonMatchingSecondRangeLower = {11246, 19388, 21248}; + int[] nonMatchingSecondRangeUpper = {13242, 20214, 23236}; + IntPointMultiRangeBuilder builder3 = new IntPointMultiRangeBuilder("point", 3); + + builder3.add(nonMatchingFirstRangeLower, nonMatchingFirstRangeUpper); + builder3.add(nonMatchingSecondRangeLower, nonMatchingSecondRangeUpper); + + query = builder3.build(); + + assertEquals(searcher.count(query), 0); + + // None match + int[] firstEqualsPointLower= {112, 296, 512}; + int[] firstEqualsPointUpper = {117, 299, 517}; + + int[] secondEqualsPointLower = {219, 514, 624}; + int[] secondEqualsPointUpper = {13242, 20214, 23236}; + + IntPointMultiRangeBuilder builder4 = new IntPointMultiRangeBuilder("point", 3); + + builder4.add(firstEqualsPointLower, firstEqualsPointUpper); + builder4.add(secondEqualsPointLower, secondEqualsPointUpper); + + query = builder4.build(); + + assertEquals(searcher.count(query), 2); + + reader.close(); + dir.close(); + } + + public void testToString() { + double[] firstDoubleLowerRange= {111, 294.3, 502.4}; + double[] firstDoubleUpperRange = {117.3, 301.8, 514.3}; + + double[] secondDoubleLowerRange = {15.3, 412.8, 415.1}; + double[] secondDoubleUpperRange = {200.4, 567.4, 642.2}; + + DoublePointMultiRangeBuilder stringTestbuilder = new DoublePointMultiRangeBuilder("point", 3); + + stringTestbuilder.add(firstDoubleLowerRange, firstDoubleUpperRange); + stringTestbuilder.add(secondDoubleLowerRange, secondDoubleUpperRange); + + Query query = stringTestbuilder.build(); + + assertEquals("point:{[111.0 TO 117.3],[294.3 TO 301.8],[502.4 TO 514.3]},{[15.3 TO 200.4],[412.8 TO 567.4],[415.1 TO 642.2]}", + query.toString()); + + long[] firstLongLowerRange= {111, 294, 502}; + long[] firstLongUpperRange = {117, 301, 514}; + + long[] secondLongLowerRange = {15, 412, 415}; + long[] secondLongUpperRange = {200, 567, 642}; + + LongPointMultiRangeBuilder stringLongTestbuilder = new LongPointMultiRangeBuilder("point", 3); + + stringLongTestbuilder.add(firstLongLowerRange, firstLongUpperRange); + stringLongTestbuilder.add(secondLongLowerRange, secondLongUpperRange); + + query = stringLongTestbuilder.build(); + + assertEquals("point:{[111 TO 117],[294 TO 301],[502 TO 514]},{[15 TO 200],[412 TO 567],[415 TO 642]}", + query.toString()); + + float[] firstFloatLowerRange= {111.3f, 294.4f, 502.2f}; + float[] firstFloatUpperRange = {117.7f, 301.2f, 514.4f}; + + float[] secondFloatLowerRange = {15.3f, 412.2f, 415.9f}; + float[] secondFloatUpperRange = {200.2f, 567.4f, 642.3f}; + + FloatPointMultiRangeBuilder stringFloatTestbuilder = new FloatPointMultiRangeBuilder("point", 3); + + stringFloatTestbuilder.add(firstFloatLowerRange, firstFloatUpperRange); + stringFloatTestbuilder.add(secondFloatLowerRange, secondFloatUpperRange); + + query = stringFloatTestbuilder.build(); + + assertEquals("point:{[111.3 TO 117.7],[294.4 TO 301.2],[502.2 TO 514.4]},{[15.3 TO 200.2],[412.2 TO 567.4],[415.9 TO 642.3]}", + query.toString()); + + int[] firstIntLowerRange= {111, 294, 502}; + int[] firstIntUpperRange = {117, 301, 514}; + + int[] secondIntLowerRange = {15, 412, 415}; + int[] secondIntUpperRange = {200, 567, 642}; + + IntPointMultiRangeBuilder stringIntTestbuilder = new IntPointMultiRangeBuilder("point", 3); + + stringIntTestbuilder.add(firstIntLowerRange, firstIntUpperRange); + stringIntTestbuilder.add(secondIntLowerRange, secondIntUpperRange); + + query = stringIntTestbuilder.build(); + + assertEquals("point:{[111 TO 117],[294 TO 301],[502 TO 514]},{[15 TO 200],[412 TO 567],[415 TO 642]}", + query.toString()); + } +}