DoubleField writes its doc values with NumericUtils.doubleToSortableLong, which flips every bit except the sign of a negative number. SortedDoubleDocValuesStats decodes each stored long with Double.longBitsToDouble, the inverse of the raw layout instead. For a positive number both layouts are the same bits, so only values with the sign bit set go wrong.
DocValuesStats(modulelucene/misc) computes count, min, max, sum, mean, variance and stdev of a doc-values field over the documents a query matches.SortedDoubleDocValuesStatsis the variant for a sorted-numeric (multi-valued) field of doubles; you run it withDocValuesStatsCollectorManager(or the olderDocValuesStatsCollector).- A double is 64 bits: one sign bit, then exponent and fraction. Read as a signed long, these raw IEEE bits order positive numbers correctly but negative numbers backwards.
- So
NumericUtils.doubleToSortableLongflips all bits but the sign bit of a negative number; then the longs sort like the doubles.DoubleFieldstores its doc values this way and reads them back withNumericUtils.sortableLongToDouble.DoubleDocValuesField(single-valued) stores raw bits instead.
DoubleField value is stored in the sortable bit layout and read back by SortedDoubleDocValuesStats as a different number, so min, sum and mean come out wrong while positive values are unaffected. Select the still to open it at full size. Generated with AI, checked against the code.- Index two documents,
DoubleField("f", -5.0, Field.Store.NO)andDoubleField("f", 3.0, Field.Store.NO), and collectnew SortedDoubleDocValuesStats("f")overMatchAllDocsQuerywithDocValuesStatsCollectorManager. - You expect min -5.0, max 3.0, sum -2.0 and mean -1.0.
- You get min -0.8749999999999999, sum 2.125 and mean 1.0625 (max 3.0 is right), with no error, with assertions on and off.
- Why:
DoubleFieldstores -5.0 as the sortable long0xbfebffffffffffff(every bit but the sign flipped, so the longs sort like the doubles). The stats class reads that long as a plain IEEE double, which is -0.8749999999999999. - Positive values have the same bits in both layouts, so all-positive data, like the upstream test's, looks fine. Every negative value is misread, sometimes larger, sometimes smaller: -0.5 comes back as -7.999999999999999.
Applications that compute SortedDoubleDocValuesStats over core's DoubleField, or over a sorted-numeric field written with the documented NumericUtils recipe, whenever a matching value is negative (signed amounts, deltas, temperatures). Default settings, any index size; with the two documents -5.0 and 3.0, min, sum, mean, variance and stdev all come out wrong, silently. The class is an opt-in, rarely used misc utility.
Mechanism, terms and numbers711 words
DocValuesStats.SortedDoubleDocValuesStats in lucene/misc computes count, min, max, sum, mean, variance and stdev of "a sorted-numeric field storing double values" over the documents a query matches, and names no encoding. Core's DoubleField (since 9.5.0) writes exactly such a field, in the NumericUtils sortable layout, and core's SortedNumericDocValuesField javadoc recommends that layout for doubles. The stats class decodes each stored long as raw IEEE bits (Double.longBitsToDouble). The two layouts agree exactly when the sign bit is clear, so positive values are exact and every negative value is read as a different number: DoubleField -5.0 and 3.0 give min -0.8749999999999999, sum 2.125 and mean 1.0625 instead of -5.0, -2.0 and -1.0, silently, with default settings. The upstream test writes positive values only, so it cannot see it. Every release from 6.4.0 (documented recipe) and 9.5.0 (DoubleField) to 10.6.0 and main is affected. Decoding with NumericUtils.sortableLongToDouble fixes it; it changes results for anyone who wrote negative values as raw bits, so it needs a migration note (or a decoder argument).
- The sortable layout.
NumericUtils.doubleToSortableLong(value)issortableDoubleBits(Double.doubleToLongBits(value))(NumericUtils.java:47-48), andsortableDoubleBits(bits)isbits ^ (bits >> 63) & 0x7fffffffffffffffL(:84-85): for a negative number it flips every bit except the sign, for a non-negative number it changes nothing. The inverse issortableLongToDouble(:56-57). - What the field writes.
DoubleFieldsetsDocValuesType.SORTED_NUMERIC(DoubleField.java:52), storesNumericUtils.doubleToSortableLong(value)(:73, and:105in the setter) and reads its own value back withsortableLongToDouble(:90). - What the stats class reads (fix site).
SortedNumericDocValuesStatsopensgetSortedNumericDocValues(field)(DocValuesStats.java:297);SortedDoubleDocValuesStats.doAccumulatereadsdocValueCount()values per document and decodes each withdouble val = Double.longBitsToDouble(sndv.nextValue());(:436). Max, min, sum, the running mean and the variance all use that value (:437-450). - The NUMERIC sibling is right.
DoubleDocValuesStatsdecodesDouble.longBitsToDouble(ndv.longValue())(:256), which matchesDoubleDocValuesField, documented as encoding "viaDouble#doubleToRawLongBits" (DoubleDocValuesField.java:20-21,:39). The sorted-numeric class uses the same decode for a field type whose core writer is sortable.
-5.0 has raw bits 0xc014000000000000 and sortable bits 0xbfebffffffffffff; read as raw bits, 0xbfebffffffffffff is -0.8749999999999999. 3.0 is 0x4008000000000000 in both layouts. So for DoubleField -5.0 and 3.0 the class sees -0.8749999999999999 and 3.0.
A Z3 bit-vector/FP64 model over all 2^64 bit patterns proves: the two layouts coincide exactly when the sign bit is clear; every negative non-NaN value is read as a different number; the order among normal negatives is reversed; -0.0 and every negative subnormal but the largest-magnitude one are read as NaN (that one as -Infinity), and -Infinity as a negative subnormal; the bit flip sortableDoubleBits is its own inverse on all 2^64 patterns, so the sortable decode returns exactly the bits doubleToSortableLong started from (Double.doubleToLongBits, which canonicalises NaN).
- Executed:
DoubleField-5.0 and 3.0 give min -0.8749999999999999, max 3.0, sum 2.125, mean 1.0625, variance 3.75390625 and stdev 1.9375 (expected -5.0, 3.0, -2.0, -1.0, 16.0 and 4.0). ASortedNumericDocValuesFieldwritten with the javadoc recipeNumericUtils.doubleToSortableLonggives the same wrong values. Raw bits (Double.doubleToRawLongBits, what the upstream test writes) give the right values. ASortedNumericSortField(DOUBLE)on the sameDoubleFieldsorts -5.0, 3.0 correctly. - Executed (an independent reviewer's test):
DoubleField-2.5, 7.25 and -0.5 give min -7.999999999999999 instead of -2.5: a misread negative can come out smaller or larger than the true value. - Executed (this doc's diagram probe, main, assertions on and off):
DoubleField-0.0 and 3.0 give max NaN, sum NaN, mean NaN and min 3.0, because -0.0 is read as NaN.
Not part of this finding: the facet module's MultiDoubleValuesSource.fromDoubleField, which reads these fields the same way (another module and fix site), a wrong max() (with a positive maximum it is right unless a -0.0 or a negative subnormal is present, which can be read as NaN, as in the -0.0 case above; when every value is <= 0 it is wrong for another reason: LFV-155, the Double.MIN_VALUE seed at DocValuesStats.java:429), and the variance denominator (a separate question, not run: the sorted-numeric variance() divides by the document count(), :312-315, while doAccumulate adds one term per value, :445-450; the upstream test computes its expected value the same way, and the repro has one value per document).
Exposure and likelihood211 words
- Who is hit. Users of
SortedDoubleDocValuesStats(throughDocValuesStatsCollectorManager, since 10.5.0, orDocValuesStatsCollector) whose field is aDoubleFieldor aSortedNumericDocValuesFieldwritten withNumericUtils.doubleToSortableLong, and whose matching documents hold any negative value: each one is decoded as a different number. - Default vs opt-in. Default
IndexWriterConfig, codec and searcher, public API only; two documents suffice. The class itself is opt-in: alucene/miscutility, rarely used. It isfinalwith a one-argument constructor, so it offers no decoder choice; the workaround is to write one's own subclass of the public abstractSortedNumericDocValuesStats(protected constructor and state) with a sortable decode, which both collector entry points accept. - Silent, and hidden by positive data. No exception, with assertions on and off. All-positive data gives exact results, so tests with positive data, like upstream's, never see it.
- Scale. Any index size. With a single negative value among the matches,
min()is always wrong; in ordinary data sum, mean, variance and stdev are wrong too, and the two-document repro gets all five wrong. (Rounding, or a set that holds both a value and its mis-read image, such as -5.0 and -0.8749999999999999, can leave an aggregate unchanged.) - Likelihood: plausible. Deterministic on the first negative value once the setup is used; the setup itself is rare.
One JUnit test for lucene/misc, public API only: two documents with DoubleField -5.0 and 3.0, the stats collector manager over all documents, and one assertion on min(). Fixed input, single thread; fails with assertions on and off (seeds DEADBEEF and 5EED1129).
/*
* 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.misc.search;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.DoubleField;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestSortedDoubleStatsDoubleField extends LuceneTestCase {
public void testMinOfNegativeDoubleField() throws Exception {
try (Directory dir = newDirectory();
IndexWriter w = new IndexWriter(dir, newIndexWriterConfig())) {
for (double v : new double[] {-5.0, 3.0}) {
Document doc = new Document();
doc.add(new DoubleField("f", v, Field.Store.NO));
w.addDocument(doc);
}
try (DirectoryReader reader = DirectoryReader.open(w)) {
DocValuesStats.SortedDoubleDocValuesStats stats =
new IndexSearcher(reader)
.search(
MatchAllDocsQuery.INSTANCE,
new DocValuesStatsCollectorManager<>(
() -> new DocValuesStats.SortedDoubleDocValuesStats("f")));
// expected min -5.0; actual -0.8749999999999999 (sortable bits decoded as raw IEEE bits)
assertEquals(-5.0, stats.min(), 0.0);
}
}
}
}
cp $REPRO/TestSortedDoubleStatsDoubleField.java $LUCENE/lucene/misc/src/test/org/apache/lucene/misc/search/
cd $LUCENE && ./gradlew :lucene:misc:test --tests TestSortedDoubleStatsDoubleField -Ptests.seed=DEADBEEF -Ptests.asserts=true
# also with -Ptests.asserts=false; this test failed at both seeds run (DEADBEEF, 5EED1129)
- Expected
min()is -5.0 (and sum -2.0, mean -1.0).- Observed
AssertionError: expected:<-5.0> but was:<-0.8749999999999999>, no other error, with assertions on and off.
More files, environment, full output213 words
The test is the hunt's minimal repro, unchanged; it passes :lucene:misc:check -x test (google-java-format, ecj lint, forbidden APIs). Use the same test as the regression test.
Executed: gradle on main 5adff78bb168, assertions on (seed DEADBEEF) and off (seed 5EED1129): fails 1 of 1; with the fix it passes. The standalone runner gives the same failure on main with assertions on and off, as do the registry re-runs (seeds 5EED0E36, 5EED0D37). newDirectory() and newIndexWriterConfig() randomize only the directory and writer settings (the gradle run drew a random codec); an independent verifier's equivalent test with a plain IndexWriterConfig fails the same way.
On 10.5.0 and later the test runs as written. On 9.5.0 to 10.4.x, which have no DocValuesStatsCollectorManager, use DocValuesStatsCollector with searcher.search(query, collector); that variant fails the same way on 9.5.0 and 9.12.3.
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT) |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (only this platform was run) |
cp $REPRO/TestSortedDoubleStatsDoubleField.java $LUCENE/lucene/misc/src/test/org/apache/lucene/misc/search/
cd $LUCENE && ./gradlew :lucene:misc:test --tests TestSortedDoubleStatsDoubleField -Ptests.seed=DEADBEEF -Ptests.asserts=true
# also with -Ptests.asserts=false; this test failed at both seeds run (DEADBEEF, 5EED1129)
TestSortedDoubleStatsDoubleField > testMinOfNegativeDoubleField FAILED
java.lang.AssertionError: expected:<-5.0> but was:<-0.8749999999999999>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:2B44CD2E2B06DA91]:0)
at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
at junit@4.13.2/org.junit.Assert.failNotEquals(Assert.java:835)
at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:555)
at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:685)
at org.apache.lucene.misc.search.TestSortedDoubleStatsDoubleField.testMinOfNegativeDoubleField(TestSortedDoubleStatsDoubleField.java:47)
:lucene:misc:test (FAILURE): 1 test, 1 failure- Documented as statistics for "a sorted-numeric field storing double values", with no encoding named;
DoubleFieldis core's sorted-numeric double field. - Core's documented recipe for sorted-numeric doubles is the sortable layout, and core's readers (sorting, selectors) decode it so.
- The upstream test writes only 1, 2, 3, ..., where both layouts are identical.
The full argument318 words
- Contract. "Holds DocValues statistics for a sorted-numeric field storing
doublevalues" (DocValuesStats.java:421);min(): "The minimum value of the field. Undefined whencount()is zero." (:111). No encoding precondition appears in the class, the package docs, the misc overview, CHANGES or MIGRATE. - Core's recipe. "Note that if you want to encode doubles or floats with proper sort order, you will need to encode them with
NumericUtils" (SortedNumericDocValuesField.java:33-37), there since the sorted-numeric type was added in 2014, before this class. - Core's readers.
SortedNumericSelector("undo the numericutils sortability",:74-89), whichSortedNumericSortFielduses for every comparator (:282-368), decodes sorted-numeric doubles as sortable; so do the index sorter and Luke's doc-values view. No non-test, non-sandbox Lucene code writes raw-bit sorted-numeric doubles. - Raw bits are stated where meant.
DoubleDocValuesField("encoding doubles as NumericDocValues viaDouble#doubleToRawLongBits",:20-21) andDoubleRangeFacetCounts("This assumes that the field was indexed withDoubleDocValuesField",:53-54).DocValuesStatshas no such sentence. - Tests.
TestDocValuesStatsCollector.testDocsWithMultipleDoubleValueswritesDouble.doubleToRawLongBits(nextVal)withnextVal= 1, 2, 3, ... (:274-290). For these values both layouts are the same bits, so the test passes 8 of 8 on stock and with the fix. No misc test runs these stats on aDoubleField. - History. The raw decode is from LUCENE-7590 (2016, 6.4.0), whose thread never discusses the encoding.
DoubleFieldcame with #11997 (9.5.0), which did not touchDocValuesStats; there jpountz wrote that multi-valued floats and doubles "should useNumericUtil#doubleToSortableLongto present ordering within a document", proposed "only supporting multi-valued fields, so that there is a single encoding in doc values", pushed that change and wrote (quoted as written): "These new fields not only support indexing sorted numeric doc values, so that queries do not have to specify whether floats/doubles are indexed via their raw bits or as sortable ints/longs". So the doc-values type is SORTED_NUMERIC only; the fields also index a point.
The devil's-advocate check283 words
The strongest "intended" case (built by the devil's-advocate verifier and the cross-model reviews; the refute-lens review rated it strong, the others moderate): LUCENE-7590 added the raw decode together with a raw-bit test, so the author meant the class as the multi-valued twin of DoubleDocValuesStats/DoubleDocValuesField, which are raw bits. In 2016 core had no sorted-numeric double field, so the encoding was the user's choice, and the javadoc recipe is conditional ("if you want ... proper sort order"), an order that matters only within a document, which the statistics ignore. The facet module's MultiDoubleValuesSource.fromDoubleField reads sorted-numeric doubles as raw bits too. The class is exact for raw-bit writers, negatives included (doubleToRawLongBits -5.0 and 3.0 give min -5.0, sum -2.0), and sorted-numeric doc values carry no encoding tag, so a decode switch changes results for them. A maintainer could answer "document that it expects raw bits".
Why it fails: nothing documents raw bits for this class, while Lucene states raw bits where it means them. Core's only sorted-numeric double field and core's recipe are sortable, core's readers of such fields decode sortable, and #11997 made the new fields sortable on purpose. The upstream test pins nothing observable. Core's standard double field gets wrong statistics today, with no warning. The argument bears on how to fix it (a migration note, or a decoder argument), not on whether DoubleField users get wrong numbers. Verdict: a defect for DoubleField users, an undocumented mismatch between core's sortable writer and this reader; whether the class's contract becomes sortable or raw (documented) is the maintainers' call, and the residual risk that they prefer documenting raw bits is why the priority sits in the lower part of Medium.
Recommended Decode sortable, and say so in the javadoc (fix A)
Decode the stored longs with NumericUtils.sortableLongToDouble instead of Double.longBitsToDouble (one line), and state the expected encoding in the class javadoc. The repro, the whole lucene/misc test task and :lucene:misc:check -x test pass. It changes results for anyone who wrote negative values as raw bits, so it needs a migration note.
/**
* Holds DocValues statistics for a sorted-numeric field storing {@code double} values. Values
* must be encoded with {@link NumericUtils#doubleToSortableLong(double)}, as {@link
* org.apache.lucene.document.DoubleField} does.
*/
public static final class SortedDoubleDocValuesStats extends SortedNumericDocValuesStats<Double> {
...
double val = NumericUtils.sortableLongToDouble(sndv.nextValue()); // was Double.longBitsToDoubleAll options and trade-offs3 options ยท 434 words
One fix was built and validated in a scratch worktree of 5adff78bb168; the alternatives are listed with their trade-offs.
In DocValuesStats.SortedDoubleDocValuesStats.doAccumulate, decode each value with NumericUtils.sortableLongToDouble, the inverse of what DoubleField and core's recipe write, and add one javadoc sentence naming the expected encoding (+7/-2 with the import). The NUMERIC DoubleDocValuesStats keeps its raw decode, which matches DoubleDocValuesField.
/**
* Holds DocValues statistics for a sorted-numeric field storing {@code double} values. Values
* must be encoded with {@link NumericUtils#doubleToSortableLong(double)}, as {@link
* org.apache.lucene.document.DoubleField} does.
*/
public static final class SortedDoubleDocValuesStats extends SortedNumericDocValuesStats<Double> {
...
double val = NumericUtils.sortableLongToDouble(sndv.nextValue()); // was Double.longBitsToDoubleTrade-offs
- Behaviour change. Applications that wrote raw IEEE bits of negative values into a
SortedNumericDocValuesField(what the upstream test does, with positive values only) have their negative values decoded exactly today and misread after the fix, symmetric to the bug, so their statistics can change: an independent reviewer's raw-bit -2.5, 7.25 and -0.5 give min -7.999999999999999 under the fix (executed). Non-negative raw-bit data is unaffected. Needs a CHANGES entry and a MIGRATE note; whether to ship it on a release branch is a maintainer call. - Optionally switch the upstream test's
Double.doubleToRawLongBitstoNumericUtils.doubleToSortableLongand add a negative value. - Cost: still one decode per value, no allocation; the sortable decode adds a fixed XOR, shift and mask per value. No benchmark was run.
What the checks validated
- Gradle, JDK 25, scratch worktree of
5adff78bb168(removed afterwards): the test fails on stock with assertions on (DEADBEEF) and off (5EED1129), and passes with the fix, on and off. - With the fix: the test plus
TestDocValuesStatsCollector, 9 tests, 0 failures; the whole:lucene:misc:testtask, 317 tests (9 skipped), passes;:lucene:misc:check -x testand:lucene:misc:renderJavadocpass. - The decode line alone was validated the same way by the finder, an independent reproducer and a cross-model review, with the standalone runner.
Add a constructor SortedDoubleDocValuesStats(String field, LongToDoubleFunction decoder), with the sortable decode as default (or, where results must not change, the raw one), and name the expected encoding in the javadoc. The facet module already offers this shape: MultiDoubleValuesSource.fromField(field, LongToDoubleFunction decoder).
Trade-offs
Raw-bit writers can keep their results; more API surface. Not built.
State that the field must hold raw IEEE bits and that DoubleField is not supported.
Trade-offs
No behaviour change, but core's standard double field could then not be summarized by this class at all, against core's own recipe. Not recommended; not validated.
A probe that carried the pattern of an earlier finding (a join reading DoubleField as raw bits) to other readers of sorted-numeric doubles gave min -0.8749999999999999 for DoubleField -5.0 and 3.0, while a raw-bit control was right. The hunt unit that judged it reduced it to one test; a Z3 model proved which values are affected.
The method in full213 words
Method: non-formal (an executed differential probe; the Z3 bit-vector model only proved the trigger set afterwards).
- Round 23 of the hunt planned pattern-transfer checks: take the shape of a confirmed bug (LFV-1111, a numeric join that reads sortable
DoubleFieldvalues as raw bits) and test the property "one logical type stored in two doc-values encodings must decode to the same value on every consumer" on other readers. Its own probe (2026-11-02, maine1faeba08bde, assertions on and off, also 10.6.0) fed the same values -5.0 and 3.0 toSortedDoubleDocValuesStatsthrough three writers:DoubleFieldand the javadoc recipe gave min -0.8749999999999999, the raw-bit control the right -5.0. A second pattern-transfer probe found it again. - The hunt unit that judged it reduced it to a public-API JUnit, ran it on main, 10.6.0 and 9.5.0, modelled the trigger set in Z3 (the raw decode of the sortable layout is the identity exactly when the sign bit is clear) and checked the fix against the upstream test; a history step traced it to LUCENE-7590 and #11997.
- An independent reproducer, cross-model reviews (OpenAI GPT-5.6), a devil's-advocate verifier, an adjudication, an audit and a final repro check then re-ran it.
- An earlier sighting (2026-10-26, while LFV-155 was being verified) noticed this mis-decode, but it was not followed up then.
No public report found (2026-11-02): apache/lucene issues and PRs incl. the migrated JIRA archive, the changed files of all 408 open PRs, the Lucene lists, Elasticsearch, OpenSearch, Solr and the web. Nearest: #8641 (LUCENE-7590, which added the class) and #11997 (which added DoubleField); neither discusses the other.
Searches4 searches
None of the hits is the same bug. Not fixed upstream: the last commit to DocValuesStats.java is 0210af757c5 (#15653, 2026-05-19, the collector manager), which does not touch the decode; upstream main head 1c2072f0284, branch_10_6 and branch_11x still decode raw. The only open PR touching a DocValuesStats file, #16847, changes the test's doc-count conversion only. LFV-1111 and the earlier LFV-155 sighting are the hunt's own findings, not prior art. Search limits: keyword search only; GitHub search reads the first page per query and tokenises identifiers loosely; the list search covers dev@, java-user@, issues@ and commits@; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive; Elasticsearch, OpenSearch, Solr) | 2026-11-02 | two scripted prior-report searches (51 queries each, none rated highly relevant); 31 manual queries on class names, decode identifiers, in:title forms, open PRs; commit search; a same-day re-check | no report |
| Open PRs and upstream branches | 2026-11-02 | the changed files of all 408 open apache/lucene PRs; every PR updated since 2026-10-31; DocValuesStats.java history on main, branch_10_6 and branch_11x | #16847 touches only the test's doc-count conversion; not fixed on any branch |
| JIRA, mailing lists and the web | 2026-11-02 | LUCENE JIRA text queries; dev@, java-user@, issues@ and commits@; 5 web queries | no report |
| Nearest items (read in full) | 2026-11-02 | #8641 (42 comments), #15653 (46 comments), #11997 (comments and review comments) | the class's introduction and its collector manager, no encoding discussion; the change that added DoubleField, sortable encoding discussed, DocValuesStats never mentioned |
The raw decode dates from LUCENE-7590 (2016, first released in 6.4.0) and is unchanged; DoubleField (#11997, 9.5.0) made it reachable through core's own field. Fails on 6.4.0, 8.11.4 and 9.4.2 with the documented recipe and on 9.5.0, 9.12.3, 10.5.2, 10.6.0 and main with DoubleField. Not fixed upstream.
Versions tested9 rows
Introduced by 944b8e07f557 "LUCENE-7590: add DocValuesStats for SortedNumeric DV fields" (Shai Erera, author date 2016-12-17; branch_6x 63a5cd00173; migrated as #8641, no PR), first released in 6.4.0; git blame on main still attributes the decode line to it. Its test wrote raw bits of 1, 2, 3, ... only. Reachable through core's own field since #11997 "Add IntField, LongField, FloatField and DoubleField" (57201aa9678 on main, 827f762d137 on branch_9x, author date 2022-12-20, first released in 9.5.0), which did not touch DocValuesStats. So it is not a regression of the stats class. By code identity the raw decode is at all 78 release tags from 6.4.0 to 10.6.0 in the history clone (its 8.8.2-8.11.x tags are missing; 8.11.4 was executed). For 6.4.0-9.4.x calling it a bug rests on the documented recipe being the convention; from 9.5.0 the DoubleField case does not. Not new in 10.6.
| Version or commit | Verdict | Result |
|---|---|---|
main 5adff78bb168 | affected | this doc's test fails through gradle with assertions on and off; passes with fix A |
| 10.6.0 | affected | fails with assertions on and off (release jars) |
| 10.5.2 | affected | fails, expected:<-5.0> but was:<-0.8749999999999999> |
| 9.12.3 | affected | fails (9.x collector API) |
| 9.5.0 | first with DoubleField | fails (9.x collector API) |
| 9.4.2 | recipe only | no DoubleField (does not compile); the documented recipe fails: min -0.8749999999999999, sum 2.125, mean 1.0625 |
| 8.11.4 | recipe | the documented recipe fails, same values |
| 6.4.0 | first release | the documented recipe fails with assertions on and off, same values |
| 6.3.0 | last before | no DocValuesStats: does not compile |
An independent reproducer, cross-model reviews (OpenAI GPT-5.6, executed), a devil's-advocate verifier, an adjudication, an audit and a final repro check reproduced it and agree on the root cause; the refute-lens review rated the intended-behaviour argument strong but still judged the bug real. This doc's gradle runs reproduced it on main and validated the fix.
Checks and verdicts336 words
Every executed check saw min -0.8749999999999999 (or, with other values, another wrong min) for DoubleField with a negative value, from the same decode line, DocValuesStats.java:436. The GPT reviews ran on the earlier main checkout ea57360912ed, whose DocValuesStats.java, DocValuesStatsCollectorManager.java, DoubleField.java and NumericUtils.java are byte-identical to 5adff78bb168's.
| Check | Verdict | What it found |
|---|---|---|
| first signal (round 23) | wrong min | Public-API differential probe on main e1faeba08bde (on, off) and 10.6.0: DoubleField and the javadoc recipe give min -0.8749999999999999, sum 2.125, mean 1.0625; the raw-bit control is correct. |
| finder | reproduced | One-test JUnit fails on main (on, off), 10.6.0 (on, off) and 9.5.0; passes with the sortable decode; upstream test 8 of 8 either way; Z3 trigger-set proof and a mutation check. |
| cross-model review, intended lens | real (high) | OpenAI GPT-5.6, executed: its own test fails on main and 10.6.0; raw-bit control correct; the fix passes and changes negative raw-bit results. Rated the intended argument moderate. |
| independent reproducer | real | Own test with a plain IndexWriterConfig fails on main (on, off), 10.6.0, 10.5.2 and 9.12.3; passes with the sortable decode. |
| cross-model review, refute lens | real (high); intended argument rated strong | OpenAI GPT-5.6, executed: DoubleField -2.5, 7.25, -0.5 give min -7.999999999999999; positive and raw-bit controls pass; the fix passes; a raw-bit writer's results change under the fix. |
| devil's advocate | real | Re-ran on main (on, off) and 10.6.0: fails; raw-bit control passes; no document states a raw-bit precondition; rated the intended case moderate. |
| adjudication | confirmed (high) | Re-ran the independent reproduction's test on main (on, off), seed 1A2B3C4D: fails. Silent wrong results, security none; related to LFV-1111. |
| audit and final repro check | confirmed | The audit re-ran it on main (fails) and confirmed it; the final check confirmed the one-test repro (fails on main, on and off; passes with the fix). |
| this doc | reproduced | Gradle on main (on, off): fails; fix A validated with the whole lucene/misc test task, :lucene:misc:check -x test and the misc javadoc build. |
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 538 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,326 words
- Registry title
- SortedDoubleDocValuesStats (lucene/misc) reads core DoubleField values as raw IEEE bits, so negative values give a silently wrong min, sum and mean (DoubleField -5.0 and 3.0 give min -0.8749999999999999)
- Area
- misc / DocValuesStats: SortedDoubleDocValuesStats decode of sorted-numeric double doc values (core DoubleField)
- Priority
- Medium, rank 181: SortedDoubleDocValuesStats in lucene/misc returns a wrong min, sum and mean, and so a wrong variance and stdev, every time a matching value is negative. This holds for core DoubleField (since 9.5.0) and for the documented sortable-double encoding, with default settings in every release since 6.4.0. The class is opt-in and rarely used, so it sits in the lower part of Medium, but the trigger is ordinary signed data.
- Impact class
- silent wrong results
- Impact note
- Applications that compute DocValuesStats.SortedDoubleDocValuesStats (lucene/misc, through DocValuesStatsCollectorManager or the older DocValuesStatsCollector) over core's DoubleField, or over a SortedNumericDocValuesField written with the NumericUtils.doubleToSortableLong recipe that class's javadoc gives, get a silently wrong min, sum and mean (and the variance and stdev derived from them) when the matching values include negative numbers: every value with the sign bit set (negative numbers, -0.0, -Infinity) is read back as a different number, for example -5.0 as -0.8749999999999999 and -0.0 as NaN; default IndexWriterConfig, codec and searcher, -ea and -da, any index size; positive values are exact because their sortable and raw bits are identical, which is why the upstream test (positive values only) does not see it; DoubleField trigger in every release from 9.5.0 through 10.6.0 and main, the javadoc-recipe trigger since 6.4.0.
- Summary
- You index two documents with DoubleField("f", -5.0) and DoubleField("f", 3.0) and search MatchAllDocsQuery with new DocValuesStatsCollectorManager<>(() -> new SortedDoubleDocValuesStats("f")); you expect min() = -5.0 (sum -2.0, mean -1.0), because the class 'Holds DocValues statistics for a sorted-numeric field storing double values' and DoubleField is core's sorted-numeric double field. You get min() = -0.8749999999999999 (sum 2.125, mean 1.0625) with no error: DoubleField stores NumericUtils.doubleToSortableLong(value), but SortedDoubleDocValuesStats decodes each stored long as raw IEEE bits (Double.longBitsToDouble, DocValuesStats.java:436). Positive values are unaffected because their sortable and raw bits are the same.
- Introduced
- 944b8e07f557b9320895998fe33d71cae5199eee ('LUCENE-7590: add DocValuesStats for SortedNumeric DV fields', by Shai Erera, migrated as #8641; pre-GitHub, no PR), which added SortedDoubleDocValuesStats with the raw decode that is today DocValuesStats.java:436, unchanged since [read: git blame on main]; release carrier the branch_6x backport 63a5cd00173f7e89a478981429d4d5cd38f3cf1d, first released in 6.4.0 [exec: 6.3.0 has no DocValuesStats, 6.4.0 fails]. The same commit's test writes Double.doubleToRawLongBits of positive values only. Reachable through core's own field since 57201aa967828a678a6c440b6a2e36fd8846ca37 (main) / 827f762d137 (branch_9x, in 9.5.0), apache/lucene#11997 'Add IntField, LongField, FloatField and DoubleField' (closes #11199), which added DoubleField with sortable SORTED_NUMERIC doc values and did not touch DocValuesStats [exec: 9.4.2 has no DoubleField, 9.5.0 fails]. Not a regression of DocValuesStats (2016-12-17 (author date of 944b8e07f55 and 63a5cd00173; committed 2016-12-18); DoubleField trigger 2022-12-20 (author date of 57201aa9678 / 827f762d137, #11997))
- First affected version
- 6.4.0 [exec: the SortedNumericDocValuesField javadoc-recipe probe (NumericUtils.doubleToSortableLong of -5.0 and 3.0) fails on 6.4.0 -ea and -da; 6.3.0 has no DocValuesStats (compile error)]; with core's DoubleField, the trigger of the repro, 9.5.0 [exec: the 9.x form of the test fails on 9.5.0; 9.4.2 has no DoubleField (compile error)]. For 6.4.0-9.4.x calling it a bug rests on the javadoc recipe being the convention for sorted-numeric doubles; from 9.5.0 the DoubleField case does not depend on that reading
- Affected versions
- every release from 6.4.0 through 10.6.0 (6.4.0-6.6.6, 7.0.0-7.7.3, 8.0.0-8.11.4, 9.0.0-9.12.3, 10.0.0-10.6.0; 10.6.0 released 2026-10-29, 4ae946e5a9b) and main 5adff78bb168 (12.0.0-SNAPSHOT). Executed with core's DoubleField (the repro, all 'expected:<-5.0> but was:<-0.8749999999999999>'): main -ea and -da (finder, the independent reproduction, the intended-behaviour check, adjudication, follow-up, re-run), 10.6.0 -ea and -da, 10.5.2 -da, and 9.12.3 -da and 9.5.0 -da (9.x form of the test: DocValuesStatsCollector with search(Query, Collector)) [exec]; with the javadoc recipe: 6.4.0 -ea and -da, 8.11.4 -da and 9.4.2 -da (min -0.8749999999999999, sum 2.125, mean 1.0625) [exec]; the other releases by code identity (the raw decode is at all 78 release tags 6.4.0-10.6.0 in the history clone; its 8.8.2-8.11.x tags are missing, and 8.11.4 was executed) [read]; upstream main head 1c2072f0284 (2026-11-02) still decodes raw at DocValuesStats.java:436 (checked via the GitHub API), as do branch_10_6 and branch_11x (follow-up) [read]. Not affected: 6.3.0 and earlier (no DocValuesStats; 6.3.0 compile error [exec]); the DoubleField trigger needs 9.5.0 or later (9.4.2 compile error [exec]). Not new in 10.6 (10.5.2 fails [exec]).
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal; also reached by fuzzing/PBT
- Upstream and prior reports
- none found (2026-11-02, confidence high but not certain, on top of the finder's scripted prior-report search (51 queries), the independent reproduction's and the intended-behaviour check's spot checks and the adjudication's re-check; rechecked the same day): 99 queries in a follow-up search (a scripted prior-report search over apache/lucene issues and PRs, commits, Elasticsearch, OpenSearch and Solr, the dev@, java-user@, issues@ and commits@ lists and the LUCENE JIRA archive; manual GitHub API searches including open PRs; lists.apache.org), the changed-file lists of all 408 open apache/lucene PRs, and full reads of #8641 (42 comments) and #15653 (46 comments): no report of this mis-decode or of wrong statistics for negative DoubleField values. Not fixed upstream: the last commit to DocValuesStats.java is 0210af757c5 (#15653, author and committer date 2026-05-19), which does not touch the decode; upstream main head 1c2072f0284 still has Double.longBitsToDouble(sndv.nextValue()) at :436. A same-day recheck of GitHub searches on apache/lucene found nothing for 'SortedDoubleDocValuesStats', 'DocValuesStats DoubleField' or 'DocValuesStats sortable', and only #2065, #10880, #15653 and #8641, none about the encoding, for 'DocValuesStats'; the changed files of #16962-#16965 (created since 2026-11-01) and the items updated since 2026-11-02 (#16964, #16918, #16878, #16877, #16806, #16790) do not touch DocValuesStats; the only open PR touching a DocValuesStats file is #16847 (TestDocValuesStatsCollector.java, doc-count int conversion only). Background, not reports: #8641 (LUCENE-7590, introduced the class; no encoding discussion), #15653 (DocValuesStatsCollectorManager), #11997 (added DoubleField; jpountz wrote on 2022-12-11 that multi-valued doubles should use NumericUtil#doubleToSortableLong; DocValuesStats not mentioned)
- Lucene commit tested
- main
5adff78bb168(12.0.0-SNAPSHOT) through gradle and a standalone runner, assertions on and off; release jars 10.6.0, 10.5.2, 9.12.3 and 9.5.0 (DoubleField), 9.4.2, 8.11.4, 6.4.0 and 6.3.0 (the documentedNumericUtilsrecipe); the fix in a scratch worktree of5adff78bb168(OpenJDK 25.0.3, Linux aarch64). The cross-model reviews ran on mainea57360912ed, whoseDocValuesStats.java,DocValuesStatsCollectorManager.java,DoubleField.javaandNumericUtils.javaare byte-identical to5adff78bb168's.
- Finding record
lucene/misc/src/java/org/apache/lucene/misc/search/DocValuesStats.java:111 (at 5adff78bb168) (min(): 'The minimum value of the field'), :421 ('Holds DocValues statistics for a sorted-numeric field storing {@code double} values'; no encoding stated), :436 ('double val = Double.longBitsToDouble(sndv.nextValue());', fix site), :256 (control: NUMERIC DoubleDocValuesStats decodes raw bits, matching DoubleDocValuesField)lucene/core/src/java/org/apache/lucene/document/DoubleField.java:52 (at 5adff78bb168) (DocValuesType.SORTED_NUMERIC), :73 and :105 (NumericUtils.doubleToSortableLong), :90 (its own decode via NumericUtils.sortableLongToDouble)lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesField.java:33-37 (at 5adff78bb168) ('if you want to encode doubles or floats with proper sort order, you will need to encode them with NumericUtils')Lucene main ea57360912ed and Lucene main 5adff78bb168: DocValuesStats.java, DocValuesStatsCollectorManager.java, DoubleField.java and NumericUtils.java byte-identical (cmp)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/util/NumericUtils.java:47-48, (at 5adff78bb168) :56-57, :84-85lucene/core/src/java/org/apache/lucene/document/DoubleField.java:52, (at 5adff78bb168) :73, :90, :105; DoubleDocValuesField.java:20-21, :39lucene/misc/src/java/org/apache/lucene/misc/search/DocValuesStats.java:256 (at 5adff78bb168) (DoubleDocValuesStats), :297 (getSortedNumericDocValues), :421-450 (SortedDoubleDocValuesStats; :436 the raw decode), :429 (Double.MIN_VALUE seed, LFV-155)lucene/facet/src/java/org/apache/lucene/facet/MultiDoubleValuesSource.java:81-83 (at 5adff78bb168) (read only)
- Who is affected
lucene/misc/src/java/org/apache/lucene/misc/search/DocValuesStats.java:422, (at 5adff78bb168) :428 (final class, one-argument constructor)
- Why it looks unintended
lucene/misc/src/java/org/apache/lucene/misc/search/DocValuesStats.java:111, (at 5adff78bb168) :421lucene/core/src/java/org/apache/lucene/document/SortedNumericDocValuesField.java:33-37; (at 5adff78bb168) DoubleDocValuesField.java:20-21lucene/core/src/java/org/apache/lucene/search/SortedNumericSelector.java:74-89; (at 5adff78bb168) SortedNumericSortField.java:282-368lucene/facet/src/java/org/apache/lucene/facet/range/DoubleRangeFacetCounts.java:53-54; (at 5adff78bb168) lucene/facet/src/java/org/apache/lucene/facet/MultiDoubleValuesSource.java:81-83lucene/misc/src/test/org/apache/lucene/misc/search/TestDocValuesStatsCollector.java:274-290 (at 5adff78bb168)
- Suggested fix
lucene/facet/src/java/org/apache/lucene/facet/MultiDoubleValuesSource.java:76 (at 5adff78bb168) (fromField with a LongToDoubleFunction decoder)
- TestSortedDoubleStatsDoubleField.java (lucene/misc, public API):
repro/LFV-1129/TestSortedDoubleStatsDoubleField.java(51 lines)
A summary still (PNG).
assets/LFV-1129/bug.png(538 KB)
