Taxonomy facets can list the top dimensions of a result set with Facets#getTopDims(topNDims, topNChildren); its javadoc says the result equals the first topNDims entries of getAllDims. Dimensions whose documents can carry several values, such as tags or authors, must be declared with FacetsConfig#setMultiValued. The bug hits those.
getTopDims visits the newest dimension y first and puts it in the one-entry queue; x beats it and the entry is reused, keeping y's ordinal 4; the result is built from ordinal 4, y's child under x's name. Expected against actual on main. Select the still to open it at full size. Generated with AI, checked against the code.- Declare dimensions x and y multi-valued with
requireDimCount(as thegetTopDimsjavadoc advises) and index three documents: x=x1, x=x2, then y=y1. So x is created first. - Run a match-all query and call
getTopDims(1, 10)onFastTaxonomyFacetCounts. You expect whatgetAllDims(10)lists first: x, value 2, children x1 (1) and x2 (1). - You get x with value 1, childCount 1 and the child y1 (1): y's numbers under x's name, and x's own children are gone. No error, with assertions on or off.
- Why: dimensions are visited newest first, so y fills the one-slot queue. When x beats it, Lucene reuses the slot and sets its name and value but not its ordinal, and the children are later read from y's ordinal.
- The order breaks too:
getTopDims(2, 10)over a (3 hits), b (2) and c (1) returns a with value 1 and c's child above b with value 2. Adding the missingbottomDim.dimOrd = dimOrd;fixes it, and the whole facet test suite passes.
Applications that call getTopDims on FastTaxonomyFacetCounts or the int and float association facets with multi-valued dimensions, asking for fewer dimensions than the code admits (every dimension with a non-zero value). Deterministic: an older multi-valued dimension that outranks a newer one in the full queue gets the newer one's children. Single-valued dimensions, getAllDims, getTopChildren and SortedSetDocValues facets are correct.
Mechanism, terms and numbers791 words
Facets#getTopDims(topNDims, topNChildren) should return the same as the first topNDims entries of getAllDims. Taxonomy facets compute it with a priority queue of topNDims entries, each holding a dimension's name, ordinal and value. When the queue is full and a dimension beats the bottom entry, TaxonomyFacets#getTopDims reuses that entry and updates its name and value but not its ordinal. A multi-valued dimension's children are computed from that ordinal at the end, so the result carries the pushed-out dimension's children and childCount under the winner's name (with requireDimCount, since 9.3.0, also its value), and the list can fall out of value order. Dimensions are visited newest-created first, so it happens whenever an older multi-valued dimension outranks a newer one already in the queue; with topNDims = 1 a multi-valued winner gets the children of the newest-created dimension with a non-zero value (with requireDimCount, the newest-created one with hits). In the repro, x with 2 hits is returned as x value=1 childCount=1 [y1 (1)]. It needs FacetsConfig#setMultiValued, which multi-valued data requires, and fails with and without requireDimCount. Single-valued dimensions, getAllDims, getTopChildren, hits and SortedSetDocValues facets are correct. Every release from 9.2.0 to 10.6.0 (10.6.0 by reading) and main is affected; one added line fixes it.
- Taxonomy facets: each document gets category labels such as
x/x1throughFacetField; a side index, the taxonomy, gives each dimension (x) and each label (x/x1) an ordinal, in order of creation.FastTaxonomyFacetCountscounts a query's hits per ordinal; the association facets aggregate a value instead. - Multi-valued dimension:
FacetsConfig.setMultiValued(dim, true), "Pass true if this dimension may have more than one value per document" (FacetsConfig.java:180-181). Such data needs it:FacetsConfig.buildrejects a second value of a single-valued dimension ("is not multiValued, but it appears more than once in this document",FacetsConfig.java:233-241). WithsetRequireDimCount(dim, true)the dimension's own ordinal is indexed too, so its document count is exact (FacetsConfig.java:190-194,:386-397). getTopDims: "Returns labels for topN dimensions and their topNChildren sorted by the number of hits/aggregated values that dimension matched. Results should be the same as calling getAllDims and then only using the first topNDims" (Facets.java:60-62). Taxonomy facets override it with a priority queue oftopNDimsentries; eachDimValueholdsdim,dimOrdandvalue(TaxonomyFacets.java:46-56,:610-614).
- The loop visits dimensions newest-created first: the taxonomy's children array gives "the category that was added last" and each sibling is "the previous youngest child" (
TaxonomyIndexArrays.java:253-255,:269; the loop isTaxonomyFacets.java:620-668). - While the queue has room, each dimension with a non-zero value gets a new entry (
:652-654). Once it is full, a dimension that beats the bottom entry reuses it:bottomDim.dim = dim; bottomDim.value = dimValue; pq.updateTop();(:656-662). The entry'sdimOrdkeeps the ordinal of the dimension that was pushed out. - At the end, a single-valued dimension takes its children from a cache keyed by name (
:639-651,:678-680): correct. A multi-valued dimension's children are computed fromdimValue.dimOrd(:683), and for multi-valued dimensions withrequireDimCountthe value comes from that ordinal too (without it, the-1placeholder;:506-512).createFacetResult(topChildrenForPath, dim)labels the result with the winner's name (:685). - No entry is created after the queue is full, so the ordinals in the queue stay those of the first
topNDimsdimensions admitted: the newest-created ones with a non-zero value (:652). That includes multi-valued dimensions withoutrequireDimCount, admitted at-1even without hits (the sibling bug LFV-368). WithtopNDims= 1 a multi-valued winner therefore gets the children of the newest-created dimension with a non-zero value; withrequireDimCounton every dimension, of the newest-created dimension with hits. A single-valued winner keeps its own children, cached by name.
- x/y: taxonomy ordinals 1 = x, 4 = y. Visit y: the queue gets
{y, 4, 1}. Visit x: 2 > 1, the entry becomes{x, 4, 2}. The result is built from ordinal 4:x value=1 childCount=1 [y1 (1)]instead ofx value=2 childCount=2 [x1 (1), x2 (1)]. - a, b, c with 3, 2 and 1 hits, created in that order,
getTopDims(2, 10):[a value=1 childCount=1 [c1 (1)], b value=2 childCount=2 [b1 (1), b2 (1)]], out of value order. - Variants the verifiers executed: a name tie between dimensions without dim counts (value -1 each);
TaxonomyFacetIntAssociations(dimension a reported withb1 (7)); a multi-valued dimension replacing a single-valued one (value=0and the loser's child); and, when the pushed-out dimension has no hits (possible only through the sibling bug LFV-368), anullentry with assertions off or anAssertionErroratTaxonomyFacets.java:686with assertions on.
Exposure and likelihood385 words
- Who is exposed. Applications that call
getTopDimsonFastTaxonomyFacetCounts,TaxonomyFacetIntAssociationsorTaxonomyFacetFloatAssociations(all three executed; the float class with a value source and with indexed associations) with dimensions declared multi-valued, and ask for fewer dimensions than the code admits: every dimension with a non-zero value (TaxonomyFacets.java:652), which includes multi-valued dimensions withoutrequireDimCounteven when they have no hits (:637, the sibling LFV-368). The exact trigger is a multi-valued winner replacing the bottom entry of a full queue. The association facets cannot follow the javadoc'srequireDimCountadvice (FacetsConfig.buildrejects it,FacetsConfig.java:305-307), so for them the value stays the-1placeholder while the children and childCount are wrong. Tags, authors or genres must be multi-valued, becauseFacetsConfig.buildrejects a second value otherwise; the demo's association dimensions are multi-valued (AssociationsFacetsExample.java:59-61), though the demo does not callgetTopDims. - Default vs opt-in.
setMultiValuedis opt-in (the default dimension config is single-valued) but required for multi-valued data.requireDimCount, which the javadoc recommends forgetTopDims, does not help. The defect is in query-time code that runs after counting, so codec,IndexWriterConfigand searcher should not matter; the test (randomized directory, searcher and codec, plainIndexWriterConfig) failed with every seed tried: the fixed seeds and 10 random iterations, on Linux aarch64. - Scale. Deterministic for a fixed taxonomy, counts and query; one document with three dimensions is enough. A multi-valued entry is right only when no older multi-valued dimension had to push a newer one out of the full queue; with
topNDims= 1 andrequireDimCounton every dimension, only when the top dimension is also the newest-created one with hits. - Silent. The wrong entry has the right name and plausible numbers, and nothing is thrown (except in LFV-368's null-entry case).
FacetResult#equalscompares the value, the childCount and every child label and count but ignoresdimandpath(FacetResult.java:72-79), so a comparison withequalsmisses it only when all of those happen to match. - Not affected. Single-valued dimensions (looked up by name),
getAllDims,getTopChildren,getSpecificValue, hits, drill-down, the index (nothing is written) and SortedSetDocValues facets (looked up by name). - Reach.
getTopDimsexists since 9.2.0. GitHub code search of the Elasticsearch, OpenSearch and Solr default branches found 0 hits each forgetTopDims,FastTaxonomyFacetCountsandDirectoryTaxonomyReader.
The test shows that taxonomy getTopDims(1, 10) returns dim y's value, childCount and children under dim x's name when x (multi-valued, requireDimCount, created first, 2 hits) replaces y (1 hit) in the top-dims queue, because the replacement keeps y's dimOrd.
/*
* 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 TestTaxonomyGetTopDimsMultiValued extends LuceneTestCase {
public void testOlderDimWithMoreHits() throws Exception {
FacetsConfig config = new FacetsConfig();
for (String dim : new String[] {"x", "y"}) {
config.setMultiValued(dim, true);
config.setRequireDimCount(dim, true);
}
try (Directory dir = new ByteBuffersDirectory();
Directory taxoDir = new ByteBuffersDirectory();
IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig());
DirectoryTaxonomyWriter taxoWriter = new DirectoryTaxonomyWriter(taxoDir)) {
Document doc = new Document();
doc.add(new FacetField("x", "x1"));
writer.addDocument(config.build(taxoWriter, doc));
doc = new Document();
doc.add(new FacetField("x", "x2"));
doc.add(new FacetField("y", "y1"));
writer.addDocument(config.build(taxoWriter, doc));
try (DirectoryReader reader = DirectoryReader.open(writer);
DirectoryTaxonomyReader taxoReader = new DirectoryTaxonomyReader(taxoWriter)) {
FacetsCollector hits =
new IndexSearcher(reader)
.search(MatchAllDocsQuery.INSTANCE, new FacetsCollectorManager());
Facets facets = new FastTaxonomyFacetCounts(taxoReader, config, hits);
// Facets#getTopDims javadoc: the same as getAllDims, then using only the first topNDims
assertEquals(facets.getAllDims(10).subList(0, 1), facets.getTopDims(1, 10));
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-363/, 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.TestTaxonomyGetTopDimsMultiValued
# the module(s) facet also need their third-party runtime jars
- Expected vs actual
- Expected (Facets#getTopDims javadoc: same as getAllDims, first topNDims):
dim=x value=2 childCount=2 x1 (1) x2 (1); actual (-ea and -da):dim=x value=1 childCount=1 y1 (1).
More files, environment, full output48 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: expected:<[dim=x path=[] value=2 childCount=2
x1 (1)
x2 (1)
]> but was:<[dim=x path=[] value=1 childCount=1
y1 (1)
]>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:120)
org.junit.Assert.assertEquals(Assert.java:146)
org.apache.lucene.facet.taxonomy.TestTaxonomyGetTopDimsMultiValued.testOlderDimWithMoreHits(TestTaxonomyGetTopDimsMultiValued.java:62)- The javadoc:
getTopDimsresults "should be the same as calling getAllDims and then only using the first topNDims". - It fails with
requireDimCount, the setting the javadoc recommends; the SortedSetDocValues version of the same algorithm looks children up by name and is correct. - No upstream test runs the replacement branch with a multi-valued dimension; no commit ever set
dimOrdthere.
The full argument289 words
- Contract.
Facets.java:60-64: "Returns labels for topN dimensions and their topNChildren sorted by the number of hits/aggregated values that dimension matched. Results should be the same as calling getAllDims and then only using the first topNDims. Note that dims should be configured as requiring dim counts if using this functionality to ensure accurate counts are available".FacetResult:dimis the "Dimension that was requested",valuethe "Total number of documents containing a value for this path",labelValuesits "Child counts" (FacetResult.java:24-40). The base method isgetAllDims(topNChildren).subList(...)(Facets.java:69-72). Taxonomy facets inherited it only in development history (before #779/#806 on main and #889 on branch_9x); no release shipped that, since 9.1.0 has nogetTopDimsand 9.2.0 contains #889. - Twin.
AbstractSortedSetDocValueFacetCounts#getTopDimsruns the same queue and the same two-field update (:206-208), but has no ordinal in its entry and looks children up by name (:226,:228): correct. - Tests.
TestTaxonomyFacetCounts#testCountRoot(getTopDims(1, 10)at:677),testGetFacetResultsTwice(:722) andtestSparseFacets(:253), andTestTaxonomyFacetValueSource(:219,:702) use single-valued dimensions;testRandomasks for every dimension (:1062). The multi-valued tests ofTestTaxonomyFacetCounts(setMultiValuedat:390,:430,:481,:533) do not callgetTopDims, andTestTaxonomyFacetAssociationscallsgetTopDims(10, 10)for its 4 multi-valued dimensions, more slots than dimensions. A grep of the test trees finds no test that runs the replacement branch with a multi-valued dimension. The tests compare withFacetResult#equals(for example:678,:722), which ignoresdim. - History. Introduced by the LUCENE-10488 optimisation (#779, #806, backport #889).
git log -S 'bottomDim.dimOrd'over the full history finds no commit; nobody raised it in the review threads of 12 related PRs.
The devil's-advocate check203 words
The strongest "intended" case: Facets is @lucene.experimental; the javadoc says "should be the same", soft wording, and advises requireDimCount; the finder's first repro needs a three-way tie broken by name; the default single-valued dimensions are fine; and FacetResult#equals ignores the name anyway.
Why it fails: @lucene.experimental is about API stability, not wrong numbers. The recommended requireDimCount setting fails with no tie at all (x/y, a/b/c), and the value and childCount are wrong too, so equals sees it. Multi-valued dimensions are a designed-for input: getTopDims has its own multiValued/requireDimCount branch (TaxonomyFacets.java:629-638), and multi-valued data cannot be indexed any other way. The SortedSetDocValues version of the same algorithm is correct, and the fix costs nothing. Both reviews that rated the intended-behaviour argument called it weak; the devil's advocate rejected it (bug real, high confidence) and the adjudication confirmed the bug with high confidence. Two narrower points stand: without requireDimCount, ranking dimensions by the -1 placeholder and then by name is intended (accurate counts need requireDimCount), so there only the wrong children and childCount are the bug; and the association facets cannot set requireDimCount at all (FacetsConfig.java:305-307), so for them the javadoc's advice cannot be followed.
Recommended Set the ordinal when the bottom entry is reused
Add bottomDim.dimOrd = dimOrd; next to bottomDim.dim = dim; in TaxonomyFacets#getTopDims, so the reused queue entry gets the winner's ordinal. The repro, the other repros and the whole lucene/facet test suite pass with it, assertions on and off. Mention open PR #13287, which edits the same lines.
DimValue bottomDim = pq.top();
bottomDim.dim = dim;
+ bottomDim.dimOrd = dimOrd;
bottomDim.value = dimValue;
pq.updateTop();All options and trade-offs4 options · 619 words
The reused entry then describes the winner completely and the result phase computes the winner's own children; no extra work. Until a fix ships, applications can call getAllDims(topNChildren) and take the first topNDims entries, which is what the base Facets#getTopDims does. Only query-time results change, so no reindex is needed. Either way, add a test that compares getTopDims with the getAllDims prefix for multi-valued dimensions with fewer slots than dimensions and an older dimension winning, and that compares the dimension name, since FacetResult#equals ignores it. Open PR #13287 (2024, stale) rewrites the same replacement block for zero-valued dimensions and also sets only dim and value; a fix will conflict with it. The sibling LFV-368 (dimensions without hits admitted with the -1 placeholder) needs its own fix in the admission line.
TaxonomyFacets.java:659-662: one added line.
DimValue bottomDim = pq.top();
bottomDim.dim = dim;
+ bottomDim.dimOrd = dimOrd;
bottomDim.value = dimValue;
pq.updateTop();Trade-offs
None found: the smallest change, no extra work, and it leaves the rest of the method as it is. It keeps the mutable entry, so a future edit could drop a field again; option B removes that risk.
What the checks validated
- Scratch worktree of
a9779ef3c193, gradle, JDK 25: the new test fails 2 of 3 on stock with assertions on and off; with the fix, the new test plusTestTaxonomyFacetCounts,TestTaxonomyFacetAssociationsandTestTaxonomyFacetValueSourcepass (4 suites, 44 tests); the whole:lucene:facet:testpasses with assertions on (40 suites, 309 tests, 7 skipped, 0 failures) and off (309 tests, 8 skipped, 0 failures);:lucene:facet:check -x test(google-java-format, ecj lint, forbidden APIs) passes. - Standalone runner with the fix shadowed: the new test 3 of 3 with
-eaand-da; the independent reproducer's class 7 of 7 (4 of 7 fail on stock). Earlier, with the same line, the finder's and the devil's advocate's tests passed on main, the release-sweep class passed 9 of 9 on main, 10.5.2 and 9.12.3, and the security probe's contract, no-hit and control tests passed (5 of 6; its informational test, which asserts the stale label, flips as expected).
Replace the add/replace block with pq.insertWithOverflow(new DimValue(dim, dimOrd, dimValue)); and make the three DimValue fields final (+6/-15). The queue's comparator (value, then reverse dimension name, TaxonomyFacets.java:610-614) is exactly the hand-written replacement condition, so the same dimensions are kept.
if (valueComparator.compare(dimValue, 0) != 0) {
pq.insertWithOverflow(new DimValue(dim, dimOrd, dimValue));
}Trade-offs
Makes a stale field impossible and shortens the method; allocates one small object per dimension with a non-zero value instead of at most topNDims. A larger diff than option A.
What the checks validated
Standalone runner on main a9779ef3c193: the new test 3 of 3, the independent reproducer's class 7 of 7, the finder's 2 of 2; gradle: the whole :lucene:facet:test with assertions on (40 suites, 309 tests, 0 failures) and :lucene:facet:check -x test pass.
Drop dimOrd from DimValue and resolve the dimension's ordinal from its name in the result phase.
Trade-offs
Not validated. One ordinal lookup per returned dimension, and more change than option A for the same effect.
Call getAllDims(topNChildren) and keep the first topNDims entries, or ask getTopDims for at least as many dimensions as the code admits (every dimension with a non-zero value, including multi-valued dimensions without requireDimCount, which are admitted at -1 even without hits). More slots do not avoid the sibling LFV-368's null entries.
Trade-offs
Correct, but gives up the optimisation getTopDims exists for; not a fix.
GitHub code search of Elasticsearch, OpenSearch and Solr found no use of getTopDims, FastTaxonomyFacetCounts or DirectoryTaxonomyReader, so the exposure found is in applications that use Lucene's facet module directly.
An AI finder suspected the stale ordinal while reading TaxonomyFacets#getTopDims to build a bounded-exhaustive check of the facet result API, which compares getTopDims with the getAllDims prefix over every subset of hits of 12 modelled documents. The check then reported wrong taxonomy entries in every stock run, and the finder reduced it to a JUnit test.
The method in full169 words
Method: semi-formal. The hypothesis came from code reading; a bounded-exhaustive translation-validation harness witnessed it, and a public-API JUnit test reproduced it.
- The harness models 12 documents over taxonomy and SortedSetDocValues facets and checks every one of the 2^12 hit subsets per seed, with and without 1,100 padding documents, several segment layouts and deletions, comparing
getTopDimswith thegetAllDimsprefix and with exact counts. - On stock main every one of 8 runs reported wrong taxonomy
getTopDimsresults (seed 1: 1,920 mismatching taxonomygetTopDimsresults without a null entry among 3,504 mismatches in 703,437 checks; the other 1,584 contain a null entry, in taxonomy and SortedSetDocValues facets, the symptom of the sibling bug LFV-368). - With this one-line fix and the sibling's fix, 184 of 192 configurations completed with 0 mismatches in 131,356,014 checks; the other 8 stopped at the harness's own setup check (
AssertionError: maxDoc) before checking anything. - The finder then wrote a one-document JUnit test (three tied dimensions); the independent reproducer added the no-tie x/y case used here.
No prior report found (searched 2026-10-26, refreshed 2026-10-27): 123 logged queries over GitHub issues, PRs and commits, JIRA, four mailing lists, downstream projects and the web, the review threads of 12 related PRs and all open PRs. Open PR #13287 edits the same lines for another problem and keeps the ordinal stale.
Searches5 searches
Keyword search only: GitHub search does not index diffs, 8 queries hit the shared rate limit and were re-run, and private reports are not visible. Nearest items: #13287 (open, stale, 2024: zero-valued dimensions in the same block, still sets only dim and value); #13191 / #13198 (open: a zero-aggregation test failure with single-valued dimensions only, which this bug does not reach since 9.3.0); #11524, #779, #806, #889 (the introducing optimisation); #11361, #747, #777 (the feature and the SortedSetDocValues versions); #915, #12966 (refactors); #14008 (a different, fixed taxonomy wrong-count bug).
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene) | 2026-10-26; refreshed 2026-10-27 (re-run by the fact check) | the scripted prior-report search in two runs (32 issue and PR queries: getTopDims alone and with multiValued, dimOrd, wrong, getAllDims; the class names; bottomDim; createFacetResult; symptom phrases) and 13 manual searches; on 2026-10-27 issue and PR searches for getTopDims again | no report; the items listed above |
| Review threads and open PRs | 2026-10-26 | comments and reviews of #747, #773, #777, #779, #806, #889, #915, #12966, #13358, #13568, #13287 and #13198 grepped for dimOrd, bottomDim, updateTop and DimValue; all 410 open PRs listed and the changed files of 162 read | only #13287 touches the getTopDims block, and it keeps dimOrd stale |
| Commits | 2026-10-26 | 6 commit queries; git log -S 'bottomDim.dim = dim' and -S 'bottomDim.dimOrd' over the full history | the code came with #779 and #806; no commit has ever set bottomDim.dimOrd |
| Mailing lists (dev@, java-user@, issues@, commits@) and JIRA | 2026-10-26 | 40 list queries, 8 JIRA text queries, the comments of LUCENE-10325 and LUCENE-10488 | only notifications of the PRs above and unrelated facet threads |
| Downstream (Elasticsearch, OpenSearch, Solr) and the web | 2026-10-26 | 4 downstream queries plus code searches for getTopDims, FastTaxonomyFacetCounts and DirectoryTaxonomyReader; 9 web searches | nothing; code search found 0 hits for those three symbols in each project |
Introduced by the 2022 getTopDims optimisation for taxonomy facets (#779, #806; backport #889) and first released in 9.2.0. Every release from 9.2.0 to 10.5.2 and main fails; from 9.3.0 (#915) every multi-valued dimension is affected and the value and childCount are wrong too. Not fixed upstream.
Versions tested7 rows
Introduced by LUCENE-10488 "Optimize Facets#getTopDims" (issue #11524): #779 (57f8cb2fd6b, IntTaxonomyFacets) and #806 (f0ec2261672, FloatTaxonomyFacets) by @Yuti-G, approved by @gsmiller, merged 2022-05-13; branch_9x backport #889 (87655fd015c) the same day. Before it (and before #779/#806 on main), taxonomy facets inherited the correct base getTopDims, but only in development history: no release shipped that, since 9.1.0 has no getTopDims and 9.2.0 contains #889. 9.3.0: LUCENE-10585 (#915, 8db1e41fc07; branch_9x 075f5103e9a) sent every multi-valued dimension down the uncached path and built the value and childCount from the recomputed children; hierarchical single-valued dimensions, affected in 9.2.0, have been cached since. #12966 (9ba4af7b883, first in 10.0.0; 9.11.0 got the backport 42884fab46d, #13358) merged the int and float copies into TaxonomyFacets and #14761 (a7704f5242d) switched to a comparator queue; both kept the omission. Upstream main still has it (2026-10-27).
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | this doc's test: 2 of 3 fail (gradle and the standalone runner, assertions on and off), x value=1 childCount=1 [y1 (1)] |
| 10.5.2, 10.0.0, 9.12.3, 9.3.0 | affected | this doc's test: 2 of 3 fail, the same output (10.5.2 also with assertions off) |
| 9.2.0 | children only | this doc's test: 2 of 3 fail; the value is right, the children and childCount are not: x value=2 childCount=1 [y1 (1)] |
9.2.0 to 10.5.2 (31 releases), main ea57360912ed | release sweep | the 9-test history class: 4 of 9 fail on every release; the 3 controls pass |
| 9.2.0 with the pre-#889 taxonomy classes | bisect | 9 of 9 pass: the backport #889 introduced it |
| 9.1.0 | no API | compile error: Facets has no getTopDims |
| fix (one line) on main, 10.5.2, 9.12.3 | not affected | every repro passes; the whole facet suite passes on main |
An independent reproducer, a devil's advocate, an adjudication and an audit each confirmed it with executed tests; read-only reviews by another model family agreed on the root cause, one giving a procedural "not real" because it could not run Java. This doc's gradle runs reproduced it on main a9779ef3c193.
Checks and verdicts303 words
Not done: a cross-model review that executes code (both reviews ran read-only; separate runs re-ran the repro).
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | The bounded-exhaustive harness found wrong taxonomy entries in all 8 stock runs; the JUnit test fails 2 of 2 on main with assertions on and off and on 7 releases from 9.4.2 to 10.5.2, 1 of 2 on 9.2.0 (the requireDimCount case), and passes with the fix. |
| cross-model review, refute lens | not real (low, procedural) | An independent reviewer from another model family (OpenAI GPT-5.6), read-only: could not run Java, so "not reproduced" by construction; it agreed on the root cause, found the inputs in contract and the intended argument weak; a separate re-run failed 2 of 2. |
| independent reproducer | real | Own 7-test class: 4 of 7 fail on main (assertions on and off), 10.5.2 and 9.12.3, 40 of 70 over 10 iterations, 7 of 7 with the fix; found that the value and childCount are wrong too and that the list can fall out of order. |
| cross-model review, intended lens | real (high) | OpenAI GPT-5.6, read-only: the intended argument is weak; a separate re-run failed 2 of 2. |
| devil's advocate | real (high) | Built the strongest intended case and showed why it fails; its no-tie variant fails on main and 10.5.2 and passes with the fix. |
| adjudication | confirmed (high) | Re-ran the independent reproducer with a new seed: 4 of 7 fail with assertions on and off, 7 of 7 with the fix; not a duplicate. |
| audit | keep | Re-ran the independent reproducer with fresh seeds: 4 of 7 fail, 7 of 7 with the fix. |
| this doc | reproduced | New 3-test class through gradle and the standalone runner on main a9779ef3c193 and five releases; fixes A and B validated. |
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 478 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,398 words
- Registry title
- Taxonomy Facets#getTopDims returns another dimension's children, childCount and value under a multi-valued dimension's name, because replacing the bottom of the top-dims queue updates dim and value but not dimOrd (released since 9.2.0)
- Area
- facet / taxonomy facets (TaxonomyFacets#getTopDims, shared by FastTaxonomyFacetCounts and the int and float association facets)
- Priority
- Medium, rank 176: GetTopDims can silently give one dimension's entry another dimension's children, which is plausible, but it needs multi-valued dims with requireDimCount, a narrower configuration than its sibling LFV-368.
- Impact class
- silent wrong results
- Impact note
- Applications that call Facets#getTopDims(topNDims, topNChildren) on taxonomy facets (FastTaxonomyFacetCounts, TaxonomyFacetIntAssociations, TaxonomyFacetFloatAssociations) with dims configured through FacetsConfig#setMultiValued (with requireDimCount, as the getTopDims javadoc advises, or without) and with more dims with hits than topNDims silently get an entry that has the right dim name but another dimension's children, childCount and (since 9.3.0) value whenever a dim visited later outranks the queue bottom (dims are visited newest-created first, so typically when an older dim wins); the winner's own children are missing and the list can be out of value order; default codec, IndexWriterConfig and taxonomy, one document suffices, -ea and -da, every release from 9.2.0 to 10.6.0 (10.6.0 by reading) and main; flat single-valued dims (the default DimConfig), getAllDims, getTopChildren, hits and drill-down are correct, and nothing is written to the index.
- Summary
- You mark taxonomy facet dims as multi-valued, run a query and call getTopDims(1, 10), and expect the same as getAllDims(10).subList(0, 1), as the Facets#getTopDims javadoc says. When the winning dim is visited after the dim it pushes out of the internal top-dims queue (dim x with 2 hits, created first, and dim y with 1 hit, both with requireDimCount), Lucene returns 'dim=x value=1 childCount=1 y1 (1)' instead of 'dim=x value=2 childCount=2 x1 (1) x2 (1)', that is y's children and count under x's name. The queue-replacement branch sets bottomDim.dim and bottomDim.value but never bottomDim.dimOrd, and the children of multi-valued dims are then computed from that stale ordinal.
- Introduced
- 57f8cb2fd6bcc0a27f89dc8c36b43bfd812ded46 (main, apache/lucene#779 'LUCENE-10488: Optimize Facets#getTopDims in IntTaxonomyFacets', by Yuti-G, approved by gsmiller) and f0ec226167230a42b27a4b946c9c0f74d0f0abfc (main, #806, the same block in FloatTaxonomyFacets); first released through the branch_9x backport 87655fd015ceef36c4210a0d20f1071068544fe4 (#889, gsmiller) in 9.2.0; issue #11524 (LUCENE-10488). [read: git log -S 'bottomDim.dim = dim' and git blame at 9ba4af7b883^ in the history clone; git log -S 'bottomDim.dimOrd' finds no commit ever setting it; exec: 9.2.0 jars with IntTaxonomyFacets and FloatTaxonomyFacets from 87655fd015c^ pass 9/9, the stock 9.2.0 jars fail 4/9]. Before it, taxonomy facets inherited the default Facets#getTopDims (a getAllDims prefix), which is correct. LUCENE-10585 (#915, 8db1e41fc07; branch_9x 075f5103e9a, 9.3.0) changed which dims take the stale-ordinal path; #12966 (9ba4af7b883) and #14761 (a7704f5242d) moved the code and kept the omission (2022-05-13 (#779 merged 22:54:32Z, #806 22:54:41Z, backport #889 23:41:13Z))
- First affected version
- 9.2.0 [exec] (the first release with Facets#getTopDims, -ea and -da; there the affected dims are multi-valued dims with requireDimCount and hierarchical dims; from 9.3.0 every multi-valued dim, with or without requireDimCount, and no longer hierarchical single-valued dims); 9.1.0 has no getTopDims [exec: compile error, the only missing symbol is Facets#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 9-test JUnit (6 eviction cases over FastTaxonomyFacetCounts and TaxonomyFacetFloatAssociations with a value source and with associations, 3 controls) fails 4 of 9 on all 31 releases and main with -ea, and also with -da on 9.2.0, 9.3.0, 9.12.3, 10.5.2 and main; the controls pass everywhere; the one-line fix makes 9/9 pass on 9.12.3, 10.5.2 and main. The independent reproduction's JUnit (FastTaxonomyFacetCounts and TaxonomyFacetIntAssociations) fails 4 of 7 on main (-ea, -da), 10.5.2 and 9.12.3; the intended-behaviour check's no-tie variant fails on main (-ea, -da) and 10.5.2; the finder's JUnit fails on 9.2.0 (requireDimCount case only), 9.4.2, 9.6.0, 9.8.0, 9.9.2, 9.10.0, 9.12.3, 10.5.2 and main. [read] branch_10x and branch_10_6 have a TaxonomyFacets.java identical to 10.5.2 (not executed); the newest upstream main commit touching the file is a7704f5242d (2025-06-18). 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: code reading; also reached by exhaustive check
- Upstream and prior reports
- none found (2026-10-26, read-only, the prior-report record, verdict new: 123 logged queries: a scripted prior-report search in two runs (32 GitHub issue/PR, 6 commit, 4 downstream over Elasticsearch, OpenSearch and Solr, 40 mailing-list over dev@, java-user@, issues@ and commits@, 8 JIRA), 9 web searches, 13 manual gh searches, JIRA comments of LUCENE-10325 and LUCENE-10488, the review threads of #747, #773, #777, #779, #806, #889, #915, #12966, #13358, #13568, #13287 and #13198 grepped for dimOrd, bottomDim, updateTop and DimValue, all 410 open apache/lucene PRs listed with the changed files of 162 of them read, and git log -S over the full history; 8 of the manual gh searches hit the shared GitHub rate limit and were re-run successfully through a scripted prior-report search); 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) rewrites the same replacement block for zero-valued dims and still sets only dim and value: it neither reports nor fixes this bug, but a fix PR would conflict with it. Related, not reports: #13191 / #13198 (zero-aggregation test failure with single-valued dims only), #11524 / #779 / #806 / #889 (the introducing optimisation; nobody raised the stale ordinal in review), #11361 / #747 / #777 (the feature and the SSDV versions), #915 and #12966 (refactors), #14008 (a different taxonomy wrong-results bug, fixed). Not fixed upstream.
- 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, 10.0.0, 9.12.3, 9.3.0 and 9.2.0 release jars (9.1.0 has nogetTopDims). Earlier checks ran on mainea57360912ed, whose facet module is identical, and on all 31 releases from 9.2.0 to 10.5.2. Fix A was validated in the scratch worktree: the new test, the closest suites and the wholelucene/facetsuite with assertions on and off, and:lucene:facet:check -x test; fix B with the whole suite, assertions on.
- Finding record
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:46-55 (DimValue holds dim, dimOrd, value), :610-614 (queue ordered by value, then dim name), :619-669 (loop over dims, youngest-created first: multi-valued dims take the indexed dim count or -1 at :629-638, single-valued dims cache their children by name in intermediateResults at :639-651; admission at :652; replacement at :656-663 sets dim and value only, :660-661), :671-689 (result phase: children by name if cached, else getTopChildrenForPath(dimConfig, dimValue.dimOrd, topNChildren) at :683; createFacetResult(topChildrenForPath, dim) at :685; assert at :686)lucene/facet/src/java/org/apache/lucene/facet/Facets.java:59-67 (getTopDims javadoc: 'Results should be the same as calling getAllDims and then only using the first topNDims'; requireDimCount advice)lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java:200-228 (SSDV getTopDims rebuilds each result from the dim name; correct)lucene/facet/src/java/org/apache/lucene/facet/taxonomy/directory/TaxonomyIndexArrays.java (children/siblings arrays: newest child first)lucene/demo/src/java/org/apache/lucene/demo/facet/AssociationsFacetsExample.java:59-61 (the demo marks its association dims multi-valued)
- The finding in 30 seconds
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:46-56, 467-516 (506-512), 595-690 (610-614, 620, 629-638, 639-651, 652-662, 668, 678-686)lucene/facet/src/java/org/apache/lucene/facet/Facets.java:60-72lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:180-181, 190-194, 233-241, 386-397lucene/facet/src/java/org/apache/lucene/facet/taxonomy/directory/TaxonomyIndexArrays.java:253-255, 269
- Who is affected
lucene/facet/src/java/org/apache/lucene/facet/FacetResult.java:72-79; FacetsConfig.java:137, 233-241lucene/demo/src/java/org/apache/lucene/demo/facet/AssociationsFacetsExample.java:59-61lucene/facet/src/java/org/apache/lucene/facet/FacetsConfig.java:305-307 (AssociationFacetField cannot requireDimCount); taxonomy/TaxonomyFacets.java:629-637, 652
- Why it looks unintended
lucene/facet/src/java/org/apache/lucene/facet/Facets.java:25, 60-72; FacetResult.java:24-40, 72-79lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java:143, 200-228lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetCounts.java:253, 390, 430, 481, 533, 649-678, 694-722, 1062; TestTaxonomyFacetValueSource.java:219, 702; TestTaxonomyFacetAssociations.java:84-91, 238-380 (getTopDims(10, 10) at 238, 269, 288, 327, 359, 380); grep of lucene/*/src/test for getTopDims and setMultiValued
- Suggested fix
lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java:61-64, 231-243 (insertWithOverflow)
- Prior reports
read-only gh searches of 2026-10-27 (issues and PRs for getTopDims; PR #13287 state; upstream main TaxonomyFacets.java:659-661 and its newest commit a7704f5242d)
- History
a Lucene clone (git tag --contains 87655fd015c: 9.2.0 first)
- TestTaxonomyGetTopDimsMultiValued.java (JUnit test):
repro/LFV-363/src/org/apache/lucene/facet/taxonomy/TestTaxonomyGetTopDimsMultiValued.java(66 lines)
A light summary card, made in parallel with this doc from an executed probe trace on main a9779ef3c193 (assertions on and off, and with the fix as a control); the PNG is the summary card.
assets/LFV-363/bug.png(478 KB)
