All findings · Lucene finding · LFV-787 · completion-mixed-suggest-field-types-close-writer-at-flush-and-merge

Mixing SuggestField and ContextSuggestField under one field name closes the IndexWriter at flush and at every merge that combines the two types

Lucene accepts a SuggestField and a ContextSuggestField under one field name and compares their types only while writing segments, inside flush and merge. The resulting IllegalArgumentException closes the IndexWriter: uncommitted documents are lost, and two cleanly committed segments of the two types close every writer that merges them.

Summary

Priority
Low, rank 321A loud failure in the opt-in document suggester caused by mixing two suggest field classes under one name.
Impact
index unusable
Since
5.3.0 (LUCENE-6459, 2015)introduced 2015-05-28 · over 11 years
Affected
5.3.0 to 10.5.2, 10.6.0 (by reading) and main (8 versions run)
Status
documented
Found by
code reading and probes, then a bounded-exhaustive model-based test

AI-assisted page: this finding was first signalled by code reading or testing 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

The document suggester stores a one-byte type per suggest field: 0 for SuggestField, 1 for ContextSuggestField. The two must not share a field name, but addDocument does not check that. Only the completion postings writer does, while a flush or a merge writes the segment.

Diagram of the bug: segment A holds document 1 with a SuggestField (type 0) and segment B document 2 with a ContextSuggestField (type 1) under the same field name; both were committed and CheckIndex is clean. forceMerge(1) combines them, the completion postings writer sees type 0 and then type 1 and throws 'single field name has mixed types', and the IndexWriter closes itself, so uncommitted document 3 is lost; a new writer fails the same way. A buffered segment holding both types fails the same way at commit.
Two committed segments of one suggest field, one per class, both clean: the merge that combines them hits the type check and the IndexWriter closes itself, losing the uncommitted document. Select the still to open it at full size. Generated with AI, checked against the code.
  1. Route field suggest_field to the completion postings format, as the document suggester requires. Commit document 1 with a SuggestField ("apple"), then document 2 with a ContextSuggestField ("apricot") under the same name. Both commits succeed and CheckIndex is clean: 2 segments, 2 documents.
  2. Add document 3 and call forceMerge(1). Mixing the two classes is not supported, so an error is fair, but you expect the writer to stay open and documents 1, 2 and 3 to survive.
  3. Instead the merge throws IllegalArgumentException: single field name has mixed types and the IndexWriter closes itself; the index still has 2 documents, so document 3 is lost. A new writer, or the merge policy's own merge after a commit, closes the same way until one type has no live documents left (deleted, rewritten or reindexed).
  4. Why: every posting carries a type byte (0 for SuggestField, 1 for ContextSuggestField). addDocument never compares it; only the completion postings writer does, during flush and merge, where Lucene treats any exception as a disaster.
  5. With both types in one buffered segment, the next commit closes the writer: of 3 uncommitted documents none survive, an unrelated one included. Every release tag checked since 5.3.0 (2015) has it.

Who is affected

Applications that use the document suggester directly (lucene/suggest, opt-in through a per-field codec) and index both classes under one field name: by mistake, or by adding contexts to an existing suggest field without reindexing. One document of each type is enough. Nothing is corrupted, but the writer closes and those segments cannot be merged while both types have live documents.

Mechanism, terms and numbers651 words

The document suggester has two field classes: SuggestField writes type byte 0 and ContextSuggestField type byte 1 into every completion posting payload. One field name may not mix them; the design (LUCENE-6459) was that a mix is rejected with an IllegalArgumentException "upon indexing". But addDocument never compares the types. The only check is in CompletionFieldsConsumer, while the completion postings are written at flush and at merge, and core treats any exception there as aborting (flush) or tragic (merge). So the IndexWriter closes itself and drops every uncommitted document, unrelated ones included. If the two types landed in different segments, both commit cleanly and CheckIndex passes, but every merge that combines them closes the writer, in every new writer too, until one type has no live documents left. Present since 5.3.0 (2015); executed on 5.3.0 through 10.5.2 and main. A 4-file fix that marks the field as mixed instead of throwing, and lets CompletionQuery reject queries on it as it already does across segments, keeps the writer open; it passes the repro and the whole suggest module.

Terms

  • Document suggester: SuggestField and ContextSuggestField (lucene/suggest, @lucene.experimental) index suggestions that SuggestIndexSearcher serves from a per-segment FST. The field must be routed to a completion postings format (for example Completion104PostingsFormat) through a per-field codec.
  • Type byte: each class writes its type into every posting payload, after the surface form and the weight: 0 for SuggestField (SuggestField.java:64, :123-124, :133), 1 for ContextSuggestField (ContextSuggestField.java:51, :106-107). Both use the same FieldType (SuggestField.java:53-62, :79), so the indexing chain's per-field schema checks cannot tell them apart.
  • Aborting and tragic exceptions: an exception while a flush writes a segment aborts it and fires a tragic event; any exception in a merge is tragic. A tragic event rolls the writer back: it closes, and everything since the last commit is gone.

