All findings · Lucene finding · LFV-406 · functionrangequery-negative-scores

FunctionRangeQuery scores raw negative values, so a minimumShouldMatch query can drop a document whose value is -Infinity

FunctionRangeQuery scores each match with its raw value, so negative values give negative scores, although Lucene scores must not be negative since 8.0. A matching document whose value is -Infinity then disappears from minimumShouldMatch top-k results: 2 documents match, search(q, 10) returns 1.

Summary

Priority
Low, rank 716FunctionRangeQuery scores with raw negative values despite LUCENE-7996, and it loses a hit only for a stored -Infinity under minimumShouldMatch >= 2, the same negative-score bound profile as LFV-203.
Impact
silent wrong results
Since
LUCENE-7996 (8.0.0, 2017); hit loss since 10.1.0introduced 2017-12-06 · about 8 years
Affected
8.0.0 to 10.6.0 and main; hit loss 10.1.0 on (the minimal repro 10.5.0 on)
Status
documented
Found by
non-formal: cross-path checks under the asserting test searcher, then a public-API JUnit

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

FunctionRangeQuery scores a match with its raw value, so -Infinity scores -3.4e38. Top-k search of a "match at least 2 of these" query checks each document's score against a threshold that starts at 0, and drops this one.

Background

  1. FunctionRangeQuery. It matches the documents whose value (here from the numeric field d) lies in a range, and scores each match with that value. An open range, null to null, matches every document that has a value, -Infinity included.
  2. No negative scores. Since Lucene 8.0 (LUCENE-7996) a query must never produce a negative score. FunctionQuery, which scores every document with the same kind of value, returns 0 for a negative value.
  3. minimumShouldMatch and WANDScorer. For top-k search of a BooleanQuery of optional clauses that needs at least 2 of them, as here, Lucene uses WANDScorer. It skips documents that cannot beat the lowest score still worth collecting, a threshold that starts at 0 and relies on scores never being negative.
Diagram of the bug: doc 0 (d = -Infinity, body a) and doc 1 (d = 1.0, body b) each match two of the three SHOULD clauses of a minimumShouldMatch = 2 query, so count(q) is 2. FunctionRangeQuery scores doc 0 with its raw value, -3.4028235E38, where FunctionQuery would give 0. WANDScorer compares each document's score with the minimum competitive score, which starts at 0, and drops doc 0, so search(q, 10) returns 1 hit and totalHits says exactly 1. With the clamp at 0, doc 0 scores 0.315 and both documents are returned.
The two-document repro: both documents match two clauses, FunctionRangeQuery scores doc 0 at -3.4e38, the "at least 2 of" top-k check against the threshold 0 drops it, and with the clamp doc 0 scores 0.315 and is returned. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Index two documents: doc 0 with d = -Infinity (a DoubleDocValuesField) and body: a, doc 1 with d = 1.0 and body: b. Search "at least 2 of" FunctionRangeQuery(d, open range), body:a and body:b with IndexSearcher.search(q, 10).
  2. You expect 2 hits: each document matches two of the three clauses, and count(q) says 2. Scores must not be negative since Lucene 8.0; FunctionQuery over the same field scores doc 0 as 0.
  3. Instead you get 1 hit, doc 1 (score 1.315), and totalHits "1 hits", an exact count. No error with assertions off; with assertions on, WANDScorer throws AssertionError.
  4. Why: FunctionRangeQuery scores doc 0 with its raw value, -Infinity, mapped to -3.4e38. For "at least 2 of" top-k, WANDScorer rejects a document whose score is below the lowest score worth collecting, which starts at 0. A value such as -5 rounds to 0 in its scaled arithmetic and survives; -3.4e38 does not.
  5. Clamping the score at 0, as FunctionQuery does, fixes it: 2 hits, doc 0 scores 0.315 (its body:a score). Negative scores exist since 8.0.0; the lost hit since 10.1.0, and this two-document shape since 10.5.0.

Who is affected

Applications that rank with FunctionRangeQuery as a scoring clause over values that can be negative get negative scores (since 8.0.0). Hits are lost (10.1.0 on) only with a stored -Infinity or a summed score at or below about -4e31, a range that admits it, and the query as a SHOULD clause of a minimumNumberShouldMatch >= 2 query that reaches WANDScorer.

Mechanism, terms and numbers779 words

FunctionRangeQuery (lucene/queries) matches documents whose function value lies in a range and scores each match with the value itself: ValueSourceScorer.score() returns floatVal(doc) and maps NaN and -Infinity to -Float.MAX_VALUE. LUCENE-7996 (8.0.0) made negative scores illegal and changed FunctionQuery's identical code to return 0, but did not touch ValueSourceScorer, whose comment still says "(same as FunctionQuery, but no qWeight)". So negative values give negative scores in TopDocs and explain. Since 10.1.0 it also loses hits: top-k search of a pure-SHOULD BooleanQuery with minimumNumberShouldMatch >= 2 runs on WANDScorer, which rejects a candidate whose scaled score is below the minimum competitive score of 0. An ordinary negative score rounds to 0 there and survives; -Float.MAX_VALUE does not, so a matching document whose value is -Infinity is missing from search(q, n), whose total (exact here, with fewer than 1,000 matches) is too low while count(q) is right. Two documents and the default IndexWriterConfig, codec and IndexSearcher are enough. A one-line clamp fixes it.

