Lucene correctly rejects a document whose field f has doc values that f was never indexed with. But the per-field record it uses for that check is not fully cleared afterwards: the skip index type of the rejected document stays behind and makes the next valid documents with f look inconsistent.
- Per-field consistency. Since 9.0, a field must have the same data structures in every document: if the first indexed document has
fwithout doc values, a later document that adds doc values tofis rejected withIllegalArgumentException, and only that document is skipped. - The per-document record. To check that,
IndexingChainbuilds a small record per field of what the current document contains (index options, doc-values type, points, vectors), compares it with the field'sFieldInfo, and clears it before the next document. - Doc-values skip index. Since 10.0, doc values can carry a skip index (
DocValuesSkipIndexType.RANGE), for example throughSortedDocValuesField.indexedField. A skip index without doc values is not allowed.
f pass through the per-field record; the reset before doc 2 clears the doc-values type but not the skip index type, so doc 2 is rejected. With the fix, both are cleared and doc 2 is accepted. Select the still to open it at full size. Generated with AI, checked against the code.- You add doc 0 with
StringField("f", "a"). It is accepted, sofis a postings-only field in the in-memory segment. - Doc 1 has
StringField("f", "b")plusSortedDocValuesField.indexedField("f", "b"), doc values with a skip index. It is rightly rejected:doc values type: expected 'NONE', but it has 'SORTED'. - You expect doc 2,
StringField("f", "c"), exactly like doc 0, to be accepted. Instead it is rejected:doc values skip index type: expected 'NONE', but it has 'RANGE'. - Why: before each document Lucene clears a per-field record of what that document contains. The clearing step resets the doc-values type to
NONEbut forgets the skip index type, so doc 1'sRANGEis left behind. - Every later document with
fand no doc values fails the same way (20,000 of 20,000 in a probe) until a flush, NRT reopen or commit. One added line in the reset fixes it.
Applications on 10.0+ that use the opt-in doc-values skip index on a field that is not always sent with doc values, for example mixed document producers or a schema change. After one rejected document, valid documents with that field fail until the next flush; applications that drop failed documents lose them. Default config, any codec, any scale.
Mechanism, terms and numbers832 words
IndexWriter checks that a field has the same data structures in every document. For that, IndexingChain keeps a per-field record of what the current document contains (FieldSchema), which its javadoc says is reset with every new document. Its reset() resets every member it compares except the doc-values skip index type added in 10.0. So when a document that carries a skip-indexed doc value for field f (SortedDocValuesField.indexedField and the SortedSet, Numeric and SortedNumeric variants, or FieldType#setDocValuesSkipIndexType) is rejected, the record keeps RANGE while its doc-values type goes back to NONE. Every later document that has f without doc values then fails in the same in-memory segment, with doc values skip index type: expected 'NONE', but it has 'RANGE' or field 'f' cannot have docValuesSkipIndexType=RANGE with doc values type NONE, until the next flush, NRT reopen or commit. The failure is loud: each affected addDocument throws, the writer stays open and no corruption was seen; applications that drop failed documents lose valid data. Introduced with the skip index itself (#13449), it affects every release from 10.0.0 to 10.6.0 and main; one added line fixes it.
- The record.
IndexingChain.FieldSchema(IndexingChain.java:2209-2360) holdsdocValuesTypeanddocValuesSkipIndex(:2224-2225) among other members. Javadoc (:2210-2215): "With every new document this schema is reset. ... we compare the built schema of the current document with the corresponding FieldInfo ... If there is inconsistency, we raise an error." - The write.
updateDocFieldSchemacallssetDocValues(type, skip)only for a field with doc values (:1564-1565).setDocValues(:2291-2300) stores both values, but only whiledocValuesType == NONE(:2293-2295). - The reset. For each new document,
PerField.reset(:1876-1886) callsschema.reset(docId)unless the field type is the cached frozen type that already validated.reset(int)(:2331-2343) resetsomitNorms,storeTermVector,indexOptions,docValuesType, the three point and the three vector members, and notdocValuesSkipIndex. - The poisoned document. Doc 1 sets (
SORTED,RANGE) and is rejected byassertSameSchema(:2345-2359). Its second instance offhas a different field type, so the frozen-type fast path is dropped (:627-631) and, because validation threw, not re-armed (:697runs only after success). So doc 2 goes throughschema.reset, which leaves (NONE,RANGE). (The fast path exists since 10.5.0; before that every document goes throughreset, with the same result.) - The two failures (
initAndValidateFields,:685-699). Whenfhas aFieldInfoin the segment,assertSameSchemacompares the staleRANGEwith theFieldInfo'sNONE(:2350): "doc values skip index type: expected 'NONE', but it has 'RANGE'". When it has none yet,initializeFieldInfopasses (NONE,RANGE) tonew FieldInfo(:1402-1403), whose constructor always runscheckConsistency()(FieldInfo.java:125) and rejects it (:160-168). Either way the next document takes the same path again. - Why it lasts. A rejected document is non-aborting: it keeps its doc id and is marked deleted, and
deleteLastDocsdoes not trigger a flush (DocumentsWriterPerThread.java:283-307,:399-415). It does not even reach the flush check:DocumentsWriter.updateDocumentscallsflushControl.doAfterDocument(DocumentsWriter.java:434) only afterdwpt.updateDocuments(:427-428) returns. So the window ends when accepted documents trigger a RAM flush, or at an explicit flush, NRT reopen or commit (all three executed); a new in-memory segment starts with new records (DocumentsWriterPerThread.java:193). One more exit, executed in the fact check's probe: a later document that givesfdoc values without a skip index is itself rejected, butsetDocValuesfirst overwrites both members, so the nextStringField fdocument is accepted. A later skip-indexed document leavesRANGEbehind again.
- It persists. After one rejected document, 20,000 of 20,000 later valid documents with
fwere rejected while 20,000 of 20,000 interleaved documents withoutfwere accepted; an NRT reopen ended it, and with a 1 MB RAM buffer and other documents being indexed the first RAM-triggered flush ended it after 2,074 rejections. A second thread on the same writer, run after the first, had 100 of 100 valid documents rejected (consistent with reuse of the same in-memory segment; the probe did not inspect it). With the fix: 0. - Two more trigger paths.
fis new to the segment and the document is rejected because of another field: the first validStringField fthen fails withfield 'f' cannot have docValuesSkipIndexType=RANGE with doc values type NONE.fexists only in an earlier committed segment: the index-wide check rejects the skip-indexed document, and 5 of 5 laterStringField fdocuments fail before the next flush. - All four factories (
Sorted,SortedSet,Numeric,SortedNumericindexedField) withStringFieldorStoredFieldasf's structure fail. Plain doc values without a skip index, points and vectors in the rejected document do not affect the next one.
Exposure and likelihood326 words
- Who. Users of the doc-values skip index (
SortedDocValuesField,SortedSetDocValuesField,NumericDocValuesFieldorSortedNumericDocValuesFieldindexedField, orFieldType#setDocValuesSkipIndexType) on a field name that some documents send with a skip-indexed doc value and others without doc values. One rejected document that carries the skip-indexed doc value is enough: rejected becausefwas established without doc values (in this or an earlier segment), or because another field of that document is invalid. - What it costs. Every later document with
fand no doc values in the same in-memory segment fails withIllegalArgumentExceptionuntil the next flush, NRT reopen or commit (20,000 of 20,000 in the probe, where nothing flushed). Each failed document is deleted, as for any rejected document. Applications that log and drop failed documents lose valid data. - Not affected. The writer stays open; in every exercised path the committed index, searches and other fields were fine; documents without
fare accepted; no invalid document is accepted (by code reading, and none from this root cause in the study's bounded-exhaustive harness over its 11-type palette). - Conditions. Default
IndexWriterConfig; any codec (the rejection comes before any codec call: the test framework's per-seed codecs and, in the independent reproduction's test, the production default codec all fail); assertions on and off; ordinary scale; one thread. A second thread on the same writer was affected too (consistent with reuse of the same in-memory segment); with several concurrent segments, only the affected one should fail (inferred, not executed). - How likely: rare. It needs the opt-in skip index on a field with a mixed per-document schema: a rejected document where
fcarries a skip-indexed doc value, then documents withfand no doc values. An application that always sends the same Lucene fields for a field name never produces that mix: either every document withfwrites both doc-values members again (or, on the 10.5+ frozen-type fast path, keeps the already validated ones), or no document ever writesRANGE.
One JUnit test, one thread, deterministic, public API: add StringField f, then a document that also has SortedDocValuesField.indexedField("f") (it must throw), then StringField f again, which must be accepted.
/*
* 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.Field;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.BytesRef;
public class TestRejectedSkipIndexDoc extends LuceneTestCase {
public void testValidDocAcceptedAfterRejectedSkipIndexDoc() throws Exception {
try (Directory dir = new ByteBuffersDirectory();
IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
w.addDocument(List.of(new StringField("f", "a", Field.Store.NO)));
expectThrows(
IllegalArgumentException.class,
() ->
w.addDocument(
List.of(
new StringField("f", "b", Field.Store.NO),
SortedDocValuesField.indexedField("f", new BytesRef("b")))));
// Expected: accepted (same schema as doc 0); actual: IAE, stale skip index type 'RANGE'.
w.addDocument(List.of(new StringField("f", "c", Field.Store.NO)));
}
}
}
cd $LUCENE && ./gradlew :lucene:core:test --tests TestRejectedSkipIndexDoc -Ptests.seed=DEADBEEF -Ptests.asserts=true
- Expected
- Doc 1 is rejected, and doc 2, with the same structure as doc 0, is accepted.
- Observed
- Doc 2 is rejected too:
IllegalArgumentException: ... doc values skip index type: expected 'NONE', but it has 'RANGE', with assertions on and off.
More files, environment, full output331 words
ByteBuffersDirectory and a plain IndexWriterConfig, so nothing flushes and nothing random affects the result: the test framework picks a codec per seed, but the rejection happens before any codec call. The test sits in org.apache.lucene.index, as Lucene's own IndexWriter tests do, but uses no package-private API. The expectThrows for doc 1 passes, so the correct rejection is unchanged; the third addDocument (line 42) throws, and that exception is exactly the wrong rejection, which is why the test has no extra assertion.
The other trigger paths (the field new to the segment, the field only in a committed segment) and the four indexedField factories share the root cause and were executed by the study's longer tests; they are not part of this repro.
Regression test: this same test; it passes with the fix. Upstream may prefer it as a method in TestPerFieldConsistency or TestIndexWriter.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; with the fix: passes. Also fails on the 10.6.0 release jars |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; the code path is pure Java) |
cp $REPRO/TestRejectedSkipIndexDoc.java $LUCENE/lucene/core/src/test/org/apache/lucene/index/
cd $LUCENE && ./gradlew :lucene:core:test --tests TestRejectedSkipIndexDoc -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false
# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true (stack trimmed; -Ptests.asserts=false identical)
TestRejectedSkipIndexDoc > testValidDocAcceptedAfterRejectedSkipIndexDoc FAILED
java.lang.IllegalArgumentException: Inconsistency of field data structures across documents for field [f] of doc [2]. doc values skip index type: expected 'NONE', but it has 'RANGE'.
at __randomizedtesting.SeedInfo.seed([DEADBEEF:AF01532DE0E3BA9D]:0)
at org.apache.lucene.index.IndexingChain$FieldSchema.raiseNotSame(IndexingChain.java:2259)
at org.apache.lucene.index.IndexingChain$FieldSchema.assertSame(IndexingChain.java:2254)
at org.apache.lucene.index.IndexingChain$FieldSchema.assertSameSchema(IndexingChain.java:2350)
at org.apache.lucene.index.IndexingChain.initAndValidateFields(IndexingChain.java:695)
at org.apache.lucene.index.IndexingChain.processDocument(IndexingChain.java:641)
...
at org.apache.lucene.index.IndexWriter.addDocument(IndexWriter.java:1492)
at org.apache.lucene.index.TestRejectedSkipIndexDoc.testValidDocAcceptedAfterRejectedSkipIndexDoc(TestRejectedSkipIndexDoc.java:42)
# the three documents one by one (a small probe on the same main)
doc 0: accepted
doc 1: rejected: Inconsistency of field data structures across documents for field [f] of doc [1]. doc values type: expected 'NONE', but it has 'SORTED'.
doc 2: rejected: Inconsistency of field data structures across documents for field [f] of doc [2]. doc values skip index type: expected 'NONE', but it has 'RANGE'.- The record's javadoc says it "is reset" with every new document, yet it keeps a rejected document's value.
MIGRATE.mddefines a field's structures by the first document indexed;TestPerFieldConsistencyasserts that a correct document is accepted after rejected ones.reset()clears every other compared member; #13449 added the skip index without a reset line.
The full argument293 words
- The class says so.
FieldSchemais "A schema of the field in the current document. With every new document this schema is reset" (IndexingChain.java:2210). - The documented rule is about indexed documents.
MIGRATE.md:1557-1564: "The per field data-structures are implicitly defined by the first document indexed that contains a certain field." Doc 2 matches the first indexed document exactly; doc 1 was never indexed. The rejection is non-aborting: the writer stays open for the next document. - Upstream tests assert recovery. In its "diff segment, same index" phases,
TestPerFieldConsistencystarts a new in-memory segment (writer.flush()at:159,:207), adds documents that the index-wide check rejects, then, in that same in-memory segment,writer.addDocument(doc0); // add document with correct data structuresand asserts it is indexed (:173,:225; asserts at:176-178,:228-230), the shape of the committed-segment trigger path described above; its random doc-values field never usesindexedField. The skip-index teststestDocValuesMixedSkippingIndexandtestDocValuesSkippingIndexWithoutDocValues(TestIndexWriter.java:4998-5053) check only the rejected document and add nothing after it. - The leftover pair is one Lucene forbids. (
NONE,RANGE) is whatFieldInfo.checkConsistencyrejects (FieldInfo.java:160-168) and whatupdateDocFieldSchemarejects for a single field (IndexingChain.java:1566-1572). - History shows an omission. LUCENE-9334 (
d03662c48bf, 2021) createdFieldSchemaand reset every member it compared; LUCENE-10577 (a693fe819b0, 2022) addedvectorEncodingtoreset()in the same commit; #13449 (048770205cb) added the skip index member, its write, its comparison and its use ininitializeFieldInfo, but no reset line, and #13784 (b59a357e586) kept it that way. Neither PR's review discussesreset(). - Asymmetry. The same three-document sequence with plain doc values (no skip index), points or vectors in doc 1 recovers at once.
The devil's-advocate check152 words
The strongest case: the first document really is invalid, and IndexWriter does not promise to roll back every bit of transient state after a non-aborting exception; the DocumentsWriter javadoc says such exceptions "can still partially update the index structures. These updates are consistent, but, they represent only a part of the document" (DocumentsWriter.java:76-79).
Why it fails: that sentence is about index structures holding part of the failed document, which deleting the document makes invisible. The stale skip index type is not a structure any later document could match: it rejects documents identical to the first indexed one, the pair it leaves is one FieldInfo forbids, and whether a document is accepted then depends on when the last flush happened, which no documented rule describes. The works-as-designed review by an independent reviewer from another model family (OpenAI GPT-5.6), which ran its own tests, rated the argument moderate and the bug real.
Recommended Reset the skip index type in FieldSchema.reset
Add docValuesSkipIndex = DocValuesSkipIndexType.NONE; to IndexingChain.FieldSchema.reset(int), next to docValuesType = DocValuesType.NONE;. The repro passes with assertions on and off, and so do the closest suites and the whole core module.
void reset(int doc) {
resetJustDocId(doc);
omitNorms = false;
storeTermVector = false;
indexOptions = IndexOptions.NONE;
docValuesType = DocValuesType.NONE;
+ docValuesSkipIndex = DocValuesSkipIndexType.NONE;
pointDimensionCount = 0;All options and trade-offs3 options ยท 397 words
Option A was built and run here and by every independent check. B and C were considered and not run.
One added line in IndexingChain.java, after :2336, so that the per-document reset clears both doc-values members that setDocValues writes together.
void reset(int doc) {
resetJustDocId(doc);
omitNorms = false;
storeTermVector = false;
indexOptions = IndexOptions.NONE;
docValuesType = DocValuesType.NONE;
+ docValuesSkipIndex = DocValuesSkipIndexType.NONE;
pointDimensionCount = 0;Trade-offs
- None found.
setDocValuesalready writes the member whenever doc values are present (IndexingChain.java:2293-2295), so documents with doc values, with or without a skip index, are validated exactly as before. - The frozen-type fast path (
resetJustDocId, #15886) is untouched. - Cost: one field store per field per document, on a path that already resets ten others.
What the checks validated
Applied to a scratch worktree of 645d4fe14a20 and run through gradle with OpenJDK 25.0.3, seed DEADBEEF (2026-10-30):
- stock: the minimal test fails with assertions on and off;
- with the fix: the minimal test passes with assertions on and off;
- the closest suites
TestPerFieldConsistency,TestIndexWriter(incl. the two skip-index tests),TestIndexWriterExceptions,TestDocValuesIndexing,TestFieldInfos,TestDocValuesandTestSortedSkipperScorerSupplierpass (213 tests, 0 failures, 4 skipped; assertions on, seed DEADBEEF); - the whole
:lucene:core:testpasses (748 classes, 8,897 tests, 0 failures, 322 skipped; assertions on, seed DEADBEEF), and so doescheckGoogleJavaFormat.
Every independent check also ran this line: the finder's 3 tests, the independent reproduction's 6, the intended-behaviour check's 7 and the TestPerFieldConsistency-pattern variant (5) pass with it; at the introducing commit its boolean form fixes the repro; in the bounded-exhaustive harness it removes all 1,404 wrongly rejected sequences.
Compare and pass docValuesSkipIndex only when docValuesType != NONE, in assertSameSchema and initializeFieldInfo.
Trade-offs
Hides the symptom at two read sites instead of fixing the state; any future reader of the member would see the stale value again. Not run.
Allocate a new FieldSchema per document, so no member can be forgotten.
Trade-offs
Adds an allocation per field per document on the indexing path; and reset() today never clears the attributes map (IndexingChain.java:2331-2343), which initializeFieldInfo passes on (:1405), so a wholesale reset changes more than this bug. Not run.
Search servers that map each field name to one fixed set of Lucene fields send the same types in every document, so by code reading they never leave the stale value behind; only per-document field typing can.
While writing an Alloy model of the indexing chain's per-field schema state, the finder noticed by reading that reset() skips the skip index member. Alloy then produced 3-document counterexamples, a bounded-exhaustive harness on the real jar attributed 1,404 wrongly rejected sequences to it, and a public-API JUnit confirmed it.
The method in full135 words
Method: semi-formal (the hunt's tier: the first signal was code reading while modelling, which a formal tool then witnessed).
- Model. An Alloy 6.2 model of the per-segment
FieldSchema/PerFieldstate machine, checked against a stateless reference of the per-document schema contract. The checksStockLocalNoPoisonandRowOnlyStockNoPoisongave 3-document counterexamples, andStockNoPoisonone at the full 18-schema scope; with the fix,SkipFixLocalNoPoisonis UNSAT (a bounded proof in a small universe). - Harness. A bounded-exhaustive differential harness on the real jar ran 246,848 document sequences over an 11-type palette and found 1,404 sequences where a valid document was rejected; this one-line fix alone removes all of them.
- Test. The counterexample was minimised to a JUnit on the stock jars; the minimal repro above is its first case, cut down to three documents and no extra assertion.
None found: no issue, PR, list thread or downstream report describes later valid documents failing after a rejected skip-indexed document (searched 2026-10-26). Closest: #15761, which shows the same message label in the opposite direction (expected 'RANGE', has 'NONE'), as an existing test's expected output, and #15125, the same defect class in other code.
Searches3 searches
- #15761 (closed): the same message label in the opposite direction ("doc values skip index type: expected 'RANGE', but it has 'NONE'"), as the expected output of the first case of
testDocValuesMixedSkippingIndex(TestIndexWriter.java:5011) inside an unrelated merge-thread race, fixed test-only by #16226. Nobody looked at the document after the rejected one. - #15125 (backport #16396): transient
IndexingChainstate left by a schema exception, there an NPE in the index-sort comparators at flush; different code and fix. - Context: #11 / #10374 (LUCENE-9334, origin of
FieldSchemaand its per-document reset), #13449 (added the member without a reset line), #13784 (carried it forward), #15886 and #16171 (frozenFieldTypecache). - Downstream: Elasticsearch #128541 and #157003 and OpenSearch #19660 show a different, index-wide message ("cannot change field ... to inconsistent docValuesSkipIndexType") that this bug cannot produce.
- Same family in the registry: LFV-094 and LFV-101 (a rejected
addBatchleaves doc-values or points buffer state; different code and fixes). - Limits: keyword search, best effort; "no hit" does not prove nobody noticed.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive; Elasticsearch, OpenSearch, Solr) | 2026-10-26 | four prior-art runs (62, 24, 45 and 40 queries) and 20 manual GraphQL searches on apache/lucene plus 6 per downstream repo: both exact error messages, docValuesSkipIndex, DocValuesSkipIndexType, indexedField, FieldSchema, assertSameSchema, resetJustDocId, rejected-document and flush phrasings; the file lists of all 412 open PRs (patches of the 9 relevant ones read) and of 307 closed-unmerged PRs since 2024-06-01 | related items only (#15761, #15125, #11, #13449, #13784, #15886, #16171); none the same bug |
| Mailing lists (dev@, java-user@, issues@, commits@, builds@), JIRA archive, web | 2026-10-26 | 20 list queries per prior-art run plus 25 direct lists.apache.org queries; 4 JIRA and 8 web queries | no report; only PR and commit notifications of the related items |
| Branch heads, git history, CHANGES.txt | 2026-10-26 and 2026-10-30 | reset() on main, branch_10_6 and the 10.6.0 tag; git log -S on the member; CHANGES.txt | no fix and no entry |
Introduced with the doc-values skip index by #13449 (048770205cb, merged 2024-06-13), which added the member but no reset line; first released in 10.0.0. Fails on every release from 10.0.0 to 10.6.0 and on main; 9.x lacks the API.
Versions tested6 rows
Introduced by 048770205cb0b424901cf23627cae7ea44560de4, "GITHUB#13449: Sparse index, optional skip list on top of doc values (#13449)" (iverase, co-authored by jpountz; merged 2024-06-13, milestone 10.0.0). It added hasDocValuesSkipIndex to FieldSchema, its write in setDocValues, its comparison in assertSameSchema and its use in initializeFieldInfo, but not to reset(). At that commit the repro fails in the boolean form of the messages and passes with the one-line fix (plain javac build of the commit). b59a357e586 (#13784, jpountz, 2024-09-17) turned the boolean into DocValuesSkipIndexType and left reset() unchanged; #15886 (10.5.0) split out resetJustDocId() and kept the omission.
Not fixed: upstream main 2f424fd428da (2026-10-30) has no later commit to IndexingChain.java; reset() on branch_10_6 and at the 10.6.0 tag has no skip-index line (branch_10x was decommissioned on 2026-10-29 by #16911).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails, assertions on and off (gradle and standalone); passes with the fix |
| 10.6.0 | affected | minimal test fails, assertions on and off |
| 10.0.0 to 10.5.2 (12 releases) | affected | the finder's three-case test fails 3 of 3 on every release |
main ea57360912ed, a9779ef3c193 | affected | the finder's test fails 3 of 3 (assertions on and off on ea57360912ed) |
048770205cb (#13449) | introducing commit | fails in the boolean form of the messages; passes with the one-line fix |
| 9.11.0, 9.12.3 and older | no such API | the test does not compile: no indexedField and no doc-values skip index |
An independent reproducer, a devil's-advocate verifier and a works-as-designed review by OpenAI GPT-5.6 each wrote and ran their own tests; all say real, with passing controls. An adjudication confirmed it with new seeds; a re-run on newer main and this doc's gradle run reproduce it, and the fix passed every check.
Checks and verdicts295 words
Each check ran its own test or re-ran an earlier one on stock jars; one cross-model refute pass gave no final verdict and counts neither way.
| Check | Verdict | What it found |
|---|---|---|
| Finder | candidate | Alloy counterexamples; three tests fail on main (assertions on and off), 10.0.0 and 10.5.2, and pass with the fix. |
| review (OpenAI GPT-5.6, read-only) | real (high) | Root cause agrees, the fix fixes it, inputs in contract. |
| Refute lens (OpenAI GPT-5.6, executed) | no final verdict | No final verdict in 8 attempts; its own tests failed on main, 10.0.0 and 10.5.2 and passed with the fix. Counts neither way. |
| Independent reproducer | real | Own test from the claim only: 2 base field types x 4 factories plus 3 controls; fails 3 of 6 on main, 10.5.2 and 10.0.0; controls pass; its own fix passes 6/6. |
| Works-as-designed lens (OpenAI GPT-5.6, executed) | real (high) | Own test fails on main, 10.0.0 and 10.5.2; flush and no-skip-index controls pass; upstream skip-index tests and TestPerFieldConsistency pass with the fix; the intended-behaviour argument rated moderate and rebutted. |
| Intended-behaviour devil's advocate | real (high) | Four per-attribute controls recover and only the three skip-index variants fail; the history shows an omission. |
| Adjudication | confirmed (high) | Re-ran the independent reproduction's test with seed 1234ABCD (3 of 6 fail with assertions on and off; 6/6 with the fix) and the finder's with seed 5EED5EED (3 of 3 fail). |
| Re-run on main a9779ef3c193 | still reproduces | The finder's test fails 3 of 3 with the same messages. |
| This doc's runs | real | Gradle in a scratch worktree of 645d4fe14a20: the minimal test fails on stock and passes with the fix, as do the closest suites and the whole core module; the 10.6.0 release jars fail. |
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 402 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,555 words
- Registry title
- A rejected document leaves its doc-values skip index type in the field's per-document schema state, so later valid documents with that field are rejected until the in-memory segment is flushed
- Area
- index / IndexingChain per-field schema validation (FieldSchema)
- Priority
- Low, rank 711: Moved down from Medium: after one rejected document with a skip-indexed doc value, valid documents with that field are rejected until the next flush, loud and only for mixed per-document schemas on the opt-in skip index, which fixed-mapping servers never produce.
- Impact class
- crash
- Impact note
- Applications on 10.0+ that give one field a doc-values skip index in some documents (the Sorted, SortedSet, Numeric or SortedNumeric DocValuesField.indexedField factories, or FieldType#setDocValuesSkipIndexType) and no doc values in others, once one document that carries the skip-indexed doc values for that field is rejected (because the field's established schema has no doc values, in this or an earlier segment, or because another field of the same document is invalid); default IndexWriterConfig and codec, with -ea and -da; every later document with that field but no doc values in the same in-memory segment then throws IllegalArgumentException until the next flush, NRT reopen or commit (20,000 of 20,000 in the follow-up's probe, where nothing flushed); the writer stays open and nothing is corrupted, but an application that logs and drops failed documents loses them; ordinary scale; released 10.0.0-10.5.2 and, by reading, 10.6.0.
- Summary
- You index a document in which field f carries a skip-indexed doc-values field (for example SortedDocValuesField.indexedField, new in 10.0), IndexWriter correctly rejects it with IllegalArgumentException because f is already indexed without doc values (or because another field of that document is invalid), and you keep adding documents that have f without doc values. You expect only the bad document to be rejected, as for any non-aborting exception, and the next valid documents to be accepted, as they are when the rejected document used doc values without a skip index. Instead every later document with f and no doc values in the same in-memory segment is rejected ("doc values skip index type: expected 'NONE', but it has 'RANGE'", or "field 'f' cannot have docValuesSkipIndexType=RANGE with doc values type NONE") until the next flush, because IndexingChain.FieldSchema.reset() clears the per-document doc-values type but not the skip index type, so the rejected document's RANGE is left behind.
- Introduced
- 048770205cb (#13449 'Sparse index: optional skip list on top of doc values', by iverase, co-authored by jpountz; milestone 10.0.0), which added the docValuesSkipIndex member to IndexingChain.FieldSchema together with its write in setDocValues and its comparison in assertSameSchema, but no line in reset(); carried forward by b59a357e586 (#13784 'Change docValuesSkipIndex from a boolean to an enum', which left reset() unchanged) [read: git log -S docValuesSkipIndex and -S hasDocValuesSkipIndex on IndexingChain.java return only these two commits; git log -L on FieldSchema.reset lists every commit that changed it (d03662c48bf, 6d4b5eaba35, acf45d8a315, 28ba8b77975, feac4cd09ee, a693fe819b0, 9e1c6a37c6e) and none adds the member; exec: the finder's repro fails 3 of 3 at 048770205cb, in the boolean form of the messages, and passes 3/3 there with the one-line reset fix]; the commit's parent has no doc-values skip index API, so no earlier commit can be affected (2024-06-13 (#13449 merged into main as 048770205cb); 2024-09-17 (#13784, b59a357e586); 10.0.0 tagged 2024-10-14)
- First affected version
- 10.0.0 [exec]
- Affected versions
- all 12 releases from 10.0.0 through 10.5.2 (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 ea57360912ed (unreleased 11.0.0) [exec: the finder's repro fails 3 of 3 with -ea on every release and on main with -ea and -da (follow-up history sweep, seed DEADBEEF); independently the independent reproduction, the intended-behaviour check and the cross-model reviewer reproduce with their own tests on main with -ea and -da and on 10.0.0 and 10.5.2; the follow-up's probe gives the same counts on 10.5.2 with -da]; the unreleased introducing commit 048770205cb fails too [exec, boolean form of the messages]; 9.x and older are not affected: they have no doc-values skip index API (the repro does not compile on 9.11.0 and 9.12.3 [exec]; no DocValuesSkipper in 9.11.1, 9.12.0 and 9.12.3 [read]); branch_10x (1309534296e3) and branch_10_6 (2763db11b0a7) still lack the reset line [read, not run], so 10.6.0 (released 2026-10-29) is affected too; not fixed upstream as of main ea57360912ed [read]
- 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
- semi-formal: Alloy, code reading; also reached by exhaustive check
- Upstream and prior reports
- none found (2026-10-26, the prior-report record, read-only: the follow-up's scripted prior-report search with 62 queries (24 GitHub issue/PR, 3 commit, 3 downstream-repo, 20 mailing-list over dev@, java-user@, issues@ and commits@, 4 JIRA-archive, 8 web), a parallel lists+JIRA run (24), the finder's run (45) and verifier's run (40); 20 manual GraphQL issue/PR searches on apache/lucene and 6 on downstream repos (Elasticsearch, OpenSearch, Solr); 25 direct lists.apache.org queries including builds@; the file lists of all 412 open apache/lucene PRs, with the patches of the 9 relevant ones read, and of 307 closed-unmerged PRs created since 2024-06-01; reset() on main, branch_10x and branch_10_6; git history and CHANGES.txt; the review threads of the related PRs); related prior items, none of them this bug: #15761 (closed; the same message text, but as the expected output of TestIndexWriter#testDocValuesMixedSkippingIndex inside an unrelated merge-thread race, fixed test-only by #16226), #15125 and its backport #16396 (same defect class: transient IndexingChain state left by a schema exception, there an NPE in the index-sort comparators at flush; different fix site), #11 / #10374 (LUCENE-9334, origin of FieldSchema and its per-document reset), #13449 (added the member without a reset line), #13784 (carried the omission forward), #15886 and #16171 (frozen-FieldType cache; split reset() into resetJustDocId() and reset()); downstream elastic/elasticsearch#128541 and #157003 and opensearch-project/OpenSearch#19660 show a different, index-wide message ('cannot change field ... to inconsistent docValuesSkipIndexType') that this bug cannot produce
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test fails with assertions on and off, through gradle in a scratch worktree and through a standalone runner; with the one-line fix it passes, together with the closest suites and the whole core module. Also fails on the 10.6.0 release jars (assertions on and off); the study's version sweep covers 10.0.0 to 10.5.2, the introducing commit and mainea57360912edanda9779ef3c193. Upstream main2f424fd428da(2026-10-30) has no later commit toIndexingChain.java.
- Finding record
lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2154-2161 (FieldSchema javadoc: 'A schema of the field in the current document. With every new document this schema is reset.'), :2169-2170 (docValuesType and docValuesSkipIndex members), :2236-2245 (setDocValues writes both only when docValuesType is NONE), :2276-2288 (reset(int) resets docValuesType and every other member except docValuesSkipIndex), :2290-2304 (assertSameSchema compares the skip index type at :2295), :1337-1358 (initializeFieldInfo passes s.docValuesType and s.docValuesSkipIndex to new FieldInfo), :1821-1831 (PerField.reset calls schema.reset when the field type is not the validated frozen one)lucene/core/src/java/org/apache/lucene/index/FieldInfo.java:125 (the constructor calls checkConsistency) and :160-168 (a skip index type incompatible with the doc-values type is rejected; message)lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:283-306 (a non-aborting exception keeps the document's id and marks it deleted via deleteLastDocs) and :399-415 (deleteLastDocs does not trigger a flush); lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java:76-79 (class javadoc: non-aborting exceptions can partially update the index with consistent structures of the failed document, which is then marked deleted; the strongest intended-behaviour foothold, cited by the cross-model review)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:1465-1466 (addDocument: on an exception 'the index will be consistent, but this document may not have been added')lucene/MIGRATE.md:1557-1564 ('Require consistency between data-structures on a per-field basis': structures are defined by the first document indexed that contains the field)lucene/core/src/test/org/apache/lucene/document/TestPerFieldConsistency.java:173 and :225 (after rejected documents, 'add document with correct data structures' and assert it is indexed; its random doc-values field never uses indexedField) and lucene/core/src/test/org/apache/lucene/index/TestIndexWriter.java:4998 and :5033 (testDocValuesMixedSkippingIndex and testDocValuesSkippingIndexWithoutDocValues check only the rejected document)lucene/core/src/java/org/apache/lucene/document/SortedDocValuesField.java:64, SortedSetDocValuesField.java:65, NumericDocValuesField.java:58, SortedNumericDocValuesField.java:66 (public indexedField factories) and FieldType.java:531 (setDocValuesSkipIndexType)SECURITY.md:28, 38, 50 (threat model, cited by the security record)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2209-2216 (at 645d4fe14a20) (javadoc), :2224-2225, :1555-1573 (:1564-1565), :2291-2300 (:2293-2295), :1876-1886, :2331-2343, :2345-2359 (:2349, :2350), :2258-2272, :621-637 (:627-631), :685-699 (:693, :695, :697), :1372-1414 (:1402-1403)lucene/core/src/java/org/apache/lucene/index/FieldInfo.java:125, (at 645d4fe14a20) :160-168lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:193, (at 645d4fe14a20) :283-307, :399-415; DocumentsWriter.java:427-434
- Who is affected
lucene/core/src/java/org/apache/lucene/document/SortedDocValuesField.java:64, (at 645d4fe14a20) SortedSetDocValuesField.java:65, NumericDocValuesField.java:58, SortedNumericDocValuesField.java:66, FieldType.java:531lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:1564-1565, (at 645d4fe14a20) :1876-1886, :2291-2300 (doc values written only for fields that have them; frozen-type fast path resets only the doc id)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2210, (at 645d4fe14a20) :1566-1572; FieldInfo.java:160-168; DocumentsWriter.java:76-79lucene/MIGRATE.md:1557-1564 (at 645d4fe14a20)lucene/core/src/test/org/apache/lucene/document/TestPerFieldConsistency.java:159, (at 645d4fe14a20) :173, :176-178, :207, :225, :228-230; lucene/core/src/test/org/apache/lucene/index/TestIndexWriter.java:4998-5031, :5033-5053
- Suggested fix
lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2291-2300, (at 645d4fe14a20) :2327-2343, :1405, :1876-1886
- Prior reports
a Lucene clone: git show origin/main, origin/branch_10_6 and releases/lucene/10.6.0 :lucene/core/src/java/org/apache/lucene/index/IndexingChain.java (reset() has no skip-index line; read 2026-10-30)
- History
a Lucene clone: git log 645d4fe14a20..origin/main -- IndexingChain.java (empty, origin/main 716d6f216f52); git log -1 origin/branch_10x (48cf3f515d6 'Decommission branch 10x (#16911)'); git for-each-ref releases/lucene/10.0.0 (tag 2024-10-14)
- TestRejectedSkipIndexDoc.java (one test, public API):
repro/LFV-116/src/org/apache/lucene/index/TestRejectedSkipIndexDoc.java(45 lines)
Made in parallel with this doc from the code at 645d4fe14a20, the minimal repro, the fix diff and the git history; re-rendered after an independent check of every frame.
assets/LFV-116/bug.png(402 KB)
