ValueSource.fromDoubleValuesSource wraps a DoubleValuesSource for the function-query API. Its values keep the exact double, but it does not override getRangeScorer, the range test FunctionRangeQuery uses, so it inherits the generic one that compares in float.
time <= t returns all three; the typed sources stay exact. Select the still to open it at full size. Generated with AI, checked against the code.- You store event times as epoch milliseconds in a long doc-values field
time: t − 1 ms, t and t + 60,000 ms (one minute later), with t = 1,700,000,000,000. You wrap it for the function-query API:ValueSource.fromDoubleValuesSource(DoubleValuesSource.fromLongField("time")). - You filter
time <= twithnew FunctionRangeQuery(source, null, t, true, true)and expect the first two events, which is whatLongFieldSource("time")andFunctionMatchQueryreturn. - Instead all three match, with no error. The bridged values inherit the generic range test of
FunctionValues: it parses the bound withFloat.parseFloatand compares each value cast tofloat. Floats near 1.7e12 are 131,072 apart, so every value from t − 26,623 ms to t + 104,447 ms counts as equal to t. - The same happens wherever two numbers share a float:
[1000000000 TO 1000000000]over ids matches the 65 ids 999,999,968 to 1,000,000,032;[1.0 TO 1.0]over a double field also matches 0.9999999999999999 and 1.0000000000000002;[* TO 1e300]matches 1e301, because 1e300 becomes Infinity.
Code that range-filters a DoubleValuesSource through ValueSource.fromDoubleValuesSource with FunctionRangeQuery (experimental): doc-values fields, LongValuesSource.toDoubleValuesSource(), or compiled expressions, whose only route to a ValueSource is this bridge. Long ids above 2^24 and epoch timestamps hit it with ordinary values; doubles only right next to a bound or beyond the float range. No Lucene production code calls the bridge.
Mechanism, terms and numbers620 words
ValueSource.fromDoubleValuesSource(DoubleValuesSource) adapts a DoubleValuesSource (a double or long doc-values field, LongValuesSource.toDoubleValuesSource(), a compiled expression) to the function-query ValueSource API. Its FunctionValues keeps the exact double in doubleVal but does not override getRangeScorer, so it inherits the generic default, which parses both bounds with Float.parseFloat and compares each value cast to float. FunctionRangeQuery, the only production caller of getRangeScorer, therefore range-filters the bridged source in float: any value that rounds to the same float as a bound counts as equal to it, and a bound or value too large for a float becomes infinite. Over epoch milliseconds [* TO 1700000000000] also returns an event one minute later (the window runs from t − 26,623 ms to t + 104,447 ms); [1000000000 TO 1000000000] matches 65 ids; [1.0 TO 1.0] matches 0.9999999999999999 and 1.0000000000000002; [* TO 1e300] matches 1e301. No error is raised. The same field through DoubleFieldSource / LongFieldSource, or FunctionMatchQuery over the same DoubleValuesSource, returns the exact hits. The bridge came in LUCENE-7737 (7.0.0) without a range override; it is affected from 7.0.0 on (executed on 11 versions up to main). Building the bridged values on DoubleDocValues, as the typed double sources do, fixes it (+3/−9 lines in ValueSource.java).
- The bridge:
ValueSource.fromDoubleValuesSource(in)(ValueSource.java:267-272). It unwraps a source that came fromValueSource.asDoubleValuesSource()(since #15406, 10.4.0) and otherwise returnsnew FromDoubleValuesSource(in). It has no javadoc. FunctionRangeQuery: keeps itsNumberbounds as strings ("declared as strings because FunctionValues.getRangeScorer takes String args and parses them",FunctionRangeQuery.java:48-49) and asks the source'sFunctionValuesfor a range scorer (:170-172), the only production caller ofgetRangeScorer.- The float default:
FunctionValues.getRangeScorer(FunctionValues.java:224-293). Typed values override it:DoubleDocValuescompares doubles (DoubleDocValues.java:86-147),LongDocValueslongs (LongDocValues.java:90-125).
FromDoubleValuesSource.getValuesreturnsnew FunctionValues() {...}(ValueSource.java:302) withfloatVal=(float) inner.doubleValue()(:308-314),doubleVal= the exactinner.doubleValue()(:316-322) andexists=inner.advanceExact(doc)(:324-327), and nogetRangeScorer.- So
FunctionRangeQuerygets the default, which parses the bounds withFloat.parseFloat(FunctionValues.java:245,:250) and comparesfloatVal(doc)(:261,:270,:279,:288) afterexists(doc). "1700000000000"becomes the float 1,700,000,038,912; t − 1, t and t + 60,000 all cast to that float, so<=holds for all three."1.0E300"becomesInfinity, and(float) 1e301isInfinitytoo.DoubleFieldSourceandLongFieldSourcereturn typed values with their own range test, andFunctionMatchQueryapplies its predicate to the exact double: over the same data they return the exact hits.
| field and values | range | exact | bridged |
|---|---|---|---|
| long: t − 1, t, t + 60,000 (t = 1,700,000,000,000) | [* TO t] | t − 1, t | all three |
| long: 999,999,999, 1,000,000,000, 1,000,000,001 | [1000000000 TO 1000000000] | 1,000,000,000 | all three |
| double: 0.9999999999999999, 1.0, 1.0000000000000002 | [1.0 TO 1.0] | 1.0 | all three |
| double: 1e300, 1e301 | [* TO 1e300] | 1e300 | both |
Window probe (every integer in a range indexed): [1000000000 TO 1000000000] matches 65 ids, 999,999,968 to 1,000,000,032 (the bound and 64 wrong ids); [t TO t] matches 131,071 timestamps, t − 26,623 to t + 104,447 ms (t and 131,070 wrong ones); [* TO t] admits values up to t + 104,447 ms. The hunt's independent reproducer also executed: {1.0 TO *] misses 1.0000000000000002 (exclusive bounds drop values that round onto the bound); [1e39 TO *] matches 3.5e38; at exact float midpoints both directions flip ([1.0000000596046448 TO 1.0000000596046448] misses the document with exactly that value, {1.0000001788139343 TO *] admits the equal one); an expressions-module source over-matches like a field. Not affected: documents without a value, integers below 2^24 (16,777,216), and bounds and values that are exact floats.
Exposure and likelihood368 words
- Who. Code that combines
FunctionRangeQuery(or its owngetRangeScorercall) withValueSource.fromDoubleValuesSource(...)of aDoubleValuesSourcethat did not start as aValueSource:DoubleValuesSource.fromDoubleField/fromLongField,LongValuesSource.toDoubleValuesSource(), an expressions-module source. For expressions this is the only route into theValueSourceAPI since 7.0.0; up to 6.3,Expression.getValueSourcereturned values with an exact double range test (executed on 6.3.0), which LUCENE-7609 removed in 6.4.0. - When. Deterministically, and only next to a bound: a value that differs from a bound but rounds to the same float counts as equal to it, so an inclusive bound admits it from just outside the range and an exclusive bound drops it from just inside; a value on the correct side of an inclusive bound is still judged right. The window is one float step wide (2^-24 to 2^-23 of the magnitude) around the bound's float: ±32 around 1e9; about 131 s around any epoch-ms bound from 2004 to 2039, placed differently per bound (t − 26,623 to t + 104,447 ms for t = 1.7e12). Numbers too large for a float (from about 3.4028236e38) become ±
Infinity, so a bound and a value beyond it can compare equal; at exact float midpoints a value equal to a bound can round apart from it. Rare for ordinary doubles; ordinary for id- and timestamp-scale longs (65 ids match a point query near 1e9; at t = 1.7e12 a "published before t" cut-off admits items up to 104 s late). - What goes wrong. A wrong hit set with no error: extra hits for inclusive bounds, missing hits for exclusive bounds, both at exact float midpoints. Query-time only; the index is not touched.
- Default vs opt-in. Default
IndexWriterConfig, codec and searcher; the trigger is the API pair. No Lucene production code callsfromDoubleValuesSource, andFunctionRangeQueryis@lucene.experimental(FunctionRangeQuery.java:42). Since 10.4.0 (#15406) a typedValueSourceconverted to aDoubleValuesSourceand back is unwrapped and exact. - Size of the check. A bounded-exhaustive sweep of about 30 range, exact and set query paths against an exact reference (983,164 double, 268,692 int and 483,557 long checks) found this path as the only one with unexplained mismatches on double, int and long fields (5,997, 606 and 1,528).
The test shows that FunctionRangeQuery over ValueSource.fromDoubleValuesSource(DoubleValuesSource.fromDoubleField("d")) compares the value and the bounds as floats, so [1.0 TO 1.0] matches a document whose value is 1.0000000000000002.
/*
* 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.queries.function;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.DoubleDocValuesField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestFromDoubleValuesSourceRange extends LuceneTestCase {
public void testRangeComparesDoubles() throws Exception {
try (Directory dir = new ByteBuffersDirectory();
IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
doc.add(new DoubleDocValuesField("d", Math.nextUp(1.0)));
w.addDocument(doc);
try (DirectoryReader r = DirectoryReader.open(w)) {
ValueSource vs =
ValueSource.fromDoubleValuesSource(DoubleValuesSource.fromDoubleField("d"));
// expected 0 hits (1.0000000000000002 is not in [1.0, 1.0]); actual 1 (compared as floats)
FunctionRangeQuery query = new FunctionRangeQuery(vs, 1.0, 1.0, true, true);
assertEquals(0, new IndexSearcher(r).count(query));
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-409/, 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/queries/build/libs/lucene-queries-$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.queries.function.TestFromDoubleValuesSourceRange
- Expected vs actual
- Expected 0 hits; actual 1 hit (
expected:<0> but was:<1>), with -ea and -da.
More files, environment, full output34 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:<0> but was:<1>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:647)
org.junit.Assert.assertEquals(Assert.java:633)
org.apache.lucene.queries.function.TestFromDoubleValuesSourceRange.testRangeComparesDoubles(TestFromDoubleValuesSourceRange.java:43)- Over the same data,
DoubleFieldSource,LongFieldSourceandFunctionMatchQueryreturn the exact hits; only the bridge differs. - The bridge's own
doubleValis exact; the typed double, long and int values override the float default, whose comment says ranges need "different behavior depending on the type of fields". - Nothing in Lucene documents or tests float matching for the bridge.
The full argument242 words
- Two public paths over the same data disagree.
FunctionRangeQueryoverDoubleFieldSource("d")andFunctionMatchQuery(DoubleValuesSource.fromDoubleField("d"), v -> v >= 1.0 && v <= 1.0)return {1.0}; overfromDoubleValuesSource(DoubleValuesSource.fromDoubleField("d"))the same range returns three documents. - The values are doubles.
doubleValis exact (ValueSource.java:316-322) andfloatValis a narrowing of it (:308-314). The comment above the default says a range "needs different behavior depending on the type of fields" (FunctionValues.java:228-229); theValueFillerjavadoc asks implementations to define their own "if their value is not a float" (:137-140). - The typed double, long and int values override it.
DoubleDocValues,LongDocValuesandIntDocValuesdefine their owngetRangeScorer, as doDocTermsIndexDocValues(over term ordinals) andEnumFieldSource's values;FloatDocValues(whose values are floats),BoolDocValuesandStrDocValueskeep the float default. LUCENE-5247 ("expressions should use DoubleDocValues not FunctionValues directly",9db7442d648, 2013, by rmuir) moved the typed range scorers intoInt/Long/DoubleDocValues; its motivation was anUnsupportedOperationException, so it is the pattern, not a precision decision. - Nothing in Lucene documents float semantics for the bridge.
fromDoubleValuesSourcehas no javadoc;FunctionRangeQuery"matches docs in which the values in the value source match a configured range" (FunctionRangeQuery.java:32-33); upstream tests use the bridge only for scores, sorting and the round trip; no CHANGES or MIGRATE entry mentions range precision. Downstream reliance on today's float matching cannot be ruled out. The bridge is maintained API: #14654 and #15406 (2025) fixed other methods.
The devil's-advocate check232 words
The strongest works-as-designed case (built by the intended-behaviour devil's advocate and a review by a model from another family, OpenAI GPT-5.6): FunctionValues is float-centric by heritage (the default parses floats, objectVal "by default return[s] a Float()", FunctionRangeQuery says "The score is the float value" and is @lucene.experimental); the adapter began in the LUCENE-7737 patch as a Solr-side helper that wrapped spatial distance sources for sorting and scoring, and that discussion called ValueSource "being phased out (or moved to Solr)"; no Lucene production code calls the bridge; FunctionMatchQuery is the modern exact alternative; and SOLR-6562 (2014, resolved Invalid) called float precision in Solr's sum/sub function math working as designed.
Why it fails: float scores do not imply float filtering; the bridged value is an exact double and only the range test narrows it (in SOLR-6562 the values themselves were computed in float); the typed double and long sources compare in their own type; nothing in Lucene documents the float behaviour; and results such as [* TO 1e300] matching 1e301, or an inclusive point query missing the document with exactly that value, are not "values between the specified range". A maintainer may still answer "use FunctionMatchQuery", or prefer the narrow fix B or a documented limitation; the reply is that this public adapter silently returns wrong hits and the fix is small and leaves the range test's per-document cost unchanged.
Recommended Return a DoubleDocValues from FromDoubleValuesSource.getValues
Build the bridged values on DoubleDocValues, as DoubleFieldSource does (ValueSource.java, +3/−9): a double range test, with floatVal derived from doubleVal. Validated: the repro, a new regression test, the lucene/queries module (517 tests), lint and format pass. Side effects: objectVal becomes Double; Float bounds over a bridged float field change (0.3f stops matching 0.3f).
import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.queries.function.docvalues.DoubleDocValues;
...
- return new FunctionValues() {
+ // DoubleDocValues compares ranges in double and derives floatVal etc. from doubleVal
+ return new DoubleDocValues(this) {
...
- @Override
- public float floatVal(int doc) throws IOException {
- if (inner.advanceExact(doc) == false) {
- return 0;
- }
- return (float) inner.doubleValue();
- }All options and trade-offs4 options · 714 words
Two fixes were applied in a scratch worktree at a9779ef3c193 and run through gradle (seed DEADBEEF), with the repro and a regression test in TestFunctionRangeQuery; both pass. A is recommended; B is the narrow variant.
Return a DoubleDocValues from FromDoubleValuesSource.getValues (validated by the checks) (recommended)
ValueSource.java only (+3/−9): import DoubleDocValues, replace new FunctionValues() with new DoubleDocValues(this) and drop the hand-written floatVal, which DoubleDocValues derives as (float) doubleVal. The range test is then DoubleDocValues.getRangeScorer (Double.parseDouble, doubleVal), as for DoubleFieldSource and, since LUCENE-5247, expressions.
import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.queries.function.docvalues.DoubleDocValues;
...
- return new FunctionValues() {
+ // DoubleDocValues compares ranges in double and derives floatVal etc. from doubleVal
+ return new DoubleDocValues(this) {
...
- @Override
- public float floatVal(int doc) throws IOException {
- if (inner.advanceExact(doc) == false) {
- return 0;
- }
- return (float) inner.doubleValue();
- }Trade-offs
- Scores,
explain,toString(doc)andexistsdo not change (floatValstays(float) doubleVal, 0 when missing); the range test does the same work per document as today (exists, then one value read). Floatbounds over a float-valued source change (also with B):FunctionRangeQuerykeeps 0.3f as"0.3", so over aFloatDocValuesFieldholding 0.3f, bridged withDoubleValuesSource.fromFloatField,[* TO 0.3f]and[0.3f TO 0.3f]match it today (asFloatFieldSourcedoes) and not with the fix: the value is(double) 0.3f= 0.30000001192092896 >Double.parseDouble("0.3")(executed).FloatFieldSourcestays the float-exact path. Name it in CHANGES.- The other projections change from the
FunctionValuesdefaults toDoubleDocValues:objectValreturns aDouble(nullwhen missing) instead of aFloat;getValueFillerfills aMutableValueDoublewithexists;byteVal,shortVal,intValandlongValreturn casts,strValreturnsDouble.toString(doubleVal)andboolValdoubleVal != 0, where today they throwUnsupportedOperationException;bytesVal, which callsstrVal, starts working; the values become aninstanceof DoubleDocValues. A downstream that printsobjectValcan see it. - Cost:
objectValand theValueFillernow callexistsanddoubleVal(advanceExacttwice for a document with a value) where today they callfloatValonce. - Unchanged (read): documents without a value and NaN values never match; a missing exclusive bound still excludes an infinite value.
ValueSource.javastarts importing fromqueries.function.docvalues, which already imports fromqueries.function.
What the checks validated
- Stock
a9779ef3c193: the repro fails 4 of 4 and the regression test fails (14 tests, 5 failed), with assertions on and off. - Fix: 14 of 14 pass, assertions on and off;
:lucene:queries:testpasses, 517 tests, 5 skipped (the 512 existing tests plus the 5 new ones);ecjLint,checkGoogleJavaFormatandforbiddenApispass. - Classpath runner with the fix shadowed: the repro passes 4 of 4. Earlier in the hunt the same change passed every verifier's test, the history probe and the queries module.
The change is +22 lines in the anonymous class, from the hunt's independent reproducer: parse both bounds with Double.parseDouble, keep null as ±infinity, and compare doubleVal(doc) after exists(doc).
Trade-offs
The same Float-bound change as A (executed: 0.3f over a bridged float field no longer matches [* TO 0.3f]); no objectVal, ValueFiller or accessor change; the cost is a second copy of the comparison DoubleDocValues already has.
What the checks validated
- The repro passes 4 of 4 and
TestFunctionRangeQuery10 of 10, assertions on and off;:lucene:queries:testpasses, 517 tests, 5 skipped;ecjLint,checkGoogleJavaFormatandforbiddenApispass. - Classpath runner with the reproducer's identical override shadowed: the repro passes 4 of 4.
regression-test.diff adds testRangeOverDoubleValuesSource: three documents with a double and a long value; [1.0 TO 1.0] must count 1, [* TO 1e300] 2, [* TO 1700000000000] over the long field 1. fix-with-test.diff is A plus this test.
Trade-offs
No upstream test range-filters a bridged source today. On stock main the test stops at its first assertion; it has no midpoint, exclusive-bound, missing-value or Float-bound case (the repro and the reproducer's tests cover the first three).
Change FunctionValues.getRangeScorer itself.
Trade-offs
FunctionValues.doubleVal throws UnsupportedOperationException by default (FunctionValues.java:60-62), so every float-only implementation that relies on the default would break.
By code reading only, Solr's {!frange} reaches this path for configured expression functions, geodist() on spatial-strategy fields and a few other wrappers. As a scale comparison, the geodist() float window (±0.24 mm at 5 km) is far smaller than LatLonPoint's 4.66 mm latitude step, so expressions over date or large-id fields look like the realistic case.
A bounded-exhaustive cross-path check ran about 30 range, exact and set query paths over every bound combination of a type-limit value alphabet on the real jars, against an exact reference. The bridged FunctionRangeQuery was the only unexplained mismatch class on double, int and long fields; a Z3 floating-point model then characterised the window.
The method in full189 words
Method: bounded-exhaustive cross-path translation validation on the stock jars (semi-formal tier), in a hunt unit on range-bound handling across query paths.
- The harness built every (lower, upper, includeLower, includeUpper) over a type-limit value alphabet, ran each through about 30 range, exact and set query paths and compared each hit set with an exact Java reference: 983,164 double, 268,692 int and 483,557 long checks; the float sweep (729,052 checks) had no unexplained class.
- The bridged path was added to the sweep after code reading showed that
FromDoubleValuesSourcehas no range override. It left 5,997 double, 606 int and 1,528 long mismatches; every other mismatch was attributed to a registered or documented class. - A Z3 floating-point model characterised when the float and double tests disagree. Its one assumption,
parseFloat(Double.toString(b)) == (float) b, fails at exact float midpoints, which the independent reproducer then executed in both directions. - The class was reduced to a public-API JUnit (found 2026-10-26 on main
ea57360912ed), then checked by an independent reproducer, an intended-behaviour devil's advocate, reviews by a model from another family (OpenAI GPT-5.6), an adjudication, a history and prior-report follow-up and a credibility audit.
No prior report found (2026-10-26) in GitHub issues, PRs and commits, LUCENE and SOLR JIRA, five mailing lists, downstream repositories and the web, nor in the changed files of all 411 open PRs. Closest: #9720, the same float default throwing for a VectorValueSource.
Searches3 searches
Verdict new: no report by anyone. Judgment call: SOLR-6562 (2014, Invalid) called float precision of Solr's function math working as designed; there the values are computed in float, here an exact double is narrowed only for the range test, so the row stays "new" (counting it as the same discussion would make it "known"). Keyword search cannot prove absence; one web query was throttled, Solr's GitHub issues are disabled (its JIRA was searched) and private reports are invisible.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits; Elasticsearch, OpenSearch, Solr; dev@, java-user@, issues@, commits@; JIRA; the web | 2026-10-26 | two runs of the hunt's prior-report tool, 84 queries | no report; related only: #9720, #6311 (LUCENE-5247), #8788 (LUCENE-7737, the introducing issue) |
| GitHub issues and code (apache/lucene, apache/solr, Elasticsearch, OpenSearch); JIRA (LUCENE, SOLR); five lists | 2026-10-26 | 16 issue searches, 4 code searches, 25 JQL queries, 29 list queries, 7 web searches | background only: SOLR-6562, LUCENE-1649 (#2723), SOLR-8867, LUCENE-6727 (#7785), #14654, #15406 |
| all 411 open apache/lucene PRs; upstream history of the four files | 2026-10-26 | changed-file lists; commit history | #14775 (ValueSource.java) and #16847 (ValueSourceScorer.java) are unrelated; no fix upstream |
Born with the bridge in LUCENE-7737 (2f2e00ffe23, committed 2017-06-27, first released in 7.0.0), without a range override; never fixed. Executed: 7.0.0 to 10.5.2 (10 releases) and main fail; 6.6.x has no such API. Since 10.4.0 a round-tripped typed ValueSource is unwrapped and exact.
Versions tested7 rows
Introduced by LUCENE-7737, "Remove spatial-extras dependency on queries module" (2f2e00ffe23, authored by romseygeek on 2017-06-13, committed 2017-06-27; a JIRA patch reviewed by dsmiley and jpountz, migrated as #8788). It added fromDoubleValuesSource and FromDoubleValuesSource with an exact doubleVal, a narrowing floatVal and no getRangeScorer; the discussion (21 comments) never mentions range queries or precision. Later commits on the class (LUCENE-7736, LUCENE-8741, LUCENE-9394 warning fixes, LUCENE-9570 reformatting, #14654 getSortField, #15406 round trip) do not touch range scoring. Not a regression.
For expressions users there is earlier context: up to 6.3 Expression.getValueSource gave an exact double range test (LUCENE-5247); LUCENE-7609 removed it in 6.4.0, and since 7.0.0 the float bridge is the only route. No commit between a9779ef3c193 and upstream main 20ced71c1e6 (2026-10-27) touches the function package.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | the compact JUnit fails 4 of 4 through gradle (assertions on and off) and the classpath runner (-ea, -da) |
| 10.5.2, 9.12.3 | affected | the compact JUnit fails 4 of 4 (10.5.2 also with -da) |
| 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 10.0.0, 10.3.2, 10.4.0 | affected | main() probe: identical mismatches on every version; the typed controls pass |
| 10.4.0 and later, typed ValueSource round trip | narrowed by #15406 | fromDoubleValuesSource(vs.asDoubleValuesSource()) returns vs, so that case is exact; 7.0.0 and 10.3.2 fail it |
| 6.6.0, 6.6.6 | no API | compile error on fromDoubleValuesSource only |
| 6.3.0 expressions | older API | Expression.getValueSource: [1.0 TO 1.0] matches only 1.0 |
fix (scratch worktree at a9779ef3c193) | not affected | both fixes: the repro and the regression test pass; lucene/queries passes |
An independent reproducer and an intended-behaviour devil's advocate reproduced it with their own tests, controls passing; a review by a model from another family (OpenAI GPT-5.6) rated it real; an adjudication and a credibility audit re-ran it; this doc's gradle runs reproduce it on a9779ef3c193 and validate two fixes.
Checks and verdicts296 words
Every executing check wrote or re-ran a JUnit on stock main; every control passed and every fix check passed. The cross-model reviews were reading-only (separate runs re-ran the repro).
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Cross-path sweep, then a public-API JUnit: fails 2 of 2 on main (-ea, -da), 10.5.2 and 9.12.3; passes with the fix; the queries module passes with it. |
| cross-model review, refute lens (OpenAI GPT-5.6) | not real, low (procedural) | Could not run anything, and the review votes not real without a run of its own; its reading agrees with the root cause. A separate run re-ran the repro: fails 2 of 2. |
| independent reproducer | real | Own JUnit from outside org.apache.lucene: fails 9 of 10 on main and 10.5.2 (-ea, -da); found the midpoint flips and the epoch-ms window; its own minimal fix passes 10 of 10. |
| cross-model review, intended lens (OpenAI GPT-5.6) | real, medium | Intended-behaviour argument moderate; root cause, fix and in-contract inputs agreed. |
| intended-behaviour devil's advocate | real, high (not intended); medium on impact | FunctionMatchQuery over the same source as an in-Lucene oracle: the bridged range disagrees (fails 2 of 3 on main and 10.5.2, passes with the fix). |
| adjudication | confirmed, high | Re-ran the reproducer's test (fails 9 of 10, -ea and -da), the finder's (2 of 2) and the fix check (passes); listed the corrections. |
| Follow-up and credibility audit | new; not security-sensitive; keep | History runs on 11 versions, prior-report searches, a security screen, fresh-seed re-runs. |
| this doc | reproduced; two fixes validated | Compact JUnit fails through gradle and the classpath runner on main and two releases; a window probe measures 65 ids and 131,071 ms; both fixes pass the repro, the regression test and the queries module. |
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 476 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,524 words
- Registry title
- FunctionRangeQuery over ValueSource.fromDoubleValuesSource(...) compares bounds and values as floats, not doubles, so double and long ranges silently gain or lose hits near a bound and beyond Float.MAX_VALUE
- Area
- queries / function queries (the ValueSource.fromDoubleValuesSource bridge: its FunctionValues inherits the float default FunctionValues.getRangeScorer that FunctionRangeQuery calls)
- Priority
- Medium, rank 252: FunctionRangeQuery over a bridged DoubleValuesSource compares in float and so silently adds or drops hits on ordinary id-scale and timestamp-scale longs, but the API pair is rare with no in-tree caller, so it closes the narrow deterministic silent rows just above the loud LFV-100.
- Impact class
- silent wrong results
- Impact note
- Applications that filter with lucene/queries FunctionRangeQuery (or another FunctionValues.getRangeScorer caller) over ValueSource.fromDoubleValuesSource(...) of a DoubleValuesSource that did not start as a ValueSource (a double or long doc-values field via DoubleValuesSource.fromDoubleField/fromLongField, LongValuesSource.toDoubleValuesSource, or an expressions-module source, the only ValueSource route for expressions since 7.0) get wrong hit sets with no error for values that differ from a bound but round to the same float (a relative window of about 2^-24: about 64 ids near 1e9, about -27 s to +104 s near today's epoch milliseconds; integers below 2^24 and float-exact bounds are unaffected) and for values or bounds beyond +-Float.MAX_VALUE; default IndexWriterConfig, codec and searcher, one document suffices, every release from 7.0.0 to 10.5.2 and main; no Lucene production code calls the bridge, and since 10.4.0 (#15406) a round-tripped typed ValueSource is unwrapped and therefore exact.
- Summary
- You index DoubleDocValuesField("d") values 0.9999999999999999, 1.0 and 1.0000000000000002 and search new FunctionRangeQuery(ValueSource.fromDoubleValuesSource(DoubleValuesSource.fromDoubleField("d")), 1.0, 1.0, true, true), expecting only the document whose value is 1.0, as you get with new DoubleFieldSource("d") or with FunctionMatchQuery over the same DoubleValuesSource. Instead all three match, because the bridged FunctionValues keeps the exact double in doubleVal but does not override getRangeScorer, so it inherits the FunctionValues default that parses both bounds with Float.parseFloat and compares the value cast to float. Likewise {1.0 TO ] misses 1.0000000000000002, [ TO 1e300] matches 1e301, [16777217 TO 16777217] on a long field also matches 16777216, and [* TO 1700000000000] over epoch milliseconds also matches 1700000060000, all without an error.
- Introduced
- 2f2e00ffe235ba590f43bf07dabd4201e6da53c6 (LUCENE-7737 'Remove spatial-extras dependency on queries module', a JIRA patch by romseygeek with reviewers dsmiley and jpountz, migrated as apache/lucene#8788; no pull request) [read: git log -S, git log -L and git blame in the full-history clone; the commit adds fromDoubleValuesSource and FromDoubleValuesSource with an exact doubleVal, floatVal = (float) value and no getRangeScorer override, and none of the later commits on the class (LUCENE-7736 2a4dd499bbd, LUCENE-8741 1c46dd1f9b1, LUCENE-9394/LUCENE-9570 formatting, #14654 b0f992369ba getSortField, #15406 ba277acb0f3 round trip) touches range scoring; exec: 6.6.0 and 6.6.6 lack the API (compile error on that symbol only), 7.0.0 fails]. Present in the code as first written, not a regression, so there is nothing earlier to bisect (2017-06-27 (committed to master; authored 2017-06-13))
- First affected version
- 7.0.0 (tagged 2017-09-18) [exec: the history probe fails]; 6.6.0 (the last release before the commit) and 6.6.6 (the last 6.x release) lack the API [exec: compile error on fromDoubleValuesSource only]. the hunt's record executed back to 8.0.0 and put the origin at 7.0 by reading; the history stage executed 7.0.0
- Affected versions
- Every release from 7.0.0 to 10.5.2 and main ea57360912ed; not fixed upstream (the newest upstream change to ValueSource.java is ba277acb0f3, #15406, which does not touch range scoring; no open PR touches the fix sites; read 2026-10-26). [exec] The history probe FromDvsRangeHistoryProbe (public API, main(), bridged vs typed vs exact reference) fails on 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.3.2, 10.4.0, 10.5.2 and main with identical output and passes on main with the fix; the finder repro fails on 9.12.3, 10.5.2 and main (-ea and -da); the independent reproduction's test fails on 9.12.3, 10.5.2 (-ea and -da) and main (-ea and -da); the intended-behaviour check's cross-check fails on 10.5.2 and main; an expressions-module source fails on 7.0.0, 10.5.2 and main. [read] 7.0.0-8.8.1 and 9.0.0-10.5.2 by git tag --contains plus the same code shape at every tag; 8.8.2 and 8.9.0-8.11.3 (released from the separate 8.x branch, not tagged in the history clone) inferred between 8.8.1 and the executed 8.11.4. Trigger narrowed in 10.4.0 by #15406: fromDoubleValuesSource(vs.asDoubleValuesSource()) returns vs, so the typed-ValueSource round trip is exact on 10.4.0 and later [exec: 7.0.0 and 10.3.2 fail, 10.4.0, 10.5.2 and main pass]; sources that did not start as a ValueSource stay affected [exec]. Old releases ran on JDK 25 with SHA-1-verified release jars
- 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, Z3
- Upstream and prior reports
- none found (2026-10-26, the finder's searches, read-only; the prior-report record verdict new: 84 scripted prior-report search queries in two runs (apache/lucene issues, PRs and commits; Elasticsearch, OpenSearch and Solr; the dev@, java-user@, issues@ and commits@ lists; JIRA; web), 16 manual GitHub issue searches (apache/lucene, apache/solr, Elasticsearch, OpenSearch), 4 GitHub code searches, 25 manual JIRA queries (LUCENE and SOLR), 29 manual list queries over five lists, 7 web searches, the upstream commit history of the four touched files and the changed files of all 411 open apache/lucene PRs; 26 hits read and classified, none the same bug; the intended-behaviour check's and the finder's searches found nothing either). Not fixed upstream: the newest upstream change to ValueSource.java is ba277acb0f3 (#15406, 2025-12-02), which does not touch range scoring; no open PR touches FunctionValues.java, FunctionRangeQuery.java or DoubleDocValues.java, and the two open PRs touching ValueSource.java (#14775) and ValueSourceScorer.java (#16847) are unrelated. Closest items, not reports: #9720 (LUCENE-8674, open: the same float default getRangeScorer throws UnsupportedOperationException for a VectorValueSource; different trigger, symptom and fix site); SOLR-6562 (2014, resolved Invalid: float precision of Solr's sum/sub function math called working as designed; there the values are computed in float, here the value is an exact double and only the range test drops to float); #2723 / LUCENE-1649 (closed Not A Problem in 2011: an int-to-float cast in the pre-4.0 function API, code since removed); #6311 / LUCENE-5247 (2013 precedent: getRangeScorer moved into Int/Long/DoubleDocValues and expression sources made to extend DoubleDocValues, motivated by an UnsupportedOperationException); #8788 (the introducing issue LUCENE-7737; no range or precision discussion); #15406 and #14654 (same class, other methods)
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26): the compact JUnit and a regression test added toTestFunctionRangeQuery, through gradle in a clean worktree, stock and with each of two fixes, assertions on and off, plus the wholelucene/queriesmodule and its lint and format checks with each fix (2026-10-28); through the hunt's classpath runner, the compact JUnit on main (-ea,-da, both fixes shadowed), 10.5.2 (-ea,-da) and 9.12.3, and a window-size probe on main. The hunt's checks ran its own repros on mainea57360912ed(the four files involved are identical) and amain()probe on 11 versions from 7.0.0 to main.
- Finding record
lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:267-271 (at ea57360912ed) (fromDoubleValuesSource: a WrappedDoubleValuesSource is unwrapped, anything else gets new FromDoubleValuesSource(in)), :274-354 (private class FromDoubleValuesSource, no javadoc on the factory), :289-329 (getValues), :302 (return new FunctionValues() {...}), :309-314 (floatVal returns (float) inner.doubleValue()), :317-322 (doubleVal returns the exact inner.doubleValue()), :325-327 (exists = inner.advanceExact(doc)); no getRangeScorer anywhere in the filelucene/queries/src/java/org/apache/lucene/queries/function/FunctionValues.java:224-293 (at ea57360912ed) (default getRangeScorer: javadoc 'matches documents with values between the specified range' at :225, comment 'needs different behavior depending on the type of fields' at :228-229, Float.parseFloat bounds at :245 and :250, floatVal comparisons, exists() checked first at :260, :269, :278, :287), :101 (objectVal 'by default return a Float()'), :138-140 (ValueFiller: implementations are encouraged to define their own if their value is not a float)lucene/queries/src/java/org/apache/lucene/queries/function/docvalues/DoubleDocValues.java:86-147 (at ea57360912ed) (the double getRangeScorer the same field gets through DoubleFieldSource: Double.parseDouble at :98 and :104, doubleVal comparisons)lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java:31-42 (at ea57360912ed) (javadoc 'matches docs in which the values in the value source match a configured range. The score is the float value.', @lucene.experimental at :42), :172 (calls functionValues.getRangeScorer; the only production caller, git grep); git grep: fromDoubleValuesSource has no production caller in lucene/*/src/java
- The finding in 30 seconds
lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:267-272, 288-329 (302, 308-314, 316-322, 324-327); no getRangeScorer in the filelucene/queries/src/java/org/apache/lucene/queries/function/FunctionValues.java:224-293 (245, 250, 261, 270, 279, 288); lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java:48-49, 55-67, 170-172lucene/queries/src/java/org/apache/lucene/queries/function/docvalues/DoubleDocValues.java:86-147; lucene/queries/src/java/org/apache/lucene/queries/function/docvalues/LongDocValues.java:90-125; lucene/queries/src/java/org/apache/lucene/queries/function/FunctionMatchQuery.java:92-100 (predicate at 98-99)
- Who is affected
lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java:42; lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:267-272 (git grep: no production caller)
- Why it looks unintended
lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:267, 308-322; lucene/queries/src/java/org/apache/lucene/queries/function/FunctionValues.java:101, 137-140, 225-229; lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java:31-44a Lucene main checkout: git grep getRangeScorer over lucene/*/src/java (overrides in DoubleDocValues, LongDocValues, IntDocValues, DocTermsIndexDocValues, EnumFieldSource; none in FloatDocValues, BoolDocValues, StrDocValues)
- Suggested fix
lucene/queries/src/java/org/apache/lucene/queries/function/docvalues/DoubleDocValues.java:38-84, 86-147, 149-165; lucene/queries/src/java/org/apache/lucene/queries/function/FunctionValues.java:40-71, 89-97, 99-102, 155-169, 232-293
- TestFromDoubleValuesSourceRange.java (JUnit test):
repro/LFV-409/src/org/apache/lucene/queries/function/TestFromDoubleValuesSourceRange.java(47 lines)
Made in parallel with this doc; the values on screen are the repro's (main a9779ef3c193).
assets/LFV-409/bug.png(476 KB)
