This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_10x in repository https://gitbox.apache.org/repos/asf/solr.git
commit 3a6ef7970c213bea206354f8316bad108296af08 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. (cherry picked from commit c8abb33884cb369bfcaac47b6b02b2e1a0c850aa) --- .../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 b279ceb2ff5..2be2231c409 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;
