All findings · Lucene finding · LFV-116 · fieldschema-reset-keeps-skip-index

A rejected document with a skip-indexed doc value makes later valid documents with that field fail until the next flush

After IndexWriter rightly rejects one document whose field f carries a skip-indexed doc value (such as SortedDocValuesField.indexedField, new in 10.0), it also rejects every later valid document with f and no doc values until the next flush. Every release from 10.0.0 to 10.6.0 has it.

Summary

Priority
Low, rank 711Moved 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
crash
Since
10.0.0 (#13449, 2024)introduced 2024-06-13 · about 2 years
Affected
every release from 10.0.0 to 10.6.0 and main
Status
documented
Found by
an Alloy model of the per-field schema check, then a public-API JUnit

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

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.

Background

  1. Per-field consistency. Since 9.0, a field must have the same data structures in every document: if the first indexed document has f without doc values, a later document that adds doc values to f is rejected with IllegalArgumentException, and only that document is skipped.
  2. The per-document record. To check that, IndexingChain builds a small record per field of what the current document contains (index options, doc-values type, points, vectors), compares it with the field's FieldInfo, and clears it before the next document.
  3. Doc-values skip index. Since 10.0, doc values can carry a skip index (DocValuesSkipIndexType.RANGE), for example through SortedDocValuesField.indexedField. A skip index without doc values is not allowed.
Diagram of the bug: doc 0 with StringField f is accepted; doc 1 with StringField f plus a skip-indexed sorted doc value for f is rejected; the per-field record is then reset to doc-values type NONE but keeps skip index type RANGE; doc 2 with StringField f, the same structure as doc 0, is rejected with 'expected NONE, but it has RANGE'. With the one-line fix the reset also clears the skip index type and doc 2 is accepted.
Three documents for field 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.

The finding, step by step

  1. You add doc 0 with StringField("f", "a"). It is accepted, so f is a postings-only field in the in-memory segment.
  2. Doc 1 has StringField("f", "b") plus SortedDocValuesField.indexedField("f", "b"), doc values with a skip index. It is rightly rejected: doc values type: expected 'NONE', but it has 'SORTED'.
  3. 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'.
  4. Why: before each document Lucene clears a per-field record of what that document contains. The clearing step resets the doc-values type to NONE but forgets the skip index type, so doc 1's RANGE is left behind.
  5. Every later document with f and 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.

Who is affected

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 code (main 645d4fe14a20)

  1. The record. IndexingChain.FieldSchema (IndexingChain.java:2209-2360) holds docValuesType and docValuesSkipIndex (: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."
  2. The write. updateDocFieldSchema calls setDocValues(type, skip) only for a field with doc values (:1564-1565). setDocValues (:2291-2300) stores both values, but only while docValuesType == NONE (:2293-2295).
  3. The reset. For each new document, PerField.reset (:1876-1886) calls schema.reset(docId) unless the field type is the cached frozen type that already validated. reset(int) (:2331-2343) resets omitNorms, storeTermVector, indexOptions, docValuesType, the three point and the three vector members, and not docValuesSkipIndex.
  4. The poisoned document. Doc 1 sets (SORTED, RANGE) and is rejected by assertSameSchema (:2345-2359). Its second instance of f has a different field type, so the frozen-type fast path is dropped (:627-631) and, because validation threw, not re-armed (:697 runs only after success). So doc 2 goes through schema.reset, which leaves (NONE, RANGE). (The fast path exists since 10.5.0; before that every document goes through reset, with the same result.)
  5. The two failures (initAndValidateFields, :685-699). When f has a FieldInfo in the segment, assertSameSchema compares the stale RANGE with the FieldInfo's NONE (:2350): "doc values skip index type: expected 'NONE', but it has 'RANGE'". When it has none yet, initializeFieldInfo passes (NONE, RANGE) to new FieldInfo (:1402-1403), whose constructor always runs checkConsistency() (FieldInfo.java:125) and rejects it (:160-168). Either way the next document takes the same path again.
  6. Why it lasts. A rejected document is non-aborting: it keeps its doc id and is marked deleted, and deleteLastDocs does not trigger a flush (DocumentsWriterPerThread.java:283-307, :399-415). It does not even reach the flush check: DocumentsWriter.updateDocuments calls flushControl.doAfterDocument (DocumentsWriter.java:434) only after dwpt.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 gives f doc values without a skip index is itself rejected, but setDocValues first overwrites both members, so the next StringField f document is accepted. A later skip-indexed document leaves RANGE behind again.

More of the same (executed by the study, not part of the minimal repro)

  • It persists. After one rejected document, 20,000 of 20,000 later valid documents with f were rejected while 20,000 of 20,000 interleaved documents without f were 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. f is new to the segment and the document is rejected because of another field: the first valid StringField f then fails with field 'f' cannot have docValuesSkipIndexType=RANGE with doc values type NONE. f exists only in an earlier committed segment: the index-wide check rejects the skip-indexed document, and 5 of 5 later StringField f documents fail before the next flush.
  • All four factories (Sorted, SortedSet, Numeric, SortedNumeric indexedField) with StringField or StoredField as f'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, NumericDocValuesField or SortedNumericDocValuesField indexedField, or FieldType#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 because f was 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 f and no doc values in the same in-memory segment fails with IllegalArgumentException until 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 f are 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 f carries a skip-indexed doc value, then documents with f and no doc values. An application that always sends the same Lucene fields for a field name never produces that mix: either every document with f writes both doc-values members again (or, on the 10.5+ frozen-type fast path, keeps the already validated ones), or no document ever writes RANGE.

Minimal repro

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.

TestRejectedSkipIndexDoc.java (one test, public API)45 lines
/*
 * 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)));
    }
  }
}
Command
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.

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

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.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; with the fix: passes. Also fails on the 10.6.0 release jars
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (the only platform run; the code path is pure Java)
Command (environment)
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

Observed output

# 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'.

Why it looks unintended

  • The record's javadoc says it "is reset" with every new document, yet it keeps a rejected document's value.
  • MIGRATE.md defines a field's structures by the first document indexed; TestPerFieldConsistency asserts 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. FieldSchema is "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, TestPerFieldConsistency starts 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 structures and 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 uses indexedField. The skip-index tests testDocValuesMixedSkippingIndex and testDocValuesSkippingIndexWithoutDocValues (TestIndexWriter.java:4998-5053) check only the rejected document and add nothing after it.
  • The leftover pair is one Lucene forbids. (NONE, RANGE) is what FieldInfo.checkConsistency rejects (FieldInfo.java:160-168) and what updateDocFieldSchema rejects for a single field (IndexingChain.java:1566-1572).
  • History shows an omission. LUCENE-9334 (d03662c48bf, 2021) created FieldSchema and reset every member it compared; LUCENE-10577 (a693fe819b0, 2022) added vectorEncoding to reset() in the same commit; #13449 (048770205cb) added the skip index member, its write, its comparison and its use in initializeFieldInfo, but no reset line, and #13784 (b59a357e586) kept it that way. Neither PR's review discusses reset().
  • 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.

Suggested fix

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.

Reset the skip index type in FieldSchema.reset (validated by the checks) (recommended)

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. setDocValues already 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, TestDocValues and TestSortedSkipperScorerSupplier pass (213 tests, 0 failures, 4 skipped; assertions on, seed DEADBEEF);
  • the whole :lucene:core:test passes (748 classes, 8,897 tests, 0 failures, 322 skipped; assertions on, seed DEADBEEF), and so does checkGoogleJavaFormat.

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.

Ignore the skip index type when the document has no doc values (not recommended)

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.

Rebuild the record for each document (not recommended)

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.

Downstream note

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.

How it was found

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/PerField state machine, checked against a stateless reference of the per-document schema contract. The checks StockLocalNoPoison and RowOnlyStockNoPoison gave 3-document counterexamples, and StockNoPoison one at the full 18-schema scope; with the fix, SkipFixLocalNoPoison is 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.

Prior reports, history and checks

Prior reports

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 IndexingChain state 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 FieldSchema and its per-document reset), #13449 (added the member without a reset line), #13784 (carried it forward), #15886 and #16171 (frozen FieldType cache).
  • 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 addBatch leaves doc-values or points buffer state; different code and fixes).
  • Limits: keyword search, best effort; "no hit" does not prove nobody noticed.
What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive; Elasticsearch, OpenSearch, Solr)2026-10-26four 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-01related items only (#15761, #15125, #11, #13449, #13784, #15886, #16171); none the same bug
Mailing lists (dev@, java-user@, issues@, commits@, builds@), JIRA archive, web2026-10-2620 list queries per prior-art run plus 25 direct lists.apache.org queries; 4 JIRA and 8 web queriesno report; only PR and commit notifications of the related items
Branch heads, git history, CHANGES.txt2026-10-26 and 2026-10-30reset() on main, branch_10_6 and the 10.6.0 tag; git log -S on the member; CHANGES.txtno fix and no entry

History

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).

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails, assertions on and off (gradle and standalone); passes with the fix
10.6.0 affectedminimal test fails, assertions on and off
10.0.0 to 10.5.2 (12 releases) affectedthe finder's three-case test fails 3 of 3 on every release
main ea57360912ed, a9779ef3c193 affectedthe finder's test fails 3 of 3 (assertions on and off on ea57360912ed)
048770205cb (#13449) introducing commitfails in the boolean form of the messages; passes with the one-line fix
9.11.0, 9.12.3 and older no such APIthe test does not compile: no indexedField and no doc-values skip index

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
Finder candidateAlloy 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 verdictNo 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 realOwn 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 reproducesThe finder's test fails 3 of 3 with the same messages.
This doc's runs realGradle 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.

Diagram file

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

Sources and provenance

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

Finding record

LFV-116, snapshot of 2026-11-06

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 main ea57360912ed and a9779ef3c193. Upstream main 2f424fd428da (2026-10-30) has no later commit to IndexingChain.java.

Lucene and upstream sources

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-168
  • lucene/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:531
  • lucene/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-79
  • lucene/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)

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

Repro files

About the diagrams

Made in parallel with this 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.

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