All findings · Lucene finding · LFV-727 · taxonomy-hierarchical-path-value-excludes-docs-at-path

Taxonomy facets: a hierarchical sub-path's total leaves out the documents labeled at that sub-path

In a single-valued hierarchical taxonomy dimension, FacetResult.value of a sub-path only adds up its children: with one document at Publish Date/2010 and one at Publish Date/2010/10, getTopChildren(10, "Publish Date", "2010").value is 1, while getSpecificValue and a drill-down on the same path give 2. Every release since 4.7.0 does this.

Summary

Priority
Low, rank 325Only the aggregate sub-path value of an opt-in hierarchical single-valued taxonomy dimension is low, child counts stay correct, and the child-sum meaning was documented before 8.9.
Impact
silent wrong results
Since
4.7.0 (LUCENE-5339, 2014)introduced 2014-01-04 · over 12 years
Affected
4.7.0 to 10.6.0 (10.6.0 by reading) and main
Status
documented
Found by
exhaustive check of FacetsConfig options, confirmed by JUnit

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

Taxonomy facets count each document under the ordinal of its label. For a single-valued hierarchical dimension they then roll the counts up the tree, so each node also counts the documents below it. FacetResult.value of a path should be the number of matching documents labeled at or below that path.

Summary card of the repro. Two documents in the hierarchical dimension Publish Date: A labeled 2010 and B labeled 2010/10. After the roll-up the 2010 node counts 2 and the 10 node 1. getTopChildren(10, Publish Date, 2010) adds the children of 2010 only and returns value 1; expected 2, which getSpecificValue, a drill-down on Publish Date/2010 and SortedSetDocValues faceting return.
The repro: document A labeled Publish Date/2010, document B labeled Publish Date/2010/10. After the roll-up the 2010 node holds 2, but the sub-path total adds only its child 10 and reports 1; getSpecificValue, the drill-down and SortedSetDocValues faceting report 2. Select the still to open it at full size. Generated with AI, checked against the code.
  1. Configure setHierarchical("Publish Date", true) and index two documents into a taxonomy: A labeled Publish Date/2010, B labeled Publish Date/2010/10. Both belong to 2010: a drill-down on Publish Date/2010 matches 2.
  2. Ask FastTaxonomyFacetCounts for the children of 2010 with getTopChildren(10, "Publish Date", "2010") or getAllChildren("Publish Date", "2010"). You expect FacetResult.value = 2, the "number of documents containing a value for this path".
  3. You get value 1, with the one child 10 (1). On the same Facets object getSpecificValue("Publish Date", "2010") returns 2, and SortedSetDocValues faceting with the same config returns 2.
  4. Why: at search time the counts are rolled up, so the 2010 node already holds 2 (A plus B). But for a single-valued dimension both methods return the sum of the children's counts instead, and A is not a child.
  5. Child counts, the dimension total, getSpecificValue and drill-down are right. An 8-line fix that returns the rolled-up value passes the whole facet test suite.

Who is affected

Applications using taxonomy facets (FastTaxonomyFacetCounts, or float aggregation with a DoubleValuesSource) on a dimension set setHierarchical(dim, true) and left single-valued, where some documents stop at an inner node, such as a product filed under a parent category. The sub-path total leaves those documents out (for counts it is low by exactly their number); child counts stay right.

Mechanism, terms and numbers610 words

For a dimension configured setHierarchical(dim, true) and left single-valued, taxonomy faceting reports a sub-path's FacetResult.value (from getTopChildren(n, dim, path...) and getAllChildren(dim, path...)) as the sum of its immediate children's values. Documents labeled at the sub-path itself are not children, so they are left out. With one document at Publish Date/2010 and one at Publish Date/2010/10 the value of Publish Date/2010 is 1, while getSpecificValue on the same Facets object and a drill-down on the path give 2, and SortedSetDocValues faceting with the same config gives 2. The right number is already computed: the search-time roll-up makes each node count the documents at and below it, and getSpecificValue returns that; only the multiValued + requireDimCount branch of the two methods uses it. FastTaxonomyFacetCounts (dense and sparse counting) and TaxonomyFacetFloatAssociations with a DoubleValuesSource are affected. Child counts, the dimension-level value, getSpecificValue and drill-down are right (on the value-source path, LabelAndValue.count has a separate defect). Every lucene-facet release from 4.7.0 (2014) to 10.6.0 (10.6.0 by reading) and main gives the same numbers; until 8.8.2 the javadoc described the child sum, since 8.9.0 it promises a document count. Returning the rolled-up value fixes it, and the whole lucene/facet test suite passes with that change.

Terms

  • Hierarchical dimension: FacetsConfig.setHierarchical(dim, true) allows labels with more than one component, such as Publish Date/2010/10. A document may stop at any depth: nothing requires leaf or equal-depth labels.
  • Single-valued (the default): each document has at most one label in the dimension. FacetsConfig.build then indexes only the label's own ordinal, not its ancestors (FacetsConfig.java:386-398: ancestors only when multi-valued).
  • Roll-up: after counting, rollup() adds every node's descendants into it for each single-valued hierarchical dimension (TaxonomyFacets.java:262-299). getSpecificValue returns that rolled-up number (:537-553).
  • FacetResult.value: "Total number of documents containing a value for this path ... counted once" (FacetResult.java:30-35), returned by getTopChildren(n, dim, path...) and getAllChildren(dim, path...) next to the child labels.

