The merged view keeps one term-vectors sub-reader per segment and stores null for a segment without term vectors. Four of its methods skip that null; clone() calls clone() on every entry and throws.
- A merge policy can renumber the documents of a merge (as
BPReorderingMergePolicydoes) by overridingOneMerge#reorder. When index sorting is off and the merge has two or more segments,IndexWritercalls it with one mergedCodecReaderview over all of them (for a single segment it passes the plain reader). - The view reads each segment through that segment's own readers. A segment in which no document stored term vectors has no term-vectors reader at all:
null.
null. get checks for the null and returns no term vectors for doc 1; clone() calls clone() on every entry and throws on segment 1's. Select the still to open it at full size. Generated with AI, checked against the code.- Index 2 one-document segments: doc 0 has field
f="a"with term vectors stored; doc 1 is an empty document, so its segment has no term vectors. - Merge them (
addIndexesorforceMerge(1)) with a merge policy whoseOneMerge#reorder(view, ...)callsview.getTermVectorsReader().clone(), for example to give each worker its own reader. - You expect a clone that returns doc 0's term vectors and
nullfor doc 1, as the uncloned reader does. - Instead
clone()throwsNullPointerExceptionatSlowCompositeCodecReaderWrapper.java:233, with assertions on and off. OnforceMergetheIndexWriterthen closes itself, drops uncommitted documents, and fails the same way after a reopen. - Why: the view keeps one sub-reader per segment and stores
nullfor segment 1.get,prefetch,checkIntegrityandcloseskip thenull;clone()callsclone()on every entry.
Only applications with their own merge code: a custom OneMerge#reorder that clones the view's term-vectors reader, or a custom codec that clones its merge inputs during a reordering merge. Nothing in Lucene does either. The data is ordinary (any segment without term vectors); on the merge path the writer closes and uncommitted changes are lost.
Mechanism, terms and numbers357 words
When index sorting is off, IndexWriter passes every merge of two or more segments, including an addIndexes(CodecReader...) merge of two or more readers, to OneMerge#reorder as one merged CodecReader view, SlowCompositeCodecReaderWrapper (IndexWriter.java:3561-3562, :5556-5557); not when the segments have blocks but the index has no parent field (:3551-3556, :5547-5552). The view's getTermVectorsReader() keeps one sub-reader per segment, and the sub-reader is null for a segment without term vectors (SlowCompositeCodecReaderWrapper.java:206-213, SegmentReader.java:246-253). get, prefetch, checkIntegrity and close handle that null; clone() maps TermVectorsReader::clone over the array and throws NullPointerException at :233.
Nothing in Lucene clones this reader (merges use getMergeInstance(), MergeState.java:150-152), so only application code reaches it: a custom OneMerge#reorder, or a custom codec that clones MergeState.termVectorsReaders during a reordering merge. On the merge path IndexWriter treats the exception as a tragic event (IndexWriter.java:5111). The fix is a one-line null guard in clone().
- The view.
SlowCompositeCodecReaderWrapper.wrap(readers)returns the single reader itself for one input and a new wrapper for two or more (SlowCompositeCodecReaderWrapper.java:48-57); the class is "primarily targeted at merging, not searching" (:43-45). - A
nullsub-reader on purpose.getTermVectorsReader()mapsCodecReader::getTermVectorsReaderover the inputs (:206-213);SegmentReader.getTermVectorsReader()returnsnullwhen the segment has no term vectors, else a fresh clone (SegmentReader.java:246-253). - Four methods handle it.
closeusesIOUtils.close, which skipsnull(:225-228);checkIntegrity(:237-244),prefetch(:246-253) andget(:255-264) test the entry first. clone()does not (the fix site).Arrays.stream(readers).map(TermVectorsReader::clone)(:230-235) dereferences thenullentry at:233.SortingCodecReader's term-vectorsclone()delegates (SortingCodecReader.java:831-834), so a sorting view over the merged view fails too.
Also executed: the reversed order (no term vectors first) and SortingCodecReader.wrap(view, ...) fail the same way; two segments that both have term vectors clone fine; a merge of segments of which none has term vectors also fails, because the view then returns a non-null reader whose entries are all null, so a caller's own null check does not help.
Exposure and likelihood272 words
- Who. A
OneMerge#reorderoverride (@lucene.experimental,MergePolicy.java:311-328) that clonesview.getTermVectorsReader(), for example one reader per task of the executor the hook receives "to parallelize the reordering logic" (:321-322). Or a custom codec whoseTermVectorsWriter.mergeclones the publicMergeState.termVectorsReadersentries during a merge that reorders (areorderthat returns a non-nullDocMap, so the merge reads throughSortingCodecReader.wrap(view, ...),IndexWriter.java:5574-5575); this trigger needs no@lucene.internalgetter and was executed on main. - Default vs opt-in. Opt-in: custom code is required. The data shape is common: any merge of two or more segments where at least one has no term vectors, including an index with no term vectors at all.
- Effect. A
NullPointerException. InaddIndexes(CodecReader...)it reaches the caller and the writer stays open: with the repro'sSerialMergeSchedulerit propagates straight out ofmergeScheduler.merge(IndexWriter.java:3312); with a concurrent scheduler it is recorded and rethrown at:3380-3382; only anErroris tragic there (:3390-3392). In a merge,IndexWriter.mergesends any non-abortThrowabletotragicEvent(t, "merge")(:5064-5112,:5111). An impact probe onforceMerge(1)showed: the writer is closed, its tragic exception is the NPE, an uncommitted third document is gone after reopening (2 docs left), and a reopened writer fails and closes the same way. That close isIndexWriter's generic handling of an exception that escapes the application's own reorder or codec code; stock merges without such code do not fail. Committed data stays intact; no wrong results. - Scale. Two one-document segments.
- Workaround. Call
getTermVectorsReader()(ortermVectors()) again instead ofclone(): each call builds fresh per-segment readers.
One JUnit test method: two one-document segments (one with term vectors, one without), addIndexes through a merge policy whose reorder clones the view's term-vectors reader. Single-threaded; it fails with assertions on and off and with every seed tried.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.index;
import java.io.IOException;
import java.util.concurrent.Executor;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.TextField;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestMergedViewTermVectorsClone extends LuceneTestCase {
public void testCloneWhenOneSegmentHasNoTermVectors() throws Exception {
FieldType tvType = new FieldType(TextField.TYPE_NOT_STORED);
tvType.setStoreTermVectors(true);
MergePolicy mp =
new FilterMergePolicy(NoMergePolicy.INSTANCE) {
@Override
public MergeSpecification findMerges(CodecReader... readers) {
MergeSpecification spec = new MergeSpecification();
spec.add(
new OneMerge(readers) {
@Override
public Sorter.DocMap reorder(CodecReader view, Directory dir, Executor executor)
throws IOException {
// expected: a clone that reads doc 0's term vectors; actual:
// NullPointerException
assertNotNull(view.getTermVectorsReader().clone().get(0));
return null;
}
});
return spec;
}
};
try (Directory src = newDirectory();
Directory dest = newDirectory()) {
try (IndexWriter w = new IndexWriter(src, new IndexWriterConfig())) {
Document withTv = new Document();
withTv.add(new Field("f", "a", tvType));
w.addDocument(withTv);
w.commit();
w.addDocument(new Document());
}
try (DirectoryReader r = DirectoryReader.open(src);
IndexWriter w =
new IndexWriter(
dest,
new IndexWriterConfig()
.setMergePolicy(mp)
.setMergeScheduler(new SerialMergeScheduler()))) {
w.addIndexes(
(CodecReader) r.leaves().get(0).reader(), (CodecReader) r.leaves().get(1).reader());
}
}
}
}
cp $REPRO/TestMergedViewTermVectorsClone.java $LUCENE/lucene/core/src/test/org/apache/lucene/index/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.index.TestMergedViewTermVectorsClone -Ptests.seed=DEADBEEF
# and with -Ptests.asserts=false: the same failure
- Expected
clone()returns a reader whoseget(0)returns doc 0's term vectors.- Observed
NullPointerExceptionatSlowCompositeCodecReaderWrapper$SlowCompositeTermVectorsReaderWrapper.clone(SlowCompositeCodecReaderWrapper.java:233), thrown out ofaddIndexes.
More files, environment, full output175 words
TestMergedViewTermVectorsClone (one method, extends LuceneTestCase, package org.apache.lucene.index of the lucene/core tests). new IndexWriterConfig() with SerialMergeScheduler, so the merge runs on the calling thread and the exception comes straight out of addIndexes. About 1 s. It runs in LuceneTestCase's randomized environment (codec and directory vary with the seed); the failure does not depend on it: every seed tried failed (DEADBEEF, and 1234ABCD and 77 in the final repro check's runs). The getter it calls, CodecReader#getTermVectorsReader(), is public but @lucene.internal; the codec trigger does not need it. It is the hunt's minimal test after the code formatter, which only re-wrapped two lines; it passes checkGoogleJavaFormat and can serve as the regression test.
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT): fails; 9.9.0 to 10.6.0 (7 releases run): the same scenario fails; 9.8.0: API absent |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (nothing platform-specific) |
TestMergedViewTermVectorsClone > testCloneWhenOneSegmentHasNoTermVectors FAILED
java.lang.NullPointerException
at __randomizedtesting.SeedInfo.seed([DEADBEEF:4A8CFE97FA2AAD8A]:0)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
...
at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:652)
at org.apache.lucene.index.SlowCompositeCodecReaderWrapper$SlowCompositeTermVectorsReaderWrapper.clone(SlowCompositeCodecReaderWrapper.java:233)
at org.apache.lucene.index.TestMergedViewTermVectorsClone$1$1.reorder(TestMergedViewTermVectorsClone.java:45)
at org.apache.lucene.index.IndexWriter.addIndexesReaderMerge(IndexWriter.java:3562)
at org.apache.lucene.index.IndexWriter$AddIndexesMergeSource.merge(IndexWriter.java:3463)
at org.apache.lucene.index.SerialMergeScheduler.merge(SerialMergeScheduler.java:38)
at org.apache.lucene.index.IndexWriter.addIndexes(IndexWriter.java:3312)
at org.apache.lucene.index.TestMergedViewTermVectorsClone.testCloneWhenOneSegmentHasNoTermVectors(TestMergedViewTermVectorsClone.java:68)TermVectorsReader.clone()promises "a clone that one caller at a time may use to read term vectors", with no precondition.- The class stores the
nullitself and skips it inget,prefetch,checkIntegrityandclose;clone(), from the same commit, is the outlier. - For unsupported operations this class throws
UnsupportedOperationException; upstream fixed the same pattern for points in this wrapper (#12859).
The full argument185 words
- Contract.
TermVectorsReader.clone(): "Create a clone that one caller at a time may use to read term vectors" (TermVectorsReader.java:45-47); no precondition, no exception documented. - The class contradicts itself. The
nullentry is a state the view creates (SlowCompositeCodecReaderWrapper.java:206-213) and handles in four methods (:225-264). Unsupported operations in this class throwUnsupportedOperationException(:366,:371,:491). - History.
getTermVectorsReader(),clone(), the null-checkedgetandcheckIntegrityand the null-skippingcloseall arrived inf7cab164501(#12622, 2023-11-23);prefetchcame later with its own guard (#13758, 10.0.0). Nothing has touchedclone()since. No javadoc, CHANGES or MIGRATE entry saysclone()is unsupported on this view. - Tests and precedent. Upstream
TestSortingCodecReaderclones the term-vectors reader of a wrapper view (SortingCodecReaderoverSlowCodecReaderWrapper) and asserts on the clone (TestSortingCodecReader.java:229-237), so cloning a merge-oriented wrapper's term-vectors reader is exercised; that view wraps a single segment with nonullsub-reader, so the test does not cover this case. Maintainers fixed the same pattern elsewhere: #12858/#12859 (nullPointValuesin this wrapper, 9.9.0) and #14123/#14125 (null vectors reader inSortingCodecReader).
The devil's-advocate check122 words
The strongest case for "not a bug", made by the cross-model reviewer (OpenAI GPT-5.6), which reproduced the NPE and agreed on the root cause: the trigger calls CodecReader#getTermVectorsReader(), which is @lucene.internal (CodecReader.java:44-50), inside an @lucene.experimental hook, and normal merges call getMergeInstance(), not clone().
Why it does not settle it: the codec trigger uses no internal getter, only the codec API and the public MergeState.termVectorsReaders; @lucene.internal and @lucene.experimental speak to API stability, not to a documented method throwing NPE on a state the class creates itself; and a caller that checks for null first still gets the NPE. A maintainer may still call it out of scope or too minor, a fair outcome for a Low bug.
Recommended Null guard in clone() (fix A)
Map a null sub-reader to null in clone(): .map(r -> r == null ? null : r.clone()). With it the repro passes with assertions on and off, and 61 tests of the closest suites pass.
Arrays.stream(readers)
.map(r -> r == null ? null : r.clone())
.toArray(TermVectorsReader[]::new),All options and trade-offs2 options ยท 285 words
Fix A was built and checked through gradle on 5adff78bb168; B was not run. The same missing-sub-reader pattern in this wrapper's vector copy() is registered as LFV-269 (a different method, trigger and version range); the two are related hardening in the same class and could be considered together.
In SlowCompositeTermVectorsReaderWrapper.clone() (SlowCompositeCodecReaderWrapper.java:230-235), clone only the non-null sub-readers and keep null for the others, as get, prefetch, checkIntegrity and close already assume.
Arrays.stream(readers)
.map(r -> r == null ? null : r.clone())
.toArray(TermVectorsReader[]::new),Trade-offs
None found: the clone then returns null for documents of a segment without term vectors, exactly like the original reader. The change is confined to clone().
What the checks validated
- Gradle in a scratch worktree at
5adff78bb168, seedDEADBEEF: the repro fails without the fix (assertions on and off: 1 test, 1 failure) and passes with it (assertions off). - With the fix and assertions on: the repro (1),
TestSortingCodecReader(3),TestGetVectorCountCodecWrappers(1),TestAddIndexes(39),TestIndexWriterMerging(13) andTestTermVectors(4): 61 tests, 0 failures.checkGoogleJavaFormatpasses. The full core suite was not run; only OpenJDK 25.0.3 on Linux aarch64. - Earlier: the hunt's shadow-class runs of the same change pass the minimal repro with
-eaand-da; the independent reproducer's own null-tolerantclone()passes itsforceMerge(1)test.
When the view is built, put an empty TermVectorsReader (whose get returns null) where a segment has none.
Trade-offs
Removes the null checks everywhere, but is a larger change to a class that already handles null consistently in four methods. Not recommended.
What the checks validated
Not run.
Downstream products are affected only if their own merge-policy or codec code clones this reader; documents and queries alone never reach it.
A proof-obligation audit of SlowCompositeCodecReaderWrapper's term-vectors reader against the TermVectorsReader.clone contract: every method must cope with the null sub-reader the view stores for a segment without term vectors. clone() does not; a public-API JUnit test then witnessed the exception.
The method in full98 words
Method: semi-formal (a proof-obligation audit of a class's contract, witnessed by an executable repro).
- The finder listed the states the view can be in (a
nullentry for each segment without term vectors) and checked each method of the term-vectors wrapper against them; onlyclone()fails the obligation. - The finder then wrote a public-API JUnit test and the one-line null guard that makes it pass.
- An independent reproducer, an executed cross-model review and a devil's-advocate verifier reproduced it with their own tests, and an adjudication re-ran the reproducer's test with a fresh seed ("Prior reports, history and checks").
No matching prior report found in apache/lucene issues, PRs and commits, the mailing lists, downstream trackers or the web (searched 2026-11-03). Nearest: #12858/#12859 (points in this wrapper, fixed) and #14123/#14125 (vectors in SortingCodecReader).
Searches3 searches
No matching public report was found in the documented searches. Nearest items, none a report of this bug: #12622 (the introducing PR; its 28 review comments never discuss clone() or a missing term-vectors sub-reader), #12858/#12859 (null PointValues in this wrapper, fixed in 9.9.0), #14123/#14125 (null vectors reader in SortingCodecReader), #14092 (this wrapper's vector copy). No open PR has a hunk on the term-vectors clone(). Limits: keyword search; GitHub search does not index every review comment (those of #12622 were read through the API); private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive) | 2026-11-03; re-checked the same day | two scripted prior-report searches (45 queries each, different phrasings) and 27 manual searches (issues and PRs in all states, three downstream repos); the 28 review comments of #12622 | no report; #12858/#12859 and #14123/#14125 are the nearest |
| Open PRs | 2026-11-03 | the changed files of all 410 open apache/lucene PRs; the patches of 8 of the 12 that touch a related file, including all 3 that touch SlowCompositeCodecReaderWrapper; PRs updated since 2026-11-01 | none changes the term-vectors clone() |
| Mailing lists, downstream, web | 2026-11-03 | dev@, java-user@, issues@ and commits@ (the scripted searches' queries plus 5 searches over all lucene.apache.org lists), Elasticsearch, OpenSearch and Solr trackers, 3 web searches | nothing about this bug |
Introduced with the class itself in #12622 (f7cab164501, 2023-11-23), first released in 9.9.0; 9.8.0 has no such API. Every executed release since fails, through 10.6.0 and main. Not fixed upstream.
Versions tested5 rows
Introduced by f7cab1645017 on main (squash-merge of #12622 "Add a merge policy wrapper that performs recursive graph bisection on merge.", Adrien Grand, 2023-11-23; branch_9x copy 6b1538b33136). The same change created SlowCompositeCodecReaderWrapper and added OneMerge#reorder, so it is not a regression: the trigger API arrived with the defect.
The version runs used the hunt's earlier one-test variant of the same scenario; 9.9.0 and 9.11.1 a copy with the two-argument reorder(CodecReader, Directory) those releases have; 10.5.2 the independent reproducer's forceMerge(1) test. By reading, every release tag from 9.9.0 to 10.6.0 has the unguarded line, and so do upstream main 1c2072f0284 (2026-11-02), branch_11x, branch_10_6 and branch_9_12.
| Version or commit | Verdict | Result |
|---|---|---|
| 9.8.0 | API absent | does not compile: no merged view, no OneMerge#reorder |
| 9.9.0 | JUnit, -ea and -da | fails, same NPE |
| 9.11.1, 9.12.0, 9.12.3, 10.0.0, 10.5.2 | JUnit | fails, same NPE |
| 10.6.0 | JUnit, -ea and -da | fails, same NPE |
| main 5adff78bb168 | JUnit, -ea and -da | fails, same NPE |
A finder, an independent reproducer, an executed cross-model review (OpenAI GPT-5.6: reproduced it, but judged it out of contract), a devil's-advocate verifier, an adjudication, an impact probe, an audit and a final repro check reproduced it. This doc's gradle runs reproduced it on main and validated the fix.
Checks and verdicts251 words
The cross-model review ran on an older main, ea57360912ed; its test was re-run on 5adff78bb168 (mixed case fails, control passes). Every other main run used 5adff78bb168.
| Check | Verdict | What it found |
|---|---|---|
| finder | reproduced | Own test fails on main (assertions on and off; 2 more seeds with assertions on), 10.6.0 and 9.12.3; the null guard makes it pass. |
| independent reproducer | real | Own forceMerge(1) test fails on main (on, off), 10.6.0, 10.5.2 and 9.12.3; passes with its own null-tolerant clone(). |
| cross-model review, refute lens (OpenAI GPT-5.6, executed) | not real (high), on reach only | Reproduced the NPE with its own test (assertions on and off), control passes, agrees on the root cause; argues the trigger is outside the supported API (see the devil's advocate). |
| devil's-advocate verifier | real | No intended or documented reading survives; found the codec trigger and the no-term-vectors-at-all case. |
| adjudication | confirmed (medium), Low | Re-ran the reproducer's test with a fresh seed, assertions on and off: fails. |
| separate re-run, impact probe, audit and final repro check | fail / tragic close / crash / minor | Fresh seeds fail on main; the probe shows the tragic close on forceMerge; the audit downgraded the impact from index unusable to crash; the final repro check kept the one-test repro and set the impact to minor. |
| this doc's gradle fix check | reproduced; fix A validated | Fails without the fix (on, off); passes with it; 61 tests of 5 closest suites pass. |
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 498 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,390 words
- Registry title
- The term-vectors reader of the merged view that IndexWriter hands to OneMerge#reorder (SlowCompositeCodecReaderWrapper) throws NullPointerException from clone() as soon as one merged segment has no term vectors, although the same class handles that null sub-reader in get, prefetch, checkIntegrity and close; on the merge path this closes the IndexWriter (since 9.9.0)
- Area
- core / index: SlowCompositeCodecReaderWrapper (merged CodecReader view handed to OneMerge#reorder), term vectors
- Priority
- Low, rank 679: Cloning the term-vectors reader of the merged view handed to OneMerge#reorder throws NullPointerException when a merged segment has no term vectors, and on the merge path that closes the IndexWriter, drops uncommitted changes and fails again after a reopen, in every release since 9.9.0. Only custom reorder or codec code makes that clone and nothing in Lucene does, so it is contrived despite the severe effect.
- Impact class
- minor
- Impact note
- Hits only applications with their own merge code that clones the merged view's term-vectors reader (a custom @lucene.experimental OneMerge#reorder calling view.getTermVectorsReader().clone(), the getter being @lucene.internal, or a custom codec whose TermVectorsWriter.merge clones MergeState.termVectorsReaders during a reordering merge; nothing in Lucene does either): then every merge or addIndexes(CodecReader...) of two or more segments of which at least one has no term vectors (an index with no term vectors at all included) throws NullPointerException with -ea and -da, no scale needed; on the forceMerge/background-merge path IndexWriter treats it as a tragic event, closes itself and drops uncommitted changes, and the same merge fails again after a reopen, while addIndexes just throws; no wrong result and no corruption of committed data; in every release from 9.9.0 to 10.6.0 and on main.
- Summary
- You write a merge policy whose OneMerge#reorder clones the term-vectors reader of the merged view it receives, and merge (or addIndexes) a segment with term vectors together with one in which no document has a term-vector field, expecting a reader that returns the first segment's term vectors, as the uncloned reader does. Instead clone() throws NullPointerException at SlowCompositeCodecReaderWrapper.java:233: the view stores a null sub-reader for the segment without term vectors (:206-213), and clone() maps TermVectorsReader::clone over that array (:230-235) without the null check that get, prefetch and checkIntegrity have and that close gets from IOUtils.close. On the merge path the exception closes the IndexWriter as a tragic event, and it fails the same way after a reopen.
- Introduced
- f7cab1645017d863331b42900581b67d3591e2da (main; squash-merge of apache/lucene#12622 'Add a merge policy wrapper that performs recursive graph bisection on merge.' by Adrien Grand (jpountz), parent 76fe6bdbc16); the same change is on branch_9x as 6b1538b33136a4737ad0d22b3633e62f3d7f362b (same author and author date), which 9.9.0 contains. That one change creates SlowCompositeCodecReaderWrapper with the unguarded clone() next to the null-checked get() and checkIntegrity() and the null-skipping close(), and adds OneMerge#reorder [read: git log -S 'TermVectorsReader::clone' over lucene/core/src/java in the full history finds only these two commits; git blame -w -M -C attributes getTermVectorsReader (:206-213) and clone() (:230-235) to f7cab164501; exec: 9.8.0 has neither the class nor OneMerge#reorder (compile error), 9.9.0 fails]. Not a regression: the trigger API arrived with the defect. Later commits on the file (24 on main, e.g. #13758 adding a null-guarded prefetch, #13289 adding the Executor parameter to reorder) never touched clone(). (2023-11-23 (author date = commit date 2023-11-23T13:25:00+01:00 on main; the branch_9x commit 6b1538b3313 was committed 23 s later))
- First affected version
- 9.9.0 [exec] (tag 2023-12-04; fails with -ea and -da, run with a copy of the test that differs only in the two-argument reorder(CodecReader, Directory) signature that 9.9.0-9.11.x have); 9.8.0 has no SlowCompositeCodecReaderWrapper and no OneMerge#reorder [exec: compile error]
- Affected versions
- 9.9.0 through 10.6.0 (4ae946e5a9b) and unreleased main 5adff78bb168 (12.0.0-SNAPSHOT): 9.9.0-9.9.2, 9.10.0, 9.11.0-9.11.1, 9.12.0-9.12.3, 10.0.0-10.6.0 [read: git tag --contains f7cab164501 and 6b1538b3313 over releases/lucene/*; each tag has the unguarded '.map(TermVectorsReader::clone)' line]. [exec] fail: main 5adff78bb168 with -ea and -da (finder, the independent reproduction, the GPT reviewer's test re-run on 5adff78, the intended-behaviour check, adjudication, re-run), 10.6.0 (-ea and -da), 10.5.2, 10.0.0, 9.12.3, 9.12.0, 9.11.1 and 9.9.0 (-ea and -da; 9.9.0 and 9.11.1 with the two-argument reorder copy). Not run: 9.9.1, 9.9.2, 9.10.0, 9.11.0, 9.12.1, 9.12.2 and 10.1.0-10.5.1 (identical line by reading each tag). Not fixed upstream: the line is unchanged on upstream main 1c2072f0284 (2026-11-02), branch_11x, branch_10_6 and branch_9_12. Not new in 10.6 (9.9.0 and 10.5.2 fail). All runs used JDK 25.0.3 on Linux aarch64.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that disputed it (the adjudication ruled it real), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: proof-obligation audit
- Upstream and prior reports
- none found (2026-11-03, on top of the finder's, the intended-behaviour check's and the adjudication's searches; rechecked the same day): two scripted prior-report searches with different phrasings (45 queries each over apache/lucene issues and PRs, commits, Elasticsearch/OpenSearch/Solr, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive), 27 manual GitHub searches (issues and PRs in all states, incl. three downstream repos), 5 searches over all lucene.apache.org lists, 3 web searches, the 28 review comments of #12622 read through the API, a file-list scan of all 410 open apache/lucene PRs (12 touch a related file; the patches of #16879, #16847, #16571, #13342, #15430, #16864, #13948 and #12636 have no term-vectors clone hunk) and the PRs updated since 2026-11-01: no issue, PR, commit, list thread or JIRA entry reports or fixes the clone() NullPointerException. Nearest items, not this bug: #12858/#12859 (null PointValues in this wrapper's getValues, fixed in 9.9.0), #14123/#14125 (null vectors reader in SortingCodecReader, fixed there only, 2025), #14092 (this wrapper's vector copy, the LFV-269 area), and the introducing PR #12622 (s1monw's 'filter non null' review remark is a style comment on another loop, not a report). Unfixed on upstream main 1c2072f0284, branch_11x, branch_10_6 and branch_9_12; the newest commit on the file is 874ba4d39af (#16647, author date 2026-09-08).
- Lucene commit tested
- main
5adff78bb168(2026-11-02) through gradle on 2026-11-03 in a scratch worktree, seedDEADBEEF: the repro without the fix (assertions on and off: fails) and with fix A (assertions off: passes; assertions on, with 5 closest suites: 61 tests, 0 failures), pluscheckGoogleJavaFormat(passes). The hunt ran the same scenario on 9.9.0, 9.11.1, 9.12.0, 9.12.3, 10.0.0, 10.5.2 and 10.6.0 (fail) and 9.8.0 (API absent). Upstream main1c2072f0284was read on 2026-11-03. Only Linux aarch64 with OpenJDK 25.0.3 was run.
- Finding record
lucene/core/src/java/org/apache/lucene/index/SlowCompositeCodecReaderWrapper.java:43-45 (at 5adff78bb168) (class javadoc: 'primarily targeted at merging, not searching'), :206-213 (getTermVectorsReader maps CodecReader::getTermVectorsReader over the inputs, null for an input without term vectors), :225-228 (close via IOUtils.close, which skips nulls), :230-235 (clone maps TermVectorsReader::clone with no null check; NPE at :233; fix site), :237-244, :246-253, :255-264 (checkIntegrity, prefetch and get test for the null sub)lucene/core/src/java/org/apache/lucene/index/SegmentReader.java:246-253 (at 5adff78bb168) (getTermVectorsReader returns null when the segment has no term vectors, else a fresh clone)lucene/core/src/java/org/apache/lucene/codecs/TermVectorsReader.java:45-47 (at 5adff78bb168) (clone(): 'Create a clone that one caller at a time may use to read term vectors', no precondition)lucene/core/src/java/org/apache/lucene/index/CodecReader.java:44-50 (at 5adff78bb168) (getTermVectorsReader: 'Expert: retrieve underlying TermVectorsReader', @lucene.internal)lucene/core/src/java/org/apache/lucene/index/MergePolicy.java:311-328 (at 5adff78bb168) (OneMerge#reorder receives a merged view and an executor 'to parallelize the reordering logic', @lucene.experimental)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:5064-5112 (at 5adff78bb168) (merge: any Throwable from mergeMiddle reaches tragicEvent(t, "merge") at :5111)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3376-3393 (at 5adff78bb168) (addIndexes(CodecReader...) rethrows a merge exception; only an Error is a tragic event)lucene/core/src/java/org/apache/lucene/index/MergeState.java:150-152 (at 5adff78bb168) (merges take getTermVectorsReader() then getMergeInstance(), not clone())lucene/core/src/java/org/apache/lucene/index/SortingCodecReader.java:831-834 (at 5adff78bb168) (the sorting view's term-vectors clone delegates to the wrapped reader)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/index/SlowCompositeCodecReaderWrapper.java:43-57, (at 5adff78bb168) :206-264lucene/core/src/java/org/apache/lucene/index/SegmentReader.java:246-253 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/index/SortingCodecReader.java:831-834 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/util/IOUtils.java:80-86 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3561-3562, (at 5adff78bb168) :5556-5557lucene/misc/src/java/org/apache/lucene/misc/index/BPReorderingMergePolicy.java:133 (at 5adff78bb168) (overrides reorder)
- Who is affected
lucene/core/src/java/org/apache/lucene/index/MergePolicy.java:311-328 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/index/MergeState.java:150-152 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3312, (at 5adff78bb168) :3376-3393, :3472-3473, :5055, :5064-5112, :5574-5575
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/codecs/TermVectorsReader.java:45-47 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/index/CodecReader.java:44-50 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/index/SlowCompositeCodecReaderWrapper.java:206-264, (at 5adff78bb168) :366, :371, :491lucene/core/src/test/org/apache/lucene/index/TestSortingCodecReader.java:229-237 (at 5adff78bb168)https://github.com/apache/lucene/pull/12622, https://github.com/apache/lucene/pull/12859, https://github.com/apache/lucene/pull/14125
- Prior reports
https://github.com/apache/lucene/issues/12858, https://github.com/apache/lucene/issues/14123, https://github.com/apache/lucene/pull/14092
- Diagrams
lucene/core/src/java/org/apache/lucene/index/SlowCompositeCodecReaderWrapper.java:206-264 (at 5adff78bb168)
- TestMergedViewTermVectorsClone.java (one JUnit test):
repro/LFV-1218/TestMergedViewTermVectorsClone.java(73 lines)
The diagram follows the repro's index and the code at 5adff78bb168 (SlowCompositeCodecReaderWrapper.java:206-264).
assets/LFV-1218/bug.png(498 KB)
