Index: lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java (revision ) +++ lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java (revision ) @@ -0,0 +1,161 @@ +package org.apache.lucene.queries.function; + +/* + * 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. + */ + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Weight; + +/** + * A Query wrapping a {@link ValueSource} that matches docs in which the values in the value source match a configured + * range. The score is the float value. This can be a slow query if run by itself since it must visit all docs; + * ideally it's combined with other queries. + * It's mostly a wrapper around + * {@link FunctionValues#getRangeScorer(IndexReader, String, String, boolean, boolean)}. + * + * @lucene.experimental + */ +public class FunctionRangeQuery extends Query { + + private final ValueSource valueSource; + + // These two are declared as strings because FunctionValues.getRangeScorer takes String args and parses them. + private final String lowerVal; + private final String upperVal; + private final boolean includeLower; + private final boolean includeUpper; + + public FunctionRangeQuery(ValueSource valueSource, Double lowerVal, Double upperVal, + boolean includeLower, boolean includeUpper) { + this(valueSource, lowerVal == null ? null : lowerVal.toString(), upperVal == null ? null : upperVal.toString(), + includeLower, includeUpper); + } + + public FunctionRangeQuery(ValueSource valueSource, Long lowerVal, Long upperVal, + boolean includeLower, boolean includeUpper) { + this(valueSource, lowerVal == null ? null : lowerVal.toString(), upperVal == null ? null : upperVal.toString(), + includeLower, includeUpper); + } + + public FunctionRangeQuery(ValueSource valueSource, String lowerVal, String upperVal, + boolean includeLower, boolean includeUpper) { + this.valueSource = valueSource; + this.lowerVal = lowerVal; + this.upperVal = upperVal; + this.includeLower = includeLower; + this.includeUpper = includeUpper; + } + + @Override + public String toString(String field) { + StringBuilder sb = new StringBuilder(); + sb.append("frange("); + sb.append(valueSource); + sb.append("):"); + sb.append(includeLower ? '[' : '{'); + sb.append(lowerVal == null ? "*" : lowerVal); + sb.append(" TO "); + sb.append(upperVal == null ? "*" : upperVal); + sb.append(includeUpper ? ']' : '}'); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FunctionRangeQuery)) return false; + if (!super.equals(o)) return false; + + FunctionRangeQuery that = (FunctionRangeQuery) o; + + if (includeLower != that.includeLower) return false; + if (includeUpper != that.includeUpper) return false; + if (!valueSource.equals(that.valueSource)) return false; + if (lowerVal != null ? !lowerVal.equals(that.lowerVal) : that.lowerVal != null) return false; + return !(upperVal != null ? !upperVal.equals(that.upperVal) : that.upperVal != null); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + valueSource.hashCode(); + result = 31 * result + (lowerVal != null ? lowerVal.hashCode() : 0); + result = 31 * result + (upperVal != null ? upperVal.hashCode() : 0); + result = 31 * result + (includeLower ? 1 : 0); + result = 31 * result + (includeUpper ? 1 : 0); + return result; + } + + @Override + public Weight createWeight(IndexSearcher searcher, boolean needsScores) throws IOException { + return new FunctionRangeWeight(searcher); + } + + private class FunctionRangeWeight extends Weight { + private final Map vsContext; + + public FunctionRangeWeight(IndexSearcher searcher) { + super(FunctionRangeQuery.this); + vsContext = ValueSource.newContext(searcher); + } + + //Note: this uses the functionValue's floatVal() as the score; queryNorm/boost is ignored. + + @Override + public void extractTerms(Set terms) { + //none + } + + @Override + public float getValueForNormalization() throws IOException { + return 1f; + } + + @Override + public void normalize(float norm, float topLevelBoost) { + //no-op + } + + @Override + public Explanation explain(LeafReaderContext context, int doc) throws IOException { + FunctionValues functionValues = valueSource.getValues(vsContext, context); + Scorer scorer = scorer(context); + if (scorer.advance(doc) == doc) { + return Explanation.match(scorer.score(), FunctionRangeQuery.this.toString(), functionValues.explain(doc)); + } else { + return Explanation.noMatch(FunctionRangeQuery.this.toString(), functionValues.explain(doc)); + } + } + + @Override + public Scorer scorer(LeafReaderContext context) throws IOException { + FunctionValues functionValues = valueSource.getValues(vsContext, context); + // getRangeScorer takes String args and parses them. Weird. + return functionValues.getRangeScorer(context.reader(), lowerVal, upperVal, includeLower, includeUpper); + } + } +} Index: lucene/queries/src/java/org/apache/lucene/queries/function/ValueSourceScorer.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- lucene/queries/src/java/org/apache/lucene/queries/function/ValueSourceScorer.java (revision 1689312) +++ lucene/queries/src/java/org/apache/lucene/queries/function/ValueSourceScorer.java (revision ) @@ -21,29 +21,41 @@ import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.MultiFields; +import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.TwoPhaseIterator; import org.apache.lucene.util.Bits; /** * {@link Scorer} which returns the result of {@link FunctionValues#floatVal(int)} as * the score for a document. + * + * @lucene.internal */ public class ValueSourceScorer extends Scorer { protected final IndexReader reader; - private int doc = -1; - protected final int maxDoc; + private final DocIdSetIterator disi; + private final TwoPhaseIterator twoPhaseIterator; protected final FunctionValues values; protected boolean checkDeletes; private final Bits liveDocs; + //TODO use LeafReaderContext not IndexReader? protected ValueSourceScorer(IndexReader reader, FunctionValues values) { - super(null); + super(null);//no weight this.reader = reader; - this.maxDoc = reader.maxDoc(); + //TODO consider caching values so we can return the score without double-comp. But note discussions in LUCENE-4574 this.values = values; setCheckDeletes(true); this.liveDocs = MultiFields.getLiveDocs(reader); + this.twoPhaseIterator = new TwoPhaseIterator(DocIdSetIterator.all(reader.maxDoc())) { + @Override + public boolean matches() throws IOException { + return ValueSourceScorer.this.matches(docID()); - } + } + }; + this.disi = TwoPhaseIterator.asDocIdSetIterator(twoPhaseIterator); + } public IndexReader getReader() { return reader; @@ -62,29 +74,32 @@ } @Override + public TwoPhaseIterator asTwoPhaseIterator() { + return twoPhaseIterator; + } + + @Override public int docID() { - return doc; + return disi.docID(); } @Override public int nextDoc() throws IOException { - for (; ;) { - doc++; - if (doc >= maxDoc) return doc = NO_MORE_DOCS; - if (matches(doc)) return doc; + return disi.nextDoc(); - } + } - } @Override public int advance(int target) throws IOException { - // also works fine when target==NO_MORE_DOCS - doc = target - 1; - return nextDoc(); + return disi.advance(target); } @Override public float score() throws IOException { - return values.floatVal(doc); + // Current Lucene priority queues can't handle NaN and -Infinity, so + // map to -Float.MAX_VALUE. This conditional handles both -infinity + // and NaN since comparisons with NaN are always false. + float score = values.floatVal(disi.docID()); + return score>Float.NEGATIVE_INFINITY ? score : -Float.MAX_VALUE; } @Override @@ -94,6 +109,6 @@ @Override public long cost() { - return maxDoc; + return disi.cost(); } }