The bug

  1. getAllChildren (TaxonomyFacets.java:342-415) and getTopChildrenForPath (:467-516, used by getTopChildren) walk the path's immediate children with a count above 0 and aggregate their values: dense branch :373-386 and :489-503, sparse hash-map branch :360-371 and :478-488.
  2. Only for multi-valued dims do they replace that sum: with the path's own value for requireDimCount (:393-395, :506-508), or with -1 without it. The single-valued branch keeps the sum: "Our aggregateddim value is accurate, so we keep it" (:401).
  3. That holds at the dimension level, where every label has at least one component below the dimension. For a sub-path it fails as soon as some matching documents are labeled at the sub-path itself: for counts the value is low by exactly the number of those documents (with a value source, their values are left out of the aggregate), although the path's own rolled-up value is already correct.
  4. SortedSetDocValues faceting returns the path's own count for hierarchical dims ("hierarchical dims index all path counts directly", AbstractSortedSetDocValueFacetCounts.java:341-345).

The numbers

Documents A at Publish Date/2010 and B at Publish Date/2010/10; only setHierarchical("Publish Date", true); MatchAllDocsQuery.

callexpectedtaxonomy (FastTaxonomyFacetCounts)
getTopChildren(10, "Publish Date", "2010").value21 (child 10 (1))
getAllChildren("Publish Date", "2010").value21
getSpecificValue("Publish Date", "2010")22
drill-down hits on Publish Date/201022
getTopChildren(10, "Publish Date").value (dimension)22
SortedSetDocValues faceting, same config: sub-path value22
taxonomy with multi-valued + requireDimCount: sub-path value22

The same numbers on main with assertions on and off, on 10.5.2, 9.12.3 and 9.3.0, in the dense and the sparse counting branch. With a DoubleValuesSource and SUM (A worth 10, B worth 1): sub-path value 1.0, getSpecificValue 11.0.

