All findings · Lucene finding · LFV-329 · dvs-rescorer-zero-leaf-reader-throws

DoubleValuesSourceRescorer and LateInteractionRescorer throw NoSuchElementException instead of returning no hits when the reader has no segments

Rescore the 0 hits of a searcher whose reader has no segments (a brand-new or emptied index) with DoubleValuesSourceRescorer or LateInteractionRescorer, and rescore() throws NoSuchElementException instead of returning 0 hits. Every release from 10.3.0 to 10.6.0 and main are affected.

Summary

Priority
Low, rank 617The 10.3+ rescorers throw NoSuchElementException only on readers with no segments, which is loud and has an easy workaround, so it heads the low loud-crash rows.
Impact
crash
Since
10.3.0 (#14776, merged 2025-06-15)introduced 2025-06-15 · about 1 year
Affected
10.3.0 to 10.6.0 and main (all 8 releases run fail)
Status
documented
Found by
AI tools: a code-reading lead, confirmed by an exhaustive check of the stock jar on tiny indexes

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

A rescorer asked to rescore 0 hits on a reader with no segments throws instead of returning 0 hits.

Background: segments and rescoring

  1. A Lucene index is a list of segments, and IndexReader.leaves() returns one entry per segment. A brand-new index, an NRT reader opened before the first document, an index after deleteAll() and new MultiReader() have none: leaves() is an empty list, and every search on them returns 0 hits.
  2. A Rescorer takes the top hits of a cheap first-pass search and re-scores them: rescore(searcher, firstPassTopDocs, topN). DoubleValuesSourceRescorer (new in 10.3.0) re-scores with a DoubleValuesSource; LateInteractionRescorer extends it to rerank with multi-vectors.
Summary of the bug: new IndexSearcher(new MultiReader()) has no segments (leaves() is empty); search(MatchAllDocsQuery.INSTANCE, 10) returns 0 hits. DoubleValuesSourceRescorer.rescore(searcher, firstPass, 10) calls leaves().getFirst() at DoubleValuesSourceRescorer.java:53 before its per-hit loop and throws java.util.NoSuchElementException, with assertions on and off; LateInteractionRescorer inherits rescore(). QueryRescorer and SortRescorer look up segments only inside their per-hit loops and return 0 hits. With an early return when there are no hits, the rescorer returns 0 hits.
The minimal repro: a searcher over a reader with no segments returns 0 hits; rescore() asks for the first segment before it looks at the hits and throws, while QueryRescorer walks segments only per hit and returns 0 hits. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Open a searcher on a reader with no segments: a brand-new index, an NRT reader before the first document, an index after deleteAll(), or new MultiReader(). search(MatchAllDocsQuery.INSTANCE, 10) correctly returns 0 hits.
  2. Rescore those 0 hits with a DoubleValuesSourceRescorer (or LateInteractionRescorer), topN 10. You expect 0 hits back, which is what QueryRescorer and SortRescorer return.
  3. Instead rescore() throws java.util.NoSuchElementException with no message, with assertions on and off.
  4. Why: before it looks at any hit, rescore() takes the first segment with leaves().getFirst() (DoubleValuesSourceRescorer.java:53), and there is none. With 1 segment the same call works, even when the first pass has 0 hits.

Who is affected

Applications that rescore with a DoubleValuesSourceRescorer subclass or LateInteractionRescorer (both new in 10.3.0) whenever the whole reader has no segments: an index created at startup, an NRT reader before the first document, after deleteAll(). Default settings. No wrong hits are returned; skipping the rescore on an empty first pass avoids it.

Mechanism, terms and numbers484 words

DoubleValuesSourceRescorer.rescore looks up the reader's first segment (leaves().getFirst()) before it looks at the first-pass hits. On a reader with no segments (a brand-new index, an NRT reader before the first document, an index after deleteAll() or after every document was deleted and merged away, an empty MultiReader) that list is empty, and the JDK throws java.util.NoSuchElementException with no message instead of returning the 0 hits the first pass already produced. LateInteractionRescorer inherits the method. QueryRescorer and SortRescorer return 0 hits for the same input. It never returns wrong hits, and a reader with one segment is never affected; the failure is one unchecked exception, in every release since the classes were added in 10.3.0 and on main.

The bug

  1. DoubleValuesSourceRescorer.rescore (DoubleValuesSourceRescorer.java:45-86) rewrites the values source (:47), then clones the first-pass hits and sorts them by doc id (:49-50).
  2. Before its per-hit loop it runs List<LeafReaderContext> leaves = searcher.getIndexReader().leaves(); (:52), LeafReaderContext ctx = leaves.getFirst(); (:53) and int leafEnd = ctx.docBase + ctx.reader().maxDoc(); (:55). Nothing checks whether there are hits or segments.
  3. With no segments, leaves is empty and the JDK's List.getFirst() throws NoSuchElementException with no message. It is not a Lucene assertion, so -ea and -da behave the same.
  4. The per-hit loop (:58-77) would not have run at all for 0 hits. The method's deliberate check, an IllegalStateException for hit doc ids past the last segment (:60-66), is for hits from another searcher.
  5. LateInteractionRescorer extends DoubleValuesSourceRescorer (LateInteractionRescorer.java:32) and does not override rescore(), so it fails at the same line.
  6. QueryRescorer (QueryRescorer.java:55, :67, :73) and SortRescorer (SortRescorer.java:46, :60, :66) get the same leaves list but index it only inside while (hitUpto < hits.length), so with 0 hits they never touch a segment and return 0 hits.

The numbers

  • The minimal repro: new IndexSearcher(new MultiReader()), a MatchAllDocsQuery first pass with 0 hits, rescore(searcher, firstPass, 10): NoSuchElementException at DoubleValuesSourceRescorer.java:53, with -ea and -da, on main and on the 10.6.0 jars.
  • Six ordinary ways to a reader with no segments, all failing at the same line on main, 10.5.2 and 10.3.0: a committed empty index; an NRT reader before the first document; deleteAll(), then an NRT reader; every document deleted, then forceMerge(1) (the fully deleted segment is dropped); a KnnFloatVectorQuery first pass on an empty index followed by LateInteractionRescorer; an empty MultiReader.
  • Not affected: a 1-segment index whose first pass has 0 hits (0 hits back) or 1 hit (rescored normally); an empty sub-reader inside a non-empty MultiReader.
  • Lucene's own test reached it: the original TestDoubleValuesSourceRescorer.testBasic indexed nextInt(100) documents, and 6 of 1000 iterations on the 10.3.0 jars threw this exception. #14793 later raised the count to nextInt(50, 100) to fix a different failure, which also removed the 0-document draw.
Exposure and likelihood265 words
  • Who is hit. Callers of DoubleValuesSourceRescorer subclasses or LateInteractionRescorer, the multi-vector reranker whose javadoc describes running a cheap first pass and then reranking its top hits (LateInteractionRescorer.java:25-28), on any searcher whose whole reader has no segments: a freshly created index, an NRT reader before the first document arrives, after deleteAll(), after every document was deleted and merged away, an empty MultiReader, and by the same token a shard whose index is still empty (reasoned; not run as a shard setup).
  • Default vs opt-in. Default IndexWriterConfig, codec and IndexSearcher; the trigger is the opt-in rescorer API. DoubleValuesSourceRescorer itself has no @lucene.experimental tag; its base class Rescorer and its subclass LateInteractionRescorer do.
  • Scale. One rescore call fails, immediately, with an unchecked exception that has no message. No wrong hits are returned and Lucene writes nothing; for an empty first pass produced by that same searcher, the correct answer was empty. With one segment the same call works.
  • Likelihood. Deterministic whenever an application that always rescores meets an empty reader. Lucene's own randomized test could reach it: the original testBasic indexed nextInt(100) documents, and in a 1000-iteration replay on the 10.3.0 jars (by this hunt, not an upstream CI record) 6 iterations threw this exception; #14793 later changed the document count. Solr, the one downstream project found that uses DoubleValuesSourceRescorer, skips rescoring when the first pass is empty, so it is not affected; code search also found one small application using LateInteractionRescorer (not examined), and other downstream use is unknown.
  • Workaround. Skip rescore() when the first pass returned no hits.

Minimal repro

One JUnit class, one method, public API only: a searcher over new MultiReader() (no segments, like a brand-new index), a match-all first pass with 0 hits, then rescore. No index, no random data; it fails with assertions on and off and is also the suggested regression test.

TestDoubleValuesSourceRescorerNoSegments.java (JUnit for Lucene's build)27 lines
package org.apache.lucene.search;

import java.util.NoSuchElementException;
import org.apache.lucene.index.MultiReader;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestDoubleValuesSourceRescorerNoSegments extends LuceneTestCase {

  public void testRescoreOnReaderWithoutSegments() throws Exception {
    IndexSearcher searcher = new IndexSearcher(new MultiReader());
    TopDocs firstPass = searcher.search(MatchAllDocsQuery.INSTANCE, 10);
    Rescorer rescorer =
        new DoubleValuesSourceRescorer(DoubleValuesSource.constant(1)) {
          @Override
          protected float combine(float firstPassScore, boolean valuePresent, double sourceValue) {
            return (float) sourceValue;
          }
        };
    TopDocs rescored;
    try {
      rescored = rescorer.rescore(searcher, firstPass, 10);
    } catch (NoSuchElementException e) {
      throw new AssertionError("expected 0 rescored hits (as QueryRescorer returns), got " + e, e);
    }
    assertEquals(0, rescored.scoreDocs.length);
  }
}
Command
cp $REPRO/TestDoubleValuesSourceRescorerNoSegments.java $LUCENE/lucene/core/src/test/org/apache/lucene/search/
cd $LUCENE && ./gradlew --no-daemon :lucene:core:test --tests org.apache.lucene.search.TestDoubleValuesSourceRescorerNoSegments -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same failure)
Expected
0 rescored hits, as QueryRescorer and SortRescorer return for the same input.
Observed
java.util.NoSuchElementException (no message) from List.getFirst at DoubleValuesSourceRescorer.java:53, with assertions on and off.

All files of this repro: repro/LFV-329/

More files, environment, full output152 words

The test searches new IndexSearcher(new MultiReader()) with MatchAllDocsQuery.INSTANCE (0 hits) and rescores the result with an anonymous DoubleValuesSourceRescorer over DoubleValuesSource.constant(1), topN 10. It turns the NoSuchElementException into an AssertionError so the failure states expected vs actual; the cause keeps the getFirst() frame. LateInteractionRescorer inherits the same rescore() and fails the same way. The method could equally go into the existing TestDoubleValuesSourceRescorer. On 10.3.x replace MatchAllDocsQuery.INSTANCE with new MatchAllDocsQuery() (the constant first appears in 10.4.0).

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); every release from 10.3.0 to 10.6.0 also fails; 10.2.2 and older: classes absent
jdkOpenJDK 25.0.3
osLinux aarch64

