All findings · Lucene finding · LFV-729 · facets-drilldown-none-dim-breaks-shared-index-field-schema

A DrillDownTermsIndexing.NONE facet dimension in the shared index field makes IndexWriter reject documents since Lucene 9.0

Since Lucene 9.0, DrillDownTermsIndexing.NONE on one facet dimension in the default shared field $facets makes IndexWriter reject a document with only that dimension after one with any other, and vice versa (one sorted-set order slips through on 10.5+ via another bug). After a commit, the rejection lasts until a reindex.

Summary

Priority
Low, rank 367A loud IllegalArgumentException behind the opt-in DrillDownTermsIndexing.NONE, with a setIndexFieldName workaround; nothing is silently wrong.
Impact
wrong error
Since
9.0.0 (2021), via LUCENE-9334introduced 2021-04-14 · about 5 years
Affected
9.0.0 to 10.6.0 (10.6.0 by reading) and main; opt-in NONE in a shared index field
Status
documented
Found by
a bounded-exhaustive check of FacetsConfig option combinations against a model of the javadoc

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

FacetsConfig writes each dimension's doc values and drill-down terms into the dimension's index field, $facets for every dimension by default. NONE skips the terms but keeps the doc values, so how $facets is indexed in a document depends on which dimensions that document has.

Summary card. Title: FacetsConfig: a DrillDownTermsIndexing.NONE dimension in a shared index field makes IndexWriter reject the second kind of document (Lucene 9.0.0 and later). Example: setDrillDownTermsIndexing("rating", NONE), author at the default ALL, both in $facets; doc 0 has only rating, doc 1 has author. Step 1: config.build(taxoWriter, a): rating is NONE, so $facets gets only SortedNumericDocValuesField, index options NONE (indexDrillDownTerms adds no StringField for NONE, FacetsConfig.java:419-460). Step 2: config.build(taxoWriter, b): author is ALL, so $facets also gets two StringField drill-down terms, index options DOCS; build() puts every dimension's doc values and terms into its indexFieldName (:278-313), $facets by default (:126-130). Step 3: since 9.0 IndexWriter requires one schema per field, so the second kind of document is rejected (IndexingChain.java:2345-2346 in a segment, FieldInfo.java:276-286 across the index, LUCENE-9334). Table, expected vs actual on 9.0.0 to main: rating doc then author doc (taxonomy or sorted-set): expected both indexed, actual author doc: expected 'NONE', but it has 'DOCS'; author doc then rating doc (taxonomy): rating doc: expected 'DOCS', but it has 'NONE'; rating doc, commit(), new IndexWriter, author doc: every author doc: cannot change field "$facets". Lucene 8.9.0 to 8.11.4 index both documents in either order and after a commit. Workaround: config.setIndexFieldName("rating", "rating_facets"): both documents indexed. Why: FacetsConfig.build emits two schemas for one field from documented per-dimension settings and does not check this; nothing is corrupted and no result is wrong. Affected: every release from 9.0.0 to 10.5.2 and main; on 10.5.0 and later one sorted-set order is accepted because of another bug (LFV-118).
The example on main: config.build gives doc 0 (rating only) a $facets with doc values only and doc 1 (author) drill-down terms too, so IndexWriter rejects the second kind. The table adds the reverse order and the commit case; 8.9.0 to 8.11.4 and the workaround index both documents. Select the still to open it at full size. Generated with AI, checked against the code.
  1. You call config.setDrillDownTermsIndexing("rating", NONE) and leave author at the default ALL. Both dimensions stay in the default index field, $facets.
  2. You index doc 0 with only FacetField("rating", "5") and doc 1 with only FacetField("author", "Bob"). You expect 2 documents, as on 8.9.0 to 8.11.4, and as on main when rating has its own index field.
  3. config.build gives doc 0 a $facets with doc values only (index options NONE), because NONE adds no drill-down terms; doc 1's $facets also gets two drill-down terms (index options DOCS).
  4. Since 9.0 a field must be indexed the same way in every document, so the second addDocument throws IllegalArgumentException "... field [$facets] of doc [1]. index options: expected 'NONE', but it has 'DOCS'" and numDocs stays 1. Sorted-set and association facets fail the same way, and with taxonomy facets so does the reverse order.
  5. After a commit, a new IndexWriter rejects every author document ("cannot change field "$facets" from index options=NONE to inconsistent index options=DOCS") until deleteAll() or a reindex.

Who is affected

Applications that set DrillDownTermsIndexing.NONE on some, not all, dimensions of one index field (the default $facets), with taxonomy, sorted-set or association facets, as soon as one document has only NONE dimensions there. Opt-in, any index size. The error depends on the data, so it can first appear in production. Workaround: setIndexFieldName.

Mechanism, terms and numbers854 words

FacetsConfig lets you switch off drill-down terms per dimension with setDrillDownTermsIndexing(dim, DrillDownTermsIndexing.NONE), and by default every dimension writes to the same index field, $facets. A NONE dimension still writes its doc values into that field but no StringField terms, so a document whose dimensions in the field are all NONE gives $facets index options NONE, while a document with any other dimension gives it DOCS. Since Lucene 9.0 (LUCENE-9334) a field must be indexed the same way in every document, so IndexWriter.addDocument / updateDocument throws IllegalArgumentException for the second kind of document: with the NONE-only document first for taxonomy, sorted-set and association facets alike, and in either order for taxonomy facets (one sorted-set order is accepted on 10.5.0 and later only because of a separate core bug). Once one kind is committed, a new IndexWriter keeps rejecting the other kind until the index is cleared with deleteAll() or rebuilt. Nothing is corrupted and no result is wrong, but the failure depends on the data, so it can first appear in production. The same layout works on 8.9.0 to 8.11.4, the releases that introduced NONE, and it is the layout of upstream's own NONE tests, which pass only because every test document also has a term-indexing dimension. The workaround, setIndexFieldName for the NONE dimensions, is not documented. Two fixes in FacetsConfig were validated: a fail-fast check with javadoc (recommended, no format change), and an indexed no-terms field that keeps the field's schema uniform (makes the layout work, but changes how such documents are indexed).

