/*
* 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.Objects;
import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.SortedNumericDocValues;
import org.apache.lucene.index.SortedSetDocValues;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.BytesRef;
/**
* A range query that works on top of the doc values APIs. Such queries are
* usually slow since they do not use an inverted index. However, in the
* dense case where most documents match this query, it <b>might</b> be as
* fast or faster than a regular {@link PointRangeQuery}.
*
* <p>
* <b>NOTE</b>: be very careful using this query: it is
* typically much slower than using {@code TermsQuery},
* but in certain specialized cases may be faster.
*
* @lucene.experimental
*/
public final class DocValuesRangeQuery extends Query {
/** Create a new numeric range query on a numeric doc-values field. The field
* must has been indexed with either {@link DocValuesType#NUMERIC} or
* {@link DocValuesType#SORTED_NUMERIC} doc values. */
public static Query newLongRange(String field, Long lowerVal, Long upperVal, boolean includeLower, boolean includeUpper) {
return new DocValuesRangeQuery(field, lowerVal, upperVal, includeLower, includeUpper);
}
/** Create a new numeric range query on a numeric doc-values field. The field
* must has been indexed with {@link DocValuesType#SORTED} or
* {@link DocValuesType#SORTED_SET} doc values. */
public static Query newBytesRefRange(String field, BytesRef lowerVal, BytesRef upperVal, boolean includeLower, boolean includeUpper) {
return new DocValuesRangeQuery(field, deepCopyOf(lowerVal), deepCopyOf(upperVal), includeLower, includeUpper);
}
private static BytesRef deepCopyOf(BytesRef b) {
if (b == null) {
return null;
} else {
return BytesRef.deepCopyOf(b);
}
}
private final String field;
private final Object lowerVal, upperVal;
private final boolean includeLower, includeUpper;
private DocValuesRangeQuery(String field, Object lowerVal, Object upperVal, boolean includeLower, boolean includeUpper) {
this.field = Objects.requireNonNull(field);
this.lowerVal = lowerVal;
this.upperVal = upperVal;
this.includeLower = includeLower;
this.includeUpper = includeUpper;
}
@Override
public boolean equals(Object other) {
return sameClassAs(other) &&
equalsTo(getClass().cast(other));
}
private boolean equalsTo(DocValuesRangeQuery other) {
return field.equals(other.field) &&
Objects.equals(lowerVal, other.lowerVal) &&
Objects.equals(upperVal, other.upperVal) &&
includeLower == other.includeLower &&
includeUpper == other.includeUpper;
}
@Override
public int hashCode() {
return 31 * classHash() + Objects.hash(field, lowerVal, upperVal, includeLower, includeUpper);
}
public String getField() {
return field;
}
public Object getLowerVal() {
return lowerVal;
}
public Object getUpperVal() {
return upperVal;
}
public boolean isIncludeLower() {
return includeLower;
}
public boolean isIncludeUpper() {
return includeUpper;
}
@Override
public String toString(String field) {
StringBuilder sb = new StringBuilder();
if (this.field.equals(field) == false) {
sb.append(this.field).append(':');
}
sb.append(includeLower ? '[' : '{');
sb.append(lowerVal == null ? "*" : lowerVal.toString());
sb.append(" TO ");
sb.append(upperVal == null ? "*" : upperVal.toString());
sb.append(includeUpper ? ']' : '}');
return sb.toString();
}
@Override
public Query rewrite(IndexReader reader) throws IOException {
if (lowerVal == null && upperVal == null) {
return new FieldValueQuery(field);
}
return super.rewrite(reader);
}
@Override
public Weight createWeight(IndexSearcher searcher, boolean needsScores, float boost) throws IOException {
if (lowerVal == null && upperVal == null) {
throw new IllegalStateException("Both min and max values must not be null, call rewrite first");
}
return new RandomAccessWeight(DocValuesRangeQuery.this, boost) {
@Override
protected Bits getMatchingDocs(LeafReaderContext context) throws IOException {
if (lowerVal instanceof Long || upperVal instanceof Long) {
final SortedNumericDocValues values = DocValues.getSortedNumeric(context.reader(), field);
final long min;
if (lowerVal == null) {
min = Long.MIN_VALUE;
} else if (includeLower) {
min = (long) lowerVal;
} else {
if ((long) lowerVal == Long.MAX_VALUE) {
return null;
}
min = 1 + (long) lowerVal;
}
final long max;
if (upperVal == null) {
max = Long.MAX_VALUE;
} else if (includeUpper) {
max = (long) upperVal;
} else {
if ((long) upperVal == Long.MIN_VALUE) {
return null;
}
max = -1 + (long) upperVal;
}
if (min > max) {
return null;
}
return new Bits() {
@Override
public boolean get(int doc) {
try {
if (doc > values.docID()) {
values.advance(doc);
}
if (doc == values.docID()) {
final int count = values.docValueCount();
for (int i = 0; i < count; ++i) {
final long value = values.nextValue();
if (value >= min && value <= max) {
return true;
}
}
}
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
return false;
}
@Override
public int length() {
return context.reader().maxDoc();
}
};
} else if (lowerVal instanceof BytesRef || upperVal instanceof BytesRef) {
final SortedSetDocValues values = DocValues.getSortedSet(context.reader(), field);
final long minOrd;
if (lowerVal == null) {
minOrd = 0;
} else {
final long ord = values.lookupTerm((BytesRef) lowerVal);
if (ord < 0) {
minOrd = -1 - ord;
} else if (includeLower) {
minOrd = ord;
} else {
minOrd = ord + 1;
}
}
final long maxOrd;
if (upperVal == null) {
maxOrd = values.getValueCount() - 1;
} else {
final long ord = values.lookupTerm((BytesRef) upperVal);
if (ord < 0) {
maxOrd = -2 - ord;
} else if (includeUpper) {
maxOrd = ord;
} else {
maxOrd = ord - 1;
}
}
if (minOrd > maxOrd) {
return null;
}
return new Bits() {
@Override
public boolean get(int doc) {
try {
if (doc > values.docID()) {
values.advance(doc);
}
if (doc == values.docID()) {
for (long ord = values.nextOrd(); ord != SortedSetDocValues.NO_MORE_ORDS; ord = values.nextOrd()) {
if (ord >= minOrd && ord <= maxOrd) {
return true;
}
}
}
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
return false;
}
@Override
public int length() {
return context.reader().maxDoc();
}
};
} else {
throw new AssertionError();
}
}
};
}
}