Exposure and likelihood250 words
  • Who is exposed. Applications that use FastTaxonomyFacetCounts (both its dense and its sparse counting branch) or TaxonomyFacetFloatAssociations with a DoubleValuesSource, on a hierarchical dimension left single-valued, with mixed-depth labels (a product filed directly under a parent category next to products in sub-categories, a date known only to the year next to full dates), and that show FacetResult.value of a sub-path from getTopChildren(n, dim, path...) or getAllChildren(dim, path...).
  • Default vs opt-in. setHierarchical is opt-in configuration (needed for multi-component labels); single-valued is the default. Everything else is default.
  • Scale. Any index size; deterministic, no concurrency. For counts the value is low by exactly the number of matching documents labeled at the requested path. With a DoubleValuesSource and SUM it is low by the sum of their values (1.0 instead of 11.0); MAX has no such formula and was not run.
  • Silent. No exception or log line, and the child counts look right. Nothing is stored wrong, so a fix needs no reindex.
  • Not affected. Child labels and counts, the dimension-level value, getSpecificValue, drill-down hit counts, SortedSetDocValues faceting, multi-valued + requireDimCount dims, and data whose labels all have the same depth (the demo's full dates). On the value-source path, LabelAndValue.count has its own separate defect. A sub-path with no child that has a count is a different case, which the fix does not change: getTopChildren returns null in both taxonomy and SortedSetDocValues faceting, and taxonomy getAllChildren returns null too, while SortedSetDocValues getAllChildren returns the path count with no labels (executed).

Minimal repro

for a sub-path of a single-valued hierarchical taxonomy dimension, FacetResult.value leaves out the documents labeled at the path itself (docs at Publish Date/2010 and Publish Date/2010/10).

TestHierarchicalSubPathValue.java (JUnit test)60 lines
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.facet.taxonomy;

import org.apache.lucene.document.Document;
import org.apache.lucene.facet.FacetField;
import org.apache.lucene.facet.Facets;
import org.apache.lucene.facet.FacetsCollector;
import org.apache.lucene.facet.FacetsCollectorManager;
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.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestHierarchicalSubPathValue extends LuceneTestCase {

  public void testSubPathValueCountsDocsLabeledAtThePath() throws Exception {
    FacetsConfig config = new FacetsConfig();
    config.setHierarchical("Publish Date", true);
    try (Directory dir = new ByteBuffersDirectory();
        Directory taxoDir = new ByteBuffersDirectory();
        IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig());
        DirectoryTaxonomyWriter taxoWriter = new DirectoryTaxonomyWriter(taxoDir)) {
      for (String[] path : new String[][] {{"2010"}, {"2010", "10"}}) {
        Document doc = new Document();
        doc.add(new FacetField("Publish Date", path));
        writer.addDocument(config.build(taxoWriter, doc));
      }
      try (DirectoryReader reader = DirectoryReader.open(writer);
          TaxonomyReader taxoReader = new DirectoryTaxonomyReader(taxoWriter)) {
        FacetsCollector fc =
            new IndexSearcher(reader).search(new MatchAllDocsQuery(), new FacetsCollectorManager());
        Facets facets = new FastTaxonomyFacetCounts(taxoReader, config, fc);
        // Both docs contain a value for Publish Date/2010: expected 2, actual 1 (the child sum only)
        assertEquals(2, facets.getTopChildren(10, "Publish Date", "2010").value.intValue());
      }
    }
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-727/, 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.taxonomy.TestHierarchicalSubPathValue
# the module(s) facet also need their third-party runtime jars
Expected vs actual
Expected getTopChildren(10, "Publish Date", "2010").value = 2 (both documents contain a value for the path); actual 1 (only the child 2010/10 is summed).

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

More files, environment, full output34 words

Environment

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

Observed output

# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
    => java.lang.AssertionError: expected:<2> but was:<1>
       org.junit.Assert.fail(Assert.java:89)
       org.junit.Assert.failNotEquals(Assert.java:835)
       org.junit.Assert.assertEquals(Assert.java:647)
       org.junit.Assert.assertEquals(Assert.java:633)
       org.apache.lucene.facet.taxonomy.TestHierarchicalSubPathValue.testSubPathValueCountsDocsLabeledAtThePath(TestHierarchicalSubPathValue.java:56)

Why it looks unintended

  • FacetResult.value is the "Total number of documents containing a value for this path": here 2.
  • On the same Facets object getSpecificValue and a drill-down give 2; SortedSetDocValues faceting gives 2, and its nightly randomized test asserts it.
  • The code's comment, "Our aggregateddim value is accurate", holds only at the dimension level.
The full argument341 words
  • Contract. FacetResult.java:30-35: "Total number of documents containing a value for this path, even those not included in the topN. If a document contains multiple values for the same path, it will only be counted once in this value." The demo user guide repeats it (demo/facet/package-info.java:217-219). setHierarchical only says "has depth > 1 paths" (FacetsConfig.java:170), FacetField needs only one path component (FacetField.java:53-54), and FacetsConfig.build accepts mixed depths (it rejects only a second label in a single-valued dimension: FacetsConfig.java:275-277 calls checkSeen, :233-241). The guide's "path from root to a leaf for the current document" (package-info.java:131) describes one document's label; nothing enforces that it is a leaf of the taxonomy, and the same guide says the hierarchical category list "allows counting any sub-category in the taxonomy, and not just leaf nodes" (:251-255).
  • Intent. The current wording came from LUCENE-9953 (#10992, 2021) and the dev@ thread "FacetResult#value semantics?", where mikemccand wrote "we really should strive to count each hit only once" and "the goal here is to say 'if the user were to drill down on this value, how many total hits would THAT query return?'". The drill-down here returns 2.
  • Tests. The @Nightly TestSortedSetDocValuesFacets#testRandomHierarchicalFlatMix (LUCENE-10250) indexes single-valued hierarchical dims with random path lengths and expects the inclusive sub-path value; switching SortedSetDocValues to the child sum makes it fail (executed mutation check). No taxonomy test covers mixed-depth labels; TestSortedSetDocValuesFacets#testBasicHierarchical does not test this: its hierarchical dimension c is multi-valued, and its c/buzz documents also have children, so it gives 2 under either reading.
  • Consistency. One Facets object gives 2 from getSpecificValue and 1 from FacetResult.value; the same documents give 2 when the dimension is multi-valued + requireDimCount, and 2 with SortedSetDocValues faceting.
  • Value sources. "Number of documents" cannot literally describe a weighted 11.0. For value sources the argument is consistency: getSpecificValue and the path's rolled-up value give 11.0, the sub-path value 1.0. Only SUM was executed.
  • History. The pre-4.7 facet API returned 2 for the same data (executed on 4.6.1).
The devil's-advocate check232 words

The strongest "intended" case: the child sum is a commented choice from LUCENE-5339 (2013); from 4.7.0 to 8.8.2 the javadoc said "Total value for this path (sum of all child counts, or sum of all child values)", exactly what the code computes; the 2021 rewording was written for another class (LongValueFacetCounts) and may have overreached; later refactors (LUCENE-10550, #12966) kept the behaviour; getSpecificValue already gives the inclusive number; and the facet API is @lucene.experimental.

Why it fails: the current javadoc and user guide promise a document count with no exception, and the rewording came with a stated drill-down intent that this result breaks; the comment's reasoning is only valid at the dimension level, where it was written; SortedSetDocValues faceting implements and tests the inclusive count for exactly this data shape; one object returning 2 and 1 for the same path is not a coherent semantics; and reading the guide's "path from root to a leaf" as a leaf-only rule fails too, since nothing enforces it and the same guide promises counts for "any sub-category ... not just leaf nodes". @lucene.experimental is about API stability, not wrong numbers. Verdict: a defect (adjudication: confirmed, high). On code fix versus javadoc fix the case is moderate, not decisive: the child sum was deliberate, documented for seven years and kept through two refactors. A maintainer may still prefer to document the child sum.

Suggested fix

Recommended Use the rolled-up path value for sub-paths of single-valued hierarchical dims

In TaxonomyFacets.getAllChildren and getTopChildrenForPath, for a sub-path of a single-valued hierarchical dimension, return the path's own rolled-up value (getAggregationValue(pathOrd), the number getSpecificValue returns) instead of the children's sum. +8 lines; the repro and all 309 facet tests pass with assertions on and off.

All options and trade-offs3 options ยท 820 words

The dimension level is unchanged, and so are child counts. The roll-up the fix relies on always runs for these dimensions: every taxonomy class that can see a hierarchical dimension calls it, and association fields cannot be hierarchical. Only a query-time number changes, so no reindex is needed. It is still a compatibility change: the edited code is in package-private base classes, but the value is exposed through the public, non-final FastTaxonomyFacetCounts and TaxonomyFacetFloatAssociations (also in DrillSideways results), and it has been the child sum since 2014, so it needs a CHANGES entry. If the MAX-starts-at-zero association fix is applied too, the two patches touch neighbouring lines of getAllChildren.

Use the rolled-up path value for sub-paths of single-valued hierarchical dims (validated by the checks) (recommended)

Two hunks in TaxonomyFacets.java, after the multi-valued branch of getAllChildren (:393-402) and of getTopChildrenForPath (:505-513). In getAllChildren the variable dimOrd holds the ordinal of the requested path (:344-345); getTopDims calls getTopChildrenForPath with a dimension ordinal, whose parent is the root, so the dimension level keeps today's sum.

@@ -397,6 +397,10 @@ abstract class TaxonomyFacets extends Facets {
         // Our aggregated value is not correct, in general:
         aggregatedValue = missingAggregationValue();
       }
+    } else if (dimConfig.hierarchical && path.length > 0) {
+      // A sub-path of a single-valued hierarchical dim: rollup() already aggregated the documents
+      // labeled with the path itself and those below it into the path's own ordinal
+      aggregatedValue = getAggregationValue(dimOrd);
     } else {
       // Our aggregateddim value is accurate, so we keep it
     }
@@ -510,6 +514,10 @@ abstract class TaxonomyFacets extends Facets {
         // Our aggregated value is not correct, in general:
         aggregatedValueNumber = missingAggregationValue();
       }
+    } else if (dimConfig.hierarchical && parents.get(pathOrd) != TaxonomyReader.ROOT_ORDINAL) {
+      // A sub-path of a single-valued hierarchical dim: rollup() already aggregated the documents
+      // labeled with the path itself and those below it into the path's own ordinal
+      aggregatedValueNumber = getAggregationValue(pathOrd);
     }
 
     return new TopChildrenForPath(aggregatedValueNumber, childCount, q);

Trade-offs

  • Changes a number that has been the child sum since 4.7.0; needs a CHANGES entry. One array or hash lookup per call.
  • For value sources it returns the rolled-up aggregate, consistent with getSpecificValue: SUM 11.0 in the reproducer's case; for MAX the maximum over the path and its descendants, subject to the separate MAX-starts-at-zero defect (LFV-547: with only negative values both today's and the fixed value are 0). MAX was not executed.
  • Relies on rollup() having run, which holds for every caller in the module: FastTaxonomyFacetCounts in both count paths and TaxonomyFacetFloatAssociations with a value source; association fields cannot be hierarchical (FacetsConfig.java:301-303).
  • A compatibility change: TaxonomyFacets and its two base subclasses are package-private, but the value is exposed through the public, non-final FastTaxonomyFacetCounts and TaxonomyFacetFloatAssociations, so callers that read today's sub-path value see it change.
  • It also changes FacetResult.value in DrillSideways results, which count with the same FastTaxonomyFacetCounts (DrillSideways.java:159, :165). TestDrillSideways and TestParallelDrillSideways pass with the fix (14 + 14 tests, assertions on and off), but none of their documents is labeled at an inner node that also has labeled children, so no DrillSideways test covers the mixed-depth case.

What the checks validated

  • Scratch worktree of a9779ef3c193, gradle, JDK 25: the new test fails 1 of 3 on stock (assertions on and off) and passes 3 of 3 with the fix; the whole :lucene:facet:test, 40 suites and 309 tests, 0 failures, with assertions on (7 skipped) and off (8 skipped); :lucene:demo:test, 23 tests, 0 failures; :lucene:facet:check -x test (google-java-format, ecj lint and forbidden APIs) passes.
  • Standalone runner with the fix shadowed: the new test 3 of 3 with assertions on and off; the reproducer's and the reviewer's tests 7 of 7 (dense, sparse and value-source cases); the probe prints value=2 for both sub-path calls. Earlier, a reviewer ran the same logic against four taxonomy test classes (four separate runs, 19 + 4 + 10 + 12 = 45 tests, 0 failures).

Use the rolled-up value at every level of a single-valued hierarchical dim

Drop the sub-path condition (else if (dimConfig.hierarchical)).

Trade-offs

Simpler; at the dimension level the rolled-up value should equal the child sum, because no document can be labeled at the dimension alone. It also touches the dimension-level path used by getTopDims; not run. The equality holds for counts and for the built-in SUM and MAX; AssociationAggregationFunction is a public extension point, and a custom function for which 0 is not an identity, or that is not associative and commutative, could change the dimension value.

Document the child sum

Say in FacetResult.value (or the taxonomy classes) that for single-valued hierarchical taxonomy dims a sub-path's value is the sum of its children.

Trade-offs

Not recommended: keeps taxonomy and SortedSetDocValues faceting disagreeing for the same config, contradicts the drill-down intent behind the 2021 wording, and leaves getSpecificValue and FacetResult.value disagreeing on one object.

Downstream note

Products that show hierarchical taxonomy facets, such as category trees or date hierarchies, display sub-path totals lower than the hit count a click on that node returns until they upgrade; nothing is stored wrong, so the fix needs no reindex.

How it was found

An AI finder compared Lucene's facet results with a reference written from the javadoc, over every combination of FacetsConfig options and every set of up to three small documents. The sub-path total differed in exactly the taxonomy configurations with hierarchical, single-valued dimensions; the finder reduced it to a two-document JUnit test.

The method in full184 words

Method: bounded-exhaustive translation validation (the hunt's "semi-formal" tier), a hunt unit (FacetsConfig and drill-down indexing matrix), 2026-10-27.

  • The harness ran the stock jar against a reference written from the javadoc (the value of a path is the number of matching documents with a label at or below it) over DrillDownTermsIndexing x hierarchical x multiValued x requireDimCount x index-field layout x config style x {taxonomy, SortedSetDocValues} faceting, every multiset of up to 3 small documents. In the run below each case was indexed in one of four layouts (1 or 2 segments, with or without deletes), taken in rotation; single-document cases use one segment without deletes.
  • In the maxDocs=3 run with assertions off (685,912,348 checks over 400 configs) the mismatch class "path value is the children's sum, not the path count" appeared 99,920 times, in exactly the 50 taxonomy configs with hierarchical and single-valued settings (requireDimCount on or off, every drill-down indexing mode and field layout) and in no SortedSetDocValues or multi-valued config.
  • The finder reduced it to the two-document public-API JUnit; then independent AI tools reproduced, reviewed and judged it (see the checks).

Prior reports, history and checks

Prior reports

No prior report found (best-effort search, 2026-10-27): 74 tool queries over GitHub, commits, downstream projects, four mailing lists, JIRA and the web; 29 direct searches (6 of them re-ran the tool's web queries by hand); and all 410 open PRs. Nearest: the 2021 dev@ thread that set today's FacetResult.value wording.

Searches4 searches

No issue, PR, JIRA entry, list thread, downstream issue, Q&A post or web page found describes the sub-path value leaving out documents labeled at the path, or the taxonomy vs SortedSetDocValues difference. Upstream main has no lucene/facet change since a9779ef3c193, and no open PR changes the value branch.

Nearest items, none of them this bug: the dev@ thread "FacetResult#value semantics?" (2021) and LUCENE-9953 (#10992), which set the current wording and its drill-down intent; LUCENE-10250 (#509), where SortedSetDocValues faceting chose the inclusive path count; LUCENE-9952 (#10991, fix #611), SortedSetDocValues dimension counts; LUCENE-5339 (#6403), the origin; LUCENE-5184 (#6248, 2013, old API), a dimension count of 0, "Not A Problem"; the sandbox PR #13568, a 2024 review comment by epotyom that documents at a/b and at a/b/c must both count for a/b, about the sandbox recorder only; LUCENE-5536 (#6599), association-field values are not rolled up by design. Gaps: GitHub search is phrase-like and quota-limited, JIRA queries were read in their first 50-60 results, web coverage is best effort, private reports are not visible.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene)2026-10-27a scripted prior-report search (20 issue and PR queries, 3 commit queries) and 16 direct searches (issues, PRs, open PRs, commits; Lucene.NET)no report; the items listed above
Open PRs2026-10-27the changed files of all 410 open apache/lucene PRs; the 4 that touch taxonomy value code (#13287, #12862, #12547, #13948) were readnone changes the sub-path value branch
Mailing lists and JIRA2026-10-2735 tool queries over dev@, java-user@, issues@ and commits@, 8 JIRA-archive tool queries, 3 direct JIRA queriesrelated threads only (FacetResult#value semantics, 2021); LUCENE-5184
Downstream repositories and web2026-10-272 downstream tool queries; 6 tool web queries and 10 direct ones (6 of the 10 re-ran the tool's 6 by hand), plus Stack Overflowno report

History

Introduced by LUCENE-5339 (2014, first released in 4.7.0), when the javadoc still described the child sum; it contradicts the javadoc since 8.9.0 and SortedSetDocValues faceting since 9.1.0. A probe gives the same numbers on all 119 releases from 4.7.0 to 10.5.2 and on main. Not fixed upstream.

Versions tested8 rows

Introduced by LUCENE-5339 "Simplify the facet module APIs" (#6403, mikemccand; svn era, no PR): first written on the lucene5339 branch in 5458dbf2484 (2013-11-14), with 189feaacb6f (requireDimCount and "Our sum'd dim count is accurate, so we keep it") and 458786d0f41 (IntTaxonomyFacets / FloatTaxonomyFacets); merged 2014-01-04 as 7477fde1cb0 (branch_4x, r1555342) and 9d0b60388da (trunk, r1555338); released in 4.7.0. Its javadoc then read "Total value for this path (sum of all child counts, or sum of all child values)". LUCENE-9953 (#131 65820e5170e on main, lucene-solr #2491 3a7387d039b on branch_8x, 2021-05-18) changed the javadoc to "Total number of documents containing a value for this path" without touching the taxonomy code. LUCENE-10250 (#509, 9.1.0) gave SortedSetDocValues faceting hierarchical dims with the inclusive value; LUCENE-10550 (#914, 2022-06-21; bdcb4b37164 on main, first released in 10.0.0; the 9.x commit b2c454c8be1, released in 9.3.0) copied the child sum into getAllChildren; #12966 (9ba4af7b883; backport #13358 42884fab46d, 9.11.0) moved it into TaxonomyFacets unchanged.

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthis doc's test: 1 of 3 fails (gradle and the standalone runner, assertions on and off); probe: value 1, getSpecificValue 2, drill-down 2
10.5.2, 9.12.3, 9.3.0 affectedthis doc's test: 1 of 3 fails with the same message; probe on 10.5.2: the same numbers
9.3.0 ... 10.5.2 getAllChildrenrelease probe: getAllChildren value 1 (the method exists from 9.3.0)
9.1.0 ... 10.5.2 vs SortedSetDocValuesrelease probe: SortedSetDocValues gives 2 for the same config, taxonomy 1
8.9.0 javadoc changedrelease probe: value 1; first release whose javadoc promises a document count (8.8.2 still says child sum)
all 119 releases 4.7.0 ... 10.5.2 affectedrelease probe: dense and sparse value 1, getSpecificValue 2, drill-down 2, multi-valued + requireDimCount control 2; assertions off also on 4.7.0, 7.2.0, 8.8.2, 8.9.0, 9.0.0, 9.1.0, 9.3.0, 9.12.3, 10.0.0 and 10.5.2
4.6.1 old APIno Facets / FacetsConfig API (the probe does not compile); the old facet API returns 2 under all three ordinal policies
main + the fix not affectedthe test passes 3 of 3, the probe prints value 2, and the whole lucene/facet suite passes

Independent checks

An independent reproducer, a devil's advocate, an adjudication and reviews by another model family (which ran their own tests) all found it real; the adjudication confirmed it with high confidence. Registry re-runs and this doc's gradle runs reproduced it on main a9779ef3c193.

Checks and verdicts350 words

Every check except the security assessment wrote or ran its own test against the stock jars, with assertions on and off.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateBounded-exhaustive harness flagged the mismatch class in exactly the 50 hierarchical single-valued taxonomy configs; the two-document JUnit fails 1 of 3 on main (-ea, -da), 10.5.2 and 9.12.3 and passes with the fix.
cross-model review, intended lens real (high)An independent reviewer from another model family (OpenAI GPT-5.6) reproduced it with its own test on main, 10.5.2 and 9.12.3; rated the intended-behaviour argument moderate; root cause agreed; the fix fixes it.
Independent reproducer realOwn 5-test class outside the Lucene packages: 3 of 5 fail on main (-ea, -da), 10.5.2 and 9.12.3; dense and sparse branch value 1 vs 2; value-source SUM 1.0 vs 11.0; controls pass.
Cross-model review, refute lens real (high)OpenAI GPT-5.6: rated the historical intended-behaviour case strong but not overriding the current contract; its own test fails on main, 10.5.2 and 9.12.3; with the fix its tests and 45 existing taxonomy tests pass (four runs: 19 + 4 + 10 + 12); re-run on a9779ef3c193.
Devil's advocate realFound the 2021 dev@ drill-down intent; mutation check: the nightly SortedSetDocValues hierarchical test fails under child-sum semantics; high on meaning, moderate on code vs javadoc fix.
adjudication confirmed (high)Re-ran the independent reproduction's test with a new seed (3 of 5 fail, -ea and -da) and the finder's test (1 of 3 fail).
security assessment noneA too-low aggregate for documents the caller's query already matched; no documented untrusted-input guarantee; file publicly.
registry re-runs reproducedOn main a9779ef3c193: 1 of 3 fails with -ea and with -da; 3 of 3 pass with the fix shadowed.
this doc's validation reproduced, fix validatedGradle in a scratch worktree: 1 of 3 fails on stock, -ea and -da; with the fix the test, the whole lucene/facet suite (309 tests, -ea and -da), lucene/demo and :lucene:facet:check pass.

Diagram file

The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,504 words

Finding record

LFV-727, snapshot of 2026-11-06

Registry title
Taxonomy facets: for a sub-path of a single-valued hierarchical dimension, FacetResult.value from getTopChildren/getAllChildren is the sum of the children, so documents labeled at the sub-path itself are left out, while getSpecificValue, drill-down and SortedSetDocValues faceting count them
Area
facet / taxonomy facets (TaxonomyFacets#getTopChildrenForPath and #getAllChildren: FacetResult.value of a sub-path; FastTaxonomyFacetCounts, TaxonomyFacetFloatAssociations with a DoubleValuesSource)
Priority
Low, rank 325: Only the aggregate sub-path value of an opt-in hierarchical single-valued taxonomy dimension is low, child counts stay correct, and the child-sum meaning was documented before 8.9.
Impact class
silent wrong results
Impact note
Hits applications that use taxonomy faceting (FastTaxonomyFacetCounts in both its dense and its sparse counting branch, or TaxonomyFacetFloatAssociations with a DoubleValuesSource) on a dimension configured setHierarchical(dim, true) and left single-valued, where some documents are labeled at an inner node (Publish Date/2010 next to Publish Date/2010/10), and that read FacetResult.value of a sub-path from getTopChildren(n, dim, path...) or getAllChildren(dim, path...): the value is low by the number of matching documents labeled exactly at that path, with no error, while the child labels and counts, the dimension-level value, getSpecificValue, drill-down, SortedSetDocValues faceting with the same config and the taxonomy with multiValued + requireDimCount are right; opt-in hierarchical config, no scale needed, deterministic with -ea and -da, every release from 4.7.0 to 10.6.0 (10.6.0 by reading).
Summary
You index two documents with taxonomy facets in a dimension configured only with setHierarchical("Publish Date", true): one labeled Publish Date/2010 and one labeled Publish Date/2010/10. Both documents have a value for the path Publish Date/2010 (a DrillDownQuery on it matches 2 and getSpecificValue("Publish Date", "2010") returns 2), so FacetResult.value of getTopChildren(10, "Publish Date", "2010") and getAllChildren("Publish Date", "2010") should be 2, as its javadoc ('Total number of documents containing a value for this path') and SortedSetDocValues faceting with the same config say. FastTaxonomyFacetCounts returns 1 for both: for a single-valued dimension it adds up the path's immediate children and never uses the path's own rolled-up count, so the document labeled at 2010 itself is left out.
Introduced
7477fde1cb063b5c849369afa531bb28df4566fc (LUCENE-5339 'Simplify the facet module APIs' = apache/lucene#6403, by mikemccand; merge to branch_4x, svn r1555342, 2014-01-04; trunk merge 9d0b60388da, r1555338, same day; Subversion era, no pull request) [read: git log --all -S on every historical form of the value branch, copy-detecting git blame and the 4.6.1 vs 4.7.0 release diff in the history clone]. The child-sum code was first written on the lucene5339 branch in 5458dbf2484 (2013-11-14) and took its current shape in 189feaacb6f (2013-11-20: requireDimCount and 'Our sum'd dim count is accurate, so we keep it') and 458786d0f41 (2013-11-27: IntTaxonomyFacets / FloatTaxonomyFacets). It was copied into getAllChildren by LUCENE-10550 (#914, bdcb4b37164, 9.3.0) and moved into TaxonomyFacets by #12966 (9ba4af7b883; 9.x backport 42884fab46d, #13358; 9.11.0) with unchanged values [exec]. At introduction the code matched its javadoc ('Total value for this path (sum of all child counts, or sum of all child values), even those not included in the topN'); LUCENE-9953 (#131 = 65820e5170e on main, lucene-solr #2491 = 3a7387d039b on branch_8x, merged 2021-05-18) rewrote the javadoc to 'Total number of documents containing a value for this path' without changing the taxonomy code. Not a regression within the current API. (2014-01-04 (7477fde1cb0 / 9d0b60388da; code first written 2013-11-14 on the lucene5339 branch); contradicts its javadoc since 2021-05-18 (LUCENE-9953))
First affected version
4.7.0 [exec] (the behaviour; then documented as the child sum). 8.9.0 is the first release whose FacetResult.value javadoc it contradicts [read: javadoc at the 8.8.2 and 8.9.0 release tags]; 9.1.0 is the first release where SortedSetDocValues faceting returns the inclusive value for the same config [exec]. 4.6.1 and older have no Facets / FacetsConfig API [exec: compile error]; 4.6.1's old facet API returns the inclusive value 2 for the same data under all three OrdinalPolicy values [exec, -ea and -da]
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, version-portable main() probe: getTopChildren value 1 in the dense and the sparse branch, getSpecificValue 2, drill-down 2, multiValued + requireDimCount control 2; getAllChildren value 1 from 9.3.0; SortedSetDocValues control 2 from 9.1.0]; -da also executed on 4.7.0, 7.2.0, 8.8.2, 8.9.0, 9.0.0, 9.1.0, 9.3.0, 9.12.3, 10.0.0 and 10.5.2 with the same result; the finder's JUnit (needs 9.3.0+ APIs) fails on 9.3.0, 9.12.3, 10.0.0, 10.5.2 and main a9779ef3c193 (-ea and -da). Not fixed: no lucene/facet commit on upstream main a95c754ee00 since a9779ef3c193, branch_10x and branch_10_6 have the identical value branch [read], so 10.6.0 (released 2026-10-29 from branch_10_6) is affected too, and no open PR changes it. All runs on 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 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, follow-up search, read-only; the prior-report record, verdict new): two scripted prior-report searches (run A: 20 GitHub issue/PR queries on apache/lucene, 3 commit, 2 downstream over Elasticsearch, OpenSearch and Solr, 20 mailing-list over dev@, java-user@, issues@ and commits@, 4 JIRA-archive, 6 web; 270 hits, 2 high, 30 medium, none reporting it; run B: 15 list and 4 JIRA queries, 47 hits), 16 own GitHub searches (apache/lucene issues, PRs and commits, apache/lucenenet), 3 own JIRA queries, 10 web queries plus Stack Overflow, the changed files of all 410 open apache/lucene PRs (4 touch taxonomy value code: #13287, #12862, #12547, #13948; diffs read, none changes the sub-path value branch), and upstream main (2 commits past a9779ef3c193, neither in lucene/facet); the finder's 51-query run (172 hits) and the intended-behaviour check's 24-query run found nothing either. Related, not this bug: the dev@ thread 'FacetResult#value semantics?' (2021-05) and LUCENE-9953 (#10992), which set the current javadoc and the drill-down-count intent; LUCENE-9952 (#10991, fix #611, SortedSetDocValues dimension counts); LUCENE-10250 (#509, SortedSetDocValues chose the inclusive counts[pathOrd]); LUCENE-10584 (#11620); LUCENE-5339 (#6403, the origin); LUCENE-5184 (#6248, 2013, old API, a dimension count of 0 under ALL_BUT_DIMENSION, Not A Problem); sandbox PR #13568 (a 2024 review comment that a/b must count documents labeled a/b as well as a/b/c, about the sandbox CountFacetRecorder only); #12966, #914, #11010, #14008, #13191 / #13287, #12862, #12547, #6599 (LUCENE-5536: association-field values are deliberately not rolled up), #12553, #11583, #14619, #5675, #5950, #11668, #13740. Search gaps: GitHub search is phrase-like and quota-limited, JIRA 'text ~' queries were read only in their first 50-60 results, web coverage is best effort, private reports are not visible
Lucene commit tested
main a9779ef3c193 (commit of 2026-10-26; tested 2026-10-27) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the 10.5.2, 9.12.3 and 9.3.0 release jars (this doc's test). A version-portable probe covered all 119 lucene-facet releases from 4.7.0 to 10.5.2 and the old API of 4.6.1. The cross-model reviews ran on main ea57360912ed, whose facet module is identical. The fix was validated in the scratch worktree: the new test, the whole lucene/facet suite (40 suites, 309 tests) with assertions on and off, lucene/demo, and :lucene:facet:check -x test.

Lucene and upstream sources

Finding record
  • lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:262-285 (at a9779ef3c193) (rollup(): single-valued hierarchical dims), :342-415 (getAllChildren: child sum, multiValued branch :393-399, 'Our aggregateddim value is accurate, so we keep it' :401), :467-515 (getTopChildrenForPath: child sum, getAggregationValue(pathOrd) only for multiValued + requireDimCount :506-512), :537-553 (getSpecificValue returns the rolled-up getAggregationValue(ord))
  • lucene/facet/src/java/org/apache/lucene/facet/FacetResult.java:30-35 (at a9779ef3c193) (value javadoc 'Total number of documents containing a value for this path ... counted once'); lucene/demo/src/java/org/apache/lucene/demo/facet/package-info.java:217-219 (user guide: total number of documents containing a value for this path)
  • lucene/facet/src/java/org/apache/lucene/facet/taxonomy/FastTaxonomyFacetCounts.java:117 (at a9779ef3c193) and :174, TaxonomyFacetFloatAssociations.java:165 (callers of rollup()); lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:386 (ancestor ordinals indexed only when multiValued); lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java:341-345 ('hierarchical dims index all path counts directly: return getCount(pathOrd)')
  • lucene/facet/src/test/org/apache/lucene/facet/sortedset/TestSortedSetDocValuesFacets.java:1437-1439 (at a9779ef3c193) (@Nightly testRandomHierarchicalFlatMix)
  • a Lucene main checkout: git diff --stat ea57360912ed a9779ef3c193 -- lucene/facet (empty)
  • https://github.com/apache/lucene/pull/13568 review comment by epotyom, 2024-07-18, on lucene/sandbox/.../recorders/CountFacetRecorder.java ('some documents might have full path a/b/c assigned to them, and some can have a/b; we still need to rollup and count both documents for a/b'; read-only gh api GET in the registration check)
The finding in 30 seconds
  • lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:262-299 (rollup), :342-415 (getAllChildren), :467-516 (getTopChildrenForPath), :537-553 (getSpecificValue)
  • lucene/facet/src/java/org/apache/lucene/facet/FacetResult.java:30-35; lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:170, :386-398
  • lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java:341-345
Who is affected
  • lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:371-378, :386-398
  • lucene/demo/src/java/org/apache/lucene/demo/facet/SimpleFacetsExample.java:56, :70-90 (full-depth dates)
  • lucene/facet/src/java/org/apache/lucene/facet/taxonomy/FastTaxonomyFacetCounts.java:117, :174; TaxonomyFacetFloatAssociations.java:165
  • lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:308-310, :389-391; lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java:64-94, :368-370 (a sub-path with no counted child)
Why it looks unintended
  • lucene/facet/src/java/org/apache/lucene/facet/FacetResult.java:30-35; lucene/demo/src/java/org/apache/lucene/demo/facet/package-info.java:217-219; lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:170, :233-241, :275-277; lucene/facet/src/java/org/apache/lucene/facet/FacetField.java:53-54; lucene/demo/src/java/org/apache/lucene/demo/facet/package-info.java:131, :251-255
  • lucene/facet/src/java/org/apache/lucene/facet/Facets.java:25, :47-50
  • lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:401
  • lucene/facet/src/test/org/apache/lucene/facet/sortedset/TestSortedSetDocValuesFacets.java:1437-1439 (@Nightly testRandomHierarchicalFlatMix)
  • lucene/facet/src/test/org/apache/lucene/facet/sortedset/TestSortedSetDocValuesFacets.java:317-322 (testBasicHierarchical: dimension c multi-valued)
Suggested fix
  • lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:42, :344-345, :393-402, :505-513, :645, :683; lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:301-303
  • lucene/facet/src/java/org/apache/lucene/facet/DrillSideways.java:159, :165; lucene/facet/src/java/org/apache/lucene/facet/taxonomy/FastTaxonomyFacetCounts.java:40; lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacetFloatAssociations.java:51; lucene/facet/src/java/org/apache/lucene/facet/taxonomy/AssociationAggregationFunction.java:23, :31; lucene/facet/src/java/org/apache/lucene/facet/taxonomy/FloatTaxonomyFacets.java:112-121, :147-149 (MAX starts at 0)

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

Repro files

About the diagrams

A light summary card of the repro, made in parallel with this doc from the executed repro and the code.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.