Skip to content

Commit f5a2af6

Browse files
authored
Query phase: fold collector wrappers into a single top level collector (elastic#97030)
The query phase uses a number of different collectors and combines them together, pretty much one per feature that the search API exposes: there is a collector for post_filter, one for min_score, one for terminate_after, one for aggs. While this is very flexible, we always combine such collectors together in the same way (e.g. terminate_after must be the first one, post_filter is only applied to top docs collection, min score is applied to both aggs and top docs). This means that despite we could flexibly compose collectors, we need to apply each feature predictably which makes the composability not needed. Furthermore, composability causes complexity. The terminate_after functionality is a clear example of complexity introduced as a consequence of having a complex collector tree: it relies on a multi collector, and throws an exception to force terminating the collection for all other collectors in the tree. If there was a single collector aware of post_filter, min_score and terminate_after at the same time, we could simply reuse Lucene mechanisms to early terminate the collection (CollectionTerminatedException) instead of forcing the termination throwing an exception that Lucene does not handle. Furthermore, MultiCollector is a complex and generic collector to combine multiple collectors together, while we always every combine maximum two collectors with it, which are more or less fixed (e.g. top docs and aggs). This PR introduces a new top-level collector that is inspired by MultiCollector in that it holds the top docs and the optional aggs collector and applies post_filter, min_score as well as terminate_after as part of its execution. This allows us to have a specialized collector for our needs, less flexibility and more control. This surfaced some strange behaviour that we may want to change as a follow-up in how terminate_after makes us collecting docs even when all possible collections have been early terminated. The goal of this PR though is to have feature parity with query phase before the refactoring, without any change of behaviour. A nice benefit of this work is that it allows us to rely on CollectionTerminatedException for the terminate_after functionality. This simplifies the introduction of multi-threaded collector managers when it comes to handling exceptions.
1 parent 684af2d commit f5a2af6

File tree

11 files changed

+1635
-661
lines changed

11 files changed

+1635
-661
lines changed

docs/reference/search/profile.asciidoc

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,16 @@ The API returns the following result:
166166
"rewrite_time": 451233,
167167
"collector": [
168168
{
169-
"name": "SimpleTopScoreDocCollector",
170-
"reason": "search_top_hits",
171-
"time_in_nanos": 775274
169+
"name": "QueryPhaseCollector",
170+
"reason": "search_query_phase",
171+
"time_in_nanos": 775274,
172+
"children" : [
173+
{
174+
"name": "SimpleTopScoreDocCollector",
175+
"reason": "search_top_hits",
176+
"time_in_nanos": 775274
177+
}
178+
]
172179
}
173180
]
174181
}
@@ -509,9 +516,16 @@ Looking at the previous example:
509516
--------------------------------------------------
510517
"collector": [
511518
{
512-
"name": "SimpleTopScoreDocCollector",
513-
"reason": "search_top_hits",
514-
"time_in_nanos": 775274
519+
"name": "QueryPhaseCollector",
520+
"reason": "search_query_phase",
521+
"time_in_nanos": 775274,
522+
"children" : [
523+
{
524+
"name": "SimpleTopScoreDocCollector",
525+
"reason": "search_top_hits",
526+
"time_in_nanos": 775274
527+
}
528+
]
515529
}
516530
]
517531
--------------------------------------------------
@@ -520,14 +534,14 @@ Looking at the previous example:
520534
// TESTRESPONSE[s/(?<=[" ])\d+(\.\d+)?/$body.$_path/]
521535

522536

523-
We see a single collector named `SimpleTopScoreDocCollector` wrapped into
524-
`CancellableCollector`. `SimpleTopScoreDocCollector` is the default "scoring and
525-
sorting" `Collector` used by {es}. The `reason` field attempts to give a plain
526-
English description of the class name. The `time_in_nanos` is similar to the
527-
time in the Query tree: a wall-clock time inclusive of all children. Similarly,
528-
`children` lists all sub-collectors. The `CancellableCollector` that wraps
529-
`SimpleTopScoreDocCollector` is used by {es} to detect if the current search was
530-
cancelled and stop collecting documents as soon as it occurs.
537+
We see a top-level collector named `QueryPhaseCollector` which holds a child
538+
`SimpleTopScoreDocCollector`. `SimpleTopScoreDocCollector` is the default
539+
"scoring and sorting" `Collector` used by {es}. The `reason` field attempts
540+
to give a plain English description of the class name. The `time_in_nanos`
541+
is similar to the time in the Query tree: a wall-clock time inclusive of all
542+
children. Similarly, `children` lists all sub-collectors. When aggregations
543+
are requested, the `QueryPhaseCollector` will hold an additional child
544+
collector with reason `aggregation` that is the one performing aggregations.
531545

