getAllDims sorts the dims with a comparator that answers "tie" whenever NaN is involved and then orders by name. That breaks transitivity, so the sort can return finite dims out of order.
- Each document carries labels such as
a/x; the first component (a) is the dimension, or dim. TaxonomyFacetFloatAssociationsadds up (SUM) or takes the maximum (MAX) of a float per label, fromFloatAssociationFacetFieldweights or aDoubleValuesSource; a dim's value is the aggregate of its labels.getAllDims(n)returns one result per dim, sorted by value, highest first.getTopDims(k, n)returns the top k dims and is documented to equal the first k entries ofgetAllDims(n).
getAllDims visits the dims in, the two comparisons the sort makes, the circular order that results, getTopDims disagreeing, the 32-dim exception and the fix. Select the still to open it at full size. Generated with AI, checked against the code.- Index three documents, one
FloatAssociationFacetFieldeach: dim a with weight 1.0, dim b with NaN, dim c with 2.0. Aggregate them withTaxonomyFacetFloatAssociationsand SUM. - You expect
getAllDims(10)to list the dims by value, highest first, so c=2.0 before a=1.0, andgetTopDims(1, 10)to return its first entry. - Instead
getAllDims(10)returns[a=1.0, b=NaN, c=2.0], andgetTopDims(1, 10)returns[b=NaN]. - Why: the sort tests
x > yboth ways and falls back to the name. With NaN both tests are false, so b ties with everything: a before b, b before c by name, yet c before a by value. No order satisfies that. - With 32 or more dims the same sort can throw an
IllegalArgumentException. NaN needs no NaN input: aclicks / viewsweight of 0/0, or weights whose SUM overflows to +Inf and -Inf in one dim, do the same.
Applications that call getAllDims on float taxonomy facets (float associations or a DoubleValuesSource, SUM or MAX) when one dim aggregates to NaN. Three dims suffice for the wrong order; the exception needs 32 or more, which the documented "sparse faceting" use invites. Hits, counts, drill-down and int facets are unaffected. Rare, but deterministic once such a value is indexed.
Mechanism, terms and numbers713 words
TaxonomyFacets#getAllDims sorts the per-dim results with a.value.doubleValue() > b.value.doubleValue(), then the reverse, and otherwise the dim name (TaxonomyFacets.java:580-590). Every > with NaN is false, so a dim whose value is NaN ties with every value, is placed by name only, and the comparator is no longer transitive. With dims a=1, b=NaN, c=2 (float associations, SUM), getAllDims(10) returns [a=1.0, b=NaN, c=2.0] while getTopDims(1, 10) returns [b=NaN]: getTopDims ranks with the NaN-safe valueComparator (Float.compare, FloatTaxonomyFacets.java:48) since #12966, and its javadoc says it should equal the first entries of getAllDims. With 32 or more dims, List.sort can detect the broken contract and getAllDims throws IllegalArgumentException: Comparison method violates its general contract!. NaN also comes from ordinary inputs: a clicks / views weight with 0/0, or finite weights whose float SUM overflows to +Inf and -Inf inside one dim. Hits, counts, drill-down and int facets are not affected. Released since 4.7.0; the default association path reaches it since 9.11.0. The fix sorts with valueComparator, then the dim name.
Mechanism (Lucene main 645d4fe14a20):
- Collect.
getAllDimswalks the taxonomy root's children and siblings and adds oneFacetResultper dim (TaxonomyFacets.java:564-578). The taxonomy lists the youngest child first (TaxonomyIndexArrays.java:253-255,:268-269), so dims added as a, b, c are visited as[c=2.0, b=NaN, a=1.0]. A dim's value is a fold over its labels that starts at0f(FloatTaxonomyFacets.java:129-150), so one NaN label makes the dim NaN. - Sort.
TaxonomyFacets.java:580-590:
// Sort by highest value, tie break by dim:
results.sort(
(a, b) -> {
if (a.value.doubleValue() > b.value.doubleValue()) {
return -1;
} else if (b.value.doubleValue() > a.value.doubleValue()) {
return 1;
} else {
return a.dim.compareTo(b.dim);
}
});- Wrong order. For a=1, b=NaN, c=2 the comparator says a<b (name), b<c (name) and c<a (value): a cycle, so it is not a valid
Comparator. On the visiting order[c, b, a], the JDK'sArrayList.sortasks onlycompare(b, c) = -1andcompare(a, b) = -1, takes the run as strictly descending, reverses it and returns[a=1.0, b=NaN, c=2.0]; it never compares a with c. (Shown with a verbatim copy of the comparator that logs its calls; the realgetAllDimsreturns the same list.) - Exception. From 32 elements the JDK's TimSort merges sorted runs and, on some arrangements, meets the contradiction and throws
IllegalArgumentException("Comparison method violates its general contract!"), whichList.sortdocuments for a comparator that breaks its contract. Below 32 elements it never merges and never throws. getTopDimsuses another order. Its queue orders dims byvalueComparator, then the name (TaxonomyFacets.java:610-614), and admits dims with it (:652-664). For float facets that isFloat.compare(FloatTaxonomyFacets.java:48), a total order that ranks NaN above +Infinity, so it returns[b=NaN, c=2.0, a=1.0]. Int facets compare ints and cannot be NaN.- Where NaN comes from. Nothing checks finiteness:
FloatAssociationFacetFieldstores any float (FloatAssociationFacetField.java:33-40),TaxonomyFacetFloatAssociations.java:151casts anyDoubleValuesSourcedouble to float, SUM is+and MAX isMath.max(AssociationAggregationFunction.java:40-65).
Executed triggers (main 645d4fe14a20, assertions on and off; the NaN-free controls pass). Every row also failed on 10.6.0, assertions on and off; the first three also on 9.11.0 and the last three also on 10.5.2 (assertions on):
| trigger | getAllDims(10) on main |
|---|---|
| weights a=1.0, b=NaN, c=2.0 (the minimal test) | [a=1.0, b=NaN, c=2.0] |
| c gets two documents of 1.0 (2 hits, value 2.0), a one of 1.0 | [a=1.0, b=NaN, c=2.0]: c loses on hits and on value |
expression weight clicks / views with MAX; b's only document has 0 clicks and 0 views | [a=1.0, b=NaN, c=2.0] |
| finite weights only: in dim b, 3e38 twice (+Inf) on one label and -3e38 twice (-Inf) on another | [a=1.0, b=NaN, c=2.0] |
| 32 dims with hits, one of them NaN | IllegalArgumentException: Comparison method violates its general contract! |
| the 6 taxonomy insertion orders of a, b, c | value order broken in 2 of 6; getTopDims prefix broken in 4 of 6 |
How often a NaN dim breaks the order depends on the arrangement: a simulation of the verbatim comparator (one NaN, distinct values, random order, 100,000 lists per size) misorders finite dims in 5.45% of 3-dim, 21.32% of 10-dim and 39.37% of 32-dim lists, and throws in 0.28% of 32-dim lists.
Exposure and likelihood193 words
- Who. Users of
TaxonomyFacetFloatAssociationsoverFloatAssociationFacetFieldweights or aDoubleValuesSource, with SUM or MAX, who callgetAllDims, or compare it withgetTopDims. These are opt-in facet types, and the facet classes are@lucene.experimental. - Settings. Otherwise defaults: default
IndexWriterConfig, codec, taxonomy andFacetsConfigon the association path since 9.11.0; the value-source path with the default dim config was run on 9.11.0, 10.6.0 and main, and withmultiValued+requireDimCountdims it is affected since 4.7.0. - Scale. Three dims for the wrong order; 32 or more dims with hits for the exception.
getAllDimsis documented for "sparse" faceting "where many different dimensions were indexed" (Facets.java:53-55). - Effect. The "top" dim shown can be a lower-valued one; code that truncates or pages
getAllDimsgets the wrong dims;getTopDimsandgetAllDimsdisagree; with many dims the facet call fails.getTopDimsdoes not use this comparator (it ranks dims withvalueComparator), but the child lists it returns have a separate NaN issue (LFV-825); nothing is written to the index. - Likelihood. Rare: ordinary weights (scores, prices, ratings) do not aggregate to NaN, but computed weights can (0/0), and so can SUM overflow.
One JUnit test in lucene/facet, public API: three documents with float weights a=1.0, b=NaN, c=2.0, SUM, then getAllDims(10). It asserts only that c comes before a, nothing about where NaN goes. Deterministic; it fails with assertions on and off.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.facet.taxonomy;
import java.util.List;
import org.apache.lucene.document.Document;
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 TestGetAllDimsNaN extends LuceneTestCase {
public void testGetAllDimsOrdersNonNaNDims() throws Exception {
FacetsConfig config = new FacetsConfig();
try (Directory dir = new ByteBuffersDirectory();
Directory taxoDir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig());
DirectoryTaxonomyWriter tw = new DirectoryTaxonomyWriter(taxoDir)) {
String[] dims = {"a", "b", "c"};
float[] values = {1f, Float.NaN, 2f};
for (int i = 0; i < dims.length; i++) {
Document doc = new Document();
doc.add(new FloatAssociationFacetField(values[i], dims[i], "x"));
w.addDocument(config.build(tw, doc));
}
}
try (DirectoryReader r = DirectoryReader.open(dir);
DirectoryTaxonomyReader tr = new DirectoryTaxonomyReader(taxoDir)) {
FacetsCollector fc =
new IndexSearcher(r).search(new MatchAllDocsQuery(), new FacetsCollectorManager());
Facets facets =
new TaxonomyFacetFloatAssociations(tr, config, fc, AssociationAggregationFunction.SUM);
List<String> all =
facets.getAllDims(10).stream().map(fr -> fr.dim + "=" + fr.value).toList();
// expected: c=2.0 ranked before a=1.0 (getAllDims sorts by value); actual: a=1.0 first
assertTrue(
"expected c=2.0 before a=1.0, got " + all,
all.indexOf("c=2.0") < all.indexOf("a=1.0"));
}
}
}
}
cp $REPRO/TestGetAllDimsNaN.java $LUCENE/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/
cd $LUCENE && ./gradlew :lucene:facet:test --tests org.apache.lucene.facet.taxonomy.TestGetAllDimsNaN -Ptests.seed=DEADBEEF -Ptests.asserts=true
# and the same with -Ptests.asserts=false
- Expected
getAllDims(10)lists c=2.0 before a=1.0: sorted by value, highest first.- Observed
[a=1.0, b=NaN, c=2.0], with assertions on and off: a=1.0 is listed above c=2.0.
More files, environment, full output264 words
One test class, one method, public API only: ByteBuffersDirectory, default IndexWriterConfig and FacetsConfig, single thread, nothing randomized by the test body (the seed drives only the test framework's own randomized defaults, such as the codec, locale and time zone; the test failed under three seeds, with assertions on and off). Copy it into lucene/facet/src/test/org/apache/lucene/facet/taxonomy/ of a Lucene checkout and run the command.
Before using it as the regression test of a PR, run ./gradlew :lucene:facet:tidy: it joins the two assertTrue arguments onto one line, the only change, after which the format check passes.
Left out on purpose: the getTopDims comparison, the 32-dim exception and the other NaN sources; they follow from the same comparator and were executed separately (see the mechanism above).
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also the 10.6.0 and 10.5.2 release jars |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; the code path is pure Java) |
cp $REPRO/TestGetAllDimsNaN.java $LUCENE/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/
cd $LUCENE && ./gradlew :lucene:facet:test --tests org.apache.lucene.facet.taxonomy.TestGetAllDimsNaN -Ptests.seed=DEADBEEF -Ptests.asserts=true
# and the same with -Ptests.asserts=false
# main 645d4fe14a20, gradle, -Ptests.asserts=true (the same with false)
TestGetAllDimsNaN > testGetAllDimsOrdersNonNaNDims FAILED
java.lang.AssertionError: expected c=2.0 before a=1.0, got [a=1.0, b=NaN, c=2.0]
at __randomizedtesting.SeedInfo.seed([DEADBEEF:3C0FEC18EB78D5B1]:0)
at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
at junit@4.13.2/org.junit.Assert.assertTrue(Assert.java:42)
at org.apache.lucene.facet.taxonomy.TestGetAllDimsNaN.testGetAllDimsOrdersNonNaNDims(TestGetAllDimsNaN.java:61)
:lucene:facet:test (FAILURE): 1 test, 1 failure
# with the fix (option A), assertions on and off
:lucene:facet:test (SUCCESS): 1 test
# the same three dims, traced (main jars, -ea and -da)
visiting order: [c=2.0, b=NaN, a=1.0]
compare(b=NaN, c=2.0) = -1
compare(a=1.0, b=NaN) = -1
getAllDims(10): [a=1.0, b=NaN, c=2.0]
getTopDims(1, 10): [b=NaN]
getTopDims(3, 10): [b=NaN, c=2.0, a=1.0]- The code says "Sort by highest value, tie break by dim", yet lists 1.0 above 2.0.
- The javadoc says
getTopDimsshould equal the first entries ofgetAllDims; here it returns[b=NaN]whilegetAllDimsstarts with a=1.0. - #12966 moved
getTopDimsto a NaN-safe order and leftgetAllDimsbehind; Lucene accepts NaN weights.
The full argument202 words
- The code's intent.
TaxonomyFacets.java:580: "Sort by highest value, tie break by dim". No rule for where NaN goes explains 1.0 above 2.0. - The javadoc.
Facets.java:52-57:getAllDimsreturns dims "sorted by the number of hits that dimension matched";:59-62:getTopDimsis "sorted by the number of hits/aggregated values ... Results should be the same as calling getAllDims and then only using the first topNDims". The basegetTopDimsis literallygetAllDims(topNChildren).subList(0, ...)(:69-72). - The tests.
TestTaxonomyFacetAssociations.java:327-329,:359-361and:380-382assert thatgetTopDims(10, 10)equalsgetAllDims(10)for float associations, with finite values only. No facet test uses NaN, and the test helperFacetTestCase#sortFacetResults(:209-221) copies the same comparator. - The JDK contract. The exception is
List.sortreporting a brokenComparator, not an input check. - History. #12966 (2024; 9.11.0 through #13358) added
valueComparatorand movedgetTopDimsto it, but leftgetAllDimson the 2013 comparator; none of its 55 comments and reviews mentions NaN. The same change is the 9.11.0 bug fix "Aggregation facets no longer assume that aggregation values are positive" (lucene/CHANGES.txt:2922). - NaN is accepted. The facet module rejects NaN only for range bounds (
range/DoubleRange.java:61-69,rangeonrange/DoubleRange.java:43-51,facetset/DimRange.java:62-94); weights are not checked.
The devil's-advocate check188 words
The case for "intended": NaN is garbage in, as with NaN scores in core search (LUCENE-2271); the facet APIs are @lucene.experimental; the getAllDims javadoc speaks of "number of hits" and says nothing about NaN; AssociationAggregationFunction.java:25-28 assumes "valid values are always positive"; and in the minimal test every dim has one hit, so a literal "by hits, then by name" reading would accept [a, b, c].
Why it fails: (1) when c has 2 hits and value 2.0 and a has 1 hit and 1.0, the result is still [a=1.0, b=NaN, c=2.0], wrong under both readings (executed on main, 10.6.0 and 9.11.0); (2) getTopDims and getAllDims disagree wherever NaN goes, against the javadoc's stated expectation ("Results should be the same") and the upstream equality tests (finite inputs); (3) the exception is a Comparator contract violation, which experimental status does not excuse; (4) 0/0 and SUM overflow produce NaN from ordinary inputs, so rejecting explicit NaN does not prevent it; (5) #12966 already chose a NaN-safe order for getTopDims and dropped the positive-values assumption as a bug fix. The cross-model refute review rated the intended argument "weak".
Recommended Sort getAllDims with valueComparator, as getTopDims does
Sort getAllDims with valueComparator.compare(b.value, a.value), then the dim name, the order getTopDims already uses (+4/-6 lines in TaxonomyFacets.java). A NaN dim is then listed first, as in getTopDims. The repro passes, and so do all 307 lucene/facet tests and the format check.
// Sort by highest value, tie break by dim:
results.sort(
(a, b) -> {
int cmp = valueComparator.compare(b.value, a.value);
if (cmp != 0) {
return cmp;
}
return a.dim.compareTo(b.dim);
});All options and trade-offs3 options · 524 words
Option A was built and run in a scratch worktree of 645d4fe14a20; B was run on 4.7.0 only; C was not built. Rejecting NaN weights at index time is not a fix on its own: NaN also comes from SUM overflow and from a DoubleValuesSource at query time.
Replace the two > tests in the getAllDims comparator (TaxonomyFacets.java:580-590) by valueComparator, then the dim name. Full diff:
--- a/lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java
+++ b/lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java
@@ -580,13 +580,11 @@
// Sort by highest value, tie break by dim:
results.sort(
(a, b) -> {
- if (a.value.doubleValue() > b.value.doubleValue()) {
- return -1;
- } else if (b.value.doubleValue() > a.value.doubleValue()) {
- return 1;
- } else {
- return a.dim.compareTo(b.dim);
+ int cmp = valueComparator.compare(b.value, a.value);
+ if (cmp != 0) {
+ return cmp;
}
+ return a.dim.compareTo(b.dim);
});
return results;
}// Sort by highest value, tie break by dim:
results.sort(
(a, b) -> {
int cmp = valueComparator.compare(b.value, a.value);
if (cmp != 0) {
return cmp;
}
return a.dim.compareTo(b.dim);
});Trade-offs
- One order for both methods; a NaN dim is listed first, where
getTopDimsalready puts it. - Int facets:
Comparator.comparingIntgives today's order. Float facets:Float.compareand>differ only on NaN and on a-0.0/+0.0pair; the built-in SUM and MAX folds start at+0.0and cannot produce-0.0(solver queries; reasoned, not executed). - A custom
AssociationAggregationFunction(public abstract class) that returns-0.0fwould now be ordered below+0.0instead of by name (read, not run). - Scope: this fixes the dim order of
getAllDimsonly (and with it the disagreement withgetTopDimsand the exception). The child labels inside each dim come fromgetTopChildren, whose queue can still pick the wrong children when a label is NaN: that is LFV-825, with its own fix. - Optional: change the test helper
FacetTestCase#sortFacetResultsthe same way (identical for finite values).
What the checks validated
Scratch worktree of 645d4fe14a20, gradle --no-daemon, OpenJDK 25.0.3, seed DEADBEEF:
- stock + test: fails with assertions on and off;
git applyof the fix is clean; - fix + test: passes with assertions on and off;
- fix + test,
--tests 'org.apache.lucene.facet.taxonomy.*': 139 tests, 6 skipped, 0 failures; - fix + test, all of
:lucene:facet:test: 307 tests, 8 skipped, 0 failures (40 classes, including TestTaxonomyFacetAssociations, TestTaxonomyFacetValueSource, TestTaxonomyFacetCounts and TestTaxonomyFacetCounts2); - fix + tidied test:
:lucene:facet:checkGoogleJavaFormatpasses; - earlier, shadowed onto the main jars: every verifier's and the tie-break's tests pass. Not run: the full Lucene suite, nightly tests, benchmarks.
Double.compare(b.value.doubleValue(), a.value.doubleValue()), then the dim name: the same order as A, for branches without valueComparator (before 9.11.0).
Trade-offs
On main, A keeps one source of truth with getTopDims.
What the checks validated
4.7.0 release jars with only the comparator replaced: the history program passes.
Make the float valueComparator (FloatTaxonomyFacets.java:48) NaN-last and use it in getAllDims as in A.
Trade-offs
Arguably friendlier output, but it changes what getTopDims returns today and its admission test (TaxonomyFacets.java:652-664).
A product that lets untrusted documents set float association weights can see one facet call return dims out of order or fail with an exception; hits, counts and the index are unaffected.
A bounded, exhaustive differential search compared Lucene's facet ordering with an exact reference ordering on small inputs that include NaN and infinities: every failure had a NaN dim, none without one. Solver queries showed the comparator is not transitive once NaN is allowed, and a simulation of the comparator found the 32-dim exception.
The method in full161 words
The hunt targeted NaN and other special values in the facet top-k paths. A sweep over the stock jar (1-4 labels and 4 dims, inputs {1, 2, 3, NaN, +Inf, -Inf, -0.0}, association and value-source facets, SUM and MAX, dense and sparse, 1 or 2 segments, assertions on and off) found 928 getAllDims-order failures and 11,022 getTopDims-vs-getAllDims failures, all with a NaN dim and none without one. SMT queries (cvc5 and z3) showed that the getAllDims order is not transitive with NaN and transitive without it, and that it differs from the getTopDims order only on NaN or a signed-zero pair, which the SUM and MAX folds cannot produce. A pure-Java simulation of the verbatim comparator found that 12,843 of 200,000 random lists throw, the smallest with 32 dims, and a seeded search then reproduced the exception on the stock jar. An independent scan of the compiled classes for comparators without a NaN branch flagged the same site.
No prior report found: three searches (2026-10-28 and twice on 2026-10-30, over 600 logged queries) of Lucene's GitHub issues and PRs, the JIRA archive, the mailing lists, downstream projects and the web.
Searches3 searches
The documented public searches found no issue, PR, review comment, JIRA entry, list thread, downstream issue or web page that reports the NaN-unsafe getAllDims comparator or its symptoms. Nearest items, none of them this bug: #6403 (LUCENE-5339, where the comparator came from; no NaN in its comments), #12966 (added valueComparator for getTopDims only), #747 / #11361 (added getTopDims and the "same as getAllDims" javadoc), #806 / #11524 (a getTopDims performance change), #13287 and #12862 (the only open PRs that edit TaxonomyFacets.java; neither touches the getAllDims sort), #6596 (LUCENE-5533, int association SUM overflow, nothing on NaN or ordering), #3347 (LUCENE-2271, NaN scores in core search).
Limits: keyword search; private reports to security@lucene.apache.org are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub apache/lucene issues, PRs and commits; patches of the 20 open facet PRs | 2026-10-30 | two search-tool runs (56 queries), 28 manual queries, commit and file-history checks, 10.6.0 tag and upstream branches | no report; #6403, #12966, #747, #806, #13287, #12862 related only |
| dev@, java-user@, issues@, commits@; JIRA archive; Elasticsearch, OpenSearch, Solr, Lucene.NET; web | 2026-10-30 | list queries and the last two days of mail, 10 JQL queries, downstream queries, 10 web queries | no report |
| the same sources | 2026-10-30 and 2026-10-28 | 145 and 282 logged queries; threads of #6403, #12966, #747, #806, #13287 and #13795 read in full | no report |
Introduced with the Facets API on the LUCENE-5339 branch in 2013 and first released in 4.7.0 (2014); the default float-association path reaches it since 9.11.0 (#12966). Every release from 4.7.0 to 10.6.0 and main are affected; not fixed upstream.
Versions tested7 rows
5458dbf2484 (2013-11-14, "LUCENE-5339: current patch", Michael McCandless) wrote the first dim sort with > in both directions, in the value-source class; 51265435699d (2013-11-18) moved it, on doubleValue(), into the new base class TaxonomyFacets#getAllDims, today's code site. Both reached a release through the branch_4x commit 7477fde1cb0 (2014-01-04), in 4.7.0. Later edits never handled NaN: on the branch, f8ae207e806 (2013-11-24) replaced the dim-name tie-break that both commits already had with return 0 and 034595f5e9f (2013-12-02) restored it, before any release; after that came a named constant, reformatting, List.sort in #915 and an inlined lambda in #13795. #12966 (9ba4af7b883, 2024; 9.x backport #13358, 9.11.0) stopped dropping labels whose value is not > 0, so a NaN dim reaches getAllDims on the default association path, and moved getTopDims to the NaN-safe valueComparator but not getAllDims. getTopDims exists since 9.2.0, so the disagreement starts there. All runs used JDK 25; the exception needs TimSort, the JDK's default object sort since Java 7. Upstream main, branch_11x, branch_10_6 and the 10.6.0 tag still have the comparator (2026-10-30).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the minimal test fails through gradle, assertions on and off |
| 10.6.0, 10.5.2 (release jars) | affected | the minimal test fails, assertions on and off |
| 9.11.0 | affected | the minimal test fails; the value-source path fails too |
| 4.7.0, 4.7.2, 4.10.4, 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.1.0, 9.2.0, 9.10.0, 9.12.3, 10.0.0 | affected | the history program fails on the value-source path (multiValued + requireDimCount dims) |
| 9.10.0 (association path) | NaN label dropped | the minimal test passes: before #12966 labels whose value is not > 0 were dropped |
| 4.6.1 | no Facets API | the repro does not compile: the API does not exist |
| with the fix (main; 4.7.0 with option B) | not affected | the minimal test, all lucene/facet tests and the format check pass; 4.7.0 passes |
Every verifier and the adjudication rated it real; a reproducer and a tie-break re-ran it on main, 10.6.0 and 10.5.2. The reviewers from another model family could not execute code: their one "not real" was procedural, and they agreed on the root cause.
Checks and verdicts342 words
The reviewers from another model family (OpenAI GPT-5.6) ran reading only (an operator re-ran the repro for the refute review afterwards; it fails on main); the finder, verifiers and judge ran on main a9779ef3c193, whose lucene/facet equals 645d4fe14a20's; the executed evidence comes from the finder, independent verifiers, the adjudication, the reproducer, the tie-break and this doc's own gradle runs. Not done: a cross-model review that executes code.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Sweep, solver queries and simulation; its JUnit fails on main a9779ef3c193, 10.5.2, 10.0.0 and 9.12.3 and passes with the fix. |
| cross-model review, refute lens | not real, low, procedural | Could not run any command; agreed on the root cause and that the inputs are in contract; intended argument "weak". An operator re-ran the repro for this record afterwards: it fails on main 645d4fe14a20. |
| Independent reproducer | real | Own 5-test JUnit: 4 of 5 fail on main a9779ef3c193, 10.5.2, 9.12.3 and 9.11.0; the control passes; 5 of 5 with the fix. |
| Cross-model review, intended lens | real, low | Intended argument weak; the operator's finite-input JUnit fails 2 of 2 on main a9779ef3c193 and passes with the fix. |
| Devil's advocate | real, medium-high | Built the "NaN is garbage in" case and rejected it; own JUnit fails 4 of 5 on main a9779ef3c193, 9.11.0 and 10.5.2. |
| adjudication | confirmed, high | Re-ran the independent reproduction's test on main a9779ef3c193 with a new seed: 4 of 5 fail with assertions on and off; not a duplicate. |
| current-main reproducer | real | The minimal test fails on main and 10.5.2, assertions on and off; passes with the fix. |
| Second adjudication | real, high | Four separately written test classes fail on main 645d4fe14a20, assertions on and off. On releases the coverage differs by class: the minimal test and a new 4-test class also fail on 10.6.0 and 9.11.0, an earlier 6-test class on 10.6.0 and 10.5.2, and the finder's class ran on main only. Their controls pass; all pass with the fix. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 594 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,542 words
- Registry title
- Taxonomy Facets#getAllDims sorts float-valued dims with a NaN-unsafe < / > comparator, so one dim whose value is NaN puts the other dims out of value order, breaks the documented rule that getTopDims equals the first entries of getAllDims, and with 32 or more dims can make getAllDims throw 'Comparison method violates its general contract!' (released since 4.7.0)
- Area
- facet / taxonomy facets (TaxonomyFacets#getAllDims ordering of float-valued dims: float association and value-source facets)
- Priority
- Low, rank 587: A NaN float facet value is a rare boundary input that silently misorders dims, makes getTopDims disagree with getAllDims and makes getAllDims throw at 32 or more dims, while hits and counts stay correct; it has the same NaN-boundary profile as LFV-427 but is milder than that row's empty groups.
- Impact class
- silent wrong results
- Impact note
- Applications that call Facets#getAllDims on float-valued taxonomy facets (TaxonomyFacetFloatAssociations, fed by FloatAssociationFacetField weights or a DoubleValuesSource, SUM or MAX) silently get the finite dims out of value order (a=1 listed before c=2) and getTopDims(k, n) results that differ from getAllDims(n).subList(0, k), and with 32 or more dims with hits getAllDims can throw IllegalArgumentException, as soon as one dim's aggregate is NaN, which finite inputs can also produce (a float SUM that overflows to +Inf and -Inf inside one dim, or finite doubles beyond the float range on the DoubleValuesSource path); default IndexWriterConfig, codec, taxonomy and FacetsConfig on the association path since 9.11.0, value-source dims set multiValued + requireDimCount since 4.7.0, -ea and -da, no scale needed; getTopDims never throws, hits and drill-down are correct, and nothing is written to the index.
- Summary
- You aggregate float values per taxonomy dim (FloatAssociationFacetField weights or a DoubleValuesSource) and call getAllDims, documented to sort dims by value, and getTopDims, documented to return the same as the first entries of getAllDims. If one dim's value is NaN (dims a=1, b=NaN, c=2), getAllDims can return [a=1.0, b=NaN, c=2.0], with a ahead of c, while getTopDims(1, 10) returns [b=NaN]; with 32 or more dims, getAllDims can instead throw IllegalArgumentException 'Comparison method violates its general contract!' from List.sort. The getAllDims comparator compares doubleValue() with > in both directions and falls back to the dim name, so NaN ties with every value and the order is not transitive, while getTopDims has used the NaN-safe Float.compare (valueComparator) since #12966 (9.11.0).
- Introduced
- 51265435699d (LUCENE-5339 development branch, svn r1543202, author Michael McCandless, 'LUCENE-5339: add base class for taxo facet impls; catch wrong index field name for a given dim'): it added TaxonomyFacets.getAllDims with the value-descending < / > comparator on doubleValue(); the per-class getAllDims comparators it replaced compared value.intValue(), a total order. It reached releases through the trunk merge 9d0b60388da and the branch_4x backport 7477fde1cb0 ('LUCENE-5371, LUCENE-5339: ... simplify facet APIs'); JIRA LUCENE-5339 (GitHub mirror #6403), no PR (svn era). Later edits kept the logic: 034595f5e9f (2013-12-02, final tie-break by dim name), cfe57ec320b (2013-12-31, BY_VALUE_THEN_DIM), 8db1e41fc07 (LUCENE-10585, #915, 2022, List.sort) and 644feeb02a9 (#13795, 2024, inlined as a lambda). The default association path reaches the comparator since 9ba4af7b883 (#12966, 2024-04-05; 9.x backport 42884fab46d / #13358, first released in 9.11.0), which stopped dropping labels whose value is not > 0 and moved getTopDims and the queues, but not getAllDims, to valueComparator. [read: git log -S, git log -L and git blame in the history clone, git merge-base --is-ancestor on the 4.6.1 and 4.7.0 tags; exec: 4.6.1 has no Facets API (compile error), 4.7.0 fails, and 4.7.0 with only this comparator replaced passes] (2013-11-18 (author date of 51265435699d); 2014-01-04 (merged to trunk and branch_4x); the default association path since 2024-04-05 (#12966))
- First affected version
- 4.7.0 [exec] (released 2014-02-25, the first release with the Facets/getAllDims API; there on the value-source path with multiValued + requireDimCount dims, whose value is the dim ordinal's own aggregate); the default single-valued FloatAssociationFacetField path from 9.11.0 [exec] (9.10.0 drops NaN labels on that path [exec]); 4.6.1 has no such API [exec: compile error]
- Affected versions
- Every release from 4.7.0 to 10.6.0 and main a9779ef3c193; not fixed upstream. [exec] The follow-up's portable main() repro (a value-source path and an association path, with controls that replace NaN by 1.5 and pass everywhere) fails on 18 sampled releases (4.7.0, 4.10.4, 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.1.0, 9.2.0, 9.10.0, 9.11.0, 9.12.3, 10.0.0, 10.5.2) and main: the value-source path misorders and throws on all of them, the association path from 9.11.0 on, and the getTopDims prefix check fails from 9.2.0, the first release with getTopDims. The finder's JUnit fails 3 of 3 on main (-ea and -da), 10.5.2, 10.0.0, 9.12.3 and 9.11.0; the independent reproduction's JUnit fails 4 of 5 on main (-ea and -da), 10.5.2, 9.12.3 and 9.11.0, and the independent reproduction's value-source JUnit fails on main and 9.10.0; the intended-behaviour check's JUnit fails 4 of 5 on main (-ea and -da), 10.5.2 and 9.11.0. Releases between the sampled ones were not run; git log -L shows the comparator logic unchanged across them. [read] upstream main 20ced71c1e6e and branch_10_6 4ae946e5a9b9 have the same comparator, so the released 10.6.0 has it; its release jars fail the tie-break tests with -ea and -da [exec, 2026-10-30]. All runs on JDK 25 (Linux aarch64); the exception needs a sort that checks the Comparator contract (TimSort, the default since Java 7), and symptoms on the Java 6 runtimes of the 4.x era were not executed.
- 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 adjudication that ruled it real, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: exhaustive check; also reached by Z3/cvc5
- Upstream and prior reports
- none found (2026-10-28, the prior-report record, verdict new: 282 logged queries: a scripted prior-report search in four runs over GitHub issues and PRs, commits, Elasticsearch, OpenSearch and Solr, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive; 25 manual gh searches; 7 web searches; the threads of #6403 (LUCENE-5339, 90 JIRA comments), #12966 (55 review items), #747, #806, #13287, #13795 and others read in full; all 411 open apache/lucene PRs with their changed files; git log -S over the full history; plus the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's searches). Re-checked 2026-10-28: no PR created since 2026-10-26 (#16907-#16926) and no open PR updated since then changes lucene/facet; the newest commit touching TaxonomyFacets.java is a7704f5242d (#14761, author date 2025-06-18). Nearest items, none of them this bug: #6403 (where the comparator came from; nobody mentions NaN), #12966 (added valueComparator, used it for getTopDims only; no NaN discussion), #747 / #11361 and #806 / #11524 (getTopDims and its float optimisation, with the 'same as getAllDims' javadoc), #13287 (open, stale; changes only the getTopDims admission test), #3347 (LUCENE-2271, NaN scores in the core hit queue, a different component). Not fixed upstream: main 20ced71c1e6e and branch_10_6 4ae946e5a9b9 have the same comparator.
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test through gradle in a scratch worktree, assertions on and off, stock and with the fix; with the fix also the 139 taxonomy tests, all 307lucene/facettests and the format check. Release jars: the minimal test fails on 10.6.0, 10.5.2 and 9.11.0 and passes on 9.10.0 (the association path drops the NaN label there); the history runs cover 4.6.1 (no API), 4.7.0 and 18 sampled releases. OpenJDK 25.0.3, Linux aarch64.
- Finding record
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:580-590 (at a9779ef3c193) (getAllDims: 'Sort by highest value, tie break by dim'; results.sort with a.value.doubleValue() > b.value.doubleValue(), then the reverse, else a.dim.compareTo(b.dim)), :610-614 (getTopDims queue ordered by valueComparator, then dim name)lucene/facet/src/java/org/apache/lucene/facet/taxonomy/FloatTaxonomyFacets.java:48 (at a9779ef3c193) (valueComparator = Float.compare on floatValue())lucene/facet/src/java/org/apache/lucene/facet/Facets.java:52-56 (at a9779ef3c193) (getAllDims javadoc: 'sorted by the number of hits that dimension matched'; used for sparse faceting with many dims), :58-63 (getTopDims javadoc: 'Results should be the same as calling getAllDims and then only using the first topNDims')lucene/facet/src/test/org/apache/lucene/facet/FacetTestCase.java:209-221 (at ea57360912ed) (sortFacetResults uses the same < / > comparator) and lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetAssociations.java:238-240 (asserts getTopDims(10, 10) equals getAllDims(10)); lucene/facet is identical at ea57360912ed and a9779ef3c193 (git diff --stat empty)
- The finding in 30 seconds
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:564-578, :580-590, :610-614, :652-664 at 645d4fe14a20lucene/facet/src/java/org/apache/lucene/facet/taxonomy/FloatTaxonomyFacets.java:48, :129-150 at 645d4fe14a20lucene/facet/src/java/org/apache/lucene/facet/taxonomy/directory/TaxonomyIndexArrays.java:253-255, :268-269 at 645d4fe14a20lucene/facet/src/java/org/apache/lucene/facet/taxonomy/FloatAssociationFacetField.java:33-40; TaxonomyFacetFloatAssociations.java:151, :191; lucene/facet/src/java/org/apache/lucene/facet/taxonomy/AssociationAggregationFunction.java:40-65 at 645d4fe14a20JDK 25 sources: java/util/List.java sort (@throws IllegalArgumentException), java/util/TimSort.java (MIN_MERGE = 32)
- Who is affected
lucene/facet/src/java/org/apache/lucene/facet/Facets.java:25, :52-57 at 645d4fe14a20
- Why it looks unintended
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:580 at 645d4fe14a20lucene/facet/src/java/org/apache/lucene/facet/Facets.java:52-57, :59-62, :69-72 at 645d4fe14a20lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetAssociations.java:327-329, :359-361, :380-382, :691-702 at 645d4fe14a20lucene/facet/src/test/org/apache/lucene/facet/FacetTestCase.java:209-221 at 645d4fe14a20lucene/CHANGES.txt:2902, :2922 at 645d4fe14a20 (9.11.0 Bug Fixes, GITHUB#12966)lucene/facet/src/java/org/apache/lucene/facet/range/DoubleRange.java:61-69; rangeonrange/DoubleRange.java:43-51; facetset/DimRange.java:62-63, :69-70, :86-87, :93-94 at 645d4fe14a20lucene/facet/src/java/org/apache/lucene/facet/taxonomy/AssociationAggregationFunction.java:25-28 at 645d4fe14a20
- Suggested fix
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/FloatTaxonomyFacets.java:48; lucene/facet/src/java/org/apache/lucene/facet/taxonomy/IntTaxonomyFacets.java:49; lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyFacets.java:99, :652-664 at 645d4fe14a20lucene/facet/src/java/org/apache/lucene/facet/taxonomy/AssociationAggregationFunction.java:23, :31 at 645d4fe14a20
- TestGetAllDimsNaN.java (lucene/facet, one test):
repro/LFV-820/src/org/apache/lucene/facet/taxonomy/TestGetAllDimsNaN.java(67 lines)
Made in parallel with this doc. Every value on screen comes from an executed probe and the minimal test on main 645d4fe14a20; the code panels show verbatim source lines.
assets/LFV-820/bug.png(594 KB)
![Diagram of the getAllDims sort on Lucene main with dims a=1.0, b=NaN, c=2.0: the dims are visited as c, b, a; the comparator answers b before c and a before b by name because every > with NaN is false, so the sort returns [a=1.0, b=NaN, c=2.0] without ever comparing a with c; getTopDims, which uses Float.compare, returns [b=NaN, c=2.0, a=1.0]; with 32 dims and one NaN, getAllDims throws; the fix sorts getAllDims with valueComparator.](../assets/LFV-820/bug.png)