Skip to content

Commit 44211bc

Browse files
SOLR-17018: add QueryLimits support to Learning To Rank rescoring (#2348)
Co-authored-by: Christine Poerschke <[email protected]>
1 parent 5a48126 commit 44211bc

File tree

8 files changed

+178
-12
lines changed

8 files changed

+178
-12
lines changed

solr/CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ Bug Fixes
152152

153153
* SOLR-17198: AffinityPlacementFactory can fail if Shard leadership changes occur while it is collecting metrics.
154154
(Paul McArthur)
155+
156+
* SOLR-17018: Add QueryLimits support to Learning To Rank rescoring.
157+
(Alessandro Benedetti)
155158

156159
* SOLR-14892: Queries with shards.info and shards.tolerant can yield multiple null keys in place of shard names
157160
(Mathieu Marie, David Smiley)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.solr.search;
18+
19+
public class IncompleteRerankingException extends RuntimeException {
20+
21+
public IncompleteRerankingException() {
22+
super();
23+
}
24+
}

solr/core/src/java/org/apache/solr/search/ReRankCollector.java

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -128,22 +128,26 @@ public TopDocs topDocs(int start, int howMany) {
128128
}
129129

130130
ScoreDoc[] mainScoreDocs = mainDocs.scoreDocs;
131-
ScoreDoc[] mainScoreDocsClone =
132-
(reRankScaler != null && reRankScaler.scaleScores())
133-
? deepCloneAndZeroOut(mainScoreDocs)
134-
: null;
131+
boolean zeroOutScores = reRankScaler != null && reRankScaler.scaleScores();
132+
ScoreDoc[] mainScoreDocsClone = deepClone(mainScoreDocs, zeroOutScores);
135133
ScoreDoc[] reRankScoreDocs = new ScoreDoc[Math.min(mainScoreDocs.length, reRankDocs)];
136134
System.arraycopy(mainScoreDocs, 0, reRankScoreDocs, 0, reRankScoreDocs.length);
137135

138136
mainDocs.scoreDocs = reRankScoreDocs;
139137

140138
// If we're scaling scores use the replace rescorer because we just want the re-rank score.
141-
TopDocs rescoredDocs =
142-
reRankScaler != null && reRankScaler.scaleScores()
143-
? reRankScaler
144-
.getReplaceRescorer()
145-
.rescore(searcher, mainDocs, mainDocs.scoreDocs.length)
146-
: reRankQueryRescorer.rescore(searcher, mainDocs, mainDocs.scoreDocs.length);
139+
TopDocs rescoredDocs;
140+
try {
141+
rescoredDocs =
142+
zeroOutScores // previously zero-ed out scores are to be replaced
143+
? reRankScaler
144+
.getReplaceRescorer()
145+
.rescore(searcher, mainDocs, mainDocs.scoreDocs.length)
146+
: reRankQueryRescorer.rescore(searcher, mainDocs, mainDocs.scoreDocs.length);
147+
} catch (IncompleteRerankingException ex) {
148+
mainDocs.scoreDocs = mainScoreDocsClone;
149+
rescoredDocs = mainDocs;
150+
}
147151

148152
// Lower howMany to return if we've collected fewer documents.
149153
howMany = Math.min(howMany, mainScoreDocs.length);
@@ -208,13 +212,15 @@ public TopDocs topDocs(int start, int howMany) {
208212
}
209213
}
210214

