Skip to content

Commit 2dc8bfa

Browse files
committed
Rescorer support script. #52338
1 parent c2e8625 commit 2dc8bfa

File tree

4 files changed

+438
-1
lines changed

4 files changed

+438
-1
lines changed

server/src/main/java/org/elasticsearch/search/rescore/RescorerBuilder.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public abstract class RescorerBuilder<RB extends RescorerBuilder<RB>>
3333
protected Integer windowSize;
3434

3535
private static final ParseField WINDOW_SIZE_FIELD = new ParseField("window_size");
36+
private static final ParseField SCRIPT_FIELD = new ParseField("script");
3637

3738
/**
3839
* Construct an empty RescoreBuilder.
@@ -80,7 +81,14 @@ public static RescorerBuilder<?> parseFromXContent(XContentParser parser) throws
8081
throw new ParsingException(parser.getTokenLocation(), "rescore doesn't support [" + fieldName + "]");
8182
}
8283
} else if (token == XContentParser.Token.START_OBJECT) {
83-
rescorer = parser.namedObject(RescorerBuilder.class, fieldName, null);
84+
if (rescorer != null) {
85+
throw new ParsingException(parser.getTokenLocation(), "you can either define [query] or [script], not both.");
86+
}
87+
if (SCRIPT_FIELD.match(fieldName, parser.getDeprecationHandler())) {
88+
rescorer = ScriptRescorerBuilder.fromXContent(parser);
89+
} else {
90+
rescorer = parser.namedObject(RescorerBuilder.class, fieldName, null);
91+
}
8492
} else {
8593
throw new ParsingException(parser.getTokenLocation(), "unexpected token [" + token + "] after [" + fieldName + "]");
8694
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.search.rescore;
10+
11+
import org.apache.lucene.search.Explanation;
12+
import org.apache.lucene.search.IndexSearcher;
13+
import org.apache.lucene.search.Query;
14+
import org.apache.lucene.search.ScoreDoc;
15+
import org.apache.lucene.search.TopDocs;
16+
import org.elasticsearch.index.query.ScriptQueryBuilder;
17+
18+
import java.io.IOException;
19+
import java.util.Arrays;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.Set;
23+
24+
import static java.util.stream.Collectors.toSet;
25+
26+
public class ScriptRescorer implements Rescorer {
27+
28+
public static final Rescorer INSTANCE = new ScriptRescorer();
29+
30+
@Override
31+
public TopDocs rescore(TopDocs topDocs, IndexSearcher searcher, RescoreContext rescoreContext) throws IOException {
32+
33+
assert rescoreContext != null;
34+
if (topDocs == null || topDocs.scoreDocs.length == 0) {
35+
return topDocs;
36+
}
37+
38+
// First take top slice of incoming docs, to be rescored:
39+
TopDocs topNFirstPass = topN(topDocs, rescoreContext.getWindowSize());
40+
41+
// Save doc IDs for which rescoring was applied to be used in score explanation
42+
Set<Integer> topNDocIDs = Collections.unmodifiableSet(
43+
Arrays.stream(topNFirstPass.scoreDocs).map(scoreDoc -> scoreDoc.doc).collect(toSet()));
44+
45+
rescoreContext.setRescoredDocs(topNDocIDs);
46+
47+
final ScriptRescoreContext rescore = (ScriptRescorer.ScriptRescoreContext) rescoreContext;
48+
ScriptRescorerBuilder.ScriptQuery query = rescore.query();
49+
if (rescore.needsScores) {
50+
query.setHits(topNFirstPass.scoreDocs);
51+
}
52+
org.apache.lucene.search.Rescorer rescorer = new org.apache.lucene.search.QueryRescorer(query) {
53+
@Override
54+
protected float combine(float firstPassScore, boolean secondPassMatches, float secondPassScore) {
55+
return secondPassScore;
56+
}
57+
};
58+
59+
// Rescore them:
60+
TopDocs rescored = rescorer.rescore(searcher, topNFirstPass, rescoreContext.getWindowSize());
61+
62+
// Splice back to non-topN hits and resort all of them:
63+
return rescored;
64+
}
65+
66+
@Override
67+
public Explanation explain(int topLevelDocId, IndexSearcher searcher, RescoreContext rescoreContext,
68+
Explanation sourceExplanation) throws IOException {
69+
if (sourceExplanation == null) {
70+
return Explanation.noMatch("nothing matched");
71+
}
72+
ScriptRescorer.ScriptRescoreContext rescore = (ScriptRescorer.ScriptRescoreContext) rescoreContext;
73+
Explanation rescoreExplain = searcher.explain(rescore.query(), topLevelDocId);
74+
Explanation prim;
75+
if (rescoreExplain != null && rescoreExplain.isMatch()) {
76+
prim = Explanation.match(rescoreExplain.getValue().floatValue(),"detail of:",
77+
rescore.needsScores ? Arrays.asList(sourceExplanation, rescoreExplain)
78+
: Arrays.asList(rescoreExplain));
79+
} else {
80+
prim = Explanation.noMatch("Rescore pass did not match", sourceExplanation);
81+
}
82+
return prim;
83+
}
84+
85+
/**
86+
* Returns a new {@link TopDocs} with the topN from the incoming one, or the same TopDocs if the number of hits is already &lt;=
87+
* topN.
88+
*/
89+
private TopDocs topN(TopDocs in, int topN) {
90+
if (in.scoreDocs.length < topN) {
91+
return in;
92+
}
93+
94+
ScoreDoc[] subset = new ScoreDoc[topN];
95+
System.arraycopy(in.scoreDocs, 0, subset, 0, topN);
96+
97+
return new TopDocs(in.totalHits, subset);
98+
}
99+
100+
public static class ScriptRescoreContext extends RescoreContext {
101+
private ScriptRescorerBuilder.ScriptQuery query;
102+
private boolean needsScores;
103+
104+
public ScriptRescoreContext(int windowSize) {
105+
super(windowSize, ScriptRescorer.INSTANCE);
106+
}
107+
108+
public void setQuery(ScriptRescorerBuilder.ScriptQuery query) {
109+
this.query = query;
110+
}
111+
112+
@Override
113+
public List<Query> getQueries() {
114+
return Collections.singletonList(query);
115+
}
116+
117+
public ScriptRescorerBuilder.ScriptQuery query() {
118+
return query;
119+
}
120+
121+
public void setNeedsScores(boolean needsScores) {
122+
this.needsScores = needsScores;
123+
}
124+
}
125+
}

0 commit comments

Comments
 (0)