setParentField makes IndexWriter mark the last document of each block with an internal parent field. A merge policy may keep segments whose documents are all deleted (keepFullyDeletedSegment); SoftDeletesRetentionMergePolicy does when its retention query matches. Merges and FieldInfos.getMergedFieldInfos require every segment to name the same parent field.
- Your config sets
setParentField("_parent")without an index sort and keeps soft-deleted documents withSoftDeletesRetentionMergePolicy("_soft", MatchAllDocsQuery::new, ...). You add documentaand commit. - You add the block
[child0, child1, parent];child1has a 32,767-byte keyword, one byte over the limit.addDocumentsthrowsIllegalArgumentExceptionand the writer stays open. You expect the block not to be added and the index to stay consistent. - But
child0andchild1were already indexed and are now deleted, and the parent marker is written only at a block's last document, which was never reached. The commit now holds a segment with 2 documents, 0 live and no_parentfield, and the retention policy keeps it. - You add
b, commit and runforceMerge(1)on the 3 segments, expecting 1 segment with 2 documents. You getIllegalStateException: expected parent doc field to be "_parent " across all segments but found a segment with different field "null", and IndexWriter closes itself, losing any uncommitted documents. - The segment is committed: a new IndexWriter, even with the default
TieredMergePolicy, fails the same way, andFieldInfos.getMergedFieldInfos, whichUnifiedHighlighteruses, throws on multi-segment readers that hold it.forceMergeDeletes()removes it.
Applications that set a parent field without an index sort and keep fully deleted segments, for example SoftDeletesRetentionMergePolicy with a match-all retention query. One rejected block, alone in a fresh in-memory segment, then makes merges close IndexWriter until forceMergeDeletes() removes it. UnifiedHighlighter fails on multi-segment readers holding it, also with an index sort.
Mechanism, terms and numbers871 words
With IndexWriterConfig#setParentField and no index sort, IndexWriter's row-by-row indexing path creates the parent field in a segment only when it indexes the last document of a block. When it rejects a block at a child with a documented, non-aborting exception, such as a term longer than MAX_TERM_LENGTH, the documents it already indexed stay in the segment as deleted documents. If that block was the only content of its in-memory segment, for example the first thing indexed after a commit, the segment is flushed with no live documents and no parent field. Merge policies normally drop it; SoftDeletesRetentionMergePolicy with a matching retention query, or any policy whose keepFullyDeletedSegment returns true, keeps it. From then on every merge that combines it only with segments holding no multi-document block throws IllegalStateException ("expected parent doc field ... across all segments") and closes IndexWriter, losing uncommitted documents, also with each new writer on the index, until forceMergeDeletes() removes the segment. Readers that hold it next to other segments fail FieldInfos.getMergedFieldInfos, so UnifiedHighlighter fails, also with an index sort. The data stays intact and CheckIndex reports the index clean. Introduced by #12829 in 9.10.0; every release since and main are affected.
- Document block:
IndexWriter.addDocumentsadds several documents together, children first and the parent last; the javadoc promises that a reader sees "all or none of the documents" (IndexWriter.java:1496-1497). - Parent field: with
IndexWriterConfig.setParentField("_parent"), "IndexWriter will add an internal field to every root document" (IndexWriterConfig.java:580-581): the last document of each block, and every single document. It lets index sorting and merge-time reordering keep blocks together. - Fully deleted segment: a segment whose documents are all deleted. IndexWriter drops it unless the merge policy's
keepFullyDeletedSegmentreturns true (IndexWriter.java:6429-6432,MergePolicy.java:761-767).SoftDeletesRetentionMergePolicyreturns true when its retention query matches any document of the segment, evaluated with hard deletes ignored (SoftDeletesRetentionMergePolicy.java:80-93). - Merged view: without an index sort, a merge of two or more segments first wraps them in one
SlowCompositeCodecReaderWrapper(for optional reordering), whose field infos come fromFieldInfos.getMergedFieldInfos. That method requires every segment to name the same parent field.
- Registered at the last document only. On the row-by-row path that
addDocument(s)andupdateDocument(s)use,IndexingChain.processDocumentqueues and indexes the parent field onlyif (parentPf != null && lastDocInBlock)(IndexingChain.java:604-608,:645-652). (The column-batch path,addBatch, creates it eagerly,:824-833.) - A rejected block stays behind, deleted.
DocumentsWriterPerThread.updateDocumentsindexes a block's documents one by one and, on a non-aborting exception at a child, marks those it already processed deleted (deleteLastDocs,DocumentsWriterPerThread.java:303-306);hasBlocksis set only when the whole block succeeded (:297-298). The writer stays open. - An empty segment without the field. If the block was the only content of its in-memory segment, the segment is flushed with 0 live documents, no parent FieldInfo and
hasBlocks=false. Kept by the merge policy, it is committed. - The merged view checks it. Unless some input has a multi-document block and some input lacks the parent field (independent checks,
IndexWriter.java:5548-5555),mergeMiddlebuilds the merged view even when no reorderer is configured (:5557). For two or more inputs, its constructor callsgetMergedFieldInfos(SlowCompositeCodecReaderWrapper.java:96), andgetAndValidateParentFieldthrows when one segment says_parentand another saysnull, also for a segment with no live documents (FieldInfos.java:209,:220-238). The soft-deletes field in the same method toleratesnull(:203-208). - Tragic. The merge exception closes IndexWriter (
tragicEvent(t, "merge"),IndexWriter.java:5112), and uncommitted documents are lost. The bad segment is still in the last commit, so the next writer fails the same way.UnifiedHighlighter(UnifiedHighlighter.java:715-730) andMatchRegionRetriever(:479) callgetMergedFieldInfoson the top-level reader, so they fail on readers that hold the segment next to a segment with the parent field (a one-segment reader is returned unchecked,FieldInfos.java:200-201). - Escape hatches. A singleton merge never builds the view (
SlowCompositeCodecReaderWrapper.java:49-57), soforceMergeDeletes()or a delete-reclaiming merge of the empty segment alone removes it. From 9.11.0 on (#13128), a merge that includes a multi-document-block segment skips the view and succeeds.
A second trigger, found while writing this doc, comes from caller misuse: a single document that contains the reserved field name ("_parent" is a reserved field, IndexingChain.java:617-620, rejected on purpose) is rejected before the parent field is initialized (:641) and leaves a 1-document segment with no fields at all; forceMerge(1) then fails the same way on main and 10.5.2. It carries less weight than the oversized term.
| scenario | expected | got |
|---|---|---|
retention policy (match-all over LogDocMergePolicy), segments [1 doc, 1 live, _parent] [2 docs, 0 live, no parent field] [1 doc, 1 live, _parent], forceMerge(1) | 1 segment, 2 docs | IllegalStateException, writer closed (tragic) |
the same with a plain keepFullyDeletedSegment() == true policy (no document soft-deleted; the independent reproducer also fails with no soft-deletes field) | 1 segment, 2 docs | same |
new IndexWriter with TieredMergePolicy on the committed index, forceMerge(1) | 1 segment, 2 docs | same |
FieldInfos.getMergedFieldInfos(DirectoryReader.open(dir)) | parent field _parent | IllegalStateException |
| controls: default merge policy, no rejection, no parent field, accepted 2-document blocks (independent reproducer) | merges work | merges work |
Exposure and likelihood341 words
- Who. Opt-in on two counts: a parent field set without an index sort, and a merge policy that keeps fully deleted segments, typically
SoftDeletesRetentionMergePolicywhose retention query matches the rejected documents (match-all, or a range that covers them), or any policy whosekeepFullyDeletedSegmentreturns true. The default configuration has neither. - Trigger. A block rejected at a child by a documented non-aborting exception: an oversized term, or a doc-values type conflict in the child (both executed); also, by caller misuse, a single document carrying the reserved parent field name (executed). It must be the only content of a fresh in-memory segment, for example the first thing indexed after a commit or refresh; a rejected block that shares its segment with an accepted document does no harm. A randomized stress run hit it without targeting in 12 of 3,200 rounds.
- Writes. Each merge that pairs the empty segment only with segments built from single documents throws and closes IndexWriter: in a probe, another client's 3 acknowledged documents were lost on each of two failures. Natural merges hit it (with
TieredMergePolicydefaults the first natural merge after the rejection failed), and so does the merge-on-refresh of an NRT reopen. It repeats with each new writer, even with a plainTieredMergePolicy, untilforceMergeDeletes(), or a merge with a multi-document-block segment, removes it.TieredMergePolicymay also reclaim the segment on its own when deletes are a large share of the index. - Reads. On a reader with two or more segments that holds this one (a one-segment reader is not checked),
FieldInfos.getMergedFieldInfosthrows, so everyUnifiedHighlighterrequest with the default offset source fails; search itself works. This also happens with an index sort. ASoftDeletesDirectoryReaderWrapperview hides the empty segment and highlighting works. - Not affected. Committed data stays intact and
CheckIndexreports no problems. With an index sort, merges skip the merged view and succeed, which removes the segment. - Scale and likelihood. No scale needed (the repro submits 5 documents and writes 3 segments); rare in practice because all the preconditions must line up.
The test shows that with setParentField and SoftDeletesRetentionMergePolicy, a block rejected at its child (term > MAX_TERM_LENGTH) right after a commit leaves a fully deleted segment without the parent field that breaks forceMerge (TestRejectedBlockSegmentWithoutParentField#testForceMergeAfterBlockRejectedAtChild).
/*
* 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.util.List;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestRejectedBlockSegmentWithoutParentField extends LuceneTestCase {
public void testForceMergeAfterBlockRejectedAtChild() throws Exception {
IndexWriterConfig iwc =
new IndexWriterConfig()
.setParentField("_parent")
.setSoftDeletesField("_soft")
.setMergePolicy(
new SoftDeletesRetentionMergePolicy(
"_soft", MatchAllDocsQuery::new, new LogDocMergePolicy()))
.setMergeScheduler(new SerialMergeScheduler());
try (Directory dir = new ByteBuffersDirectory();
IndexWriter w = new IndexWriter(dir, iwc)) {
w.addDocument(new Document());
w.commit();
Document badChild = new Document();
badChild.add(
new StringField("f", "x".repeat(IndexWriter.MAX_TERM_LENGTH + 1), Field.Store.NO));
expectThrows(
IllegalArgumentException.class, () -> w.addDocuments(List.of(badChild, new Document())));
try {
w.forceMerge(1);
} catch (Throwable t) {
// Expected: forceMerge succeeds (the block was rejected, non-aborting); actual: it throws.
throw new AssertionError("forceMerge(1) after a block rejected at a child threw " + t, t);
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-563/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.index.TestRejectedBlockSegmentWithoutParentField
- Expected vs actual
- Expected: forceMerge(1) succeeds. Actual: IllegalStateException "expected parent doc field to be "_parent " across all segments but found a segment with different field "null""; fails with -ea and -da.
More files, environment, full output84 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: forceMerge(1) after a block rejected at a child threw java.lang.IllegalStateException: expected parent doc field to be "_parent " across all segments but found a segment with different field "null"
org.apache.lucene.index.TestRejectedBlockSegmentWithoutParentField.testForceMergeAfterBlockRejectedAtChild(TestRejectedBlockSegmentWithoutParentField.java:52)
Caused by: java.lang.IllegalStateException: expected parent doc field to be "_parent " across all segments but found a segment with different field "null"
org.apache.lucene.index.FieldInfos.getAndValidateParentField(FieldInfos.java:226)
org.apache.lucene.index.FieldInfos.getMergedFieldInfos(FieldInfos.java:209)
org.apache.lucene.index.SlowCompositeCodecReaderWrapper.<init>(SlowCompositeCodecReaderWrapper.java:96)
org.apache.lucene.index.SlowCompositeCodecReaderWrapper.wrap(SlowCompositeCodecReaderWrapper.java:56)
org.apache.lucene.index.IndexWriter.mergeMiddle(IndexWriter.java:5557)
[...]- IndexWriter documents the rejection: a term over
MAX_TERM_LENGTHthrowsIllegalArgumentException, and after an exception "the index will be consistent". keepFullyDeletedSegmentis a public hook; Lucene's ownTestIndexWriterExceptionskeeps segments holding only failed documents.- When #12829 added the check, its author agreed in review that users should never be able to trigger it.
The full argument355 words
- Contracts.
IndexWriter#addDocument: "if an Exception is hit (for example disk full) then the index will be consistent, but this document may not have been added" and "each term in the document can be no longer than MAX_TERM_LENGTH in bytes, otherwise an IllegalArgumentException will be thrown" (IndexWriter.java:1465-1466,:1480-1481).addDocuments: "Atomically adds a block of documents ... such that an external reader will see all or none of the documents" (:1496-1497); that promise itself is kept, since the block's indexed documents are deleted and no reader sees them; what breaks is "the index will be consistent", because the leftover segment later makes merges fail.MergePolicy#keepFullyDeletedSegment: a segment for which it returns true "should be kept even if it's fully deleted", useful "if the merge policy implements retention policies for soft deletes" (MergePolicy.java:761-763). Nothing documents that a rejected document can make merges fail. - Tests.
TestIndexWriterExceptionskeeps fully deleted segments on purpose: "we also need to keep fully deleted segments since otherwise we clean up fully deleted ones and if we flush the one that has only the failed document the docFreq checks will be off below" (TestIndexWriterExceptions.java:806-811).TestSoftDeletesRetentionMergePolicyasserts that the policy keeps fully soft-deleted segments whose documents match (:80-97). No upstream test combines a parent field, a rejected document and a kept fully deleted segment. - Intent of the check. In the #12829 review, a reviewer (@mikemccand) asked of
getAndValidateParentField: "As far as we know, users should never be able to do anything that would trigger this? This means we have some bug that allowed broken schema into the index?", and the author (@s1monw) answered "yeah I think so too". #13016 requires the parent field to be configured before any document is added, "to prevent documents without the parent field from being indexed". - Precedent. CHANGES 9.11.0: "GITHUB#13369: Fix NRT opening failure when soft deletes are enabled and the document fails to index before a point field is written" (
CHANGES.txt:2911-2912): the same class (a non-aborting failure plus soft deletes leaving field infos that do not match the segment), fixed as a bug.
The devil's-advocate check248 words
Strongest case for "not a Lucene bug": the retention policy, or the custom policy, chose to keep a segment whose documents are all hard-deleted, so the policy should be fixed (the sibling registry row LFV-228 shows the same "retention ignores hard deletes" pattern in another method of SoftDeletesRetentionMergePolicy).
Why it fails: a plain keepFullyDeletedSegment() == true policy fails the same way with no document soft-deleted, and the independent reproducer and the devil's-advocate verifier reproduce it with no soft-deletes field at all; once the segment is committed, a writer with the default TieredMergePolicy fails too; multi-segment readers fail regardless of the merge policy; and the inconsistent segment is written by IndexWriter itself, from a rejection it documents as leaving the index consistent. The other readings fail as well: keepFullyDeletedSegment "is for testing" (the stock SoftDeletesRetentionMergePolicy implements it and core tests rely on it); "a parent field without an index sort is optional" (it is legal, and the reader-side failure happens with an index sort too); "huge tokens are the application's job" (that is the security threat model, and the writer rejects the term as documented). The devil's-advocate verifier and the cross-model refute review rated the intended-behaviour case weak. A later cross-model fact check of this doc rated it per path: weak for the retention policy, moderate for a custom keepFullyDeletedSegment() == true policy and for the reader-side check (a deliberate schema guard), and strong for the reserved-name trigger, which is misuse. Verdict: a defect for the oversized-term trigger.
Recommended A: in getAndValidateParentField, skip a segment with no parent field and no live documents
Let FieldInfos.getAndValidateParentField skip a segment with no parent field and no live documents; as the check runs at merge and read time, existing indexes should work too. Repros pass with assertions on and off; with assertions on, so do 660 tests in 21 nearby upstream classes. Registering the field in every segment also works, but not for old indexes.
String field = ctx.reader().getFieldInfos().getParentField();
if (field == null && ctx.reader().numDocs() == 0) {
// a segment without live documents (for example one that holds only documents of a
// rejected block, which never got the parent field) cannot contribute any document
continue;
}All options and trade-offs4 options ยท 917 words
Two fixes were applied in a scratch worktree at a9779ef3c193 and run through gradle (seed DEADBEEF) with the paste-ready test and the 21 closest upstream classes in core, misc, highlighter and backward-codecs; both were also shadow-run with the standalone runner, with the single-document probe. Either makes every repro pass. Which one, or both, is the maintainers' call: a maintainer may well prefer D, or A + D (prevention plus tolerance), which was not applied or tested as one patch.
A: in getAndValidateParentField, skip a segment with no parent field and no live documents (validated by the checks) (recommended)
The finder's sketch, +5 lines in FieldInfos.java. Such a segment contributes no document to a merge or a reader, so it cannot break block structure.
String field = ctx.reader().getFieldInfos().getParentField();
if (field == null && ctx.reader().numDocs() == 0) {
// a segment without live documents (for example one that holds only documents of a
// rejected block, which never got the parent field) cannot contribute any document
continue;
}Trade-offs
- Fixes merges, the reader-side
getMergedFieldInfos/UnifiedHighlighterfailure and the single-document trigger. Because the check runs at merge and read time, it should also let a fixed release open and merge indexes that already hold such a segment: the patched writer still writes the empty segment, and a new writer then merges it fine (testNewWriterOnCommittedIndex). No index written by an old release (9.10.0 to 10.5.2) was opened under the patch, and A does not rewrite the index. - A segment with live documents and no parent field still fails, so two related paths that end in the same check from other causes (
addIndexes(Directory)of an index without a parent field into a writer with one; reopening a field-less index with a parent field) are not covered; they were seen once by the devil's-advocate verifier and are not part of this report. - It adds a
numDocs()call per segment:SegmentReaderreturns a stored count (SegmentReader.java:234-237), butIndexReader.numDocs()"may run in O(maxDoc)" for other implementations (IndexReader.java:295-296), andgetMergedFieldInfosaccepts any reader. The check then depends on live-document counts while the empty segment's other field metadata is still merged; no targeted test covers custom leaf readers or unusualOneMerge.wrapForMergewrappers. The empty segment stays until some merge drops it.
What the checks validated
- Stock: the paste-ready test fails 4 of 4; with the independent reproducer's classes 13 tests, 9 failed (assertions on and off).
- With A: 13 of 13 pass with assertions on and off. With assertions on, the 21 closest upstream classes (
TestFieldInfos,TestSoftDeletesRetentionMergePolicy,TestIndexWriterExceptions,TestIndexWriterExceptions2,TestAddIndexes,TestIndexSorting,TestIndexWriter,TestIndexWriterMergePolicy,TestIndexWriterForceMerge,TestSoftDeletesDirectoryReaderWrapper,TestColumnBatchIndexing,TestIndexOptions,TestIndexWriterDelete,TestFilterMergePolicy,TestMergePolicy,TestTieredMergePolicy,TestLogMergePolicy; miscTestBPReorderingMergePolicy,TestBPIndexReorderer; highlighterTestUnifiedHighlighter; backward-codecsTestIndexSortBackwardsCompatibility) pass: 660 tests, 18 skipped, 0 failures (673 tests in 24 classes counting the 3 repro classes).checkGoogleJavaFormatpasses. - With assertions off, a later fact check ran the paste-ready test with
TestFieldInfos,TestSoftDeletesRetentionMergePolicyandTestIndexWriterExceptionsunder A: 64 tests, 3 skipped, 0 failures. The single-document probe passes with A shadowed. The full:lucene:core:testsuite was not run.
+10/-5 in IndexingChain.processDocument: initialize the parent field's FieldInfo with the segment's first document, before its own fields, as the column-batch path already does (IndexingChain.java:824-833), instead of queueing it only for a block's last document.
if (parentPf != null) {
if (parentPf.fieldInfo == null) {
parentPf.schema.resetJustDocId(docID);
initializeFieldInfo(parentPf);
parentPf.trySetValidatedFrozenFieldType();
}
if (lastDocInBlock) {
parentPf.schema.resetJustDocId(docID);
}
}Trade-offs
- Prevents new bad segments for both triggers and follows #13016's intent that no segment lacks the parent field; it keeps the per-segment schema invariant and does not relax
FieldInfos, which a maintainer may prefer. - Does not help an index that already holds such a segment: the check is unchanged (code reading).
- It creates the parent FieldInfo, with numeric doc values and a
NumericDocValuesWriter(IndexingChain.java:1372,:1423), in a segment that may never get a parent value (all its documents rejected). The suites pass, but no focusedCheckIndexor assertion test covers such a segment. - With an index sort, block-aware sorting is used only when the segment has blocks (
IndexingChain.java:261), so an all-rejected segment's empty parent field is not read there (code reading); the index-sort classes run wereTestIndexSortingandTestIndexSortBackwardsCompatibility.
What the checks validated
- The paste-ready test passes 4 of 4 with assertions on and off; with assertions on, the same 21 upstream classes pass: 660 tests, 18 skipped, 0 failures (664 tests in 22 classes with the repro class);
checkGoogleJavaFormatpasses; the single-document probe passes with D shadowed. - The independent reproducer's classes were left out: they assert the bug's precondition (a segment without the parent field), so they fail by design once that segment is no longer written.
Also covers the two related paths above.
Trade-offs
It would let segments whose live documents carry no parent marker merge into a segment that has the field, which #13016 set out to prevent ("documents without the parent field ... later to be treated as child documents upon merge").
Fixes merges for writers without a reorderer.
Trade-offs
Leaves the reader-side failure, and OneMerge.reorder (MergePolicy.java:325) has no way to say "not needed" without an API change.
Elasticsearch, OpenSearch and Solr set the parent field only together with an index sort (main branches, code reading), so their merges skip this check; a reader that still holds such a segment would fail FieldInfos.getMergedFieldInfos either way, which was shown in Lucene only.
A randomized stress test of IndexWriter with a sequence-number replay oracle (soft deletes, parent-field blocks, rejected blocks, merges) saw this exception close the writer in 12 of 3,200 rounds, 9 of them with no injected fault. Code reading found the mechanism, reduced to a deterministic JUnit. Not a formal method.
The method in full162 words
Method: randomized stress testing with a replay oracle, then code reading (non-formal tier).
- The hunt unit drove IndexWriter's public API with random operations and one injected fault per round, and checked every acknowledged operation against a replay by sequence number. Stage 2 added soft deletes with
SoftDeletesRetentionMergePolicy, parent-field document blocks, rejected blocks, doc-values updates and merges on full flush (8 seeds x 400 rounds). - 12 of 3,200 rounds ended with an unexpected tragic close carrying this
IllegalStateException; in 9 of them the injected fault never fired, so the failure came from ordinary API calls. - The finder traced it by reading the parent-field registration and the merge path, and wrote a deterministic 3-test public-API JUnit; this doc's 4-test version narrows the claims and adds the reader-side test.
- Found 2026-10-26 on main
a9779ef3c193; then checked by a cross-model review (reading only), an independent reproducer, a refute review, a devil's-advocate verifier, an adjudication, a follow-up (history, prior reports, security) and an audit.
No prior report found (2026-10-26, 51 tool queries and 38 manual searches over GitHub, the mailing lists, the JIRA archive, downstream projects and the web; the changed files of all 406 open PRs), re-checked 2026-10-27. Closest: #13353/#13369, the same class of bug for a point field, fixed in 9.11.0.
Searches5 searches
Verdict new. The error text has 0 hits everywhere searched. The nearest items are the same class of bug or the changes that built and narrowed this one.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits; Elasticsearch, OpenSearch, Solr; all of GitHub | 2026-10-26 | the hunt's prior-report tool (16 issue/PR, 3 commit, 2 downstream-repository queries) and 38 manual GitHub searches, including both fragments of the error message | no report; related: #13353/#13369 (same class, point field, fixed 9.11.0), #13340/#13341 (missing parent FieldInfo in an empty index, fixed at writer open), #12829, #13016, #13128, #15960 |
| dev@, java-user@, issues@, commits@; JIRA archive; web | 2026-10-26 | 20 list queries, 4 JIRA-archive queries, 6 web queries; 1,607 distinct hits in total, 0 high, all 15 medium read | no report |
| Open apache/lucene PRs | 2026-10-26 | changed files of all 406 open PRs; diffs of the 11 closest grepped for the code involved | none changes the code involved; #13536 and #16571 touch SoftDeletesRetentionMergePolicy / IndexWriter only |
| apache/lucene (re-check) | 2026-10-27 | issue search for the error text, PR search for getAndValidateParentField, upstream main head | 0 hits; main head then a95c754ee003, not fixed; a listing of PRs opened since 2026-10-25 hit the rate limit |
| apache/lucene (fact check of this doc) | 2026-10-27 | the issues and PRs created 2026-10-24 to 27, and the commits on main after a9779ef3c193 | none of the 17 issues and PRs is about this bug; main at f7bcc0abd816, 3 commits later, none touching the code involved: not fixed |
Introduced by #12829 (df6bd25ce44e, merged 2024-01-11, first released in 9.10.0), which added both the last-document-only parent marker and the cross-segment check. The repro fails on all 19 releases from 9.10.0 to 10.5.2 and on main; on 9.10.0 more merges fail; 9.9.2 has no setParentField.
Versions tested6 rows
Introduced by #12829 "Add support for index sorting with document blocks" (@s1monw, merged into main on 2024-01-11 as df6bd25ce44e; branch_9x backport 0aa88910ca9, first released in 9.10.0). It added the parent marker on a block's last document and the check in getMergedFieldInfos at the same time (blame on main; git log -S getAndValidateParentField finds only this commit). The preconditions were older and harmless before: the merged view in mergeMiddle without an index sort (#12622, 9.9.0) and keepFullyDeletedSegment with SoftDeletesRetentionMergePolicy (LUCENE-8233, 7.4.0). Later, #13128 (9.11.0) added the guard that lets merges with a multi-document-block segment succeed, and #15960 (10.5.0) moved the last-document-only logic into IndexingChain without changing behaviour. Not bisected commit by commit, because the repro needs the API that #12829 itself added. Upstream main f7bcc0abd816 (2026-10-27) does not fix it.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | paste-ready test 4 of 4 fail (gradle and runner, assertions on and off, two seeds); finder's test 3 of 3; independent reproducer 5 of 5 |
| 10.5.2, 9.12.3 | affected | paste-ready test 4 of 4 fail (assertions off); finder's and reproducer's tests fail |
| 9.11.0 to 10.5.1 (all releases between) | affected | finder's test 3 of 3 fail on each (history pass) |
| 9.10.0 | wider | paste-ready test 4 of 4 fail; merges that include a multi-document-block segment fail too (before #13128) |
| 9.9.2 | no API | no setParentField: the repro does not compile; the no-parent-field control passes |
| fix A / fix D (scratch worktree) | not affected | all repros pass; with assertions on, 660 tests in the 21 closest upstream classes pass with each |
An independent reproducer and a devil's-advocate verifier reproduced it with their own tests; reviews by a model from another family (reading only) agreed on the root cause; an adjudication confirmed it with high confidence and an audit kept it. This doc's own runs re-ran the repro and validated two fixes.
Checks and verdicts378 words
Each executing check wrote or re-ran its own JUnit on the stock code; every control passed.
| Check | Verdict | What it found |
|---|---|---|
| cross-model review | real (medium) | An independent reviewer from another model family (OpenAI GPT-5.6) confirmed the mechanism from the code; its only reservation, the tragic close, is shown by the executed repro. |
| independent reproducer | real | Own 5-variant JUnit fails 5 of 5 on main (assertions on and off), 10.5.2, 9.12.3 and 9.10.0; 4 controls pass. Narrowed the scope: block segments protect from 9.11 on; forceMergeDeletes() recovers; natural merges hit it. |
| refute review | real (high); intended-behaviour case weak | The same model family, reading only; a separate run re-ran the finder's test with another seed (3 of 3 fail). Its probe showed that a plain TieredMergePolicy reopen still fails, an NRT reopen throws, and a shared segment is harmless. |
| devil's advocate | real | No intended reading survives. Added the doc-values-conflict trigger, the clean CheckIndex, the reader-side failure and the natural-merge geometry; raised two related leads. |
| adjudication | confirmed (high) | Re-ran the reproducer's tests with a new seed (5 of 5 fail, assertions on and off) and its controls (4 of 4 pass). |
| security screen | not security-sensitive | Executed probe: an NRT reopen after one rejected block loses another client's 3 acknowledged documents; forceMergeDeletes() recovers; with an index sort merges succeed. Out of scope under SECURITY.md (re-evaluated 2026-10-27). |
| audit and registration check | keep | Fresh seeds: the finder's test fails 3 of 3 on main (assertions on and off) and 10.5.2; the reproducer's 5 of 5 fail on main (assertions off) and pass with fix A shadowed. |
| this doc | reproduced; two fixes validated | The paste-ready test fails 4 of 4 through gradle (assertions on and off) and on 10.5.2, 9.12.3 and 9.10.0; found the single-document trigger; fixes A and D pass with the closest classes. |
| fact checks of this doc (two) | the bug holds; wording narrowed | One re-ran the repro through gradle (assertions on and off), on 10.5.2 and 9.10.0 and over 25 seeds (100 of 100 fail), and both fixes; an independent reviewer from another model family (OpenAI GPT-5.6, reading only) traced the main example by hand. Both found only wording and count errors, fixed in this version. |
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,409 words
- Registry title
- A document block rejected at a child can be flushed as a fully deleted segment without the parent field; if the merge policy keeps it (SoftDeletesRetentionMergePolicy can), merges that combine it only with segments without multi-document blocks throw 'expected parent doc field ... across all segments' and close IndexWriter, again after every reopen
- Area
- index / IndexWriter document blocks (parent field) and merges
- Priority
- Medium, rank 160: A child-rejected block can leave a parent-field-less fully deleted segment that tragically closes IndexWriter on merges, the same loud index-unusable opt-in profile as LFV-417, so it follows that row.
- Impact class
- index unusable
- Impact note
- Hits only applications that set IndexWriterConfig#setParentField without an index sort and use a merge policy that keeps fully deleted segments (SoftDeletesRetentionMergePolicy with a retention query that matches the rejected documents, or any keepFullyDeletedSegment that returns true), after IndexWriter rejects a block at a child (a term longer than MAX_TERM_LENGTH, or a doc-values type conflict) while that block is the only content of its in-memory segment: each later merge, natural or forced, that combines the empty segment only with segments holding no multi-document block then closes IndexWriter with a tragic exception and loses the uncommitted documents, again after every reopen, until IndexWriter.forceMergeDeletes() (or a merge that includes a multi-document block segment) removes it; the committed data stays intact and CheckIndex calls the index clean, with an index sort the merges succeed, but on any reader that still holds the segment FieldInfos.getMergedFieldInfos and UnifiedHighlighter with its default offset source throw; ordinary scale, released 9.10.0-10.5.2 and main.
- Summary
- You set IndexWriterConfig#setParentField (no index sort) and a merge policy that keeps fully deleted segments, such as SoftDeletesRetentionMergePolicy, and IndexWriter rejects a document block at a child with a documented non-aborting exception (for example a term longer than MAX_TERM_LENGTH) while that block is the only content of its in-memory segment, for example the first thing indexed after a commit. You expect the rejection to leave a consistent index that keeps merging, as the addDocument javadoc promises ('if an Exception is hit ... the index will be consistent'). Instead the block is flushed as a fully deleted segment whose FieldInfos have no parent field (IndexWriter registers the parent field only when it reaches a block's last document), the merge policy keeps it, and every merge that combines it only with segments holding no multi-document block throws IllegalStateException 'expected parent doc field to be "_parent " across all segments but found a segment with different field "null"' and closes IndexWriter, also after the index is reopened, until forceMergeDeletes removes the segment.
- Introduced
- df6bd25ce44ead0b38cffca6ef9bc3a9ce63dbea (#12829 'Add support for index sorting with document blocks', author s1monw, main); its branch_9x backport 0aa88910ca9 shipped first, in 9.10.0 [read: blame on main puts FieldInfos.getAndValidateParentField and its call in getMergedFieldInfos on df6bd25ce44e, git log -S getAndValidateParentField finds only that commit, and #12829 also added the last-document-only parent registration ('if (iterator.hasNext() == false) doc = addParentField(doc, parentField)' in DocumentsWriterPerThread); exec at release granularity: 9.9.2 has no setParentField, so the repro does not compile there while the no-parent-field control passes, and 9.10.0 fails]; #12829 added both halves at once; the preconditions were already there and harmless before it: the merged view that IndexWriter.mergeMiddle builds when no index sort is configured (f7cab164501, #12622, 9.9.0) and MergePolicy#keepFullyDeletedSegment / SoftDeletesRetentionMergePolicy (ecc17f90233, LUCENE-8233, 7.4.0); later 4a3f4cfe974 (#13128, 9.11.0) narrowed it (a merge that includes a multi-document block segment skips the merged view) and 34c9495241d (#15960, 10.5.0) moved the last-document-only registration into IndexingChain without changing behaviour (10.4.0 and 10.5.0 give the same results [exec]); not bisected commit by commit, because the repro needs the API that #12829 itself added (2024-01-11 (#12829 merged into main as df6bd25ce44, 15:11 UTC))
- First affected version
- 9.10.0
- Affected versions
- all 19 releases from 9.10.0 through 10.5.2 (9.10.0, 9.11.0, 9.11.1, 9.12.0-9.12.3, 10.0.0, 10.1.0, 10.2.0-10.2.2, 10.3.0-10.3.2, 10.4.0, 10.5.0-10.5.2) and main a9779ef3c193 (unreleased 11.0.0) [exec: the finder's JUnit fails 3 of 3 on every one of them with -ea, and on main also with -da; the independent reproduction's JUnit fails 5 of 5 on main -ea/-da, 10.5.2, 9.12.3 and 9.10.0]; the scope is wider on 9.10.0, where merges that include a multi-document block segment fail too; from 9.11.0 on (#13128) such merges succeed [exec: history probe and the independent reproduction control]; 9.9.2 and earlier are not affected (no IndexWriterConfig#setParentField; the repro does not compile on 9.9.2) [exec]; the previous hunt main ea57360912ed is affected [read: none of the 7 commits from it to a9779ef3c193 touch the parent-field, merged-view or field-info validation code]; not fixed on upstream main a95c754ee003 (2026-10-26) [read]; aarch64 with JDK 25 only (no platform-specific code involved)
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal baseline: stress; also reached by code reading
- Upstream and prior reports
- none found (2026-10-26, the prior-report record, read-only: a scripted prior-report search with 51 queries (16 GitHub issue/PR, 3 commit, 2 downstream-repo, 20 mailing-list (dev@, java-user@, issues@, commits@), 4 JIRA-archive, 6 web), 1,607 distinct hits, 0 high, all 15 medium read; 38 manual GitHub searches on apache/lucene, three downstream repositories and all of GitHub; the changed files of all 406 open apache/lucene PRs, with the diffs of the 11 closest grepped for the code involved; git history; the exact error text has 0 hits everywhere; earlier intended-behaviour check GitHub searches also found nothing); related prior items, none the same bug: #13353 and its fix #13369 (closed, 9.11.0: same bug class, a non-aborting document failure with soft deletes leaving a segment whose FieldInfos do not match its files, for an eagerly registered point field; different field, check and fix site), #13340/#13341 (a missing parent FieldInfo in an empty index, fixed at writer open only), #12829 (introduced both halves), #13016 (states that no segment should lack the parent field), #13128 (narrows the trigger), #15960 (moved the registration), and the open PRs #13536 and #16571 (touch SoftDeletesRetentionMergePolicy or IndexWriter but not the code involved)
- Lucene commit tested
- main
a9779ef3c193(2026-10-26): the paste-ready JUnit through gradle in a scratch worktree, stock and with two fixes, with assertions on and off, and with assertions on together with the 21 closest upstream test classes in core, misc, highlighter and backward-codecs (2026-10-27); runs with the standalone runner on main (two seeds), 10.5.2, 9.12.3 and 9.10.0; the finder's JUnit on all 19 releases 9.10.0 to 10.5.2 (history pass); checks by an independent reproducer, a devil's-advocate verifier, a cross-model reviewer, an adjudication, an audit and the registration check. OpenJDK 25.0.3, Linux aarch64.
- Finding record
lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:601-608 and :644-652 (the parent field's PerField is initialized and indexed only when lastDocInBlock, :604 and :645)lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:245-310 (updateDocuments; on a non-aborting exception the block's documents are marked deleted, deleteLastDocs at :306) and :399 (deleteLastDocs)lucene/core/src/java/org/apache/lucene/index/FieldInfos.java:196-238 (getMergedFieldInfos calls getAndValidateParentField at :209; the IllegalStateException thrown at :226 when one leaf has the parent field and another has null; the soft-deletes field in the same method is null-tolerant)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:1465 and :1480-1481 (addDocument: 'if an Exception is hit ... the index will be consistent'; a term longer than MAX_TERM_LENGTH throws IllegalArgumentException), :1496 (addDocuments: 'Atomically adds a block'), :5548-5557 (mergeMiddle: hasBlocksButNoParentField guard and SlowCompositeCodecReaderWrapper.wrap), :3551-3561 (addIndexesReaderMerge, same pattern), :5112 (tragicEvent(t, "merge"))lucene/core/src/java/org/apache/lucene/index/SoftDeletesRetentionMergePolicy.java:80-93 (keepFullyDeletedSegment runs the retention query over FilterCodecReader.wrapLiveDocs(reader, null, reader.maxDoc()), i.e. with hard deletes ignored)lucene/core/src/java/org/apache/lucene/index/MergePolicy.java:761-767 (keepFullyDeletedSegment javadoc: 'should be kept even if it's fully deleted')lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:579-591 (setParentField javadoc: an internal field added to every root document)lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java:726 and lucene/highlighter/src/java/org/apache/lucene/search/matchhighlight/MatchRegionRetriever.java:479 (both call FieldInfos.getMergedFieldInfos on the top-level reader)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:583-652 (processDocument; parent field at :604-608 and :645-652; reserved-name check :617-620; initAndValidateFields call :641)lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:245-310, 399-410 (updateDocuments; setHasBlocks :297-298; deleteLastDocs :303-306)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:1496-1497, 5548-5558, 5112, 6429-6432lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:579-591; MergePolicy.java:761-767; SoftDeletesRetentionMergePolicy.java:80-93lucene/core/src/java/org/apache/lucene/index/SlowCompositeCodecReaderWrapper.java:49-57, 96; FieldInfos.java:196-238lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java:715-730; lucene/highlighter/src/java/org/apache/lucene/search/matchhighlight/MatchRegionRetriever.java:479
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:1465-1466, 1480-1481, 1496-1497lucene/core/src/java/org/apache/lucene/index/MergePolicy.java:761-767; IndexWriterConfig.java:579-591lucene/core/src/test/org/apache/lucene/index/TestIndexWriterExceptions.java:806-811; TestSoftDeletesRetentionMergePolicy.java:80-97lucene/CHANGES.txt:2911-2912 (GITHUB#13369, section Lucene 9.11.0)
- Suggested fix
lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:261, 824-833, 1372, 1423; MergePolicy.java:320-328; IndexReader.java:292-298; SegmentReader.java:233-237
- TestRejectedBlockSegmentWithoutParentField.java (JUnit test):
repro/LFV-563/src/org/apache/lucene/index/TestRejectedBlockSegmentWithoutParentField.java(56 lines)
A step-by-step picture made in parallel with this doc from a probe run on main a9779ef3c193.
assets/LFV-563/bug.png(498 KB)
