Support multiple rescores

Detects if rescores arrive as an array instead of a plain object.  If so
then parse each element of the array as a separate rescore to be executed
one after another.  It looks like this:
   "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)"
               }
            }
         }
      }
   } ]

Rescores as a single object are still supported.

Closes #4748
This commit is contained in:
Nik Everett 2014-01-15 17:58:22 -05:00 committed by Adrien Grand
parent 37f80c8d80
commit 93a8e80aff
17 changed files with 339 additions and 124 deletions

View File

@ -79,3 +79,55 @@ for <<query-dsl-function-score-query,`function query`>> 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.

View File

@ -126,13 +126,10 @@ public class TransportExplainAction extends TransportShardSingleOperationAction<
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

View File

@ -794,13 +794,66 @@ public class SearchRequestBuilder extends ActionRequestBuilder<SearchRequest, Se
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 @@ public class SearchRequestBuilder extends ActionRequestBuilder<SearchRequest, Se
private SuggestBuilder suggestBuilder() {
return sourceBuilder().suggest();
}
private RescoreBuilder rescoreBuilder() {
return sourceBuilder().rescore();
}
}

View File

@ -399,12 +399,12 @@ public class PercolateContext extends SearchContext {
}
@Override
public RescoreSearchContext rescore() {
public List<RescoreSearchContext> rescore() {
throw new UnsupportedOperationException();
}
@Override
public void rescore(RescoreSearchContext rescore) {
public void addRescore(RescoreSearchContext rescore) {
throw new UnsupportedOperationException();
}

View File

@ -48,6 +48,7 @@ import org.elasticsearch.search.suggest.SuggestBuilder;
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 class SearchSourceBuilder implements ToXContent {
private SuggestBuilder suggestBuilder;
private RescoreBuilder rescoreBuilder;
private List<RescoreBuilder> rescoreBuilders;
private Integer defaultRescoreWindowSize;
private ObjectFloatOpenHashMap<String> indexBoost = null;
@ -438,6 +440,16 @@ public class SearchSourceBuilder implements ToXContent {
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 class SearchSourceBuilder implements ToXContent {
return suggestBuilder;
}
public RescoreBuilder rescore() {
if (rescoreBuilder == null) {
rescoreBuilder = new RescoreBuilder();
public SearchSourceBuilder addRescorer(RescoreBuilder rescoreBuilder) {
if (rescoreBuilders == null) {
rescoreBuilders = new ArrayList<RescoreBuilder>();
}
return rescoreBuilder;
rescoreBuilders.add(rescoreBuilder);
return this;
}
public SearchSourceBuilder clearRescorers() {
rescoreBuilders = null;
return this;
}
/**
@ -898,8 +916,36 @@ public class SearchSourceBuilder implements ToXContent {
suggestBuilder.toXContent(builder, params);
}
if (rescoreBuilder != null) {
rescoreBuilder.toXContent(builder, params);
if (rescoreBuilders != null) {
// Strip empty rescoreBuilders from the request
Iterator<RescoreBuilder> 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) {

View File

@ -32,6 +32,7 @@ import org.elasticsearch.common.collect.HppcMaps;
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 class DfsPhase implements SearchPhase {
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);

View File

@ -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.fetch.FetchSubPhase;
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 class ExplainFetchSubPhase implements FetchSubPhase {
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);

View File

@ -70,6 +70,7 @@ import org.elasticsearch.search.scan.ScanContext;
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<RescoreSearchContext> rescore;
private SearchLookup searchLookup;
@ -342,12 +343,18 @@ public class DefaultSearchContext extends SearchContext {
this.suggest = suggest;
}
public RescoreSearchContext rescore() {
return this.rescore;
public List<RescoreSearchContext> 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<RescoreSearchContext>();
}
this.rescore.add(rescore);
}
public boolean hasFieldDataFields() {

View File

@ -134,11 +134,11 @@ public abstract class SearchContext implements Releasable {
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<RescoreSearchContext> rescore();
public abstract void rescore(RescoreSearchContext rescore);
public abstract void addRescore(RescoreSearchContext rescore);
public abstract boolean hasFieldDataFields();

View File

@ -33,6 +33,7 @@ import org.elasticsearch.search.facet.FacetPhase;
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 class QueryPhase implements SearchPhase {
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);
}

View File

@ -108,32 +108,32 @@ public final class QueryRescorer implements Rescorer {
@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 @@ public final class QueryRescorer implements Rescorer {
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 final class QueryRescorer implements Rescorer {
@Override
public void extractTerms(SearchContext context, RescoreSearchContext rescoreContext, Set<Term> termsSet) {
((QueryRescoreContext) context.rescore()).query().extractTerms(termsSet);
((QueryRescoreContext) rescoreContext).query().extractTerms(termsSet);
}
}

View File

@ -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 class RescoreBuilder implements ToXContent {
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;
}

View File

@ -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 class RescoreParseElement implements SearchParseElement {
if (windowSize != null) {
rescoreContext.setWindowSize(windowSize.intValue());
}
context.rescore(rescoreContext);
context.addRescore(rescoreContext);
}
}

View File

@ -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.SearchParseElement;
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 class RescorePhase extends AbstractComponent implements SearchPhase {
@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);
}
}
}

View File

@ -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

View File

@ -205,12 +205,12 @@ class TestSearchContext extends SearchContext {
}
@Override
public RescoreSearchContext rescore() {
public List<RescoreSearchContext> rescore() {
return null;
}
@Override
public void rescore(RescoreSearchContext rescore) {
public void addRescore(RescoreSearchContext rescore) {
}
@Override

View File

@ -21,8 +21,10 @@ package org.elasticsearch.search.rescore;
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 @@ public class QueryRescorerTests extends ElasticsearchIntegrationTest {
@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 = indexRandomNumbers("whitespace", between(1,5));
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();
final int iters = atLeast(50);
for (int i = 0; i < iters; i++) {
int resultSize = between(5, 30);
@ -335,19 +321,19 @@ public class QueryRescorerTests extends ElasticsearchIntegrationTest {
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 class QueryRescorerTests extends ElasticsearchIntegrationTest {
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 class QueryRescorerTests extends ElasticsearchIntegrationTest {
}
}
}
@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;
}
}