A rescorer asked to rescore 0 hits on a reader with no segments throws instead of returning 0 hits.
- 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 afterdeleteAll()andnew MultiReader()have none:leaves()is an empty list, and every search on them returns 0 hits. - A
Rescorertakes 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 aDoubleValuesSource;LateInteractionRescorerextends it to rerank with multi-vectors.
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.- Open a searcher on a reader with no segments: a brand-new index, an NRT reader before the first document, an index after
deleteAll(), ornew MultiReader().search(MatchAllDocsQuery.INSTANCE, 10)correctly returns 0 hits. - Rescore those 0 hits with a
DoubleValuesSourceRescorer(orLateInteractionRescorer), topN 10. You expect 0 hits back, which is whatQueryRescorerandSortRescorerreturn. - Instead
rescore()throwsjava.util.NoSuchElementExceptionwith no message, with assertions on and off. - Why: before it looks at any hit,
rescore()takes the first segment withleaves().getFirst()(DoubleValuesSourceRescorer.java:53), and there is none. With 1 segment the same call works, even when the first pass has 0 hits.
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.
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).- Before its per-hit loop it runs
List<LeafReaderContext> leaves = searcher.getIndexReader().leaves();(:52),LeafReaderContext ctx = leaves.getFirst();(:53) andint leafEnd = ctx.docBase + ctx.reader().maxDoc();(:55). Nothing checks whether there are hits or segments. - With no segments,
leavesis empty and the JDK'sList.getFirst()throwsNoSuchElementExceptionwith no message. It is not a Lucene assertion, so-eaand-dabehave the same. - The per-hit loop (
:58-77) would not have run at all for 0 hits. The method's deliberate check, anIllegalStateExceptionfor hit doc ids past the last segment (:60-66), is for hits from another searcher. LateInteractionRescorer extends DoubleValuesSourceRescorer(LateInteractionRescorer.java:32) and does not overriderescore(), so it fails at the same line.QueryRescorer(QueryRescorer.java:55,:67,:73) andSortRescorer(SortRescorer.java:46,:60,:66) get the sameleaveslist but index it only insidewhile (hitUpto < hits.length), so with 0 hits they never touch a segment and return 0 hits.
- The minimal repro:
new IndexSearcher(new MultiReader()), aMatchAllDocsQueryfirst pass with 0 hits,rescore(searcher, firstPass, 10):NoSuchElementExceptionatDoubleValuesSourceRescorer.java:53, with-eaand-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, thenforceMerge(1)(the fully deleted segment is dropped); aKnnFloatVectorQueryfirst pass on an empty index followed byLateInteractionRescorer; an emptyMultiReader. - 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.testBasicindexednextInt(100)documents, and 6 of 1000 iterations on the 10.3.0 jars threw this exception. #14793 later raised the count tonextInt(50, 100)to fix a different failure, which also removed the 0-document draw.
Exposure and likelihood265 words
- Who is hit. Callers of
DoubleValuesSourceRescorersubclasses orLateInteractionRescorer, 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, afterdeleteAll(), after every document was deleted and merged away, an emptyMultiReader, 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 andIndexSearcher; the trigger is the opt-in rescorer API.DoubleValuesSourceRescoreritself has no@lucene.experimentaltag; its base classRescorerand its subclassLateInteractionRescorerdo. - 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
testBasicindexednextInt(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 usesDoubleValuesSourceRescorer, skips rescoring when the first pass is empty, so it is not affected; code search also found one small application usingLateInteractionRescorer(not examined), and other downstream use is unknown. - Workaround. Skip
rescore()when the first pass returned no hits.
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.
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);
}
}
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
QueryRescorerandSortRescorerreturn for the same input. - Observed
java.util.NoSuchElementException(no message) fromList.getFirstatDoubleValuesSourceRescorer.java:53, with assertions on and off.
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).
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); every release from 10.3.0 to 10.6.0 also fails; 10.2.2 and older: classes absent |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux aarch64 |
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- 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. QueryRescorerandSortRescorer, implementations of the same method, return 0 hits for the same input.- The maintainers fixed an analogous empty-input crash in
RescoreTopNQueryas 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 onRescorer,DoubleValuesSourceRescorerorLateInteractionRescorerasks for a non-empty reader or non-empty hits. - The siblings.
QueryRescorerandSortRescorerreturn 0 hits for an empty committed index on 9.12.3, 10.2.2, 10.3.0, 10.5.2 and main, and fornew MultiReader()on main, 10.5.2 and 10.3.0. - Zero-segment readers are a supported searcher input.
TestIndexSearcher.testGetSlicesNoLeavesNoExecutorandtestGetSlicesNoLeavesWithExecutorbuild anIndexSearcherovernew MultiReader()and check that it has 0 slices (TestIndexSearcher.java:224-233), and the test framework'sQueryUtils.wrapUnderlyingReaderdeliberately mixes emptyMultiReaders into the searchers it checks queries against (QueryUtils.java:165-180). - Lucene guards this case elsewhere before taking a first segment:
MultiDocValues.getNumericValuesreturns null for 0 leaves (MultiDocValues.java:183-184),JoinUtilreturnsMatchNoDocsQuery("JoinUtil.createJoinQuery with no segments")(JoinUtil.java:533-534),StringValueFacetCountschecksleafCount == 0(StringValueFacetCounts.java:265). A verifier's grep for direct first-segment calls (leaves().get(0),getFirst()) foundDoubleValuesSourceRescorer.java:53to 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
testBasiccould index 0 documents and hit this exception; #14793 (ChrisHegarty) raised the count to fix a different failure, the test's ownArrayIndexOutOfBoundsExceptionatscoreDocs[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.
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.
+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
QueryRescorerandSortRescorerreturn and keeps the first pass'stotalHits, as the normal path does (:85). FixesLateInteractionRescorer, which inherits the method. - Foreign hits on an empty reader (documented misuse) still throw
NoSuchElementExceptioninstead of the method'sIllegalStateException; 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) andTestSortRescorer(3), 17 tests, 0 failures;:lucene:core:checkGoogleJavaFormatpasses. 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.
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() - 1holds (both -1), so the method's ownIllegalStateExceptionfires instead ofNoSuchElementException. - 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.
Return the empty result when there are no hits, else throw the IllegalStateException.
Trade-offs
Same behaviour as C, with more lines.
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.
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:53as 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:
QueryRescorerandDoubleValuesSourceRescorerover every index of up to 5 documents (including an emptyMultiReaderand an emptyDirectoryReader), 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.QueryRescorerhad no mismatch;DoubleValuesSourceRescorerhad 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.
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.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs, commits (apache/lucene incl. the migrated JIRA archive; downstream repositories) | 2026-10-26 | an 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 projects | no report; closest #14793 (different failure of the same test), #14776 (added the line), #14729 (added LateInteractionRescorer) |
| Mailing lists, JIRA, web | 2026-10-26 | 20 automated and 32 manual list queries over dev@, java-user@, issues@, commits@ and builds@; SOLR and LUCENE JIRA queries; 6 web queries | nothing about this bug; builds@ has no failure mail for TestDoubleValuesSourceRescorer; SOLR-17951 uses the class but skips empty first passes |
| Open PRs and upstream history | 2026-10-26 | changed-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 #14793 | no open PR touches either file; no fix |
| Re-checks for this page | 2026-10-30 | commits 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 #16937 | unchanged (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 |
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.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 (unreleased 11.0) | affected | fails, -ea and -da (gradle and standalone) |
main ea57360912ed, a9779ef3c193 | affected | fail (ea57360912ed with -ea and -da) |
| 10.6.0 | affected | fails, -ea and -da (minimal test) |
| 10.5.2 | affected | fails, -ea and -da |
| 10.4.0, 10.5.0, 10.5.1 | affected | fail (-ea) |
| 10.3.0 to 10.3.2 | affected | fail (10.3.0 with -ea and -da) |
| 10.2.2, 9.12.3 | n/a | do not compile: no DoubleValuesSourceRescorer or LateInteractionRescorer; QueryRescorer and SortRescorer return 0 hits on the same input |
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.
| Check | Verdict | What it found |
|---|---|---|
| Finder | candidate | Exhaustive 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 | real | Own 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 weak | Read-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, high | Re-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 | keep | Re-ran the finder's and the intended-behaviour check's tests with a new seed; same failures. |
| Re-verify on main 645d4fe14a20 | still reproduces | Finder 2/3, the independent reproduction 4/7, the intended-behaviour check 5/8 fail with -ea and -da. |
| This page's runs | reproduced; fix validated | Gradle 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. |
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.
- PNG 431 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,206 words
- 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 main645d4fe14a20,ea57360912edanda9779ef3c193and 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.
- 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-66lucene/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-180lucene/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:265lucene/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)
- TestDoubleValuesSourceRescorerNoSegments.java (JUnit for Lucene's build):
repro/LFV-329/src/org/apache/lucene/search/TestDoubleValuesSourceRescorerNoSegments.java(27 lines)
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).
assets/LFV-329/bug.png(431 KB)
