Skip to content

Commit ec8d5ec

Browse files
committed
Fix handling of terminate_after when size is 0 (#58212)
`terminate_after` is ignored on search requests that don't return top hits (`size` set to 0) and do not tracked the number of hits accurately (`track_total_hits`). We use early termination when the number of hits to track is reached during collection but this breaks the hard termination of `terminate_after` if it happens before we reached the `terminate_after` value. This change ensures that we continue to check `terminate_after` even if the tracking of total hits has reached the provided value. Closes #57624
1 parent 796cb9e commit ec8d5ec

File tree

2 files changed

+59
-5
lines changed

2 files changed

+59
-5
lines changed

server/src/main/java/org/elasticsearch/search/query/QueryCollectorContext.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.lucene.search.MultiCollector;
2525
import org.apache.lucene.search.Query;
2626
import org.apache.lucene.search.ScoreMode;
27+
import org.apache.lucene.search.SimpleCollector;
2728
import org.apache.lucene.search.Weight;
2829
import org.elasticsearch.common.lucene.MinimumScoreCollector;
2930
import org.elasticsearch.common.lucene.search.FilteredCollector;
@@ -41,6 +42,17 @@
4142
import static org.elasticsearch.search.profile.query.CollectorResult.REASON_SEARCH_TERMINATE_AFTER_COUNT;
4243

4344
abstract class QueryCollectorContext {
45+
private static final Collector EMPTY_COLLECTOR = new SimpleCollector() {
46+
@Override
47+
public void collect(int doc) {
48+
}
49+
50+
@Override
51+
public ScoreMode scoreMode() {
52+
return ScoreMode.COMPLETE_NO_SCORES;
53+
}
54+
};
55+
4456
private String profilerName;
4557

4658
QueryCollectorContext(String profilerName) {
@@ -124,15 +136,15 @@ Collector create(Collector in ) throws IOException {
124136
static QueryCollectorContext createMultiCollectorContext(Collection<Collector> subs) {
125137
return new QueryCollectorContext(REASON_SEARCH_MULTI) {
126138
@Override
127-
Collector create(Collector in) throws IOException {
139+
Collector create(Collector in) {
128140
List<Collector> subCollectors = new ArrayList<> ();
129141
subCollectors.add(in);
130142
subCollectors.addAll(subs);
131143
return MultiCollector.wrap(subCollectors);
132144
}
133145

134146
@Override
135-
protected InternalProfileCollector createWithProfiler(InternalProfileCollector in) throws IOException {
147+
protected InternalProfileCollector createWithProfiler(InternalProfileCollector in) {
136148
final List<InternalProfileCollector> subCollectors = new ArrayList<> ();
137149
subCollectors.add(in);
138150
if (subs.stream().anyMatch((col) -> col instanceof InternalProfileCollector == false)) {
@@ -152,12 +164,20 @@ protected InternalProfileCollector createWithProfiler(InternalProfileCollector i
152164
*/
153165
static QueryCollectorContext createEarlyTerminationCollectorContext(int numHits) {
154166
return new QueryCollectorContext(REASON_SEARCH_TERMINATE_AFTER_COUNT) {
155-
private EarlyTerminatingCollector collector;
167+
private Collector collector;
156168

169+
/**
170+
* Creates a {@link MultiCollector} to ensure that the {@link EarlyTerminatingCollector}
171+
* can terminate the collection independently of the provided <code>in</code> {@link Collector}.
172+
*/
157173
@Override
158-
Collector create(Collector in) throws IOException {
174+
Collector create(Collector in) {
159175
assert collector == null;
160-
this.collector = new EarlyTerminatingCollector(in, numHits, true);
176+
177+
List<Collector> subCollectors = new ArrayList<> ();
178+
subCollectors.add(new EarlyTerminatingCollector(EMPTY_COLLECTOR, numHits, true));
179+
subCollectors.add(in);
180+
this.collector = MultiCollector.wrap(subCollectors);
161181
return collector;
162182
}
163183
};

server/src/test/java/org/elasticsearch/search/query/QueryPhaseTests.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,40 @@ public void testTerminateAfterEarlyTermination() throws Exception {
452452
assertThat(context.queryResult().topDocs().topDocs.scoreDocs.length, equalTo(0));
453453
assertThat(collector.getTotalHits(), equalTo(1));
454454
}
455+
456+
// tests with trackTotalHits and terminateAfter
457+
context.terminateAfter(10);
458+
context.setSize(0);
459+
for (int trackTotalHits : new int[] { -1, 3, 76, 100}) {
460+
context.trackTotalHitsUpTo(trackTotalHits);
461+
TotalHitCountCollector collector = new TotalHitCountCollector();
462+
context.queryCollectors().put(TotalHitCountCollector.class, collector);
463+
QueryPhase.executeInternal(context);
464+
assertTrue(context.queryResult().terminatedEarly());
465+
if (trackTotalHits == -1) {
466+
assertThat(context.queryResult().topDocs().topDocs.totalHits.value, equalTo(0L));
467+
} else {
468+
assertThat(context.queryResult().topDocs().topDocs.totalHits.value, equalTo((long) Math.min(trackTotalHits, 10)));
469+
}
470+
assertThat(context.queryResult().topDocs().topDocs.scoreDocs.length, equalTo(0));
471+
assertThat(collector.getTotalHits(), equalTo(10));
472+
}
473+
474+
context.terminateAfter(7);
475+
context.setSize(10);
476+
for (int trackTotalHits : new int[] { -1, 3, 75, 100}) {
477+
context.trackTotalHitsUpTo(trackTotalHits);
478+
EarlyTerminatingCollector collector = new EarlyTerminatingCollector(new TotalHitCountCollector(), 1, false);
479+
context.queryCollectors().put(EarlyTerminatingCollector.class, collector);
480+
QueryPhase.executeInternal(context);
481+
assertTrue(context.queryResult().terminatedEarly());
482+
if (trackTotalHits == -1) {
483+
assertThat(context.queryResult().topDocs().topDocs.totalHits.value, equalTo(0L));
484+
} else {
485+
assertThat(context.queryResult().topDocs().topDocs.totalHits.value, equalTo(7L));
486+
}
487+
assertThat(context.queryResult().topDocs().topDocs.scoreDocs.length, equalTo(7));
488+
}
455489
reader.close();
456490
dir.close();
457491
}

0 commit comments

Comments
 (0)