7474 */
7575public class SearchPhaseController extends AbstractComponent {
7676
77- private static final Comparator <AtomicArray .Entry <? extends QuerySearchResultProvider >> QUERY_RESULT_ORDERING = (o1 , o2 ) -> {
78- int i = o1 .value .shardTarget ().getIndex ().compareTo (o2 .value .shardTarget ().getIndex ());
79- if (i == 0 ) {
80- i = o1 .value .shardTarget ().getShardId ().id () - o2 .value .shardTarget ().getShardId ().id ();
81- }
82- return i ;
83- };
84-
8577 private static final ScoreDoc [] EMPTY_DOCS = new ScoreDoc [0 ];
8678
8779 private final BigArrays bigArrays ;
@@ -153,6 +145,9 @@ private static long optionalSum(long left, long right) {
153145 * named completion suggestion across all shards. If more than one named completion suggestion is specified in the
154146 * request, the suggest docs for a named suggestion are ordered by the suggestion name.
155147 *
148+ * Note: The order of the sorted score docs depends on the shard index in the result array if the merge process needs to disambiguate
149+ * the result. In oder to obtain stable results the shard index (index of the result in the result array) must be the same.
150+ *
156151 * @param ignoreFrom Whether to ignore the from and sort all hits in each shard result.
157152 * Enabled only for scroll search, because that only retrieves hits of length 'size' in the query phase.
158153 * @param resultsArr Shard result holder
@@ -163,26 +158,31 @@ public ScoreDoc[] sortDocs(boolean ignoreFrom, AtomicArray<? extends QuerySearch
163158 return EMPTY_DOCS ;
164159 }
165160
161+ final QuerySearchResult result ;
166162 boolean canOptimize = false ;
167- QuerySearchResult result = null ;
168163 int shardIndex = -1 ;
169164 if (results .size () == 1 ) {
170165 canOptimize = true ;
171166 result = results .get (0 ).value .queryResult ();
172167 shardIndex = results .get (0 ).index ;
173168 } else {
169+ boolean hasResult = false ;
170+ QuerySearchResult resultToOptimize = null ;
174171 // lets see if we only got hits from a single shard, if so, we can optimize...
175172 for (AtomicArray .Entry <? extends QuerySearchResultProvider > entry : results ) {
176173 if (entry .value .queryResult ().hasHits ()) {
177- if (result != null ) { // we already have one, can't really optimize
174+ if (hasResult ) { // we already have one, can't really optimize
178175 canOptimize = false ;
179176 break ;
180177 }
181178 canOptimize = true ;
182- result = entry .value .queryResult ();
179+ hasResult = true ;
180+ resultToOptimize = entry .value .queryResult ();
183181 shardIndex = entry .index ;
184182 }
185183 }
184+ result = canOptimize ? resultToOptimize : results .get (0 ).value .queryResult ();
185+ assert result != null ;
186186 }
187187 if (canOptimize ) {
188188 int offset = result .from ();
@@ -228,74 +228,62 @@ public ScoreDoc[] sortDocs(boolean ignoreFrom, AtomicArray<? extends QuerySearch
228228 return docs ;
229229 }
230230
231- @ SuppressWarnings ("unchecked" )
232- AtomicArray .Entry <? extends QuerySearchResultProvider >[] sortedResults = results .toArray (new AtomicArray .Entry [results .size ()]);
233- Arrays .sort (sortedResults , QUERY_RESULT_ORDERING );
234- QuerySearchResultProvider firstResult = sortedResults [0 ].value ;
235-
236- int topN = firstResult .queryResult ().size ();
237- int from = firstResult .queryResult ().from ();
238- if (ignoreFrom ) {
239- from = 0 ;
240- }
231+ final int topN = result .queryResult ().size ();
232+ final int from = ignoreFrom ? 0 : result .queryResult ().from ();
241233
242234 final TopDocs mergedTopDocs ;
243- int numShards = resultsArr .length ();
244- if (firstResult .queryResult ().topDocs () instanceof CollapseTopFieldDocs ) {
245- CollapseTopFieldDocs firstTopDocs = (CollapseTopFieldDocs ) firstResult .queryResult ().topDocs ();
235+ final int numShards = resultsArr .length ();
236+ if (result .queryResult ().topDocs () instanceof CollapseTopFieldDocs ) {
237+ CollapseTopFieldDocs firstTopDocs = (CollapseTopFieldDocs ) result .queryResult ().topDocs ();
246238 final Sort sort = new Sort (firstTopDocs .fields );
247239
248240 final CollapseTopFieldDocs [] shardTopDocs = new CollapseTopFieldDocs [numShards ];
249- for (AtomicArray .Entry <? extends QuerySearchResultProvider > sortedResult : sortedResults ) {
241+ if (result .size () != shardTopDocs .length ) {
242+ // TopDocs#merge can't deal with null shard TopDocs
243+ final CollapseTopFieldDocs empty = new CollapseTopFieldDocs (firstTopDocs .field , 0 , new FieldDoc [0 ],
244+ sort .getSort (), new Object [0 ], Float .NaN );
245+ Arrays .fill (shardTopDocs , empty );
246+ }
247+ for (AtomicArray .Entry <? extends QuerySearchResultProvider > sortedResult : results ) {
250248 TopDocs topDocs = sortedResult .value .queryResult ().topDocs ();
251249 // the 'index' field is the position in the resultsArr atomic array
252250 shardTopDocs [sortedResult .index ] = (CollapseTopFieldDocs ) topDocs ;
253251 }
254- // TopDocs#merge can't deal with null shard TopDocs
255- for (int i = 0 ; i < shardTopDocs .length ; ++i ) {
256- if (shardTopDocs [i ] == null ) {
257- shardTopDocs [i ] = new CollapseTopFieldDocs (firstTopDocs .field , 0 , new FieldDoc [0 ],
258- sort .getSort (), new Object [0 ], Float .NaN );
259- }
260- }
261252 mergedTopDocs = CollapseTopFieldDocs .merge (sort , from , topN , shardTopDocs );
262- } else if (firstResult .queryResult ().topDocs () instanceof TopFieldDocs ) {
263- TopFieldDocs firstTopDocs = (TopFieldDocs ) firstResult .queryResult ().topDocs ();
253+ } else if (result .queryResult ().topDocs () instanceof TopFieldDocs ) {
254+ TopFieldDocs firstTopDocs = (TopFieldDocs ) result .queryResult ().topDocs ();
264255 final Sort sort = new Sort (firstTopDocs .fields );
265256
266257 final TopFieldDocs [] shardTopDocs = new TopFieldDocs [resultsArr .length ()];
267- for (AtomicArray .Entry <? extends QuerySearchResultProvider > sortedResult : sortedResults ) {
258+ if (result .size () != shardTopDocs .length ) {
259+ // TopDocs#merge can't deal with null shard TopDocs
260+ final TopFieldDocs empty = new TopFieldDocs (0 , new FieldDoc [0 ], sort .getSort (), Float .NaN );
261+ Arrays .fill (shardTopDocs , empty );
262+ }
263+ for (AtomicArray .Entry <? extends QuerySearchResultProvider > sortedResult : results ) {
268264 TopDocs topDocs = sortedResult .value .queryResult ().topDocs ();
269265 // the 'index' field is the position in the resultsArr atomic array
270266 shardTopDocs [sortedResult .index ] = (TopFieldDocs ) topDocs ;
271267 }
272- // TopDocs#merge can't deal with null shard TopDocs
273- for (int i = 0 ; i < shardTopDocs .length ; ++i ) {
274- if (shardTopDocs [i ] == null ) {
275- shardTopDocs [i ] = new TopFieldDocs (0 , new FieldDoc [0 ], sort .getSort (), Float .NaN );
276- }
277- }
278268 mergedTopDocs = TopDocs .merge (sort , from , topN , shardTopDocs );
279269 } else {
280270 final TopDocs [] shardTopDocs = new TopDocs [resultsArr .length ()];
281- for (AtomicArray .Entry <? extends QuerySearchResultProvider > sortedResult : sortedResults ) {
271+ if (result .size () != shardTopDocs .length ) {
272+ // TopDocs#merge can't deal with null shard TopDocs
273+ Arrays .fill (shardTopDocs , Lucene .EMPTY_TOP_DOCS );
274+ }
275+ for (AtomicArray .Entry <? extends QuerySearchResultProvider > sortedResult : results ) {
282276 TopDocs topDocs = sortedResult .value .queryResult ().topDocs ();
283277 // the 'index' field is the position in the resultsArr atomic array
284278 shardTopDocs [sortedResult .index ] = topDocs ;
285279 }
286- // TopDocs#merge can't deal with null shard TopDocs
287- for (int i = 0 ; i < shardTopDocs .length ; ++i ) {
288- if (shardTopDocs [i ] == null ) {
289- shardTopDocs [i ] = Lucene .EMPTY_TOP_DOCS ;
290- }
291- }
292280 mergedTopDocs = TopDocs .merge (from , topN , shardTopDocs );
293281 }
294282
295283 ScoreDoc [] scoreDocs = mergedTopDocs .scoreDocs ;
296284 final Map <String , List <Suggestion <CompletionSuggestion .Entry >>> groupedCompletionSuggestions = new HashMap <>();
297285 // group suggestions and assign shard index
298- for (AtomicArray .Entry <? extends QuerySearchResultProvider > sortedResult : sortedResults ) {
286+ for (AtomicArray .Entry <? extends QuerySearchResultProvider > sortedResult : results ) {
299287 Suggest shardSuggest = sortedResult .value .queryResult ().suggest ();
300288 if (shardSuggest != null ) {
301289 for (CompletionSuggestion suggestion : shardSuggest .filter (CompletionSuggestion .class )) {
0 commit comments