532546
It should be noted that Collector times are **independent** from the Query
533547
times. They are calculated, combined, and normalized independently! Due to the
@@ -537,7 +551,7 @@ Collectors into the Query section, so they are displayed in separate portions.
537551
For reference, the various collector reasons are:
538552

539553
[horizontal]
540-
`search_sorted`::
554+
`search_top_hits`::
541555

542556
A collector that scores and sorts documents. This is the most common collector and will be seen in most
543557
simple searches
@@ -547,20 +561,13 @@ For reference, the various collector reasons are:
547561
A collector that only counts the number of documents that match the query, but does not fetch the source.
548562
This is seen when `size: 0` is specified
549563

550-
`search_terminate_after_count`::
551-
552-
A collector that terminates search execution after `n` matching documents have been found. This is seen
553-
when the `terminate_after_count` query parameter has been specified
554-
555-
`search_min_score`::
556-
557-
A collector that only returns matching documents that have a score greater than `n`. This is seen when
558-
the top-level parameter `min_score` has been specified.
559-
560-
`search_multi`::
564+
`search_query_phase`::
561565

562-
A collector that wraps several other collectors. This is seen when combinations of search, aggregations,
563-
global aggs, and post_filters are combined in a single search.
566+
A collector that incorporates collecting top hits as well aggregations as part of the query phase.
567+
It supports terminating the search execution after `n` matching documents have been found (when
568+
`terminate_after` is specified), as well as only returning matching documents that have a score
569+
greater than `n` (when `min_score` is provided). Additionally, it is able to filter matching top
570+
hits based on the provided `post_filter`.
564571

565572
`search_timeout`::
566573

@@ -723,21 +730,14 @@ The API returns the following result:
723730
"rewrite_time": 4769,
724731
"collector": [
725732
{
726-
"name": "MultiCollector",
727-
"reason": "search_multi",
733+
"name": "QueryPhaseCollector",
734+
"reason": "search_query_phase",
728735
"time_in_nanos": 1945072,
729736
"children": [
730737
{
731-
"name": "FilteredCollector",
732-
"reason": "search_post_filter",
733-
"time_in_nanos": 500850,
734-
"children": [
735-
{
736-
"name": "SimpleTopScoreDocCollector",
737-
"reason": "search_top_hits",
738-
"time_in_nanos": 22577
739-
}
740-
]
738+
"name": "SimpleTopScoreDocCollector",
739+
"reason": "search_top_hits",
740+
"time_in_nanos": 22577
741741
},
742742
{
743743
"name": "BucketCollectorWrapper: [BucketCollectorWrapper[bucketCollector=[my_scoped_agg, my_global_agg]]]",
@@ -772,9 +772,9 @@ major portions of the query are represented:
772772
2. The second `TermQuery` (message:search) represents the `post_filter` query.
773773

774774
The Collector tree is fairly straightforward, showing how a single
775-
CancellableCollector wraps a MultiCollector which also wraps a FilteredCollector
776-
to execute the post_filter (and in turn wraps the normal scoring
777-
SimpleCollector), a BucketCollector to run all scoped aggregations.
775+
QueryPhaseCollector that holds the normal scoring SimpleTopScoreDocCollector
776+
used to collect top hits, as well as BucketCollectorWrapper to run all scoped
777+
aggregations.
778778

779779
===== Understanding MultiTermQuery output
780780

server/src/main/java/org/elasticsearch/common/lucene/MinimumScoreCollector.java

Lines changed: 0 additions & 98 deletions
This file was deleted.

server/src/main/java/org/elasticsearch/common/lucene/search/FilteredCollector.java

Lines changed: 0 additions & 89 deletions
This file was deleted.

server/src/main/java/org/elasticsearch/search/profile/query/CollectorResult.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,8 @@ public class CollectorResult extends ProfilerCollectorResult implements ToXConte
3737

3838
public static final String REASON_SEARCH_COUNT = "search_count";
3939
public static final String REASON_SEARCH_TOP_HITS = "search_top_hits";
40-
public static final String REASON_SEARCH_TERMINATE_AFTER_COUNT = "search_terminate_after_count";
41-
public static final String REASON_SEARCH_POST_FILTER = "search_post_filter";
42-
public static final String REASON_SEARCH_MIN_SCORE = "search_min_score";
4340
public static final String REASON_SEARCH_MULTI = "search_multi";
41+
public static final String REASON_SEARCH_QUERY_PHASE = "search_query_phase";
4442
public static final String REASON_AGGREGATION = "aggregation";
4543
public static final String REASON_AGGREGATION_GLOBAL = "aggregation_global";
4644

0 commit comments

Comments
 (0)