The code (main 645d4fe14a20)

  1. The score is the raw value. ValueSourceScorer.java:84-92: float score = values.floatVal(disi.docID()); then return score > Float.NEGATIVE_INFINITY ? score : -Float.MAX_VALUE; with the comments "(same as FunctionQuery, but no qWeight)" and "Current Lucene priority queues can't handle NaN and -Infinity, so map to -Float.MAX_VALUE". getMaxScore returns Float.POSITIVE_INFINITY (:94-97).
  2. FunctionQuery clamps. FunctionQuery.java:130-137: if (val >= 0 == false) { // this covers NaN as well ... returns 0 (:132).
  3. Matching is separate and correct. FunctionRangeQuery.java:168-174 builds the scorer with FunctionValues.getRangeScorer; for a double field that is DoubleDocValues.java:87-147, where an open lower bound becomes Double.NEGATIVE_INFINITY (:95-96) and the match test is docVal >= l && docVal <= u (:116). So -Infinity matches, NaN never does.
  4. Routing. For this pure-SHOULD query under ScoreMode.TOP_SCORES with minShouldMatch > 1, BooleanScorerSupplier.java:315-322 declines the bulk scorers ("Fall back to BS2/WANDScorer") and :589-590 builds a WANDScorer. IndexSearcher.search(q, n) is TOP_SCORES (IndexSearcher.java:100, :599; TopScoreDocCollector.java:58).
  5. Scaling. WANDScorer compares scores as scaled longs. The scale comes from the sum of the clauses' max scores (WANDScorer.java:186-193); the range clause's +Infinity gives scalingFactor(+Infinity) = scalingFactor(MAX_VALUE) - 1 (:66, :74-75), that is 2^-105, and scaled scores are rounded up (:104).
  6. The check. WANDScorer.java:333-344 scales the summed score of the clauses that match the candidate and loops while (scaledLeadScore < minCompetitiveScore || freq < minShouldMatch), returning false once scaledLeadScore + tailMaxScore < minCompetitiveScore; the threshold starts at 0 (:163). With assertions on, scaleMaxScore trips assert maxScore >= 0 (:92) first.

The numbers

Doc 0 scores -3.4028235E38 + 0.31506687 (body:a). Scaled by 2^-105 that is about -8.4 million, far below 0, so WANDScorer rejects it. A score of -5 scales to about -1e-31, which rounds up to 0, so it survives with assertions off: the -5.0 control document is returned in every run that completes, 30 of 33 (score -4.837875 in the four-document check on 10.5.0); the three stock runs with assertions on throw at WANDScorer.java:92 first and return nothing. The boundary is a summed (lead) score of about -2^105 (about -4.06e31): a finite value of -1e32 is dropped and -1e31 is kept.

An exhaustive search, TopScoreDocCollectorManager(10, Integer.MAX_VALUE) (COMPLETE, TopScoreDocCollector.java:58-59), returns both documents: doc 1 = 1.3150668, doc 0 = -3.4028235E38. On this two-document index it does not run on WANDScorer: with minShouldMatch > 1 the cost threshold is maxDoc / 3 = 0, which the clauses meet (BooleanScorerSupplier.java:212-230), and optionalBulkScorer() declines only under TOP_SCORES (:315-322), so it returns a BooleanScorer (:331-337), which has no minimum-competitive-score check (an executed probe prints BooleanScorer for COMPLETE and WANDScorer for TOP_SCORES). A COMPLETE search that does reach WANDScorer also skips the check (WANDScorer.java:194-196, :334). explain gives doc 0 (abridged) "-3.4028235E38 = sum of: -3.4028235E38 = frange(double(d)):[* TO *] (-Infinity = double(d)=-Infinity), 0.31506687 = weight(body:a in 0)". FunctionQuery(d) alone scores doc 0 as 0.0.

At scale: 20,000 documents in 8 segments, 574 with value -Infinity; the same query shape returned 11,532 of 11,893 matches, reported as exactly "11532 hits", and searchAfter paging skipped the same 361 documents, all valued -Infinity (main and 10.5.2).

The negative score on its own

One document with d = -5.0 and FunctionRangeQuery(d, [-10, 10]): the score is -5.0 in TopDocs and explain, where FunctionQuery(d) scores 0.0. In a disjunction, a document that matches tag:x and the range clause scores -4.9171267, below its tag:x-only score 0.082873434. LuceneTestCase.newSearcher rejects the score (AssertingScorer.java:131), and on 8.0.0 to 10.0.0 a top-k search with assertions on throws in core TopScoreDocCollector.

Exposure and likelihood366 words
  • Who. Users of FunctionRangeQuery (an @lucene.experimental class) as a scoring clause, not in a FILTER clause or a ConstantScoreQuery, over a value source that can be negative. Solr's frange uses its own constant-scoring query; Elasticsearch and OpenSearch do not reference FunctionRangeQuery, getRangeScorer or ValueSourceScorer (read-only code search, 2026-10-26).
  • Negative scores (8.0.0 on). Negative scores in TopDocs and explain; matching the range clause can lower a document's score below that of a document that matches only the other clause; Lucene's own asserting test searcher rejects the score, so downstream test suites that use it can fail; on 8.0.0 to 10.0.0 any top-k search with assertions on throws in core TopScoreDocCollector.
  • Lost hits (10.1.0 on). Silent wrong results: the -Infinity document is missing from top-k and from searchAfter paging, and an exact totalHits is too low while count() is right. No exception with assertions off. On 10.1.0 to 10.4.0 only sparse SHOULD clauses or a query nested in a conjunction reach WANDScorer; since 10.5.0 dense top-level queries such as the two-document repro do too.
  • What it takes. A stored -Infinity (for example the log of 0), or a finite value low enough that the document's summed score is at or below about -2^105 (about -4e31; in the tested shapes a field value of about that size); a range that admits it (an open or -Infinity lower bound; a lower bound such as -10 excludes -Infinity); and the scored query as a SHOULD clause of a minimumNumberShouldMatch >= 2 query that reaches WANDScorer (shown for the pure-SHOULD, sparse and nested shapes). NaN cannot trigger it: it fails every range test.
  • What it does not do. Which documents match never changes: count(), FILTER, MUST_NOT and ConstantScoreQuery uses were correct in all 17 probe shapes, and in the probe every omitted result was a document valued -Infinity; no document with a non-negative score was omitted. A -Infinity document scores about -3.4e38 (near the lowest finite float), so it would have ranked at the end; with the default 1,000-hit counting threshold it shows in short result lists, in an exact total, or when paging to the end.
  • How likely: contrived. Three unusual conditions must hold at once.

Minimal repro

One JUnit test, two documents, default codec; indexing, query and search use public production APIs (the test uses Lucene's test framework): doc 0 has d = -Infinity and body:a, doc 1 has d = 1.0 and body:b; the query needs 2 of FunctionRangeQuery(d, open range), body:a, body:b, and search(q, 10) must return all count(q) matches.

TestFunctionRangeQueryNegativeScoreHitLoss.java (one test)73 lines
/*
 * 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.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.queries.function.valuesource.DoubleFieldSource;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;

public class TestFunctionRangeQueryNegativeScoreHitLoss extends LuceneTestCase {

  public void testMinShouldMatchKeepsNegativeInfinityDoc() throws Exception {
    try (Directory dir = new ByteBuffersDirectory()) {
      try (IndexWriter w =
          new IndexWriter(dir, new IndexWriterConfig().setCodec(TestUtil.getDefaultCodec()))) {
        Document doc0 = new Document();
        doc0.add(new DoubleDocValuesField("d", Double.NEGATIVE_INFINITY));
        doc0.add(new TextField("body", "a", Field.Store.NO));
        w.addDocument(doc0);
        Document doc1 = new Document();
        doc1.add(new DoubleDocValuesField("d", 1.0));
        doc1.add(new TextField("body", "b", Field.Store.NO));
        w.addDocument(doc1);
      }
      try (DirectoryReader reader = DirectoryReader.open(dir)) {
        IndexSearcher searcher = new IndexSearcher(reader);
        Query query =
            new BooleanQuery.Builder()
                .add(
                    new FunctionRangeQuery(
                        new DoubleFieldSource("d"), (Number) null, (Number) null, true, true),
                    BooleanClause.Occur.SHOULD)
                .add(new TermQuery(new Term("body", "a")), BooleanClause.Occur.SHOULD)
                .add(new TermQuery(new Term("body", "b")), BooleanClause.Occur.SHOULD)
                .setMinimumNumberShouldMatch(2)
                .build();
        // expected: all count(q) = 2 matches; actual: 1, doc 0 (FunctionRangeQuery score < 0) is lost
        assertEquals(
            "hits of search(q, 10)",
            searcher.count(query),
            searcher.search(query, 10).scoreDocs.length);
      }
    }
  }
}
Command
cd $LUCENE && ./gradlew :lucene:queries:test --tests TestFunctionRangeQueryNegativeScoreHitLoss -Ptests.seed=DEADBEEF -Ptests.asserts=false
Expected
search(q, 10) returns both matching documents, as many as count(q) = 2.
Observed
1 hit, doc 1: AssertionError: hits of search(q, 10) expected:<2> but was:<1>; with assertions on, AssertionError at WANDScorer.java:92.

All files of this repro: repro/LFV-406/

More files, environment, full output342 words

ByteBuffersDirectory, the default codec pinned so the seed cannot pick another one, a plain IndexSearcher that runs the one leaf on the calling thread; nothing random. Three SHOULD clauses are needed, because with two, "at least 2" is a plain conjunction and WANDScorer is not used; the open lower bound is needed, because with a lower bound of -10 the -Infinity document does not match. The test asserts only the lost hit; the negative score is the cause, shown by the assertion that fires with assertions on and by the fix making the test pass.

Release jars: the test fails on 10.5.0 and 10.6.0 (assertions on and off) and passes on 10.4.0, where this dense shape still ran on BooleanScorer; on 10.0.0 and 9.12.3 it passes with assertions off and, with assertions on, hits the old core negative-score check in TopScoreDocCollector.

Regression test: this same test; it passes with the recommended fix and fits as one more method of the existing TestFunctionRangeQuery.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; with the fix: passes. Releases 10.5.0 and 10.6.0: fail; 10.4.0: passes
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (the only platform run; the code path is pure Java)
Command (environment)
cp $REPRO/TestFunctionRangeQueryNegativeScoreHitLoss.java $LUCENE/lucene/queries/src/test/org/apache/lucene/queries/function/
cd $LUCENE && ./gradlew :lucene:queries:test --tests TestFunctionRangeQueryNegativeScoreHitLoss -Ptests.seed=DEADBEEF -Ptests.asserts=false
# assertions on (-Ptests.asserts=true) fails earlier, with AssertionError at WANDScorer.scaleMaxScore

Observed output

# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=false (stack trimmed)
TestFunctionRangeQueryNegativeScoreHitLoss > testMinShouldMatchKeepsNegativeInfinityDoc FAILED
    java.lang.AssertionError: hits of search(q, 10) expected:<2> but was:<1>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:D7A0144D2DA9FECF]:0)
        at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:647)
        at org.apache.lucene.queries.function.TestFunctionRangeQueryNegativeScoreHitLoss.testMinShouldMatchKeepsNegativeInfinityDoc(TestFunctionRangeQueryNegativeScoreHitLoss.java:66)

# the same with -Ptests.asserts=true
TestFunctionRangeQueryNegativeScoreHitLoss > testMinShouldMatchKeepsNegativeInfinityDoc FAILED
    java.lang.AssertionError
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:D7A0144D2DA9FECF]:0)
        at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.WANDScorer.scaleMaxScore(WANDScorer.java:92)
        at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.WANDScorer$2.matches(WANDScorer.java:336)
        at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.Weight$DefaultBulkScorer.scoreTwoPhaseIterator(Weight.java:311)

# the numbers behind it (a main() probe on the same two documents, not part of the repro)
query: (frange(double(d)):[* TO *] body:a body:b)~2
count(q) = 2
search(q, 10): totalHits=1 hits hits: doc1=1.3150668
exhaustive (totalHitsThreshold MAX_VALUE): totalHits=2 hits hits: doc1=1.3150668 doc0=-3.4028235E38
frange alone, search(frange, 10): totalHits=2 hits hits: doc1=1.0 doc0=-3.4028235E38
FunctionQuery(d) alone, search(fq, 10): totalHits=2 hits hits: doc1=1.0 doc0=0.0
# with the clamp: search(q, 10): totalHits=2 hits hits: doc1=1.3150668 doc0=0.31506687

Why it looks unintended

  • Since Lucene 8.0 "Scorers are no longer allowed to produce negative scores" (MIGRATE.md); the test framework and WANDScorer still assert it.
  • ValueSourceScorer says it scores "same as FunctionQuery"; LUCENE-7996 changed FunctionQuery's identical code to return 0 but did not touch this class.
  • count(q) says 2 and search(q, 10) says exactly 1: nothing documents that top-k may drop a match.
The full argument166 words
  • The contract. MIGRATE.md:1368-1372 ("Scorer must produce positive scores (LUCENE-7996)": "Scorers are no longer allowed to produce negative scores"); CHANGES.txt:6636 (8.0.0, API Changes: "Queries are now required to produce positive scores"); CHANGES.txt:6732-6733 ("FunctionQuery and FunctionScoreQuery now return a score of 0 when the function produces a negative value"). On main the test framework enforces it (AssertingScorer.java:131 assert Float.compare(score, 0f) >= 0, AssertingScorable.java:36).
  • Core relies on it. WANDScorer asserts maxScore >= 0 (WANDScorer.java:92) and throws "Scores must be positive or null" for a negative max score (:70-71).
  • The class says it mirrors FunctionQuery. ValueSourceScorer.java:86 "(same as FunctionQuery, but no qWeight)" and :28 "This is similar to FunctionQuery". a8a63464e7d (LUCENE-7996) replaced FunctionQuery's identical -Float.MAX_VALUE mapping with the 0 clamp; ValueSourceScorer and FunctionRangeQuery are not among its 22 files.
  • Two APIs disagree on the match set. count(q) = 2, search(q, 10) = exactly 1 hit.
  • Precedent. Negative scores in other code were fixed as bugs: #12727, #16139, #12342, #11184.
The devil's-advocate check159 words

The strongest case: FunctionRangeQuery.java:33 says "The score is the float value", and ValueSourceScorer.java:26 and FunctionValues.java:224-227 say the same, all @lucene.experimental. Scorable.score() states no sign, and since #13939 (10.1.0) core TopScoreDocCollector no longer asserts score >= 0. A second argument: Lucene tolerates negative scores from IndriDirichletSimilarity, which its similarity test base exempts.

Why it fails: those javadocs come from LUCENE-6720 (2015), two years before the rule, and LUCENE-7996 changed FunctionQuery's identical raw-value code. #13939 moved the assertion into the test framework (AssertingScorable); it did not drop it. The LUCENE-7996 discussion (#9044, 19 comments) never mentions this class, so it looks missed rather than exempted; that is an inference from silence, nobody said so. Even if a maintainer preferred to document negative scores, WANDScorer would still lose the hit, so a javadoc change alone cannot fix it. The Indri exemption is local to that class and is itself the cause of another hit-loss bug (LFV-190).

Suggested fix

Recommended Clamp the score at 0 in ValueSourceScorer (validated)

Clamp the score at 0 in ValueSourceScorer.score(), as FunctionQuery does: return score > 0 ? score : 0f;, and update the javadocs that say the score is the raw value. The repro passes with assertions on and off, and so do the closest suites and the whole queries module (512 tests).

// Scores must not be negative (LUCENE-7996): map negative values, -0.0 and NaN to 0. This
// conditional handles NaN since comparisons with NaN are always false.
return score > 0 ? score : 0f;
All options and trade-offs4 options · 580 words

Option A was built and run through gradle. B (document negative scores) would leave the lost hit, C (clamp only inside FunctionRangeQuery) is a narrower variant of A, and D (make WANDScorer tolerate negative scores) works against the documented rule. Workarounds that need no fix: put a FunctionRangeQuery that only restricts results in a FILTER clause or a ConstantScoreQuery, use a finite lower bound where documents valued -Infinity need not match, or reject non-finite values at index time.

Clamp the score at 0 in ValueSourceScorer (validated) (recommended)

ValueSourceScorer.score() (ValueSourceScorer.java:91) returns the value only when it is positive, else 0, the rule FunctionQuery applies. The same change updates the javadocs: "The score is the float value, or 0 if it is negative or NaN" in FunctionRangeQuery, a sentence in the ValueSourceScorer class javadoc, and "or 0 where that is negative or NaN" in FunctionValues.getScorer and getRangeScorer (3 files, +12/-10).

// Scores must not be negative (LUCENE-7996): map negative values, -0.0 and NaN to 0. This
// conditional handles NaN since comparisons with NaN are always false.
return score > 0 ? score : 0f;

Trade-offs

  • Behaviour change. Documents with negative values now tie at 0 instead of being ordered by their negative value; users who need that order should sort by the value. Every ValueSourceScorer that does not override score() changes the same way: the getRangeScorer implementations in FunctionValues, DoubleDocValues, LongDocValues, IntDocValues, DocTermsIndexDocValues and EnumFieldSource, FunctionValues.getScorer, and downstream subclasses. Worth a CHANGES.txt entry.
  • -0.0. It becomes 0 here; FunctionQuery lets -0.0 through, which is the separate LFV-273.
  • Matching unchanged. matches() is separate, so count(), filters and ConstantScoreQuery uses do not change.
  • Cost. One comparison per scored document; no benchmark was run.

What the checks validated

Applied to a scratch worktree of 645d4fe14a20 and run through gradle with OpenJDK 25.0.3, seed DEADBEEF (2026-10-31; the worktree was removed afterwards):

  • stock: the minimal test fails with assertions on and off;
  • with the fix: the minimal test passes with assertions on and off;
  • the closest suites TestFunctionRangeQuery, TestValueSources, TestFunctionQuerySort, TestFunctionQueryExplanations, TestDocValuesFieldSources and TestFieldScoreQuery pass (64 tests, 0 failures);
  • the whole :lucene:queries:test passes (51 classes, 512 tests, 0 failures, 5 skipped), and so do :lucene:expressions:test (170 tests) and :lucene:spatial-extras:test (117 tests, 5 skipped); no module outside lucene/queries uses FunctionRangeQuery, ValueSourceScorer or getRangeScorer;
  • checkGoogleJavaFormat, ecjLintMain, forbiddenApisMain and renderJavadoc for queries pass.
  • Earlier, the code-only clamp made the finder's negative-score test, an independent reproducer's tests, the 255-check security probe and all three follow-up shapes pass.

Document negative scores instead (not recommended)

Change no code; say in the javadocs that FunctionRangeQuery may return negative scores.

Trade-offs

Matches today's "The score is the float value" wording, but breaks the global rule, the test framework keeps rejecting the score, and WANDScorer keeps losing the hit. Not run.

Clamp only in FunctionRangeQuery

Wrap the scorer that FunctionRangeWeight.scorerSupplier returns (FunctionRangeQuery.java:168-174) so that it clamps, and leave ValueSourceScorer as it is.

Trade-offs

Narrower: other users of getRangeScorer/getScorer keep raw scores, but a scorer that can return negative scores stays in the API, and it is more code than the first option. Not run.

Make WANDScorer tolerate negative scores (not recommended)

For example clamp the scaled lead score at 0 in WANDScorer.matches().

Trade-offs

Hides this symptom on one path only, against the documented rule, in a hot core loop; other scorers and collectors may make the same assumption. Not run.

Downstream note

Where end users write the value of a numeric field that an application ranks with a scored FunctionRangeQuery inside a minimumShouldMatch query, a stored -Infinity hides only that user's own document; no other, non-matching, deleted or filtered document is affected.

How it was found

A hunt unit ran cross-path checks of FunctionRangeQuery under Lucene's asserting test searcher, which rejected a negative score. Reading the code and history traced it to LUCENE-7996, and a public-API JUnit confirmed it. A later randomized security probe of 17 query shapes found the lost hits in the minimumShouldMatch path.

The method in full135 words

Method: non-formal (the hunt's tier for findings from directed testing and code reading). The hunt unit compared range-bound behaviour across query paths.

  • Assertion. Its cross-path FunctionRangeQuery checks ran under the test framework's asserting searcher, and AssertingScorer rejected a negative score.
  • Trace. Code and history reading led to LUCENE-7996 (a8a63464e7d), which clamped FunctionQuery, BoostedQuery and FunctionScoreQuery but not ValueSourceScorer; a public-API JUnit on the stock jar (one document, d = -5.0) failed with assertions on and off.
  • Hit loss. After the verifier panel, the follow-up's security screen ran a randomized probe (20,000 documents, 17 query shapes, 255 checks) and found the minimumShouldMatch >= 2 hit loss (361 missing documents, all valued -Infinity). Three minimal checks then mapped it across releases, and a 2026-10-29 minimization cut it to the two-document test above.

Prior reports, history and checks

Prior reports

No prior report found: 217 logged searches on 2026-10-26 (GitHub issues, PRs and commits, the JIRA archive, the mailing lists, downstream code, the web) and a re-check of GitHub on 2026-10-31. Closest: #9044, the LUCENE-7996 discussion, which never mentions this class.

Searches3 searches
  • #9044 (LUCENE-7996, the rule; none of its 19 comments mentions FunctionRangeQuery, ValueSourceScorer or getRangeScorer), #13939 (moved the core TopScoreDocCollector score >= 0 assertion into the test framework), #3347 (LUCENE-2271, open since 2010: -Infinity and NaN function scores break TopScoreDocCollector; background to the -Float.MAX_VALUE mapping, which Solr's FunctionQuery added in 2009 with SOLR-1111, a66ef33fb18), #7778 (LUCENE-6720, added FunctionRangeQuery), #14021 and #16176 (the WANDScorer changes behind the hit loss; neither mentions negative scores or function queries).
  • Open PR #16847 (long doc ids) touches only the ValueSourceScorer constructor; it may conflict with a fix at merge time.
  • Precedents: #11184, #12342, #12727, #16139 fixed negative scores in other code.
  • Same family, the hunt's own findings (different fix sites): LFV-273, LFV-203, LFV-083. Related but registered as known (partial overlap with existing upstream reports, so no own issue): LFV-190.
  • Limits: GitHub's search rate limit was saturated on 2026-10-26 (GraphQL used instead); private lists and private security reports could not be searched.
What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive; apache/solr, apache/lucene-solr, Elasticsearch, OpenSearch; all of GitHub)2026-10-26the scripted prior-report search (16 issue and PR queries, 3 commit queries) and 97 GraphQL searches (class and method names, negative score, -Float.MAX_VALUE, LUCENE-7996 follow-ups), plus the changed-file lists of all 411 open apache/lucene PRs; for the hit loss 6 more searches (WANDScorer negative score, minShouldMatch negative score missing, scaleMaxScore assertion negative, WANDScorer -Float.MAX_VALUE, minimumShouldMatch FunctionRangeQuery)20 hits read, none the same bug; the hit-loss searches returned 0 hits
JIRA archive (LUCENE, SOLR), mailing lists (dev@, java-user@, issues@, commits@, builds@ lucene; Solr lists), downstream code, web2026-10-2618 JIRA queries, 44 list queries, 5 downstream code queries, 15 web searches, 19 readsno report; only notification mails of the items above; Elasticsearch and OpenSearch do not reference FunctionRangeQuery
GitHub issues and PRs (apache/lucene)2026-10-31issues updated since 2026-10-20 for FunctionRangeQuery, ValueSourceScorer, negative score, WANDScorer negative, frange negative, minimumShouldMatch negative; PRs for ValueSourceScorer, FunctionRangeQuery, negative scores0 results

History

Introduced in 8.0.0 by LUCENE-7996 (a8a63464e7d, 2017), which forbade negative scores but did not clamp this scorer. The hit loss arrived with #14021 (10.1.0) and widened to dense top-level queries with #16176 (10.5.0). Unchanged on main, branch_11x and 10.6.0.

Versions tested7 rows

Introduced by a8a63464e7da63b3dbc884634fd0e00b3f0c140b, LUCENE-7996 "Queries are now required to produce positive scores." (Adrien Grand, 2017-12-06; a JIRA patch, now GitHub issue #9044; first release 8.0.0). It made negative scores illegal and clamped FunctionQuery, BoostedQuery and FunctionScoreQuery, but not ValueSourceScorer. score() itself dates from LUCENE-6720 (13f8ab93a97 / 88d137632d3, 2015, release 5.4.0), when negative scores were legal and FunctionQuery scored the same way.

The hit loss needs WANDScorer's per-candidate score check from #14021 "Make WANDScorer compute scores on the fly" (Adrien Grand, 2024-11-26; main d9aa525c9ea, release line 3f620dcd34f, first released in 10.1.0); before it, matches() compared the sum of max scores. #16176 "Restore WANDScorer for TOP_SCORES + minShouldMatch > 1" (Tianxiao Wei, 2026-06-03; main 135b348b370, release line 4f720f944b8, first released in 10.5.0) sent dense top-level queries, which 10.1.0 to 10.4.0 ran on BooleanScorer, to WANDScorer too.

#13939 (10.1.0) moved the score >= 0 assertion from core TopScoreDocCollector into the test framework, so since 10.1.0 a plain search with assertions on no longer throws in the collector on a negative score (WANDScorer still asserts on the scores it scales).

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails with assertions off (1 of 2 hits) and on (AssertionError at WANDScorer.java:92), gradle and standalone runner; passes with the fix
10.5.0, 10.6.0 affectedminimal test fails, assertions on and off (release jars)
10.5.2 affecteddense, sparse and nested checks lose the hit; the security probe misses 361 -Infinity documents
10.1.0, 10.4.0 sparse and nested shapesthe sparse and nested checks lose the hit; the dense four-document check passes on both (BooleanScorer), and the minimal test on 10.4.0
8.0.0, 8.11.4, 9.12.3, 10.0.0 negative score onlyscore -5.0 where FunctionQuery gives 0; no hit loss in the tested shapes; with assertions on, AssertionError in core TopScoreDocCollector
7.7.3 before the ruleFunctionQuery and FunctionRangeQuery both score -5.0, legal at the time
upstream main 2f424fd428da, branch_11x 603af870375, branch_10_6 7e5aec4370e same codethe same score() line and WANDScorer checks as 645d4fe14a20 (read, not run)

Independent checks

Confirmed by an independent reproducer, a devil's-advocate verifier, a reviewer from another model family (OpenAI GPT-5.6) and an adjudication; the hit loss, found after that panel, was re-run by a credibility audit, the minimization and this doc's gradle run. The only dissent was a procedural "not real" from a read-only review.

Checks and verdicts358 words

The verifier panel (the independent reproduction, the cross-model review, the intended-behaviour check, adjudication) reviewed the negative score; the hit loss was found later by the follow-up's security probe and re-checked by the audit, the minimal checks and this doc, not by the panel.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (cross-path checks, then JUnit) candidateAssertingScorer rejected a negative score; the public-API JUnit (d = -5.0) fails 1/1 on main with assertions on and off and on 10.5.2.
Refute review (OpenAI GPT-5.6, read-only) not real (procedural, low)Could not run Java; agrees on the root cause and rates the "The score is the float value" argument strong. A separate run re-ran the repro: fails 1/1.
Independent reproducer realOwn 5-test class: 3 of 5 fail on main and 10.5.2 (assertions on and off), 4 of 5 on 10.0.0 and 9.12.3 with assertions on; controls pass; no hit loss in its shapes.
Works-as-designed lens (OpenAI GPT-5.6, read-only) real (medium)A separate run re-ran the repro on main (assertions on and off), 10.0.0 and 10.1.0: fails each time.
Intended-behaviour devil's advocate real (medium-high)The javadoc, Scorable, collector, CHANGES/MIGRATE and Indri arguments each fail; newSearcher rejects the score on main.
Adjudication confirmed (medium), class minorRe-ran the finder's repro and the independent reproduction's tests; rated the impact before the hit loss was known.
Follow-up security probe not security-sensitive; found the hit loss20,000 documents, 17 shapes, 255 checks: one shape misses 361 -Infinity documents on main and 10.5.2; every match set is right; 0 failures with the fix.
Follow-up minimal checks and credibility audit confirmed; likelihood downgraded to contrived33 runs over 3 shapes, 12 releases and main; the audit re-ran the finder test and two shapes on main and executed -1e32 (dropped) and -1e31 (kept).
Minimal repro verifiedThe two-document test fails on 645d4fe14a20 (two seeds, assertions on and off); the clamp passes.
This doc realGradle in a scratch worktree of 645d4fe14a20: stock fails, the fix passes the test and the suites; release jars 9.12.3, 10.0.0, 10.4.0, 10.5.0 and 10.6.0.

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,714 words

Finding record

LFV-406, snapshot of 2026-11-06

Registry title
FunctionRangeQuery scores a document with its raw, possibly negative, function value although LUCENE-7996 forbids negative scores and FunctionQuery over the same source clamps them to 0; since 10.1.0 a matching document whose value is -Infinity can then disappear from the top-k results of a minimumShouldMatch >= 2 BooleanQuery
Area
queries / function queries (ValueSourceScorer behind FunctionRangeQuery, LUCENE-7996 non-negative score rule; the hit loss surfaces in core WANDScorer)
Priority
Low, rank 716: FunctionRangeQuery scores with raw negative values despite LUCENE-7996, and it loses a hit only for a stored -Infinity under minimumShouldMatch >= 2, the same negative-score bound profile as LFV-203.
Impact class
silent wrong results
Impact note
Applications that score with lucene/queries FunctionRangeQuery (not wrapped in a FILTER clause or ConstantScoreQuery) over a field that can hold negative values get negative scores in TopDocs and explain, and a document that also matches the function clause can rank below one that matches only the other clause; when the field holds -Infinity (scored -Float.MAX_VALUE; by code reading also finite values at or below about -4e31) and the range admits it, a FunctionRangeQuery SHOULD clause of a minimumShouldMatch >= 2 BooleanQuery makes top-k search silently leave that matching document out and report an exact total hit count that is too low while count() includes it (executed with -da on 10.1.0, 10.4.0, 10.5.2 and main; on 10.1.0 to 10.4.0 only when the query takes the WANDScorer path, i.e. sparse SHOULD clauses or a nested query, and since 10.5.0 also for dense top-level queries), and with -ea that path throws AssertionError in WANDScorer, as plain -ea top-k searches did in core TopScoreDocCollector on 8.0.0 to 10.0.0; match sets of count(), FILTER/MUST_NOT/ConstantScoreQuery uses and every other document are unaffected; default IndexWriterConfig, codec and searcher, four to 100 documents suffice; released since 8.0.0.
Summary
You index a document with DoubleDocValuesField("d", -5.0) and search new FunctionRangeQuery(new DoubleFieldSource("d"), -10.0, 10.0, true, true), expecting a non-negative score, because since LUCENE-7996 (8.0) scorers must not produce negative scores and FunctionQuery over the same source scores that document 0. Instead it scores -5.0 (-Infinity and NaN become -Float.MAX_VALUE), because ValueSourceScorer.score() returns the raw value and LUCENE-7996 clamped FunctionQuery, BoostedQuery and FunctionScoreQuery but not this scorer. Since 10.1.0 this also loses hits: with minimumNumberShouldMatch = 2 over SHOULD clauses FunctionRangeQuery(d, open range), body:a and body:b (sparse term clauses; also dense ones since 10.5.0), a matching document whose value is -Infinity is missing from search(q, 10), whose totalHits says '3 hits' while count(q) is 4, because WANDScorer rejects a candidate whose scaled score is below the initial minimum competitive score of 0.
Introduced
a8a63464e7da63b3dbc884634fd0e00b3f0c140b (LUCENE-7996 'Queries are now required to produce positive scores.', a JIRA patch, now GitHub issue #9044, no pull request): it made non-negative scores a rule for every Scorer and replaced FunctionQuery's identical NaN/-Infinity -> -Float.MAX_VALUE mapping with a 0 clamp, but did not touch ValueSourceScorer or FunctionRangeQuery (neither is among its 22 files) [read: commit diff and tag ancestry; exec: on 7.7.3 FunctionQuery and FunctionRangeQuery both score -5.0, on 8.0.0 FunctionQuery scores 0 and FunctionRangeQuery -5.0; the commit and its parent were not built]; ValueSourceScorer.score() itself is unchanged since LUCENE-6720 (13f8ab93a97 trunk / 88d137632d3 branch_5x, 2015-08-06), the raw floatVal score dates from SOLR-2429 (e219ad401cf, 2011) [read]; the hit loss comes from WANDScorer's per-candidate score check added by #14021 'Make WANDScorer compute scores on the fly' (d9aa525c9ea main / 3f620dcd34f release line, first released in 10.1.0) [read: git log -S scaledLeadScore; exec: the sparse and nested checks pass on 10.0.0 and fail on 10.1.0]; #16176 'Restore WANDScorer for TOP_SCORES + minShouldMatch > 1' (135b348b370 main / 4f720f944b8 release line, first released in 10.5.0) widened it to dense top-level queries, which 10.1.0 to 10.4.0 ran with BooleanScorer [exec: the dense top-level check passes on 10.4.0 and fails on 10.5.0; read: BooleanScorerSupplier.booleanScorer at 10.4.0 uses BooleanScorer only when cost() >= maxDoc / 3] (2017-12-06 (a8a63464e7d); hit loss from 2024-11-26 (#14021, first released in 10.1.0), widened to dense top-level queries on 2026-06-03 (#16176, first released in 10.5.0))
First affected version
8.0.0 [exec] for the negative score (first release with a8a63464e7d; 5.4.0 to 7.7.x score the same way but before the rule, identical to FunctionQuery; 7.7.3 executed); 10.1.0 [exec] for the hit loss (sparse top-level and nested minimumShouldMatch >= 2 queries); 10.5.0 [exec] for dense top-level ones
Affected versions
every release from 8.0.0 through 10.6.0 (10.6.0 by reading) and main ea57360912ed [exec: negative score at 8.0.0, 8.11.4, 9.12.3, 10.0.0, 10.1.0, 10.5.2 and main with -da, and at 10.1.0, 10.5.2 and main with -ea; plain -ea top-k search throws AssertionError in core TopScoreDocCollector at 8.0.0 (:71), 8.11.4 (:80), 9.12.3 and 10.0.0 (:75), moved to the test framework by #13939 in 10.1.0]; hit loss [exec -da, follow-up check wand-check/]: sparse top-level shape (4 target documents plus 96 fillers that match one clause) fails on 10.1.0, 10.4.0, 10.5.2 and main and passes on 9.12.3 and 10.0.0; nested shape (MUST (minimumShouldMatch=2 query) + MUST body:x) fails on 10.1.0, 10.4.0, 10.5.2 and main and passes on 9.12.3 and 10.0.0; dense 4-document top-level shape fails on 10.5.0, 10.5.2 and main and passes on 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.1.0, 10.2.0, 10.2.2, 10.3.0 and 10.4.0; with -ea all three shapes throw AssertionError at WANDScorer.scaleMaxScore (WANDScorer.java:92) on main; the follow-up security probe also shows it on main and 10.5.2 (20,000 documents, 361 -Infinity documents missing); not affected: 5.4.0 to 7.7.x (no rule yet), and FunctionRangeQuery does not exist before 5.4.0; branch_10x (unreleased) and branch_10_6 (10.6.0, released 2026-10-29) have the same ValueSourceScorer.score() [read, 2026-10-26]; not fixed upstream
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
non-formal
Upstream and prior reports
none found (2026-10-26, the prior-report record: 217 logged read-only queries, namely the scripted prior-report search over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the LUCENE JIRA archive and the web, 97 GitHub GraphQL searches over apache/lucene, apache/solr, apache/lucene-solr, elastic/elasticsearch, opensearch-project/OpenSearch and all of GitHub, 14 LUCENE and SOLR JIRA queries, 24 more list queries including builds@ lucene and the Solr lists, downstream code search and 19 reads including the changed-file lists of all 411 open apache/lucene PRs; 20 hits read, none the same bug; plus, for the hit loss, 6 GitHub searches by this follow-up (WANDScorer negative score(s), minShouldMatch negative score missing, scaleMaxScore assertion negative, WANDScorer -Float.MAX_VALUE, minimumShouldMatch FunctionRangeQuery: 0 hits) and reads of #16176, #14021 and #13408, which never mention negative scores or function queries); not fixed upstream (ValueSourceScorer.score() unchanged on main cf398bfb142c, branch_10x and branch_10_6; the only open PR touching ValueSourceScorer.java, #16847, changes the constructor only and may conflict at merge time); context, not reports: #9044 (LUCENE-7996, the rule and the FunctionQuery clamp; its 19 comments never mention FunctionRangeQuery, ValueSourceScorer or getRangeScorer), #13939 (moved the core TopScoreDocCollector score >= 0 assertion into the test framework), #3347 (LUCENE-2271, open since 2010: -Infinity and NaN function scores break TopScoreDocCollector; background to the -Float.MAX_VALUE mapping, which Solr's FunctionQuery added in 2009 with SOLR-1111, a66ef33fb18), #16176 and #14021 (the WANDScorer routing and scoring changes behind the hit loss); precedents for fixing negative scores elsewhere: #11184/#361, #12342, #12700/#12727, #16139; search gaps: GitHub's search rate limit was saturated (GraphQL used instead), web search is a general index, private lists not searched
Lucene commit tested
main 645d4fe14a20 (2026-10-28): the minimal test fails with assertions on and off, through gradle in a scratch worktree and through a standalone runner; with the recommended fix it passes, together with the closest suites and the whole queries module. Release jars: the test fails on 10.5.0 and 10.6.0 and passes on 10.4.0, 10.0.0 and 9.12.3 with assertions off. Upstream main 2f424fd428da (now 12.0.0-SNAPSHOT), branch_11x 603af870375, branch_10_6 7e5aec4370e and the 10.6.0 tag carry the same code (read, 2026-10-30 refs).

Lucene and upstream sources

Finding record
  • lucene/queries/src/java/org/apache/lucene/queries/function/ValueSourceScorer.java:25-28 (at ea57360912ed) (class javadoc 'returns the result of FunctionValues#floatVal(int) as the score'), :84-92 (score(): '(same as FunctionQuery, but no qWeight)', 'return score > Float.NEGATIVE_INFINITY ? score : -Float.MAX_VALUE;'), :94-97 (getMaxScore returns Float.POSITIVE_INFINITY)
  • lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java:33 (at ea57360912ed) ('The score is the float value. This can be a slow query ...'); FunctionValues.java:225-226 (getRangeScorer 'produces scores equal to floatVal(int)'); FunctionQuery.java:132 ('if (val >= 0 == false) { // this covers NaN as well ...' return 0)
  • lucene/core/src/java/org/apache/lucene/search/WANDScorer.java:75 (at ea57360912ed) (scalingFactor(+Infinity) = scalingFactor(Float.MAX_VALUE) - 1), :92 ('assert maxScore >= 0'), :340 ('while (scaledLeadScore < minCompetitiveScore || freq < minShouldMatch)'); BooleanScorerSupplier.java:315-322 (#16176: TOP_SCORES with minShouldMatch > 1 falls back to WANDScorer), :589-590 (WANDScorer when TOP_SCORES and top-level scoring clause, or minShouldMatch > 1); TotalHits.java:53-54 (toString 'N hits' for EQUAL_TO, 'N+ hits' otherwise)
  • lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingScorer.java:131 (at ea57360912ed) ('assert Float.compare(score, 0f) >= 0 : score;'), AssertingScorable.java:36 ('assert score >= 0'); lucene/MIGRATE.md:1368-1375 ('Scorer must produce positive scores (LUCENE-7996)'); lucene/CHANGES.txt:6603 ('LUCENE-7996: Queries are now required to produce positive scores.'), :6699 ('FunctionQuery and FunctionScoreQuery now return a score of 0 ...')
The finding in 30 seconds
  • lucene/queries/src/java/org/apache/lucene/queries/function/ValueSourceScorer.java:25-36, (at 645d4fe14a20) :84-92, :94-97
  • lucene/queries/src/java/org/apache/lucene/queries/function/FunctionQuery.java:130-137 (at 645d4fe14a20) (:132); FunctionRangeQuery.java:31-43, :168-174; FunctionValues.java:206-209, :224-232; docvalues/DoubleDocValues.java:87-147 (:95-96, :110-117)
  • lucene/core/src/java/org/apache/lucene/search/WANDScorer.java:57, (at 645d4fe14a20) :60-83, :90-105, :163, :177-196, :326-358; BooleanScorerSupplier.java:315-322, :589-590; IndexSearcher.java:100, :599; TopScoreDocCollector.java:56-59
Who is affected
  • lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java:42 (at 645d4fe14a20) (@lucene.experimental); docvalues/DoubleDocValues.java:116; lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:100 (TOTAL_HITS_THRESHOLD = 1000)
Why it looks unintended
  • lucene/MIGRATE.md:1368-1375; (at 645d4fe14a20) lucene/CHANGES.txt:6611-6637 (8.0.0, API Changes), :6718-6733 (8.0.0, Changes in Runtime Behavior)
  • lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingScorer.java:131; (at 645d4fe14a20) AssertingScorable.java:36; lucene/core/src/java/org/apache/lucene/search/WANDScorer.java:69-71, :92; Scorable.java:27-28
  • lucene/queries/src/java/org/apache/lucene/queries/function/ValueSourceScorer.java:26, (at 645d4fe14a20) :28, :86; FunctionRangeQuery.java:33; FunctionValues.java:224-227
Suggested fix
  • lucene/queries/src/java/org/apache/lucene/queries/function/FunctionQuery.java:132; (at 645d4fe14a20) FunctionValues.java:206-232; docvalues/*DocValues.java and valuesource/EnumFieldSource.java (getRangeScorer implementations that inherit score())
  • lucene/: (at 645d4fe14a20) grep for FunctionRangeQuery, ValueSourceScorer and getRangeScorer finds them only under lucene/queries
History
  • a Lucene clone refs (2026-10-30): origin/main 2f424fd428da, origin/branch_11x 603af8703753, origin/branch_10_6 7e5aec4370e0, releases/lucene/10.6.0: ValueSourceScorer.java:91, WANDScorer.java:92, :340 (read)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Made in parallel with this doc from the code at 645d4fe14a20, the minimal repro and the probe run of it (count, top-k, exhaustive scores, with and without the clamp).

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.