The bug

  1. The only comparison is at write time. CompletionFieldsConsumer.write writes the delegate postings, then one CompletionTermWriter per field (:85-113). CompletionTermWriter.write reads every posting's type byte; the first sets the field's type and a different one throws IllegalArgumentException("single field name has mixed types") (:214-220, throw at :219).
  2. It runs inside flush and merge. DocumentsWriterPerThread.flush catches every Throwable as the aborting exception (:585-589), and maybeAbort aborts and fires the tragic event (:594-605). IndexWriter.merge catches every Throwable and calls tragicEvent(t, "merge") (:5109-5113).
  3. Each segment stores one type. The type is saved per field and segment (CompletionFieldsConsumer.java:105-110, :149), so a segment of each type flushes and commits cleanly. A merge re-reads the live postings of its input segments; if they hold both types it hits the check again, in every writer, until one type has no live documents left. Same-type merges work.
  4. The query side already copes. CompletionQuery.rewrite compares the segments' types and throws IllegalStateException("... has values of multiple types") (CompletionQuery.java:87-103), added in the same commit as the write-time check.

The example, step by step

Commit doc 1 (SuggestField "apple"): segment A, type 0. Commit doc 2 (ContextSuggestField "apricot"): segment B, type 1. Both commits succeed; CheckIndex is clean. Add doc 3 (no suggest field) and call forceMerge(1): the merge sees type 0 then type 1 and throws; the writer closes; reopening the index finds 2 documents. A new writer's forceMerge(1) closes it too. Deleting doc 2 lets the merge succeed.

Also affected: a buffered segment holding both types (the next commit, NRT reopen, or close() with the default commitOnClose=true, closes the writer and loses all uncommitted documents), upstream's own input of one document with both fields, a natural merge picked by the merge policy, and addIndexes(Directory), which imports a segment of the other type with no check. addIndexes(CodecReader) over both types throws but leaves the writer open (the finder's probe and the fact checks executed it).

