diff --git a/docs/reference/search/request/rescore.asciidoc b/docs/reference/search/request/rescore.asciidoc index 50112648772b1..cadf9e8f7b359 100644 --- a/docs/reference/search/request/rescore.asciidoc +++ b/docs/reference/search/request/rescore.asciidoc @@ -79,3 +79,55 @@ for <> rescores. |`max` |Take the max of original score and the rescore query score. |`min` |Take the min of the original score and the rescore query score. |======================================================================= + +==== Multiple Rescores + +It is also possible to execute multiple rescores in sequence: +[source,js] +-------------------------------------------------- +curl -s -XPOST 'localhost:9200/_search' -d '{ + "query" : { + "match" : { + "field1" : { + "operator" : "or", + "query" : "the quick brown", + "type" : "boolean" + } + } + }, + "rescore" : [ { + "window_size" : 100, + "query" : { + "rescore_query" : { + "match" : { + "field1" : { + "query" : "the quick brown", + "type" : "phrase", + "slop" : 2 + } + } + }, + "query_weight" : 0.7, + "rescore_query_weight" : 1.2 + } + }, { + "window_size" : 10, + "query" : { + "score_mode": "multiply", + "rescore_query" : { + "function_score" : { + "script_score": { + "script": "log10(doc['numeric'].value + 2)" + } + } + } + } + } ] +} +' +-------------------------------------------------- + +The first one gets the results of the query then the second one gets the +results of the first, etc. The second rescore will "see" the sorting done +by the first rescore so it is possible to use a large window on the first +rescore to pull documents into a smaller window for the second rescore. diff --git a/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java b/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java index 59ddff5a6ad76..ed9939998d197 100644 --- a/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java +++ b/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java @@ -126,13 +126,10 @@ protected ExplainResponse shardOperation(ExplainRequest request, int shardId) th context.parsedQuery(indexService.queryParserService().parseQuery(request.source())); context.preProcess(); int topLevelDocId = result.docIdAndVersion().docId + result.docIdAndVersion().context.docBase; - Explanation explanation; - if (context.rescore() != null) { - RescoreSearchContext ctx = context.rescore(); + Explanation explanation = context.searcher().explain(context.query(), topLevelDocId); + for (RescoreSearchContext ctx : context.rescore()) { Rescorer rescorer = ctx.rescorer(); - explanation = rescorer.explain(topLevelDocId, context, ctx); - } else { - explanation = context.searcher().explain(context.query(), topLevelDocId); + explanation = rescorer.explain(topLevelDocId, context, ctx, explanation); } if (request.fields() != null || (request.fetchSourceContext() != null && request.fetchSourceContext().fetchSource())) { // Advantage is that we're not opening a second searcher to retrieve the _source. Also diff --git a/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java b/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java index 8fe4c2df66a3f..8e45c9c7e3781 100644 --- a/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java +++ b/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java @@ -794,13 +794,66 @@ public SearchRequestBuilder addSuggestion(SuggestBuilder.SuggestionBuilder su return this; } + /** + * Clears all rescorers on the builder and sets the first one. To use multiple rescore windows use + * {@link #addRescorer(org.elasticsearch.search.rescore.RescoreBuilder.Rescorer, int)}. + * @param rescorer rescorer configuration + * @return this for chaining + */ public SearchRequestBuilder setRescorer(RescoreBuilder.Rescorer rescorer) { - rescoreBuilder().rescorer(rescorer); + sourceBuilder().clearRescorers(); + return addRescorer(rescorer); + } + + /** + * Clears all rescorers on the builder and sets the first one. To use multiple rescore windows use + * {@link #addRescorer(org.elasticsearch.search.rescore.RescoreBuilder.Rescorer, int)}. + * @param rescorer rescorer configuration + * @param window rescore window + * @return this for chaining + */ + public SearchRequestBuilder setRescorer(RescoreBuilder.Rescorer rescorer, int window) { + sourceBuilder().clearRescorers(); + return addRescorer(rescorer, window); + } + + /** + * Adds a new rescorer. + * @param rescorer rescorer configuration + * @return this for chaining + */ + public SearchRequestBuilder addRescorer(RescoreBuilder.Rescorer rescorer) { + sourceBuilder().addRescorer(new RescoreBuilder().rescorer(rescorer)); return this; } + /** + * Adds a new rescorer. + * @param rescorer rescorer configuration + * @param window rescore window + * @return this for chaining + */ + public SearchRequestBuilder addRescorer(RescoreBuilder.Rescorer rescorer, int window) { + sourceBuilder().addRescorer(new RescoreBuilder().rescorer(rescorer).windowSize(window)); + return this; + } + + /** + * Clears all rescorers from the builder. + * @return this for chaining + */ + public SearchRequestBuilder clearRescorers() { + sourceBuilder().clearRescorers(); + return this; + } + + /** + * Sets the rescore window for all rescorers that don't specify a window when added. + * @param window rescore window + * @return this for chaining + */ public SearchRequestBuilder setRescoreWindow(int window) { - rescoreBuilder().windowSize(window); + sourceBuilder().defaultRescoreWindowSize(window); return this; } @@ -980,9 +1033,4 @@ private HighlightBuilder highlightBuilder() { private SuggestBuilder suggestBuilder() { return sourceBuilder().suggest(); } - - private RescoreBuilder rescoreBuilder() { - return sourceBuilder().rescore(); - } - } diff --git a/src/main/java/org/elasticsearch/percolator/PercolateContext.java b/src/main/java/org/elasticsearch/percolator/PercolateContext.java index 628ca33796649..a728d4d2a9720 100644 --- a/src/main/java/org/elasticsearch/percolator/PercolateContext.java +++ b/src/main/java/org/elasticsearch/percolator/PercolateContext.java @@ -399,12 +399,12 @@ public void suggest(SuggestionSearchContext suggest) { } @Override - public RescoreSearchContext rescore() { + public List rescore() { throw new UnsupportedOperationException(); } @Override - public void rescore(RescoreSearchContext rescore) { + public void addRescore(RescoreSearchContext rescore) { throw new UnsupportedOperationException(); } diff --git a/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index fc4f4a8a26717..4f7ff68f39390 100644 --- a/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -48,6 +48,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -114,7 +115,8 @@ public static HighlightBuilder highlight() { private SuggestBuilder suggestBuilder; - private RescoreBuilder rescoreBuilder; + private List rescoreBuilders; + private Integer defaultRescoreWindowSize; private ObjectFloatOpenHashMap indexBoost = null; @@ -438,6 +440,16 @@ public SearchSourceBuilder aggregations(XContentBuilder facets) { return aggregations(facets.bytes()); } + /** + * Set the rescore window size for rescores that don't specify their window. + * @param defaultRescoreWindowSize + * @return + */ + public SearchSourceBuilder defaultRescoreWindowSize(int defaultRescoreWindowSize) { + this.defaultRescoreWindowSize = defaultRescoreWindowSize; + return this; + } + /** * Sets a raw (xcontent / json) addAggregation. */ @@ -473,11 +485,17 @@ public SuggestBuilder suggest() { return suggestBuilder; } - public RescoreBuilder rescore() { - if (rescoreBuilder == null) { - rescoreBuilder = new RescoreBuilder(); + public SearchSourceBuilder addRescorer(RescoreBuilder rescoreBuilder) { + if (rescoreBuilders == null) { + rescoreBuilders = new ArrayList(); } - return rescoreBuilder; + rescoreBuilders.add(rescoreBuilder); + return this; + } + + public SearchSourceBuilder clearRescorers() { + rescoreBuilders = null; + return this; } /** @@ -898,8 +916,36 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws suggestBuilder.toXContent(builder, params); } - if (rescoreBuilder != null) { - rescoreBuilder.toXContent(builder, params); + if (rescoreBuilders != null) { + // Strip empty rescoreBuilders from the request + Iterator itr = rescoreBuilders.iterator(); + while (itr.hasNext()) { + if (itr.next().isEmpty()) { + itr.remove(); + } + } + + // Now build the request taking care to skip empty lists and only send the object form + // if there is just one builder. + if (rescoreBuilders.size() == 1) { + builder.startObject("rescore"); + rescoreBuilders.get(0).toXContent(builder, params); + if (rescoreBuilders.get(0).windowSize() == null && defaultRescoreWindowSize != null) { + builder.field("window_size", defaultRescoreWindowSize); + } + builder.endObject(); + } else if (!rescoreBuilders.isEmpty()) { + builder.startArray("rescore"); + for (RescoreBuilder rescoreBuilder : rescoreBuilders) { + builder.startObject(); + rescoreBuilder.toXContent(builder, params); + if (rescoreBuilder.windowSize() == null && defaultRescoreWindowSize != null) { + builder.field("window_size", defaultRescoreWindowSize); + } + builder.endObject(); + } + builder.endArray(); + } } if (stats != null) { diff --git a/src/main/java/org/elasticsearch/search/dfs/DfsPhase.java b/src/main/java/org/elasticsearch/search/dfs/DfsPhase.java index 635a927523ad9..2be9c8263b659 100644 --- a/src/main/java/org/elasticsearch/search/dfs/DfsPhase.java +++ b/src/main/java/org/elasticsearch/search/dfs/DfsPhase.java @@ -32,6 +32,7 @@ import org.elasticsearch.search.SearchParseElement; import org.elasticsearch.search.SearchPhase; import org.elasticsearch.search.internal.SearchContext; +import org.elasticsearch.search.rescore.RescoreSearchContext; import java.util.AbstractSet; import java.util.Collection; @@ -70,8 +71,8 @@ public void execute(SearchContext context) { termsSet.clear(); } context.query().extractTerms(new DelegateSet(termsSet)); - if (context.rescore() != null) { - context.rescore().rescorer().extractTerms(context, context.rescore(), new DelegateSet(termsSet)); + for (RescoreSearchContext rescoreContext : context.rescore()) { + rescoreContext.rescorer().extractTerms(context, rescoreContext, new DelegateSet(termsSet)); } Term[] terms = termsSet.toArray(Term.class); diff --git a/src/main/java/org/elasticsearch/search/fetch/explain/ExplainFetchSubPhase.java b/src/main/java/org/elasticsearch/search/fetch/explain/ExplainFetchSubPhase.java index 2697f5d1f1828..02bdfbe3cd7a4 100644 --- a/src/main/java/org/elasticsearch/search/fetch/explain/ExplainFetchSubPhase.java +++ b/src/main/java/org/elasticsearch/search/fetch/explain/ExplainFetchSubPhase.java @@ -19,7 +19,6 @@ package org.elasticsearch.search.fetch.explain; import com.google.common.collect.ImmutableMap; - import org.apache.lucene.search.Explanation; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.search.SearchParseElement; @@ -28,7 +27,6 @@ import org.elasticsearch.search.internal.InternalSearchHit; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.search.rescore.RescoreSearchContext; -import org.elasticsearch.search.rescore.Rescorer; import java.io.IOException; import java.util.Map; @@ -61,14 +59,10 @@ public boolean hitExecutionNeeded(SearchContext context) { public void hitExecute(SearchContext context, HitContext hitContext) throws ElasticsearchException { try { final int topLevelDocId = hitContext.hit().docId(); - Explanation explanation; + Explanation explanation = context.searcher().explain(context.query(), topLevelDocId); - if (context.rescore() != null) { - RescoreSearchContext ctx = context.rescore(); - Rescorer rescorer = ctx.rescorer(); - explanation = rescorer.explain(topLevelDocId, context, ctx); - } else { - explanation = context.searcher().explain(context.query(), topLevelDocId); + for (RescoreSearchContext rescore : context.rescore()) { + explanation = rescore.rescorer().explain(topLevelDocId, context, rescore, explanation); } // we use the top level doc id, since we work with the top level searcher hitContext.hit().explanation(explanation); diff --git a/src/main/java/org/elasticsearch/search/internal/DefaultSearchContext.java b/src/main/java/org/elasticsearch/search/internal/DefaultSearchContext.java index b860b20e1cb16..8f4f98d6f230c 100644 --- a/src/main/java/org/elasticsearch/search/internal/DefaultSearchContext.java +++ b/src/main/java/org/elasticsearch/search/internal/DefaultSearchContext.java @@ -70,6 +70,7 @@ import org.elasticsearch.search.suggest.SuggestionSearchContext; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -160,7 +161,7 @@ public class DefaultSearchContext extends SearchContext { private SuggestionSearchContext suggest; - private RescoreSearchContext rescore; + private List rescore; private SearchLookup searchLookup; @@ -342,12 +343,18 @@ public void suggest(SuggestionSearchContext suggest) { this.suggest = suggest; } - public RescoreSearchContext rescore() { - return this.rescore; + public List rescore() { + if (rescore == null) { + return Collections.emptyList(); + } + return rescore; } - public void rescore(RescoreSearchContext rescore) { - this.rescore = rescore; + public void addRescore(RescoreSearchContext rescore) { + if (this.rescore == null) { + this.rescore = new ArrayList(); + } + this.rescore.add(rescore); } public boolean hasFieldDataFields() { diff --git a/src/main/java/org/elasticsearch/search/internal/SearchContext.java b/src/main/java/org/elasticsearch/search/internal/SearchContext.java index 6ec1cfbd9ac12..0ebcd32fae45e 100644 --- a/src/main/java/org/elasticsearch/search/internal/SearchContext.java +++ b/src/main/java/org/elasticsearch/search/internal/SearchContext.java @@ -134,11 +134,11 @@ public static SearchContext current() { public abstract void suggest(SuggestionSearchContext suggest); /** - * @return the rescore context or null if rescoring wasn't specified or isn't supported + * @return list of all rescore contexts. empty if there aren't any. */ - public abstract RescoreSearchContext rescore(); + public abstract List rescore(); - public abstract void rescore(RescoreSearchContext rescore); + public abstract void addRescore(RescoreSearchContext rescore); public abstract boolean hasFieldDataFields(); diff --git a/src/main/java/org/elasticsearch/search/query/QueryPhase.java b/src/main/java/org/elasticsearch/search/query/QueryPhase.java index 0e6aacd00a87f..0abe7286a59be 100644 --- a/src/main/java/org/elasticsearch/search/query/QueryPhase.java +++ b/src/main/java/org/elasticsearch/search/query/QueryPhase.java @@ -33,6 +33,7 @@ import org.elasticsearch.search.internal.ContextIndexSearcher; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.search.rescore.RescorePhase; +import org.elasticsearch.search.rescore.RescoreSearchContext; import org.elasticsearch.search.sort.SortParseElement; import org.elasticsearch.search.sort.TrackScoresParseElement; import org.elasticsearch.search.suggest.SuggestPhase; @@ -115,9 +116,9 @@ public void execute(SearchContext searchContext) throws QueryPhaseExecutionExcep topDocs = searchContext.searcher().search(query, null, numDocs, searchContext.sort(), searchContext.trackScores(), searchContext.trackScores()); } else { - if (searchContext.rescore() != null) { - rescore = true; - numDocs = Math.max(searchContext.rescore().window(), numDocs); + rescore = !searchContext.rescore().isEmpty(); + for (RescoreSearchContext rescoreContext : searchContext.rescore()) { + numDocs = Math.max(rescoreContext.window(), numDocs); } topDocs = searchContext.searcher().search(query, numDocs); } diff --git a/src/main/java/org/elasticsearch/search/rescore/QueryRescorer.java b/src/main/java/org/elasticsearch/search/rescore/QueryRescorer.java index b8e26a86dd590..5583d03b92d8a 100644 --- a/src/main/java/org/elasticsearch/search/rescore/QueryRescorer.java +++ b/src/main/java/org/elasticsearch/search/rescore/QueryRescorer.java @@ -108,32 +108,32 @@ public String name() { @Override public void rescore(TopDocs topDocs, SearchContext context, RescoreSearchContext rescoreContext) throws IOException { assert rescoreContext != null; - QueryRescoreContext rescore = ((QueryRescoreContext) rescoreContext); - TopDocs queryTopDocs = context.queryResult().topDocs(); - if (queryTopDocs == null || queryTopDocs.totalHits == 0 || queryTopDocs.scoreDocs.length == 0) { + if (topDocs == null || topDocs.totalHits == 0 || topDocs.scoreDocs.length == 0) { return; } + QueryRescoreContext rescore = (QueryRescoreContext) rescoreContext; ContextIndexSearcher searcher = context.searcher(); - topDocs = searcher.search(rescore.query(), new TopDocsFilter(queryTopDocs), queryTopDocs.scoreDocs.length); - context.queryResult().topDocs(merge(queryTopDocs, topDocs, rescore)); + TopDocsFilter filter = new TopDocsFilter(topDocs, rescoreContext.window()); + TopDocs rescored = searcher.search(rescore.query(), filter, rescoreContext.window()); + context.queryResult().topDocs(merge(topDocs, rescored, rescore)); } @Override - public Explanation explain(int topLevelDocId, SearchContext context, RescoreSearchContext rescoreContext) throws IOException { - QueryRescoreContext rescore = ((QueryRescoreContext) context.rescore()); + public Explanation explain(int topLevelDocId, SearchContext context, RescoreSearchContext rescoreContext, + Explanation sourceExplanation) throws IOException { + QueryRescoreContext rescore = ((QueryRescoreContext) rescoreContext); ContextIndexSearcher searcher = context.searcher(); - Explanation primaryExplain = searcher.explain(context.query(), topLevelDocId); - if (primaryExplain == null) { + if (sourceExplanation == null) { // this should not happen but just in case return new ComplexExplanation(false, 0.0f, "nothing matched"); } Explanation rescoreExplain = searcher.explain(rescore.query(), topLevelDocId); float primaryWeight = rescore.queryWeight(); - ComplexExplanation prim = new ComplexExplanation(primaryExplain.isMatch(), - primaryExplain.getValue() * primaryWeight, + ComplexExplanation prim = new ComplexExplanation(sourceExplanation.isMatch(), + sourceExplanation.getValue() * primaryWeight, "product of:"); - prim.addDetail(primaryExplain); + prim.addDetail(sourceExplanation); prim.addDetail(new Explanation(primaryWeight, "primaryWeight")); if (rescoreExplain != null && rescoreExplain.isMatch()) { float secondaryWeight = rescore.rescoreQueryWeight(); @@ -341,14 +341,14 @@ private static final class TopDocsFilter extends Filter { private final int[] docIds; - public TopDocsFilter(TopDocs topDocs) { - this.docIds = new int[topDocs.scoreDocs.length]; + public TopDocsFilter(TopDocs topDocs, int max) { ScoreDoc[] scoreDocs = topDocs.scoreDocs; - for (int i = 0; i < scoreDocs.length; i++) { + max = Math.min(max, scoreDocs.length); + this.docIds = new int[max]; + for (int i = 0; i < max; i++) { docIds[i] = scoreDocs[i].doc; } Arrays.sort(docIds); - } @Override @@ -411,7 +411,7 @@ public long cost() { @Override public void extractTerms(SearchContext context, RescoreSearchContext rescoreContext, Set termsSet) { - ((QueryRescoreContext) context.rescore()).query().extractTerms(termsSet); + ((QueryRescoreContext) rescoreContext).query().extractTerms(termsSet); } } diff --git a/src/main/java/org/elasticsearch/search/rescore/RescoreBuilder.java b/src/main/java/org/elasticsearch/search/rescore/RescoreBuilder.java index 04af439d05ae5..fde282427d71c 100644 --- a/src/main/java/org/elasticsearch/search/rescore/RescoreBuilder.java +++ b/src/main/java/org/elasticsearch/search/rescore/RescoreBuilder.java @@ -19,12 +19,12 @@ package org.elasticsearch.search.rescore; -import java.io.IOException; - import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.QueryBuilder; +import java.io.IOException; + public class RescoreBuilder implements ToXContent { private Rescorer rescorer; @@ -44,16 +44,20 @@ public RescoreBuilder windowSize(int windowSize) { return this; } + public Integer windowSize() { + return windowSize; + } + + public boolean isEmpty() { + return rescorer == null; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - if (rescorer != null) { - builder.startObject("rescore"); - if (windowSize != null) { - builder.field("window_size", windowSize); - } - rescorer.toXContent(builder, params); - builder.endObject(); + if (windowSize != null) { + builder.field("window_size", windowSize); } + rescorer.toXContent(builder, params); return builder; } diff --git a/src/main/java/org/elasticsearch/search/rescore/RescoreParseElement.java b/src/main/java/org/elasticsearch/search/rescore/RescoreParseElement.java index a1d0e5de1828f..9168c22e6ead7 100644 --- a/src/main/java/org/elasticsearch/search/rescore/RescoreParseElement.java +++ b/src/main/java/org/elasticsearch/search/rescore/RescoreParseElement.java @@ -32,6 +32,16 @@ public class RescoreParseElement implements SearchParseElement { @Override public void parse(XContentParser parser, SearchContext context) throws Exception { + if (parser.currentToken() == XContentParser.Token.START_ARRAY) { + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + parseSingleRescoreContext(parser, context); + } + } else { + parseSingleRescoreContext(parser, context); + } + } + + private void parseSingleRescoreContext(XContentParser parser, SearchContext context) throws Exception { String fieldName = null; RescoreSearchContext rescoreContext = null; Integer windowSize = null; @@ -62,7 +72,7 @@ public void parse(XContentParser parser, SearchContext context) throws Exception if (windowSize != null) { rescoreContext.setWindowSize(windowSize.intValue()); } - context.rescore(rescoreContext); + context.addRescore(rescoreContext); } } diff --git a/src/main/java/org/elasticsearch/search/rescore/RescorePhase.java b/src/main/java/org/elasticsearch/search/rescore/RescorePhase.java index baf4600f0b4da..1b242c058bcce 100644 --- a/src/main/java/org/elasticsearch/search/rescore/RescorePhase.java +++ b/src/main/java/org/elasticsearch/search/rescore/RescorePhase.java @@ -19,9 +19,7 @@ package org.elasticsearch.search.rescore; -import java.io.IOException; -import java.util.Map; - +import com.google.common.collect.ImmutableMap; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; @@ -30,7 +28,8 @@ import org.elasticsearch.search.SearchPhase; import org.elasticsearch.search.internal.SearchContext; -import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.util.Map; /** */ @@ -54,13 +53,13 @@ public void preProcess(SearchContext context) { @Override public void execute(SearchContext context) throws ElasticsearchException { - final RescoreSearchContext ctx = context.rescore(); - final Rescorer rescorer = ctx.rescorer(); try { - rescorer.rescore(context.queryResult().topDocs(), context, ctx); + for (RescoreSearchContext ctx : context.rescore()) { + ctx.rescorer().rescore(context.queryResult().topDocs(), context, ctx); + } } catch (IOException e) { throw new ElasticsearchException("Rescore Phase Failed", e); - } + } } diff --git a/src/main/java/org/elasticsearch/search/rescore/Rescorer.java b/src/main/java/org/elasticsearch/search/rescore/Rescorer.java index 5ff6c567aeb5a..142e7e24dff4d 100644 --- a/src/main/java/org/elasticsearch/search/rescore/Rescorer.java +++ b/src/main/java/org/elasticsearch/search/rescore/Rescorer.java @@ -54,13 +54,15 @@ public interface Rescorer { /** * Executes an {@link Explanation} phase on the rescorer. * - * @param topLevelDocId the global / top-level document ID to explain - * @param context the current {@link SearchContext} - * @param rescoreContext TODO + * @param topLevelDocId the global / top-level document ID to explain + * @param context the explanation for the results being fed to this rescorer + * @param rescoreContext context for this rescorer + * @param sourceExplanation explanation of the source of the documents being fed into this rescore * @return the explain for the given top level document ID. * @throws IOException if an {@link IOException} occurs */ - public Explanation explain(int topLevelDocId, SearchContext context, RescoreSearchContext rescoreContext) throws IOException; + public Explanation explain(int topLevelDocId, SearchContext context, RescoreSearchContext rescoreContext, + Explanation sourceExplanation) throws IOException; /** * Parses the {@link RescoreSearchContext} for this impelementation diff --git a/src/test/java/org/elasticsearch/index/search/child/TestSearchContext.java b/src/test/java/org/elasticsearch/index/search/child/TestSearchContext.java index 6e5a1e1cd0c23..94a8c41e7f9a6 100644 --- a/src/test/java/org/elasticsearch/index/search/child/TestSearchContext.java +++ b/src/test/java/org/elasticsearch/index/search/child/TestSearchContext.java @@ -205,12 +205,12 @@ public void suggest(SuggestionSearchContext suggest) { } @Override - public RescoreSearchContext rescore() { + public List rescore() { return null; } @Override - public void rescore(RescoreSearchContext rescore) { + public void addRescore(RescoreSearchContext rescore) { } @Override diff --git a/src/test/java/org/elasticsearch/search/rescore/QueryRescorerTests.java b/src/test/java/org/elasticsearch/search/rescore/QueryRescorerTests.java index 0cf491ff2c45e..2688d44b742fa 100644 --- a/src/test/java/org/elasticsearch/search/rescore/QueryRescorerTests.java +++ b/src/test/java/org/elasticsearch/search/rescore/QueryRescorerTests.java @@ -21,8 +21,10 @@ +import org.apache.lucene.search.Explanation; import org.apache.lucene.util.English; import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchType; import org.elasticsearch.common.lucene.search.function.CombineFunction; @@ -203,24 +205,8 @@ private static final void assertEquivalentOrSubstringMatch(String query, SearchR @Test // forces QUERY_THEN_FETCH because of https://github.com/elasticsearch/elasticsearch/issues/4829 public void testEquivalence() throws Exception { - client().admin() - .indices() - .prepareCreate("test") - .addMapping( - "type1", - jsonBuilder().startObject().startObject("type1").startObject("properties").startObject("field1") - .field("analyzer", "whitespace").field("type", "string").endObject().endObject().endObject().endObject()) - .setSettings(ImmutableSettings.settingsBuilder().put("index.number_of_shards", between(1, 5)).put("index.number_of_replicas", between(0, 1))).execute().actionGet(); - ensureGreen(); - - int numDocs = atLeast(100); - IndexRequestBuilder[] docs = new IndexRequestBuilder[numDocs]; - for (int i = 0; i < numDocs; i++) { - docs[i] = client().prepareIndex("test", "type1", String.valueOf(i)).setSource("field1", English.intToEnglish(i)); - } + int numDocs = indexRandomNumbers("whitespace", between(1,5)); - indexRandom(true, docs); - ensureGreen(); final int iters = atLeast(50); for (int i = 0; i < iters; i++) { int resultSize = between(5, 30); @@ -335,19 +321,19 @@ public void testExplain() throws Exception { String[] scoreModes = new String[]{ "max", "min", "avg", "total", "multiply", "" }; String[] descriptionModes = new String[]{ "max of:", "min of:", "avg of:", "sum of:", "product of:", "sum of:" }; - for (int i = 0; i < scoreModes.length; i++) { - QueryRescorer rescoreQuery = RescoreBuilder.queryRescorer(QueryBuilders.matchQuery("field1", "the quick brown").boost(4.0f)) + for (int innerMode = 0; innerMode < scoreModes.length; innerMode++) { + QueryRescorer innerRescoreQuery = RescoreBuilder.queryRescorer(QueryBuilders.matchQuery("field1", "the quick brown").boost(4.0f)) .setQueryWeight(0.5f).setRescoreQueryWeight(0.4f); - if (!"".equals(scoreModes[i])) { - rescoreQuery.setScoreMode(scoreModes[i]); + if (!"".equals(scoreModes[innerMode])) { + innerRescoreQuery.setScoreMode(scoreModes[innerMode]); } SearchResponse searchResponse = client() .prepareSearch() .setSearchType(SearchType.DFS_QUERY_THEN_FETCH) .setQuery(QueryBuilders.matchQuery("field1", "the quick brown").operator(MatchQueryBuilder.Operator.OR)) - .setRescorer(rescoreQuery).setRescoreWindow(5).setExplain(true).execute() + .setRescorer(innerRescoreQuery).setRescoreWindow(5).setExplain(true).execute() .actionGet(); assertHitCount(searchResponse, 3); assertFirstHit(searchResponse, hasId("1")); @@ -355,29 +341,41 @@ public void testExplain() throws Exception { assertThirdHit(searchResponse, hasId("3")); for (int j = 0; j < 3; j++) { - assertThat(searchResponse.getHits().getAt(j).explanation().getDescription(), equalTo(descriptionModes[i])); + assertThat(searchResponse.getHits().getAt(j).explanation().getDescription(), equalTo(descriptionModes[innerMode])); + } + + for (int outerMode = 0; outerMode < scoreModes.length; outerMode++) { + QueryRescorer outerRescoreQuery = RescoreBuilder.queryRescorer(QueryBuilders.matchQuery("field1", "the quick brown") + .boost(4.0f)).setQueryWeight(0.5f).setRescoreQueryWeight(0.4f); + + if (!"".equals(scoreModes[outerMode])) { + outerRescoreQuery.setScoreMode(scoreModes[outerMode]); + } + + searchResponse = client() + .prepareSearch() + .setSearchType(SearchType.DFS_QUERY_THEN_FETCH) + .setQuery(QueryBuilders.matchQuery("field1", "the quick brown").operator(MatchQueryBuilder.Operator.OR)) + .addRescorer(innerRescoreQuery).setRescoreWindow(5) + .addRescorer(outerRescoreQuery).setRescoreWindow(10) + .setExplain(true).get(); + assertHitCount(searchResponse, 3); + assertFirstHit(searchResponse, hasId("1")); + assertSecondHit(searchResponse, hasId("2")); + assertThirdHit(searchResponse, hasId("3")); + + for (int j = 0; j < 3; j++) { + Explanation explanation = searchResponse.getHits().getAt(j).explanation(); + assertThat(explanation.getDescription(), equalTo(descriptionModes[outerMode])); + assertThat(explanation.getDetails()[0].getDetails()[0].getDescription(), equalTo(descriptionModes[innerMode])); + } } } } @Test public void testScoring() throws Exception { - client().admin() - .indices() - .prepareCreate("test") - .addMapping( - "type1", - jsonBuilder().startObject().startObject("type1").startObject("properties").startObject("field1") - .field("index", "not_analyzed").field("type", "string").endObject().endObject().endObject().endObject()) - .setSettings(ImmutableSettings.settingsBuilder().put("index.number_of_shards", between(1,5)).put("index.number_of_replicas", between(0,1))).get(); - int numDocs = atLeast(100); - IndexRequestBuilder[] docs = new IndexRequestBuilder[numDocs]; - for (int i = 0; i < numDocs; i++) { - docs[i] = client().prepareIndex("test", "type1", String.valueOf(i)).setSource("field1", English.intToEnglish(i)); - } - - indexRandom(true, docs); - ensureGreen(); + int numDocs = indexRandomNumbers("keyword", between(1,5)); String[] scoreModes = new String[]{ "max", "min", "avg", "total", "multiply", "" }; float primaryWeight = 1.1f; @@ -461,4 +459,60 @@ public void testScoring() throws Exception { } } } + + @Test + public void testMultipleRescores() throws Exception { + int numDocs = indexRandomNumbers("keyword", 1); + QueryRescorer eightIsGreat = RescoreBuilder.queryRescorer( + QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("field1", English.intToEnglish(8))).boostMode(CombineFunction.REPLACE) + .add(ScoreFunctionBuilders.scriptFunction("1000.0f"))).setScoreMode("total"); + QueryRescorer sevenIsBetter = RescoreBuilder.queryRescorer( + QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("field1", English.intToEnglish(7))).boostMode(CombineFunction.REPLACE) + .add(ScoreFunctionBuilders.scriptFunction("10000.0f"))).setScoreMode("total"); + + // First set the rescore window large enough that both rescores take effect + SearchRequestBuilder request = client().prepareSearch().setRescoreWindow(numDocs); + request.addRescorer(eightIsGreat).addRescorer(sevenIsBetter); + SearchResponse response = request.get(); + assertFirstHit(response, hasId("7")); + assertSecondHit(response, hasId("8")); + + // Now squash the second rescore window so it never gets to see a seven + response = request.setSize(1).clearRescorers().addRescorer(eightIsGreat).addRescorer(sevenIsBetter, 1).get(); + assertFirstHit(response, hasId("8")); + // We have no idea what the second hit will be because we didn't get a chance to look for seven + + // Now use one rescore to drag the number we're looking for into the window of another + QueryRescorer ninetyIsGood = RescoreBuilder.queryRescorer( + QueryBuilders.functionScoreQuery(QueryBuilders.queryString("*ninety*")).boostMode(CombineFunction.REPLACE) + .add(ScoreFunctionBuilders.scriptFunction("1000.0f"))).setScoreMode("total"); + QueryRescorer oneToo = RescoreBuilder.queryRescorer( + QueryBuilders.functionScoreQuery(QueryBuilders.queryString("*one*")).boostMode(CombineFunction.REPLACE) + .add(ScoreFunctionBuilders.scriptFunction("1000.0f"))).setScoreMode("total"); + request.clearRescorers().addRescorer(ninetyIsGood).addRescorer(oneToo, 10); + response = request.setSize(2).get(); + assertFirstHit(response, hasId("91")); + assertFirstHit(response, hasScore(2001.0f)); + assertSecondHit(response, hasScore(1001.0f)); // Not sure which one it is but it is ninety something + } + + private int indexRandomNumbers(String analyzer, int shards) throws Exception { + client().admin() + .indices() + .prepareCreate("test") + .addMapping( + "type1", + jsonBuilder().startObject().startObject("type1").startObject("properties").startObject("field1") + .field("analyzer", analyzer).field("type", "string").endObject().endObject().endObject().endObject()) + .setSettings(ImmutableSettings.settingsBuilder().put("index.number_of_shards", shards).put("index.number_of_replicas", between(0,1))).get(); + int numDocs = atLeast(100); + IndexRequestBuilder[] docs = new IndexRequestBuilder[numDocs]; + for (int i = 0; i < numDocs; i++) { + docs[i] = client().prepareIndex("test", "type1", String.valueOf(i)).setSource("field1", English.intToEnglish(i)); + } + + indexRandom(true, docs); + ensureGreen(); + return numDocs; + } }