/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch 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.elasticsearch.index.query.functionscore; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.TextField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.SortedNumericDocValues; import org.apache.lucene.index.Term; import org.apache.lucene.search.Explanation; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.RandomApproximationQuery; import org.apache.lucene.search.Scorer; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.Weight; import org.apache.lucene.search.SortField; import org.apache.lucene.store.Directory; import org.apache.lucene.util.Accountable; import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.lucene.search.function.CombineFunction; import org.elasticsearch.common.lucene.search.function.FieldValueFactorFunction; import org.elasticsearch.common.lucene.search.function.FiltersFunctionScoreQuery; import org.elasticsearch.common.lucene.search.function.FiltersFunctionScoreQuery.FilterFunction; import org.elasticsearch.common.lucene.search.function.FiltersFunctionScoreQuery.ScoreMode; import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery; import org.elasticsearch.common.lucene.search.function.LeafScoreFunction; import org.elasticsearch.common.lucene.search.function.RandomScoreFunction; import org.elasticsearch.common.lucene.search.function.ScoreFunction; import org.elasticsearch.common.lucene.search.function.WeightFactorFunction; import org.elasticsearch.index.Index; import org.elasticsearch.index.fielddata.AtomicFieldData; import org.elasticsearch.index.fielddata.AtomicNumericFieldData; import org.elasticsearch.index.fielddata.IndexFieldData; import org.elasticsearch.index.fielddata.IndexNumericFieldData; import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.index.fielddata.SortedBinaryDocValues; import org.elasticsearch.index.fielddata.SortedNumericDoubleValues; import org.elasticsearch.search.MultiValueMode; import org.elasticsearch.test.ESTestCase; import org.junit.After; import org.junit.Before; import java.io.IOException; import java.util.Collection; import java.util.concurrent.ExecutionException; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsEqual.equalTo; public class FunctionScoreTests extends ESTestCase { private static final String UNSUPPORTED = "Method not implemented. This is just a stub for testing."; /** * Stub for IndexFieldData. Needed by some score functions. Returns 1 as count always. */ private static class IndexFieldDataStub implements IndexFieldData<AtomicFieldData> { @Override public String getFieldName() { return "test"; } @Override public AtomicFieldData load(LeafReaderContext context) { return new AtomicFieldData() { @Override public ScriptDocValues getScriptValues() { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public SortedBinaryDocValues getBytesValues() { return new SortedBinaryDocValues() { @Override public boolean advanceExact(int docId) { return true; } @Override public int docValueCount() { return 1; } @Override public BytesRef nextValue() { return new BytesRef("0"); } }; } @Override public long ramBytesUsed() { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public Collection<Accountable> getChildResources() { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public void close() { } }; } @Override public AtomicFieldData loadDirect(LeafReaderContext context) throws Exception { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public SortField sortField(@Nullable Object missingValue, MultiValueMode sortMode, XFieldComparatorSource.Nested nested, boolean reverse) { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public void clear() { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public Index index() { throw new UnsupportedOperationException(UNSUPPORTED); } } /** * Stub for IndexNumericFieldData needed by some score functions. Returns 1 as value always. */ private static class IndexNumericFieldDataStub implements IndexNumericFieldData { @Override public NumericType getNumericType() { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public String getFieldName() { return "test"; } @Override public AtomicNumericFieldData load(LeafReaderContext context) { return new AtomicNumericFieldData() { @Override public SortedNumericDocValues getLongValues() { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public SortedNumericDoubleValues getDoubleValues() { return new SortedNumericDoubleValues() { @Override public boolean advanceExact(int docId) { return true; } @Override public int docValueCount() { return 1; } @Override public double nextValue() { return 1d; } }; } @Override public ScriptDocValues getScriptValues() { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public SortedBinaryDocValues getBytesValues() { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public long ramBytesUsed() { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public Collection<Accountable> getChildResources() { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public void close() { } }; } @Override public AtomicNumericFieldData loadDirect(LeafReaderContext context) throws Exception { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public SortField sortField(@Nullable Object missingValue, MultiValueMode sortMode, XFieldComparatorSource.Nested nested, boolean reverse) { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public void clear() { throw new UnsupportedOperationException(UNSUPPORTED); } @Override public Index index() { throw new UnsupportedOperationException(UNSUPPORTED); } } private static final ScoreFunction RANDOM_SCORE_FUNCTION = new RandomScoreFunction(0, 0, new IndexFieldDataStub()); private static final ScoreFunction FIELD_VALUE_FACTOR_FUNCTION = new FieldValueFactorFunction("test", 1, FieldValueFactorFunction.Modifier.LN, 1.0, null); private static final ScoreFunction GAUSS_DECAY_FUNCTION = new DecayFunctionBuilder.NumericFieldDataScoreFunction(0, 1, 0.1, 0, GaussDecayFunctionBuilder.GAUSS_DECAY_FUNCTION, new IndexNumericFieldDataStub(), MultiValueMode.MAX); private static final ScoreFunction EXP_DECAY_FUNCTION = new DecayFunctionBuilder.NumericFieldDataScoreFunction(0, 1, 0.1, 0, ExponentialDecayFunctionBuilder.EXP_DECAY_FUNCTION, new IndexNumericFieldDataStub(), MultiValueMode.MAX); private static final ScoreFunction LIN_DECAY_FUNCTION = new DecayFunctionBuilder.NumericFieldDataScoreFunction(0, 1, 0.1, 0, LinearDecayFunctionBuilder.LINEAR_DECAY_FUNCTION, new IndexNumericFieldDataStub(), MultiValueMode.MAX); private static final ScoreFunction WEIGHT_FACTOR_FUNCTION = new WeightFactorFunction(4); private static final String TEXT = "The way out is through."; private static final String FIELD = "test"; private static final Term TERM = new Term(FIELD, "through"); private Directory dir; private IndexWriter w; private DirectoryReader reader; private IndexSearcher searcher; @Before public void initSearcher() throws IOException { dir = newDirectory(); w = new IndexWriter(dir, newIndexWriterConfig(new StandardAnalyzer())); Document d = new Document(); d.add(new TextField(FIELD, TEXT, Field.Store.YES)); d.add(new TextField("_uid", "1", Field.Store.YES)); w.addDocument(d); w.commit(); reader = DirectoryReader.open(w); searcher = newSearcher(reader); } @After public void closeAllTheReaders() throws IOException { reader.close(); w.close(); dir.close(); } public void testExplainFunctionScoreQuery() throws IOException { Explanation functionExplanation = getFunctionScoreExplanation(searcher, RANDOM_SCORE_FUNCTION); checkFunctionScoreExplanation(functionExplanation, "random score function (seed: 0)"); assertThat(functionExplanation.getDetails()[0].getDetails().length, equalTo(0)); functionExplanation = getFunctionScoreExplanation(searcher, FIELD_VALUE_FACTOR_FUNCTION); checkFunctionScoreExplanation(functionExplanation, "field value function: ln(doc['test'].value?:1.0 * factor=1.0)"); assertThat(functionExplanation.getDetails()[0].getDetails().length, equalTo(0)); functionExplanation = getFunctionScoreExplanation(searcher, GAUSS_DECAY_FUNCTION); checkFunctionScoreExplanation(functionExplanation, "Function for field test:"); assertThat(functionExplanation.getDetails()[0].getDetails()[0].toString(), equalTo("0.1 = exp(-0.5*pow(MAX[Math.max(Math.abs" + "(1.0(=doc value) - 0.0(=origin))) - 0.0(=offset), 0)],2.0)/0.21714724095162594)\n")); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails().length, equalTo(0)); functionExplanation = getFunctionScoreExplanation(searcher, EXP_DECAY_FUNCTION); checkFunctionScoreExplanation(functionExplanation, "Function for field test:"); assertThat(functionExplanation.getDetails()[0].getDetails()[0].toString(), equalTo("0.1 = exp(- MAX[Math.max(Math.abs(1.0(=doc value) - 0.0(=origin))) - 0.0(=offset), 0)] * 2.3025850929940455)\n")); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails().length, equalTo(0)); functionExplanation = getFunctionScoreExplanation(searcher, LIN_DECAY_FUNCTION); checkFunctionScoreExplanation(functionExplanation, "Function for field test:"); assertThat(functionExplanation.getDetails()[0].getDetails()[0].toString(), equalTo("0.1 = max(0.0, ((1.1111111111111112" + " - MAX[Math.max(Math.abs(1.0(=doc value) - 0.0(=origin))) - 0.0(=offset), 0)])/1.1111111111111112)\n")); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails().length, equalTo(0)); functionExplanation = getFunctionScoreExplanation(searcher, WEIGHT_FACTOR_FUNCTION); checkFunctionScoreExplanation(functionExplanation, "product of:"); assertThat(functionExplanation.getDetails()[0].getDetails()[0].toString(), equalTo("1.0 = constant score 1.0 - no function provided\n")); assertThat(functionExplanation.getDetails()[0].getDetails()[1].toString(), equalTo("4.0 = weight\n")); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails().length, equalTo(0)); assertThat(functionExplanation.getDetails()[0].getDetails()[1].getDetails().length, equalTo(0)); } public Explanation getFunctionScoreExplanation(IndexSearcher searcher, ScoreFunction scoreFunction) throws IOException { FunctionScoreQuery functionScoreQuery = new FunctionScoreQuery(new TermQuery(TERM), scoreFunction, 0.0f, CombineFunction.AVG, 100); Weight weight = searcher.createNormalizedWeight(functionScoreQuery, true); Explanation explanation = weight.explain(searcher.getIndexReader().leaves().get(0), 0); return explanation.getDetails()[1]; } public void checkFunctionScoreExplanation(Explanation randomExplanation, String functionExpl) { assertThat(randomExplanation.getDescription(), equalTo("min of:")); assertThat(randomExplanation.getDetails()[0].getDescription(), equalTo(functionExpl)); } public void testExplainFiltersFunctionScoreQuery() throws IOException { Explanation functionExplanation = getFiltersFunctionScoreExplanation(searcher, RANDOM_SCORE_FUNCTION); checkFiltersFunctionScoreExplanation(functionExplanation, "random score function (seed: 0)", 0); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails()[1].getDetails().length, equalTo(0)); functionExplanation = getFiltersFunctionScoreExplanation(searcher, FIELD_VALUE_FACTOR_FUNCTION); checkFiltersFunctionScoreExplanation(functionExplanation, "field value function: ln(doc['test'].value?:1.0 * factor=1.0)", 0); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails()[1].getDetails().length, equalTo(0)); functionExplanation = getFiltersFunctionScoreExplanation(searcher, GAUSS_DECAY_FUNCTION); checkFiltersFunctionScoreExplanation(functionExplanation, "Function for field test:", 0); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails()[1].getDetails()[0].toString(), equalTo("0.1 = " + "exp(-0.5*pow(MAX[Math.max(Math.abs(1.0(=doc value) - 0.0(=origin))) - 0.0(=offset), 0)],2.0)/0.21714724095162594)\n")); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails()[1].getDetails()[0].getDetails().length, equalTo(0)); functionExplanation = getFiltersFunctionScoreExplanation(searcher, EXP_DECAY_FUNCTION); checkFiltersFunctionScoreExplanation(functionExplanation, "Function for field test:", 0); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails()[1].getDetails()[0].toString(), equalTo("0.1 = exp(- MAX[Math.max(Math.abs(1.0(=doc value) - 0.0(=origin))) - 0.0(=offset), 0)] * 2.3025850929940455)\n")); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails()[1].getDetails()[0].getDetails().length, equalTo(0)); functionExplanation = getFiltersFunctionScoreExplanation(searcher, LIN_DECAY_FUNCTION); checkFiltersFunctionScoreExplanation(functionExplanation, "Function for field test:", 0); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails()[1].getDetails()[0].toString(), equalTo("0.1 = max(0.0, ((1.1111111111111112 - MAX[Math.max(Math.abs(1.0(=doc value) - 0.0(=origin)))" + " - 0.0(=offset), 0)])/1.1111111111111112)\n")); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails()[1].getDetails()[0].getDetails().length, equalTo(0)); // now test all together functionExplanation = getFiltersFunctionScoreExplanation(searcher , RANDOM_SCORE_FUNCTION , FIELD_VALUE_FACTOR_FUNCTION , GAUSS_DECAY_FUNCTION , EXP_DECAY_FUNCTION , LIN_DECAY_FUNCTION ); checkFiltersFunctionScoreExplanation(functionExplanation, "random score function (seed: 0)", 0); assertThat(functionExplanation.getDetails()[0].getDetails()[0].getDetails()[1].getDetails().length, equalTo(0)); checkFiltersFunctionScoreExplanation(functionExplanation, "field value function: ln(doc['test'].value?:1.0 * factor=1.0)", 1); assertThat(functionExplanation.getDetails()[0].getDetails()[1].getDetails()[1].getDetails().length, equalTo(0)); checkFiltersFunctionScoreExplanation(functionExplanation, "Function for field test:", 2); assertThat(functionExplanation.getDetails()[0].getDetails()[2].getDetails()[1].getDetails()[0].toString(), equalTo("0.1 = " + "exp(-0.5*pow(MAX[Math.max(Math.abs(1.0(=doc value) - 0.0(=origin))) - 0.0(=offset), 0)],2.0)/0.21714724095162594)\n")); assertThat(functionExplanation.getDetails()[0].getDetails()[2].getDetails()[1].getDetails()[0].getDetails().length, equalTo(0)); checkFiltersFunctionScoreExplanation(functionExplanation, "Function for field test:", 3); assertThat(functionExplanation.getDetails()[0].getDetails()[3].getDetails()[1].getDetails()[0].toString(), equalTo( "0.1 = exp(- " + "MAX[Math.max(Math.abs(1.0(=doc value) - 0.0(=origin))) - 0.0(=offset), 0)] * 2.3025850929940455)\n")); assertThat(functionExplanation.getDetails()[0].getDetails()[3].getDetails()[1].getDetails()[0].getDetails().length, equalTo(0)); checkFiltersFunctionScoreExplanation(functionExplanation, "Function for field test:", 4); assertThat(functionExplanation.getDetails()[0].getDetails()[4].getDetails()[1].getDetails()[0].toString(), equalTo("0.1 = max(0.0, ((1.1111111111111112 - MAX[Math.max(Math.abs(1.0(=doc value) - 0.0(=origin)))" + " - 0.0(=offset), 0)])/1.1111111111111112)\n")); assertThat(functionExplanation.getDetails()[0].getDetails()[4].getDetails()[1].getDetails()[0].getDetails().length, equalTo(0)); } public Explanation getFiltersFunctionScoreExplanation(IndexSearcher searcher, ScoreFunction... scoreFunctions) throws IOException { FiltersFunctionScoreQuery filtersFunctionScoreQuery = getFiltersFunctionScoreQuery(FiltersFunctionScoreQuery.ScoreMode.AVG, CombineFunction.AVG, scoreFunctions); return getExplanation(searcher, filtersFunctionScoreQuery).getDetails()[1]; } protected Explanation getExplanation(IndexSearcher searcher, FiltersFunctionScoreQuery filtersFunctionScoreQuery) throws IOException { Weight weight = searcher.createNormalizedWeight(filtersFunctionScoreQuery, true); return weight.explain(searcher.getIndexReader().leaves().get(0), 0); } public FiltersFunctionScoreQuery getFiltersFunctionScoreQuery(FiltersFunctionScoreQuery.ScoreMode scoreMode, CombineFunction combineFunction, ScoreFunction... scoreFunctions) { FilterFunction[] filterFunctions = new FilterFunction[scoreFunctions.length]; for (int i = 0; i < scoreFunctions.length; i++) { filterFunctions[i] = new FiltersFunctionScoreQuery.FilterFunction( new TermQuery(TERM), scoreFunctions[i]); } return new FiltersFunctionScoreQuery(new TermQuery(TERM), scoreMode, filterFunctions, Float.MAX_VALUE, Float.MAX_VALUE * -1, combineFunction); } public void checkFiltersFunctionScoreExplanation(Explanation randomExplanation, String functionExpl, int whichFunction) { assertThat(randomExplanation.getDescription(), equalTo("min of:")); Explanation explanation = randomExplanation.getDetails()[0]; assertThat(explanation.getDescription(), equalTo("function score, score mode [avg]")); Explanation functionExplanation = randomExplanation.getDetails()[0].getDetails()[whichFunction]; assertThat(functionExplanation.getDescription(), equalTo("function score, product of:")); assertThat(functionExplanation.getDetails()[0].getDescription(), equalTo("match filter: " + FIELD + ":" + TERM.text())); assertThat(functionExplanation.getDetails()[1].getDescription(), equalTo(functionExpl)); } private static float[] randomFloats(int size) { float[] values = new float[size]; for (int i = 0; i < values.length; i++) { values[i] = randomFloat() * (randomBoolean() ? 1.0f : -1.0f) * randomInt(100) + 1.e-5f; } return values; } private static double[] randomDoubles(int size) { double[] values = new double[size]; for (int i = 0; i < values.length; i++) { values[i] = randomDouble() * (randomBoolean() ? 1.0d : -1.0d) * randomInt(100) + 1.e-5d; } return values; } private static class ScoreFunctionStub extends ScoreFunction { private double score; ScoreFunctionStub(double score) { super(CombineFunction.REPLACE); this.score = score; } @Override public LeafScoreFunction getLeafScoreFunction(LeafReaderContext ctx) throws IOException { return new LeafScoreFunction() { @Override public double score(int docId, float subQueryScore) { return score; } @Override public Explanation explainScore(int docId, Explanation subQueryScore) throws IOException { return Explanation.match((float) score, "a random score for testing"); } }; } @Override public boolean needsScores() { return false; } @Override protected boolean doEquals(ScoreFunction other) { return false; } @Override protected int doHashCode() { return 0; } } public void testSimpleWeightedFunction() throws IOException, ExecutionException, InterruptedException { int numFunctions = randomIntBetween(1, 3); float[] weights = randomFloats(numFunctions); double[] scores = randomDoubles(numFunctions); ScoreFunctionStub[] scoreFunctionStubs = new ScoreFunctionStub[numFunctions]; for (int i = 0; i < numFunctions; i++) { scoreFunctionStubs[i] = new ScoreFunctionStub(scores[i]); } WeightFactorFunction[] weightFunctionStubs = new WeightFactorFunction[numFunctions]; for (int i = 0; i < numFunctions; i++) { weightFunctionStubs[i] = new WeightFactorFunction(weights[i], scoreFunctionStubs[i]); } FiltersFunctionScoreQuery filtersFunctionScoreQueryWithWeights = getFiltersFunctionScoreQuery( FiltersFunctionScoreQuery.ScoreMode.MULTIPLY , CombineFunction.REPLACE , weightFunctionStubs ); TopDocs topDocsWithWeights = searcher.search(filtersFunctionScoreQueryWithWeights, 1); float scoreWithWeight = topDocsWithWeights.scoreDocs[0].score; double score = 1; for (int i = 0; i < weights.length; i++) { score *= weights[i] * scores[i]; } assertThat(scoreWithWeight / (float) score, is(1f)); float explainedScore = getExplanation(searcher, filtersFunctionScoreQueryWithWeights).getValue(); assertThat(explainedScore / scoreWithWeight, is(1f)); filtersFunctionScoreQueryWithWeights = getFiltersFunctionScoreQuery( FiltersFunctionScoreQuery.ScoreMode.SUM , CombineFunction.REPLACE , weightFunctionStubs ); topDocsWithWeights = searcher.search(filtersFunctionScoreQueryWithWeights, 1); scoreWithWeight = topDocsWithWeights.scoreDocs[0].score; double sum = 0; for (int i = 0; i < weights.length; i++) { sum += weights[i] * scores[i]; } assertThat(scoreWithWeight / (float) sum, is(1f)); explainedScore = getExplanation(searcher, filtersFunctionScoreQueryWithWeights).getValue(); assertThat(explainedScore / scoreWithWeight, is(1f)); filtersFunctionScoreQueryWithWeights = getFiltersFunctionScoreQuery( FiltersFunctionScoreQuery.ScoreMode.AVG , CombineFunction.REPLACE , weightFunctionStubs ); topDocsWithWeights = searcher.search(filtersFunctionScoreQueryWithWeights, 1); scoreWithWeight = topDocsWithWeights.scoreDocs[0].score; double norm = 0; sum = 0; for (int i = 0; i < weights.length; i++) { norm += weights[i]; sum += weights[i] * scores[i]; } assertThat(scoreWithWeight / (float) (sum / norm), is(1f)); explainedScore = getExplanation(searcher, filtersFunctionScoreQueryWithWeights).getValue(); assertThat(explainedScore / scoreWithWeight, is(1f)); filtersFunctionScoreQueryWithWeights = getFiltersFunctionScoreQuery( FiltersFunctionScoreQuery.ScoreMode.MIN , CombineFunction.REPLACE , weightFunctionStubs ); topDocsWithWeights = searcher.search(filtersFunctionScoreQueryWithWeights, 1); scoreWithWeight = topDocsWithWeights.scoreDocs[0].score; double min = Double.POSITIVE_INFINITY; for (int i = 0; i < weights.length; i++) { min = Math.min(min, weights[i] * scores[i]); } assertThat(scoreWithWeight / (float) min, is(1f)); explainedScore = getExplanation(searcher, filtersFunctionScoreQueryWithWeights).getValue(); assertThat(explainedScore / scoreWithWeight, is(1f)); filtersFunctionScoreQueryWithWeights = getFiltersFunctionScoreQuery( FiltersFunctionScoreQuery.ScoreMode.MAX , CombineFunction.REPLACE , weightFunctionStubs ); topDocsWithWeights = searcher.search(filtersFunctionScoreQueryWithWeights, 1); scoreWithWeight = topDocsWithWeights.scoreDocs[0].score; double max = Double.NEGATIVE_INFINITY; for (int i = 0; i < weights.length; i++) { max = Math.max(max, weights[i] * scores[i]); } assertThat(scoreWithWeight / (float) max, is(1f)); explainedScore = getExplanation(searcher, filtersFunctionScoreQueryWithWeights).getValue(); assertThat(explainedScore / scoreWithWeight, is(1f)); } public void testWeightOnlyCreatesBoostFunction() throws IOException { FunctionScoreQuery filtersFunctionScoreQueryWithWeights = new FunctionScoreQuery(new MatchAllDocsQuery(), new WeightFactorFunction(2), 0.0f, CombineFunction.MULTIPLY, 100); TopDocs topDocsWithWeights = searcher.search(filtersFunctionScoreQueryWithWeights, 1); float score = topDocsWithWeights.scoreDocs[0].score; assertThat(score, equalTo(2.0f)); } public void testMinScoreExplain() throws IOException { Query query = new MatchAllDocsQuery(); Explanation queryExpl = searcher.explain(query, 0); FunctionScoreQuery fsq = new FunctionScoreQuery(query, null, 0f, null, Float.POSITIVE_INFINITY); Explanation fsqExpl = searcher.explain(fsq, 0); assertTrue(fsqExpl.isMatch()); assertEquals(queryExpl.getValue(), fsqExpl.getValue(), 0f); assertEquals(queryExpl.getDescription(), fsqExpl.getDescription()); fsq = new FunctionScoreQuery(query, null, 10f, null, Float.POSITIVE_INFINITY); fsqExpl = searcher.explain(fsq, 0); assertFalse(fsqExpl.isMatch()); assertEquals("Score value is too low, expected at least 10.0 but got 1.0", fsqExpl.getDescription()); FiltersFunctionScoreQuery ffsq = new FiltersFunctionScoreQuery(query, ScoreMode.SUM, new FilterFunction[0], Float.POSITIVE_INFINITY, 0f, CombineFunction.MULTIPLY); Explanation ffsqExpl = searcher.explain(ffsq, 0); assertTrue(ffsqExpl.isMatch()); assertEquals(queryExpl.getValue(), ffsqExpl.getValue(), 0f); assertEquals(queryExpl.getDescription(), ffsqExpl.getDetails()[0].getDescription()); ffsq = new FiltersFunctionScoreQuery(query, ScoreMode.SUM, new FilterFunction[0], Float.POSITIVE_INFINITY, 10f, CombineFunction.MULTIPLY); ffsqExpl = searcher.explain(ffsq, 0); assertFalse(ffsqExpl.isMatch()); assertEquals("Score value is too low, expected at least 10.0 but got 1.0", ffsqExpl.getDescription()); } public void testPropagatesApproximations() throws IOException { Query query = new RandomApproximationQuery(new MatchAllDocsQuery(), random()); IndexSearcher searcher = newSearcher(reader); searcher.setQueryCache(null); // otherwise we could get a cached entry that does not have approximations FunctionScoreQuery fsq = new FunctionScoreQuery(query, null, null, null, Float.POSITIVE_INFINITY); for (boolean needsScores : new boolean[] {true, false}) { Weight weight = searcher.createWeight(fsq, needsScores, 1f); Scorer scorer = weight.scorer(reader.leaves().get(0)); assertNotNull(scorer.twoPhaseIterator()); } FiltersFunctionScoreQuery ffsq = new FiltersFunctionScoreQuery(query, ScoreMode.SUM, new FilterFunction[0], Float.POSITIVE_INFINITY, null, CombineFunction.MULTIPLY); for (boolean needsScores : new boolean[] {true, false}) { Weight weight = searcher.createWeight(ffsq, needsScores, 1f); Scorer scorer = weight.scorer(reader.leaves().get(0)); assertNotNull(scorer.twoPhaseIterator()); } } public void testFunctionScoreHashCodeAndEquals() { Float minScore = randomBoolean() ? null : 1.0f; CombineFunction combineFunction = randomFrom(CombineFunction.values()); float maxBoost = randomBoolean() ? Float.POSITIVE_INFINITY : randomFloat(); ScoreFunction function = randomBoolean() ? null : new DummyScoreFunction(combineFunction); FunctionScoreQuery q = new FunctionScoreQuery(new TermQuery(new Term("foo", "bar")), function, minScore, combineFunction, maxBoost); FunctionScoreQuery q1 = new FunctionScoreQuery(new TermQuery(new Term("foo", "bar")), function, minScore, combineFunction, maxBoost); assertEquals(q, q); assertEquals(q.hashCode(), q.hashCode()); assertEquals(q, q1); assertEquals(q.hashCode(), q1.hashCode()); FunctionScoreQuery diffQuery = new FunctionScoreQuery(new TermQuery(new Term("foo", "baz")), function, minScore, combineFunction, maxBoost); FunctionScoreQuery diffMinScore = new FunctionScoreQuery(q.getSubQuery(), function, minScore == null ? 1.0f : null, combineFunction, maxBoost); ScoreFunction otherFunciton = function == null ? new DummyScoreFunction(combineFunction) : null; FunctionScoreQuery diffFunction = new FunctionScoreQuery(q.getSubQuery(), otherFunciton, minScore, combineFunction, maxBoost); FunctionScoreQuery diffMaxBoost = new FunctionScoreQuery(new TermQuery(new Term("foo", "bar")), function, minScore, combineFunction, maxBoost == 1.0f ? 0.9f : 1.0f); FunctionScoreQuery[] queries = new FunctionScoreQuery[] { diffFunction, diffMinScore, diffQuery, q, diffMaxBoost }; final int numIters = randomIntBetween(20, 100); for (int i = 0; i < numIters; i++) { FunctionScoreQuery left = randomFrom(queries); FunctionScoreQuery right = randomFrom(queries); if (left == right) { assertEquals(left, right); assertEquals(left.hashCode(), right.hashCode()); } else { assertNotEquals(left + " == " + right, left, right); } } } public void testFilterFunctionScoreHashCodeAndEquals() { ScoreMode mode = randomFrom(ScoreMode.values()); CombineFunction combineFunction = randomFrom(CombineFunction.values()); ScoreFunction scoreFunction = new DummyScoreFunction(combineFunction); Float minScore = randomBoolean() ? null : 1.0f; Float maxBoost = randomBoolean() ? Float.POSITIVE_INFINITY : randomFloat(); FilterFunction function = new FilterFunction(new TermQuery(new Term("filter", "query")), scoreFunction); FiltersFunctionScoreQuery q = new FiltersFunctionScoreQuery(new TermQuery(new Term("foo", "bar")), mode, new FilterFunction[] { function }, maxBoost, minScore, combineFunction); FiltersFunctionScoreQuery q1 = new FiltersFunctionScoreQuery(new TermQuery(new Term("foo", "bar")), mode, new FilterFunction[] { function }, maxBoost, minScore, combineFunction); assertEquals(q, q); assertEquals(q.hashCode(), q.hashCode()); assertEquals(q, q1); assertEquals(q.hashCode(), q1.hashCode()); FiltersFunctionScoreQuery diffCombineFunc = new FiltersFunctionScoreQuery(new TermQuery(new Term("foo", "bar")), mode, new FilterFunction[] { function }, maxBoost, minScore, combineFunction == CombineFunction.AVG ? CombineFunction.MAX : CombineFunction.AVG); FiltersFunctionScoreQuery diffQuery = new FiltersFunctionScoreQuery(new TermQuery(new Term("foo", "baz")), mode, new FilterFunction[] { function }, maxBoost, minScore, combineFunction); FiltersFunctionScoreQuery diffMode = new FiltersFunctionScoreQuery(new TermQuery(new Term("foo", "bar")), mode == ScoreMode.AVG ? ScoreMode.FIRST : ScoreMode.AVG, new FilterFunction[] { function }, maxBoost, minScore, combineFunction); FiltersFunctionScoreQuery diffMaxBoost = new FiltersFunctionScoreQuery(new TermQuery(new Term("foo", "bar")), mode, new FilterFunction[] { function }, maxBoost == 1.0f ? 0.9f : 1.0f, minScore, combineFunction); FiltersFunctionScoreQuery diffMinScore = new FiltersFunctionScoreQuery(new TermQuery(new Term("foo", "bar")), mode, new FilterFunction[] { function }, maxBoost, minScore == null ? 0.9f : null, combineFunction); FilterFunction otherFunc = new FilterFunction(new TermQuery(new Term("filter", "other_query")), scoreFunction); FiltersFunctionScoreQuery diffFunc = new FiltersFunctionScoreQuery(new TermQuery(new Term("foo", "bar")), mode, randomBoolean() ? new FilterFunction[] { function, otherFunc } : new FilterFunction[] { otherFunc }, maxBoost, minScore, combineFunction); FiltersFunctionScoreQuery[] queries = new FiltersFunctionScoreQuery[] { diffQuery, diffMaxBoost, diffMinScore, diffMode, diffFunc, q, diffCombineFunc }; final int numIters = randomIntBetween(20, 100); for (int i = 0; i < numIters; i++) { FiltersFunctionScoreQuery left = randomFrom(queries); FiltersFunctionScoreQuery right = randomFrom(queries); if (left == right) { assertEquals(left, right); assertEquals(left.hashCode(), right.hashCode()); } else { assertNotEquals(left + " == " + right, left, right); } } } public void testExplanationAndScoreEqualsEvenIfNoFunctionMatches() throws IOException { IndexSearcher localSearcher = newSearcher(reader); ScoreMode scoreMode = randomFrom(new ScoreMode[]{ScoreMode.SUM, ScoreMode.AVG, ScoreMode.FIRST, ScoreMode.MIN, ScoreMode.MAX, ScoreMode.MULTIPLY}); CombineFunction combineFunction = randomFrom(new CombineFunction[]{CombineFunction.SUM, CombineFunction.AVG, CombineFunction.MIN, CombineFunction.MAX, CombineFunction.MULTIPLY, CombineFunction.REPLACE}); // check for document that has no macthing function FiltersFunctionScoreQuery query = new FiltersFunctionScoreQuery(new TermQuery(new Term(FIELD, "out")), scoreMode, new FilterFunction[]{new FilterFunction(new TermQuery(new Term("_uid", "2")), new WeightFactorFunction(10))}, Float.MAX_VALUE, Float.NEGATIVE_INFINITY, combineFunction); TopDocs searchResult = localSearcher.search(query, 1); Explanation explanation = localSearcher.explain(query, searchResult.scoreDocs[0].doc); assertThat(searchResult.scoreDocs[0].score, equalTo(explanation.getValue())); // check for document that has a matching function query = new FiltersFunctionScoreQuery(new TermQuery(new Term(FIELD, "out")), scoreMode, new FilterFunction[]{new FilterFunction(new TermQuery(new Term("_uid", "1")), new WeightFactorFunction(10))}, Float.MAX_VALUE, Float.NEGATIVE_INFINITY, combineFunction); searchResult = localSearcher.search(query, 1); explanation = localSearcher.explain(query, searchResult.scoreDocs[0].doc); assertThat(searchResult.scoreDocs[0].score, equalTo(explanation.getValue())); } public void testWeightFactorNeedsScore() { for (boolean needsScore : new boolean[] {true, false}) { WeightFactorFunction function = new WeightFactorFunction(10.0f, new ScoreFunction(CombineFunction.REPLACE) { @Override public LeafScoreFunction getLeafScoreFunction(LeafReaderContext ctx) throws IOException { return null; } @Override public boolean needsScores() { return needsScore; } @Override protected boolean doEquals(ScoreFunction other) { return false; } @Override protected int doHashCode() { return 0; } }); assertEquals(needsScore, function.needsScores()); } } private static class DummyScoreFunction extends ScoreFunction { protected DummyScoreFunction(CombineFunction scoreCombiner) { super(scoreCombiner); } @Override public LeafScoreFunction getLeafScoreFunction(LeafReaderContext ctx) throws IOException { return null; } @Override public boolean needsScores() { return false; } @Override protected boolean doEquals(ScoreFunction other) { return other == this; } @Override protected int doHashCode() { return 0; }; } }