Skip to content

Commit a46151b

Browse files
committed
Fix scroll query with a sort that is a prefix of the index sort (#27498)
During a scroll, if the search sort matches the index sort we use the sort values of the last doc returned by the previous scroll to optimize the main query with a `SearchAfterSortedDocQuery`. This query can "jump" directly to the first document that sorts after the provided sort values. This optim is also applied if the search sort is a prefix of the index sort but this case throws an exception because we use the index sort (instead of the search sort) to validate the sort values of the last document. This change fixes this bug and adds a test for it.
1 parent 59a3fb2 commit a46151b

File tree

2 files changed

+45
-39
lines changed

2 files changed

+45
-39
lines changed

core/src/main/java/org/elasticsearch/search/query/QueryPhase.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,12 @@ static boolean execute(SearchContext searchContext, final IndexSearcher searcher
162162
searchContext.terminateAfter(searchContext.size());
163163
searchContext.trackTotalHits(false);
164164
} else if (canEarlyTerminate(indexSort, searchContext)) {
165-
// now this gets interesting: since the index sort matches the search sort, we can directly
165+
// now this gets interesting: since the search sort is a prefix of the index sort, we can directly
166166
// skip to the desired doc
167167
if (after != null) {
168168
BooleanQuery bq = new BooleanQuery.Builder()
169169
.add(query, BooleanClause.Occur.MUST)
170-
.add(new SearchAfterSortedDocQuery(indexSort, (FieldDoc) after), BooleanClause.Occur.FILTER)
170+
.add(new SearchAfterSortedDocQuery(searchContext.sort().sort, (FieldDoc) after), BooleanClause.Occur.FILTER)
171171
.build();
172172
query = bq;
173173
}

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

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import org.elasticsearch.test.TestSearchContext;
6363

6464
import java.io.IOException;
65+
import java.util.ArrayList;
6566
import java.util.List;
6667
import java.util.concurrent.atomic.AtomicBoolean;
6768

@@ -465,11 +466,11 @@ public void testIndexSortingEarlyTermination() throws Exception {
465466

466467
public void testIndexSortScrollOptimization() throws Exception {
467468
Directory dir = newDirectory();
468-
final Sort sort = new Sort(
469+
final Sort indexSort = new Sort(
469470
new SortField("rank", SortField.Type.INT),
470471
new SortField("tiebreaker", SortField.Type.INT)
471472
);
472-
IndexWriterConfig iwc = newIndexWriterConfig().setIndexSort(sort);
473+
IndexWriterConfig iwc = newIndexWriterConfig().setIndexSort(indexSort);
473474
RandomIndexWriter w = new RandomIndexWriter(random(), dir, iwc);
474475
final int numDocs = scaledRandomIntBetween(100, 200);
475476
for (int i = 0; i < numDocs; ++i) {
@@ -483,44 +484,49 @@ public void testIndexSortScrollOptimization() throws Exception {
483484
}
484485
w.close();
485486

486-
TestSearchContext context = new TestSearchContext(null, indexShard);
487-
context.parsedQuery(new ParsedQuery(new MatchAllDocsQuery()));
488-
ScrollContext scrollContext = new ScrollContext();
489-
scrollContext.lastEmittedDoc = null;
490-
scrollContext.maxScore = Float.NaN;
491-
scrollContext.totalHits = -1;
492-
context.scrollContext(scrollContext);
493-
context.setTask(new SearchTask(123L, "", "", "", null));
494-
context.setSize(10);
495-
context.sort(new SortAndFormats(sort, new DocValueFormat[] {DocValueFormat.RAW, DocValueFormat.RAW}));
496-
497487
final IndexReader reader = DirectoryReader.open(dir);
498-
IndexSearcher contextSearcher = new IndexSearcher(reader);
499-
500-
QueryPhase.execute(context, contextSearcher, checkCancelled -> {}, sort);
501-
assertThat(context.queryResult().topDocs().totalHits, equalTo((long) numDocs));
502-
assertNull(context.queryResult().terminatedEarly());
503-
assertThat(context.terminateAfter(), equalTo(0));
504-
assertThat(context.queryResult().getTotalHits(), equalTo((long) numDocs));
505-
int sizeMinus1 = context.queryResult().topDocs().scoreDocs.length - 1;
506-
FieldDoc lastDoc = (FieldDoc) context.queryResult().topDocs().scoreDocs[sizeMinus1];
488+
List<SortAndFormats> searchSortAndFormats = new ArrayList<>();
489+
searchSortAndFormats.add(new SortAndFormats(indexSort, new DocValueFormat[]{DocValueFormat.RAW, DocValueFormat.RAW}));
490+
// search sort is a prefix of the index sort
491+
searchSortAndFormats.add(new SortAndFormats(new Sort(indexSort.getSort()[0]), new DocValueFormat[]{DocValueFormat.RAW}));
492+
for (SortAndFormats searchSortAndFormat : searchSortAndFormats) {
493+
IndexSearcher contextSearcher = new IndexSearcher(reader);
494+
TestSearchContext context = new TestSearchContext(null, indexShard);
495+
context.parsedQuery(new ParsedQuery(new MatchAllDocsQuery()));
496+
ScrollContext scrollContext = new ScrollContext();
497+
scrollContext.lastEmittedDoc = null;
498+
scrollContext.maxScore = Float.NaN;
499+
scrollContext.totalHits = -1;
500+
context.scrollContext(scrollContext);
501+
context.setTask(new SearchTask(123L, "", "", "", null));
502+
context.setSize(10);
503+
context.sort(searchSortAndFormat);
504+
505+
QueryPhase.execute(context, contextSearcher, checkCancelled -> {}, searchSortAndFormat.sort);
506+
assertThat(context.queryResult().topDocs().totalHits, equalTo((long) numDocs));
507+
assertNull(context.queryResult().terminatedEarly());
508+
assertThat(context.terminateAfter(), equalTo(0));
509+
assertThat(context.queryResult().getTotalHits(), equalTo((long) numDocs));
510+
int sizeMinus1 = context.queryResult().topDocs().scoreDocs.length - 1;
511+
FieldDoc lastDoc = (FieldDoc) context.queryResult().topDocs().scoreDocs[sizeMinus1];
507512

508-
contextSearcher = getAssertingEarlyTerminationSearcher(reader, 10);
509-
QueryPhase.execute(context, contextSearcher, checkCancelled -> {}, sort);
510-
assertNull(context.queryResult().terminatedEarly());
511-
assertThat(context.queryResult().topDocs().totalHits, equalTo((long) numDocs));
512-
assertThat(context.terminateAfter(), equalTo(0));
513-
assertThat(context.queryResult().getTotalHits(), equalTo((long) numDocs));
514-
FieldDoc firstDoc = (FieldDoc) context.queryResult().topDocs().scoreDocs[0];
515-
for (int i = 0; i < sort.getSort().length; i++) {
516-
@SuppressWarnings("unchecked")
517-
FieldComparator<Object> comparator = (FieldComparator<Object>) sort.getSort()[i].getComparator(1, i);
518-
int cmp = comparator.compareValues(firstDoc.fields[i], lastDoc.fields[i]);
519-
if (cmp == 0) {
520-
continue;
513+
contextSearcher = getAssertingEarlyTerminationSearcher(reader, 10);
514+
QueryPhase.execute(context, contextSearcher, checkCancelled -> {}, searchSortAndFormat.sort);
515+
assertNull(context.queryResult().terminatedEarly());
516+
assertThat(context.queryResult().topDocs().totalHits, equalTo((long) numDocs));
517+
assertThat(context.terminateAfter(), equalTo(0));
518+
assertThat(context.queryResult().getTotalHits(), equalTo((long) numDocs));
519+
FieldDoc firstDoc = (FieldDoc) context.queryResult().topDocs().scoreDocs[0];
520+
for (int i = 0; i < searchSortAndFormat.sort.getSort().length; i++) {
521+
@SuppressWarnings("unchecked")
522+
FieldComparator<Object> comparator = (FieldComparator<Object>) searchSortAndFormat.sort.getSort()[i].getComparator(1, i);
523+
int cmp = comparator.compareValues(firstDoc.fields[i], lastDoc.fields[i]);
524+
if (cmp == 0) {
525+
continue;
526+
}
527+
assertThat(cmp, equalTo(1));
528+
break;
521529
}
522-
assertThat(cmp, equalTo(1));
523-
break;
524530
}
525531
reader.close();
526532
dir.close();

0 commit comments

Comments
 (0)