Terms

  • Dimension and index field. A facet dimension (rating, author) has a DimConfig. Its indexFieldName is the Lucene field that receives the dimension's doc values (taxonomy ordinals, sorted-set labels or association bytes) and its drill-down terms. It defaults to DEFAULT_INDEX_FIELD_NAME = "$facets", the field that "holds the drill-downs and ords (as doc values)" (FacetsConfig.java:61-62, :126-130).
  • Drill-down terms. StringField terms such as author and author\u001FBob that DrillDownQuery searches. DrillDownTermsIndexing picks which ones a dimension writes, per dimension (setDrillDownTermsIndexing(dimName, ...), :216-226); NONE means "Index no drill down terms" (:74-79). The default is ALL.
  • Per-field schema. Since 9.0 (LUCENE-9334) every document must index a field with the same index options and doc-values type. Doc values alone mean index options NONE; a StringField means DOCS.

The bug

  1. build() groups the facet fields by index field and calls the taxonomy, sorted-set or association path, which adds the doc values to indexFieldName (:412, :480-497, :532) and calls indexDrillDownTerms for each dimension (:400, :501, :530).
  2. indexDrillDownTerms adds StringField(indexFieldName, ...) terms for every mode except NONE: if (dimConfig.drillDownTermsIndexing != DrillDownTermsIndexing.NONE) { (:419-460, :421).
  3. So a document whose dimensions in $facets are all NONE has only doc values there (index options NONE); any other document has doc values plus terms (DOCS). Inside one document the core merges the two (IndexingChain.java:2278-2289), so a document with both dimensions is fine.
  4. Across documents the core compares: assertSameSchema in a segment (IndexingChain.java:2345-2346, message at :2233-2234, :2258-2272) and verifySameIndexOptions across segments and after a commit (FieldInfos.java:511-515, FieldInfo.java:276-286). The second kind of document is rejected (from 10.5.0 a frozen-field-type cache can skip the in-segment check, IndexingChain.java:621-637, :1876-1896, which is why one sorted-set order is accepted; LFV-118). build() checks none of this; it does reject mixed association types in one field, with "use FacetsConfig to change the indexFieldName for each dimension" (:316-337).

The numbers

Executed on main a9779ef3c193, assertions on and off (identical); rating is NONE, author the default, both in $facets. The 8.x column and the per-release results come from a version-independent main() that records only accept or reject; counts and drill-down were checked on main and 10.5.2:

what you index8.9.0 to 8.11.49.0.0 to main
taxonomy: rating-only doc, then author docboth indexed2nd rejected: "... field [$facets] of doc [1]. index options: expected 'NONE', but it has 'DOCS'."
taxonomy: author doc, then rating-only docboth indexed2nd rejected: "... expected 'DOCS', but it has 'NONE'."
sorted-set: rating-only doc, then author docboth indexed2nd rejected: "... expected 'NONE', but it has 'DOCS'."
rating-only doc, commit(), new IndexWriter, author docboth indexedrejected: "cannot change field "$facets" from index options=NONE to inconsistent index options=DOCS"
controls: rating in its own field; no NONE; every dimension NONEboth indexed (the first two; every-dimension-NONE not run on 8.x)both indexed

Two IntAssociationFacetField dimensions sharing one field, one of them NONE, fail the same way with the NONE-only document first (the only order run). After the commit case, rating-only documents still go in and every author document is rejected; deleteAll() resets the schema. On 10.5.0 and later one order, a sorted-set author document and then a rating-only document in the same in-memory segment, is accepted, but only because of a separate core bug (LFV-118, a frozen-field-type validation cache); 9.0.0 to 10.4.0 reject it.

Exposure and likelihood313 words
  • Who. Applications that set DrillDownTermsIndexing.NONE on some, but not all, dimensions mapped to one index field, by default $facets, with taxonomy (FacetField), sorted-set (SortedSetDocValuesFacetField) or association facets (all three executed; for associations only IntAssociationFacetField, whose code path the float and bytes variants share). Ordinary sparse data triggers it: one document whose dimensions in the field are all NONE, and another with a term-indexing dimension there.
  • Effect. addDocument / updateDocument throws a clean, non-aborting IllegalArgumentException and the document is not added as a live document. Nothing is corrupted and no search result is wrong. As with any non-aborting exception, IndexWriter still uses up a document ID for it and marks it deleted (DocumentsWriterPerThread.java:283-291); with taxonomy or association facets, build() has already added its labels to the taxonomy index (FacetsConfig.java:383, :517), where they stay unused, and deleteAll() on the main index does not remove them. Whichever kind of document is committed first decides which kind is rejected afterwards, until deleteAll() or a reindex.
  • Default vs opt-in. NONE is opt-in. Once it is set, the failing layout is the default one, and it is the layout of upstream's own NONE tests.
  • Scale. None needed: two documents, deterministic, assertions on and off.
  • Not run. x86, concurrent writers, and a dedicated JUnit for a hierarchical NONE dimension (the bounded harness covered hierarchical configurations). All runs were on Linux aarch64.
  • Workaround. setIndexFieldName for each NONE dimension. It is not free: counting then needs a separate Facets instance or SortedSetDocValuesReaderState per index field. The setIndexFieldName javadoc says the field name is "only used by the taxonomy based facet methods" (FacetsConfig.java:203-206), which is stale and hides the workaround from sorted-set users.
  • How likely. Plausible, not common: one downstream code base found exposes NONE per field with the index field optional; nobody has reported it since 9.0.0 (2021).

Minimal repro

The test shows that with FacetsConfig.setDrillDownTermsIndexing("rating", NONE) and every dimension in the default $facets field, a sorted-set facet document with only "rating" followed by one with only "author" makes IndexWriter reject the second document, because FacetsConfig gives $facets two different schemas.

TestDrillDownNoneSharedIndexField.java (JUnit test)46 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.facet;

import org.apache.lucene.document.Document;
import org.apache.lucene.facet.sortedset.SortedSetDocValuesFacetField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestDrillDownNoneSharedIndexField extends LuceneTestCase {

  public void testNoneOnlyDocThenDefaultDimDoc() throws Exception {
    FacetsConfig config = new FacetsConfig();
    config.setDrillDownTermsIndexing("rating", FacetsConfig.DrillDownTermsIndexing.NONE);
    try (Directory dir = new ByteBuffersDirectory();
        IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
      Document rating = new Document();
      rating.add(new SortedSetDocValuesFacetField("rating", "5"));
      w.addDocument(config.build(rating));
      Document author = new Document();
      author.add(new SortedSetDocValuesFacetField("author", "Bob"));
      try {
        w.addDocument(config.build(author));
      } catch (IllegalArgumentException e) {
        throw new AssertionError("expected the author document to be indexed, but got: " + e, e);
      }
    }
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-729/, 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/facet/build/libs/lucene-facet-$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.facet.TestDrillDownNoneSharedIndexField
# the module(s) facet also need their third-party runtime jars
Expected vs actual
Expected: both documents are indexed. Actual: the second addDocument throws IllegalArgumentException: Inconsistency of field data structures across documents for field [$facets] of doc [1]. index options: expected 'NONE', but it has 'DOCS'.

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

More files, environment, full output88 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: expected the author document to be indexed, but got: java.lang.IllegalArgumentException: Inconsistency of field data structures across documents for field [$facets] of doc [1]. index options: expected 'NONE', but it has 'DOCS'.
       org.apache.lucene.facet.TestDrillDownNoneSharedIndexField.testNoneOnlyDocThenDefaultDimDoc(TestDrillDownNoneSharedIndexField.java:42)
     Caused by: java.lang.IllegalArgumentException: Inconsistency of field data structures across documents for field [$facets] of doc [1]. index options: expected 'NONE', but it has 'DOCS'.
       org.apache.lucene.index.IndexingChain$FieldSchema.raiseNotSame(IndexingChain.java:2259)
       org.apache.lucene.index.IndexingChain$FieldSchema.assertSame(IndexingChain.java:2254)
       org.apache.lucene.index.IndexingChain$FieldSchema.assertSameSchema(IndexingChain.java:2346)
       org.apache.lucene.index.IndexingChain.initAndValidateFields(IndexingChain.java:695)
       org.apache.lucene.index.IndexingChain.processDocument(IndexingChain.java:641)
       [...]

Why it looks unintended

  • NONE is a documented per-dimension option, all dimensions share $facets by default, and nothing says a NONE dimension needs its own field.
  • Upstream's own NONE tests use this shared layout; they pass only because every test document also has a term-indexing dimension.
  • It works on 8.9.0 to 8.11.4, which introduced NONE, and broke in 9.0 (LUCENE-9334).
The full argument280 words
  • The contract. NONE: "Index no drill down terms" (FacetsConfig.java:74-79); the setter takes a dimension name (:216-226); every dimension defaults to $facets (:126-130). The demo's facet package docs: "FacetsConfig has a per-dimension config of the Category Terms field, e.g. you can choose to not index them at all" (package-info.java:234-238, the "Category Terms Field" section). Their "Multiple Category Lists" section says one field for all category-list (doc values) information "will suit most applications" (:355-357); that supports the shared default layout, but it is about the doc values, not the drill-down terms. No javadoc, CHANGES entry or MIGRATE note asks for a separate field.
  • Upstream's tests. TestDrillDownQuery.testDrillDownTermsDifferentOptions puts NONE dimension a in $facets next to term-indexing dimensions b to e; TestSortedSetDocValuesFacets.testDrillDownOptions and testHierarchicalDrillDownOptions put NONE dimension c next to d to g. Every test document carries a term-indexing dimension, so they never build a NONE-only document. Two came with the feature (LUCENE-9385); testHierarchicalDrillDownOptions was added in 2022 by LUCENE-10250 (#509, first released in 9.1.0), after LUCENE-9334, with the same shared layout. They are still the only upstream tests that set NONE.
  • History. LUCENE-9385 (#25) added NONE on 2021-03-27 (8.9.0); LUCENE-9334 (#11) made the core check exact on 2021-04-14 (9.0.0). Before it, the core silently upgraded a doc-values-only field to the indexed options. Neither PR discussion mentions the other.
  • Precedent. LUCENE-9334 broke another facet layout, the taxonomy index's full-path field (LUCENE-9450, #10490); upstream fixed that in the facet module before 9.0.0.
  • The module's convention. build() already rejects mixed association types in one field and tells the user to change the indexFieldName (:316-337). NONE has no such check.
The devil's-advocate check246 words

The reviewers built the strongest case for "works as designed" (the refute-lens reviewer rated it strong, including its reading that applications must keep the dimensions of one field compatible, and still judged the bug real with the defect in FacetsConfig; the intended-lens reviewer rated it moderate; the devil's advocate found it does not hold): LUCENE-9334 deliberately fixes one schema per field (CHANGES: "A field across all documents within an index must be indexed with the same index options and data-structures"); in a 2021 dev@ thread a maintainer confirmed that documents with terms and doc values on a field require later documents to have "both terms and doc values on this field too, or nothing"; FacetsConfig is @lucene.experimental; setIndexFieldName exists; in LUCENE-7518 (2016) a maintainer wrote "One must use different field names" for taxonomy vs sorted-set doc values; and DEFAULT_INDEX_FIELD_NAME's javadoc says the field holds the drill-downs.

Why it fails: the core rule is not in dispute. The defect is that FacetsConfig turns a documented per-dimension option and its own default field into documents the core must reject, with no validation or caveat. LUCENE-7518 was a static conflict between two facet methods, hit by the first mixed document; here one method and one option give a conflict that depends on which dimensions each document carries. @lucene.experimental allows API changes, not an option that silently needs an undocumented layout. These points shape the fix, not whether it is a bug. Verdict: not intended, a defect.

Suggested fix

Recommended Fail fast in FacetsConfig.build and document the rule

Fail fast and document: FacetsConfig.build remembers, per index field, whether its documents had drill-down terms and rejects a document of the other kind with an error that names the field and says to change its indexFieldName, like its existing check for mixed association types. The NONE javadoc states the rule. Its 5 tests and the whole facet module pass.

for (Map.Entry<String, Boolean> ent : hasDrillDownTerms.entrySet()) {
  Boolean previous = drillDownTermsByField.putIfAbsent(ent.getKey(), ent.getValue());
  if (previous != null && previous.booleanValue() != ent.getValue().booleanValue()) {
    throw new IllegalArgumentException(
        "indexed field \"" + ent.getKey() + "\" would get drill down terms in some documents"
            + " and none in others, because dimensions that use DrillDownTermsIndexing.NONE"
            + " share it with dimensions that index drill down terms; a field must be indexed"
            + " the same way in every document, so use FacetsConfig to change the"
            + " indexFieldName for the NONE dimensions");
  }
}
All options and trade-offs4 options ยท 1,021 words

Both options A and B were applied to a scratch worktree of main a9779ef3c193 and validated through gradle: the repro, their own or the hunt's tests, the whole lucene/facet module, the demo facet examples, and the lint, format and javadoc checks. A is recommended because it changes no index format and can be backported, though it is not behaviour-preserving in every case (see its trade-offs); B is the option that makes the shared layout work, at the cost of a compatibility break that was executed. C and D were not run.

Fail fast in FacetsConfig.build and document the rule (validated by the checks) (recommended)

+43/-2 in FacetsConfig.java. build() notes, per index field, whether this document indexes drill-down terms into it (any dimension there not NONE); a per-instance map keeps the first answer per field, the same best-effort pattern as the existing association-type check (assocDimTypes). A document with the other answer is rejected before any taxonomy ordinal is added. A NOTE on NONE states the rule and points to setIndexFieldName; the stale setIndexFieldName javadoc is corrected.

for (Map.Entry<String, Boolean> ent : hasDrillDownTerms.entrySet()) {
  Boolean previous = drillDownTermsByField.putIfAbsent(ent.getKey(), ent.getValue());
  if (previous != null && previous.booleanValue() != ent.getValue().booleanValue()) {
    throw new IllegalArgumentException(
        "indexed field \"" + ent.getKey() + "\" would get drill down terms in some documents"
            + " and none in others, because dimensions that use DrillDownTermsIndexing.NONE"
            + " share it with dimensions that index drill down terms; a field must be indexed"
            + " the same way in every document, so use FacetsConfig to change the"
            + " indexFieldName for the NONE dimensions");
  }
}

Trade-offs

  • No index-format change; backportable to 10.x. Not behaviour-preserving in every case: it also rejects the sorted-set order "both dimensions, then a NONE-only document" in one in-memory segment, which stock 10.5.0+ and main accept only because of LFV-118 (the independent reproduction's test, now 5 of 8 failing), plus the shared-config cases below.
  • It does not make the shared layout work: the error still comes when the second kind of document arrives, now from build(), before IndexWriter and the taxonomy see it, with a message that names the field and the remedy.
  • Best effort: the map sees only documents this FacetsConfig built. After a restart against an index that already holds the other kind, the core's "cannot change field" error remains (the javadoc then explains it). One FacetsConfig shared by several indexes, or reused after deleteAll(), could reject a document that a particular index would accept.
  • The map records a document's answer when build() runs, not when IndexWriter accepts it: a document that is built but never added, or that fails later (a null taxonomy writer, or an IndexWriter rejection for another reason), still fixes the field's answer, and with several index fields the earlier fields' answers are recorded before a later field throws.
  • A stricter, deterministic check at configuration time ("NONE must not share a field with a term-indexing dimension") would break layouts that work today: upstream's own NONE tests, and indexes where every dimension in $facets is NONE.

What the checks validated

  • Its test TestFacetsConfigDrillDownNoneCheck (5 tests: both orders rejected by build() with the new message; controls: every document has a term-indexing dimension, every dimension NONE, NONE in its own field) passes with assertions on and off.
  • The repro, the finder's and the independent reproducer's tests now fail inside build() with the new message: 12 failures across the three files, none from IndexWriter.
  • The whole lucene/facet module with the new test: 311 tests, 0 failed, 8 skipped. Demo facet examples: 19 tests, 0 failed. ecjLint, checkGoogleJavaFormat and renderJavadoc: BUILD SUCCESSFUL.

Keep the field's schema uniform with an indexed field that has no terms (validated by the checks)

+66/-3 in FacetsConfig.java. When all of a document's dimensions in an index field are NONE and some dimension may index drill-down terms into that field (the default DimConfig's field, or a configured term-indexing dimension's field), build() adds new Field(indexFieldName, <empty TokenStream>, <StringField's index options, tokenized>). The field is then indexed (DOCS) in every document, and the NONE dimensions still add no terms. Same javadoc fixes.

Trade-offs

  • Makes the documented default layout work, with counts and drill-down unchanged.
  • Compatibility break, executed: an index written before the fix in which the shared field never had terms (for example every dimension in $facets is NONE) accepts more documents on stock, but after the fix its next NONE-only document is rejected with "cannot change field "$facets" from index options=NONE to inconsistent index options=DOCS". A new index with that layout works with the fix. So it needs a MIGRATE entry and suits a major release, not a 10.x backport.
  • Adds an unusual empty indexed field; reviewers may prefer a fail-fast rule.
  • Not measured: indexing cost, RAM, segment metadata or merge effects of the empty field. Its predicate looks at the configuration, not the index: it adds the field when the default DimConfig or any configured term-indexing dimension maps to that field, even if no such dimension ever appears in the index (the compatibility case above).

What the checks validated

  • The repro (7), the finder's (5) and the independent reproducer's (8) tests: 20 of 20 pass, assertions on and off.
  • The whole lucene/facet module with the three repro files: 326 tests, 0 failed, 8 skipped. Demo facet examples: 19 tests, 0 failed. Lint, format and javadoc: BUILD SUCCESSFUL.
  • Compatibility probe (sorted-set facets, every dimension NONE, on disk): stock then stock accepted; fix then fix accepted; stock then fix rejected, as above.

Route NONE dimensions' doc values to a derived field

Write a NONE dimension's doc values into a separate, derived field, so the shared field never has a doc-values-only occurrence.

Trade-offs

Not run. Changes where the counts of those dimensions are read (FastTaxonomyFacetCounts and DefaultSortedSetDocValuesReaderState read one field), so it needs search-side changes and changes the layout of existing indexes. Not recommended.

Documentation only

The javadoc part of the fail-fast option: a NOTE on NONE and the corrected setIndexFieldName javadoc.

Trade-offs

Cheapest; leaves the late, data-dependent core error as the only signal.

Downstream note

Of the downstream code bases found that call setDrillDownTermsIndexing, one exposes NONE per field with the index field optional, so it can hit this; another server gives each facet field its own index field by default.

How it was found

A bounded-exhaustive harness ran combinations of FacetsConfig options for two dimensions (300 configurations, one or two documents each, 265,819,726 checks) against a reference written from the javadoc. It flagged "build rejected a legal document" in 41 configurations, each with a NONE and a term-indexing dimension in one field; a public-API JUnit confirmed it.

The method in full241 words

Method: semi-formal, bounded-exhaustive translation validation of the FacetsConfig option matrix, in a hunt unit on FacetsConfig and drill-down indexing (2026-10-27, main a9779ef3c193).

  • Harness. For two dimensions with independently drawn options (drill-down mode, hierarchical, multi-valued, dimension count), taxonomy or sorted-set facets and three field layouts (shared $facets, shared custom field, separate fields), it indexed every multiset of one or two documents of up to two labels each, in four segment layouts (one or two segments, with or without a deleted document), with the stock jars, and compared accept/reject decisions, counts, drill-down and drill-sideways results with a reference model written from the javadoc. Mixed tier: 300 seeded configurations, 265,819,726 checks, assertions off.
  • Signal. The class "build rejected a legal document" appeared in 41 of the 300 configurations, in no separate-field or same-mode one. Each of the 41 has one dimension with NONE and the other with a term-indexing mode in a shared field, and they are all such configurations the oracle checked: it counts a rejection only when dimension a is multi-valued, and 76 of the 300 configurations mix the two modes in a shared field.
  • Witness and checks. The finder then wrote a public-API JUnit on the stock jars, and the claim went through an independent reproducer, reviews by a model from another family (OpenAI GPT-5.6, refute and intended lenses), an intended-behaviour devil's advocate, an adjudication and a credibility audit. Bounded scope only; no model checker or prover was involved.

Prior reports, history and checks

Prior reports

No prior report found (2026-10-27: 291 logged queries over GitHub issues, PRs, commits and review threads, JIRA, four Lucene lists, three downstream projects, Q&A sites and the web). Closest: the two changes that interact (LUCENE-9385, LUCENE-9334), LUCENE-9450 (a similar facet conflict fixed before 9.0) and LUCENE-7518.

Searches4 searches

Verdict new: 20 hits read, none the same bug. Closest, all different: #10425 / #25 / lucene-solr#2471 (LUCENE-9385, the option; nobody discusses dimensions with different modes sharing a field); #10374 / #11 (LUCENE-9334, the core rule; no mention of FacetsConfig or drill-down terms; the facet module comes up only for the taxonomy back-compat test, tracked as #10490); #10490 (LUCENE-9450, a precedent fixed in the facet module before 9.0); the dev@ thread "Are the new index consistency checks too strict?" (2021-09, the same core rule in an application's own fields; the rule was kept, so the fix belongs in FacetsConfig); LUCENE-10082 (better schema error messages); #8569 (LUCENE-7518, "One must use different field names" for taxonomy vs sorted-set doc values). Open PRs touching FacetsConfig.java (#11780, #12862) do not touch drill-down indexing. Keyword search cannot prove absence.

What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene issues, PRs and commits; Elasticsearch, OpenSearch and Solr; the dev@, java-user@, issues@ and commits@ lists; JIRA; the web2026-10-27the hunt's prior-report tool in three runs plus a web re-run (facet names, core schema names, docs and options): 214 queriesno report; the feature PR #25 and word-overlap hits only
GitHub by hand (apache/lucene, lucene-solr, Elasticsearch, OpenSearch, Solr, a downstream server, all of GitHub, code search)2026-10-2743 manual searches; code search for downstream users of setDrillDownTermsIndexingonly #25 names the option; one downstream code base exposes NONE per field, with no issues filed
Review threads and open PRs2026-10-27the review comments of #25, #11 and 7 related PRs read; changed files of all 410 open PRsnobody discusses mixed modes in one field; #11780 and #12862 touch FacetsConfig.java but not drill-down indexing
Mailing lists, JIRA archive, Q&A sites2026-10-2719 list-archive queries (6 threads read), all 67 facet-module JIRA issues since 2021-03-01, 9 Q&A queriestwo threads on the core rule (no facets); no facet report

History

Introduced in 9.0.0 by LUCENE-9334 (#11, d03662c48bf, merged 2021-04-14), which made the per-field schema check exact; NONE came 18 days earlier (LUCENE-9385, #25, released in 8.9.0). Executed: 8.9.0 to 8.11.4 pass, all 33 releases from 9.0.0 to 10.5.2 and main fail; the commit bisect pins d03662c48bf.

Versions tested10 rows

Introduced by d03662c48bf "LUCENE-9334 Consistency of field data structures" (apache/lucene#11, issue #10374, merged 2021-04-14, first released in 9.0.0, never backported to 8.x). Trigger 3648a1020a8 "LUCENE-9385: Add FacetsConfig option to control which drill-down terms are indexed for a FacetLabel (#25)" (merged 2021-03-27; 8.x backport e812a508590, lucene-solr#2471, 8.9.0) added NONE and indexDrillDownTerms, unchanged on main. Before LUCENE-9334 the core silently upgraded a doc-values-only field to the indexed options, so the layout worked. An unnoticed interaction of two changes 18 days apart; the defect is in the facet layer. branch_10x and branch_10_6 carry the same code; nothing upstream has changed it since (the 4 main commits after a9779ef3c193 as of 2026-10-27 touch neither lucene/facet nor the core check). Not run: 8.9 to 8.11 also accept the two kinds in different segments, and MIGRATE.md:1566-1567 says an older index that breaks the per-field rule "can not be updated", so such indexes may need a rebuild on 9.x.

Versions and branches executed, with results
Version or commitVerdictResult
8.8.2 API absentcompile error: no DrillDownTermsIndexing (older releases predate the option)
8.9.0, 8.10.0, 8.10.1, 8.11.0 to 8.11.4 not affectedthe option exists; every document accepted in every order and after a commit (8.9.0 and 8.11.4 also with assertions off)
9.0.0 (first with LUCENE-9334) affectedtaxonomy both orders, sorted-set both orders and the commit case rejected; controls pass (also with assertions off)
9.1.0 to 9.12.3, 10.0.0 to 10.4.0 affectedthe same; the finder's JUnit fails 3 of 5 on 9.1.0, 9.12.3 and 10.0.0
10.5.0 to 10.5.2 affectedthe same, except the sorted-set order term-document-first is accepted (LFV-118); this doc's test fails 4 of 7 on 10.5.2
main a9779ef3c193 affectedthis doc's test fails 4 of 7 with assertions on and off, 40 of 40 over 10 iterations
main 3648a1020a8 (LUCENE-9385) and 79f14b17429 (parent of LUCENE-9334) bisectevery document accepted (3648a1020a8 with assertions on; 79f14b17429 on and off)
main d03662c48bf (LUCENE-9334) bisectrejected, with assertions on and off; the commit touches no facet source
fail-fast fix fixthe second kind rejected by FacetsConfig.build with a message naming the field and indexFieldName
no-terms-field fix fixevery repro passes; an index written before the fix with a terms-free shared field rejects new NONE-only documents

Independent checks

An independent reproducer, reviews by a model from another family (OpenAI GPT-5.6), an intended-behaviour devil's advocate, an adjudication and a credibility audit each reproduced it with their own or re-run tests on main, assertions on and off, controls passing; this doc's gradle runs reproduce it too.

Checks and verdicts373 words

Every executing check wrote or re-ran a public-API test on stock main; every control passed. The refute-lens review rated the works-as-designed argument strong and still judged the bug real (high confidence); the intended-lens review rated it moderate; the devil's advocate found it does not hold (high confidence). All placed the defect in FacetsConfig.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateBounded-exhaustive harness signal, then a JUnit that fails 3 of 5 on main (assertions on and off), 10.5.2 and 9.12.3; a portable main() fails on 9.0.0 and passes on 8.9.0 and 8.11.4; probes for the commit case and association facets.
cross-model review, refute lens real (high)OpenAI GPT-5.6, executed: own sorted-set test fails on main with assertions on and off; own main() fails on 9.0.0 and passes on 8.11.4; both orders stay rejected after commit and reopen. Rated the works-as-designed argument strong (including that applications must keep one field's dimensions compatible), yet judged the bug real, high confidence, with the defect in the facet layer.
independent reproducer realOwn test from the claim only: 4 of 8 fail on main and 10.5.2, 5 of 8 on 9.12.3; its main() fails from 9.0.0 on and passes on 8.9.0 and 8.11.4.
cross-model review, intended lens real (high)OpenAI GPT-5.6, executed: own taxonomy test sees both orders and the reopen rejection, controls pass; probe rejects on main and 9.0.0, accepts on 8.11.4. Intended case moderate.
intended-behaviour devil's advocate realBuilt the strongest "works as designed" case and showed why it fails (high confidence that it is not intended); found that upstream's NONE tests use the shared layout, that deleteAll() resets the schema and that LFV-118 masks one sorted-set order on 10.5+.
adjudication confirmed (high)Re-ran the independent reproduction's test (4 of 8 fail) and the finder's (3 of 5) with assertions on and off.
credibility audit keepRe-ran the finder's test on main with two seeds, 3 of 5 fail; impact class kept at "wrong error".
this doc (gradle, a9779ef3c193) reproduces; two fixes validatedStock: 4 of 7 fail with assertions on and off; module 313 tests, 4 failed (the repro). Both fixes pass their tests, the facet module and lint.

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

Finding record

LFV-729, snapshot of 2026-11-06

Registry title
A facet dimension set to DrillDownTermsIndexing.NONE that shares its index field ($facets by default) with term-indexing dimensions makes FacetsConfig emit two schemas for that field, so since 9.0.0 IndexWriter rejects documents of one kind once a document of the other kind is indexed, and after a commit until the index is cleared or rebuilt
Area
facet / FacetsConfig drill-down term indexing (DrillDownTermsIndexing.NONE in a shared indexFieldName) vs the core per-field schema check (IndexingChain FieldSchema, FieldInfo.verifySameIndexOptions)
Priority
Low, rank 367: A loud IllegalArgumentException behind the opt-in DrillDownTermsIndexing.NONE, with a setIndexFieldName workaround; nothing is silently wrong.
Impact class
wrong error
Impact note
Hits applications that set DrillDownTermsIndexing.NONE on some but not all facet dimensions mapped to one index field (the default $facets, the layout of upstream's own NONE tests), with taxonomy, sorted-set or association facets, as soon as one document's dimensions in that field are all NONE and another document has a term-indexing dimension there: addDocument/updateDocument throws a clean IllegalArgumentException from the core per-field schema check, and once one kind is committed a new IndexWriter rejects every document of the other kind until deleteAll() or a reindex; nothing is corrupted and no result is silently wrong, but the failure depends on the data, so it can first surface in production; workaround: give NONE dimensions their own index field with setIndexFieldName; opt-in (NONE is not the default), no scale needed, -ea and -da, every release from 9.0.0 to 10.6.0 (10.6.0 by reading) and main.
Summary
You call config.setDrillDownTermsIndexing("rating", NONE), leave every dimension in the default index field $facets, and index one document with only FacetField("rating", "5") and then one with FacetField("author", "Bob"); you expect both to be indexed, as they are on Lucene 8.9 to 8.11 and as they are when rating gets its own index field. FacetsConfig.build gives the first document a doc-values-only $facets (NONE adds no drill-down terms) and the second a $facets with drill-down terms, and since 9.0 Lucene requires one schema per field, so the second addDocument throws IllegalArgumentException 'Inconsistency of field data structures across documents for field [$facets] of doc [1]. index options: expected 'NONE', but it has 'DOCS'' (with taxonomy facets the reverse order fails the same way). After a commit, a new IndexWriter keeps rejecting documents of the other kind ('cannot change field "$facets" from index options=NONE to inconsistent index options=DOCS') until the index is cleared or rebuilt.
Introduced
d03662c48bfc5bf2be4840a7f743f9cb64b17fee (apache/lucene#11, LUCENE-9334 'Consistency of field data structures', mayya-sharipova, merged to main 2021-04-14; issue #10374) is the first commit at which the repro fails [exec: commit-level A/B on the full-history clone, its parent 79f14b17429 passes, with -ea and -da]. It is the second of two changes. The first, 3648a1020a8ab6b729ed53e0360499e624bb0cd1 (apache/lucene#25, LUCENE-9385 'Add option to skip indexing facet drill down terms', zacharymorn, merged 2021-03-27; 8.x backport e812a508590, apache/lucene-solr#2471), added DrillDownTermsIndexing with NONE and the still-unchanged indexDrillDownTerms, which adds no StringField for a NONE dimension while the dimension's doc values still go into the shared indexFieldName [read: git blame on main]; at 3648a1020a8 the repro passes [exec], because the core then silently upgraded a doc-values-only occurrence of a field (index options NONE) to DOCS (FieldInfo.update). LUCENE-9334 replaced that with an exact per-field schema check (IndexingChain FieldSchema.assertSameSchema, FieldInfo.verifySameIndexOptions); its rejection is intended (CHANGES 9.0.0, MIGRATE.md). Neither PR's discussion mentions the other, and #25's tests always put a term-indexing dimension in every document. An unnoticed interaction of two changes 18 days apart; the defect is in the facet layer (2021-04-14 (d03662c48bf, LUCENE-9334); the trigger API dates from 2021-03-27 (3648a1020a8, LUCENE-9385) and 2021-03-29 (8.x backport e812a508590, released in 8.9.0, where the bug does not occur))
First affected version
9.0.0 [exec]; 8.9.0 to 8.11.4 have the API and accept every document [exec]; 8.8.2 and older lack DrillDownTermsIndexing [exec: compile error on 8.8.2]; LUCENE-9334 was never backported to 8.x
Affected versions
Every release from 9.0.0 through 10.5.2 (all 33 on Maven Central: 9.0.0 to 9.12.3 and 10.0.0 to 10.5.2) and main a9779ef3c193 [exec, version-independent public-API main() probe, -ea; the -da runs on 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main give the same results]; the finder's JUnit fails 3 of 5 on 9.1.0, 9.12.3, 10.0.0, 10.5.2 and main [exec]. branch_10x and branch_10_6 (10.6.0, released 2026-10-29) carry the same FacetsConfig and IndexingChain code [read], so 10.6.0 is affected too; the two upstream main commits after a9779ef3c193 do not touch lucene/facet or the core check [read]. Not affected: 8.9.0, 8.10.0, 8.10.1 and 8.11.0 to 8.11.4 [exec]. One scenario differs from 10.5.0 on: with sorted-set facets, a term-indexing document followed by a NONE-only document in the same in-memory segment is (wrongly) accepted because of the registered core bug LFV-118 (frozen-FieldType validation cache, #15886); every other scenario, and every scenario after a commit and a new writer, still fails on 10.5.0 through main. All runs JDK 25, aarch64
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal: exhaustive check
Upstream and prior reports
none found (2026-10-27, read-only; the prior-report record verdict new: 291 logged queries, namely 214 from the scripted prior-report search over apache/lucene issues, PRs and commits, Elasticsearch, OpenSearch and Solr, the dev@, java-user@, issues@ and commits@ lists, JIRA and the web, plus 43 manual GitHub searches, 19 list-archive queries, 5 JIRA searches, 9 Stack Exchange searches and a history check; the PR #25 and #11 review threads read in full; all 67 facet-module JIRA issues created after 2021-03-01 triaged; 20 hits read and judged, none the same bug; the finder's 51-query run and the intended-behaviour check's own run agree). Closest items, all different: #10425 / PR #25 / lucene-solr#2471 (LUCENE-9385, the NONE feature; nobody discusses dimensions with different modes sharing a field), #10374 / PR #11 (LUCENE-9334, the core rule; no mention of FacetsConfig or drill-down terms), #10490 (LUCENE-9450, a precedent where LUCENE-9334 broke another facet-module layout, the taxonomy index's full-path field, which upstream fixed in the facet module before 9.0.0), the dev@ thread 'Are the new index consistency checks too strict?' (2021-09, the same core rule in an application's own schema; the maintainers kept the rule), LUCENE-10082 (better schema error messages) and #8569 (LUCENE-7518, 2016: 'One must use different field names' for taxonomy vs sorted-set doc values). No open PR touches indexDrillDownTerms (the two open PRs that touch FacetsConfig.java, #11780 and #12862, do not)
Lucene commit tested
main a9779ef3c193 (commit of 2026-10-26): this doc's JUnit file through gradle in a scratch worktree, stock and with each of the two fixes, assertions on and off, plus the whole lucene/facet module, the demo facet examples and the lint, format and javadoc checks with each fix (2026-10-27); the classpath runner on main (assertions on and off, and 10 iterations) and on the 10.5.2 release jars. The hunt's checks ran its tests on main a9779ef3c193 and on every release from 8.8.2 to 10.5.2, and bisected the main history.

Lucene and upstream sources

Finding record
  • lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:57 (@lucene.experimental), :61 (DEFAULT_INDEX_FIELD_NAME javadoc), :74-79 (NONE 'Index no drill down terms'), :126-130 (DimConfig defaults: ALL, DEFAULT_INDEX_FIELD_NAME), :203-214 (setIndexFieldName javadoc 'only used by the taxonomy based facet methods'), :216-226 (setDrillDownTermsIndexing per dimension), :316-337 (mixed association types in one field rejected with 'use FacetsConfig to change the indexFieldName'), :360-416, :462-504, :506-534 (processFacetFields, processSSDVFacetFields, processAssocFacetFields put each dimension's doc values into its indexFieldName), :419-460 (indexDrillDownTerms adds no StringField for NONE)
  • lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2234-2260 (FieldSchema 'Inconsistency of field data structures across documents for field', assertSame, raiseNotSame), :2346 (assertSameSchema, per the repro stack trace); lucene/core/src/java/org/apache/lucene/index/FieldInfo.java:246, :276-292 (verifySameIndexOptions, 'cannot change field ... from index options=')
  • lucene/demo/src/java/org/apache/lucene/demo/facet/package-info.java:234-238 ('a per-dimension config ... you can choose to not index them at all'), quoted by the finder, the independent reproduction and the intended-behaviour check
The finding in 30 seconds
  • lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:61-62, 74-79, 126-130, 216-226, 269-339, 400, 412, 419-460, 480-497, 501, 530, 532, 316-337 (at a9779ef3c193)
  • lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:621-637, 685-698, 1876-1896, 2233-2234, 2258-2272, 2278-2289, 2345-2346; lucene/core/src/java/org/apache/lucene/index/FieldInfos.java:511-515; lucene/core/src/java/org/apache/lucene/index/FieldInfo.java:276-286
Who is affected
  • lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:203-206 (setIndexFieldName javadoc)
  • lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:283-291, 406-409; lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:383, 517 (taxoWriter.addCategory before IndexWriter)
Why it looks unintended
  • lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:57, 61-62, 74-79, 126-130, 216-226, 316-337
  • lucene/demo/src/java/org/apache/lucene/demo/facet/package-info.java:229-243 (Category Terms Field), 354-357 (Multiple Category Lists)
  • lucene/facet/src/test/org/apache/lucene/facet/TestDrillDownQuery.java:309-345; lucene/facet/src/test/org/apache/lucene/facet/sortedset/TestSortedSetDocValuesFacets.java:726, 809
  • lucene/CHANGES.txt:4723-4727 (LUCENE-9334, 9.0.0), 5353-5354 (LUCENE-9385, 8.9.0); lucene/MIGRATE.md:1557-1567
Suggested fix
  • lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:64-68, 316-337 (assocDimTypes, the existing best-effort check)

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 a public-API probe run on main a9779ef3c193 (assertions on and off), 9.0.0 and 8.11.4; the values on screen come from the probe's log.

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.