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.
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.- You call
config.setDrillDownTermsIndexing("rating", NONE)and leaveauthorat the defaultALL. Both dimensions stay in the default index field,$facets. - You index doc 0 with only
FacetField("rating", "5")and doc 1 with onlyFacetField("author", "Bob"). You expect 2 documents, as on 8.9.0 to 8.11.4, and as on main whenratinghas its own index field. config.buildgives doc 0 a$facetswith doc values only (index optionsNONE), becauseNONEadds no drill-down terms; doc 1's$facetsalso gets two drill-down terms (index optionsDOCS).- Since 9.0 a field must be indexed the same way in every document, so the second
addDocumentthrowsIllegalArgumentException"... field [$facets] of doc [1]. index options: expected 'NONE', but it has 'DOCS'" andnumDocsstays 1. Sorted-set and association facets fail the same way, and with taxonomy facets so does the reverse order. - After a commit, a new
IndexWriterrejects everyauthordocument ("cannot change field "$facets" from index options=NONE to inconsistent index options=DOCS") untildeleteAll()or a reindex.
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).
- Dimension and index field. A facet dimension (
rating,author) has aDimConfig. ItsindexFieldNameis 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 toDEFAULT_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.
StringFieldterms such asauthorandauthor\u001FBobthatDrillDownQuerysearches.DrillDownTermsIndexingpicks which ones a dimension writes, per dimension (setDrillDownTermsIndexing(dimName, ...),:216-226);NONEmeans "Index no drill down terms" (:74-79). The default isALL. - 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; aStringFieldmeansDOCS.
build()groups the facet fields by index field and calls the taxonomy, sorted-set or association path, which adds the doc values toindexFieldName(:412,:480-497,:532) and callsindexDrillDownTermsfor each dimension (:400,:501,:530).indexDrillDownTermsaddsStringField(indexFieldName, ...)terms for every mode exceptNONE:if (dimConfig.drillDownTermsIndexing != DrillDownTermsIndexing.NONE) {(:419-460,:421).- So a document whose dimensions in
$facetsare allNONEhas only doc values there (index optionsNONE); 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. - Across documents the core compares:
assertSameSchemain a segment (IndexingChain.java:2345-2346, message at:2233-2234,:2258-2272) andverifySameIndexOptionsacross 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).
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 index | 8.9.0 to 8.11.4 | 9.0.0 to main |
|---|---|---|
taxonomy: rating-only doc, then author doc | both indexed | 2nd rejected: "... field [$facets] of doc [1]. index options: expected 'NONE', but it has 'DOCS'." |
taxonomy: author doc, then rating-only doc | both indexed | 2nd rejected: "... expected 'DOCS', but it has 'NONE'." |
sorted-set: rating-only doc, then author doc | both indexed | 2nd rejected: "... expected 'NONE', but it has 'DOCS'." |
rating-only doc, commit(), new IndexWriter, author doc | both indexed | rejected: "cannot change field "$facets" from index options=NONE to inconsistent index options=DOCS" |
controls: rating in its own field; no NONE; every dimension NONE | both 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.NONEon 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 onlyIntAssociationFacetField, whose code path the float and bytes variants share). Ordinary sparse data triggers it: one document whose dimensions in the field are allNONE, and another with a term-indexing dimension there. - Effect.
addDocument/updateDocumentthrows a clean, non-abortingIllegalArgumentExceptionand the document is not added as a live document. Nothing is corrupted and no search result is wrong. As with any non-aborting exception,IndexWriterstill 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, anddeleteAll()on the main index does not remove them. Whichever kind of document is committed first decides which kind is rejected afterwards, untildeleteAll()or a reindex. - Default vs opt-in.
NONEis opt-in. Once it is set, the failing layout is the default one, and it is the layout of upstream's ownNONEtests. - Scale. None needed: two documents, deterministic, assertions on and off.
- Not run. x86, concurrent writers, and a dedicated JUnit for a hierarchical
NONEdimension (the bounded harness covered hierarchical configurations). All runs were on Linux aarch64. - Workaround.
setIndexFieldNamefor eachNONEdimension. It is not free: counting then needs a separateFacetsinstance orSortedSetDocValuesReaderStateper index field. ThesetIndexFieldNamejavadoc 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
NONEper field with the index field optional; nobody has reported it since 9.0.0 (2021).
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.
/*
* 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);
}
}
}
}
# 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
addDocumentthrowsIllegalArgumentException: Inconsistency of field data structures across documents for field [$facets] of doc [1]. index options: expected 'NONE', but it has 'DOCS'.
More files, environment, full output88 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: 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)
[...]NONEis a documented per-dimension option, all dimensions share$facetsby default, and nothing says aNONEdimension needs its own field.- Upstream's own
NONEtests 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.testDrillDownTermsDifferentOptionsputsNONEdimensionain$facetsnext to term-indexing dimensionsbtoe;TestSortedSetDocValuesFacets.testDrillDownOptionsandtestHierarchicalDrillDownOptionsputNONEdimensioncnext todtog. Every test document carries a term-indexing dimension, so they never build aNONE-only document. Two came with the feature (LUCENE-9385);testHierarchicalDrillDownOptionswas 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 setNONE. - History. LUCENE-9385 (#25) added
NONEon 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 theindexFieldName(:316-337).NONEhas 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.
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.
+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(), beforeIndexWriterand the taxonomy see it, with a message that names the field and the remedy. - Best effort: the map sees only documents this
FacetsConfigbuilt. 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). OneFacetsConfigshared by several indexes, or reused afterdeleteAll(), could reject a document that a particular index would accept. - The map records a document's answer when
build()runs, not whenIndexWriteraccepts it: a document that is built but never added, or that fails later (a null taxonomy writer, or anIndexWriterrejection 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 ("
NONEmust not share a field with a term-indexing dimension") would break layouts that work today: upstream's ownNONEtests, and indexes where every dimension in$facetsisNONE.
What the checks validated
- Its test
TestFacetsConfigDrillDownNoneCheck(5 tests: both orders rejected bybuild()with the new message; controls: every document has a term-indexing dimension, every dimensionNONE,NONEin 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 fromIndexWriter. - The whole
lucene/facetmodule with the new test: 311 tests, 0 failed, 8 skipped. Demo facet examples: 19 tests, 0 failed.ecjLint,checkGoogleJavaFormatandrenderJavadoc: BUILD SUCCESSFUL.
+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
$facetsisNONE) accepts more documents on stock, but after the fix its nextNONE-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
DimConfigor 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/facetmodule 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.
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.
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.
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.
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
NONEand 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.
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.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits; Elasticsearch, OpenSearch and Solr; the dev@, java-user@, issues@ and commits@ lists; JIRA; the web | 2026-10-27 | the hunt's prior-report tool in three runs plus a web re-run (facet names, core schema names, docs and options): 214 queries | no 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-27 | 43 manual searches; code search for downstream users of setDrillDownTermsIndexing | only #25 names the option; one downstream code base exposes NONE per field, with no issues filed |
| Review threads and open PRs | 2026-10-27 | the review comments of #25, #11 and 7 related PRs read; changed files of all 410 open PRs | nobody discusses mixed modes in one field; #11780 and #12862 touch FacetsConfig.java but not drill-down indexing |
| Mailing lists, JIRA archive, Q&A sites | 2026-10-27 | 19 list-archive queries (6 threads read), all 67 facet-module JIRA issues since 2021-03-01, 9 Q&A queries | two threads on the core rule (no facets); no facet report |
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.
| Version or commit | Verdict | Result |
|---|---|---|
| 8.8.2 | API absent | compile 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 affected | the 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) | affected | taxonomy 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 | affected | the 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 | affected | the 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 | affected | this 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) | bisect | every document accepted (3648a1020a8 with assertions on; 79f14b17429 on and off) |
main d03662c48bf (LUCENE-9334) | bisect | rejected, with assertions on and off; the commit touches no facet source |
| fail-fast fix | fix | the second kind rejected by FacetsConfig.build with a message naming the field and indexFieldName |
| no-terms-field fix | fix | every repro passes; an index written before the fix with a terms-free shared field rejects new NONE-only documents |
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.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Bounded-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 | real | Own 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 | real | Built 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 | keep | Re-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 validated | Stock: 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. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 602 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,459 words
- 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 wholelucene/facetmodule, 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 maina9779ef3c193and on every release from 8.8.2 to 10.5.2, and bisected the main history.
- 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-337lucene/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, 809lucene/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)
- TestDrillDownNoneSharedIndexField.java (JUnit test):
repro/LFV-729/src/org/apache/lucene/facet/TestDrillDownNoneSharedIndexField.java(46 lines)
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.
assets/LFV-729/bug.png(602 KB)
