- The taxonomy gives every path a number, its ordinal:
Date,Date/2010andDate/2010/10each get one. FacetsConfig.buildstores the ordinals of a document's labels in a doc-values field; facet counting reads them.TaxonomyFacetLabelsreads a document's stored ordinals and turns each one back into its path.
Date/2010/10: with Date multi-valued, FacetsConfig also stores the ordinal of Date/2010, and TaxonomyFacetLabels turns both stored ordinals back into labels. Select the still to open it at full size. Generated with AI, checked against the code.- Mark
Datehierarchical and multi-valued (FacetsConfig.setHierarchical,setMultiValued) and index one document withnew FacetField("Date", "2010", "10"). - Read its labels with
TaxonomyFacetLabels, whose javadoc says it lets you "skip also adding stored fields for these values". You expect the one label you added,[Date/2010/10]; withoutsetMultiValuedthat is what you get. - Instead you get
[Date/2010, Date/2010/10]. For multi-valued dimensionsFacetsConfigstores every ancestor with the document so counts are right, and the reader turns every stored entry back into a label. - With
setRequireDimCountthe bare dimension comes back too: a multi-valuedAuthorgivenAuthor/BobreturnsAuthorandAuthor/Bob. Counts, drill-down and the index are correct; nothing throws.
Applications that show per-hit facet labels from TaxonomyFacetLabels instead of stored fields, as its javadoc suggests, for a dimension set multi-valued plus hierarchical (categories, folder paths) or multi-valued plus requireDimCount. A path with d components below the dimension comes back as d labels. Default single-valued dimensions, counts and (default) drill-down are unaffected. Every release since 8.7.0.
Mechanism, terms and numbers480 words
TaxonomyFacetLabels (since 8.7.0) is documented as a way to "retrieve previously indexed facet labels, allowing you to skip also adding stored fields for these values". For a dimension configured multi-valued plus hierarchical, it also returns every ancestor of each added path (Date/2010/10 reads back as [Date/2010, Date/2010/10]), and for multi-valued plus requireDimCount the bare dimension (Author). FacetsConfig indexes these on purpose so that multi-valued counts are right, and after dedup they cannot be told apart from labels the user added, so this is a documentation/contract defect: the index and the counts are correct, and the fix is a javadoc note.
- Hierarchical dimension (
FacetsConfig.setHierarchical): paths may have more than one component below the dimension, such asDate/2010/10. - Multi-valued dimension (
setMultiValued): "this dimension may have more than one value per document" (FacetsConfig.java:180). requireDimCount (setRequireDimCount): "you require accurate counts of the dimension" (:190-193). All three default to false (:113-124).
- For
multiValued && (hierarchical || requireDimCount),FacetsConfig.processFacetFieldsadds the ordinal of every ancestor of each added path ("Add all parents too"), and removes the dimension's own ordinal unlessrequireDimCountis set (FacetsConfig.java:386-398). It then sorts and dedupes the ordinals (:403-415). - This is intended:
TaxonomyFacets.rollupadds child counts into parents only for hierarchical single-valued dims (TaxonomyFacets.java:272), because a document withDate/2010/10andDate/2010/11would be counted twice atDate/2010. Multi-valued dims store each ancestor once per document instead (LUCENE-5339, 4.7.0). FacetLabelReader.nextFacetLabel(docId)returnstaxoReader.getPath(ord)for every stored ordinal, with no filter (TaxonomyFacetLabels.java:111-140). The reader receives noFacetsConfig(constructor:45-48), and the ordinals carry no record of who added them.- So the ancestors come back as labels. The dim-filtered
nextFacetLabel(docId, dim)keeps proper descendants of the dimension (:142-150,:207-213): it returns the ancestors too, but never the bare dimension. - After dedup, a document given only
Date/2010/10and one givenDate/2010andDate/2010/10store identical ordinals (executed), so no general reader-side filter can drop only the implicit ones. The dimension setting alone triggers it: the repro's document has one value.
| dimension config | labels added | labels returned |
|---|---|---|
Date multi-valued + hierarchical (the repro) | Date/2010/10 | [Date/2010, Date/2010/10] |
Date hierarchical only (control) | Date/2010/10 | [Date/2010/10] |
Publish Date multi-valued + hierarchical | Author/Bob, Publish Date/2010/10/15 | [Author/Bob, Publish Date/2010, Publish Date/2010/10, Publish Date/2010/10/15] |
Author multi-valued + requireDimCount | same | [Author, Author/Bob, Publish Date/2010/10/15] |
Category multi-valued + hierarchical | Category/Books, Category/Electronics/Phones/Android | [Category/Books, Category/Electronics, Category/Electronics/Phones, Category/Electronics/Phones/Android] |
Color multi-valued + requireDimCount | Color/blue, Color/red | [Color, Color/blue, Color/red] |
The table lists each document's labels as a set; the reader returns them in ordinal order, and its javadoc says they "may not be in the same order in which they were indexed" (TaxonomyFacetLabels.java:102-103). The requireDimCount rows use non-hierarchical dims, which accept only one-component paths such as Author/Bob (FacetsConfig.java:371-377 throws for more).
Exposure and likelihood170 words
- Who. Code that lists a hit's labels (breadcrumbs, tag lists) from this class on a dimension configured
setMultiValuedplussetHierarchicalor plussetRequireDimCount. A document given only a deeper path, such asDate/2010/10, shows ancestors it was never given, deterministically, with assertions on and off; a document that also added those ancestors explicitly sees no difference. - Opt-in. Every trigger needs non-default
FacetsConfigsettings, and the class is@lucene.experimental. The default codec,IndexWriterConfigand taxonomy suffice. - Scale.
Date/2010/10returns 2 labels,Category/Electronics/Phones/Android3;requireDimCountadds the dimension; shared ancestors of several values come back once. - Not affected. Counts; drill-down with the default
DrillDownTermsIndexing.ALL(DrillDownQuerymatches the ancestor in both configurations); the index; single-valued dims. With a non-default drill-down indexing that skips sub-paths, such asFULL_PATH_ONLY, aDrillDownQueryon a returned ancestor label matches nothing (executed). In the Lucene tree only tests use the class; GitHub code search of the default branches of Elasticsearch, OpenSearch and Solr found no use ofTaxonomyFacetLabels,DirectoryTaxonomyReaderorDirectoryTaxonomyWriter(2026-11-01).
One JUnit test, one method, public API only: one document with Date/2010/10 in a multi-valued hierarchical dimension, read back with TaxonomyFacetLabels. Deterministic, single thread; fails with assertions on and off.
/*
* 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 java.util.ArrayList;
import java.util.List;
import org.apache.lucene.document.Document;
import org.apache.lucene.facet.FacetField;
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.index.IndexWriterConfig;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestTaxonomyFacetLabelsMultiValuedHierarchical extends LuceneTestCase {
public void testLabelsOfMultiValuedHierarchicalDim() throws Exception {
FacetsConfig config = new FacetsConfig();
config.setHierarchical("Date", true);
config.setMultiValued("Date", true);
Document doc = new Document();
doc.add(new FacetField("Date", "2010", "10"));
try (Directory dir = new ByteBuffersDirectory();
Directory taxoDir = new ByteBuffersDirectory();
IndexWriter w = new IndexWriter(dir, new IndexWriterConfig());
DirectoryTaxonomyWriter tw = new DirectoryTaxonomyWriter(taxoDir)) {
w.addDocument(config.build(tw, doc));
try (DirectoryReader r = DirectoryReader.open(w);
TaxonomyReader tr = new DirectoryTaxonomyReader(tw)) {
TaxonomyFacetLabels.FacetLabelReader labels =
new TaxonomyFacetLabels(tr, FacetsConfig.DEFAULT_INDEX_FIELD_NAME)
.getFacetLabelReader(r.leaves().get(0));
List<FacetLabel> actual = new ArrayList<>();
for (FacetLabel l = labels.nextFacetLabel(0); l != null; l = labels.nextFacetLabel(0)) {
actual.add(l);
}
// expected only the added label Date/2010/10, actual also returns its ancestor Date/2010
assertEquals(List.of(new FacetLabel("Date", "2010", "10")), actual);
}
}
}
}
cp $REPRO/TestTaxonomyFacetLabelsMultiValuedHierarchical.java $LUCENE/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/
cd $LUCENE
./gradlew :lucene:facet:test --tests org.apache.lucene.facet.taxonomy.TestTaxonomyFacetLabelsMultiValuedHierarchical -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same failure)
- Expected
[Date/2010/10], the one label the document was given- Observed
[Date/2010, Date/2010/10]: the ancestor that FacetsConfig indexes for counting comes back as a label
More files, environment, full output186 words
- Input. One dimension, one two-level path,
ByteBuffersDirectory, defaultIndexWriterConfig, no randomness; the package is for locality only. - Assertions. Pass
-Ptests.assertsexplicitly: the build picks assertions from the seed, and forDEADBEEFit picks off. - Releases. The same file fails unchanged on 9.1.0, 9.12.3, 10.0.0, 10.5.2 and 10.6.0; before 9.1 only the
LuceneTestCasepackage differs (the finder's class fails on 8.7.0 to 9.0.0 too). - Regression test (optional for a javadoc fix): this same method with the expected list set to the documented output,
List.of(new FacetLabel("Date", "2010"), new FacetLabel("Date", "2010", "10")); it passes with assertions on and off.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also fails on 9.1.0, 9.12.3, 10.0.0, 10.5.2 and 10.6.0 |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux, aarch64 |
cp $REPRO/TestTaxonomyFacetLabelsMultiValuedHierarchical.java $LUCENE/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/
cd $LUCENE
./gradlew :lucene:facet:test --tests org.apache.lucene.facet.taxonomy.TestTaxonomyFacetLabelsMultiValuedHierarchical -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same failure)
# gradle, main 645d4fe14a20, seed DEADBEEF, -Ptests.asserts=true (the same with false)
TestTaxonomyFacetLabelsMultiValuedHierarchical > testLabelsOfMultiValuedHierarchicalDim FAILED
java.lang.AssertionError: expected:<[FacetLabel: [Date, 2010, 10]]> but was:<[FacetLabel: [Date, 2010], FacetLabel: [Date, 2010, 10]]>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:70C10E831672F2CA]:0)
at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
at junit@4.13.2/org.junit.Assert.failNotEquals(Assert.java:835)
at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:120)
at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:146)
at org.apache.lucene.facet.taxonomy.TestTaxonomyFacetLabelsMultiValuedHierarchical.testLabelsOfMultiValuedHierarchicalDim(TestTaxonomyFacetLabelsMultiValuedHierarchical.java:56)
1 test completed, 1 failed- The javadoc offers the class to "skip also adding stored fields for these values", and stored fields return exactly what was added.
- Upstream tests use only single-valued dims and expect exactly the added labels; flipping only
setMultiValuedchanges the labels. - The ancestors are indexed on purpose and indistinguishable after dedup, so the defect is the missing documentation, not the code.
The full argument191 words
- Contract. Class javadoc (
TaxonomyFacetLabels.java:27-29). CHANGES 8.7.0: "LUCENE-9444: Add utility class to retrieve facet labels from the taxonomy index for a facet field so such fields do not also have to be redundantly stored". Issue #10484: "A facet field may be included in the list of fields whose values are to be returned for each hit"; a comment by mikemccand (2020-08-01): "We want to make it easy for users to get back their originally indexed facet labels." - Tests.
TestTaxonomyFacetLabels.testBasicsets onlysetHierarchicaland asserts exactly 10 labels for 5 documents with 2 each;TestTaxonomyFacetCounts.testRandomuses a plainFacetsConfigand expects exactly the added labels. No test, review comment or decision covers multi-valued hierarchical or requireDimCount dims. - Metamorphic. For one document, flipping only
setMultiValued(documented as "may have more than one value per document") turns[Category/Electronics/Phones]into[Category/Electronics, Category/Electronics/Phones](executed). - Through
FacetField, the bare dimension is never a user label:new FacetField("Author")throws "path must have at least one element", so in a field built byFacetsConfigit comes only from requireDimCount. Code that bypassesFacetsConfig(TaxonomyWriter.addCategory(new FacetLabel("Author"))plus its ownSortedNumericDocValuesField) can still index one on purpose.
The devil's-advocate check146 words
Strongest case for "works as designed": the javadoc says previously indexed, and the ancestors are indexed; the demo user guide says "Multi value support requires storing all parent category ordinals in the category list field for accurate counting"; the reader takes no FacetsConfig; ordinals carry no record of who added them; the class is @lucene.experimental.
Why it fails for the documentation: it defeats a code-bug framing, which is why this is classed as a documentation defect, but not the gap itself. The guide sentence is about counting, lives in the demo module, is not linked from TaxonomyFacetLabels and says nothing about the dimension ordinal; a user who picks the class over stored fields cannot learn that the labels differ from what they added. Cross-model reviews (OpenAI GPT-5.6) split the same way: not a code bug (refute lens), a real documentation defect (works-as-designed lens).
Recommended A. Document the ancestor and dimension labels in the class javadoc
Document it: a TaxonomyFacetLabels javadoc note that multi-valued hierarchical (or requireDimCount) dimensions also return the ancestor labels, and the dimension, indexed for counting, indistinguishable from added ones; also fix the stale BinaryDocValues @param. Validated: format, lint and javadoc pass, TestTaxonomyFacetLabels passes.
All options and trade-offs3 options ยท 667 words
No general reader-side filter can remove only the implicit ancestors: after dedup they are indistinguishable from ancestors the user added, and the reader receives no FacetsConfig. An application that never adds ancestors explicitly can filter them itself, or keep stored fields for those dims.
A. Document the ancestor and dimension labels in the class javadoc (validated by the checks) (recommended)
Add a NOTE paragraph to the class javadoc, with the Date/2010/10 example, and change the getFacetLabelReader @param from BinaryDocValues to SortedNumericDocValues (stale since 9.0.0).
* Utility class to easily retrieve previously indexed facet labels, allowing you to skip also
* adding stored fields for these values, reducing your index size.
*
+ * <p><b>NOTE</b>: a label is returned for every taxonomy ordinal indexed for the document, which is
+ * not always exactly the labels that were added. For a dimension that is {@link
+ * org.apache.lucene.facet.FacetsConfig#setMultiValued(String, boolean) multi-valued} and also
+ * {@link org.apache.lucene.facet.FacetsConfig#setHierarchical(String, boolean) hierarchical} or
+ * {@link org.apache.lucene.facet.FacetsConfig#setRequireDimCount(String, boolean) requires the
+ * dimension count}, {@link org.apache.lucene.facet.FacetsConfig} also indexes the ancestors of
+ * every added path, so that counts are correct, and these are returned too: a document given only
+ * {@code Date/2010/10} returns {@code Date/2010} and {@code Date/2010/10}. With {@code
+ * requireDimCount}, the dimension itself ({@code Date}) is indexed and returned as well, except by
+ * {@link FacetLabelReader#nextFacetLabel(int, String)}. Such ancestors cannot be told apart from
+ * ancestors that were added explicitly; keep stored fields for these dimensions if you need exactly
+ * the added labels.
+ *
* @lucene.experimental
*/
...
- * @param readerContext LeafReaderContext used to access the {@code BinaryDocValues} facet field
+ * @param readerContext LeafReaderContext used to access the {@code SortedNumericDocValues} facet
+ * fieldTrade-offs
- No behaviour change, no risk.
- Optionally add one line to the demo guide's
MultiValuedbullet and toFacetsConfig#setMultiValuedpointing at the label consequence.
What the checks validated
Scratch worktree at 645d4fe14a20: checkGoogleJavaFormat, ecjLintMain and renderJavadoc for :lucene:facet pass (the independent fact check found the note's five links resolving in the rendered page); TestTaxonomyFacetLabels passes; the minimal test still fails, as it must; the same method with the documented expectation passes with assertions on and off.
In a field built by FacetsConfig, a length-1 label can only come from requireDimCount (FacetField needs at least one path component), and the dim-filtered overload already excludes it. null passes through as before.
assert currentPos <= currentDocOrdinalCount;
- if (currentPos == currentDocOrdinalCount) {
- return null;
+ while (currentPos < currentDocOrdinalCount) {
+ int ord = (int) ordinalValues.nextValue();
+ currentPos++;
+ FacetLabel label = taxoReader.getPath(ord);
+ // Skip the bare dimension label: FacetsConfig indexes it only for requireDimCount
+ // counting, and a FacetField always has at least one path component
+ if (label == null || label.length > 1) {
+ return label;
+ }
}
-
- int ord = (int) ordinalValues.nextValue();
- currentPos++;
-
- return taxoReader.getPath(ord);
+ return null;Trade-offs
- A behaviour change in an experimental API that someone might rely on (for example to list a document's dims); it fixes only the requireDimCount half.
- It changes the generic meaning of a public reader and would also hide a bare dimension that code bypassing
FacetsConfigindexed on purpose; passing the facet suite does not show it is compatible for such callers. - With B, reword A's requireDimCount sentence.
What the checks validated
On top of A: format, ECJ lint and javadoc pass; the whole :lucene:facet:test suite passes (306 tests, 7 skipped); the independent reproducer's requireDimCount test ([Color, Color/blue, Color/red] on stock main) passes, while its three ancestor tests and the minimal test still fail, as expected.
A leaf-only filter would silently drop ancestors the user added (indistinguishable after dedup). Recording which ordinals were added explicitly needs an index-format change and extra storage, out of proportion for an experimental utility. Workaround, not a Lucene fix: an application that never adds ancestors explicitly can apply the leaf-only filter itself.
Downstream code that shows these labels to users displays ancestors that a document given only a deeper path was never tagged with; an application that indexes untrusted, very deep paths into such a dimension should cap the depth, since a path with d components below the dimension reads back as d labels.
Non-formal: model-based metamorphic testing of the facet API. One relation says the labels TaxonomyFacetLabels returns for a document equal the FacetField paths added to it; 400 random iterations (327,122 checks) reported 3,010 mismatches. Independent verifiers reproduced it with their own tests.
The method in full101 words
- Method. A random model-based harness indexed random documents under random dimension configurations and checked the public facet API against a reference model written from the javadoc, independent of the implementation. Relation F7: the labels returned for a document equal the paths added to it. Run on main: 400 iterations, seed 2, 327,122 checks, 3,010 F7 mismatches (extra ancestor and dimension labels).
- Verification. The finder's public-API JUnit, an independent reproducer (7 tests), cross-model reviews (OpenAI GPT-5.6, reading only), a devil's advocate with probes, an adjudication re-run and a follow-up with a release matrix; the minimal repro then cut it to one method.
No prior public report found in the recorded searches (2026-10-26, re-checked 2026-11-01): GitHub issues and PRs incl. the JIRA archive, commits, mailing lists, Elasticsearch, OpenSearch, Solr, the web and the files of all 407 open PRs. Closest: #10484 / lucene-solr #1893, which added the class, and LUCENE-5339, which added the parent ordinals.
Searches4 searches
The demo facet user guide's sentence on storing parent ordinals "for accurate counting" documents the index side, not label retrieval, and is not a report. Open PR #12862 touches TaxonomyFacetLabels.java (one import) without a behaviour change. A keyword search cannot prove that no report exists.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene, incl. the migrated JIRA archive), all of GitHub | 2026-10-26 | a scripted prior-report search (80 queries, 194 distinct hits) plus manual searches for TaxonomyFacetLabels, FacetLabelReader, nextFacetLabel | related only: #10484, lucene-solr #1893 and #1928, #264, #443, #11100, #6403, #12862 |
| Threads read in full | 2026-10-26 | #10484 (31 comments), reviews of lucene-solr #1893 and #1928, #264, #443, #12995; LUCENE-5339 (90 comments) | ancestors, multi-valued and requireDimCount dims are never discussed for label retrieval |
| Open PRs, mailing lists, downstream, web | 2026-10-26 | changed files of all 407 open PRs; dev@, java-user@, issues@, commits@; Elasticsearch, OpenSearch, Solr; web | no report; one 2016 Q&A page (older than the class) and one slide deck could not be read |
| Re-check (independent fact check) | 2026-11-01 | GitHub issue and PR search of apache/lucene for TaxonomyFacetLabels, FacetLabelReader, nextFacetLabel; git diff of lucene/facet to upstream main f815e679bf6c | only #10484 and #11100; facet module unchanged; #16937 and #16950 do not concern facets |
Introduced with the class by LUCENE-9444 (lucene-solr #1893, 24aadc220ba9 and backport d71923065dba, 2020-09-28); first released in 8.7.0. Every executed release from 8.7.0 to 10.6.0 and main fails the same way; 8.6.3 has no such class. Not a regression; not fixed upstream.
Versions tested7 rows
Introduced by 24aadc220ba9 "LUCENE-9444: add utility class to retrieve facet labels from the taxonomy index for a facet field (#1893)" (goankur, 2020-09-28, master, first released in 9.0.0) and its branch_8x backport d71923065dba (first released in 8.7.0). The ancestor indexing is older and intended: the current "Add all parents too" loop comes from LUCENE-5339 (2013), first released in 4.7.0 through the branch_4x commit 7477fde1cb0 (2014-01-04); on the feature branch it appeared in 5458dbf2484 and moved into FacetsConfig in 59c08c3fa13b, which git tag --contains first lists in 6.0.0. 4.6.0 already had parent-ordinal policies (CategoryListParams.OrdinalPolicy). Later commits (7f8b7ffbcad2 LUCENE-9570, the LUCENE-9856 cleanups, LUCENE-10062 (#443 5fe8f0e3f18 in 9.0.0, 0ba310782fb in 9.1.0; on main #264 6ee69e06fbd1, first released in 10.0.0), 2a0b7f20567 #12995) keep the behaviour; LUCENE-10062 left the BinaryDocValues @param stale.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | fails, assertions on and off (gradle and standalone) |
| 10.6.0 | affected | fails (minimal test) |
| 9.1.0, 9.12.3, 10.0.0, 10.5.2 | affected | fail (minimal test and the finder's class) |
| 8.7.0, 8.11.4, 9.0.0 | affected | fail (the finder's class, identical values) |
| 8.6.3 | no class | does not compile: TaxonomyFacetLabels does not exist yet |
fix A on 645d4fe14a20 | documented | behaviour unchanged by design; the documented expectation passes |
fix B on 645d4fe14a20 | dimension only | the bare dimension is gone; ancestors still returned |
The finder, an independent reproducer, a devil's advocate, an adjudication, a follow-up re-run, an audit and this doc's gradle runs all reproduce it. Cross-model reviews (OpenAI GPT-5.6) agreed on the cause: not a code bug, a real documentation defect.
Checks and verdicts291 words
Neither cross-model review ran Java itself; the executed evidence comes from the other checks, all on aarch64 with JDK 25.
| Check | Verdict | What it found |
|---|---|---|
| finder | reproduced | Model run: 3,010 mismatches; its JUnit fails 2 of 3 on main (assertions on and off), 10.5.2, 9.12.3, 9.1.0. |
| cross-model review, refute lens | not real as a code bug | OpenAI GPT-5.6, reading only: the ancestors are stored on purpose; agreed on the root cause; moved the category to doc. |
| Independent reproducer | real (doc) | Own 7 tests fail 4 of 7 on main, 10.5.2, 9.12.3; the bare dimension leaks only through the unfiltered overload. |
| Works-as-designed lens | real (doc), medium | OpenAI GPT-5.6; a separate executed test confirmed its two code-reading claims. |
| Devil's advocate | real (doc) | Reproduced on 8.7.0; probes: identical ordinals with or without an explicit ancestor; drill-down matches in both configs. |
| adjudication | confirmed, medium | Re-ran the independent reproduction's class: 4 of 7 fail with assertions on and off; category doc, impact minor. |
| Follow-up, re-runs and audit | reproduces | Release matrix 8.6.3 to main; re-runs 2 of 3 and 4 of 7; a credibility audit agreed. |
| minimal repro and this doc | verified | One method; fails on main (gradle and standalone, assertions on and off) and 5 releases; fixes A and B validated. |
| independent fact check of this doc | real, one provenance error (fixed) | Re-ran the repro (standalone and gradle, assertions on and off), 9.0.0 to 10.6.0 and 8.7.0, and fixes A and B; two commit hashes were paired with the wrong releases. |
| cross-model fact check of this doc | real; wording too strong (fixed) | OpenAI GPT-5.6, reading only: behaviour real; intended-behaviour case strong for the ancestors; recommends a narrowly framed javadoc fix. |
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 455 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,242 words
- Registry title
- TaxonomyFacetLabels javadoc offers it as a replacement for stored fields, but for dimensions configured multi-valued plus hierarchical (or multi-valued plus requireDimCount) it also returns the ancestor paths and the bare dimension that FacetsConfig indexes only for counting (documentation/contract defect; every release since 8.7.0)
- Area
- facet / taxonomy (TaxonomyFacetLabels, per-document facet label retrieval)
- Priority
- Low, rank 1094: TaxonomyFacetLabels returns ancestor and bare-dimension labels for multi-valued hierarchical dims, a contract defect in an experimental utility.
- Impact class
- minor
- Impact note
- Applications that read per-hit facet labels with the @lucene.experimental TaxonomyFacetLabels utility instead of stored fields, as its javadoc suggests, on a dimension configured with FacetsConfig#setMultiValued plus setHierarchical (or plus setRequireDimCount) get, for every such document, the ancestor paths of each label (Category/Electronics and Category/Electronics/Phones next to Category/Electronics/Phones/Android) and, with requireDimCount and the unfiltered nextFacetLabel(docId), a bare dimension label such as 'Color' that the document was never given; default codec, IndexWriterConfig and taxonomy, -ea and -da, every release from 8.7.0 to 10.6.0 and main; counts, drill-down and the index are correct, nothing throws, single-valued dimensions (the default) are unaffected, and only the documentation is wrong, because the ancestor ordinals are indexed on purpose and cannot be told apart from ancestors the user added.
- Summary
- You add FacetField("Author", "Bob") and FacetField("Publish Date", "2010", "10", "15") to a document and read its labels back with TaxonomyFacetLabels, whose javadoc offers it as a way to skip stored fields for these values, so you expect [Author/Bob, Publish Date/2010/10/15]; that is what you get when both dimensions are single-valued. With Publish Date configured multi-valued and hierarchical you get [Author/Bob, Publish Date/2010, Publish Date/2010/10, Publish Date/2010/10/15], and with Author configured multi-valued with requireDimCount you get [Author, Author/Bob, Publish Date/2010/10/15]. FacetsConfig deliberately indexes those ancestor (and dimension) ordinals so that multi-valued counts are right, and the reader turns every indexed ordinal back into a label; the javadoc never says so.
- Introduced
- 24aadc220ba9578f581637b9fd0e7e973d46426c (master, apache/lucene-solr#1893, LUCENE-9444 / apache/lucene#10484, by goankur; first released in 9.0.0) and its branch_8x backport d71923065dba0b6fe275cb60bd5cffb017509827 (first released in 8.7.0). It added TaxonomyFacetLabels with the stored-field-replacement javadoc and an unfiltered nextFacetLabel(docId) that returns taxoReader.getPath(ord) for every stored ordinal [read: git log --follow and git blame of TaxonomyFacetLabels.java in the history clone; exec: 8.6.3 has no such class (compile error), 8.7.0 fails]. The other ingredient, FacetsConfig indexing every parent ordinal for multiValued && (hierarchical || requireDimCount) dims, is intended and dates from LUCENE-5339 (59c08c3fa13b, 2013-11-23, first released in 4.7.0). Later commits on the file (7f8b7ffbcad2 LUCENE-9570 reformat, the LUCENE-9856 cleanups, 6ee69e06fbd1 LUCENE-10062 / #264 in 9.0.0, 2a0b7f20567 #12995) keep the behaviour. Not a regression (2020-09-28 (PR #1893 merged 14:55:38Z on master; the branch_8x backport the same day))
- First affected version
- 8.7.0 [exec] (the first release with TaxonomyFacetLabels; 8.6.3 has no such class [exec: compile error])
- Affected versions
- Every release that has the class: 8.7.0 to 8.11.4, 9.0.0 to 9.12.3, 10.0.0 to 10.6.0, and main (ea57360912ed, 645d4fe14a20); not fixed upstream. [exec] the minimal test fails on the 10.6.0 release jars (-ea and -da) and on main 645d4fe14a20 (2026-11-01); the finder's JUnit fails 2 of 3 (the single-valued control passes) with identical values on 8.7.0, 8.11.4, 9.0.0, 9.1.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea; main also -da) (follow-up); the independent reproduction's JUnit fails 4 of 7 on main (-ea, -da), 10.5.2 and 9.12.3; the intended-behaviour check ran 8.7.0 (fail) and 8.6.3 (compile error). [read] the other releases from git tag --contains 24aadc220ba9 and no later behaviour change; 8.8.2 to 8.11.3 (untagged in the history clone) inferred from 8.7.0 and 8.11.4 behaving identically. TaxonomyFacetLabels.java is unchanged on main, branch_10x and branch_10_5 since #12995 (2024-01-22). All runs on aarch64 with JDK 25
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that disputed it (the adjudication ruled it real), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal
- Upstream and prior reports
- none found (2026-10-26): a scripted prior-report search in two runs, 80 queries with 194 distinct hits, over apache/lucene GitHub issues, PRs and commits, downstream repositories (Elasticsearch, OpenSearch, Solr), the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web; 20 manual checks: all-of-GitHub issue and PR searches for TaxonomyFacetLabels, FacetLabelReader and nextFacetLabel, all 31 comments of #10484 (LUCENE-9444), the review comments of lucene-solr #1893 and #1928 and of #264, #443 and #12995, the 90 comments of LUCENE-5339, list-archive counts with threads read, all 407 open apache/lucene PRs with their changed files, git log --grep and -S over the full history; the finder's earlier scripted search (45 queries) and the independent reproduction's GitHub searches agree; the intended-behaviour check's own searches were cut short. Related, not reports: #10484 / lucene-solr #1893 (introduced the class; nobody discussed ancestors, multi-valued dims or requireDimCount), lucene-solr #1928 (test coverage, single-valued dims only), #264 / #443 / #11100 (LUCENE-10062, the switch to SortedNumericDocValues), #6403 / LUCENE-5339 (origin of the parent-ordinal indexing, about counting); open PR #12862 touches TaxonomyFacetLabels.java (one import after a package move) without a behaviour change. The demo facet user guide (lucene/demo/src/java/org/apache/lucene/demo/facet/package-info.java:324-328) says multi-valued dims store 'all parent category ordinals ... for accurate counting'; it discloses the index-side mechanism but not the label-retrieval consequence, and it is documentation, not a report. Not fixed upstream.
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test through gradle with assertions on and off in a scratch worktree, before and after fix A and with fix B (2026-11-01), and standalone (2026-10-29, 2026-11-01); releases 9.1.0, 9.12.3, 10.0.0, 10.5.2 and 10.6.0 with the minimal test (2026-11-01); 8.6.3, 8.7.0, 8.11.4, 9.0.0, 9.1.0, 9.12.3, 10.0.0, 10.5.2 and mainea57360912edwith the finder's 3-test class (2026-10-26).TaxonomyFacetLabels.javaandFacetsConfig.javaare unchanged fromea57360912edthrough645d4fe14a20to upstream main2f424fd428d(2026-10-30).
- Finding record
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacetLabels.java:27-31 (class javadoc: 'retrieve previously indexed facet labels, allowing you to skip also adding stored fields for these values'), :58 (stale 'BinaryDocValues' @param), :111-140 (nextFacetLabel(docId) returns getPath for every stored ordinal), :142-150 (isDescendant), :170-216 (dim-filtered overload) at ea57360912edlucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:386-398 (multiValued && (hierarchical || requireDimCount): append every parent ordinal; drop the dimension ordinal unless requireDimCount) and :403-415 (sort and dedupe) at ea57360912edlucene/facet/src/java/org/apache/lucene/facet/FacetField.java:53-55 (a FacetField needs at least one path component, so a bare-dimension label is never a user label)lucene/demo/src/java/org/apache/lucene/demo/facet/package-info.java:324-328 (multi-valued dims store all parent category ordinals 'for accurate counting')
- The finding in 30 seconds
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacetLabels.java:27-31 (class javadoc), :45-48 (constructor), :102-103 (order note), :111-140 (nextFacetLabel(docId)), :142-150 (isDescendant), :170-216 (dim-filtered overload)lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:113-124 (DimConfig defaults), :180, :190-193 (setter javadocs), :371-377 (non-hierarchical dims reject multi-component paths), :383-398 (parent ordinals), :403-415 (sort and dedupe)lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:272 (rollup only for hierarchical single-valued dims)
- Who is affected
lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:113-124 (defaults false)lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacetLabels.java:31 (@lucene.experimental)lucene/*/src (grep: TaxonomyFacetLabels is used only by FacetTestCase, TestTaxonomyFacetLabels and TestTaxonomyFacetCounts)lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:81-85 (FULL_PATH_ONLY indexes only the full path)
- Minimal repro
build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/TestsAndRandomizationPlugin.java:240-249 (assertions picked from the seed)
- Why it looks unintended
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacetLabels.java:27-31lucene/CHANGES.txt:5562-5564 (LUCENE-9444 entry; 8.7.0 header at :5536)https://github.com/apache/lucene/issues/10484 (issue body; mikemccand comment 2020-08-01, read 2026-11-01)lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetLabels.java:131, :141, :159lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetCounts.java:965, :972, :1024, :1068-1078lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:180 (setMultiValued javadoc)lucene/facet/src/java/org/apache/lucene/facet/FacetField.java:53-55lucene/demo/src/java/org/apache/lucene/demo/facet/package-info.java:324-328lucene/facet/src/java/org/apache/lucene/facet/taxonomy/FacetLabel.java:66-70, taxonomy/TaxonomyWriter.java:59; lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesField.java:77
- Suggested fix
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacetLabels.java:58 (stale @param), :111-150
- TestTaxonomyFacetLabelsMultiValuedHierarchical.java:
repro/LFV-529/src/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetLabelsMultiValuedHierarchical.java(60 lines)
Made in parallel with this doc.
assets/LFV-529/bug.png(455 KB)
![A document given only Date/2010/10. With Date multi-valued and hierarchical, FacetsConfig stores the ordinals of Date/2010 and Date/2010/10; TaxonomyFacetLabels maps both back to labels and returns [Date/2010, Date/2010/10] instead of [Date/2010/10].](../assets/LFV-529/bug.png)