211-
private ScoreDoc[] deepCloneAndZeroOut(ScoreDoc[] scoreDocs) {
215+
private ScoreDoc[] deepClone(ScoreDoc[] scoreDocs, boolean zeroOut) {
212216
ScoreDoc[] scoreDocs1 = new ScoreDoc[scoreDocs.length];
213217
for (int i = 0; i < scoreDocs.length; i++) {
214218
ScoreDoc scoreDoc = scoreDocs[i];
215219
if (scoreDoc != null) {
216220
scoreDocs1[i] = new ScoreDoc(scoreDoc.doc, scoreDoc.score);
217-
scoreDoc.score = 0f;
221+
if (zeroOut) {
222+
scoreDoc.score = 0f;
223+
}
218224
}
219225
}
220226
return scoreDocs1;

solr/modules/ltr/src/java/org/apache/solr/ltr/LTRRescorer.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.apache.lucene.search.TotalHits;
3232
import org.apache.lucene.search.Weight;
3333
import org.apache.solr.ltr.interleaving.OriginalRankingLTRScoringQuery;
34+
import org.apache.solr.search.IncompleteRerankingException;
35+
import org.apache.solr.search.QueryLimits;
3436
import org.apache.solr.search.SolrIndexSearcher;
3537

3638
/**
@@ -234,6 +236,13 @@ protected static boolean scoreSingleHit(
234236

235237
scorer.getDocInfo().setOriginalDocScore(hit.score);
236238
hit.score = scorer.score();
239+
if (QueryLimits.getCurrentLimits()
240+
.maybeExitWithPartialResults(
241+
"Learning To Rank rescoring -"
242+
+ " The full reranking didn't complete."
243+
+ " If partial results are tolerated the reranking got reverted and all documents preserved their original score and ranking.")) {
244+
throw new IncompleteRerankingException();
245+
}
237246
if (hitUpto < topN) {
238247
reranked[hitUpto] = hit;
239248
// if the heap is not full, maybe I want to log the features for this
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"name" : "slow",
4+
"class" : "org.apache.solr.ltr.feature.SolrFeature",
5+
"params" : { "q" : "{!func}sleep(1000,999)" }
6+
}
7+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"class": "org.apache.solr.ltr.model.LinearModel",
3+
"name": "slowModel",
4+
"features": [
5+
{
6+
"name": "slow"
7+
}
8+
],
9+
"params": {
10+
"weights": {
11+
"slow": 1
12+
}
13+
}
14+
}

solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTRQParserPlugin.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public static void before() throws Exception {
2929

3030
loadFeatures("features-linear.json");
3131
loadModels("linear-model.json");
32+
33+
loadFeatures("features-slow.json");
34+
loadModels("linear-slow-model.json"); // just a linear model with one feature
3235
}
3336

3437
@AfterClass
@@ -137,4 +140,93 @@ public void ltrNoResultsTest() throws Exception {
137140
query.add("rq", "{!ltr reRankDocs=3 model=6029760550880411648}");
138141
assertJQ("/query" + query.toQueryString(), "/response/numFound/==0");
139142
}
143+
144+
@Test
145+
public void ltr_expensiveFeatureRescoring_shouldTimeOutAndReturnPartialResults()
146+
throws Exception {
147+
/* One SolrFeature is defined: {!func}sleep(1000,999)
148+
* It simulates a slow feature extraction, sleeping for 1000ms and returning 999 as a score when finished
149+
* */
150+
151+
final String solrQuery = "_query_:{!edismax qf='id' v='8^=10 9^=5 7^=3 6^=1'}";
152+
final SolrQuery query = new SolrQuery();
153+
query.setQuery(solrQuery);
154+
query.setFields("id", "score");
155+
query.setRows(4);
156+
query.setTimeAllowed(300);
157+
query.add("fv", "true");
158+
query.add("rq", "{!ltr model=slowModel reRankDocs=3}");
159+
160+
assertJQ(
161+
"/query" + query.toQueryString(),
162+
"/response/numFound/==4",
163+
"/responseHeader/partialResults/==true",
164+
"/responseHeader/partialResultsDetails/=='Limits exceeded! (Learning To Rank rescoring - "
165+
+ "The full reranking didn\\'t complete. "
166+
+ "If partial results are tolerated the reranking got reverted and "
167+
+ "all documents preserved their original score and ranking.)"
168+
+ ": Query limits: [TimeAllowedLimit:LIMIT EXCEEDED]'",
169+
"/response/docs/[0]/id=='8'",
170+
"/response/docs/[0]/score==10.0",
171+
"/response/docs/[1]/id=='9'",
172+
"/response/docs/[1]/score==5.0",
173+
"/response/docs/[2]/id=='7'",
174+
"/response/docs/[2]/score==3.0",
175+
"/response/docs/[3]/id=='6'",
176+
"/response/docs/[3]/score==1.0");
177+
}
178+
179+
@Test
180+
public void ltr_expensiveFeatureRescoringAndPartialResultsNotTolerated_shouldRaiseException()
181+
throws Exception {
182+
/* One SolrFeature is defined: {!func}sleep(1000,999)
183+
* It simulates a slow feature extraction, sleeping for 1000ms and returning 999 as a score when finished
184+
* */
185+
final String solrQuery = "_query_:{!edismax qf='id' v='8^=10 9^=5 7^=3 6^=1'}";
186+
final SolrQuery query = new SolrQuery();
187+
query.setQuery(solrQuery);
188+
query.setFields("id", "score");
189+
query.setRows(4);
190+
query.setTimeAllowed(300);
191+
query.add("partialResults", "false");
192+
query.add("fv", "true");
193+
query.add("rq", "{!ltr model=slowModel reRankDocs=3}");
194+
195+
assertJQ(
196+
"/query" + query.toQueryString(),
197+
"/error/msg=='org.apache.solr.search.QueryLimitsExceededException: Limits exceeded! (Learning To Rank rescoring - "
198+
+ "The full reranking didn\\'t complete. "
199+
+ "If partial results are tolerated the reranking got reverted and all documents preserved their original score and ranking.)"
200+
+ ": Query limits: [TimeAllowedLimit:LIMIT EXCEEDED]'");
201+
}
202+
203+
@Test
204+
public void ltr_expensiveFeatureRescoringWithinTimeAllowed_shouldReturnRerankedResults()
205+
throws Exception {
206+
/* One SolrFeature is defined: {!func}sleep(1000,999)
207+
* It simulates a slow feature extraction, sleeping for 1000ms and returning 999 as a score when finished
208+
* */
209+
210+
final String solrQuery = "_query_:{!edismax qf='id' v='8^=10 9^=5 7^=3 6^=1'}";
211+
final SolrQuery query = new SolrQuery();
212+
query.setQuery(solrQuery);
213+
query.setFields("id", "score");
214+
query.setRows(4);
215+
query.setTimeAllowed(5000);
216+
query.add("fv", "true");
217+
query.add("rq", "{!ltr model=slowModel reRankDocs=3}");
218+
219+
assertJQ(
220+
"/query" + query.toQueryString(),
221+
"/response/numFound/==4",
222+
"/response/docs/[0]/id=='7'",
223+
"/response/docs/[0]/score==999.0",
224+
"/response/docs/[1]/id=='8'",
225+
"/response/docs/[1]/score==999.0",
226+
"/response/docs/[2]/id=='9'",
227+
"/response/docs/[2]/score==999.0",
228+
"/response/docs/[3]/id=='6'",
229+
// original score for the 4th document due to reRankDocs=3 limit
230+
"/response/docs/[3]/score==1.0");
231+
}
140232
}

solr/solr-ref-guide/modules/query-guide/pages/learning-to-rank.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,17 @@ The output will include feature values as a comma-separated list, resembling the
499499
}}
500500
----
501501

502+
=== Running a Rerank Query and Query Limits
503+
504+
Apache Solr allows to define Query Limits to interrupt particularly expensive queries (xref:query-guide:common-query-parameters.adoc#timeallowed-parameter[Time Allowed], xref:query-guide:common-query-parameters.adoc#cpuallowed-parameter[Cpu Allowed]).
505+
506+
If a query limit is exceeded while reranking, the rescoring is aborted and fully reverted.
507+
508+
The original ranked list is returned and the response marked with the responseHeader 'partialResults'.
509+
The details of what limit was exceeded is returned in the responseHeader 'partialResultsDetails'.
510+
511+
See xref:query-guide:common-query-parameters.adoc#partialresults-parameter[Partial Results Parameter] for more details on how to handle partial results.
512+
502513
=== Running a Rerank Query Interleaving Two Models
503514

504515
To rerank the results of a query, interleaving two models (myModelA, myModelB) add the `rq` parameter to your search, passing two models in input, for example:

0 commit comments

Comments
 (0)