You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ro...@apache.org on 2026/08/10 13:48:13 UTC
(lucene) branch main updated: GITHUB#12370: early-terminate string sort when field is missing from the whole index (#16411)
This is an automated email from the ASF dual-hosted git repository.
romseygeek pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/lucene.git
The following commit(s) were added to refs/heads/main by this push:
new c541d76048f GITHUB#12370: early-terminate string sort when field is missing from the whole index (#16411)
c541d76048f is described below
commit c541d76048f0e0fbb0da35f91d8fa8198bf5fb91
Author: Serhiy Bzhezytskyy <me...@serhiy-bzhezytskyy.com>
AuthorDate: Mon Aug 10 16:48:06 2026 +0300
GITHUB#12370: early-terminate string sort when field is missing from the whole index (#16411)
When sorting on a string (SortedSet/SortedDoc) field that does not exist
anywhere in the index and the sort has no tie breaker, all documents tie on
the missing value. Once the top-N queue is full, a further missing value can
no longer compete, so the remaining documents should be skipped instead of
fully collected.
---
lucene/CHANGES.txt | 6 ++
.../search/comparators/TermOrdValComparator.java | 13 ++-
.../apache/lucene/search/TestSortOptimization.java | 118 +++++++++++++++++++++
3 files changed, 135 insertions(+), 2 deletions(-)
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 410fb5bb3ab..5d0775a30de 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -406,6 +406,12 @@ Optimizations
* GITHUB#16180: Add bulk intoBitSet for SortedDocValues ordinal ranges. (Prithvi S)
+* GITHUB#12370: Sorting on a string field no longer scans every document when the top-N queue fills
+ with missing values -- whether because the field is missing from the whole index, or because
+ sort-missing-first put missing values at the top. When the sort has no tie breaker, a further
+ missing value cannot displace one already in the queue, so the rest can be skipped.
+ (Serhiy Bzhezytskyy)
+
Bug Fixes
---------------------
diff --git a/lucene/core/src/java/org/apache/lucene/search/comparators/TermOrdValComparator.java b/lucene/core/src/java/org/apache/lucene/search/comparators/TermOrdValComparator.java
index 406f5061224..23b6ece00d4 100644
--- a/lucene/core/src/java/org/apache/lucene/search/comparators/TermOrdValComparator.java
+++ b/lucene/core/src/java/org/apache/lucene/search/comparators/TermOrdValComparator.java
@@ -297,8 +297,10 @@ public class TermOrdValComparator extends FieldComparator<BytesRef> {
if (dense || topValue != null) {
return true;
} else if (reverse == sortMissingLast) {
- // Missing values are always competitive, we can never skip
- return false;
+ // Missing values sort best here, so they stay competitive for as long as the queue is not
+ // full of them. With no tie breaker they stop competing once it is, so skipping is still
+ // worth enabling; updateCompetitiveIterator decides when it can actually start.
+ return singleSort;
} else {
return true;
}
@@ -434,6 +436,13 @@ public class TermOrdValComparator extends FieldComparator<BytesRef> {
}
} else if (sortMissingLast || dense) {
minOrd = 0;
+ } else if (bottomValue == null && singleSort) {
+ // The worst entry in the queue is itself a missing value and there is no tie breaker, so
+ // another missing value can no longer compete: missing values are no longer competitive.
+ // (bottomValue==null is stronger than bottomOrd==missingOrd, which can also be produced
+ // by
+ // a real value that maps before ord 0 in this segment.)
+ minOrd = 0;
} else {
// Missing values are still competitive.
minOrd = -1;
diff --git a/lucene/core/src/test/org/apache/lucene/search/TestSortOptimization.java b/lucene/core/src/test/org/apache/lucene/search/TestSortOptimization.java
index 2442d7dbf21..e0afe589fdd 100644
--- a/lucene/core/src/test/org/apache/lucene/search/TestSortOptimization.java
+++ b/lucene/core/src/test/org/apache/lucene/search/TestSortOptimization.java
@@ -1239,6 +1239,124 @@ public class TestSortOptimization extends LuceneTestCase {
dir.close();
}
+ /**
+ * GITHUB#12370: when the sort field is missing from the whole index (not just one segment) and
+ * the sort has no tie breaker, all documents tie on the missing value, so once the top-N queue is
+ * full the remaining documents are non-competitive and must be skipped rather than fully
+ * collected.
+ */
+ public void testStringSortOptimizationFieldMissingInWholeIndex() throws IOException {
+ final Directory dir = newDirectory();
+ final IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig());
+
+ // None of the documents index the sort field, so it is absent from every segment's FieldInfos.
+ final int numDocs = atLeast(10000);
+ for (int i = 0; i < numDocs; i++) {
+ final Document doc = new Document();
+ doc.add(new StringField("other", Integer.toString(i), Field.Store.NO));
+ writer.addDocument(doc);
+ }
+
+ final DirectoryReader reader = DirectoryReader.open(writer);
+ writer.close();
+
+ final int numHits = 5;
+
+ { // ascending, sort-missing-first (the default for SortedSetSortField): missing is the best
+ // value, but with no tie breaker a second missing value can never displace one already in the
+ // queue, so the tail should be skipped.
+ SortField sortField = new SortedSetSortField("field_does_not_exist", false);
+ TopDocs topDocs = assertSearchHits(reader, new Sort(sortField), numHits, null);
+ assertNonCompetitiveHitsAreSkipped(topDocs.totalHits.value(), numDocs);
+ }
+
+ { // descending, sort-missing-first
+ SortField sortField = new SortedSetSortField("field_does_not_exist", true);
+ TopDocs topDocs = assertSearchHits(reader, new Sort(sortField), numHits, null);
+ assertNonCompetitiveHitsAreSkipped(topDocs.totalHits.value(), numDocs);
+ }
+
+ reader.close();
+ dir.close();
+ }
+
+ /**
+ * GITHUB#12370: every document carries the SAME single value for the sort field, so with no tie
+ * breaker they all compare equal and the tail is non-competitive once the queue is full, exactly
+ * as when the field is missing from every segment.
+ */
+ public void testStringSortOptimizationSingleValueInWholeIndex() throws IOException {
+ final Directory dir = newDirectory();
+ final IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig());
+
+ final int numDocs = atLeast(10000);
+ for (int i = 0; i < numDocs; i++) {
+ final Document doc = new Document();
+ doc.add(new SortedDocValuesField("field", new BytesRef("the only value")));
+ doc.add(new StringField("field", "the only value", Field.Store.NO));
+ doc.add(new StringField("other", Integer.toString(i), Field.Store.NO));
+ writer.addDocument(doc);
+ }
+
+ final DirectoryReader reader = DirectoryReader.open(writer);
+ writer.close();
+
+ final int numHits = 5;
+
+ { // ascending
+ SortField sortField = new SortField("field", SortField.Type.STRING, false);
+ TopDocs topDocs = assertSearchHits(reader, new Sort(sortField), numHits, null);
+ assertNonCompetitiveHitsAreSkipped(topDocs.totalHits.value(), numDocs);
+ }
+
+ { // descending
+ SortField sortField = new SortField("field", SortField.Type.STRING, true);
+ TopDocs topDocs = assertSearchHits(reader, new Sort(sortField), numHits, null);
+ assertNonCompetitiveHitsAreSkipped(topDocs.totalHits.value(), numDocs);
+ }
+
+ reader.close();
+ dir.close();
+ }
+
+ /**
+ * GITHUB#12370, the remaining case: the sort field DOES exist and some documents have a value,
+ * but with sort-missing-first the top-N queue fills entirely with missing values. With no tie
+ * breaker a further missing value cannot displace one already in the queue, so the tail must be
+ * skipped.
+ */
+ public void testStringSortOptimizationQueueAllMissingSortMissingFirst() throws IOException {
+ final Directory dir = newDirectory();
+ final IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig());
+
+ // Most documents lack the sort field, a few carry a value, so the field is present in
+ // FieldInfos but not dense -- unlike the missing-from-the-whole-index case above.
+ final int numDocs = atLeast(10000);
+ for (int i = 0; i < numDocs; i++) {
+ final Document doc = new Document();
+ if (i % 1000 == 0) {
+ doc.add(new SortedDocValuesField("field", new BytesRef("value" + i)));
+ doc.add(new StringField("field", "value" + i, Field.Store.NO));
+ }
+ doc.add(new StringField("other", Integer.toString(i), Field.Store.NO));
+ writer.addDocument(doc);
+ }
+
+ final DirectoryReader reader = DirectoryReader.open(writer);
+ writer.close();
+
+ final int numHits = 5;
+
+ // sortMissingFirst ascending: missing sorts best, so the queue is all-missing once full.
+ SortField sortField =
+ new SortField("field", SortField.Type.STRING, false, SortField.STRING_FIRST);
+ TopDocs topDocs = assertSearchHits(reader, new Sort(sortField), numHits, null);
+ assertNonCompetitiveHitsAreSkipped(topDocs.totalHits.value(), numDocs);
+
+ reader.close();
+ dir.close();
+ }
+
private void doTestStringSortOptimization(DirectoryReader reader) throws IOException {
final int numDocs = reader.numDocs();
final int numHits = 5;