org.apache.lucene.facet.range.DoubleRange Maven / Gradle / Ivy
Show all versions of lucene-facet Show documentation
/*
* 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.facet.range;
import java.io.IOException;
import java.util.Objects;
import org.apache.lucene.facet.MultiDoubleValues;
import org.apache.lucene.facet.MultiDoubleValuesSource;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.ConstantScoreScorer;
import org.apache.lucene.search.ConstantScoreWeight;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.DoubleValues;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryVisitor;
import org.apache.lucene.search.ScoreMode;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.ScorerSupplier;
import org.apache.lucene.search.TwoPhaseIterator;
import org.apache.lucene.search.Weight;
import org.apache.lucene.util.NumericUtils;
/**
* Represents a range over double values.
*
* @lucene.experimental
*/
public final class DoubleRange extends Range {
/** Minimum (inclusive). */
public final double min;
/** Maximum (inclusive. */
public final double max;
/** Create a DoubleRange. */
public DoubleRange(
String label, double minIn, boolean minInclusive, double maxIn, boolean maxInclusive) {
super(label);
// TODO: if DoubleDocValuesField used
// NumericUtils.doubleToSortableLong format (instead of
// Double.doubleToRawLongBits) we could do comparisons
// in long space
if (Double.isNaN(minIn)) {
throw new IllegalArgumentException("min cannot be NaN");
}
if (!minInclusive) {
minIn = Math.nextUp(minIn);
}
if (Double.isNaN(maxIn)) {
throw new IllegalArgumentException("max cannot be NaN");
}
if (!maxInclusive) {
// Why no Math.nextDown?
maxIn = Math.nextAfter(maxIn, Double.NEGATIVE_INFINITY);
}
if (minIn > maxIn) {
failNoMatch();
}
this.min = minIn;
this.max = maxIn;
}
/** True if this range accepts the provided value. */
public boolean accept(double value) {
return value >= min && value <= max;
}
LongRange toLongRange() {
return new LongRange(
label,
NumericUtils.doubleToSortableLong(min),
true,
NumericUtils.doubleToSortableLong(max),
true);
}
@Override
public String toString() {
return "DoubleRange(" + label + ": " + min + " to " + max + ")";
}
@Override
public boolean equals(Object _that) {
if (_that instanceof DoubleRange == false) {
return false;
}
DoubleRange that = (DoubleRange) _that;
return that.label.equals(this.label)
&& Double.compare(that.min, this.min) == 0
&& Double.compare(that.max, this.max) == 0;
}
@Override
public int hashCode() {
return Objects.hash(label, min, max);
}
private static class ValueSourceQuery extends Query {
private final DoubleRange range;
private final Query fastMatchQuery;
private final DoubleValuesSource valueSource;
ValueSourceQuery(DoubleRange range, Query fastMatchQuery, DoubleValuesSource valueSource) {
this.range = range;
this.fastMatchQuery = fastMatchQuery;
this.valueSource = valueSource;
}
@Override
public boolean equals(Object other) {
return sameClassAs(other) && equalsTo(getClass().cast(other));
}
private boolean equalsTo(ValueSourceQuery other) {
return range.equals(other.range)
&& Objects.equals(fastMatchQuery, other.fastMatchQuery)
&& valueSource.equals(other.valueSource);
}
@Override
public int hashCode() {
return classHash() + 31 * Objects.hash(range, fastMatchQuery, valueSource);
}
@Override
public String toString(String field) {
return "Filter(" + range.toString() + ")";
}
@Override
public void visit(QueryVisitor visitor) {
visitor.visitLeaf(this);
}
@Override
public Query rewrite(IndexSearcher indexSearcher) throws IOException {
if (fastMatchQuery != null) {
final Query fastMatchRewritten = fastMatchQuery.rewrite(indexSearcher);
if (fastMatchRewritten != fastMatchQuery) {
return new ValueSourceQuery(range, fastMatchRewritten, valueSource);
}
}
return super.rewrite(indexSearcher);
}
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost)
throws IOException {
final Weight fastMatchWeight =
fastMatchQuery == null
? null
: searcher.createWeight(fastMatchQuery, ScoreMode.COMPLETE_NO_SCORES, 1f);
return new ConstantScoreWeight(this, boost) {
@Override
public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
final int maxDoc = context.reader().maxDoc();
final DocIdSetIterator approximation;
if (fastMatchWeight == null) {
approximation = DocIdSetIterator.all(maxDoc);
} else {
Scorer s = fastMatchWeight.scorer(context);
if (s == null) {
return null;
}
approximation = s.iterator();
}
final DoubleValues values = valueSource.getValues(context, null);
final TwoPhaseIterator twoPhase =
new TwoPhaseIterator(approximation) {
@Override
public boolean matches() throws IOException {
return values.advanceExact(approximation.docID())
&& range.accept(values.doubleValue());
}
@Override
public float matchCost() {
return 100; // TODO: use cost of range.accept()
}
};
final var scorer = new ConstantScoreScorer(score(), scoreMode, twoPhase);
return new DefaultScorerSupplier(scorer);
}
@Override
public boolean isCacheable(LeafReaderContext ctx) {
return valueSource.isCacheable(ctx);
}
};
}
}
private static class MultiValueSourceQuery extends Query {
private final DoubleRange range;
private final Query fastMatchQuery;
private final MultiDoubleValuesSource valueSource;
MultiValueSourceQuery(
DoubleRange range, Query fastMatchQuery, MultiDoubleValuesSource valueSource) {
this.range = range;
this.fastMatchQuery = fastMatchQuery;
this.valueSource = valueSource;
}
@Override
public boolean equals(Object other) {
return sameClassAs(other) && equalsTo(getClass().cast(other));
}
private boolean equalsTo(MultiValueSourceQuery other) {
return range.equals(other.range)
&& Objects.equals(fastMatchQuery, other.fastMatchQuery)
&& valueSource.equals(other.valueSource);
}
@Override
public int hashCode() {
return classHash() + 31 * Objects.hash(range, fastMatchQuery, valueSource);
}
@Override
public String toString(String field) {
return "Filter(" + range.toString() + ")";
}
@Override
public void visit(QueryVisitor visitor) {
visitor.visitLeaf(this);
}
@Override
public Query rewrite(IndexSearcher indexSearcher) throws IOException {
if (fastMatchQuery != null) {
final Query fastMatchRewritten = fastMatchQuery.rewrite(indexSearcher);
if (fastMatchRewritten != fastMatchQuery) {
return new MultiValueSourceQuery(range, fastMatchRewritten, valueSource);
}
}
return super.rewrite(indexSearcher);
}
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost)
throws IOException {
final Weight fastMatchWeight =
fastMatchQuery == null
? null
: searcher.createWeight(fastMatchQuery, ScoreMode.COMPLETE_NO_SCORES, 1f);
return new ConstantScoreWeight(this, boost) {
@Override
public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
final int maxDoc = context.reader().maxDoc();
final DocIdSetIterator approximation;
if (fastMatchWeight == null) {
approximation = DocIdSetIterator.all(maxDoc);
} else {
Scorer s = fastMatchWeight.scorer(context);
if (s == null) {
return null;
}
approximation = s.iterator();
}
final MultiDoubleValues values = valueSource.getValues(context);
final TwoPhaseIterator twoPhase =
new TwoPhaseIterator(approximation) {
@Override
public boolean matches() throws IOException {
if (values.advanceExact(approximation.docID()) == false) {
return false;
}
for (int i = 0; i < values.getValueCount(); i++) {
if (range.accept(values.nextValue())) {
return true;
}
}
return false;
}
@Override
public float matchCost() {
return 100; // TODO: use cost of range.accept()
}
};
final var scorer = new ConstantScoreScorer(score(), scoreMode, twoPhase);
return new DefaultScorerSupplier(scorer);
}
@Override
public boolean isCacheable(LeafReaderContext ctx) {
return valueSource.isCacheable(ctx);
}
};
}
}
/**
* Create a Query that matches documents in this range
*
* The query will check all documents that match the provided match query, or every document in
* the index if the match query is null.
*
*
If the value source is static, eg an indexed numeric field, it may be faster to use {@link
* org.apache.lucene.search.PointRangeQuery}
*
* @param fastMatchQuery a query to use as a filter
* @param valueSource the source of values for the range check
*/
public Query getQuery(Query fastMatchQuery, DoubleValuesSource valueSource) {
return new ValueSourceQuery(this, fastMatchQuery, valueSource);
}
/**
* Create a Query that matches documents in this range
*
*
The query will check all documents that match the provided match query, or every document in
* the index if the match query is null.
*
*
If the value source is static, eg an indexed numeric field, it may be faster to use {@link
* org.apache.lucene.search.PointRangeQuery}
*
* @param fastMatchQuery a query to use as a filter
* @param valueSource the source of values for the range check
*/
public Query getQuery(Query fastMatchQuery, MultiDoubleValuesSource valueSource) {
DoubleValuesSource singleValues = MultiDoubleValuesSource.unwrapSingleton(valueSource);
if (singleValues != null) {
return new ValueSourceQuery(this, fastMatchQuery, singleValues);
} else {
return new MultiValueSourceQuery(this, fastMatchQuery, valueSource);
}
}
}