31
31
import org .apache .lucene .search .BooleanClause ;
32
32
import org .apache .lucene .search .BooleanQuery ;
33
33
import org .apache .lucene .search .Collector ;
34
+ import org .apache .lucene .search .CollectorManager ;
34
35
import org .apache .lucene .search .ConstantScoreQuery ;
35
36
import org .apache .lucene .search .DocValuesFieldExistsQuery ;
36
37
import org .apache .lucene .search .FieldDoc ;
37
38
import org .apache .lucene .search .MatchAllDocsQuery ;
38
39
import org .apache .lucene .search .Query ;
39
40
import org .apache .lucene .search .ScoreDoc ;
41
+ import org .apache .lucene .search .ScoreMode ;
40
42
import org .apache .lucene .search .Sort ;
41
43
import org .apache .lucene .search .SortField ;
42
44
import org .apache .lucene .search .TopDocs ;
45
+ import org .apache .lucene .search .TopFieldCollector ;
43
46
import org .apache .lucene .search .TopFieldDocs ;
44
47
import org .apache .lucene .search .TotalHits ;
45
48
import org .apache .lucene .search .Weight ;
71
74
import java .io .IOException ;
72
75
import java .util .ArrayList ;
73
76
import java .util .Arrays ;
77
+ import java .util .Collection ;
74
78
import java .util .Collections ;
75
79
import java .util .Comparator ;
76
80
import java .util .LinkedList ;
@@ -236,7 +240,7 @@ static boolean executeInternal(SearchContext searchContext) throws QueryPhaseExe
236
240
newFormats [0 ] = DocValueFormat .RAW ;
237
241
// Add a tiebreak on _doc in order to be able to search
238
242
// the leaves in any order. This is needed since we reorder
239
- // the leaves based on the minimum value in each segment.
243
+ // the leaves based on the minimum/maxim value in each segment.
240
244
newSortFields [newSortFields .length -1 ] = SortField .FIELD_DOC ;
241
245
newFormats [newSortFields .length -1 ] = DocValueFormat .RAW ;
242
246
System .arraycopy (oldSortFields , 0 , newSortFields , 1 , oldSortFields .length );
@@ -286,61 +290,20 @@ static boolean executeInternal(SearchContext searchContext) throws QueryPhaseExe
286
290
} else {
287
291
checkCancelled = null ;
288
292
}
289
-
290
293
searcher .setCheckCancelled (checkCancelled );
291
294
292
- final boolean doProfile = searchContext .getProfilers () != null ;
293
- // create the top docs collector last when the other collectors are known
294
- final TopDocsCollectorContext topDocsFactory = createTopDocsCollectorContext (searchContext , hasFilterCollector );
295
- // add the top docs collector, the first collector context in the chain
296
- collectors .addFirst (topDocsFactory );
297
-
298
- final Collector queryCollector ;
299
- if (doProfile ) {
300
- InternalProfileCollector profileCollector = QueryCollectorContext .createQueryCollectorWithProfiler (collectors );
301
- searchContext .getProfilers ().getCurrentQueryProfiler ().setCollector (profileCollector );
302
- queryCollector = profileCollector ;
295
+ boolean shouldRescore ;
296
+ // if we are optimizing sort and there are no other collectors
297
+ if (sortAndFormatsForRewrittenNumericSort != null && collectors .size () == 0 && searchContext .getProfilers () == null ) {
298
+ shouldRescore = searchWithCollectorManager (searchContext , searcher , query , leafSorter , timeoutSet );
303
299
} else {
304
- queryCollector = QueryCollectorContext .createQueryCollector (collectors );
305
- }
306
-
307
- try {
308
- Weight weight = searcher .createWeight (searcher .rewrite (query ), queryCollector .scoreMode (), 1f );
309
- // We search the leaves in a different order when the numeric sort optimization is
310
- // activated. Collectors expect leaves in order when searching but this is fine in this
311
- // case since we only have a TopFieldCollector and we force the tiebreak on _doc.
312
- List <LeafReaderContext > leaves = new ArrayList <>(searcher .getIndexReader ().leaves ());
313
- leafSorter .accept (leaves );
314
- for (LeafReaderContext ctx : leaves ) {
315
- searcher .searchLeaf (ctx , weight , queryCollector );
316
- }
317
- } catch (EarlyTerminatingCollector .EarlyTerminationException e ) {
318
- queryResult .terminatedEarly (true );
319
- } catch (TimeExceededException e ) {
320
- assert timeoutSet : "TimeExceededException thrown even though timeout wasn't set" ;
321
-
322
- if (searchContext .request ().allowPartialSearchResults () == false ) {
323
- // Can't rethrow TimeExceededException because not serializable
324
- throw new QueryPhaseExecutionException (searchContext , "Time exceeded" );
325
- }
326
- queryResult .searchTimedOut (true );
327
- } finally {
328
- searchContext .clearReleasables (SearchContext .Lifetime .COLLECTION );
329
- }
330
- if (searchContext .terminateAfter () != SearchContext .DEFAULT_TERMINATE_AFTER
331
- && queryResult .terminatedEarly () == null ) {
332
- queryResult .terminatedEarly (false );
333
- }
334
-
335
- final QuerySearchResult result = searchContext .queryResult ();
336
- for (QueryCollectorContext ctx : collectors ) {
337
- ctx .postProcess (result );
300
+ shouldRescore = searchWithCollector (searchContext , searcher , query , collectors , hasFilterCollector , timeoutSet );
338
301
}
339
302
340
303
// if we rewrote numeric long or date sort, restore fieldDocs based on the original sort
341
304
if (sortAndFormatsForRewrittenNumericSort != null ) {
342
305
searchContext .sort (sortAndFormatsForRewrittenNumericSort ); // restore SortAndFormats
343
- restoreTopFieldDocs (result , sortAndFormatsForRewrittenNumericSort );
306
+ restoreTopFieldDocs (queryResult , sortAndFormatsForRewrittenNumericSort );
344
307
}
345
308
346
309
ExecutorService executor = searchContext .indexShard ().getThreadPool ().executor (ThreadPool .Names .SEARCH );
@@ -351,14 +314,123 @@ static boolean executeInternal(SearchContext searchContext) throws QueryPhaseExe
351
314
}
352
315
if (searchContext .getProfilers () != null ) {
353
316
ProfileShardResult shardResults = SearchProfileShardResults .buildShardResults (searchContext .getProfilers ());
354
- result .profileResults (shardResults );
317
+ queryResult .profileResults (shardResults );
355
318
}
356
- return topDocsFactory . shouldRescore () ;
319
+ return shouldRescore ;
357
320
} catch (Exception e ) {
358
321
throw new QueryPhaseExecutionException (searchContext , "Failed to execute main query" , e );
359
322
}
360
323
}
361
324
325
+ private static boolean searchWithCollector (SearchContext searchContext , ContextIndexSearcher searcher , Query query ,
326
+ LinkedList <QueryCollectorContext > collectors , boolean hasFilterCollector , boolean timeoutSet ) throws IOException {
327
+ // create the top docs collector last when the other collectors are known
328
+ final TopDocsCollectorContext topDocsFactory = createTopDocsCollectorContext (searchContext , hasFilterCollector );
329
+ // add the top docs collector, the first collector context in the chain
330
+ collectors .addFirst (topDocsFactory );
331
+
332
+ final Collector queryCollector ;
333
+ if ( searchContext .getProfilers () != null ) {
334
+ InternalProfileCollector profileCollector = QueryCollectorContext .createQueryCollectorWithProfiler (collectors );
335
+ searchContext .getProfilers ().getCurrentQueryProfiler ().setCollector (profileCollector );
336
+ queryCollector = profileCollector ;
337
+ } else {
338
+ queryCollector = QueryCollectorContext .createQueryCollector (collectors );
339
+ }
340
+ QuerySearchResult queryResult = searchContext .queryResult ();
341
+ try {
342
+ searcher .search (query , queryCollector );
343
+ } catch (EarlyTerminatingCollector .EarlyTerminationException e ) {
344
+ queryResult .terminatedEarly (true );
345
+ } catch (TimeExceededException e ) {
346
+ assert timeoutSet : "TimeExceededException thrown even though timeout wasn't set" ;
347
+ if (searchContext .request ().allowPartialSearchResults () == false ) {
348
+ // Can't rethrow TimeExceededException because not serializable
349
+ throw new QueryPhaseExecutionException (searchContext , "Time exceeded" );
350
+ }
351
+ queryResult .searchTimedOut (true );
352
+ } finally {
353
+ searchContext .clearReleasables (SearchContext .Lifetime .COLLECTION );
354
+ }
355
+ if (searchContext .terminateAfter () != SearchContext .DEFAULT_TERMINATE_AFTER && queryResult .terminatedEarly () == null ) {
356
+ queryResult .terminatedEarly (false );
357
+ }
358
+ for (QueryCollectorContext ctx : collectors ) {
359
+ ctx .postProcess (queryResult );
360
+ }
361
+ return topDocsFactory .shouldRescore ();
362
+ }
363
+
364
+ // we use collectorManager during sort optimization
365
+ // for the sort optimization, we have already checked that there are no other collectors, no filters,
366
+ // no search after, no scroll, no collapse, no track scores
367
+ // this means we can use TopFieldCollector directly
368
+ private static boolean searchWithCollectorManager (SearchContext searchContext , ContextIndexSearcher searcher , Query query ,
369
+ CheckedConsumer <List <LeafReaderContext >, IOException > leafSorter , boolean timeoutSet ) throws IOException {
370
+ final IndexReader reader = searchContext .searcher ().getIndexReader ();
371
+ final int numHits = Math .min (searchContext .from () + searchContext .size (), Math .max (1 , reader .numDocs ()));
372
+ final SortAndFormats sortAndFormats = searchContext .sort ();
373
+
374
+ int totalHitsThreshold ;
375
+ TotalHits totalHits ;
376
+ if (searchContext .trackTotalHitsUpTo () == SearchContext .TRACK_TOTAL_HITS_DISABLED ) {
377
+ totalHitsThreshold = 1 ;
378
+ totalHits = new TotalHits (0 , TotalHits .Relation .GREATER_THAN_OR_EQUAL_TO );
379
+ } else {
380
+ int hitCount = shortcutTotalHitCount (reader , query );
381
+ if (hitCount == -1 ) {
382
+ totalHitsThreshold = searchContext .trackTotalHitsUpTo ();
383
+ totalHits = null ; // will be computed via the collector
384
+ } else {
385
+ totalHitsThreshold = 1 ;
386
+ totalHits = new TotalHits (hitCount , TotalHits .Relation .EQUAL_TO ); // don't compute hit counts via the collector
387
+ }
388
+ }
389
+
390
+ CollectorManager <TopFieldCollector , Void > manager = new CollectorManager <>() {
391
+ @ Override
392
+ public TopFieldCollector newCollector () throws IOException {
393
+ return TopFieldCollector .create (sortAndFormats .sort , numHits , null , totalHitsThreshold );
394
+ }
395
+ @ Override
396
+ public Void reduce (Collection <TopFieldCollector > collectors ) throws IOException {
397
+ TopFieldDocs [] topDocsArr = new TopFieldDocs [collectors .size ()];
398
+ int i = 0 ;
399
+ for (TopFieldCollector collector : collectors ) {
400
+ topDocsArr [i ++] = collector .topDocs ();
401
+ }
402
+ // we have to set setShardIndex to true, as Lucene can't have ScoreDocs without shardIndex set
403
+ TopFieldDocs mergedTopDocs = TopDocs .merge (sortAndFormats .sort , 0 , numHits , topDocsArr , true );
404
+ // reset shard index for all topDocs; ES will set shard index later during reduce stage
405
+ for (ScoreDoc scoreDoc : mergedTopDocs .scoreDocs ) {
406
+ scoreDoc .shardIndex = -1 ;
407
+ }
408
+ if (totalHits != null ) { // we have already precalculated totalHits for the whole index
409
+ mergedTopDocs = new TopFieldDocs (totalHits , mergedTopDocs .scoreDocs , mergedTopDocs .fields );
410
+ }
411
+ searchContext .queryResult ().topDocs (new TopDocsAndMaxScore (mergedTopDocs , Float .NaN ), sortAndFormats .formats );
412
+ return null ;
413
+ }
414
+ };
415
+
416
+ List <LeafReaderContext > leaves = new ArrayList <>(searcher .getIndexReader ().leaves ());
417
+ leafSorter .accept (leaves );
418
+ try {
419
+ Weight weight = searcher .createWeight (searcher .rewrite (query ), ScoreMode .TOP_SCORES , 1f );
420
+ searcher .search (leaves , weight , manager );
421
+ } catch (TimeExceededException e ) {
422
+ assert timeoutSet : "TimeExceededException thrown even though timeout wasn't set" ;
423
+ if (searchContext .request ().allowPartialSearchResults () == false ) {
424
+ // Can't rethrow TimeExceededException because not serializable
425
+ throw new QueryPhaseExecutionException (searchContext , "Time exceeded" );
426
+ }
427
+ searchContext .queryResult ().searchTimedOut (true );
428
+ } finally {
429
+ searchContext .clearReleasables (SearchContext .Lifetime .COLLECTION );
430
+ }
431
+ return false ; // no rescoring when sorting by field
432
+ }
433
+
362
434
private static Query tryRewriteLongSort (SearchContext searchContext , IndexReader reader ,
363
435
Query query , boolean hasFilterCollector ) throws IOException {
364
436
if (searchContext .searchAfter () != null ) return null ;
@@ -399,7 +471,7 @@ private static Query tryRewriteLongSort(SearchContext searchContext, IndexReader
399
471
if (missingValuesAccordingToSort == false ) return null ;
400
472
401
473
int docCount = PointValues .getDocCount (reader , fieldName );
402
- // is not worth to run optimization on small index
474
+ // is not worth to run optimization on small index
403
475
if (docCount <= 512 ) return null ;
404
476
405
477
// check for multiple values
0 commit comments