This is an automated email from the ASF dual-hosted git repository.
dsmiley 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 c8abb33884c SOLR-18541: CombinedQueryComponent.mergeIds gives wrong
numFound, approximateTotalHits and numFoundExact (#5117)
c8abb33884c is described below
commit c8abb33884cb369bfcaac47b6b02b2e1a0c850aa
Author: Nick <[email protected]>
AuthorDate: Tue Oct 27 14:33:37 2026 -0600
SOLR-18541: CombinedQueryComponent.mergeIds gives wrong numFound,
approximateTotalHits and numFoundExact (#5117)
Combined queries: skipped or failed shard responses no longer add to
numFound or approximateTotalHits and mark numFoundExact false;
approximateTotalHits no longer double-counts across shards.
---
.../SOLR-18541-combined-query-merge-totals.yml | 10 +
.../handler/component/CombinedQueryComponent.java | 33 +-
.../CombinedQueryComponentMergeTotalsTest.java | 375 +++++++++++++++++++++
.../handler/component/MockResponseBuilder.java | 25 ++
.../solr/handler/component/MockShardRequest.java | 41 +++
.../handler/component/MockSortSpecBuilder.java | 7 +
6 files changed, 478 insertions(+), 13 deletions(-)
diff --git a/changelog/unreleased/SOLR-18541-combined-query-merge-totals.yml
b/changelog/unreleased/SOLR-18541-combined-query-merge-totals.yml
new file mode 100644
index 00000000000..83f79fac733
--- /dev/null
+++ b/changelog/unreleased/SOLR-18541-combined-query-merge-totals.yml
@@ -0,0 +1,10 @@
+title: >
+ Combined queries: skipped or failed shard responses no longer add to
numFound or
+ approximateTotalHits and mark numFoundExact false; approximateTotalHits no
longer
+ double-counts across shards.
+type: fixed
+authors:
+ - name: Nick Shanin
+links:
+ - name: SOLR-18541
+ url: https://issues.apache.org/jira/browse/SOLR-18541
diff --git
a/solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java
b/solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java
index b4b956d7745..9a905d99962 100644
---
a/solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java
+++
b/solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java
@@ -340,16 +340,13 @@ public class CombinedQueryComponent extends
QueryComponent implements SolrCoreAw
instanceof List<?> docList
&& docList.get(queryIndex) instanceof SolrDocumentList
solrDocumentList) {
docs = Objects.requireNonNull(solrDocumentList);
- queryNumFound += docs.getNumFound();
- if (hitCountIsExact &&
Boolean.FALSE.equals(docs.getNumFoundExact())) {
- hitCountIsExact = false;
- }
}
failedShardCount +=
addShardInfo(
shardInfo, failedShardCount, srsp, rb,
queriesToCombineKeys[queryIndex], docs);
if (srsp.getException() != null) {
thereArePartialResults = true;
+ hitCountIsExact = false;
continue;
}
@@ -381,14 +378,6 @@ public class CombinedQueryComponent extends QueryComponent
implements SolrCoreAw
maxHitsTerminatedEarly = true;
}
- Object ath =
-
responseHeader.get(SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY);
- if (ath == null) {
- queryApproximateTotalHits += queryNumFound;
- } else {
- queryApproximateTotalHits += ((Number) ath).longValue();
- }
-
@SuppressWarnings("unchecked")
NamedList<List<Object>> sortFieldValues =
(NamedList<List<Object>>)
@@ -399,9 +388,27 @@ public class CombinedQueryComponent extends QueryComponent
implements SolrCoreAw
}
boolean needsUnmarshalling = ss.includesNonScoreOrDocField();
- if (thisResponseIsPartial && sortFieldValues.size() == 0 &&
needsUnmarshalling) {
+ // decided before the totals below: a skipped shard adds nothing to
this query's totals,
+ // and its hits are unreported, so the merged count cannot be exact
+ if (thisResponseIsPartial
+ && (docs == null || (sortFieldValues.size() == 0 &&
needsUnmarshalling))) {
+ hitCountIsExact = false;
continue;
}
+
+ queryNumFound += docs.getNumFound();
+ if (hitCountIsExact && Boolean.FALSE.equals(docs.getNumFoundExact())) {
+ hitCountIsExact = false;
+ }
+
+ Object ath =
+
responseHeader.get(SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY);
+ if (ath == null) {
+ queryApproximateTotalHits += docs.getNumFound();
+ } else {
+ queryApproximateTotalHits += ((Number) ath).longValue();
+ }
+
NamedList<List<Object>> unmarshalledSortFieldValues =
needsUnmarshalling
? unmarshalSortValues(ss, sortFieldValues, schema)
diff --git
a/solr/core/src/test/org/apache/solr/handler/component/CombinedQueryComponentMergeTotalsTest.java
b/solr/core/src/test/org/apache/solr/handler/component/CombinedQueryComponentMergeTotalsTest.java
new file mode 100644
index 00000000000..5dcdca59c4f
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/handler/component/CombinedQueryComponentMergeTotalsTest.java
@@ -0,0 +1,375 @@
+/*
+ * 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.handler.component;
+
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
+import static org.apache.solr.handler.component.MockShardRequest.header;
+
+import java.util.Arrays;
+import java.util.List;
+import org.apache.lucene.search.SortField;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.schema.IntPointField;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.search.SortSpec;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+/**
+ * Tests for the totals that {@link CombinedQueryComponent#mergeIds} reports:
a partial shard that
+ * the merge skips contributes nothing to its query's totals, the
approximateTotalHits fallback adds
+ * each shard's own count, and the merged exactness flag is true only when
every contributing shard
+ * reports an exact count and no shard's hits were left out. Fabricated shard
responses are driven
+ * through {@code mergeIds} directly.
+ */
+public class CombinedQueryComponentMergeTotalsTest extends SolrTestCase {
+
+ private final CombinedQueryComponent cqc = combinedQueryComponent();
+
+ @BeforeClass
+ public static void setup() {
+ assumeWorkingMockito();
+ }
+
+ private static CombinedQueryComponent combinedQueryComponent() {
+ CombinedQueryComponent cqc = new CombinedQueryComponent();
+ cqc.init(new NamedList<>());
+ cqc.inform(Mockito.mock(SolrCore.class));
+ return cqc;
+ }
+
+ private static SolrDocument doc(String id, float score) {
+ SolrDocument doc = new SolrDocument();
+ doc.addField("id", id);
+ doc.addField("score", score);
+ return doc;
+ }
+
+ private static SolrDocumentList docList(long numFound, Boolean exact,
SolrDocument... docs) {
+ SolrDocumentList list = new SolrDocumentList();
+ for (SolrDocument doc : docs) {
+ list.add(doc);
+ }
+ list.setNumFound(numFound);
+ if (exact != null) {
+ list.setNumFoundExact(exact);
+ }
+ return list;
+ }
+
+ private static SortSpec scoreSortSpec() {
+ return MockSortSpecBuilder.create()
+ .withSortFields(new SortField[] {SortField.FIELD_SCORE})
+ .withSchemaFields(Arrays.asList((SchemaField) null))
+ .build();
+ }
+
+ private static SortSpec intSortSpec() {
+ return MockSortSpecBuilder.create()
+ .withSortFields(new SortField[] {new SortField("intDefault",
SortField.Type.INT)})
+ .withSchemaFields(List.of(new SchemaField("intDefault", new
IntPointField())))
+ .withIncludesNonScoreOrDocSortField(true)
+ .build();
+ }
+
+ /** sort_values_0 is keyed by field name; the value list is per doc, in doc
order. */
+ private static NamedList<List<Object>> intSortValues() {
+ NamedList<List<Object>> sortValues = new NamedList<>();
+ sortValues.add("intDefault", List.<Object>of("1", "2", "3"));
+ return sortValues;
+ }
+
+ /** The fallback adds each shard's own count (2 and 3), not the running
total (2 then 7). */
+ @Test
+ public void fallbackUsesEachShardsOwnCount() {
+ SolrDocument a1 = doc("a1", 2.0f), a2 = doc("a2", 1.0f);
+ SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3",
1.0f);
+ MockShardRequest sreq =
+ MockShardRequest.create()
+ .withShardResponse(
+ "shard1",
+ header(
+
SolrQueryResponse.RESPONSE_HEADER_MAX_HITS_TERMINATED_EARLY_KEY, Boolean.TRUE),
+ docList(2, true, a1, a2),
+ List.of(docList(2, true, a1, a2)),
+ null)
+ .withShardResponse(
+ "shard2",
+ header(),
+ docList(3, true, b1, b2, b3),
+ List.of(docList(3, true, b1, b2, b3)),
+ null);
+ MockResponseBuilder rb =
+
MockResponseBuilder.create().withSortSpec(scoreSortSpec()).withCombinerQueries("q0");
+ cqc.mergeIds(rb, sreq);
+ assertEquals("numFound", 5, rb.getResponseDocs().getNumFound());
+ assertEquals(
+ "approximateTotalHits",
+ 5L,
+ rb.rsp
+ .getResponseHeader()
+
.get(SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY));
+ }
+
+ /** A skipped partial shard contributes nothing to the totals of the query
it was skipped for. */
+ @Test
+ public void skippedPartialShardContributesNothing() {
+ SolrDocument a1 = doc("a1", 2.0f), a2 = doc("a2", 1.0f);
+ SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3",
1.0f);
+ MockShardRequest sreq =
+ MockShardRequest.create()
+ .withShardResponse(
+ "shard1",
+ header(
+ SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY,
+ Boolean.TRUE,
+
SolrQueryResponse.RESPONSE_HEADER_MAX_HITS_TERMINATED_EARLY_KEY,
+ Boolean.TRUE,
+
SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY,
+ 10L),
+ docList(4, true, a1, a2),
+ List.of(docList(4, true, a1, a2)),
+ null)
+ .withShardResponse(
+ "shard2",
+
header(SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY, 5L),
+ docList(3, true, b1, b2, b3),
+ List.of(docList(3, true, b1, b2, b3)),
+ intSortValues());
+ MockResponseBuilder rb =
+
MockResponseBuilder.create().withSortSpec(intSortSpec()).withCombinerQueries("q0");
+ cqc.mergeIds(rb, sreq);
+ assertEquals("numFound", 3, rb.getResponseDocs().getNumFound());
+ assertEquals("numFoundExact", Boolean.FALSE,
rb.getResponseDocs().getNumFoundExact());
+ assertEquals(
+ "approximateTotalHits",
+ 5L,
+ rb.rsp
+ .getResponseHeader()
+
.get(SolrQueryResponse.RESPONSE_HEADER_APPROXIMATE_TOTAL_HITS_KEY));
+ assertEquals("response size", 3, rb.getResponseDocs().size());
+ }
+
+ @Test
+ public void skippedPartialShardWithHitsClearsExactness() {
+ SolrDocument a1 = doc("a1", 2.0f), a2 = doc("a2", 1.0f);
+ MockShardRequest sreq =
+ MockShardRequest.create()
+ .withShardResponse(
+ "shard1",
+ header(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY,
Boolean.TRUE),
+ docList(2, true, a1, a2),
+ List.of(docList(2, true, a1, a2)),
+ null)
+ .withShardResponse(
+ "shard2",
+ header(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY,
Boolean.TRUE),
+ docList(0, true),
+ List.of(docList(0, true)),
+ null);
+ MockResponseBuilder rb =
+
MockResponseBuilder.create().withSortSpec(intSortSpec()).withCombinerQueries("q0");
+ cqc.mergeIds(rb, sreq);
+ assertEquals("numFound", 0, rb.getResponseDocs().getNumFound());
+ assertEquals("numFoundExact", Boolean.FALSE,
rb.getResponseDocs().getNumFoundExact());
+ }
+
+ /** A skipped partial shard that sent no per-query entry reports no count:
inexact. */
+ @Test
+ public void skippedPartialShardWithoutCountClearsExactness() {
+ SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3",
1.0f);
+ MockShardRequest sreq =
+ MockShardRequest.create()
+ .withShardResponse(
+ "shard1",
+ header(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY,
Boolean.TRUE),
+ docList(0, true),
+ null,
+ null)
+ .withShardResponse(
+ "shard2",
+ header(),
+ docList(3, true, b1, b2, b3),
+ List.of(docList(3, true, b1, b2, b3)),
+ intSortValues());
+ MockResponseBuilder rb =
+
MockResponseBuilder.create().withSortSpec(intSortSpec()).withCombinerQueries("q0");
+ cqc.mergeIds(rb, sreq);
+ assertEquals("numFound", 3, rb.getResponseDocs().getNumFound());
+ assertEquals("numFoundExact", Boolean.FALSE,
rb.getResponseDocs().getNumFoundExact());
+ assertEquals("response size", 3, rb.getResponseDocs().size());
+ }
+
+ /** A skipped partial shard that reported no hits still leaves the count
inexact. */
+ @Test
+ public void skippedPartialShardWithoutHitsClearsExactness() {
+ SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3",
1.0f);
+ MockShardRequest sreq =
+ MockShardRequest.create()
+ .withShardResponse(
+ "shard1",
+ header(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY,
Boolean.TRUE),
+ docList(0, true),
+ List.of(docList(0, true)),
+ null)
+ .withShardResponse(
+ "shard2",
+ header(),
+ docList(3, true, b1, b2, b3),
+ List.of(docList(3, true, b1, b2, b3)),
+ intSortValues());
+ MockResponseBuilder rb =
+
MockResponseBuilder.create().withSortSpec(intSortSpec()).withCombinerQueries("q0");
+ cqc.mergeIds(rb, sreq);
+ assertEquals("numFound", 3, rb.getResponseDocs().getNumFound());
+ assertEquals("numFoundExact", Boolean.FALSE,
rb.getResponseDocs().getNumFoundExact());
+ assertEquals("response size", 3, rb.getResponseDocs().size());
+ }
+
+ /** Both shards report exact counts; the merged flag stays exact. */
+ @Test
+ public void exactnessAllExactStaysExact() {
+ SolrDocument a1 = doc("a1", 2.0f), a2 = doc("a2", 1.0f);
+ SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3",
1.0f);
+ MockShardRequest sreq =
+ MockShardRequest.create()
+ .withShardResponse(
+ "shard1",
+ header(),
+ docList(2, true, a1, a2),
+ List.of(docList(2, true, a1, a2)),
+ null)
+ .withShardResponse(
+ "shard2",
+ header(),
+ docList(3, true, b1, b2, b3),
+ List.of(docList(3, true, b1, b2, b3)),
+ null);
+ MockResponseBuilder rb =
+
MockResponseBuilder.create().withSortSpec(scoreSortSpec()).withCombinerQueries("q0");
+ cqc.mergeIds(rb, sreq);
+ assertEquals("numFoundExact", Boolean.TRUE,
rb.getResponseDocs().getNumFoundExact());
+ }
+
+ /** Both shards report inexact counts; the merged flag is inexact. */
+ @Test
+ public void exactnessAllInexactStaysInexact() {
+ SolrDocument a1 = doc("a1", 2.0f), a2 = doc("a2", 1.0f);
+ SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3",
1.0f);
+ MockShardRequest sreq =
+ MockShardRequest.create()
+ .withShardResponse(
+ "shard1",
+ header(),
+ docList(2, false, a1, a2),
+ List.of(docList(2, false, a1, a2)),
+ null)
+ .withShardResponse(
+ "shard2",
+ header(),
+ docList(3, false, b1, b2, b3),
+ List.of(docList(3, false, b1, b2, b3)),
+ null);
+ MockResponseBuilder rb =
+
MockResponseBuilder.create().withSortSpec(scoreSortSpec()).withCombinerQueries("q0");
+ cqc.mergeIds(rb, sreq);
+ assertEquals("numFoundExact", Boolean.FALSE,
rb.getResponseDocs().getNumFoundExact());
+ }
+
+ /** A partial shard with no per-query entry under a score-only sort is
skipped: inexact. */
+ @Test
+ public void scoreOnlyPartialShardWithoutPerQueryListIsSkipped() {
+ SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3",
1.0f);
+ MockShardRequest sreq =
+ MockShardRequest.create()
+ .withShardResponse(
+ "shard1",
+ header(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY,
Boolean.TRUE),
+ docList(0, true),
+ null,
+ null)
+ .withShardResponse(
+ "shard2",
+ header(),
+ docList(3, true, b1, b2, b3),
+ List.of(docList(3, true, b1, b2, b3)),
+ null);
+ MockResponseBuilder rb =
+
MockResponseBuilder.create().withSortSpec(scoreSortSpec()).withCombinerQueries("q0");
+ cqc.mergeIds(rb, sreq);
+ assertEquals("numFound", 3, rb.getResponseDocs().getNumFound());
+ assertEquals("numFoundExact", Boolean.FALSE,
rb.getResponseDocs().getNumFoundExact());
+ assertEquals("response size", 3, rb.getResponseDocs().size());
+ assertEquals(
+ "partialResults",
+ Boolean.TRUE,
+
rb.rsp.getResponseHeader().get(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY));
+ }
+
+ @Test
+ public void multipleContributingSubQueriesCountDistinctDocsExactly() {
+ SolrDocument c1 = doc("c1", 5.0f), c2 = doc("c2", 4.0f), c3 = doc("c3",
3.0f);
+ SolrDocument d1 = doc("d1", 2.0f), d2 = doc("d2", 1.0f);
+ MockShardRequest sreq =
+ MockShardRequest.create()
+ .withShardResponse(
+ "shard1",
+ header(),
+ docList(5, true, c1, c2, c3, d1, d2),
+ List.of(docList(3, true, c1, c2, c3), docList(3, true, c1, d1,
d2)),
+ null);
+ MockResponseBuilder rb =
+
MockResponseBuilder.create().withSortSpec(scoreSortSpec()).withCombinerQueries("q0",
"q1");
+ cqc.mergeIds(rb, sreq);
+ assertEquals("numFound", 5, rb.getResponseDocs().getNumFound());
+ assertEquals("numFoundExact", Boolean.TRUE,
rb.getResponseDocs().getNumFoundExact());
+ assertEquals("response size", 5, rb.getResponseDocs().size());
+ }
+
+ /** A shard that errored leaves its hits out, so the merged count is
inexact. */
+ @Test
+ public void erroredShardClearsExactness() {
+ SolrDocument b1 = doc("b1", 3.0f), b2 = doc("b2", 2.0f), b3 = doc("b3",
1.0f);
+ MockShardRequest sreq =
+ MockShardRequest.create()
+ .withErroredShard("shard1", new RuntimeException("shard
unavailable"))
+ .withShardResponse(
+ "shard2",
+ header(),
+ docList(3, true, b1, b2, b3),
+ List.of(docList(3, true, b1, b2, b3)),
+ null);
+ MockResponseBuilder rb =
+ MockResponseBuilder.create()
+ .withSortSpec(scoreSortSpec())
+ .withCombinerQueries("q0")
+ .withShardsTolerant();
+ cqc.mergeIds(rb, sreq);
+ assertEquals("numFound", 3, rb.getResponseDocs().getNumFound());
+ assertEquals("numFoundExact", Boolean.FALSE,
rb.getResponseDocs().getNumFoundExact());
+ assertEquals(
+ "partialResults",
+ Boolean.TRUE,
+
rb.rsp.getResponseHeader().get(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY));
+ }
+}
diff --git
a/solr/core/src/test/org/apache/solr/handler/component/MockResponseBuilder.java
b/solr/core/src/test/org/apache/solr/handler/component/MockResponseBuilder.java
index fa8bca48727..fb53949515d 100644
---
a/solr/core/src/test/org/apache/solr/handler/component/MockResponseBuilder.java
+++
b/solr/core/src/test/org/apache/solr/handler/component/MockResponseBuilder.java
@@ -18,14 +18,19 @@ package org.apache.solr.handler.component;
import java.util.ArrayList;
import java.util.List;
+import org.apache.solr.common.params.CombinerParams;
+import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ShardParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.schema.StrField;
+import org.apache.solr.search.ReturnFields;
import org.apache.solr.search.SortSpec;
import org.mockito.Mockito;
@@ -66,4 +71,24 @@ public class MockResponseBuilder extends ResponseBuilder {
this.setSortSpec(sortSpec);
return this;
}
+
+ public MockResponseBuilder withCombinerQueries(String... combinerQueries) {
+ SolrParams params = req.getParams();
+
Mockito.when(params.getParams(CombinerParams.COMBINER_QUERY)).thenReturn(combinerQueries);
+ Mockito.when(params.get(CombinerParams.COMBINER_ALGORITHM,
CombinerParams.DEFAULT_COMBINER))
+ .thenReturn(CombinerParams.DEFAULT_COMBINER);
+ SolrCore core = Mockito.mock(SolrCore.class);
+
Mockito.when(core.getCoreContainer()).thenReturn(Mockito.mock(CoreContainer.class));
+ Mockito.when(req.getCore()).thenReturn(core);
+
Mockito.when(rsp.getReturnFields()).thenReturn(Mockito.mock(ReturnFields.class));
+ return this;
+ }
+
+ public MockResponseBuilder withShardsTolerant() {
+ SolrParams params = req.getParams();
+ Mockito.when(params.get(ShardParams.SHARDS_TOLERANT)).thenReturn("true");
+ Mockito.when(params.getBool(Mockito.eq(CommonParams.PARTIAL_RESULTS),
Mockito.anyBoolean()))
+ .thenReturn(true);
+ return this;
+ }
}
diff --git
a/solr/core/src/test/org/apache/solr/handler/component/MockShardRequest.java
b/solr/core/src/test/org/apache/solr/handler/component/MockShardRequest.java
index 3a3b6585714..e2fd6587c7a 100644
--- a/solr/core/src/test/org/apache/solr/handler/component/MockShardRequest.java
+++ b/solr/core/src/test/org/apache/solr/handler/component/MockShardRequest.java
@@ -17,6 +17,7 @@
package org.apache.solr.handler.component;
import java.util.ArrayList;
+import java.util.List;
import org.apache.solr.client.solrj.SolrResponse;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.util.NamedList;
@@ -30,6 +31,14 @@ public class MockShardRequest extends ShardRequest {
return mockShardRequest;
}
+ public static NamedList<Object> header(Object... nameValues) {
+ NamedList<Object> header = new NamedList<>();
+ for (int i = 0; i < nameValues.length; i += 2) {
+ header.add((String) nameValues[i], nameValues[i + 1]);
+ }
+ return header;
+ }
+
public MockShardRequest withShardResponse(
NamedList<Object> responseHeader, SolrDocumentList solrDocuments) {
ShardResponse shardResponse = buildShardResponse(responseHeader,
solrDocuments);
@@ -37,6 +46,38 @@ public class MockShardRequest extends ShardRequest {
return this;
}
+ public MockShardRequest withShardResponse(
+ String shard,
+ NamedList<Object> responseHeader,
+ SolrDocumentList combinedResponse,
+ List<SolrDocumentList> perQueryResponses,
+ NamedList<List<Object>> sortValues0) {
+ NamedList<Object> response = new NamedList<>();
+ response.add("responseHeader", responseHeader);
+ response.add("response", combinedResponse);
+ if (perQueryResponses != null) {
+ response.add("response_per_query", perQueryResponses);
+ }
+ if (sortValues0 != null) {
+ response.add("sort_values_0", sortValues0);
+ }
+ SolrResponse solrResponse = Mockito.mock(SolrResponse.class);
+ Mockito.when(solrResponse.getResponse()).thenReturn(response);
+ ShardResponse shardResponse = new ShardResponse();
+ shardResponse.setShard(shard);
+ shardResponse.setSolrResponse(solrResponse);
+ responses.add(shardResponse);
+ return this;
+ }
+
+ public MockShardRequest withErroredShard(String shard, Throwable exception) {
+ ShardResponse shardResponse = new ShardResponse();
+ shardResponse.setShard(shard);
+ shardResponse.setException(exception);
+ responses.add(shardResponse);
+ return this;
+ }
+
private ShardResponse buildShardResponse(
NamedList<Object> responseHeader, SolrDocumentList solrDocuments) {
SolrResponse solrResponse = Mockito.mock(SolrResponse.class);
diff --git
a/solr/core/src/test/org/apache/solr/handler/component/MockSortSpecBuilder.java
b/solr/core/src/test/org/apache/solr/handler/component/MockSortSpecBuilder.java
index 5c42abe0af7..cbfe004b3a7 100644
---
a/solr/core/src/test/org/apache/solr/handler/component/MockSortSpecBuilder.java
+++
b/solr/core/src/test/org/apache/solr/handler/component/MockSortSpecBuilder.java
@@ -16,8 +16,10 @@
*/
package org.apache.solr.handler.component;
+import java.util.List;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
+import org.apache.solr.schema.SchemaField;
import org.apache.solr.search.SortSpec;
import org.mockito.Mockito;
@@ -39,6 +41,11 @@ public class MockSortSpecBuilder {
return this;
}
+ public MockSortSpecBuilder withSchemaFields(List<SchemaField> schemaFields) {
+ Mockito.when(sortSpec.getSchemaFields()).thenReturn(schemaFields);
+ return this;
+ }
+
public MockSortSpecBuilder withIncludesNonScoreOrDocSortField(boolean
include) {
Mockito.when(sortSpec.includesNonScoreOrDocField()).thenReturn(include);
return this;