Observed output

TestDoubleValuesSourceRescorerNoSegments > testRescoreOnReaderWithoutSegments FAILED
    java.lang.AssertionError: expected 0 rescored hits (as QueryRescorer returns), got java.util.NoSuchElementException
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:14C1189231F4CECF]:0)
        at org.apache.lucene.search.TestDoubleValuesSourceRescorerNoSegments.testRescoreOnReaderWithoutSegments(TestDoubleValuesSourceRescorerNoSegments.java:23)

        Caused by:
        java.util.NoSuchElementException
            at java.base/java.util.List.getFirst(List.java:823)
            at org.apache.lucene.search.DoubleValuesSourceRescorer.rescore(DoubleValuesSourceRescorer.java:53)
            at org.apache.lucene.search.TestDoubleValuesSourceRescorerNoSegments.testRescoreOnReaderWithoutSegments(TestDoubleValuesSourceRescorerNoSegments.java:21)

1 test completed, 1 failed

# the same with -Ptests.asserts=false and on the 10.6.0 release jars; with the recommended fix the test passes

Why it looks unintended

  • The only stated precondition is met: the hits must be "produced by the provided searcher" (Rescorer.java:38-39), and the empty hits of an empty index are.
  • QueryRescorer and SortRescorer, implementations of the same method, return 0 hits for the same input.
  • The maintainers fixed an analogous empty-input crash in RescoreTopNQuery as a bug on 2026-10-28 (#16926).
The full argument303 words
  • The contract. Rescorer.java:37-39: "@param searcher {@link IndexSearcher} used to produce the first pass topDocs" and "@param firstPassTopDocs Hits from the first pass search. It's very important that these hits were produced by the provided searcher; otherwise the doc IDs will not match!" No javadoc on Rescorer, DoubleValuesSourceRescorer or LateInteractionRescorer asks for a non-empty reader or non-empty hits.
  • The siblings. QueryRescorer and SortRescorer return 0 hits for an empty committed index on 9.12.3, 10.2.2, 10.3.0, 10.5.2 and main, and for new MultiReader() on main, 10.5.2 and 10.3.0.
  • Zero-segment readers are a supported searcher input. TestIndexSearcher.testGetSlicesNoLeavesNoExecutor and testGetSlicesNoLeavesWithExecutor build an IndexSearcher over new MultiReader() and check that it has 0 slices (TestIndexSearcher.java:224-233), and the test framework's QueryUtils.wrapUnderlyingReader deliberately mixes empty MultiReaders into the searchers it checks queries against (QueryUtils.java:165-180).
  • Lucene guards this case elsewhere before taking a first segment: MultiDocValues.getNumericValues returns null for 0 leaves (MultiDocValues.java:183-184), JoinUtil returns MatchNoDocsQuery("JoinUtil.createJoinQuery with no segments") (JoinUtil.java:533-534), StringValueFacetCounts checks leafCount == 0 (StringValueFacetCounts.java:265). A verifier's grep for direct first-segment calls (leaves().get(0), getFirst()) found DoubleValuesSourceRescorer.java:53 to be the only unguarded one in production code outside benchmarks; first-segment access written in other forms was not searched.
  • Precedent. lucene/CHANGES.txt:338 (11.0.0, Bug Fixes): "GITHUB#16926: RescoreTopNQuery no longer throws when its inner query matches no documents. (Greg Miller)". For the same API family, "LUCENE-5682: NPE in QueryRescorer when Scorer is null" (CHANGES.txt:12275) was a bug fix too.
  • History. No review comment on #14776 or #14729 discusses empty readers or empty hits, and every upstream test of the two classes indexes documents first. The original testBasic could index 0 documents and hit this exception; #14793 (ChrisHegarty) raised the count to fix a different failure, the test's own ArrayIndexOutOfBoundsException at scoreDocs[0], and does not mention this one.
The devil's-advocate check147 words

The strongest case for "works as designed": rescoring an empty result is pointless, so callers should skip it; Rescorer and LateInteractionRescorer are @lucene.experimental; the method validates its input on purpose (the IllegalStateException at :60-66), and in the #14776 review the author wanted a mismatched searcher to surface as an error, there in explain(); and no test ever specified the empty case.

Why it fails: the documented precondition is met and nothing asks callers to skip empty results; @lucene.experimental is about API stability, not about accepting crashes, and DoubleValuesSourceRescorer itself is not tagged; the deliberate check targets foreign hits, while this throw is an unguarded JDK call with no message, made before any hit is looked at; the sibling rescorers return 0 hits; and the maintainers have just fixed an analogous empty-input crash in RescoreTopNQuery as a bug. Verdict: not intended; a small robustness bug.

Suggested fix

Recommended A: return early when there are no hits

Return early when there are no hits, before the segment lookup in DoubleValuesSourceRescorer.rescore (+3 lines; fixes LateInteractionRescorer too). With it the repro and the four rescorer test suites (17 tests) pass in gradle on main, with assertions on and off.

if (hits.length == 0) {
  return new TopDocs(firstPassTopDocs.totalHits, hits);
}
List<LeafReaderContext> leaves = searcher.getIndexReader().leaves();
LeafReaderContext ctx = leaves.getFirst();
All options and trade-offs3 options · 579 words

Option A (the finder's fix) was applied in two scratch worktrees of 645d4fe14a20 and run with gradle (the worktrees were removed after the runs); options A and C were also shadowed into the stock main jars, and the independent fact check ran C through gradle. Option B was not built. All fix checks are targeted (the repro plus the rescorer suites); neither fix was run against the whole core test suite or on a 10.x branch, and C went through gradle with assertions on only. Until a fix ships, callers can skip the rescore when the first pass is empty.

A: return early when there are no hits (validated by the checks) (recommended)

+3 lines in DoubleValuesSourceRescorer.rescore, after the clone and sort (:49-50) and before the segment lookup (:52-53). It could also sit before the values-source rewrite at :47, to skip that work, but then a custom source's rewrite (and any check or side effect in it) would no longer run on an empty first pass.

if (hits.length == 0) {
  return new TopDocs(firstPassTopDocs.totalHits, hits);
}
List<LeafReaderContext> leaves = searcher.getIndexReader().leaves();
LeafReaderContext ctx = leaves.getFirst();

Trade-offs

  • Smallest change; returns what QueryRescorer and SortRescorer return and keeps the first pass's totalHits, as the normal path does (:85). Fixes LateInteractionRescorer, which inherits the method.
  • Foreign hits on an empty reader (documented misuse) still throw NoSuchElementException instead of the method's IllegalStateException; that case is not part of this bug.

What the checks validated

  • Gradle, stock 645d4fe14a20, seed DEADBEEF: the repro fails with assertions on and off (1 test, 1 failure each).
  • Gradle with the fix, assertions on: the repro, TestDoubleValuesSourceRescorer (3), TestLateInteractionRescorer (2), TestQueryRescorer (8) and TestSortRescorer (3), 17 tests, 0 failures; :lucene:core:checkGoogleJavaFormat passes. Assertions off: the same 17 tests, 0 failures. An earlier worktree gave the same 17 of 17 with assertions on.
  • Shadowed into the stock main jars with assertions off: the repro and the two upstream rescorer suites, 6 tests, 0 failures (the stock control fails 1 of 6).
  • Earlier hunt checks: the finder's fix check 3/3 plus the upstream rescorer suites 5/5; a verifier's equivalent fix turned its 7 tests green.

C: look up segments lazily inside the loop, as the sibling rescorers do (validated by the checks)

3 lines changed: start with no segment (ctx = null, currLeaf = -1, leafEnd = 0) and let the existing while (hit.doc >= leafEnd) loop advance to segment 0 on the first hit.

List<LeafReaderContext> leaves = searcher.getIndexReader().leaves();
LeafReaderContext ctx = null; // was: leaves.getFirst()
int currLeaf = -1;            // was: 0
int leafEnd = 0;              // was: ctx.docBase + ctx.reader().maxDoc()

Trade-offs

  • With no hits nothing is looked up. With hits on an empty reader, currLeaf == leaves.size() - 1 holds (both -1), so the method's own IllegalStateException fires instead of NoSuchElementException.
  • Changes the loop's starting state, so a reviewer must re-check the segment walk; its cost was not benchmarked.

What the checks validated

Shadowed into the stock main jars: the repro and the two upstream rescorer suites, 6 tests, 0 failures, with assertions on and off. The independent fact check also ran it through gradle on 645d4fe14a20, with assertions on: the repro and the four rescorer suites, 17 tests, 0 failures, and the format check passes.

B: guard leaves.isEmpty() (not built)

Return the empty result when there are no hits, else throw the IllegalStateException.

Trade-offs

Same behaviour as C, with more lines.

Downstream note

Downstream services that rescore each shard separately and fail the whole request when one shard fails can see requests fail while any shard's index is still empty; skipping the rescore on an empty first pass avoids it.

How it was found

AI tools planning a hunt over Lucene's rescorers flagged line 53 from code reading. An exhaustive check of the stock jar over every index of up to 5 documents (1,801,236 cases per rescorer) then showed this exception on readers with no segments as its only deviation, and it was reduced to one JUnit test.

The method in full178 words

Method: non-formal (a code-reading lead, then executed checks; the finder tagged it semi-formal; this page classes it by the first signal).

  • The hunt's plan for the rescoring unit listed DoubleValuesSourceRescorer.java:53 as a lead: "leaf navigation starts at the first leaf before any hit is examined".
  • An AI finder then ran a bounded-exhaustive translation-validation harness on the stock jar: QueryRescorer and DoubleValuesSourceRescorer over every index of up to 5 documents (including an empty MultiReader and an empty DirectoryReader), every subset of first-pass hits in random order, first-pass scores {0, 1}, values {missing, 0, 1} and topN from 0 to the hit count + 1: 1,801,236 checks per rescorer against exact rescoring results. QueryRescorer had no mismatch; DoubleValuesSourceRescorer had one mismatch class, this exception on readers with no segments (4 cases).
  • The finder turned it into a public-API JUnit; independent verifiers found five more ordinary ways to reach a reader with no segments, and the case was later reduced to the one-method test. So it is a code-reading find confirmed by execution, not a blind discovery by a tool.

Prior reports, history and checks

Prior reports

No prior report found (GitHub issues and PRs incl. the JIRA archive, five mailing lists, downstream projects, the web, all 411 open PRs; 2026-10-26, re-checked three times on 2026-10-30). Nearest miss: #14793, which fixed a different failure of the same test and incidentally hid this one.

Searches4 searches

No report of this bug was found, and upstream main, branch_11x, branch_10_6 and the 10.6.0 tag still have the line (branch_10x was decommissioned on 2026-10-29 by #16911). #14793 (ChrisHegarty, merged 2025-06-16) fixed a flaky failure of the same upstream test, the test's own ArrayIndexOutOfBoundsException on a non-empty index with no matches, by raising its document count; that also removed the 0-document draw that reached this bug, but nobody there mentions NoSuchElementException, empty indexes or getFirst. #16926 (an analogous empty-input crash in RescoreTopNQuery, fixed 2026-10-28) is precedent, not a report.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs, commits (apache/lucene incl. the migrated JIRA archive; downstream repositories)2026-10-26an automated pass (16 issue/PR, 3 commit, 2 downstream queries) plus 12 REST and 13 GraphQL phrasings: NoSuchElementException, empty index/reader/MultiReader, no or zero segments and leaves, comments, unmerged PRs, both test class names, downstream projectsno report; closest #14793 (different failure of the same test), #14776 (added the line), #14729 (added LateInteractionRescorer)
Mailing lists, JIRA, web2026-10-2620 automated and 32 manual list queries over dev@, java-user@, issues@, commits@ and builds@; SOLR and LUCENE JIRA queries; 6 web queriesnothing about this bug; builds@ has no failure mail for TestDoubleValuesSourceRescorer; SOLR-17951 uses the class but skips empty first passes
Open PRs and upstream history2026-10-26changed-file lists of all 411 open apache/lucene PRs; commit history of both files on main and branch_10x; full reads of #14776, #14729 and #14793no open PR touches either file; no fix
Re-checks for this page2026-10-30commits on both files on upstream main; lines 52-55 on main, branch_11x, branch_10_6 (and the 10.6.0 tag); searches for both class names, for the exception and empty-index phrasings, for recent items (PRs matching "rescore" and issues matching "rescorer" created since 2026-10-20; issues and PRs matching "rescore" created since 2026-10-28), and tracking issue #16937unchanged (last commit on the file #15826, unrelated); only #14729 names the classes; the recent-item searches return only #16932 (page alignment, unrelated); #16937 does not list it; nothing new. An independent fact check's search of issues and PRs matching "rescore" since 2026-10-20 (run twice) also returns issue #16902 (1-bit quantization of byte vector fields with int8 rescoring), unrelated

History

Introduced with the class by #14776 (merged 2025-06-15) and inherited by LateInteractionRescorer (#14729); first released in 10.3.0, not a regression. Every release from 10.3.0 to 10.6.0 and main fails; 10.2.2 and older have neither class.

Versions tested7 rows

#14776 "Add a rescorer that uses DoubleValuesSource values to re-score first pass hits" (vigyasharma, commit c8147c9e6fa1, merged 2025-06-15; branch_10x backport 02f5dc30e2fe) added the class with leaves.getFirst() before the hit loop; git blame and git log -L52,55 return only this commit, and the one later commit on the file, #15826 (8169afebd260, 2026-03-16), changes only the top-N copy. LateInteractionRescorer (#14729, d9618080b340, 2025-07-08; backport 7de24ce973d0) extends the class from its first version. CHANGES lists #14776 under 10.3.0 New Features. Not a regression.

The 10.6.0 row, the gradle runs on main and the standalone 2026-10-29 run on main 645d4fe14a20 use the minimal test; the other rows, and the 2026-10-29 re-verify on 645d4fe14a20, use the finder's 3-test class (an empty committed index, DoubleValuesSourceRescorer, LateInteractionRescorer and a QueryRescorer control); all on OpenJDK 25.0.3, Linux aarch64; release jars SHA-1 checked against Maven Central.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 (unreleased 11.0) affectedfails, -ea and -da (gradle and standalone)
main ea57360912ed, a9779ef3c193 affectedfail (ea57360912ed with -ea and -da)
10.6.0 affectedfails, -ea and -da (minimal test)
10.5.2 affectedfails, -ea and -da
10.4.0, 10.5.0, 10.5.1 affectedfail (-ea)
10.3.0 to 10.3.2 affectedfail (10.3.0 with -ea and -da)
10.2.2, 9.12.3 n/ado not compile: no DoubleValuesSourceRescorer or LateInteractionRescorer; QueryRescorer and SortRescorer return 0 hits on the same input

Independent checks

The finder, an independent reproducer, an intended-behaviour devil's advocate, an adjudication, a credibility audit, a re-verify on current main and this page's gradle runs all reproduce it. Cross-model reviews (OpenAI GPT-5.6) could run nothing: one said real, the other not real on procedure alone.

Checks and verdicts372 words

Every executing check ran on the stock jars and voted real. The only dissent is procedural: the cross-model refute review could not execute and voted not real on procedure only at low confidence while writing "Static evidence nevertheless strongly indicates a genuine defect"; a separate re-run of the repro failed as claimed.

Each independent check and its verdict
CheckVerdictWhat it found
Finder candidateExhaustive harness: the only mismatch class is this exception (4 cases of 1,801,236 checks). JUnit: 2 of 3 fail on main (-ea, -da), 10.5.2 and 10.3.0; its fix passes the repro and the upstream rescorer suites.
Cross-model review, refute lens (OpenAI GPT-5.6) not real, low (procedural)Read-only: it could run nothing; root cause agreed, inputs in contract, intended-behaviour argument weak, 9 of 10 evidence items support real. A separate run re-ran the repro: 2 of 3 fail with -da.
Independent reproducer realOwn JUnit, written from the claim: 4 of 7 fail on main (-ea, -da), 10.5.2 and 10.3.0 (empty committed index, deleteAll, empty MultiReader, LateInteractionRescorer); QueryRescorer, SortRescorer and 1-segment controls pass; its shadow fix 7/7.
Cross-model review, intended lens (OpenAI GPT-5.6) real, medium; intended argument weakRead-only review; root cause agreed; a separate run re-ran the repro: 2 of 3 fail.
Intended-behaviour devil's advocate real (not intended: high; impact low)Built and rejected the "pointless input, experimental API" case; own test fails 5 of 8 through five ordinary paths on main, 10.5.2 and 10.3.0; controls pass.
Adjudication confirmed, highRe-ran the independent reproduction's test (7 run, 4 failed, -ea and -da) and the finder's (3 run, 2 failed); asked for a modest impact claim and the ordinary trigger paths.
Credibility audit keepRe-ran the finder's and the intended-behaviour check's tests with a new seed; same failures.
Re-verify on main 645d4fe14a20 still reproducesFinder 2/3, the independent reproduction 4/7, the intended-behaviour check 5/8 fail with -ea and -da.
This page's runs reproduced; fix validatedGradle on 645d4fe14a20 in two scratch worktrees: the minimal test fails with -ea and -da; with fix A it passes with the four rescorer suites (17 tests) with -ea (plus the format check) and with -da. 10.6.0 release jars: fails with -ea and -da.

Diagram file

The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,206 words

Finding record

LFV-329, snapshot of 2026-11-06

Registry title
DoubleValuesSourceRescorer and LateInteractionRescorer throw NoSuchElementException instead of returning no hits when the searcher's reader has no segments: rescore() takes leaves().getFirst() before it looks at the hits
Area
search / rescoring
Priority
Low, rank 617: The 10.3+ rescorers throw NoSuchElementException only on readers with no segments, which is loud and has an easy workaround, so it heads the low loud-crash rows.
Impact class
crash
Impact note
Applications that rescore with a DoubleValuesSourceRescorer subclass or with LateInteractionRescorer (both new in 10.3.0) get an unchecked, message-less java.util.NoSuchElementException instead of empty TopDocs whenever the searcher's whole reader has no segments (a new committed index, an NRT reader before the first document or after deleteAll(), an index whose documents were all deleted and merged away, an empty MultiReader or empty shard), with the default codec, IndexWriterConfig and searcher, with -ea and -da, in every release from 10.3.0 to 10.6.0; a reader with at least one segment is never affected, even when the first pass has no hits, no wrong results are returned, and skipping the rescore when the first pass is empty avoids it.
Summary
You search an index that has no segments yet (for example a freshly created or emptied index, or an empty shard), get the expected 0 hits, and pass them to a DoubleValuesSourceRescorer or LateInteractionRescorer. You expect 0 rescored hits, which is what QueryRescorer and SortRescorer return for the same input. Instead rescore() throws java.util.NoSuchElementException, because it takes the reader's first segment with leaves().getFirst() before it looks at the hits, and there is no first segment.
Introduced
c8147c9e6fa1 (apache/lucene#14776 'Add a rescorer that uses DoubleValuesSource values to re-score first pass hits', by GitHub vigyasharma, main), branch_10x backport 02f5dc30e2fe, first released in 10.3.0 [read: git blame and git log -L52,55 on main lines 52-55 return only this commit, git log -S'leaves.getFirst()' returns only it and its backport, and the one later commit on the file, #15826 (8169afebd260), changes only the topN copy at lines 79-82; exec: the release jars bracket it, 10.2.2 lacks the class and 10.3.0 fails]. The unguarded getFirst() is in the first version of the class, so this is not a regression. LateInteractionRescorer (d9618080b340, #14729, 2025-07-08; backport 7de24ce973d0) extends the class from its first version and inherits rescore() (2025-06-15 (#14776 merged 2025-06-15T04:12:47Z, author date 2025-06-14; branch_10x backport committed 2025-06-15T04:21:01Z))
First affected version
10.3.0 [exec] (the first release with DoubleValuesSourceRescorer and LateInteractionRescorer; 10.2.2 and 9.12.3 do not have the classes [exec: compile errors])
Affected versions
10.3.0, 10.3.1, 10.3.2, 10.4.0, 10.5.0, 10.5.1, 10.5.2 and main ea57360912ed [exec: the finder's repro fails 2 of 3 on each, seed DEADBEEF; -da also on 10.3.0, 10.5.2 and main; the independent reproduction's and the intended-behaviour check's own tests also fail on 10.5.2 and 10.3.0]; not applicable to 10.2.2 and earlier or to any 9.x, where the API does not exist [exec: 10.2.2 and 9.12.3 compile errors; the QueryRescorer/SortRescorer control on the same zero-segment input passes on 9.12.3, 10.2.2, 10.3.0, 10.5.2 and main]; upstream main, branch_10x and branch_10_6 (10.6.0, released 2026-10-29) still have leaves.getFirst() at line 53 as read on 2026-10-26 [read]; main 645d4fe14a20 [exec 2026-10-29, -ea and -da]; branch_11x f85446cb1af [read: same blob as main]; branch_10x 657014b3c57 and branch_10_6 4ae946e5a9b identical to 10.5.2 [read 2026-10-29]; not fixed anywhere
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
non-formal: code reading; also reached by exhaustive check
Upstream and prior reports
none found (2026-10-26, follow-up prior-report record verdict new: one scripted prior-report search with 16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list (dev@, java-user@, issues@, commits@), 4 JIRA-archive and 6 web queries, 73 distinct hits, none high-overlap; the finder's earlier lists/JIRA run; 12 manual GitHub REST and 13 GraphQL phrasings; 32 lists.apache.org queries over builds@, dev@, java-user@ and issues@; SOLR and LUCENE JIRA queries; the changed-file lists of all 411 open apache/lucene PRs, none of which touches either rescorer file; the upstream commit history of both files on main and branch_10x, with no fix; full reads of #14776, #14729 and #14793). Nearest miss, not a report: #14793 (ChrisHegarty, merged 2025-06-16) fixed a different flaky failure of the same upstream test, an ArrayIndexOutOfBoundsException in the test's own scoreDocs[0] on a non-empty index with no matches, by raising its document count from nextInt(100) to nextInt(50, 100); that change also removed the 0-document draw that reached this bug (executed: 6 of 1000 iterations of the original test on 10.3.0 hit this NoSuchElementException), but nobody there mentions the exception, empty indexes or getFirst, and builds@ has no failure mail for TestDoubleValuesSourceRescorer. Related, not reports: #14776 (introduced the line), #14729 (added LateInteractionRescorer), SOLR-17951 (Solr reranks function queries with the class, but its ReRankCollector returns early on zero first-pass hits, so it never reaches this path). Gaps: GitHub's search rate limit was reached (the remaining phrasings ran through GraphQL), private reports are not visible, and searches cannot prove absence; related precedent, not a report: #16926 (same empty-input shape in RescoreTopNQuery, fixed 2026-10-28)
Lucene commit tested
main 645d4fe14a20 (commit of 2026-10-28): the minimal repro with gradle in two scratch worktrees (removed after the runs), stock with assertions on and off, and with the recommended fix together with the four rescorer test suites (and the format check); a standalone runner on main 645d4fe14a20, ea57360912ed and a9779ef3c193 and on the published jars of every release from 10.3.0 to 10.6.0; the independent tests of the finder, verifiers, an adjudication and a credibility audit. Upstream main (2f424fd428da), branch_11x and branch_10_6 still have the same lines on 2026-10-30. JDK OpenJDK 25.0.3, Linux aarch64.

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/search/DoubleValuesSourceRescorer.java:45-86 (rescore; rewrite :47, unguarded leaves.getFirst() :53, docBase/maxDoc read :55, per-hit loop :58, foreign-hit IllegalStateException :60-66)
  • lucene/core/src/java/org/apache/lucene/search/LateInteractionRescorer.java:22-32 (@lucene.experimental; extends DoubleValuesSourceRescorer, no rescore() override)
  • lucene/core/src/java/org/apache/lucene/search/Rescorer.java:30-43 (@lucene.experimental; rescore contract: hits must come from the provided searcher)
  • lucene/core/src/java/org/apache/lucene/search/QueryRescorer.java:49-73 and SortRescorer.java:38-66 (leaves looked up only inside the per-hit loop)
  • a Lucene clone git show 645d4fe14a20 (CHANGES.txt 11.0.0 Bug Fixes entry)
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/search/DoubleValuesSourceRescorer.java:45-86 (at 645d4fe14a20) (rewrite :47; clone and sort :49-50; leaves().getFirst() :52-55; per-hit loop :58-77; IllegalStateException :60-66)
  • lucene/core/src/java/org/apache/lucene/search/LateInteractionRescorer.java:22-32 (at 645d4fe14a20) (intended use; @lucene.experimental; extends DoubleValuesSourceRescorer, no rescore override)
  • lucene/core/src/java/org/apache/lucene/search/QueryRescorer.java:49-80, (at 645d4fe14a20) SortRescorer.java:38-72 (leaves indexed only inside the per-hit loop)
Who is affected
  • lucene/core/src/java/org/apache/lucene/search/LateInteractionRescorer.java:25-30, (at 645d4fe14a20) Rescorer.java:30, DoubleValuesSourceRescorer.java:25-26 (no experimental tag)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/search/Rescorer.java:30-43; (at 645d4fe14a20) DoubleValuesSourceRescorer.java:25-26, :60-66
  • lucene/core/src/test/org/apache/lucene/search/TestIndexSearcher.java:224-233; (at 645d4fe14a20) lucene/test-framework/src/java/org/apache/lucene/tests/search/QueryUtils.java:165-180
  • lucene/core/src/java/org/apache/lucene/index/MultiDocValues.java:183-184; (at 645d4fe14a20) lucene/join/src/java/org/apache/lucene/search/join/JoinUtil.java:533-534; lucene/facet/src/java/org/apache/lucene/facet/StringValueFacetCounts.java:265
  • lucene/CHANGES.txt:338-339 (at 645d4fe14a20) (#16926, 11.0.0 Bug Fixes), :12275 (LUCENE-5682)
History
  • lucene/CHANGES.txt:1588-1589 (at 645d4fe14a20) (#14776, 10.3.0 New Features)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Made in parallel with this page: a summary card (PNG), built from facts checked against the code, the minimal repro, the finder's fix and executed probe output (main and the 10.6.0 release).

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.