getTopDims(topNDims, topNChildren) returns the top dimensions of a facet result with their top labels. Its javadoc says the result should be the same as the first topNDims entries of getAllDims, which lists only the dimensions that had hits.
[b] twice; actual on main [null, b] and [null]. Taxonomy top-1 is [null] only with b indexed first. Select the still to open it at full size. Generated with AI, checked against the code.- Mark dimensions
aandbmulti-valued;requireDimCountstays false, the default. Index one document withb=b1and then one witha=a1, and search for thebdocument only. - You expect
getTopDims(10, 10)andgetTopDims(1, 10)to return[b]: the documented result is the first entries ofgetAllDims(10), which is[b]. - You get
[null, b]and[null]. The dimension without hits takes a slot, and with one slot the only dimension that has a hit is gone. With assertions enabled, Lucene's ownassert facetResult != nullfails instead. - Why: without
requireDimCountthe dimension's count is unknown, sogetTopDimsgives it the placeholder -1 without looking for hits. The check meant to skip dimensions without hits only skips 0, and the empty dimension's result isnull. - Taxonomy counts, the int and float association facets, SortedSetDocValues facets and their concurrent variant all do this;
getAllDimsis right. Admitting such a dimension only if one of its children has a hit fixes it, and the whole facet test suite passes.
Applications that call getTopDims on taxonomy or SortedSetDocValues facets with a dimension marked multi-valued and no requireDimCount, which association dimensions cannot set. A query that misses such a dimension gets a null entry whenever it ranks into the top topNDims, and can lose a dimension with hits when topNDims is small. Deleting the documents does not help in a taxonomy.
Mechanism, terms and numbers778 words
Facets#getTopDims(topNDims, topNChildren) is documented to return the same as the first topNDims entries of getAllDims, which lists only the dimensions that had hits. Taxonomy facets and SortedSetDocValues facets override it with a faster version. For a dimension marked setMultiValued(dim, true) and left at requireDimCount = false (the default, and the only option for association dimensions), the override cannot know the dimension's count and uses the placeholder -1, without checking whether any of the dimension's labels was hit. The admission check value != 0 lets -1 through; a dimension without hits has no result, so the list gets a null entry. Two documents are enough (b=b1 indexed before a=a1): getAllDims(10) = [b], but getTopDims(10, 10) = [null, b] and getTopDims(1, 10) = [null], which drops the only dimension with a hit; with -ea, Lucene's own assert facetResult != null throws. It affects FastTaxonomyFacetCounts, TaxonomyFacetIntAssociations, TaxonomyFacetFloatAssociations, SortedSetDocValuesFacetCounts and ConcurrentSortedSetDocValuesFacetCounts, in every release from 9.2.0 (SortedSetDocValues) or 9.3.0 (taxonomy) to 10.6.0 (10.6.0 by reading) and main. Admitting such a dimension only if one of its children has a hit fixes it; the repro and the whole lucene/facet test suite pass with assertions on and off.
- Dimension: a facet field such as
AuthororTag; its labels (children) are the values, such asa1.getAllDims(topN)returns oneFacetResultper dimension "that had hits" (Facets.java:53), sorted by value;getTopDims(topNDims, topNChildren)"should be the same as calling getAllDims and then only using the first topNDims" (Facets.java:61-62). - Multi-valued without
requireDimCount:FacetsConfig.setMultiValued(dim, true)lets a document carry several labels of one dimension. Then the dimension's count is not the sum of its labels' counts, and Lucene only knows it ifsetRequireDimCount(dim, true)indexes it, "which is unusual (default is false)" (FacetsConfig.java:120-124); association dimensions cannot set it at all (FacetsConfig.java:305-307). Without it the dimension's value is reported as -1, bygetAllDimstoo. - The fast
getTopDims: taxonomy facets (TaxonomyFacets.java:594-690, used byFastTaxonomyFacetCountsand the int and float association facets) and SortedSetDocValues facets (AbstractSortedSetDocValueFacetCounts.java:142-235, used bySortedSetDocValuesFacetCountsandConcurrentSortedSetDocValuesFacetCounts) rank dimensions by value in a priority queue first and compute the top labels only for the winners.
- The fast
getTopDimslooks at every dimension of the facet field in the taxonomy or reader, hit or not. For a multi-valued dimension withoutrequireDimCountit sets the value to -1, "a place-holder", without looking at the counts of its children (TaxonomyFacets.java:629-638;AbstractSortedSetDocValueFacetCounts.java:174-183). - A dimension enters the queue if its value is not 0 (
TaxonomyFacets.java:652;AbstractSortedSetDocValueFacetCounts.java:200). That check was meant to skip dimensions without hits, and does so for the other kinds of dimensions, whose value is 0 then. -1 passes. All -1 dimensions tie, and the tie goes to the smaller name. - For each dimension in the queue,
createFacetResultbuilds the result; it "Will return null if there are no children" with a hit (TaxonomyFacets.java:301-310;AbstractSortedSetDocValueFacetCounts.java:362-371). The code asserts the result is not null, "since only dims with non-zero values were considered earlier", and stores it anyway (TaxonomyFacets.java:685-687;AbstractSortedSetDocValueFacetCounts.java:228-232). getAllDimsbuilds the same results and drops the nulls (TaxonomyFacets.java:572-575;AbstractSortedSetDocValueFacetCounts.java:121-125), so the two methods disagree exactly on these dimensions.
Dimensions a and b, both multi-valued, requireDimCount false; doc 1 has b=b1, doc 2 has a=a1; the query matches doc 1.
| call | expected (first entries of getAllDims(10)) | assertions off | assertions on |
|---|---|---|---|
getAllDims(10) | [b] | [b] (right) | [b] |
getTopDims(10, 10) | [b] | [null, b] | AssertionError inside getTopDims |
getTopDims(1, 10) | [b] | [null]: b, the only dimension with a hit, is dropped | AssertionError |
getTopDims(3, 10), one non-matching document with three such dimensions named !h1, !h2, !h3 | two dimensions with hits | [null, null, null] | - |
The first three rows hold for FastTaxonomyFacetCounts (dense and sparse counting), TaxonomyFacetIntAssociations, SortedSetDocValuesFacetCounts and ConcurrentSortedSetDocValuesFacetCounts. The top-10 row was run for all of them on main with assertions on and off, and on 10.5.2, 9.12.3 and 9.3.0; the top-1 row by this doc's test for dense taxonomy counts and SortedSetDocValues facets, and by the fact check for the other three on main (on and off) and 10.5.2. The document order matters only for taxonomy top-1: with a indexed first, taxonomy facets visit b first and the sibling bug LFV-363 returns [a value=-1 childCount=1 / b1 (1)] instead of [null], with no assertion even with -ea. The last row comes from the follow-up's probe on main, taxonomy and SortedSetDocValues facets. With requireDimCount=true, or with single-valued dimensions, the same data gives [b].
Exposure and likelihood440 words
- Who is exposed. Callers of
Facets#getTopDimsonFastTaxonomyFacetCounts,TaxonomyFacetIntAssociations,TaxonomyFacetFloatAssociations,SortedSetDocValuesFacetCountsorConcurrentSortedSetDocValuesFacetCountswith at least one dimension markedsetMultiValued(dim, true)withoutrequireDimCount. That includes every multi-valued association dimension (requireDimCountis rejected for them) and the dimensions of an application that makes new dimensions multi-valued by default throughgetDefaultDimConfig, which its javadoc gives as an example ("like {@code multivalue = true}"), except SortedSetDocValues hierarchical dimensions. - When. Deterministically, whenever a query has no hits in at least one such dimension and that dimension ranks into the top
topNDimsadmitted dimensions: always whentopNDimsis at least the number of admitted dimensions, otherwise only against other -1 dimensions, by name, because a dimension with a value above -1 ranks above it. Queries without hits in some dimensions are the "sparse faceting" casegetAllDimsis documented for; a filtered query rarely hits every dimension. - What the caller sees (production, assertions off): a
nullentry, so code that reads each entry'sdimorlabelValuesthrows aNullPointerException(inferred from the returned null); a dimension with hits pushed out whentopNDimsis smaller than the number of admitted dimensions ([null]instead of[b]), which dropping nulls does not undo; in the extreme every slot ([null, null, null]). With assertions on (tests), anAssertionErrorinsidegetTopDims. - Deleting does not help (taxonomy). The taxonomy is append-only, so a dimension whose documents were all deleted and merged away still gets a null entry; SortedSetDocValues facets drop it once the deletes are merged (follow-up probe on main).
- With the sibling bug LFV-363 (taxonomy only: when an entry replaces the queue's bottom, its ordinal is not updated), the null can instead come back as the hit-less dimension's name over another dimension's labels:
[m_hidden value=-1 childCount=1 {r1=1}], wherer1belongs to the dimension that had the hit. The recommended fix here removes that combined case too. - Default vs opt-in.
getTopDimsandsetMultiValuedare opt-in calls;requireDimCount=falseis the default. Codec,IndexWriterConfigand searcher do not matter (query-time code; the tests use Lucene's randomized test settings and failed with every seed tried). - Not affected.
getAllDims,getTopChildren, hits and drill-down;requireDimCountdimensions; single-valued dimensions (all executed controls); by the code, SortedSetDocValues hierarchical dimensions (they read the indexed dimension count) and the defaultFacets#getTopDimsof other facet classes (agetAllDimsprefix). Nothing is written to the index, so a fix needs no reindex. - Reach. A read-only code search of the default branches found no call to
getTopDimsin Elasticsearch, OpenSearch or Solr; the bug reaches applications that use the facet module directly. Other branches and private applications were not searched.
The test shows that Facets#getTopDims on taxonomy (FastTaxonomyFacetCounts) and SortedSetDocValues facets admits a multi-valued dimension without requireDimCount that has no hits for the query, so its null result lands in the list.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.facet;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.facet.sortedset.DefaultSortedSetDocValuesReaderState;
import org.apache.lucene.facet.sortedset.SortedSetDocValuesFacetCounts;
import org.apache.lucene.facet.sortedset.SortedSetDocValuesFacetField;
import org.apache.lucene.facet.taxonomy.FastTaxonomyFacetCounts;
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.index.IndexableField;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestGetTopDimsDimWithoutHits extends LuceneTestCase {
private static Document doc(String id, IndexableField facet) {
Document doc = new Document();
doc.add(new StringField("id", id, Field.Store.NO));
doc.add(facet);
return doc;
}
public void testMultiValuedDimWithoutHits() throws Exception {
FacetsConfig config = new FacetsConfig();
config.setMultiValued("a", true); // requireDimCount stays false (the default)
config.setMultiValued("b", true);
try (Directory taxoIndexDir = new ByteBuffersDirectory();
Directory taxoDir = new ByteBuffersDirectory();
Directory ssdvIndexDir = new ByteBuffersDirectory();
IndexWriter taxoIndex = new IndexWriter(taxoIndexDir, new IndexWriterConfig());
DirectoryTaxonomyWriter taxoWriter = new DirectoryTaxonomyWriter(taxoDir);
IndexWriter ssdvIndex = new IndexWriter(ssdvIndexDir, new IndexWriterConfig())) {
for (String dim : new String[] {"a", "b"}) {
taxoIndex.addDocument(config.build(taxoWriter, doc(dim, new FacetField(dim, "x"))));
ssdvIndex.addDocument(config.build(doc(dim, new SortedSetDocValuesFacetField(dim, "x"))));
}
TermQuery onlyB = new TermQuery(new Term("id", "b"));
try (DirectoryReader taxoReader = DirectoryReader.open(taxoIndex);
DirectoryTaxonomyReader taxonomy = new DirectoryTaxonomyReader(taxoWriter);
DirectoryReader ssdvReader = DirectoryReader.open(ssdvIndex)) {
Facets taxo =
new FastTaxonomyFacetCounts(
taxonomy,
config,
new IndexSearcher(taxoReader).search(onlyB, new FacetsCollectorManager()));
Facets ssdv =
new SortedSetDocValuesFacetCounts(
new DefaultSortedSetDocValuesReaderState(ssdvReader, config),
new IndexSearcher(ssdvReader).search(onlyB, new FacetsCollectorManager()));
// expected: getTopDims is the getAllDims prefix [b]; actual: [null, b] and [null] (-ea: AssertionError)
assertEquals(
"taxonomy " + taxo.getAllDims(10) + " ssdv " + ssdv.getAllDims(10),
"taxonomy " + taxo.getTopDims(10, 10) + " ssdv " + ssdv.getTopDims(1, 10));
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-368/, 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.TestGetTopDimsDimWithoutHits
# the module(s) facet also need their third-party runtime jars
- Expected vs actual
- Expected the
getAllDims(10)prefix[b]for both; actual taxonomygetTopDims(10, 10)=[null, b]and SSDVgetTopDims(1, 10)=[null]with-da, and Lucene's ownassert facetResult != nullthrowsAssertionError(TaxonomyFacets.java:686) with-ea.
More files, environment, full output29 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError
org.apache.lucene.facet.taxonomy.TaxonomyFacets.getTopDims(TaxonomyFacets.java:686)
org.apache.lucene.facet.taxonomy.FastTaxonomyFacetCounts.getTopDims(FastTaxonomyFacetCounts.java:40)
org.apache.lucene.facet.TestGetTopDimsDimWithoutHits.testMultiValuedDimWithoutHits(TestGetTopDimsDimWithoutHits.java:78)- Javadoc: results "should be the same as calling getAllDims and then only using the first topNDims";
getAllDimslists dimensions "that had hits". - Lucene asserts the entry is never null, "since only dims with non-zero values were considered earlier".
- Upstream tests assert that equality for this configuration; they pass only because every dimension in them has hits.
The full argument254 words
- Contract.
Facets.java:59-64: getTopDims results "should be the same as calling getAllDims and then only using the first topNDims";Facets.java:52-56: getAllDims returns "topN labels for any dimension that had hits". Neither allows a null entry or a dimension without hits. - The code's own invariant.
assert facetResult != null(TaxonomyFacets.java:686,AbstractSortedSetDocValueFacetCounts.java:230), with the comment "should not be null since only dims with non-zero values were considered earlier" (:229). The -1 value itself is supported: "we cannot compute an accurate dim count, and use -1 as a place-holder" (TaxonomyFacets.java:635-636), andgetAllDimsreports the same -1. Only the admission is wrong. - Tests.
TestSortedSetDocValuesFacets#testBasic(:63-130) has dimensionamulti-valued withoutrequireDimCount(:65) and assertsgetTopDims(10, 10)equalsgetAllDims(10),awith value -1 included (:125-130);testCombinationsOfConfig(dimensione,:203-205) assertsgetTopDims(i, 10) == getAllDims(10).subList(0, i)for everyi(:299-306);TestTaxonomyFacetAssociations#testIntSumAssociation(:211-240) assertsgetTopDims(10, 10)equalsgetAllDims(10)for multi-valued association dimensions (:237-240). Every dimension in those tests has hits. - History. In the review of #747, which added
getTopDims, the reviewer asked to keep "the existing behavior, which is that aFacetResultis not returned when there are no children seen" and to "check for a value of0and then skip over that dim": the!= 0check is that suggestion. Nobody noted that these dimensions carry -1, not 0, even without hits. #915 (9.3.0) added the asserts; its description says "No new tests were written".
The devil's-advocate check180 words
The strongest "intended" case: Facets is @lucene.experimental; the getTopDims javadoc says "dims should be configured as requiring dim counts if using this functionality to ensure accurate counts are available", so a multi-valued dimension without requireDimCount could be called unsupported; the contract says "should"; and the ranking among -1 dimensions is arbitrary anyway.
Why it fails: the advice is about accurate counts (the -1 value, which getAllDims reports the same way), not about null entries or listing dimensions without hits. It cannot be followed for association dimensions, where FacetsConfig throws "AssociationFacetField cannot requireDimCount", and requireDimCount is documented as "unusual (default is false)". Lucene's own tests use this exact configuration and assert the prefix equality, and the code asserts the null cannot happen. Ties are broken by name the same way in both methods, so [b] is well defined. @lucene.experimental concerns API stability, not wrong results. Verdict: a defect (adjudication: confirmed, high; every verifier rated the intended case at most moderate; the only "not real" vote was a read-only review that could not run Java and agreed on the root cause).
Recommended Use the -1 placeholder only for dimensions with a child that has a hit
In both getTopDims overrides, give a multi-valued dimension without requireDimCount the placeholder -1 only if one of its children has a hit, and 0 otherwise, which the existing check already skips. The repro, the closest facet suites and the whole facet test suite (314 tests) pass with assertions on and off.
// TaxonomyFacets#getTopDims (multi-valued dim, no requireDimCount)
- dimValue = -1;
+ dimValue = hasChildWithHits(dimOrd) ? -1 : 0;
// AbstractSortedSetDocValueFacetCounts#getTopDims
- dimCount = -1;
+ dimCount = hasChildWithCount(ordRange.iterator()) ? -1 : 0;
// hasChildWithHits: walk the dim's children (dense counts) or the hash table
// (sparse counts) and return true at the first child with a count > 0All options and trade-offs4 options · 782 words
The check uses the same criterion as the per-dimension child count that getAllDims relies on (children with a count > 0, for dense and hash-table counting), so for the affected -1 branch getTopDims admits exactly the dimensions getAllDims lists, with the same -1 value and order. Other kinds of dimensions keep the unchanged value != 0 check, so dimensions with hits whose aggregate is 0 are still dropped (#13191, a different problem). The top labels are still computed only for the dimensions that make the final list. It also removes the combined case with the sibling bug LFV-363 (a hit-less dimension is never admitted), but not LFV-363 itself. Only query-time code changes, so no reindex is needed. Workaround until a fix ships: take the first topNDims entries of getAllDims(topNChildren).
Use the -1 placeholder only for dimensions with a child that has a hit (validated by the checks) (recommended)
TaxonomyFacets.java:637 and AbstractSortedSetDocValueFacetCounts.java:182: replace -1 with a check that stops at the first child with a count > 0, and use 0 (skipped by the existing admission test) when there is none. +41/-4 in the two files, including two small private helpers.
// TaxonomyFacets#getTopDims (multi-valued dim, no requireDimCount)
- dimValue = -1;
+ dimValue = hasChildWithHits(dimOrd) ? -1 : 0;
// AbstractSortedSetDocValueFacetCounts#getTopDims
- dimCount = -1;
+ dimCount = hasChildWithCount(ordRange.iterator()) ? -1 : 0;
// hasChildWithHits: walk the dim's children (dense counts) or the hash table
// (sparse counts) and return true at the first child with a count > 0Trade-offs
- One extra scan per such dimension: it stops at the first child with a hit; for a dimension without hits it walks all of its children (dense counts), the whole hash table (sparse counts, as
getTopChildrenForPathalready does per dimension) or its whole ordinal range (SortedSetDocValues). The worst case is the number of hit-less -1 dimensions times the number of their children, or times the number of non-zero hash entries in sparse mode; a maintainer may prefer one pass that marks the dimensions with hits (not implemented). No benchmark was run. - The deferred top-children computation that LUCENE-10488 added is kept: it still runs only for the dimensions in the final list.
What the checks validated
- Scratch worktree of
a9779ef3c193, gradle, JDK 25: the new test fails 7 of 8 on stock (assertions on and off) and passes 8 of 8 with the fix; withTestSortedSetDocValuesFacets,TestTaxonomyFacetCountsandTestTaxonomyFacetAssociations: 4 suites, 58 tests, 1 skipped, 0 failures, on and off; the whole:lucene:facet:test, 40 suites and 314 tests, 0 failures, with assertions on (7 skipped) and off (8 skipped);:lucene:facet:check -x test(google-java-format, ecj lint, forbidden APIs) passes. - Standalone runner with the fix shadowed: the new test 8 of 8 (
-ea,-da); the independent reproducer's 6 of 6; the history test's 13 of 13; the finder's 2 of 2; the association test's 2 of 2; the follow-up's 14-test probe 14 of 14 (7 fail on stock). A mutant with the taxonomy hash-table branch of the check forced to true fails, of the taxonomy tests, only the sparse-counting test, so that branch is covered (the 3 SortedSetDocValues tests also fail in that run because only the taxonomy source was patched in). - Not run: a backport to 9.x or 10.x (the same lines exist there), x86, a benchmark.
Compute such a dimension's top children up front and skip it if it has none (validated by the checks)
The finder's sketch: in the -1 branch call getTopChildrenForPath, skip the dimension when its child count is 0, and keep the result for the result phase. +23/-3 in the two files.
Trade-offs
Same results. It computes the top labels of every such dimension, including those that do not make the final list, which partly undoes the LUCENE-10488 optimisation.
What the checks validated
The new test passes 8 of 8 (standalone, -ea and -da); the whole :lucene:facet:test, 40 suites and 314 tests, passes with assertions on. Earlier, the finder's, the independent reproducer's, the history and the probe tests passed with it.
Replace the value != 0 test for all kinds of dimensions.
Trade-offs
Would also cover #13191 (dimensions with hits whose aggregate is 0 are dropped), the goal of open PR #13287, which removes the same taxonomy line. Broader behaviour change; not executed.
Call getAllDims(topNChildren) and take the first topNDims entries, which is what the base Facets#getTopDims does.
Trade-offs
Loses the optimisation of the fast getTopDims. Dropping nulls from the fast result is not enough when topNDims is small; setting requireDimCount is impossible for association dimensions.
Applications that share one index among users and enforce access only through the query should know that a dimension carried only by documents outside a user's view can still take null slots in that user's getTopDims result.
An AI finder wrote an exhaustive checker for the facet result API: every subset of hits over 12 modelled documents, compared getTopDims with the first entries of getAllDims and with brute-force counts. It first hit Lucene's own assertion inside getTopDims, then null entries with assertions off, and the finder reduced it to a two-document JUnit test.
The method in full171 words
Method: bounded-exhaustive translation validation of the facet result API (the hunt's "semi-formal" tier), a hunt unit (taxonomy, SortedSetDocValues and drill-sideways facets), 2026-10-26, stock main jars.
- The harness indexes 12 modelled documents and runs every one of the 2^12 hit subsets, with and without 1,100 padding documents (to reach hash-table counting), over several segment layouts and with deletions. For
FastTaxonomyFacetCounts,SortedSetDocValuesFacetCountsandConcurrentSortedSetDocValuesFacetCountsit checksgetTopDimsagainst the first entries ofgetAllDimsand exact brute-force counts. - It first stopped at an
AssertionErrorinsidegetTopDims. With assertions off, seed 1 gave 1,584 null entries (496 SortedSetDocValues, 512 concurrent, 576 taxonomy) among 3,504 mismatches in 703,437 checks; the other 1,920 mismatches are the sibling bug LFV-363's mislabelled entries. - With this fix and the sibling's, 184 of 192 configurations completed with 0 mismatches in 131,356,014 checks; the other 8 stopped at the harness's own setup check before checking anything.
- The finder reduced it to a public-API JUnit test with two documents; then independent AI tools reproduced, reviewed and judged it (see the checks).
No prior report found (best-effort search, 2026-10-26): 108 logged searches (90 tool queries, 18 manual searches and reads) over GitHub, commits, downstream projects, four mailing lists, JIRA and the web, every getTopDims issue and PR, and all 411 open PRs. Nearest: open PR #13287, which changes the same taxonomy line for a different problem and does not fix this one.
Searches5 searches
No issue, PR, review comment, JIRA entry, list thread, commit or web page reports that getTopDims admits a multi-valued dimension without requireDimCount when the query has no hits in it, or its symptoms (a null entry, the assertion, a dimension with hits pushed out). The newest upstream main fetched, a95c754ee00 (2026-10-26, facet module unchanged since a9779ef3c193), and branch_10x and branch_10_6 still have the -1 assignment, the != 0 check and the assert.
Nearest item: open, stale PR #13287 (2024-04-10, "Don't assume non-zero aggregations in getTopDims or test checkResults", for issue #13191) deletes the taxonomy admission line for the opposite defect (dimensions with hits and an aggregate of 0 dropped). It never mentions multi-valued dimensions, requireDimCount, -1, null results or SortedSetDocValues facets. Run as a shadow, it does not fix this repro (6 of 6 still fail) and makes a single-valued no-hit control return null too.
Context, not reports: #11361 / #747 / #773 (the feature), #11524 / #777 / #779 / #806 / #889 (the optimisation), #915 and #12966 (refactors), #11565 (a test NPE confirming that getTopChildren returns null for a dimension without hits), #12585 (non-positive aggregation values), #12862 (same files, getSpecificValue only). Gaps: keyword search only; GitHub search does not index diffs; one GitHub run was rate-limited (later runs completed); expired CI logs; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene) | 2026-10-26 | three scripted prior-report search runs (30 issue and PR queries, 3 commit queries: getTopDims with null, NullPointerException, AssertionError, requireDimCount, multi-valued, "no hits"; code fragments such as "dimCount != 0" and "only dims with non-zero values"); all 11 issues and PRs that mention getTopDims read | no report; #13287 and the context items above |
| Open PRs | 2026-10-26 | the changed files of all 411 open apache/lucene PRs | #13287 (same admission line), #12862 (same files, getSpecificValue only), #13948, #12547, #11780 (unrelated) |
| Review threads and history | 2026-10-26 | the review threads of #747, #777, #779, #806, #889, #915 and #12966; git log -S over the full history | the #747 review chose the != 0 skip; nobody discussed hit-less -1 dimensions; no fix commit |
| Mailing lists and JIRA | 2026-10-26 | 40 tool queries over dev@, java-user@, issues@ and commits@; 8 JIRA queries; LUCENE-9952 and LUCENE-10529 read | no report |
| Downstream repositories and web | 2026-10-26 | 2 queries over Elasticsearch, OpenSearch and Solr; 7 web queries | no report; no downstream call to getTopDims |
Introduced in 9.2.0 by #747 (LUCENE-10325, 2022), which added getTopDims with the SortedSetDocValues override; copied into the concurrent and taxonomy versions the same year. Taxonomy facets return the null from 9.3.0. Every release from 9.2.0 to 10.5.2 and main fails; 9.1.0 has no getTopDims. Not fixed upstream.
Versions tested6 rows
Introduced by LUCENE-10325 "Add getTopDims functionality to Facets", #747 (@Yuti-G, merged by @gsmiller, 7c33f04d374 on main, 2022-03-28; branch_9x backport #773, 77ccf2fb359), which added Facets#getTopDims with the SortedSetDocValues override and its dimCount != 0 check. Copies: ConcurrentSortedSetDocValuesFacetCounts and taxonomy facets (LUCENE-10488: #777 ef43242d77a, #779, #806; branch_9x #889 87655fd015c, 2022-05-13), all in 9.2.0. In 9.2.0 taxonomy getAllDims also listed the hit-less dimension, so the two methods agreed; LUCENE-10585 (#915, 8db1e41fc07; branch_9x 075f5103e9a, 2022-05-29, 9.3.0) made getAllDims drop it and added the assert, leaving the admission unchanged. #12966 (9ba4af7b883, 2024-04-05) moved the code into TaxonomyFacets and kept it. Single-file swaps on 9.2.0 pin each commit.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | this doc's test: 7 of 8 fail (gradle and standalone, assertions on and off): [null, b] and [null] |
| 10.5.2, 9.12.3, 9.3.0 | affected | this doc's test: 7 of 8 fail, the same messages (10.5.2 also with -ea: TaxonomyFacets.java:695, AbstractSortedSetDocValueFacetCounts.java:237) |
9.3.0 to 10.5.2 (30 releases), main ea57360912ed | release sweep | the follow-up's history test: 8 of 13 fail with -ea and -da; controls pass |
| 9.2.0 | SSDV only | SortedSetDocValues and concurrent: the null entry (no assert yet); taxonomy getAllDims also lists the hit-less dimension (value -1, no children), a related 9.2.0-only defect |
| 9.1.0 | no API | no getTopDims: the repro does not compile |
| main + the fix | not affected | every repro passes; the whole lucene/facet suite passes with assertions on and off |
An independent reproducer, a devil's advocate and an adjudication found it real, each with its own executed test; a read-only review by another model family agreed, and a second one, which could not run Java, gave a procedural "not real", low. The adjudication confirmed it with high confidence; this doc's gradle runs reproduced it on main a9779ef3c193.
Checks and verdicts414 words
Every check that ran code used the stock jars, with assertions on and off. The reviews by the other model family (OpenAI GPT-5.6) ran read-only; separate runs re-ran the finder's test, which failed both times.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Exhaustive harness, then a public-API JUnit: 2 of 2 fail on main (-ea, -da) and with -da on 10.5.2, 9.12.3, 9.4.2 and 9.2.0; pass with the fix. |
| cross-model review, refute lens | not real (low, procedural) | Could not run Java, so it voted not real on procedure only; root cause agreed, inputs in contract; it judged the requireDimCount advice no excuse for a null entry. A separate re-run failed as claimed. |
| Independent reproducer | real | Own 6-test JUnit: 5 of 6 fail on main, 10.5.2 and 9.12.3 (-ea, -da), 9.3.0 and 9.2.0 (-da); 6 of 6 pass with the fix. Found PR #13287 and showed it does not fix this; corrected the first taxonomy release to 9.3.0. |
| Cross-model review, intended lens | real (medium) | OpenAI GPT-5.6, read-only: intended argument moderate but no excuse; a separate run re-ran the repro and the fix, and a variant probe (concurrent the same; requireDimCount correct). |
| Devil's advocate | real | No intended-behaviour case survives: the advice is about counts and impossible for association facets, upstream tests assert the prefix equality, and the code asserts the null cannot happen. Its association test fails on main. |
| adjudication | confirmed (high) | Re-ran the independent reproduction's test with a new seed: 5 of 6 fail with -ea and -da; 6 of 6 pass with the fix. Not a duplicate. |
| Follow-up | history, prior art, security | History on 31 releases and main; prior-art search, verdict new; security probe on main, 10.5.2 and 9.12.3 (re-evaluated to none). |
| registry re-runs and audit | reproduced; keep | Fresh seeds: the finder's test fails on main (-ea, -da) and 10.5.2 (-da) and passes with the fix (main, -ea); the independent reproduction's fails 5 of 6. An independent audit re-run kept the row. |
| this doc's validation | reproduced, fix validated | New test: 7 of 8 fail on main (gradle and standalone, -ea and -da), 10.5.2, 9.12.3 and 9.3.0, and on 9.2.0 (there the taxonomy tests fail one step earlier, at getAllDims); with the recommended fix it passes, and so do the whole lucene/facet suite (314 tests, on and off) and the other verifiers' 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 416 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,556 words
- Registry title
- Facets#getTopDims on taxonomy and SortedSetDocValues facets admits a multi-valued dimension without requireDimCount even when the query has no hits in it, so the result list contains null (AssertionError with -ea) and a dimension that has hits can be pushed out (released since 9.2.0)
- Area
- facet / getTopDims of taxonomy and SortedSetDocValues facets (TaxonomyFacets#getTopDims and AbstractSortedSetDocValueFacetCounts#getTopDims, shared by FastTaxonomyFacetCounts, the taxonomy int and float association facets, SortedSetDocValuesFacetCounts and ConcurrentSortedSetDocValuesFacetCounts)
- Priority
- Medium, rank 175: Under the default requireDimCount=false, getTopDims returns null or drops the only dimension with hits on every filtered query, which is deterministic and fairly routine, but major downstream projects do not use the 9.2 API.
- Impact class
- silent wrong results
- Impact note
- Applications that call Facets#getTopDims(topNDims, topNChildren) on FastTaxonomyFacetCounts, TaxonomyFacetIntAssociations, TaxonomyFacetFloatAssociations, SortedSetDocValuesFacetCounts or ConcurrentSortedSetDocValuesFacetCounts with a dimension marked FacetsConfig#setMultiValued and left at requireDimCount=false (the default; multi-valued association dimensions cannot set it) get, whenever the query has no hits in such a dimension, a null element in the returned list (a NullPointerException in callers that read each entry; with -ea Lucene's own 'assert facetResult != null' throws inside getTopDims), and when topNDims is small the hit-less dimension can take the slot of one that has hits (SSDV getTopDims(1, 10) = [null] instead of [b]); default codec, IndexWriterConfig and searcher, two documents suffice, every release from 9.2.0 (SSDV) or 9.3.0 (taxonomy) to 10.6.0 (10.6.0 by reading) and main; getAllDims, getTopChildren, hits, drill-down, single-valued dimensions and requireDimCount dimensions are correct, and nothing is written to the index.
- Summary
- You mark facet dimensions a and b as multi-valued (FacetsConfig#setMultiValued, requireDimCount left at its default false), index one document with a=a1 and one with b=b1, search for the second document only and call getTopDims(10, 10), expecting [b], the same as getAllDims(10), as the Facets#getTopDims javadoc says. Taxonomy and SortedSetDocValues facets instead return [null, b] (with -ea, getTopDims throws AssertionError at Lucene's own 'assert facetResult != null'), and SSDV getTopDims(1, 10) returns [null], dropping b, the only dimension with a hit. For such dimensions getTopDims uses the placeholder value -1 without checking for hits, the admission test 'value != 0' lets -1 through, and the per-dimension result of a dimension without hits is null.
- Introduced
- 7c33f04d3746f855479da5177df8f19b6fdbbe84 (main, apache/lucene#747 'LUCENE-10325: Add getTopDims functionality to Facets', by Yuti-G, merged by gsmiller; issue #11361), first released through the branch_9x backport 77ccf2fb359ee926a3776ec4a78b2c0d95ee45fb (#773) in 9.2.0: it added Facets#getTopDims together with the SortedSetDocValuesFacetCounts override, whose 'dimCount != 0' admission already let the -1 placeholder of a hit-less multi-valued dimension through. Copies of the same logic: ConcurrentSortedSetDocValuesFacetCounts ef43242d77a (#777; branch_9x #889, 87655fd015c), 9.2.0; taxonomy facets got the same admission in #779 and #806 (main) and #889 (branch_9x), 9.2.0, and the null-element form of this bug from LUCENE-10585 (#915, 8db1e41fc07; branch_9x 075f5103e9a), 9.3.0, which made getTopChildren and getAllDims return null for such a dimension but left the getTopDims admission unchanged; #12966 (9ba4af7b883) moved the code into TaxonomyFacets and kept it. [read: git log -S and git blame over the full history; exec: single-file swaps on the 9.2.0 jars: SSDV without the #773 override passes, taxonomy and concurrent SSDV files from 87655fd015c^ pass, taxonomy files from 075f5103e9a reproduce the 9.3.0 behaviour] (2022-03-28 (#747 merged 22:54:07Z; backport #773 merged 2022-03-29T00:06:38Z); the concurrent SSDV and taxonomy overrides 2022-05-13 (#777, #779, #806, #889); the taxonomy null-element form 2022-05-29 (#915))
- First affected version
- 9.2.0 [exec] (the first release with Facets#getTopDims): SortedSetDocValuesFacetCounts and ConcurrentSortedSetDocValuesFacetCounts return the null element there (no AssertionError with -ea yet; the assert came with LUCENE-10585 in 9.3.0); taxonomy facets from 9.3.0 [exec] (in 9.2.0 taxonomy getAllDims also listed the hit-less dimension with value -1 and no children, so getTopDims agreed with it); 9.1.0 has no getTopDims [exec: compile error, the only missing symbol for the repro is getTopDims(int,int)]
- Affected versions
- Every release from 9.2.0 to 10.5.2 (31 releases), 10.6.0 (by reading) and main ea57360912ed; not fixed upstream. [exec] follow-up: a 13-test JUnit (8 getTopDims cases over FastTaxonomyFacetCounts, TaxonomyFacetIntAssociations, TaxonomyFacetFloatAssociations, SortedSetDocValuesFacetCounts and ConcurrentSortedSetDocValuesFacetCounts, 3 getAllDims checks, 2 controls) fails 8 of 13 on every release from 9.3.0 to 10.5.2 and on main, with -ea (AssertionError inside Lucene) and with -da (null element); 9.2.0 fails 6 of 13 (SSDV and concurrent SSDV null elements; taxonomy getAllDims lists the hit-less dimension); the controls pass everywhere; the candidate fix makes 13 of 13 pass on main. The independent reproduction's JUnit fails 5 of 6 on main, 10.5.2 and 9.12.3 (-ea and -da) and on 9.3.0 and 9.2.0 (-da); the finder's JUnit fails on main (-ea, -da), 10.5.2, 9.12.3, 9.4.2 and 9.2.0 (-da). [read] branch_10x and branch_10_6 have TaxonomyFacets.java and AbstractSortedSetDocValueFacetCounts.java byte-identical to 10.5.2 (not executed); upstream main cf398bfb142c (2026-10-26) still has the -1 assignment, the '!= 0' admission and the assert. 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 could only read the code (its low-confidence “not real” is procedural), 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-26, read-only, the prior-report record, verdict new: 108 logged queries: a scripted prior-report search in three runs (30 GitHub issue/PR, 3 commit, 2 downstream over Elasticsearch, OpenSearch and Solr, 40 mailing-list over dev@, java-user@, issues@ and commits@, 8 JIRA, 7 web), all 411 open apache/lucene PRs filtered by changed file path, the full threads of #13287, #13191 and #13198, the review threads of #747, #777, #779, #806, #889, #915 and #12966, LUCENE-9952, and git log -S over the full history); no hit is this bug. Closest item: open, stale PR #13287 (stefanvodita, 2024-04-10, 'Don't assume non-zero aggregations in getTopDims or test checkResults', for #13191) deletes the same TaxonomyFacets admission line for a different defect (dims with hits and a zero aggregate dropped); it does not fix this bug and would widen it (the independent reproduction shadow run). Related, not reports: #11361 / #747 / #773 (the feature; its review agreed on -1 for multi-valued dims without dim counts and on skipping dims whose value is 0, and nobody noted that a -1 dim can have no hits), #11524 / #777 / #779 / #806 / #889 (the optimisation), #915 and #12966 (refactors), #11565 (a test NPE that confirmed the null-for-no-hits convention of getTopChildren), #12585 (non-positive aggregation values), #12862 (same files, getSpecificValue only). Not fixed upstream (main cf398bfb142c, 2026-10-26, and branch_10x still contain the -1 assignment, the '!= 0' admission and the assert).
- 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, 9.3.0 and 9.2.0 release jars (this doc's test, unchanged). Earlier checks ran on mainea57360912ed, whose facet module is identical, and the follow-up's history test on every release from 9.1.0 to 10.5.2. The recommended fix was validated in the scratch worktree: the new test, the three closest facet suites, the wholelucene/facetsuite (40 suites, 314 tests) with assertions on and off, and:lucene:facet:check -x test.
- Finding record
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:300-310 (createFacetResult returns null when childCount == 0), :566-577 (getAllDims drops null results), :619-668 (getTopDims loop over root children: multi-valued dims take the indexed dim count or the -1 placeholder at :629-638, admission 'valueComparator.compare(dimValue, 0) != 0' at :652), :671-689 (result phase; createFacetResult at :685, 'assert facetResult != null' at :686, the null stored at :687)lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java:118-126 (getAllDims drops null results), :174-183 (dimCount = -1 for multi-valued dims without requireDimCount), :200 (admission 'dimCount != 0'), :228-232 (comment 'should not be null since only dims with non-zero values were considered earlier', the assert, the null stored)lucene/facet/src/java/org/apache/lucene/facet/Facets.java:52-67 (getAllDims 'any dimension that had hits'; getTopDims 'Results should be the same as calling getAllDims and then only using the first topNDims'; requireDimCount advice 'to ensure accurate counts are available')lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:120-124 (requireDimCount 'unusual (default is false)'), :142-151 (getDefaultDimConfig javadoc 'multivalue = true' for dimensions not known beforehand), :305-307 ('AssociationFacetField cannot requireDimCount')
- The finding in 30 seconds
lucene/facet/src/java/org/apache/lucene/facet/Facets.java:52-72lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:301-310, :572-575, :594-690 (placeholder :629-638, admission :652, result phase :671-689)lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java:121-125, :142-235 (placeholder :174-183, admission :200, result phase :215-234), :362-371lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:120-124, :305-307
- Who is affected
lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:120-124 (requireDimCount 'unusual (default is false)'), :142-151 (getDefaultDimConfig, 'multivalue = true'), :305-307 (AssociationFacetField cannot requireDimCount)lucene/facet/src/java/org/apache/lucene/facet/Facets.java:52-56 (getAllDims, 'sparse' faceting)lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:656-663 (queue replacement updates dim and value, not dimOrd)lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java:153-157, :200-209 and TaxonomyFacets.java:610-614, :652-663 (queue order: value, then name; a -1 dimension is outranked by any value above -1)
- Why it looks unintended
lucene/facet/src/java/org/apache/lucene/facet/Facets.java:25, :52-64lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:635-637, :686; lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java:229-230lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:120-124, :305-307lucene/facet/src/test/org/apache/lucene/facet/sortedset/TestSortedSetDocValuesFacets.java:63-130, :176, :203-205, :299-306lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetAssociations.java:84-91, :211-240
- Suggested fix
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:467-516 (getTopChildrenForPath: childCount counts children with count > 0, dense and sparse)lucene/CHANGES.txt:4258 (LUCENE-10488: Facets#getTopDims optimized for taxonomy faceting)
- TestGetTopDimsDimWithoutHits.java (JUnit test):
repro/LFV-368/src/org/apache/lucene/facet/TestGetTopDimsDimWithoutHits.java(82 lines)
A light summary card, made in parallel with this doc from an executed per-call trace on main a9779ef3c193 (assertions on and off, and with the finder's fix and the recommended fix shadowed in as controls; both print the same); the PNG is the summary card.
assets/LFV-368/bug.png(416 KB)
![Summary card. Title: getTopDims admits a multi-valued dimension that has no hits, and returns null for it. Example executed on main: dims a and b setMultiValued(dim, true), requireDimCount left false; doc 0 has a=a1, doc 1 has b=b1; the query id:1 hits doc 1 only. Inside getTopDims: dim a (0 hits) gets dimCount -1, -1 != 0 admits it, createFacetResult returns null (no child with a hit); dim b (1 hit) gets -1, is admitted, and returns b b1 (1). Expected, the first topNDims of getAllDims(10): getTopDims(10, 10) = [b], getTopDims(1, 10) = [b]. Actual on main: getTopDims(10, 10) = [null, b] (a null element); getTopDims(1, 10) = [null] with SortedSetDocValues facets (b is dropped); with -ea an AssertionError in getTopDims (:230; TaxonomyFacets :686). Footnote: with taxonomy facets, getTopDims(1, 10) is [null] only when the b document is indexed first; in the order shown, taxonomy visits b first and the separate stale-dimOrd bug (LFV-363) returns a's name over b1 (1), with no assertion. Affected: SortedSetDocValuesFacetCounts and its concurrent variant, FastTaxonomyFacetCounts, the taxonomy association facets. Fix: admit a -1 dim only if it has a child with a hit.](../assets/LFV-368/bug.png)