Exposure and likelihood281 words
  • Who is exposed. Direct users of SuggestField / ContextSuggestField with a field routed to a completion postings format. Triggers: adding contexts to an existing suggest field without reindexing, two code paths (one with contexts, one without), or one document with both fields (upstream's own test input).
  • Buffered mix. The next flush (commit, NRT reopen, close() with the default commitOnClose=true, or an automatic flush) closes the writer and loses every document added since the last commit, unrelated ones included: in the repro the index keeps only the 1 previously committed document.
  • Committed mix. Both commits succeed, CheckIndex is clean and suggest queries already fail with the designed IllegalStateException. Every merge whose input holds live documents of both types then closes the writer: forceMerge, a natural merge, any new writer. Each time the uncommitted documents are lost. It ends once one type has no live documents left: delete them, hard-update them to the other class (executed), or reindex.
  • Default settings. Beyond the per-field codec the suggester needs anyway, nothing unusual is required: an independent probe closed the writer through a natural merge with TieredMergePolicy and ConcurrentMergeScheduler after 8 alternating commits.
  • Bounded. The committed index stays readable and CheckIndex-clean. No scale is needed.
  • Soft-update migrations stay stuck. With SoftDeletesRetentionMergePolicy, the retained soft-deleted old version keeps the other type in the merge: after a softUpdateDocument from SuggestField to ContextSuggestField, forceMerge(1) closes the writer, while a hard updateDocument does not (the finder's exploratory probe; re-run by the cross-model fact check's probe with assertions on and off).
  • How likely. Rare: both classes under one name, Elasticsearch's completion mapping does not allow changing a field's contexts, and no report found since 5.3.0 (2015).

Minimal repro

The test shows that one SuggestField segment and one ContextSuggestField segment, both committed cleanly under the same field name; forceMerge(1) of the pair closes the IndexWriter.

TestCompletionMixedTypesMerge.java (JUnit test)52 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.search.suggest.document;

import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.SerialMergeScheduler;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;

public class TestCompletionMixedTypesMerge extends LuceneTestCase {

  public void testMergeOfCommittedSuggestAndContextSuggestSegments() throws Exception {
    IndexWriterConfig iwc =
        new IndexWriterConfig()
            .setCodec(TestUtil.alwaysPostingsFormat(new Completion104PostingsFormat()))
            .setMergeScheduler(new SerialMergeScheduler());
    try (Directory dir = newDirectory();
        IndexWriter w = new IndexWriter(dir, iwc)) {
      Document suggest = new Document();
      suggest.add(new SuggestField("suggest", "apple", 1));
      w.addDocument(suggest);
      w.commit();
      Document contextSuggest = new Document();
      contextSuggest.add(new ContextSuggestField("suggest", "banana", 1));
      w.addDocument(contextSuggest);
      w.commit();
      try {
        w.forceMerge(1);
      } catch (IllegalArgumentException ignored) {
      }
      // expected: the writer stays open (rejecting the merge is fine); actual: the merge closes it
      assertNull("the merge closed the writer", w.getTragicException());
    }
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-787/, 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:$LUCENE/lucene/analysis/common/build/libs/lucene-analysis-common-$V.jar
CP=$CP:$LUCENE/lucene/suggest/build/libs/lucene-suggest-$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.search.suggest.document.TestCompletionMixedTypesMerge
Expected vs actual
Expected: the writer stays open (rejecting the merge is fine); actual: tragic IllegalArgumentException 'single field name has mixed types', writer closed (-ea and -da).

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

More files, environment, full output45 words

Environment

luceneLucene main e1faeba08bde (2026-11-02)
jdk25.0.3+9-LTS

Observed output

# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
    => java.lang.AssertionError: the merge closed the writer expected null, but was:<java.lang.IllegalArgumentException: single field name has mixed types>
       org.junit.Assert.fail(Assert.java:89)
       org.junit.Assert.failNotNull(Assert.java:756)
       org.junit.Assert.assertNull(Assert.java:738)
       org.apache.lucene.search.suggest.document.TestCompletionMixedTypesMerge.testMergeOfCommittedSuggestAndContextSuggestSegments(TestCompletionMixedTypesMerge.java:49)

Why it looks unintended

  • The suggest module's design rejects a mix with an IllegalArgumentException (LUCENE-6459), an input error.
  • IndexWriter documents closing itself for an Error or a disaster during a checkpoint, as a guard against corrupted state (IndexWriter.java:162-165); a type mismatch corrupts nothing.
  • In the merge case nothing is rejected: both segments committed cleanly and pass CheckIndex, yet every merge closes the writer.
The full argument367 words
  • The design chose input validation. In the LUCENE-6459 review (#7518) a reviewer asked "Are you allowed to mix ContextSuggestField and SuggestField even for the same field name, within one suggester?"; the author answered "No you are not. If mixed, CompletionFieldsConsumer will throw IllegalArgumentException upon indexing.", and the reviewer replied "OK, excellent." Nothing in the thread mentions the writer closing, lost documents or merges.
  • IndexWriter's contract. The class NOTE: the writer closes itself "if you hit an Error, or disaster strikes during a checkpoint", as "a defensive measure in case any internal state (buffered documents, deletions, reference counts) were corrupted" (IndexWriter.java:162-165). addDocument: on an exception "the index will be consistent, but this document may not have been added" (:1465-1466). Design intent in the package-private DocumentsWriter (:67-79): aborting exceptions are the "certain limited times when an exception can corrupt this state"; "All other exceptions" only mark the document deleted.
  • The merge case rejects no input. Both segments were accepted and committed, CheckIndex is clean and the query side reports the state cleanly (CompletionQuery.java:87-103). Failing every merge in every writer is not validation.
  • The only test never looks at the writer. TestContextSuggestField#testMixedSuggestFields (:158-176) expects the exception from addDocument or commit(false) and stops there. No test covers two documents, separate segments, merges or addIndexes.
  • No user-facing documentation of the rule in the javadoc of SuggestField, ContextSuggestField, CompletionPostingsFormat, CompletionQuery or the package.
  • Precedents. LUCENE-5662 (#6724, "Missing Field null checks can result in aborted segments", fixed in 4.8.1) and LUCENE-8441 (#9487, a wrong index-sort field type now fails the document when it is added, 7.5) moved validation out of aborting steps as bug fixes. LUCENE-7562 (#8613) fixed a different merge-time exception in this same file, one that made merges fail again and again, as a Lucene bug. A design precedent, not a fix: #15584 (2026), an enhancement that added a new indexing-time validation, made it non-aborting from the start (IndexingChain.java:2095-2101), and its test says "ensure IndexWriter remains open and doc was skipped" (TestCustomTermFreq.java:125).
  • Timeline. LUCENE-6579 (#7637), which made every merge exception tragic, was resolved on 2015-07-14, after the design answer; both shipped in 5.3.0.
The devil's-advocate check280 words

The strongest case for "works as designed" (built by the devil's advocate; risk moderate): mixing is invalid by design (the review answer); upstream's test expects an exception from its add-and-commit block, and a 2019 commit (ad457d188e5, "Improve RIW exception handling and opt out of concurrent flushing if exception is expected") adjusted that test for a flush-time exception; core makes every flush or merge exception aborting by design (LUCENE-5987, LUCENE-6579, TestIndexWriterExceptions#testDocumentsWriterAbort); the classes and the codec API are @lucene.experimental, and the codec API cannot reject a document without aborting; the module already calls the cross-segment state invalid at query time; a major downstream server blocks the mix in its mapping.

Why it fails: it explains why an exception is thrown, not why the writer must close and lose unrelated documents; no document, test or discussion accepts that. For the merge case it offers only the generic rules (one schema per field name, every merge exception tragic); it does not say why an index that accepted and committed both segments and passes CheckIndex should close every writer that merges them, while the query side already reports the state as a clean error. The way out is rewriting data. The IndexWriter contract describes self-closing as a guard against corrupted state, upstream fixed this shape as a bug before (LUCENE-5662, LUCENE-8441, LUCENE-7562), and #15584 made a new validation non-aborting by design. Verdict: a defect (adjudication: confirmed, medium confidence). A maintainer may still call the one-segment flush case expected, or file the whole as an improvement (validate earlier, document the rule); the cross-model fact check rates the "works as designed" case strong for that flush case and moderate for the merge.

Suggested fix

Recommended Mark the field as mixed instead of throwing, and let CompletionQuery reject it

Stop throwing in the completion writer: mark the field as mixed (type 2) and let CompletionQuery, which already rejects a field whose segments differ, reject queries on it. Flushes and merges then succeed and nothing is lost; suggest queries on a mixed field fail until one type is deleted. 4 files, +17/-3; the whole suggest module passes.

--- CompletionFieldsConsumer.java (CompletionTermWriter.write)
           } else if (this.type != type) {
-            throw new IllegalArgumentException("single field name has mixed types");
+            // SuggestField and ContextSuggestField values under one field name. Throwing here would
+            // abort the flush or merge and close the IndexWriter, so mark the field instead and let
+            // CompletionQuery reject queries on it.
+            this.type = CompletionPostingsFormat.MIXED_TYPE;
           }
--- CompletionQuery.java (rewrite)
         byte t = completionTerms.getType();
+        if (t == CompletionPostingsFormat.MIXED_TYPE) {
+          throw new IllegalStateException(getField() + " has values of multiple types");
+        }
--- CompletionPostingsFormat.java: + static final byte MIXED_TYPE = 2; (with javadoc)
All options and trade-offs3 options · 748 words

Only option A was built and validated. It moves the error from indexing time to query time, so upstream's testMixedSuggestFields must change; a maintainer may prefer to keep the rejection at indexing time (option B), which needs a core hook because nothing at addDocument knows the field's type in the rest of the index. Until a fix ships, using ContextSuggestField (with zero contexts where none apply) for every document of the field avoids the mix.

Mark the field as mixed instead of throwing, and let CompletionQuery reject it (validated by the checks) (recommended)

CompletionTermWriter.write records MIXED_TYPE (2) instead of throwing; CompletionQuery.rewrite throws its existing IllegalStateException("... has values of multiple types") for a mixed segment; CompletionTerms.getType() and the format javadoc mention the new value. Merges re-derive the type from live postings, so deleting every document of one type and merging makes the field queryable again (probe executed). testMixedSuggestFields changes from "commit throws" to "a suggest query throws".

--- CompletionFieldsConsumer.java (CompletionTermWriter.write)
           } else if (this.type != type) {
-            throw new IllegalArgumentException("single field name has mixed types");
+            // SuggestField and ContextSuggestField values under one field name. Throwing here would
+            // abort the flush or merge and close the IndexWriter, so mark the field instead and let
+            // CompletionQuery reject queries on it.
+            this.type = CompletionPostingsFormat.MIXED_TYPE;
           }
--- CompletionQuery.java (rewrite)
         byte t = completionTerms.getType();
+        if (t == CompletionPostingsFormat.MIXED_TYPE) {
+          throw new IllegalStateException(getField() + " has values of multiple types");
+        }
--- CompletionPostingsFormat.java: + static final byte MIXED_TYPE = 2; (with javadoc)

Trade-offs

  • The error moves from indexing time (today the commit throws and the writer dies) to query time, so users learn about the mix later.
  • CompletionTerms.getType() (public, experimental) can return 2 and the .cmp file can store 2. The reader does not validate the byte, so nothing else changes; an older release reading such a segment would run any completion query on the mixed FST without an error. Making old readers refuse it needs a format version bump, and today COMPLETION_CODEC_VERSION is both the minimum and the current version (CompletionPostingsFormat.java:102-103, CompletionFieldsProducer.java:88-89): a plain bump would make the patched reader reject existing segments too, so it needs separate minimum and current values and back-compatibility tests. Not done here.
  • addIndexes(CodecReader) over both types, today the one path that rejects cleanly, succeeds and marks the result mixed; a suggest query then throws (executed by the fact checks).
  • Soft-update migrations: with SoftDeletesRetentionMergePolicy the merge succeeds (executed), but while the old soft-deleted versions are retained the merge input still holds both types, so the merged segment is marked mixed and suggest queries stay rejected until retention drops them (read, not queried). Upstream would have to decide whether that is acceptable.
  • The sibling bug (deleted or rejected postings counted at flush): its 3 tests also pass, but such a segment is marked mixed until a merge drops the deleted posting, so its own live-postings fix is still worth having.
  • Not benchmarked; one assignment replaces a throw, and rewrite adds one byte comparison per segment.

What the checks validated

  • Scratch worktree of a9779ef3c193, gradle, JDK 25: stock + the new test, 6 of 8 fail with assertions on and off; + the fix and the updated testMixedSuggestFields, :lucene:suggest:check passes (ecjLint, forbiddenApis, checkGoogleJavaFormat; 32 suites, 244 tests, 7 skipped, 0 failures, including the new test 8 of 8 and TestContextSuggestField 6 of 6), and the module's tests pass with assertions off.
  • Standalone runner: the new test 8 of 8 with assertions on and off; the finder's 4 tests pass; the fix probe passes 2 of 2.

Reject the conflicting document in addDocument (needs a core hook)

Keeps the error at indexing time as a non-aborting rejection, as the LUCENE-6459 design answer implies. Not built.

Trade-offs

At addDocument nothing in the suggest module knows the field's type in the rest of the index: both classes use the same FieldType, and FieldInfo attributes are not validated across documents ("the behaviour after merge is undefined", FieldInfo.java:669-671). It needs a per-field schema property that the indexing chain checks, the same check in addIndexes(Directory), and handling for indexes that already mix the types across segments (they would still need option A at merge).

Document the rule

Javadoc on SuggestField and ContextSuggestField: never index both under one field name; doing so closes the IndexWriter at the next flush or merge. Not built.

Trade-offs

No behaviour change: the writer still closes and the committed index still cannot be merged.

Downstream note

Downstream products are exposed only if they let one field name receive both field classes; Elasticsearch, for one, does not allow changing a completion field's contexts in its mapping. A server that picked the class from the structure of a user's request would let that user trigger it indirectly.

How it was found

A plan reviewer reading CompletionFieldsConsumer noticed that the type is checked only while postings are written; public-API probes then closed the writer at flush and at merge. A bounded-exhaustive model-based test of the real writer (3,257,424 runs each with assertions on and off, 0 mismatches within the model) widened it to natural merges, new writers and addIndexes(Directory).

The method in full215 words

Method: code reading and exploratory probes, then bounded-exhaustive model-based testing of the real IndexWriter.

  • During planning, an independent reviewer from another model family (OpenAI GPT-5.6) read CompletionFieldsConsumer.java and ranked this lead first; a reading of main then confirmed that the type is checked only while postings are written, after the delegate postings, and that flush treats every Throwable as aborting.
  • The finder's exploratory public-API probes gave the first executed signal: a tragic close at flush and at merge.
  • The finder then wrote a predictive model of IndexWriter with the completion postings format and compared it, operation by operation, with the real writer over every script of length up to 5 over 12 operations (adds of each type and both, plain and rejected adds, delete, flush, commit, forceMerge(1), NRT reopen, addIndexes(Directory), addIndexes(CodecReader)) and 12 configurations: 3,257,424 runs each with assertions on and off, 0 model mismatches (1 to 5 ConcurrentMergeScheduler timing interleavings per run, where forceMerge rethrows before the merge thread closes the writer, were classified separately; natural merges with merge factor 2 were checked only against the contract); a model mutant was killed with 12,328 mismatches, and with the throw removed every contract-violation class dropped to 0. It was minimised into the JUnit; independent AI tools then reproduced, reviewed and judged it.

Prior reports, history and checks

Prior reports

No prior report found (2026-10-27): 376 query records over GitHub issues, PRs, commits and review threads, the mailing lists, the JIRA archive, downstream projects and the web; the exact message returns no GitHub issue or PR. Closest: LUCENE-6459, which added the check, and LUCENE-7562, a different merge failure in the same file.

Searches4 searches

No issue, PR, review comment, JIRA ticket, list thread, downstream report or web page describes a mix that addDocument accepts being rejected only at flush or merge, the writer closing, lost documents, or merges of the two segment types failing. The check is unchanged on upstream main (f7bcc0abd816, re-read at 20ced71c1e6e), branch_10x and branch_10_6; branch_9x was tombstoned in 2024 (#13882), and its last release, 9.12.3, is affected (executed). No open PR touches the four suggest files.

Closest, not this bug: #7518 (LUCENE-6459, added the check; states only that a mix is invalid and throws "upon indexing"), #7400 (LUCENE-6339, the document suggester), #8613 (LUCENE-7562, a merge-time NPE in the same file on ghost fields, fixed as a bug in 5.5.4 and 6.4), #7637 (LUCENE-6579, made every merge exception tragic, same release), the bug-fix precedents #6724 (LUCENE-5662) and #9487 (LUCENE-8441), and #15584, a design precedent (not a bug fix).

What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene issues, PRs and commits; downstream repositories; dev@, java-user@, issues@ and commits@; JIRA archive; web2026-10-27five runs of a scripted prior-report search (the finder's, the devil's advocate's and three follow-up runs), 51 queries eachonly the related items above
GitHub (apache/lucene, apache/lucene-solr, Elasticsearch, OpenSearch, Solr, all of GitHub)2026-10-2775 GraphQL searches, including the exact message"single field name has mixed types": no issue or PR
Review threads and open PRs2026-10-27the full threads of #7518, #7400, #8613, #7637, #7049 and others; the changed files of all 409 open apache/lucene PRsonly #7518 discusses the mix; no open PR touches the files
lists.apache.org and JIRA2026-10-2730 list queries; 4 JQL queriesno report

History

Introduced with ContextSuggestField by LUCENE-6459 (be4d58c7740, branch_5x 380bb29f395, 2015-05-28) and first released in 5.3.0; not a regression. It fails the same way on 5.3.0, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.12.3, 10.5.2 and main; 5.2.1 has no ContextSuggestField.

Versions tested5 rows

Introduced by LUCENE-6459 "add common suggest API for document based NRT suggester" (trunk be4d58c7740, branch_5x 380bb29f395, both 2015-05-28; GitHub mirror #7518; svn era, no PR). The commit added ContextSuggestField, the type byte, the write-time check, the query-time check and testMixedSuggestFields. On the main line of history git log -S on the message finds only this commit (--all adds its branch_5x copy and a 2021 branch_8x tombstone), and git blame (also -w -C, ignoring the LUCENE-9570 reformat) puts today's check on it. The first release is 5.3.0: the branch_5x commit is not an ancestor of the 5.2.0 or 5.2.1 tags, and CHANGES.txt lists LUCENE-6459 under 5.3.0. The merge case also depends on LUCENE-6579 (trunk 926397824e9, 2015-07-14, "exceptions during merging are now tragic"; CHANGES lists it under 5.3.0), which shipped in the same release. Later commits on the file (LUCENE-6553, LUCENE-7562, refactorings) did not change the check.

Caveats: releases 5.x-8.x ran on JDK 25, outside their supported JDKs; the failing path is plain Java and the controls pass on the same JVM. All 94 release tags from 5.3.0 to 10.5.2 in the history clone contain the check (read).

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthe brief's test 6 of 8 fail (gradle and the standalone runner, assertions on and off); the portable main() fails all four scenarios
10.5.2 affectedthe brief's test 6 of 8 fail (assertions on; off in the fact check); the finder's and the independent reproducer's tests fail
5.3.0, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.12.3 affectedthe portable main(): all four scenarios close the writer; the same-type control passes
5.2.1 no APIno ContextSuggestField: compile error
main + fix not affectedthe brief's test 8 of 8; the whole lucene/suggest module, 244 tests, 0 failed

Independent checks

An independent reproducer, a devil's advocate and an adjudication confirmed it with their own runs; reviews by another model family agreed on the root cause but ran read-only. A registry re-run, an audit and fact checks reproduced it, and this doc's test fails 6 of 8 on main and passes with the fix.

Checks and verdicts489 words

Every executed check came from AI tools of one model family; the reviews by another model family (OpenAI GPT-5.6) ran, reading only and could not run Java, and separate runs re-ran the finder's JUnit (it fails).

Each independent check and its verdict
CheckVerdictWhat it found
finder candidate4-test public-API JUnit fails 4 of 4 on main (-ea, -da); its 6-test version-portable copy fails 6 of 6 down to 8.11.2; same-type control 4 of 4 pass; removing the throw (a root-cause check, not a fix) passes 4 of 4; model-based test: 0 model mismatches in 3,257,424 runs each with -ea and -da (timing interleavings of the concurrent merge scheduler classified separately).
cross-model review, refute lens procedural "not real" (low)Ran read-only and could not execute, so it voted not real on procedure only; it agreed with the root cause.
cross-model review, intended lens real (medium)Read-only; inputs in contract; cites the DocumentsWriter javadoc, LUCENE-7562 and #15584.
Cross-model review, root-cause lens uncertain (medium)Read-only; root cause agrees, inputs in contract; the uncertainty comes from not executing. It led to splitting off the sibling (deleted or rejected postings counted at flush).
Independent reproducer realOwn JUnit from the claim only: 5 of 6 fail on main (-ea, -da), 10.5.2 and 9.12.3; shows the natural CMS merge, the new writer, CheckIndex clean and recovery after deleting one type.
Devil's advocate real, moderate framing riskBuilt the by-design case and rejected it; its probe closed the writer through a natural merge with an all-default config after 8 alternating commits; corrected the first release to 5.3.0.
adjudication confirmed (medium)Re-ran the independent reproduction's JUnit with a new seed: 5 of 6 fail with -ea and -da, at CompletionTermWriter.write(:219) from flush or merge.
registry re-run and audit keepThe finder's JUnit fails 4 of 4 with -ea and -da (three seeds); the control passes; the independent reproduction's fails 5 of 6.
this doc's brief reproduced, one fix validatedA new 8-test JUnit fails 6 of 8 on main (gradle and the standalone runner, -ea, -da) and 10.5.2; the fix passes it and the whole suggest module with assertions on and off.
fact check ok after three wording fixesRe-ran the brief's gradle command on a clean extraction (6 of 8 fail, assertions on and off), the standalone runner on main and on 10.5.2 with assertions off, and the fix (8 of 8; the module's 244 tests). Its own probe executed addIndexes(CodecReader), close() and the NRT reopen. Found three wording errors, fixed here.
cross-model fact check core bug soundAn independent reviewer from another model family (OpenAI GPT-5.6) checked the brief read-only against a reference pack; a separate run ran a public-API probe on stock and fixed jars with assertions on and off (soft-update and hard-update cases included). Its wording findings are fixed here.

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,572 words

Finding record

LFV-787, snapshot of 2026-11-06

Registry title
Mixing SuggestField and ContextSuggestField under one field name is accepted by addDocument and rejected only when the completion postings are written at flush or merge, where the IllegalArgumentException is tragic: the IndexWriter closes and loses every uncommitted document, and two cleanly committed segments of the two types make every merge that combines them close the writer again, also in a new writer (released since 5.3.0)
Area
suggest / document suggester (CompletionFieldsConsumer type check at flush and merge; core flush and merge abort paths)
Priority
Low, rank 321: A loud failure in the opt-in document suggester caused by mixing two suggest field classes under one name.
Impact class
index unusable
Impact note
Applications of the opt-in document suggester (a field routed to the completion postings format through a per-field codec) that index both SuggestField and ContextSuggestField documents under one field name, by mistake or when they add contexts to an existing suggest field, lose their IndexWriter at the next flush if both types are buffered together (every document added since the last commit is lost, unrelated ones included), and if the types sit in separately committed segments (both flush and commit cleanly, CheckIndex is clean) every merge that combines them, including a natural background merge with an otherwise default IndexWriterConfig, closes the writer again in every new writer until all documents of one type are deleted; committed data stays readable and nothing is corrupted; no scale needed; every release from 5.3.0 to 10.6.0 (10.6.0 by reading) and main.
Summary
You index a SuggestField document and a ContextSuggestField document under the same field name; addDocument accepts both, and the suggest module's design (LUCENE-6459) says such a mix is rejected with an IllegalArgumentException, so you expect an input error that leaves the writer and your other documents alone. Instead the type is compared only in CompletionFieldsConsumer while postings are written, so the IllegalArgumentException 'single field name has mixed types' fires inside flush or merge, where IndexWriter treats every exception as tragic: the writer closes and drops every uncommitted document. If the two types landed in different segments, both commit cleanly, but every merge that combines them closes the writer, also in any new writer, until you delete all documents of one type.
Introduced
be4d58c7740bea4e797eb1acff6355eb260e7c93 (trunk, svn r1682158) and its branch_5x commit 380bb29f395 (svn r1682170, shipped in 5.3.0): LUCENE-6459 'Query Interface for suggest API' (now apache/lucene#7518; committed by GitHub mikemccand for GitHub areek; no PR, svn era). It added ContextSuggestField (type byte 1) next to SuggestField (type byte 0), the type byte in every completion posting payload, the only type comparison (CompletionTermWriter.write, at flush and merge) and its only test, testMixedSuggestFields [read: git log -S on the message finds only this commit; git blame, also with -w -C and ignoring the LUCENE-9570 reformat, puts every line of today's check on it]. Present from the first day of the API; not a regression (2015-05-28 (trunk and branch_5x commits))
First affected version
5.3.0 [exec: the follow-up's public-API main() probe shows all four scenarios closing the writer, -ea and -da; the four-scenario JUnit fails 4 of 4]; 5.2.1 and earlier have no ContextSuggestField (compile error, API absent, not a result about the bug). Correction to the finder's 'since 5.2': the branch_5x commit is not an ancestor of the 5.2.0 or 5.2.1 tags, and CHANGES.txt lists LUCENE-6459 under 5.3.0
Affected versions
Every release from 5.3.0 through 10.6.0 (10.6.0 by reading) and main ea57360912ed and a9779ef3c193; never fixed (upstream main f7bcc0abd816, branch_10x, branch_10_6 and branch_9x carry the same check; no open PR touches CompletionFieldsConsumer, SuggestField, ContextSuggestField or CompletionQuery, read-only check 2026-10-27). [exec] follow-up main() probe and JUnit: FAIL on 5.3.0, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.12.3, 10.5.2 and main a9779ef3c193 (-ea; -da also on 5.3.0 and main), same-type controls pass; finder's JUnit fails 4 of 4 on main (-ea and -da), 10.5.2, 10.0.0, 9.12.3, 9.0.0 and 8.11.2 (version-portable copy); the independent reproduction's JUnit fails 5 of 6 on main (-ea and -da), 10.5.2 and 9.12.3 (-ea and -da); the intended-behaviour check re-ran the finder's JUnit on main and 10.5.2. [read] all 94 release tags 5.3.0-10.5.2 in the history clone contain the check unchanged in substance; the clone lacks the 8.8.2-8.11.x tags (8.11.4 executed). Releases 5.x-8.x ran on JDK 25, outside their supported JDKs; the failing path is plain Java and the controls pass on the same JVM
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
non-formal: code reading; also reached by exhaustive check
Upstream and prior reports
none found (2026-10-27, read-only; the prior-report record verdict new: 376 query records, namely the scripted prior-report search in five runs (finder, the intended-behaviour check and three follow-up runs, 51 queries each over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web), 75 GitHub GraphQL searches (apache/lucene, apache/lucene-solr, elastic/elasticsearch, opensearch-project/OpenSearch, apache/solr and all of GitHub, including the exact message 'single field name has mixed types': 0 results anywhere), 30 direct list queries, 4 JIRA queries, 9 web queries (one throttled), commits by path on main, branch_10x, branch_10_6 and branch_9x, and the changed files of all 409 open apache/lucene PRs; the full review threads of #7518, #7400, #8613, #7637, #7049 and others read directly; 18 hits read, none the same bug). Closest items, not reports: #7518 / LUCENE-6459 (introduced the check; records only that a mix is invalid and throws IllegalArgumentException 'upon indexing', nothing on the writer closing, lost documents or merges), #7400 / LUCENE-6339 (the document suggester), #8613 / LUCENE-7562 (a different merge-time NullPointerException in CompletionFieldsConsumer on ghost fields, from elastic/elasticsearch#21500, fixed as a Lucene bug in 5.5.4 and 6.4), #7637 / LUCENE-6579 (made every merge exception tragic, same release 5.3.0), and the precedents #6724 / LUCENE-5662, #9487 / LUCENE-8441 and #15584 (validation errors moved out of aborting steps). Gaps: GitHub search does not index PR review comments, JIRA and list search are word-based, private reports are not visible
Lucene commit tested
main a9779ef3c193 (commit of 2026-10-26; tested 2026-10-27) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the 10.5.2 release jars (assertions on, and off in an independent fact check). A portable main() ran on 5.3.0, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.12.3, 10.5.2 and main; earlier JUnit runs covered 10.0.0, 9.12.3, 9.0.0 and 8.11.2. The fix was validated in the scratch worktree: the new test, the updated testMixedSuggestFields, the whole lucene/suggest module (244 tests) with assertions on and off, and :lucene:suggest:check. Later fact checks re-ran the repro and the fix and executed close(), the NRT reopen, addIndexes(CodecReader) and soft-update migrations with public-API probes.

Lucene and upstream sources

Finding record
  • lucene/suggest/src/java/org/apache/lucene/search/suggest/document/CompletionFieldsConsumer.java:85-113 (at a9779ef3c193) (write: delegateFieldsConsumer.write first, then one CompletionTermWriter per field), :195-227 (CompletionTermWriter.write reads the type byte of every posting payload; :214-220 the first posting fixes the type, a different one throws IllegalArgumentException 'single field name has mixed types')
  • lucene/suggest/src/java/org/apache/lucene/search/suggest/document/SuggestField.java:48 (at a9779ef3c193) (@lucene.experimental), :64, :123-124 (type() returns 0), :133 (type byte written into the payload); ContextSuggestField.java:44 (@lucene.experimental), :51, :106-107 (type() returns 1); no add-time comparison anywhere
  • lucene/suggest/src/java/org/apache/lucene/search/suggest/document/CompletionQuery.java:87-103 (at a9779ef3c193) (rewrite throws IllegalStateException '<field> has values of multiple types' when segments carry different types: the cross-segment state is handled cleanly at query time)
  • lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:585-589 (at a9779ef3c193) (flush: catch (Throwable) -> onAbortingException; maybeAbort in finally); IndexWriter.java:5109-5113 (merge: catch (Throwable) -> tragicEvent(t, "merge"))
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:162-165 (at a9779ef3c193) (class NOTE: the writer closes itself on an Error or a disaster during a checkpoint), :1465-1466 (addDocument: on an exception 'the index will be consistent, but this document may not have been added'); DocumentsWriter.java:67-79 (package-private class javadoc: aborting exceptions are the 'certain limited times when an exception can corrupt this state'; 'All other exceptions ("non-aborting exceptions")' only mark the document deleted; design intent, not public contract)
  • lucene/suggest/src/test/org/apache/lucene/search/suggest/document/TestContextSuggestField.java:158-176 (at a9779ef3c193) (testMixedSuggestFields, ':166 mixing suggest field types for same field name should error out', accepts the exception from addDocument or commit and never inspects the writer)
The finding in 30 seconds
  • lucene/suggest/src/java/org/apache/lucene/search/suggest/document/SuggestField.java:53-62, (at a9779ef3c193) :64, :79, :123-124, :127-138 (:133); ContextSuggestField.java:51, :106-107
  • lucene/suggest/src/java/org/apache/lucene/search/suggest/document/CompletionFieldsConsumer.java:85-113, (at a9779ef3c193) :105-110, :149, :195-227 (:214-220, throw :219)
  • lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:585-589, (at a9779ef3c193) :594-605; IndexWriter.java:5109-5113
  • lucene/suggest/src/java/org/apache/lucene/search/suggest/document/CompletionQuery.java:87-103 (at a9779ef3c193)
Who is affected
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:1363-1369 (at a9779ef3c193) (close() flushes only when commitOnClose is true)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:162-165, (at a9779ef3c193) :1465-1466; DocumentsWriter.java:67-79
  • lucene/suggest/src/test/org/apache/lucene/search/suggest/document/TestContextSuggestField.java:158-176 (at a9779ef3c193)
  • lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2095-2101; (at a9779ef3c193) lucene/core/src/test/org/apache/lucene/index/TestCustomTermFreq.java:125 (at a9779ef3c193) (#15584: new validation non-aborting by design)
Suggested fix
  • lucene/suggest/src/java/org/apache/lucene/search/suggest/document/CompletionFieldsProducer.java:88-89 (at a9779ef3c193) (one constant as minimum and current version), :115 (type byte read without validation); CompletionPostingsFormat.java:95, :102-103; CompletionTerms.java:39-44; CompletionQuery.java:107-113 (ContextQuery rejects only type 0), :114-117 (plain query wrapped as ContextQuery)
  • lucene/core/src/java/org/apache/lucene/index/FieldInfo.java:662-672 (at a9779ef3c193) (attributes not validated across documents)
History
  • lucene/CHANGES.txt:10575-10577 (at a9779ef3c193) (LUCENE-6579 under 5.3.0)

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

Repro files

About the diagrams

A picture of the repro's merge case, made in parallel with this doc from the executed repro on main a9779ef3c193 and the code; the PNG is the final summary card. The alt text describes the intended content.

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.