Taxonomy facets add leaf counts up to the parents at search time, but only for the dimensions they believe are hierarchical. Indexing asks the config of each dimension, default included; the roll-up looks only at the explicitly configured ones.
- A taxonomy facet dimension such as
Publish Dateholds paths like 2010/10/15.FacetsConfigsays, per dimension, whether it is hierarchical and whether a document may have several values (multiValued). - For a hierarchical, single-valued dimension, indexing stores only the number (ordinal) of each document's leaf category. After counting,
FastTaxonomyFacetCountsadds the leaf counts up the tree, the roll-up, so 2010 gets 2 and the dimension 3. - A dimension you never configure gets the default config. Since 4.8.0 a
FacetsConfigsubclass can change that default by overriding the protectedgetDefaultDimConfig().

Publish Date tree: leaf counts 1, 1 and 1; the roll-up should give 2010 (2), 2012 (1) and 3 for the dimension, but with the hierarchical default the parents stay 0. Select the still to open it at full size. Generated with AI, checked against the code.- Subclass
FacetsConfigso thatgetDefaultDimConfig()returns a hierarchicalDimConfig, as Lucene's ownTestFacetsConfig#testCustomDefaultdoes. Index 3 documents underPublish Date: 2010/10/15, 2010/10/20 and 2012/1/1. - Indexing follows that default: each document stores only its leaf category, and the counts of the parents (2010, 2012 and the dimension) are left to be added up at search time.
- You expect
FastTaxonomyFacetCountsto give what an explicitsetHierarchical("Publish Date", true)gives: value 3, with 2010 (2) and 2012 (1). - Instead
getTopChildren(10, "Publish Date")returns null andgetSpecificValue("Publish Date", "2010")returns 0. The roll-up only visits the dimensions listed ingetDimConfigs(), the explicit entries, andPublish Datehas none. - No error, and a
DrillDownQueryonPublish Date/2010still matches 2 documents. If some documents have a depth-1 label, only those show:toys (1)instead of electronics (3), books (1), toys (1).
Applications whose FacetsConfig subclass overrides getDefaultDimConfig() (or getDimConfig) so that unconfigured dimensions are hierarchical and single-valued, and that count such a dimension with taxonomy faceting. Opt-in and rare: the three real overrides found in the recorded GitHub code search use multiValued = true, which is unaffected. Deterministic, 3 documents suffice; a fix needs no reindex.
Mechanism, terms and numbers657 words
FacetsConfig#getDefaultDimConfig() is a protected hook (LUCENE-5479, 4.8.0) that sets the config of every dimension without an explicit entry; Lucene's only test of it makes those dimensions hierarchical. Indexing follows the default: for a hierarchical, single-valued dimension it stores only each document's leaf category and leaves the parent counts to a search-time roll-up. But TaxonomyFacets.rollup() picks the dimensions to roll up from config.getDimConfigs(), which holds only the explicit entries, so such a dimension is never rolled up: its dimension count and every non-leaf count stay 0, getTopChildren(dim) returns null (or only the depth-1 leaves), and nothing throws, while a DrillDownQuery on the same labels still matches the documents. It affects FastTaxonomyFacetCounts (both constructors) and the value-source aggregation of TaxonomyFacetFloatAssociations, in every release from 4.8.0 through 10.6.0 and on main. The index itself is correct, so a fix in the roll-up needs no reindex.
- The hook.
FacetsConfig.getDefaultDimConfig()(FacetsConfig.java:142-151, protected) supplies the config of every dimension without an explicit entry:getDimConfig(dim)returns the map entry if there is one, elsegetDefaultDimConfig()(:153-160). - Indexing honours it.
processFacetFieldsreadsgetDimConfig(facetField.dim)(:370); a 3-component path is accepted only because the default says hierarchical (:371-378). It stores the leaf ordinal (:383-384) and adds the ancestor ordinals only when the dimension ismultiValued(:386-398). So each document stores one ordinal:2010/10/15,2010/10/20or2012/1/1. - Counting.
FastTaxonomyFacetCountscounts each stored ordinal once (FastTaxonomyFacetCounts.java:93,:138), then callsrollup()(:117for hits,:174for count-all). The value-source path ofTaxonomyFacetFloatAssociationscalls the samerollup()(TaxonomyFacetFloatAssociations.java:164-165); its indexed-association constructors, and those ofTaxonomyFacetIntAssociations, never callrollup()for any config and are not affected (code reading).DrillSidewaysbuilds its taxonomy facets withFastTaxonomyFacetCounts(DrillSideways.java:159,:165). - The roll-up skips the dimension.
TaxonomyFacets.rollup()("Rolls up any single-valued hierarchical dimensions",TaxonomyFacets.java:261-285) loops overconfig.getDimConfigs().entrySet()(:269).getDimConfigs()returns the private map of explicit entries (FacetsConfig.java:228-231).Publish Datehas no entry, so the leaf counts (1 each) are never added up:2010/10,2010,2012/1,2012and the dimension stay 0. - What the caller sees.
getTopChildren(10, "Publish Date")collects the dimension's children with a count above 0 (TaxonomyFacets.java:478-503), finds none, and returns null for 0 children (:308-310).getSpecificValue("Publish Date")is accepted with the comment "ok: rolled up at search time" (:540-541) and returns the raw count, 0 (:553).getAllDimsdrops a dimension whosegetTopChildrenis null (:571-575).getTopDimssums a single-valued dimension's direct children (:639-650), which were never rolled up, and leaves out a dimension whose sum is 0 (:652): it returns[]where the explicit config givesvalue=3 2010 (2) 2012 (1)(executed on5adff78bb168; correct with the fix). - Everything else asks
getDimConfig.verifyDim(:221-235),getAllDims(:570) andFacetsConfig.buildsee the default. Besidesrollup(), onlyOrdinalMappingLeafReader.java:136(LFV-722) enumeratesgetDimConfigs()in core facets; the sandbox cutter has the same loop (TaxonomyFacetsCutter.java:119) and its builder refuses unconfigured dimensions instead (TaxonomyFacetBuilder.java:48-59).
| call | explicit setHierarchical | hierarchical default via getDefaultDimConfig |
|---|---|---|
getTopChildren(10, "Publish Date") | value=3, 2010 (2), 2012 (1) | null |
getTopChildren(10, "Publish Date", "2010") | value=2, 10 (2) | null |
getSpecificValue("Publish Date", "2010") | 2 | 0 |
getSpecificValue("Publish Date") | 3 | 0 |
DrillDownQuery on Publish Date/2010 | 2 hits | 2 hits |
With some depth-1 labels the result is partial: 5 documents under category/electronics/phones, /electronics/laptops, /electronics/phones, /books/fiction and /toys give value=1 childCount=1 toys (1) instead of value=5 childCount=3 electronics (3) books (1) toys (1). The value-source aggregation (TaxonomyFacetFloatAssociations, DoubleValuesSource.fromLongField("price"), MAX, prices 5, 7, 3) gives null instead of value=7.0 2010 (7.0) 2012 (3.0). Counts under the deepest parent, DrillDownQuery, SortedSetDocValues faceting with the same default, a hierarchical multiValued default and explicitly configured dimensions are correct.
Exposure and likelihood232 words
- Who is hit. Applications that subclass
FacetsConfigand overridegetDefaultDimConfig()(the documented hook, 4.8.0 and later) or the publicgetDimConfig(the workaround the LUCENE-5479 reporter had used, 4.7.0 and later) so that dimensions without an explicit entry are hierarchical and single-valued, and that count such a dimension with taxonomy faceting:FastTaxonomyFacetCounts(both constructors),TaxonomyFacetFloatAssociationswith aDoubleValuesSource, andDrillSideways' default taxonomy facets (code reading). - What goes wrong. Dimension and parent counts read 0,
getTopChildren(dim)returns null or only the depth-1 leaves, andgetAllDimsandgetTopDimsleave the dimension out. Nothing throws, andDrillDownQueryon the same labels still matches, so the facet panel and the result list disagree. - Opt-in, no scale needed. Deterministic with ordinary data (3 documents suffice), with assertions on and off.
- Likelihood: rare. All three real overrides found in the recorded GitHub code search (in 2 projects, seecr/meresco-lucene and voyanttools/trombone) use
multiValued = true, which is unaffected, and the symptom is easy to see, so a user would likely fall back to explicitsetHierarchicalcalls. - Not affected. The stock
FacetsConfig, explicitly configured dimensions, a hierarchical multiValued default, SortedSetDocValues faceting, drill-down and counts under the deepest parent. - After a fix. The index is written as designed (an index built with the default config and counted with an explicitly configured copy gives the right counts), so a fix in the roll-up corrects existing indexes at search time.
One JUnit test, one method: a FacetsConfig whose default is hierarchical, three documents, FastTaxonomyFacetCounts. It fails on main and on the 10.6.0, 10.5.2, 9.12.3 and 9.1.0 jars, with assertions on and off, and in 20 of 20 iterations; the same test with an explicit setHierarchical passes.
/*
* 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.taxonomy;
import org.apache.lucene.document.Document;
import org.apache.lucene.facet.FacetField;
import org.apache.lucene.facet.Facets;
import org.apache.lucene.facet.FacetsConfig;
import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyReader;
import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyWriter;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.IOUtils;
public class TestHierarchicalDefaultDimConfig extends LuceneTestCase {
public void testUnconfiguredHierarchicalDimIsRolledUp() throws Exception {
// Every dimension without an explicit entry is hierarchical (as in
// TestFacetsConfig#testCustomDefault); "Publish Date" has no explicit entry.
FacetsConfig config =
new FacetsConfig() {
@Override
protected DimConfig getDefaultDimConfig() {
DimConfig dimConfig = new DimConfig();
dimConfig.hierarchical = true;
return dimConfig;
}
};
Directory dir = newDirectory();
Directory taxoDir = newDirectory();
IndexWriter writer = new IndexWriter(dir, newIndexWriterConfig());
DirectoryTaxonomyWriter taxoWriter = new DirectoryTaxonomyWriter(taxoDir);
String[][] dates = {{"2010", "10", "15"}, {"2010", "10", "20"}, {"2012", "1", "1"}};
for (String[] date : dates) {
Document doc = new Document();
doc.add(new FacetField("Publish Date", date));
writer.addDocument(config.build(taxoWriter, doc));
}
DirectoryReader reader = DirectoryReader.open(writer);
TaxonomyReader taxoReader = new DirectoryTaxonomyReader(taxoWriter);
Facets facets =
new FastTaxonomyFacetCounts(
FacetsConfig.DEFAULT_INDEX_FIELD_NAME, reader, taxoReader, config);
// What config.setHierarchical("Publish Date", true) gives for the same documents:
assertEquals(
"dim=Publish Date path=[] value=3 childCount=2\n 2010 (2)\n 2012 (1)\n",
String.valueOf(facets.getTopChildren(10, "Publish Date")));
IOUtils.close(reader, taxoReader, writer, taxoWriter, dir, taxoDir);
}
}
cd $LUCENE
cp $REPRO/TestHierarchicalDefaultDimConfig.java lucene/facet/src/test/org/apache/lucene/facet/taxonomy/
./gradlew :lucene:facet:test --tests org.apache.lucene.facet.taxonomy.TestHierarchicalDefaultDimConfig -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false
- Expected
getTopChildren(10, "Publish Date")gives value=3 childCount=2, 2010 (2), 2012 (1), as with an explicitsetHierarchical.- Observed
- It returns null:
expected:<[dim=Publish Date path=[] value=3 childCount=2 ...]> but was:<[null]>.
More files, environment, full output231 words
The test sits in org.apache.lucene.facet.taxonomy next to the module's taxonomy tests and uses only public and protected API. Three documents, one dimension, single thread, deterministic. It uses the count-all constructor to stay short; the hits constructor new FastTaxonomyFacetCounts(taxoReader, config, fc) fails the same way. It compiles from 9.1.0 on (org.apache.lucene.tests). For a release, compile and run the same file against that release's lucene-core, lucene-facet and lucene-test-framework jars.
Pass -Ptests.asserts explicitly: on current main its default is drawn from the seed ("Run with assertions for ~75% of all seeds").
The expected string is what the same documents give with config.setHierarchical("Publish Date", true) (the test with that one change passes on stock main). The same test is the suggested regression test; it passes with the recommended fix.
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT), 10.6.0, 10.5.2, 9.12.3, 9.1.0: fail |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
# gradle, main 5adff78bb168, seed DEADBEEF, -Ptests.asserts=true
TestHierarchicalDefaultDimConfig > testUnconfiguredHierarchicalDimIsRolledUp FAILED
org.junit.ComparisonFailure: expected:<[dim=Publish Date path=[] value=3 childCount=2
2010 (2)
2012 (1)
]> but was:<[null]>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:C525DFB2C6A10945]:0)
...
at app//org.apache.lucene.facet.taxonomy.TestHierarchicalDefaultDimConfig.testUnconfiguredHierarchicalDimIsRolledUp(TestHierarchicalDefaultDimConfig.java:64)
# the same failure: gradle with -Ptests.asserts=false; the JUnit runner on main (-ea and -da), 10.6.0 (-ea and -da), 10.5.2, 9.12.3 and 9.1.0
# main, 20 iterations (seed 5EED0723): 20 of 20 fail
# control, the same test with explicit setHierarchical, stock main: passes
# main with the recommended fix: passes (assertions on and off)- The hook's javadoc: "The default configuration to be used for dimensions that are not yet set"; Lucene's only test of it sets
hierarchical = true. - Indexing honours the default and leaves the parents to a roll-up;
getSpecificValuesays "rolled up at search time". - The same
getDimConfigs()blind spot in another class was fixed as a bug (LUCENE-5801).
The full argument252 words
- The hook is documented for exactly this.
getDefaultDimConfigjavadoc: "Get the default configuration for new dimensions. Useful when the dimension is not known beforehand and may need different global default settings"; "@return The default configuration to be used for dimensions that are not yet set in the FacetsConfig" (FacetsConfig.java:142-148). CHANGES 4.8.0: "LUCENE-5479: FacetsConfig subclass can now customize the default per-dim facets configuration". - Lucene's only test of the hook sets
hierarchical = true(TestFacetsConfig#testCustomDefault,TestFacetsConfig.java:104-117). It checks onlygetDimConfig("foobar").hierarchical, never a count; no test combines a custom default with taxonomy counts. - The rest of the module honours the default. Indexing accepts a 3-component path only because the default is hierarchical (with the stock default
buildthrows "is not hierarchical yet has 3 components",FacetsConfig.java:371-378) and leaves the parents to the roll-up;getSpecificValue(dim)says "ok: rolled up at search time" (TaxonomyFacets.java:541);rollup()says it rolls up "any single-valued hierarchical dimensions"; SortedSetDocValues faceting with the same default counts correctly. - History: an assumption the hook outdated. The loop dates from LUCENE-5339 (2013), when the only default was the static, flat
DEFAULT_DIM_CONFIG, so a hierarchical dimension had to be configured explicitly. LUCENE-5479 (2014) made the default overridable and changed onlyFacetsConfigand its test. Its discussion (#6542) was about amultiValued = truedefault, wherebuildstores the ancestors itself and the gap does not show. - Precedent. LUCENE-5801 (#6863, 2014):
OrdinalMappingAtomicReadercollected facet fields only fromgetDimConfigs(); it was reported as "wrong facet data" and fixed.
The devil's-advocate check249 words
The strongest case for "intended": FacetsConfig is @lucene.experimental; its class javadoc says "use the setters in this class to change these settings for each dim" (FacetsConfig.java:49-51); the hook's javadoc example is multivalue = true; isDimConfigured's javadoc says an unconfigured dimension uses DEFAULT_DIM_CONFIG (:162-165); enumerating getDimConfigs() keeps the taxonomy's children and siblings arrays lazy, which two tests assert; and the sandbox TaxonomyFacetBuilder (#14237) requires explicit configuration for the same reason.
Why it fails: nothing in core documents or enforces that a hierarchical dimension must be configured explicitly. Indexing silently accepts the paths and relies on a roll-up that never comes, and the result is 0 for a label DrillDownQuery finds in 2 documents. The isDimConfigured javadoc is not older than the hook: it was added in 9.0.0 (LUCENE-9971, #149, 2021) and repeats the pre-2014 assumption. Laziness can be kept: the recommended fix walks the taxonomy only when the default needs a roll-up, and both laziness tests pass with it (it does not cover name-dependent getDimConfig overrides, which only the always-walk alternative does). The sandbox comment shows a contributor who knew the loop's limit and chose to fail loudly rather than miscount; "configure hierarchical dimensions explicitly" is a contract upstream could plausibly choose, but then core should enforce it the same way (option D) instead of returning wrong counts. Verdict: not intended, a defect; every reviewer rated the "intended" case weak (the devil's advocate rated only how upstream may frame it as medium).
Recommended A. Also roll up taxonomy dimensions that use the default config, only when that default needs it (validated for the documented hook)
After the getDimConfigs() loop in TaxonomyFacets.rollup(), if the default config is hierarchical and single-valued, roll up the taxonomy's dimensions without an explicit entry (+34 lines, one file). With it the repro passes (assertions on and off), and so do the facet module's 307 tests (assertions on: 0 failures, 7 skipped; laziness tests included) and its static checks.
All options and trade-offs4 options ยท 763 words
Four options. A was validated with gradle on main 5adff78bb168; C was run and breaks two existing tests; B and D were not run. In any case, fix the isDimConfigured javadoc (FacetsConfig.java:162-165): an unconfigured dimension uses getDefaultDimConfig(), not necessarily DEFAULT_DIM_CONFIG.
A. Also roll up taxonomy dimensions that use the default config, only when that default needs it (validated for the documented hook) (recommended)
After the existing loop, read the default through getDimConfig of a name without an explicit entry (a small helper unconfiguredDim() returns "", which a FacetField never has as its dimension, padded with '\u0000' while that name is configured). getDefaultDimConfig() takes no name, so this is exactly the default. If it is hierarchical and single-valued, walk the dimension roots (the children of the root ordinal, the same walk getAllDims does, TaxonomyFacets.java:564-578) and roll up each one without an explicit entry whose getDimConfig(dim) is hierarchical and single-valued. Explicit entries are skipped, so nothing is rolled up twice. The full diff is in the Sources block.
// Dimensions without an explicit entry are not in getDimConfigs(): they use the default
// DimConfig, which a FacetsConfig subclass may change (getDefaultDimConfig). Walk the
// taxonomy's dimensions for them only when that default needs a roll-up, so the
// children/siblings arrays stay lazy otherwise:
DimConfig defaultConfig = config.getDimConfig(unconfiguredDim());
if (defaultConfig.hierarchical && defaultConfig.multiValued == false) {
if (children == null) {
children = getChildren();
}
ParallelTaxonomyArrays.IntArray siblings = getSiblings();
for (int dimOrd = children.get(TaxonomyReader.ROOT_ORDINAL);
dimOrd != TaxonomyReader.INVALID_ORDINAL;
dimOrd = siblings.get(dimOrd)) {
String dim = taxoReader.getPath(dimOrd).components[0];
if (config.isDimConfigured(dim) == false) {
DimConfig dimConfig = config.getDimConfig(dim);
if (dimConfig.hierarchical && dimConfig.multiValued == false) {
updateValueFromRollup(dimOrd, children.get(dimOrd));
}
}
}
}Trade-offs
- The probe name works around
getDefaultDimConfigbeing protected in another package; agetDimConfigoverride that answers flat for""but hierarchical for some other names would escape the gate. So A is validated forgetDefaultDimConfigoverrides and forgetDimConfigoverrides that return the effective default for the probe name, and is incomplete for name-dependent overrides (C covers those). - When the gate opens, the children and siblings arrays are loaded once per reader, as for any explicitly configured hierarchical dimension today.
- Like the existing loop, it does not compare
indexFieldName(rolling up a dimension stored in another field adds zeros). - No index change and no reindex.
What the checks validated
- Gradle in a scratch worktree of
5adff78bb168, OpenJDK 25.0.3, seed DEADBEEF: stock, the repro fails with assertions on and off; with the fix it passes with assertions on and off. - The whole facet module with the fix,
:lucene:facet:testwith assertions on: 40 classes, 307 tests, 0 failures, 7 skipped, includingTestTaxonomyFacetCounts(19),TestTaxonomyFacetCounts2(4),TestTaxonomyFacetValueSource(10),TestTaxonomyFacetAssociations(12),TestFacetsConfig(4) and the new test. :lucene:facet:check -x test(google-java-format, ecj lint, forbidden APIs) passes with the fix and the test.- With the fix applied on
5adff78bb168, the earlier independent tests pass (2 of 2, 3 of 3 including the depth-1 case, 5 of 5 including agetDimConfigoverride), and the value-source probe givesvalue=7.0 2010 (7.0) 2012 (3.0)like the explicit config. - The
getTopDimsandgetAllDimsprobe gives the explicit config'svalue=3 2010 (2) 2012 (1)with the fix (stock:[]).
Add a public accessor to FacetsConfig that returns getDefaultDimConfig(), and use it in A instead of the probe name.
Trade-offs
Same behaviour for the documented hook, no probe name; an API addition to an @lucene.experimental class.
C. Always walk the taxonomy's dimensions and ask getDimConfig(dim) (exact, but breaks two tests) (validated by the checks)
Replace the getDimConfigs() loop with the getAllDims-style walk (diff in the Sources block). It covers every override, including name-dependent getDimConfig overrides, and the repro passes.
Trade-offs
It loads the children and siblings arrays for every count, even with only flat dimensions, and two existing laziness assertions fail: TestTaxonomyFacetCounts#testLabelWithDelimiter (:451, assertFalse(siblingsLoaded())) and TestTaxonomyFacetValueSource#testChildrenAndSiblingsLoaded (:520, expected:<false> but was:<true>); 29 tests, 2 failed.
What the checks validated
- Gradle on
5adff78bb168: the repro passes;TestTaxonomyFacetCountsandTestTaxonomyFacetValueSource: 2 of 29 fail as above.
Throw an IllegalArgumentException when build or TaxonomyFacets meets an unconfigured dimension whose config is hierarchical and single-valued, and say so in the getDefaultDimConfig javadoc, like the sandbox TaxonomyFacetBuilder (TaxonomyFacetBuilder.java:48-59).
Trade-offs
Fails loudly instead of miscounting, but breaks indexing for applications that use the hook today, and makes the hook unusable for hierarchical taxonomy dimensions, the very case its only test shows.
Products built on Lucene that let users add facet dimensions without per-dimension configuration through a hierarchical default would show the same zero or missing parent counts; the downstream overrides found (seecr/meresco-lucene, voyanttools/trombone) use multiValued = true and are unaffected.
Semi-formal: a bounded-exhaustive check compared taxonomy and SortedSetDocValues facet results with a javadoc-based reference over every FacetsConfig option combination and up to 3 small documents. Reading rollup() while building it raised the suspicion; this defect's mismatches then appeared in exactly the 20 taxonomy configs with a hierarchical, single-valued default (other mismatches: another finding, and an artifact of the check fixed before the second run).
The method in full178 words
Method: semi-formal, bounded-exhaustive translation validation of the FacetsConfig option matrix.
- The matrix: drill-down terms indexing (5 options) x hierarchical x multiValued x requireDimCount x index field layout x config style (explicit setters, or a
getDefaultDimConfigoverride) x faceting (taxonomy, SortedSetDocValues): 400 configs. Each was checked on every multiset of up to 2 (and, in a second run, 3) small documents, in 1 or 2 segments, with and without deletes, against a reference written from the javadoc. - While building the matrix, reading
TaxonomyFacets.rollup()showed that it iteratesgetDimConfigs(). In the run on the stock jars, this defect's mismatches then appeared in exactly the 20 taxonomy configs with hierarchical = true, multiValued = false and the default-override style, and in none of the explicit-setter configs (the run's other mismatches belong to a different finding and to an artifact of the check itself, fixed before the second run): 408,567,314 checks at up to 2 documents, 685,912,348 at up to 3, assertions off. - The finding was reduced to a public-API JUnit test on the real jars, with an explicit-config control.
New: the searches found no Lucene issue, PR, JIRA entry, list thread or commit, and nothing outside Lucene, that describes this defect (re-checked 2026-11-03 and 2026-11-04). Related items concern other things: the sandbox TaxonomyFacetBuilder check (#14237), the same blind spot in another class (#6863, fixed 2014), and a review remark on an unmerged draft (#11780).
Searches3 searches
No report of this defect was found (re-checked 2026-11-03 in two independent passes, and again on 2026-11-04). Related prior items, none of them this defect:
- #14237 (epotyom, merged 2025-03-03, sandbox facets): the sandbox
TaxonomyFacetBuilderthrows unless the dimension is configured explicitly, with the comment that the sandbox "TaxonomyFacetsCutter uses FacetConfig#getDimConfigs to find dimensions that need to be rolled up, hence it needs the dimension to be configured explicitly" (TaxonomyFacetBuilder.java:48-59). It is about the sandbox cutter, a different class and fix site, motivated by dimension totals for the stock flat default; it says nothing about coreTaxonomyFacets,getDefaultDimConfigor wrong counts, and the PR discussion does not mention it. The closest related item. - #6863 (LUCENE-5801, 2014, fixed):
OrdinalMappingAtomicReadercollected facet fields only fromgetDimConfigs(), reported as "wrong facet data". A different class (today'sOrdinalMappingLeafReader, LFV-722); the precedent that this blind spot is a bug. - #11780 (open draft): in a 2023 review gsmiller noted that a
config.getDimConfigs()enumeration "won't know about" fields that use the default dim config, in a proposed SortedSetDocValues helper that was never merged. Different code. - #6542 (LUCENE-5479, 2014): added the hook. The reporter wanted a
multiValued = truedefault and had used agetDimConfigoverride; the committed test setshierarchical = true; roll-up is never discussed. - #149 (LUCENE-9971, 2021): made unseen dimensions behave the same in SortedSetDocValues and taxonomy facets and added
isDimConfigured, whose javadoc namesDEFAULT_DIM_CONFIG. A different defect. - java-user, 2014-06-22: a null result for a hierarchical
Publish Datedimension, caused by a differentFacetsConfigat search time (nosetHierarchical), which the javadoc forbids. Same symptom, different cause.
LFV-722 is a different bug with the same assumption in OrdinalMappingLeafReader.
| Where | When | How | Found |
|---|---|---|---|
| GitHub apache/lucene issues, PRs and commits (incl. the migrated JIRA archive); downstream repositories; dev@, java-user@, issues@, commits@; JIRA | 2026-10-27 | the scripted prior-report search, 3 runs (72 GitHub issue and PR, 9 commit, 6 downstream, 72 mailing-list and 18 JIRA queries) plus two earlier runs of 51 and 43 queries | only the related items above |
| GitHub by hand: apache/lucene, apache/lucene-solr, apache/solr, Lucene.NET, Elasticsearch, OpenSearch; JIRA; lists.apache.org; GitHub code search; Stack Overflow | 2026-10-27 | 41 read-only GitHub searches; the changed files of all 409 open apache/lucene PRs; 9 JQL enumerations with 13 issues grepped; 9 list terms with 37 messages read; a code search for getDefaultDimConfig; 13 Stack Overflow searches | nothing about this defect; the real downstream overrides use multiValued = true |
| Re-check for this doc: GitHub issues, PRs and commits, downstream trackers, the four lists, JIRA, open PRs, web | 2026-11-04 | the scripted prior-report search, 51 queries of which 45 ran (16 GitHub, 3 commit, 2 downstream, 20 mailing-list, 4 JIRA; 97 hits, all the items above or unrelated) and 6 web queries were left to a manual pass; 4 GitHub API searches; open PRs touching TaxonomyFacets or FacetsConfig; 3 manual web searches (2 answered, 1 throttled) | nothing new; getDefaultDimConfig has 0 hits on GitHub issues and PRs, commits, downstream repositories, JIRA and the dev@, java-user@ and issues@ lists |
The roll-up loop dates from LUCENE-5339 (7477fde1cb0, 2014, first released in 4.7.0); LUCENE-5479 (1ea87054f0d, 2014-03-01, 4.8.0) made the default overridable without touching it. Two version-portable probes fail on all 116 releases from 4.8.0 to 10.5.2; the minimal test, run on 9.1.0, 9.12.3, 10.5.2, 10.6.0 and main, fails on each. Not fixed upstream.
Versions tested8 rows
The loop. LUCENE-5339 ("Simplify the facet module APIs", #6403) wrote rollup() as a loop over config.getDimConfigs(): first 5458dbf2484 (2013-11-14, a development branch), into the 4.x line by 7477fde1cb0 (branch_4x, 2014-01-04; trunk 9d0b60388da; svn era, no PR). The trigger. LUCENE-5479 (#6542) added the protected getDefaultDimConfig(): 074923dda7e (trunk) and 1ea87054f0d (branch_4x), 2014-03-01, changing only FacetsConfig and its test. Later moves kept the loop: 458786d0f41 (2013), LUCENE-7994 77e6e291bf3 (2017), #12966 9ba4af7b883 (2024, into TaxonomyFacets) and its 9.x backport #13358. Not a regression.
Two version-portable main() probes index the same 3 documents and count them with an explicit-config control. Not fixed upstream: the loop is unchanged at TaxonomyFacets.java:269 on main f72004515ec (2026-11-03), branch_10_6 facfa0d52c9 (2026-11-01) and the 10.6.0 tag; no open PR changes it.
| Version or commit | Verdict | Result |
|---|---|---|
| 4.6.1 and older | no API | no FacetsConfig (the probes do not compile) |
| 4.7.0, 4.7.1, 4.7.2 | getDimConfig override only | no getDefaultDimConfig yet; a getDimConfig override that makes unconfigured dimensions hierarchical fails |
| 4.8.0 | first release | probe: top=null, value(2010)=0, value(dim)=0, drill-down 2 hits; control correct |
| 4.8.1 to 10.5.2 (115 releases, incl. 8.11.4) | affected | both probes: the same wrong output on every release; the control correct in every run that compiled |
| 9.1.0, 9.12.3, 10.5.2 | affected | minimal test fails |
| 10.6.0 | affected | minimal test fails, assertions on and off |
main 5adff78bb168 | affected | minimal test fails (gradle and JUnit runner, assertions on and off; 20 of 20 iterations) |
| main + recommended fix | not affected | minimal test passes; facet module 307 tests, 0 failures |
Before this doc: the finder's cross-model review (OpenAI GPT-5.6, refute lens), an independent reproducer, a second cross-model review (intended lens), an intended-behaviour devil's advocate and an adjudication all rated it real (the cross-model reviews, the devil's advocate and the adjudication with high confidence). This doc re-ran it on 5adff78bb168 and 10.6.0 and validated the fix with gradle.
Checks and verdicts281 words
The reviews and verifiers ran on main a9779ef3c193 (the cross-model reviews on ea57360912ed, whose facet sources are identical); a re-verification on e1faeba08bde (2026-11-02) and this doc's runs on 5adff78bb168 reproduced it.
| Check | Verdict | What it found |
|---|---|---|
| finder | found | Bounded-exhaustive check, then a 2-test JUnit: the default-config test fails with assertions on and off, also on 10.5.2 and 9.12.3; its explicit-config control passes. |
| cross-model review (OpenAI GPT-5.6, refute lens, executed) | real (high) | Own test fails on main (4 seeds, assertions on and off), 10.5.2 and 9.12.3. It rejected the first fix sketch, which walked the taxonomy eagerly and broke a laziness test; the fix was rewritten to stay lazy. |
| independent reproducer | real | Own 3-test JUnit (both constructors, the depth-1 toys case) fails on main, 10.5.2 and 9.12.3; an isolation test shows the index is right and only the roll-up differs. |
| cross-model review (OpenAI GPT-5.6, intended lens, executed) | real (high) | Rated the intended-behaviour case weak; its own test fails only the default-override taxonomy case and passes with the fix. |
| intended-behaviour devil's advocate | real (high); framing medium | A getDimConfig override fails the same way; SortedSetDocValues and multiValued controls pass; found the sandbox comment (#14237) as the strongest counterpoint and LUCENE-5801 as precedent. |
| adjudication | confirmed (high) | Re-ran the independent test with new seeds (fails with assertions on and off, passes with the fix) and re-read the root cause. |
| this doc | reproduced; fix validated | One-method test fails on main 5adff78bb168 and on 10.6.0, 10.5.2, 9.12.3 and 9.1.0; the value-source path, earlier covered by code reading only, was executed and fails the same way; the recommended fix passes the facet module's 307 tests. |
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 587 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,451 words
- Registry title
- Taxonomy facets never roll up a single-valued hierarchical dimension whose config comes from an overridden FacetsConfig#getDefaultDimConfig, so its dimension and non-leaf counts are 0 and getTopChildren returns null or partial results
- Area
- facet / taxonomy facets (TaxonomyFacets#rollup dimension enumeration vs the overridable FacetsConfig default dimension config; FastTaxonomyFacetCounts, TaxonomyFacetFloatAssociations)
- Priority
- Medium, rank 178: Facet dimension and parent counts come out silently as 0 or null, but only with an opt-in hierarchical getDefaultDimConfig override and with an easy-to-see symptom.
- Impact class
- silent wrong results
- Impact note
- Hits applications that subclass FacetsConfig and override the protected getDefaultDimConfig (or the public getDimConfig) to return a hierarchical, single-valued DimConfig, as Lucene's own TestFacetsConfig#testCustomDefault (LUCENE-5479) does, and then count such a dimension that has no explicit entry with taxonomy faceting (FastTaxonomyFacetCounts, both constructors, and the value-source aggregation of TaxonomyFacetFloatAssociations, which calls the same rollup(), all executed): the dimension count and every non-leaf label count read 0 and getTopChildren(dim) returns null or only the depth-1 leaves, with no error, while leaf counts, drill-down, SortedSetDocValues faceting and explicitly configured dimensions stay correct; opt-in, no scale needed, deterministic with -ea and -da, every release since 4.7.0 (4.8.0 through the documented extension point).
- Summary
- You subclass FacetsConfig so that every dimension without an explicit entry is hierarchical (override getDefaultDimConfig to return a DimConfig with hierarchical = true, as TestFacetsConfig#testCustomDefault does), index three documents Publish Date/2010/10/15, Publish Date/2010/10/20 and Publish Date/2012/1/1, count them with FastTaxonomyFacetCounts, and expect what an explicit setHierarchical("Publish Date", true) gives: value=3, 2010 (2), 2012 (1). FacetsConfig.build follows the default and indexes only the leaf ordinals, relying on the search-time roll-up, but TaxonomyFacets.rollup() loops over config.getDimConfigs(), which lists only explicitly configured dimensions, so the roll-up never runs. getTopChildren(10, "Publish Date") returns null and getSpecificValue("Publish Date") and getSpecificValue("Publish Date", "2010") return 0, while a DrillDownQuery on Publish Date/2010 still matches 2 documents.
- Introduced
- 7477fde1cb063b5c849369afa531bb28df4566fc (branch_4x, svn r1555342, 2014-01-04, committed by mikemccand: 'LUCENE-5371, LUCENE-5339: ... simplify facet APIs'; trunk merge 9d0b60388da; LUCENE-5339 = apache/lucene#6403; Subversion era, no pull request) brought the reworked facet API into the 4.x line with rollup() iterating config.getDimConfigs(), i.e. only explicitly configured dimensions. The loop was first written in 5458dbf2484 (lucene5339 development branch, 2013-11-14) and was later only moved (458786d0f41, 2013-11-27, into IntTaxonomyFacets/FloatTaxonomyFacets; 9ba4af7b883, #12966 by stefanvodita, 2024-04-05, into TaxonomyFacets, 9.x backport 42884fab46d #13358) without changing what it enumerates; LUCENE-7994 (77e6e291bf3, 2017) also kept it. The documented trigger arrived with 074923dda7e (trunk) / 1ea87054f0d (branch_4x), 2014-03-01, LUCENE-5479 = #6542, which added the protected getDefaultDimConfig and changed only FacetsConfig and its test (testCustomDefault sets hierarchical = true), not the roll-up [read: git log -S'getDimConfigs().entrySet()', git blame -w -C -C -M and git tag --contains in the history clone]. Not a regression: a latent assumption from when the only default was the static, flat DEFAULT_DIM_CONFIG (2014-01-04 (7477fde1cb0; loop first written 2013-11-14 on the lucene5339 branch); documented trigger 2014-03-01 (LUCENE-5479))
- First affected version
- 4.7.0 [exec] through an override of the public FacetsConfig#getDimConfig (the workaround the LUCENE-5479 report quotes; fails on 4.7.0, 4.7.1 and 4.7.2); 4.8.0 [exec] through the documented protected getDefaultDimConfig, which first exists in 4.8.0. 4.6.1 and older have no FacetsConfig API [exec: the probes do not compile and the jars contain no FacetsConfig.class]
- Affected versions
- Every lucene-facet release on Maven Central from 4.7.0 to 10.5.2 (119 releases, including 8.11.4 and 9.12.3) [exec, getDimConfig-override probe]; 4.8.0 to 10.5.2 (116 releases) also with the getDefaultDimConfig override [exec]; main a9779ef3c193 [exec, -ea and -da]. The wrong output is identical in every affected run and the explicit-config control is correct in all 239 runs that compiled. The finder's JUnit fails on 9.1.0, 9.12.3, 10.0.0, 10.5.2 and main (it does not compile on 9.0.0 and older because of the test-framework package rename; the main() probes cover those). Not fixed: the loop is unchanged on upstream main a95c754ee00 (2026-10-26), branch_10x 1309534296e and branch_10_6 2763db11b0a (contained in 10.6.0, released 2026-10-29) [read]; the released 10.6.0 is affected too [exec, 2026-11-04: the minimal test fails on its jars with -ea and -da, and on main 5adff78bb168], and no open PR changes rollup() or getDimConfigs(). All runs on 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, code reading
- Upstream and prior reports
- related items only, no report of this defect (searched 2026-10-27, read-only, re-checked 2026-11-03; 301 queries: three scripted prior-report searches over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web, plus the finder's 51-query run and the intended-behaviour check's 43-query run; 41 manual GitHub issue searches over apache/lucene, lucene-solr, solr, lucenenet, Elasticsearch and OpenSearch; the changed files of all 409 open apache/lucene PRs; 9 JQL enumerations with 13 issues grepped; 9 list terms with 37 messages read; a GitHub code search for getDefaultDimConfig; Stack Overflow). No issue, PR, commit, JIRA entry, mailing-list thread, Q&A post or web page reports the core miscount. The closest related item: merged sandbox PR #14237 (epotyom, 2025-03-03; in 10.2.0 to 10.5.2), whose TaxonomyFacetBuilder constructor (lucene/sandbox, TaxonomyFacetBuilder.java:47-58 on a9779ef3c193) throws unless the dimension is configured explicitly, with the comment that the sandbox TaxonomyFacetsCutter 'uses FacetConfig#getDimConfigs to find dimensions that need to be rolled up, hence it needs the dimension to be configured explicitly'; it concerns the sandbox cutter (a different, out-of-scope fix site, with the flat default as its trigger), says nothing about core TaxonomyFacets, getDefaultDimConfig or wrong counts, and the PR discussion does not mention it. Related, not this bug: #6863 (LUCENE-5801, 2014, fixed: OrdinalMappingAtomicReader collected facet fields only from getDimConfigs(), reported as 'wrong facet data'; precedent, and the fix site of another finding, LFV-722), #11780 (open draft; a 2023 review comment by gsmiller that a getDimConfigs() enumeration does not know fields that use the default dim config, in an unmerged SortedSetDocValues helper), #6542 (LUCENE-5479, added getDefaultDimConfig; discussion only about multiValued = true), #6403 (LUCENE-5339), #12966 and #13358 (moved the loop), #6599 (LUCENE-5536, unrelated), and a 2014 java-user thread with the same symptom caused by different FacetsConfig instances at index and search time. Not fixed: no open PR changes rollup() or getDimConfigs(); upstream main (2 commits past a9779ef3c193, neither in facet), branch_10x and branch_10_6 are unchanged. Downstream: the getDefaultDimConfig overrides found (seecr/meresco-lucene, voyanttools/trombone) all use multiValued = true, which is unaffected. Gaps: general web search was only partly available (the 18 planned web queries were not run), GitHub search reads the first 100 results per query, solr-user@ was not read
- Lucene commit tested
- main
5adff78bb168(gradle in a scratch worktree and the JUnit runner, assertions on and off) and the released 10.6.0, 10.5.2, 9.12.3 and 9.1.0 jars, 2026-11-04. Earlier runs: maina9779ef3c193andea57360912ed(identical facet sources) ande1faeba08bde; two version-portable probes on every lucene-facet release from 4.7.0 to 10.5.2.
- Finding record
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:261-285 (at a9779ef3c193) (rollup(), javadoc 'Rolls up any single-valued hierarchical dimensions', loop over config.getDimConfigs().entrySet() at :269) and :537-541 (getSpecificValue: 'ok: rolled up at search time'); lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:141-151 (protected getDefaultDimConfig and its javadoc), :153-160 (getDimConfig falls back to it), :162-168 (isDimConfigured javadoc still names DEFAULT_DIM_CONFIG), :228-231 (getDimConfigs returns only the explicit map), :370-398 (processFacetFields uses getDimConfig; ancestors added only when multiValued, :386-398); taxonomy/FastTaxonomyFacetCounts.java:117 and :174 and taxonomy/TaxonomyFacetFloatAssociations.java:165 (callers of rollup()); lucene/facet/src/test/org/apache/lucene/facet/TestFacetsConfig.java:104-115 (testCustomDefault, LUCENE-5479, hierarchical = true); lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/TaxonomyFacetBuilder.java:47-58 (#14237 comment and check)
- The finding in 30 seconds
lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:142-160 (at 5adff78bb168) (getDefaultDimConfig, getDimConfig), :228-231 (getDimConfigs), :370-398 (processFacetFields)lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:261-285 (at 5adff78bb168) (rollup, loop at :269), :308-310, :478-503, :540-541, :553, :564-578lucene/facet/src/java/org/apache/lucene/facet/taxonomy/FastTaxonomyFacetCounts.java:93, (at 5adff78bb168) :117, :138, :174; TaxonomyFacetFloatAssociations.java:164-165; lucene/facet/src/java/org/apache/lucene/facet/DrillSideways.java:159, :165lucene/facet/src/java/org/apache/lucene/facet/taxonomy/OrdinalMappingLeafReader.java:136; (at 5adff78bb168) lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/cutters/TaxonomyFacetsCutter.java:119; lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/TaxonomyFacetBuilder.java:48-59lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:595-660 (at 5adff78bb168) (getTopDims); TaxonomyFacetFloatAssociations.java:54-61, :84-93, :169 and TaxonomyFacetIntAssociations.java:41-62 (indexed-association constructors and aggregation, no rollup() call; the only rollup() call in the class is the value-source path, :165)
- Minimal repro
Lucene main 5adff78bb168 build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/TestsAndRandomizationPlugin.java:240-248 (tests.asserts default drawn from the seed)
- Why it looks unintended
lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:49-51 (at 5adff78bb168) (class javadoc), :142-151 (getDefaultDimConfig javadoc), :162-168 (isDimConfigured javadoc), :371-378lucene/CHANGES.txt:12462-12463 (at 5adff78bb168) (LUCENE-5479, 4.8.0)lucene/facet/src/test/org/apache/lucene/facet/TestFacetsConfig.java:104-117 (at 5adff78bb168) (testCustomDefault)lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:261 (at 5adff78bb168) (rollup javadoc), :541
- Suggested fix
lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetCounts.java:451-452; (at 5adff78bb168) TestTaxonomyFacetValueSource.java:520-521 (laziness assertions)lucene/facet/src/java/org/apache/lucene/facet/FacetField.java:69-73 (at 5adff78bb168) (empty dimension rejected)
- Diagrams
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:261-285; (at 5adff78bb168) lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:228-231, :370-398
- TestHierarchicalDefaultDimConfig.java (one test method):
repro/LFV-723/TestHierarchicalDefaultDimConfig.java(70 lines)
A walk-through of the minimal repro's input, made from the code at 5adff78bb168 and the executed runs; the PNG is the summary still.
assets/LFV-723/bug.png(587 KB)
