package org.apache.lucene.search.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.Set; import org.apache.lucene.index.IndexReader; import org.apache.lucene.search.ComplexExplanation; import org.apache.lucene.search.Explanation; import org.apache.lucene.search.Query; import org.apache.lucene.search.Weight; import org.apache.lucene.search.Scorer; import org.apache.lucene.search.Searcher; import org.apache.lucene.search.Similarity; import org.apache.lucene.util.ToStringUtils; /** * Query that sets document score as a programmatic function of several (sub) scores: * <ol> * <li>the score of its subQuery (any query)</li> * <li>(optional) the score of its ValueSourceQuery (or queries). * For most simple/convenient use cases this query is likely to be a * {@link org.apache.lucene.search.function.FieldScoreQuery FieldScoreQuery}</li> * </ol> * Subclasses can modify the computation by overriding {@link #getCustomScoreProvider}. * * <p><font color="#FF0000"> * WARNING: The status of the <b>search.function</b> package is experimental. * The APIs introduced here might change in the future and will not be * supported anymore in such a case.</font> */ public class CustomScoreQuery extends Query { private Query subQuery; private ValueSourceQuery[] valSrcQueries; // never null (empty array if there are no valSrcQueries). private boolean strict = false; // if true, valueSource part of query does not take part in weights normalization. /** * Create a CustomScoreQuery over input subQuery. * @param subQuery the sub query whose scored is being customed. Must not be null. */ public CustomScoreQuery(Query subQuery) { this(subQuery, new ValueSourceQuery[0]); } /** * Create a CustomScoreQuery over input subQuery and a {@link ValueSourceQuery}. * @param subQuery the sub query whose score is being customized. Must not be null. * @param valSrcQuery a value source query whose scores are used in the custom score * computation. For most simple/convenient use case this would be a * {@link org.apache.lucene.search.function.FieldScoreQuery FieldScoreQuery}. * This parameter is optional - it can be null. */ public CustomScoreQuery(Query subQuery, ValueSourceQuery valSrcQuery) { this(subQuery, valSrcQuery!=null ? // don't want an array that contains a single null.. new ValueSourceQuery[] {valSrcQuery} : new ValueSourceQuery[0]); } /** * Create a CustomScoreQuery over input subQuery and a {@link ValueSourceQuery}. * @param subQuery the sub query whose score is being customized. Must not be null. * @param valSrcQueries value source queries whose scores are used in the custom score * computation. For most simple/convenient use case these would be * {@link org.apache.lucene.search.function.FieldScoreQuery FieldScoreQueries}. * This parameter is optional - it can be null or even an empty array. */ public CustomScoreQuery(Query subQuery, ValueSourceQuery valSrcQueries[]) { this.subQuery = subQuery; this.valSrcQueries = valSrcQueries!=null? valSrcQueries : new ValueSourceQuery[0]; if (subQuery == null) throw new IllegalArgumentException("<subquery> must not be null!"); } /*(non-Javadoc) @see org.apache.lucene.search.Query#rewrite(org.apache.lucene.index.IndexReader) */ public Query rewrite(IndexReader reader) throws IOException { CustomScoreQuery clone = null; final Query sq = subQuery.rewrite(reader); if (sq != subQuery) { clone = (CustomScoreQuery) clone(); clone.subQuery = sq; } for(int i = 0; i < valSrcQueries.length; i++) { final ValueSourceQuery v = (ValueSourceQuery) valSrcQueries[i].rewrite(reader); if (v != valSrcQueries[i]) { if (clone == null) clone = (CustomScoreQuery) clone(); clone.valSrcQueries[i] = v; } } return (clone == null) ? this : clone; } /*(non-Javadoc) @see org.apache.lucene.search.Query#extractTerms(java.util.Set) */ public void extractTerms(Set terms) { subQuery.extractTerms(terms); for(int i = 0; i < valSrcQueries.length; i++) { valSrcQueries[i].extractTerms(terms); } } /*(non-Javadoc) @see org.apache.lucene.search.Query#clone() */ public Object clone() { CustomScoreQuery clone = (CustomScoreQuery)super.clone(); clone.subQuery = (Query) subQuery.clone(); clone.valSrcQueries = new ValueSourceQuery[valSrcQueries.length]; for(int i = 0; i < valSrcQueries.length; i++) { clone.valSrcQueries[i] = (ValueSourceQuery) valSrcQueries[i].clone(); } return clone; } /* (non-Javadoc) @see org.apache.lucene.search.Query#toString(java.lang.String) */ public String toString(String field) { StringBuffer sb = new StringBuffer(name()).append("("); sb.append(subQuery.toString(field)); for(int i = 0; i < valSrcQueries.length; i++) { sb.append(", ").append(valSrcQueries[i].toString(field)); } sb.append(")"); sb.append(strict?" STRICT" : ""); return sb.toString() + ToStringUtils.boost(getBoost()); } /** Returns true if <code>o</code> is equal to this. */ public boolean equals(Object o) { if (getClass() != o.getClass()) { return false; } CustomScoreQuery other = (CustomScoreQuery)o; if (this.getBoost() != other.getBoost() || !this.subQuery.equals(other.subQuery) || this.strict != other.strict || this.valSrcQueries.length != other.valSrcQueries.length) { return false; } for (int i=0; i<valSrcQueries.length; i++) { //TODO simplify with Arrays.deepEquals() once moving to Java 1.5 if (!valSrcQueries[i].equals(other.valSrcQueries[i])) { return false; } } return true; } /** Returns a hash code value for this object. */ public int hashCode() { int valSrcHash = 0; for (int i=0; i<valSrcQueries.length; i++) { //TODO simplify with Arrays.deepHashcode() once moving to Java 1.5 valSrcHash += valSrcQueries[i].hashCode(); } return (getClass().hashCode() + subQuery.hashCode() + valSrcHash) ^ Float.floatToIntBits(getBoost()) ^ (strict ? 1234 : 4321); } /** * Returns a {@link CustomScoreProvider} that calculates the custom scores * for the given {@link IndexReader}. The default implementation returns a default * implementation as specified in the docs of {@link CustomScoreProvider}. * @since 2.9.2 */ protected CustomScoreProvider getCustomScoreProvider(IndexReader reader) throws IOException { // when deprecated methods are removed, do not extend class here, just return new default CustomScoreProvider return new CustomScoreProvider(reader) { public float customScore(int doc, float subQueryScore, float valSrcScores[]) throws IOException { return CustomScoreQuery.this.customScore(doc, subQueryScore, valSrcScores); } public float customScore(int doc, float subQueryScore, float valSrcScore) throws IOException { return CustomScoreQuery.this.customScore(doc, subQueryScore, valSrcScore); } public Explanation customExplain(int doc, Explanation subQueryExpl, Explanation valSrcExpls[]) throws IOException { return CustomScoreQuery.this.customExplain(doc, subQueryExpl, valSrcExpls); } public Explanation customExplain(int doc, Explanation subQueryExpl, Explanation valSrcExpl) throws IOException { return CustomScoreQuery.this.customExplain(doc, subQueryExpl, valSrcExpl); } }; } /** * Compute a custom score by the subQuery score and a number of * ValueSourceQuery scores. * @deprecated Will be removed in Lucene 3.1. * The doc is relative to the current reader, which is * unknown to CustomScoreQuery when using per-segment search (since Lucene 2.9). * Please override {@link #getCustomScoreProvider} and return a subclass * of {@link CustomScoreProvider} for the given {@link IndexReader}. * @see CustomScoreProvider#customScore(int,float,float[]) */ public float customScore(int doc, float subQueryScore, float valSrcScores[]) { if (valSrcScores.length == 1) { return customScore(doc, subQueryScore, valSrcScores[0]); } if (valSrcScores.length == 0) { return customScore(doc, subQueryScore, 1); } float score = subQueryScore; for(int i = 0; i < valSrcScores.length; i++) { score *= valSrcScores[i]; } return score; } /** * Compute a custom score by the subQuery score and the ValueSourceQuery score. * @deprecated Will be removed in Lucene 3.1. * The doc is relative to the current reader, which is * unknown to CustomScoreQuery when using per-segment search (since Lucene 2.9). * Please override {@link #getCustomScoreProvider} and return a subclass * of {@link CustomScoreProvider} for the given {@link IndexReader}. * @see CustomScoreProvider#customScore(int,float,float) */ public float customScore(int doc, float subQueryScore, float valSrcScore) { return subQueryScore * valSrcScore; } /** * Explain the custom score. * @deprecated Will be removed in Lucene 3.1. * The doc is relative to the current reader, which is * unknown to CustomScoreQuery when using per-segment search (since Lucene 2.9). * Please override {@link #getCustomScoreProvider} and return a subclass * of {@link CustomScoreProvider} for the given {@link IndexReader}. * @see CustomScoreProvider#customExplain(int,Explanation,Explanation[]) */ public Explanation customExplain(int doc, Explanation subQueryExpl, Explanation valSrcExpls[]) { if (valSrcExpls.length == 1) { return customExplain(doc, subQueryExpl, valSrcExpls[0]); } if (valSrcExpls.length == 0) { return subQueryExpl; } float valSrcScore = 1; for (int i = 0; i < valSrcExpls.length; i++) { valSrcScore *= valSrcExpls[i].getValue(); } Explanation exp = new Explanation( valSrcScore * subQueryExpl.getValue(), "custom score: product of:"); exp.addDetail(subQueryExpl); for (int i = 0; i < valSrcExpls.length; i++) { exp.addDetail(valSrcExpls[i]); } return exp; } /** * Explain the custom score. * @deprecated Will be removed in Lucene 3.1. * The doc is relative to the current reader, which is * unknown to CustomScoreQuery when using per-segment search (since Lucene 2.9). * Please override {@link #getCustomScoreProvider} and return a subclass * of {@link CustomScoreProvider} for the given {@link IndexReader}. * @see CustomScoreProvider#customExplain(int,Explanation,Explanation[]) */ public Explanation customExplain(int doc, Explanation subQueryExpl, Explanation valSrcExpl) { float valSrcScore = 1; if (valSrcExpl != null) { valSrcScore *= valSrcExpl.getValue(); } Explanation exp = new Explanation( valSrcScore * subQueryExpl.getValue(), "custom score: product of:"); exp.addDetail(subQueryExpl); exp.addDetail(valSrcExpl); return exp; } //=========================== W E I G H T ============================ private class CustomWeight extends Weight { Similarity similarity; Weight subQueryWeight; Weight[] valSrcWeights; boolean qStrict; public CustomWeight(Searcher searcher) throws IOException { this.similarity = getSimilarity(searcher); this.subQueryWeight = subQuery.weight(searcher); this.valSrcWeights = new Weight[valSrcQueries.length]; for(int i = 0; i < valSrcQueries.length; i++) { this.valSrcWeights[i] = valSrcQueries[i].createWeight(searcher); } this.qStrict = strict; } /*(non-Javadoc) @see org.apache.lucene.search.Weight#getQuery() */ public Query getQuery() { return CustomScoreQuery.this; } /*(non-Javadoc) @see org.apache.lucene.search.Weight#getValue() */ public float getValue() { return getBoost(); } /*(non-Javadoc) @see org.apache.lucene.search.Weight#sumOfSquaredWeights() */ public float sumOfSquaredWeights() throws IOException { float sum = subQueryWeight.sumOfSquaredWeights(); for(int i = 0; i < valSrcWeights.length; i++) { if (qStrict) { valSrcWeights[i].sumOfSquaredWeights(); // do not include ValueSource part in the query normalization } else { sum += valSrcWeights[i].sumOfSquaredWeights(); } } sum *= getBoost() * getBoost(); // boost each sub-weight return sum ; } /*(non-Javadoc) @see org.apache.lucene.search.Weight#normalize(float) */ public void normalize(float norm) { norm *= getBoost(); // incorporate boost subQueryWeight.normalize(norm); for(int i = 0; i < valSrcWeights.length; i++) { if (qStrict) { valSrcWeights[i].normalize(1); // do not normalize the ValueSource part } else { valSrcWeights[i].normalize(norm); } } } public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder, boolean topScorer) throws IOException { // Pass true for "scoresDocsInOrder", because we // require in-order scoring, even if caller does not, // since we call advance on the valSrcScorers. Pass // false for "topScorer" because we will not invoke // score(Collector) on these scorers: Scorer subQueryScorer = subQueryWeight.scorer(reader, true, false); if (subQueryScorer == null) { return null; } Scorer[] valSrcScorers = new Scorer[valSrcWeights.length]; for(int i = 0; i < valSrcScorers.length; i++) { valSrcScorers[i] = valSrcWeights[i].scorer(reader, true, topScorer); } return new CustomScorer(similarity, reader, this, subQueryScorer, valSrcScorers); } public Explanation explain(IndexReader reader, int doc) throws IOException { Explanation explain = doExplain(reader, doc); return explain == null ? new Explanation(0.0f, "no matching docs") : doExplain(reader, doc); } private Explanation doExplain(IndexReader reader, int doc) throws IOException { Scorer[] valSrcScorers = new Scorer[valSrcWeights.length]; for(int i = 0; i < valSrcScorers.length; i++) { valSrcScorers[i] = valSrcWeights[i].scorer(reader, true, false); } Explanation subQueryExpl = subQueryWeight.explain(reader, doc); if (!subQueryExpl.isMatch()) { return subQueryExpl; } // match Explanation[] valSrcExpls = new Explanation[valSrcScorers.length]; for(int i = 0; i < valSrcScorers.length; i++) { valSrcExpls[i] = valSrcScorers[i].explain(doc); } Explanation customExp = CustomScoreQuery.this.getCustomScoreProvider(reader).customExplain(doc,subQueryExpl,valSrcExpls); float sc = getValue() * customExp.getValue(); Explanation res = new ComplexExplanation( true, sc, CustomScoreQuery.this.toString() + ", product of:"); res.addDetail(customExp); res.addDetail(new Explanation(getValue(), "queryBoost")); // actually using the q boost as q weight (== weight value) return res; } public boolean scoresDocsOutOfOrder() { return false; } } //=========================== S C O R E R ============================ /** * A scorer that applies a (callback) function on scores of the subQuery. */ private class CustomScorer extends Scorer { private final CustomWeight weight; private final float qWeight; private Scorer subQueryScorer; private Scorer[] valSrcScorers; private IndexReader reader; private final CustomScoreProvider provider; private float vScores[]; // reused in score() to avoid allocating this array for each doc // constructor private CustomScorer(Similarity similarity, IndexReader reader, CustomWeight w, Scorer subQueryScorer, Scorer[] valSrcScorers) throws IOException { super(similarity); this.weight = w; this.qWeight = w.getValue(); this.subQueryScorer = subQueryScorer; this.valSrcScorers = valSrcScorers; this.reader = reader; this.vScores = new float[valSrcScorers.length]; this.provider = CustomScoreQuery.this.getCustomScoreProvider(reader); } /** @deprecated use {@link #nextDoc()} instead. */ public boolean next() throws IOException { return nextDoc() != NO_MORE_DOCS; } public int nextDoc() throws IOException { int doc = subQueryScorer.nextDoc(); if (doc != NO_MORE_DOCS) { for (int i = 0; i < valSrcScorers.length; i++) { valSrcScorers[i].advance(doc); } } return doc; } /** @deprecated use {@link #docID()} instead. */ public int doc() { return subQueryScorer.doc(); } public int docID() { return subQueryScorer.docID(); } /*(non-Javadoc) @see org.apache.lucene.search.Scorer#score() */ public float score() throws IOException { for (int i = 0; i < valSrcScorers.length; i++) { vScores[i] = valSrcScorers[i].score(); } return qWeight * provider.customScore(subQueryScorer.docID(), subQueryScorer.score(), vScores); } /** @deprecated use {@link #advance(int)} instead. */ public boolean skipTo(int target) throws IOException { return advance(target) != NO_MORE_DOCS; } public int advance(int target) throws IOException { int doc = subQueryScorer.advance(target); if (doc != NO_MORE_DOCS) { for (int i = 0; i < valSrcScorers.length; i++) { valSrcScorers[i].advance(doc); } } return doc; } // TODO: remove in 3.0 /*(non-Javadoc) @see org.apache.lucene.search.Scorer#explain(int) */ public Explanation explain(int doc) throws IOException { Explanation subQueryExpl = weight.subQueryWeight.explain(reader,doc); if (!subQueryExpl.isMatch()) { return subQueryExpl; } // match Explanation[] valSrcExpls = new Explanation[valSrcScorers.length]; for(int i = 0; i < valSrcScorers.length; i++) { valSrcExpls[i] = valSrcScorers[i].explain(doc); } Explanation customExp = customExplain(doc,subQueryExpl,valSrcExpls); float sc = qWeight * customExp.getValue(); Explanation res = new ComplexExplanation( true, sc, CustomScoreQuery.this.toString() + ", product of:"); res.addDetail(customExp); res.addDetail(new Explanation(qWeight, "queryBoost")); // actually using the q boost as q weight (== weight value) return res; } public int getSort(int fieldNumber) { return subQueryScorer.getSort(fieldNumber); } public int[] getSorts() { return subQueryScorer.getSorts(); } } public Weight createWeight(Searcher searcher) throws IOException { return new CustomWeight(searcher); } /** * Checks if this is strict custom scoring. * In strict custom scoring, the ValueSource part does not participate in weight normalization. * This may be useful when one wants full control over how scores are modified, and does * not care about normalizing by the ValueSource part. * One particular case where this is useful if for testing this query. * <P> * Note: only has effect when the ValueSource part is not null. */ public boolean isStrict() { return strict; } /** * Set the strict mode of this query. * @param strict The strict mode to set. * @see #isStrict() */ public void setStrict(boolean strict) { this.strict = strict; } /** * A short name of this query, used in {@link #toString(String)}. */ public String name() { return "custom"; } }