This is an automated email from the ASF dual-hosted git repository.
mkhludnev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new 05449b5cc98 SOLR-18245: Add reRankCutoff output on LTR queries (#4442)
05449b5cc98 is described below
commit 05449b5cc98f452458c94e148062b36f03a68bb0
Author: Darren Shaw <[email protected]>
AuthorDate: Sat Jun 27 21:27:58 2026 +0100
SOLR-18245: Add reRankCutoff output on LTR queries (#4442)
---
.../unreleased/SOLR-18245-rerank-cutoff-value.yml | 8 +
.../solr/handler/component/QueryComponent.java | 26 +++
.../solr/handler/component/ResponseBuilder.java | 1 +
.../apache/solr/search/AbstractReRankQuery.java | 5 +
.../java/org/apache/solr/search/QueryCommand.java | 11 ++
.../org/apache/solr/search/ReRankCollector.java | 78 ++++++++
.../apache/solr/ltr/search/LTRQParserPlugin.java | 10 +
.../solr/ltr/AbstractLTRSolrCloudTestBase.java | 219 +++++++++++++++++++++
.../org/apache/solr/ltr/TestLTROnSolrCloud.java | 195 +-----------------
.../org/apache/solr/ltr/TestLTRQParserPlugin.java | 80 ++++++++
.../solr/ltr/TestLTRReRankCutoffOnSolrCloud.java | 107 ++++++++++
.../test/org/apache/solr/ltr/TestLTRWithSort.java | 45 +++++
.../query-guide/pages/learning-to-rank.adoc | 52 +++++
13 files changed, 647 insertions(+), 190 deletions(-)
diff --git a/changelog/unreleased/SOLR-18245-rerank-cutoff-value.yml
b/changelog/unreleased/SOLR-18245-rerank-cutoff-value.yml
new file mode 100644
index 00000000000..ad1bf506b78
--- /dev/null
+++ b/changelog/unreleased/SOLR-18245-rerank-cutoff-value.yml
@@ -0,0 +1,8 @@
+title: Optional reRankCutoff field returned in responseHeader. Used to return
main sort value(s) from the lowest ranked document eligible for inclusion in
rerank.
+type: added
+authors:
+ - name: Darren Shaw
+ nick: shawdm
+links:
+ - name: SOLR-18245
+ url: https://issues.apache.org/jira/browse/SOLR-18245
diff --git
a/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
b/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
index 9bc219e6d53..3c8c2a7a5bd 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
@@ -80,6 +80,7 @@ import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.schema.SortableTextField;
+import org.apache.solr.search.AbstractReRankQuery;
import org.apache.solr.search.CursorMark;
import org.apache.solr.search.DocIterator;
import org.apache.solr.search.DocList;
@@ -1028,6 +1029,8 @@ public class QueryComponent extends SearchComponent {
boolean maxHitsTerminatedEarly = false;
long approximateTotalHits = 0;
int failedShardCount = 0;
+ int failedShardCountForReRankCutoff = 0;
+ NamedList<Object> reRankCutoffByShard = null;
for (ShardResponse srsp : sreq.responses) {
SolrDocumentList docs = null;
NamedList<?> responseHeader = null;
@@ -1115,6 +1118,23 @@ public class QueryComponent extends SearchComponent {
rb, srsp, "responseHeader", false));
}
+ final Object shardReRankCutoffValue =
+
responseHeader.get(AbstractReRankQuery.RERANK_CUTOFF_RESPONSE_HEADER_KEY);
+ if (shardReRankCutoffValue != null) {
+ if (reRankCutoffByShard == null) {
+ reRankCutoffByShard = new SimpleOrderedMap<>();
+ }
+ String shard = srsp.getShard();
+ if (StrUtils.isNullOrEmpty(shard)) {
+ shard = srsp.getShardAddress();
+ }
+ if (StrUtils.isNullOrEmpty(shard)) {
+ failedShardCountForReRankCutoff += 1;
+ shard = "unknown_shard_" + failedShardCountForReRankCutoff;
+ }
+ reRankCutoffByShard.add(shard, shardReRankCutoffValue);
+ }
+
final boolean thisResponseIsPartial;
thisResponseIsPartial =
Boolean.TRUE.equals(
@@ -1256,6 +1276,12 @@ public class QueryComponent extends SearchComponent {
SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY,
approximateTotalHits);
}
}
+
+ if (reRankCutoffByShard != null) {
+ rb.rsp
+ .getResponseHeader()
+ .add(AbstractReRankQuery.RERANK_CUTOFF_BY_SHARD_RESPONSE_HEADER_KEY,
reRankCutoffByShard);
+ }
}
protected void setResultIdsAndResponseDocs(
diff --git
a/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java
b/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java
index 413a962683b..aa85f9a69b4 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java
@@ -462,6 +462,7 @@ public class ResponseBuilder {
cmd.setQuery(wrap(getQuery()))
.setFilterList(getFilters())
.setSort(getSortSpec().getSort())
+ .setSortSchemaFields(getSortSpec().getSchemaFields())
.setOffset(getSortSpec().getOffset())
.setLen(getSortSpec().getCount())
.setFlags(getFieldFlags())
diff --git a/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
b/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
index 5c026e55762..2b8f7d87909 100644
--- a/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
+++ b/solr/core/src/java/org/apache/solr/search/AbstractReRankQuery.java
@@ -34,6 +34,11 @@ import
org.apache.solr.handler.component.QueryElevationComponent;
import org.apache.solr.request.SolrRequestInfo;
public abstract class AbstractReRankQuery extends RankQuery {
+ public static final String RERANK_CUTOFF_RESPONSE_HEADER_KEY =
"reRankCutoff";
+ public static final String RERANK_CUTOFF_BY_SHARD_RESPONSE_HEADER_KEY =
"reRankCutoffByShard";
+ public static final String RERANK_CUTOFF_ECHO_REQUEST_CONTEXT_KEY =
+ "solr.rerank.echoReRankCutoff";
+
protected Query mainQuery;
protected final int reRankDocs;
protected final Rescorer reRankQueryRescorer;
diff --git a/solr/core/src/java/org/apache/solr/search/QueryCommand.java
b/solr/core/src/java/org/apache/solr/search/QueryCommand.java
index 81857a7b568..8442217464b 100755
--- a/solr/core/src/java/org/apache/solr/search/QueryCommand.java
+++ b/solr/core/src/java/org/apache/solr/search/QueryCommand.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
+import org.apache.solr.schema.SchemaField;
/**
* A query request command to avoid having to change the method signatures if
we want to pass
@@ -33,6 +34,7 @@ public class QueryCommand {
private boolean isQueryCancellable;
private List<Query> filterList;
private Sort sort;
+ private List<SchemaField> sortSchemaFields;
private int offset;
private int len;
private int supersetMaxDoc;
@@ -108,6 +110,15 @@ public class QueryCommand {
return this;
}
+ public List<SchemaField> getSortSchemaFields() {
+ return sortSchemaFields;
+ }
+
+ public QueryCommand setSortSchemaFields(List<SchemaField> sortSchemaFields) {
+ this.sortSchemaFields = sortSchemaFields;
+ return this;
+ }
+
public int getOffset() {
return offset;
}
diff --git a/solr/core/src/java/org/apache/solr/search/ReRankCollector.java
b/solr/core/src/java/org/apache/solr/search/ReRankCollector.java
index ed97f783ae8..44d89d8ebfd 100644
--- a/solr/core/src/java/org/apache/solr/search/ReRankCollector.java
+++ b/solr/core/src/java/org/apache/solr/search/ReRankCollector.java
@@ -20,11 +20,14 @@ import com.carrotsearch.hppc.IntFloatHashMap;
import com.carrotsearch.hppc.IntFloatMap;
import com.carrotsearch.hppc.IntIntHashMap;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.search.FieldDoc;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.LeafCollector;
import org.apache.lucene.search.Query;
@@ -39,8 +42,10 @@ import org.apache.lucene.search.TopFieldCollectorManager;
import org.apache.lucene.search.TopScoreDocCollectorManager;
import org.apache.lucene.util.BytesRef;
import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.NamedList;
import org.apache.solr.handler.component.QueryElevationComponent;
import org.apache.solr.request.SolrRequestInfo;
+import org.apache.solr.schema.SchemaField;
/* A TopDocsCollector used by reranking queries. */
public class ReRankCollector extends TopDocsCollector<ScoreDoc> {
@@ -52,6 +57,7 @@ public class ReRankCollector extends
TopDocsCollector<ScoreDoc> {
private final Set<BytesRef> boostedPriority; // order is the "priority"
private final Rescorer reRankQueryRescorer;
private final Sort sort;
+ private final List<SchemaField> sortSchemaFields;
private final Query query;
private ReRankScaler reRankScaler;
private ReRankOperator reRankOperator;
@@ -85,6 +91,7 @@ public class ReRankCollector extends
TopDocsCollector<ScoreDoc> {
this.boostedPriority = boostedPriority;
this.query = cmd.getQuery();
Sort sort = cmd.getSort();
+ this.sortSchemaFields = cmd.getSortSchemaFields();
int maxDoc = searcher.getIndexReader().maxDoc();
int numHits = Math.min(Math.max(this.reRankDocs, length), maxDoc);
if (sort == null) {
@@ -132,6 +139,7 @@ public class ReRankCollector extends
TopDocsCollector<ScoreDoc> {
}
ScoreDoc[] mainScoreDocs = mainDocs.scoreDocs;
+ final Object reRankCutoffValue = getReRankCutoffValue(mainScoreDocs);
boolean zeroOutScores = reRankScaler != null &&
reRankScaler.scaleScores();
IntFloatMap docToOriginalScore = new IntFloatHashMap();
ScoreDoc[] mainScoreDocsClone = deepClone(mainScoreDocs,
docToOriginalScore, zeroOutScores);
@@ -174,6 +182,8 @@ public class ReRankCollector extends
TopDocsCollector<ScoreDoc> {
rescoredDocs.scoreDocs, new BoostedComp(boostedDocs,
mainDocs.scoreDocs, maxScore));
}
+ updateResponseHeader(reRankCutoffValue);
+
if (howMany == rescoredDocs.scoreDocs.length) {
if (reRankScaler != null && reRankScaler.scaleScores()) {
rescoredDocs.scoreDocs =
@@ -224,6 +234,74 @@ public class ReRankCollector extends
TopDocsCollector<ScoreDoc> {
return new TopDocs(topDocs.totalHits, rescoredDocs);
}
+ private Object getReRankCutoffValue(ScoreDoc[] mainScoreDocs) {
+ final ScoreDoc cutoffDoc = mainScoreDocs[Math.min(mainScoreDocs.length,
reRankDocs) - 1];
+ if (sort == null) {
+ return cutoffDoc.score;
+ }
+
+ if (!(cutoffDoc instanceof FieldDoc fieldDoc)
+ || fieldDoc.fields == null
+ || fieldDoc.fields.length == 0) {
+ return cutoffDoc.score;
+ }
+
+ if (fieldDoc.fields.length == 1) {
+ return marshalSortValue(fieldDoc.fields[0], 0);
+ }
+
+ final List<Object> cutoffSortValues = new
ArrayList<>(fieldDoc.fields.length);
+ for (int i = 0; i < fieldDoc.fields.length; i++) {
+ cutoffSortValues.add(marshalSortValue(fieldDoc.fields[i], i));
+ }
+ return cutoffSortValues;
+ }
+
+ private Object marshalSortValue(Object sortValue, int index) {
+ if (sortValue == null) {
+ return null;
+ }
+
+ if (sortSchemaFields != null && index < sortSchemaFields.size()) {
+ final SchemaField schemaField = sortSchemaFields.get(index);
+ if (schemaField != null) {
+ return schemaField.getType().marshalSortValue(sortValue);
+ }
+ }
+
+ if (sortValue instanceof BytesRef bytesRef) {
+ return bytesRef.utf8ToString();
+ }
+
+ return sortValue;
+ }
+
+ private void updateResponseHeader(Object reRankCutoffValue) {
+ SolrRequestInfo info = SolrRequestInfo.getRequestInfo();
+ if (info == null || info.getReq() == null || info.getRsp() == null) {
+ return;
+ }
+
+ Map<Object, Object> requestContext = info.getReq().getContext();
+ if (!Boolean.TRUE.equals(
+
requestContext.get(AbstractReRankQuery.RERANK_CUTOFF_ECHO_REQUEST_CONTEXT_KEY)))
{
+ return;
+ }
+
+ final NamedList<Object> responseHeader = info.getRsp().getResponseHeader();
+ if (responseHeader == null) {
+ return;
+ }
+
+ final int existingIndex =
+
responseHeader.indexOf(AbstractReRankQuery.RERANK_CUTOFF_RESPONSE_HEADER_KEY,
0);
+ if (existingIndex >= 0) {
+ responseHeader.setVal(existingIndex, reRankCutoffValue);
+ } else {
+
responseHeader.add(AbstractReRankQuery.RERANK_CUTOFF_RESPONSE_HEADER_KEY,
reRankCutoffValue);
+ }
+ }
+
private ScoreDoc[] deepClone(
ScoreDoc[] scoreDocs, IntFloatMap originalScoreMap, boolean zeroOut) {
ScoreDoc[] scoreDocs1 = new ScoreDoc[scoreDocs.length];
diff --git
a/solr/modules/ltr/src/java/org/apache/solr/ltr/search/LTRQParserPlugin.java
b/solr/modules/ltr/src/java/org/apache/solr/ltr/search/LTRQParserPlugin.java
index e3ba6cffe1d..ee9c54d2d7f 100644
--- a/solr/modules/ltr/src/java/org/apache/solr/ltr/search/LTRQParserPlugin.java
+++ b/solr/modules/ltr/src/java/org/apache/solr/ltr/search/LTRQParserPlugin.java
@@ -41,6 +41,7 @@ import org.apache.solr.ltr.store.rest.ManagedModelStore;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.rest.ManagedResource;
import org.apache.solr.rest.ManagedResourceObserver;
+import org.apache.solr.search.AbstractReRankQuery;
import org.apache.solr.search.QParser;
import org.apache.solr.search.QParserPlugin;
import org.apache.solr.search.SyntaxError;
@@ -75,6 +76,9 @@ public class LTRQParserPlugin extends QParserPlugin
/** query parser plugin:the param that will select how the number of
document to rerank */
public static final String RERANK_DOCS = "reRankDocs";
+ /** query parser plugin: include the candidate cutoff in the response header
*/
+ public static final String ECHO_RERANK_CUTOFF = "echoReRankCutoff";
+
/** query parser plugin: default interleaving algorithm */
public static final String DEFAULT_INTERLEAVING_ALGORITHM =
Interleaving.TEAM_DRAFT;
@@ -229,6 +233,12 @@ public class LTRQParserPlugin extends QParserPlugin
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST, "Must rerank at least 1
document");
}
+
+ req.getContext()
+ .put(
+ AbstractReRankQuery.RERANK_CUTOFF_ECHO_REQUEST_CONTEXT_KEY,
+ localParams.getBool(ECHO_RERANK_CUTOFF, false));
+
if (!isInterleaving) {
SolrQueryRequestContextUtils.setScoringQueries(req, new
LTRScoringQuery[] {rerankingQuery});
return new LTRQuery(rerankingQuery, reRankDocs);
diff --git
a/solr/modules/ltr/src/test/org/apache/solr/ltr/AbstractLTRSolrCloudTestBase.java
b/solr/modules/ltr/src/test/org/apache/solr/ltr/AbstractLTRSolrCloudTestBase.java
new file mode 100644
index 00000000000..1cbba422526
--- /dev/null
+++
b/solr/modules/ltr/src/test/org/apache/solr/ltr/AbstractLTRSolrCloudTestBase.java
@@ -0,0 +1,219 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.solr.ltr;
+
+import static java.util.stream.Collectors.toList;
+
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.IntStream;
+import org.apache.commons.io.file.PathUtils;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.response.CollectionAdminResponse;
+import org.apache.solr.cloud.MiniSolrCloudCluster;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.embedded.JettyConfig;
+import org.apache.solr.embedded.JettySolrRunner;
+import org.apache.solr.ltr.feature.FieldValueFeature;
+import org.apache.solr.ltr.feature.OriginalScoreFeature;
+import org.apache.solr.ltr.feature.SolrFeature;
+import org.apache.solr.ltr.feature.ValueFeature;
+import org.apache.solr.ltr.model.LinearModel;
+import org.junit.AfterClass;
+
+public abstract class AbstractLTRSolrCloudTestBase extends TestRerankBase {
+
+ protected MiniSolrCloudCluster solrCluster;
+
+ protected abstract int numberOfShards();
+
+ protected abstract int numberOfReplicas();
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ setupTestInit("solrconfig-ltr.xml", "schema.xml", true);
+ System.setProperty("solr.index.updatelog.enabled", "true");
+
+ final int numShards = numberOfShards();
+ final int numReplicas = numberOfReplicas();
+ setupSolrCluster(numShards, numReplicas, numShards * numReplicas);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ restTestHarness = null;
+ if (solrCluster != null) {
+ solrCluster.shutdown();
+ solrCluster = null;
+ }
+ super.tearDown();
+ }
+
+ protected void setupSolrCluster(int numShards, int numReplicas, int
numServers) throws Exception {
+ solrCluster = new MiniSolrCloudCluster(numServers, tmpSolrHome,
JettyConfig.builder().build());
+ Path configDir = tmpSolrHome.resolve("collection1/conf");
+ solrCluster.uploadConfigSet(configDir, "conf1");
+
+ createCollection(COLLECTION, "conf1", numShards, numReplicas);
+ indexDocuments(COLLECTION);
+ for (JettySolrRunner solrRunner : solrCluster.getJettySolrRunners()) {
+ if (!solrRunner.getCoreContainer().getCores().isEmpty()) {
+ String coreName =
solrRunner.getCoreContainer().getCores().iterator().next().getName();
+ restTestHarness = solrRunner.getRestClient(coreName);
+ break;
+ }
+ }
+ loadModelsAndFeatures();
+ }
+
+ private void createCollection(String name, String config, int numShards, int
numReplicas)
+ throws Exception {
+ CollectionAdminResponse response;
+ CollectionAdminRequest.Create create =
+ CollectionAdminRequest.createCollection(name, config, numShards,
numReplicas);
+ response = create.process(solrCluster.getSolrClient());
+
+ if (response.getStatus() != 0 || response.getErrorMessages() != null) {
+ fail("Could not create collection. Response" + response);
+ }
+ solrCluster.waitForActiveCollection(name, numShards, numShards *
numReplicas);
+ }
+
+ private void indexDocument(
+ String collection, String id, String title, String description, int
popularity)
+ throws Exception {
+ SolrInputDocument doc = new SolrInputDocument();
+ doc.setField("id", id);
+ doc.setField("title", title);
+ doc.setField("description", description);
+ doc.setField("popularity", popularity);
+ if (popularity != 1) {
+ // check that empty values will be read as default
+ doc.setField("dvIntField", popularity);
+ doc.setField("dvLongField", popularity);
+ doc.setField("dvFloatField", ((float) popularity) / 10);
+ doc.setField("dvDoubleField", ((double) popularity) / 10);
+ doc.setField("dvStrNumField", popularity % 2 == 0 ? "F" : "T");
+ doc.setField("dvStrBoolField", popularity % 2 == 0 ? "T" : "F");
+ }
+ solrCluster.getSolrClient().add(collection, doc);
+ }
+
+ private void indexDocuments(final String collection) throws Exception {
+ final int collectionSize = 8;
+ // put documents in random order to check that advanceExact is working
correctly
+ List<Integer> docIds = IntStream.rangeClosed(1,
collectionSize).boxed().collect(toList());
+ Collections.shuffle(docIds, random());
+
+ int docCounter = 1;
+ for (int docId : docIds) {
+ final int popularity = docId;
+ indexDocument(collection, String.valueOf(docId), "a1", "bloom",
popularity);
+ // maybe commit in the middle in order to check that everything works
fine for multi-segment
+ // case
+ if (docCounter == collectionSize / 2 && random().nextBoolean()) {
+ solrCluster.getSolrClient().commit(collection);
+ }
+ docCounter++;
+ }
+ solrCluster.getSolrClient().commit(collection, true, true);
+ }
+
+ private void loadModelsAndFeatures() throws Exception {
+ final String featureStore = "test";
+ final String[] featureNames =
+ new String[] {
+ "powpularityS",
+ "c3",
+ "original",
+ "dvIntFieldFeature",
+ "dvLongFieldFeature",
+ "dvFloatFieldFeature",
+ "dvDoubleFieldFeature",
+ "dvStrNumFieldFeature",
+ "dvStrBoolFieldFeature"
+ };
+ final String jsonModelParams =
+ "{\"weights\":{\"powpularityS\":1.0,\"c3\":1.0,\"original\":0.1,"
+ + "\"dvIntFieldFeature\":0.1,\"dvLongFieldFeature\":0.1,"
+ +
"\"dvFloatFieldFeature\":0.1,\"dvDoubleFieldFeature\":0.1,\"dvStrNumFieldFeature\":0.1,\"dvStrBoolFieldFeature\":0.1}}";
+
+ loadFeature(
+ featureNames[0],
+ SolrFeature.class.getName(),
+ featureStore,
+ "{\"q\":\"{!func}pow(popularity,2)\"}");
+ loadFeature(featureNames[1], ValueFeature.class.getName(), featureStore,
"{\"value\":2}");
+ loadFeature(featureNames[2], OriginalScoreFeature.class.getName(),
featureStore, null);
+ loadFeature(
+ featureNames[3],
+ FieldValueFeature.class.getName(),
+ featureStore,
+ "{\"field\":\"dvIntField\"}");
+ loadFeature(
+ featureNames[4],
+ FieldValueFeature.class.getName(),
+ featureStore,
+ "{\"field\":\"dvLongField\"}");
+ loadFeature(
+ featureNames[5],
+ FieldValueFeature.class.getName(),
+ featureStore,
+ "{\"field\":\"dvFloatField\"}");
+ loadFeature(
+ featureNames[6],
+ FieldValueFeature.class.getName(),
+ featureStore,
+ "{\"field\":\"dvDoubleField\",\"defaultValue\":-4.0}");
+ loadFeature(
+ featureNames[7],
+ FieldValueFeature.class.getName(),
+ featureStore,
+ "{\"field\":\"dvStrNumField\",\"defaultValue\":-5}");
+ loadFeature(
+ featureNames[8],
+ FieldValueFeature.class.getName(),
+ featureStore,
+ "{\"field\":\"dvStrBoolField\"}");
+
+ loadModel(
+ "powpularityS-model",
+ LinearModel.class.getName(),
+ featureNames,
+ featureStore,
+ jsonModelParams);
+ reloadCollection(COLLECTION);
+ }
+
+ private void reloadCollection(String collection) throws Exception {
+ CollectionAdminRequest.Reload reloadRequest =
+ CollectionAdminRequest.reloadCollection(collection);
+ CollectionAdminResponse response =
reloadRequest.process(solrCluster.getSolrClient());
+ assertEquals(0, response.getStatus());
+ assertTrue(response.isSuccess());
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ if (null != tmpSolrHome) {
+ PathUtils.deleteDirectory(tmpSolrHome);
+ tmpSolrHome = null;
+ }
+ }
+}
diff --git
a/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTROnSolrCloud.java
b/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTROnSolrCloud.java
index 31a70b9652f..df7dba8a830 100644
--- a/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTROnSolrCloud.java
+++ b/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTROnSolrCloud.java
@@ -15,56 +15,22 @@
*/
package org.apache.solr.ltr;
-import static java.util.stream.Collectors.toList;
-
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering;
-import java.nio.file.Path;
-import java.util.Collections;
-import java.util.List;
-import java.util.stream.IntStream;
-import org.apache.commons.io.file.PathUtils;
-import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.SolrQuery;
-import org.apache.solr.client.solrj.response.CollectionAdminResponse;
import org.apache.solr.client.solrj.response.QueryResponse;
-import org.apache.solr.cloud.MiniSolrCloudCluster;
-import org.apache.solr.common.SolrInputDocument;
-import org.apache.solr.embedded.JettyConfig;
-import org.apache.solr.embedded.JettySolrRunner;
-import org.apache.solr.ltr.feature.FieldValueFeature;
-import org.apache.solr.ltr.feature.OriginalScoreFeature;
-import org.apache.solr.ltr.feature.SolrFeature;
-import org.apache.solr.ltr.feature.ValueFeature;
-import org.apache.solr.ltr.model.LinearModel;
-import org.junit.AfterClass;
import org.junit.Test;
@ThreadLeakLingering(linger = 10)
-public class TestLTROnSolrCloud extends TestRerankBase {
-
- private MiniSolrCloudCluster solrCluster;
- String solrconfig = "solrconfig-ltr.xml";
- String schema = "schema.xml";
+public class TestLTROnSolrCloud extends AbstractLTRSolrCloudTestBase {
@Override
- public void setUp() throws Exception {
- super.setUp();
- setupTestInit(solrconfig, schema, true);
- System.setProperty("solr.index.updatelog.enabled", "true");
-
- int numberOfShards = random().nextInt(4) + 1;
- int numberOfReplicas = random().nextInt(2) + 1;
-
- int numberOfNodes = numberOfShards * numberOfReplicas;
-
- setupSolrCluster(numberOfShards, numberOfReplicas, numberOfNodes);
+ protected int numberOfShards() {
+ return random().nextInt(4) + 1;
}
@Override
- public void tearDown() throws Exception {
- restTestHarness = null;
- solrCluster.shutdown();
- super.tearDown();
+ protected int numberOfReplicas() {
+ return random().nextInt(2) + 1;
}
@Test
@@ -319,155 +285,4 @@ public class TestLTROnSolrCloud extends TestRerankBase {
assertEquals("1", queryResponse.getResults().get(7).get("id").toString());
assertEquals(result7_features,
queryResponse.getResults().get(7).get("features").toString());
}
-
- private void setupSolrCluster(int numShards, int numReplicas, int
numServers) throws Exception {
- solrCluster = new MiniSolrCloudCluster(numServers, tmpSolrHome,
JettyConfig.builder().build());
- Path configDir = tmpSolrHome.resolve("collection1/conf");
- solrCluster.uploadConfigSet(configDir, "conf1");
-
- createCollection(COLLECTION, "conf1", numShards, numReplicas);
- indexDocuments(COLLECTION);
- for (JettySolrRunner solrRunner : solrCluster.getJettySolrRunners()) {
- if (!solrRunner.getCoreContainer().getCores().isEmpty()) {
- String coreName =
solrRunner.getCoreContainer().getCores().iterator().next().getName();
- restTestHarness = solrRunner.getRestClient(coreName);
- break;
- }
- }
- loadModelsAndFeatures();
- }
-
- private void createCollection(String name, String config, int numShards, int
numReplicas)
- throws Exception {
- CollectionAdminResponse response;
- CollectionAdminRequest.Create create =
- CollectionAdminRequest.createCollection(name, config, numShards,
numReplicas);
- response = create.process(solrCluster.getSolrClient());
-
- if (response.getStatus() != 0 || response.getErrorMessages() != null) {
- fail("Could not create collection. Response" + response);
- }
- solrCluster.waitForActiveCollection(name, numShards, numShards *
numReplicas);
- }
-
- void indexDocument(String collection, String id, String title, String
description, int popularity)
- throws Exception {
- SolrInputDocument doc = new SolrInputDocument();
- doc.setField("id", id);
- doc.setField("title", title);
- doc.setField("description", description);
- doc.setField("popularity", popularity);
- if (popularity != 1) {
- // check that empty values will be read as default
- doc.setField("dvIntField", popularity);
- doc.setField("dvLongField", popularity);
- doc.setField("dvFloatField", ((float) popularity) / 10);
- doc.setField("dvDoubleField", ((double) popularity) / 10);
- doc.setField("dvStrNumField", popularity % 2 == 0 ? "F" : "T");
- doc.setField("dvStrBoolField", popularity % 2 == 0 ? "T" : "F");
- }
- solrCluster.getSolrClient().add(collection, doc);
- }
-
- private void indexDocuments(final String collection) throws Exception {
- final int collectionSize = 8;
- // put documents in random order to check that advanceExact is working
correctly
- List<Integer> docIds = IntStream.rangeClosed(1,
collectionSize).boxed().collect(toList());
- Collections.shuffle(docIds, random());
-
- int docCounter = 1;
- for (int docId : docIds) {
- final int popularity = docId;
- indexDocument(collection, String.valueOf(docId), "a1", "bloom",
popularity);
- // maybe commit in the middle in order to check that everything works
fine for multi-segment
- // case
- if (docCounter == collectionSize / 2 && random().nextBoolean()) {
- solrCluster.getSolrClient().commit(collection);
- }
- docCounter++;
- }
- solrCluster.getSolrClient().commit(collection, true, true);
- }
-
- private void loadModelsAndFeatures() throws Exception {
- final String featureStore = "test";
- final String[] featureNames =
- new String[] {
- "powpularityS",
- "c3",
- "original",
- "dvIntFieldFeature",
- "dvLongFieldFeature",
- "dvFloatFieldFeature",
- "dvDoubleFieldFeature",
- "dvStrNumFieldFeature",
- "dvStrBoolFieldFeature"
- };
- final String jsonModelParams =
- "{\"weights\":{\"powpularityS\":1.0,\"c3\":1.0,\"original\":0.1,"
- + "\"dvIntFieldFeature\":0.1,\"dvLongFieldFeature\":0.1,"
- +
"\"dvFloatFieldFeature\":0.1,\"dvDoubleFieldFeature\":0.1,\"dvStrNumFieldFeature\":0.1,\"dvStrBoolFieldFeature\":0.1}}";
-
- loadFeature(
- featureNames[0],
- SolrFeature.class.getName(),
- featureStore,
- "{\"q\":\"{!func}pow(popularity,2)\"}");
- loadFeature(featureNames[1], ValueFeature.class.getName(), featureStore,
"{\"value\":2}");
- loadFeature(featureNames[2], OriginalScoreFeature.class.getName(),
featureStore, null);
- loadFeature(
- featureNames[3],
- FieldValueFeature.class.getName(),
- featureStore,
- "{\"field\":\"dvIntField\"}");
- loadFeature(
- featureNames[4],
- FieldValueFeature.class.getName(),
- featureStore,
- "{\"field\":\"dvLongField\"}");
- loadFeature(
- featureNames[5],
- FieldValueFeature.class.getName(),
- featureStore,
- "{\"field\":\"dvFloatField\"}");
- loadFeature(
- featureNames[6],
- FieldValueFeature.class.getName(),
- featureStore,
- "{\"field\":\"dvDoubleField\",\"defaultValue\":-4.0}");
- loadFeature(
- featureNames[7],
- FieldValueFeature.class.getName(),
- featureStore,
- "{\"field\":\"dvStrNumField\",\"defaultValue\":-5}");
- loadFeature(
- featureNames[8],
- FieldValueFeature.class.getName(),
- featureStore,
- "{\"field\":\"dvStrBoolField\"}");
-
- loadModel(
- "powpularityS-model",
- LinearModel.class.getName(),
- featureNames,
- featureStore,
- jsonModelParams);
- reloadCollection(COLLECTION);
- }
-
- private void reloadCollection(String collection) throws Exception {
- CollectionAdminRequest.Reload reloadRequest =
- CollectionAdminRequest.reloadCollection(collection);
- CollectionAdminResponse response =
reloadRequest.process(solrCluster.getSolrClient());
- assertEquals(0, response.getStatus());
- assertTrue(response.isSuccess());
- }
-
- @AfterClass
- public static void after() throws Exception {
- if (null != tmpSolrHome) {
- PathUtils.deleteDirectory(tmpSolrHome);
- tmpSolrHome = null;
- }
- }
}
diff --git
a/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTRQParserPlugin.java
b/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTRQParserPlugin.java
index 8f793845e3a..8f5f140f4fd 100644
--- a/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTRQParserPlugin.java
+++ b/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTRQParserPlugin.java
@@ -231,4 +231,84 @@ public class TestLTRQParserPlugin extends TestRerankBase {
// original score for the 4th document due to reRankDocs=3 limit
"/response/docs/[3]/score==1.0");
}
+
+ @Test
+ public void ltrEchoReRankCutoff_shouldAddHeaderField() throws Exception {
+ final String solrQuery = "_query_:{!edismax qf='id' v='8^=10 9^=5 7^=3
6^=1'}";
+ final SolrQuery query = new SolrQuery();
+ query.setQuery(solrQuery);
+ query.setFields("id", "score");
+ query.setRows(2);
+ query.add("rq", "{!ltr model=6029760550880411648 reRankDocs=3
echoReRankCutoff=true}");
+
+ assertJQ("/query" + query.toQueryString(),
"/responseHeader/reRankCutoff==3.0");
+ }
+
+ @Test
+ public void ltrEchoReRankCutoff_defaultShouldNotAddHeaderField() throws
Exception {
+ final String solrQuery = "_query_:{!edismax qf='id' v='8^=10 9^=5 7^=3
6^=1'}";
+ final SolrQuery query = new SolrQuery();
+ query.setQuery(solrQuery);
+ query.setFields("id", "score");
+ query.setRows(2);
+ query.add("rq", "{!ltr model=6029760550880411648 reRankDocs=3}");
+
+ assertJQ("/query" + query.toQueryString(),
"!/responseHeader/reRankCutoff==");
+ }
+
+ @Test
+ public void ltrEchoReRankCutoff_falseShouldNotAddHeaderField() throws
Exception {
+ final String solrQuery = "_query_:{!edismax qf='id' v='8^=10 9^=5 7^=3
6^=1'}";
+ final SolrQuery query = new SolrQuery();
+ query.setQuery(solrQuery);
+ query.setFields("id", "score");
+ query.setRows(2);
+ query.add("rq", "{!ltr model=6029760550880411648 reRankDocs=3
echoReRankCutoff=false}");
+
+ assertJQ("/query" + query.toQueryString(),
"!/responseHeader/reRankCutoff==");
+ }
+
+ @Test
+ public void ltrEchoReRankCutoff_withNoResults_shouldNotAddHeaderField()
throws Exception {
+ final SolrQuery query = new SolrQuery();
+ query.setQuery("title:title_that_does_not_exist");
+ query.add("fl", "*,[fv]");
+ query.add("rows", "3");
+ query.add("rq", "{!ltr model=6029760550880411648 reRankDocs=3
echoReRankCutoff=true}");
+
+ assertJQ(
+ "/query" + query.toQueryString(),
+ "/response/numFound/==0",
+ "!/responseHeader/reRankCutoff==");
+ }
+
+ @Test
+ public void ltrEchoReRankCutoff_withFieldSortShouldAddSortCutoffValue()
throws Exception {
+ final String solrQuery = "_query_:{!edismax qf='id' v='8^=10 9^=5 7^=3
6^=1'}";
+ final SolrQuery query = new SolrQuery();
+ query.setQuery(solrQuery);
+ query.setFields("id", "score");
+ query.setRows(2);
+ query.setSort("id", SolrQuery.ORDER.asc);
+ query.add("rq", "{!ltr model=6029760550880411648 reRankDocs=3
echoReRankCutoff=true}");
+
+ assertJQ("/query" + query.toQueryString(),
"/responseHeader/reRankCutoff=='8'");
+ }
+
+ @Test
+ public void
ltrEchoReRankCutoff_withMixedScoreAndFieldSort_shouldAddBothValuesInOrder()
+ throws Exception {
+ final String solrQuery = "_query_:{!edismax qf='id' v='8^=10 9^=5 7^=3
6^=1'}";
+ final SolrQuery query = new SolrQuery();
+ query.setQuery(solrQuery);
+ query.setFields("id", "score");
+ query.setRows(2);
+ query.add("sort", "score desc,id asc");
+ query.add("rq", "{!ltr model=6029760550880411648 reRankDocs=3
echoReRankCutoff=true}");
+
+ assertJQ(
+ "/query" + query.toQueryString(),
+ "/responseHeader/reRankCutoff/[0]==3.0",
+ "/responseHeader/reRankCutoff/[1]=='7'");
+ }
}
diff --git
a/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTRReRankCutoffOnSolrCloud.java
b/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTRReRankCutoffOnSolrCloud.java
new file mode 100644
index 00000000000..71367bef542
--- /dev/null
+++
b/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTRReRankCutoffOnSolrCloud.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.solr.ltr;
+
+import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering;
+import java.util.List;
+import org.apache.solr.client.solrj.request.QueryRequest;
+import org.apache.solr.client.solrj.request.SolrQuery;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.search.AbstractReRankQuery;
+import org.junit.Test;
+
+@ThreadLeakLingering(linger = 10)
+public class TestLTRReRankCutoffOnSolrCloud extends
AbstractLTRSolrCloudTestBase {
+
+ private static final int NUM_SHARDS = 3;
+ private static final int NUM_REPLICAS = 2;
+
+ @Override
+ protected int numberOfShards() {
+ return NUM_SHARDS;
+ }
+
+ @Override
+ protected int numberOfReplicas() {
+ return NUM_REPLICAS;
+ }
+
+ @Test
+ public void distributedRerankCutoffScore_shouldBeReturnedForShardsWithHits()
throws Exception {
+ final SolrQuery query = newBaseRerankQuery();
+ query.add("rq", "{!ltr model=powpularityS-model reRankDocs=3
echoReRankCutoff=true}");
+
+ final QueryRequest queryRequest = new QueryRequest(query);
+ queryRequest.setPath("/query");
+ final QueryResponse queryResponse =
+ queryRequest.process(solrCluster.getSolrClient(), COLLECTION);
+
+ @SuppressWarnings("unchecked")
+ final NamedList<Object> perShardCutoff =
+ (NamedList<Object>)
+ queryResponse
+ .getResponseHeader()
+
.get(AbstractReRankQuery.RERANK_CUTOFF_BY_SHARD_RESPONSE_HEADER_KEY);
+ assertNotNull(perShardCutoff);
+
+ for (int i = 0; i < perShardCutoff.size(); i++) {
+ assertNotNull(perShardCutoff.getName(i));
+ assertNotNull(perShardCutoff.getVal(i));
+ assertTrue(perShardCutoff.getVal(i) instanceof List);
+ assertEquals(2, ((List<?>) perShardCutoff.getVal(i)).size());
+ }
+ }
+
+ @Test
+ public void
distributedRerankCutoffScore_defaultShouldNotBeReturnedPerShard() throws
Exception {
+ final SolrQuery query = newBaseRerankQuery();
+ query.add("rq", "{!ltr model=powpularityS-model reRankDocs=3}");
+
+ final QueryRequest queryRequest = new QueryRequest(query);
+ queryRequest.setPath("/query");
+ final QueryResponse queryResponse =
+ queryRequest.process(solrCluster.getSolrClient(), COLLECTION);
+ assertNull(
+ queryResponse
+ .getResponseHeader()
+
.get(AbstractReRankQuery.RERANK_CUTOFF_BY_SHARD_RESPONSE_HEADER_KEY));
+ }
+
+ @Test
+ public void distributedRerankCutoffScore_falseShouldNotBeReturnedPerShard()
throws Exception {
+ final SolrQuery query = newBaseRerankQuery();
+ query.add("rq", "{!ltr model=powpularityS-model reRankDocs=3
echoReRankCutoff=false}");
+
+ final QueryRequest queryRequest = new QueryRequest(query);
+ queryRequest.setPath("/query");
+ final QueryResponse queryResponse =
+ queryRequest.process(solrCluster.getSolrClient(), COLLECTION);
+ assertNull(
+ queryResponse
+ .getResponseHeader()
+
.get(AbstractReRankQuery.RERANK_CUTOFF_BY_SHARD_RESPONSE_HEADER_KEY));
+ }
+
+ private SolrQuery newBaseRerankQuery() {
+ final SolrQuery query = new SolrQuery("{!func}sub(8,field(popularity))");
+ query.setFields("id", "score");
+ query.setRows(4);
+ query.setParam("sort", "score desc,id asc");
+ return query;
+ }
+}
diff --git a/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTRWithSort.java
b/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTRWithSort.java
index af8664aff24..52647fa5959 100644
--- a/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTRWithSort.java
+++ b/solr/modules/ltr/src/test/org/apache/solr/ltr/TestLTRWithSort.java
@@ -95,6 +95,51 @@ public class TestLTRWithSort extends TestRerankBase {
assertJQ("/query" + query.toQueryString(),
"/response/docs/[3]/score==1.0");
}
+ @Test
+ public void ltrEchoReRankCutoff_withFunctionSortShouldAddFunctionValue()
throws Exception {
+ loadFeature(
+ "powpularityS", SolrFeature.class.getName(),
"{\"q\":\"{!func}pow(popularity,2)\"}");
+
+ loadModel(
+ "powpularityS-model",
+ LinearModel.class.getName(),
+ new String[] {"powpularityS"},
+ "{\"weights\":{\"powpularityS\":1.0}}");
+
+ final SolrQuery query = new SolrQuery();
+ query.setQuery("title:a1");
+ query.add("fl", "id,score");
+ query.add("rows", "4");
+ query.add("sort", "pow(popularity,2) desc");
+ query.add("rq", "{!ltr model=powpularityS-model reRankDocs=4
echoReRankCutoff=true}");
+
+ assertJQ("/query" + query.toQueryString(),
"/responseHeader/reRankCutoff==25.0");
+ }
+
+ @Test
+ public void
ltrEchoReRankCutoff_withMultipleFunctionSortsShouldAddAllValues() throws
Exception {
+ loadFeature(
+ "powpularityS", SolrFeature.class.getName(),
"{\"q\":\"{!func}pow(popularity,2)\"}");
+
+ loadModel(
+ "powpularityS-model",
+ LinearModel.class.getName(),
+ new String[] {"powpularityS"},
+ "{\"weights\":{\"powpularityS\":1.0}}");
+
+ final SolrQuery query = new SolrQuery();
+ query.setQuery("title:a1");
+ query.add("fl", "id,score");
+ query.add("rows", "4");
+ query.add("sort", "pow(popularity,2) desc,sum(popularity,1) desc");
+ query.add("rq", "{!ltr model=powpularityS-model reRankDocs=4
echoReRankCutoff=true}");
+
+ assertJQ(
+ "/query" + query.toQueryString(),
+ "/responseHeader/reRankCutoff/[0]==25.0",
+ "/responseHeader/reRankCutoff/[1]==6.0");
+ }
+
@Test
public void interleavingTwoModelsWithSort_shouldInterleave() throws
Exception {
TeamDraftInterleaving.setRANDOM(
diff --git
a/solr/solr-ref-guide/modules/query-guide/pages/learning-to-rank.adoc
b/solr/solr-ref-guide/modules/query-guide/pages/learning-to-rank.adoc
index cf13add48d8..b98af2b6fbc 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/learning-to-rank.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/learning-to-rank.adoc
@@ -518,6 +518,58 @@ The output will include feature values as a
comma-separated list, resembling the
}}
----
+==== Rerank Cutoff Score
+The rerank cutoff score is the first-pass ranking value of the lowest ranked
document eligible for reranking. It tells you the score or sort value that a
document would have needed to beat to be included in the reranking stage. To
obtain the rerank cutoff score, add the `echoReRankCutoff` parameter to the ltr
query.
+
+[source,text]
+----
+http://localhost:8983/solr/techproducts/query?q=test&rq={!ltr model=myModel
reRankDocs=100 echoReRankCutoff=true}&fl=id,score
+----
+
+When set to `true`, Solr adds a `reRankCutoff` to the responseHeader. For the
default first-pass relevance ranking, the value is the relevance score of the
last document eligible for reranking. When an explicit sort is used,
`reRankCutoff` reflects the sort value of that last eligible document.
+[source,json]
+----
+"responseHeader": {
+ "status": 0,
+ "QTime": 1,
+ "reRankCutoff": 12.345
+}
+----
+
+When multiple sorts are used (e.g. `sort=price desc,discount asc`), the
response header contains the cutoff value for each sort in order:
+
+[source,json]
+----
+"responseHeader": {
+ "status": 0,
+ "QTime": 1,
+ "reRankCutoff": [
+ 11.99,
+ 2.20
+ ]
+}
+----
+
+For distributed queries, when `echoReRankCutoff` is `true`, Solr returns
shard-local cutoff values in `reRankCutoffByShard`:
+
+[source,json]
+----
+"responseHeader": {
+ "status": 0,
+ "QTime": 3,
+ "reRankCutoffByShard": {
+ "shard1": [
+ 3.0,
+ 2.0
+ ],
+ "shard2": [
+ 5.0,
+ 2.3
+ ]
+ }
+}
+----
+
=== Running a Rerank Query and Query Limits
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]).