All findings · Lucene finding · LFV-893 · valuesource-bridge-never-calls-createweight

ValueSource#asDoubleValuesSource and #asLongValuesSource never call createWeight, so a ValueSource that keeps per-query state throws NullPointerException

asDoubleValuesSource() and asLongValuesSource() skip a ValueSource's per-query setup step, createWeight, so sources that compute something there (ttf, sttf, or your own) throw NullPointerException in FunctionScoreQuery and in sorts, on every release run from 6.4.0 to 10.6.0. One document is enough: boosting by ttf(text, test) should score 2 and throws instead.

Summary

Priority
Low, rank 625The ValueSource bridges skip createWeight and throw a loud NPE only for stateful sources, the same missing-call shape as LFV-776, and since the bridges feed FunctionScoreQuery and sorts they rank above the narrower map() sibling LFV-886.
Impact
crash
Since
6.4.0 (LUCENE-7610, 2017-01-06)introduced 2017-01-06 · about 9 years
Affected
6.4.0 to 10.6.0 and main (14 releases run)
Status
documented
Found by
AI tools: a bounded-exhaustive check of the createWeight contract

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) 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

The adapters that let a ValueSource stand in for a DoubleValuesSource or LongValuesSource skip the source's per-query setup, so sources that need that setup fail.

Background: the two function-source APIs

  1. A ValueSource (the older function API, used by FunctionQuery) runs in two steps: createWeight(context, searcher) once per query, where it may compute per-query numbers and store them in a context map, then getValues(context, segment) for each segment, which reads them back.
  2. A DoubleValuesSource (the newer API, used by FunctionScoreQuery, boostByValue and sorts) has no such step: the caller calls rewrite(searcher) once, then getValues per segment. asDoubleValuesSource() and asLongValuesSource() wrap a ValueSource as one; this page calls them the bridge.
  3. ttf(field, term) (TotalTermFreqValueSource) is one number per query: how often the term occurs in the whole index. It computes that number in createWeight.
Summary card of the bug. One document "test test" (ttf = 2): new FunctionQuery(ttf) scores 2.0; FunctionScoreQuery.boostByValue(new MatchAllDocsQuery(), ttf.asDoubleValuesSource()) throws at ValueSource.java:210. Left panel, executed on main: FunctionQuery builds newContext(searcher), calls createWeight(context, searcher), the context holds "searcher" and this (ttf) = 2, and getValues returns 2, score 2.0. The bridge builds newContext(searcher) but never calls createWeight, the context holds "searcher" and "scorer" but no entry for ttf, and getValues returns null: NullPointerException. Right panel: the bridges call in.getValues without in.createWeight (ValueSource.java:199-205, explain 235-239, asLongValuesSource 127-133); ttf returns context.get(this), which only its createWeight fills; the contract says getValues gets the context previously passed to createWeight; FunctionQuery, FunctionRangeQuery and getSortField call it first; MIGRATE.md maps the removed BoostedQuery, which called createWeight, to boostByValue. Below: a loud crash of that query only, rare, and a fix sketch that calls in.createWeight on the context the bridge builds.
Left: FunctionQuery calls createWeight, which stores ttf = 2 in the context, and scores 2.0; the bridge skips that call, the context has no entry for ttf, and getValues returns null: NullPointerException. Right: the code and the contract. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Index one document whose text is "test test", so ttf(text, test), the term's total frequency in the index, is 2. Boost a match-all query by it: FunctionScoreQuery.boostByValue(new MatchAllDocsQuery(), ttf.asDoubleValuesSource()).
  2. You expect 1 hit with score 2: match-all scores 1, times 2. new FunctionQuery(ttf) on the same index scores 2.0.
  3. You get NullPointerException: ... "this.val$fv" is null at ValueSource.java:210, with assertions on and off.
  4. Why: a ValueSource runs in two steps, createWeight once per query (ttf counts the term there and stores the 2 in a context map), then getValues per segment, which reads it back. The bridge builds a fresh map for each segment and calls only getValues, so ttf finds nothing and returns null.
  5. The same happens for sttf, functions that contain ttf or sttf, any custom ValueSource that keeps state from createWeight, explain, and asLongValuesSource() sorts. Fields, constants, the score and maxdoc() work.

Who is affected

Code that passes a ValueSource keeping per-query state in createWeight through the bridge: in-tree ttf and sttf (@lucene.internal) or functions containing them, or a custom source that follows the createWeight javadoc. Default configuration, any index size, every release run since 6.4.0; the query fails loudly at its first matching document. Fields, constants, the score and maxdoc() work.

Mechanism, terms and numbers644 words

ValueSource#asDoubleValuesSource() and #asLongValuesSource() wrap a ValueSource so it can be used where Lucene expects a DoubleValuesSource or LongValuesSource, for example in FunctionScoreQuery.boostByValue, the documented replacement for the removed BoostedQuery, or in a sort. A ValueSource is used in two steps: createWeight(context, searcher) once per query, where it "can optionally store weight info in the context", then getValues(context, segment), which gets "the context that was previously passed to createWeight()". The wrappers build a fresh context for every segment and call getValues without ever calling createWeight, although their rewrite(searcher) receives the searcher that call needs. TotalTermFreqValueSource (ttf) and SumTotalTermFreqValueSource (sttf) compute their number in createWeight and return context.get(this) from getValues, so through the wrappers they return null and the search throws NullPointerException; a custom ValueSource written the way the javadoc describes finds its state missing too. One document is enough, with a default IndexWriterConfig and a plain IndexSearcher, assertions on or off. No version of the bridges has called createWeight since they appeared in 6.4.0 (2017), and every release run, 6.4.0 to 10.6.0, fails, as does main; the boostByValue form used here exists from 7.3.0, and older releases fail through a sort or new FunctionScoreQuery.

The bug

  1. asDoubleValuesSource() returns new WrappedDoubleValuesSource(this, null) (ValueSource.java:183-186).
  2. FunctionScoreQuery.createWeight rewrites its source with the searcher (FunctionScoreQuery.java:121); boostByValue wraps the source in MultiplicativeBoostValuesSource (:84-86), whose rewrite rewrites the bridge (:315-317). The bridge's rewrite only returns a new wrapper that holds the searcher (ValueSource.java:243-246). No createWeight.
  3. Per segment, FunctionScoreWeight.scorerSupplier calls getValues (FunctionScoreQuery.java:239). The bridge builds newContext(searcher), a fresh map that holds only "searcher" (ValueSource.java:200), adds "scorer" (:203) and calls in.getValues(context, ctx) (:205), with no in.createWeight(context, searcher) before it.
  4. TotalTermFreqValueSource.getValues is return (FunctionValues) context.get(this); (TotalTermFreqValueSource.java:59-62); only its createWeight (:65-81) sums the term's frequency over all segments and stores that entry. So the bridge's fv is null. SumTotalTermFreqValueSource is the same (:51-54, :57-74).
  5. Scoring the document goes through FunctionScoreQuery.java:244-245 and :299 and DoubleValues.java:79 to fv.doubleVal(scorer.docId) (ValueSource.java:210): NullPointerException.
  6. asLongValuesSource() has the same shape (:126-133; the NPE comes from fv.exists(doc), :144), and so has the bridge's explain (:235-240).

The native consumers do it right: FunctionQuery.FunctionWeight (FunctionQuery.java:70-71), FunctionRangeQuery (FunctionRangeQuery.java:148-149) and ValueSourceSortField.rewrite (ValueSource.java:379-382) each build one context and call createWeight(context, searcher) before any getValues.

The numbers

  • Minimal repro, one document "test test": expected score 2; actual NullPointerException ("this.val$fv" is null). On the same index new FunctionQuery(ttf) scores 2.0 and boostByValue over IndexReaderFunctions.totalTermFreq scores 2.0.
  • Two documents ("test test x", "test y"): ttf is 3 and sttf is 5 through FunctionQuery; through the bridge, FunctionScoreQuery with ttf, boostByValue with sttf and a sort on ttf.asLongValuesSource().getSortField(false) all throw.
  • A custom source that stores numDocs in createWeight: 2 through FunctionQuery; through FunctionScoreQuery and through an asLongValuesSource() sort its getValues sees a context its createWeight never filled (0 calls).
  • ProductFloatFunction(const 2, ttf): 6 through FunctionQuery (on the devil's advocate's 3-document index, where ttf is 3), NullPointerException through boostByValue; FunctionQuery makes 1 createWeight call, the bridge 0. searcher.explain through the bridge throws too.

Not affected, and not claimed

  • Stateless sources (fields, constants, arithmetic on them), the score, and statistics that need only the searcher or the top-level reader work through the bridge: maxdoc() and docfreq() read the "searcher" entry that newContext adds, and numdocs() reads the top-level reader.
  • QueryValueSource and ScaleFloatFunction do not crash, because they compute their state lazily when it is missing; that they redo the work per segment through the bridge is code reading only.
  • A custom source that silently falls back to a default when its state is missing would give wrong scores instead of an exception; that variant was not tested.
Exposure and likelihood278 words
  • Who is hit. Code that passes such a source through asDoubleValuesSource() into FunctionScoreQuery (scoring, boostByValue, explain), or through asLongValuesSource() (or asDoubleValuesSource()) into a sort. In-tree: TotalTermFreqValueSource (ttf), SumTotalTermFreqValueSource (sttf) and any function containing them. Outside the tree: any custom ValueSource that stores state in createWeight, as its javadoc describes.
  • On the documented migration path. MIGRATE.md:1377-1381 says BoostedQuery "may be replaced by calls to FunctionScoreQuery.boostByValue()". BoostedQuery called createWeight on its ValueSource; boostByValue reaches a ValueSource only through asDoubleValuesSource().
  • Default vs opt-in. A default new IndexWriterConfig() and a plain IndexSearcher; the trigger is the opt-in combination of a stateful source with the bridge. Under Lucene's test framework the codec is randomized (the gradle runs used Asserting(Lucene104)); the codec plays no part, because ttf's term lookup lives in the createWeight that is never called, so the bridge throws before ttf reads anything.
  • Scale. One document is enough. At any index size a search throws as soon as it evaluates the source for a matching document, with assertions on and off; a query that matches nothing in a segment never reaches the bridge there (FunctionScoreQuery.java:236-237; explain of a non-match returns at :177-178). The failure is loud, only that query fails, and nothing is written to the index.
  • Downstream (read, not run). Apache Solr builds ttf() and sttf() from these classes, and its boost parsers wrap the boost function with asDoubleValuesSource() through boostByValue. That end-to-end path was not run.
  • How likely. Rare: ttf and sttf are per-term constants, seldom useful as a per-document boost or sort key, custom stateful sources are uncommon, and the bug has gone unreported since 6.4.0 (2017).

Minimal repro

One JUnit class for lucene/queries/src/test/org/apache/lucene/queries/function/, one method, public classes only (TotalTermFreqValueSource is marked @lucene.internal): one document, default IndexWriterConfig, plain IndexSearcher. It fails with assertions on and off, and it is also the suggested regression test (run ./gradlew tidy before committing it: two lines are over 100 characters).

TestValueSourceBridgeTotalTermFreq.java (JUnit for Lucene's build)57 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.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.queries.function.valuesource.TotalTermFreqValueSource;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.BytesRef;

public class TestValueSourceBridgeTotalTermFreq extends LuceneTestCase {

  public void testBoostByTotalTermFreq() throws Exception {
    try (Directory dir = new ByteBuffersDirectory()) {
      try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
        Document doc = new Document();
        doc.add(new TextField("text", "test test", Field.Store.NO));
        w.addDocument(doc);
      }
      try (DirectoryReader reader = DirectoryReader.open(dir)) {
        ValueSource ttf = new TotalTermFreqValueSource("text", "test", "text", new BytesRef("test"));
        Query q = FunctionScoreQuery.boostByValue(new MatchAllDocsQuery(), ttf.asDoubleValuesSource());
        TopDocs hits;
        try {
          hits = new IndexSearcher(reader).search(q, 1);
        } catch (NullPointerException e) {
          throw new AssertionError("expected score 2 (ttf, as FunctionQuery gives), got " + e, e);
        }
        assertEquals(2f, hits.scoreDocs[0].score, 0f);
      }
    }
  }
}
Command
cp $REPRO/TestValueSourceBridgeTotalTermFreq.java $LUCENE/lucene/queries/src/test/org/apache/lucene/queries/function/
cd $LUCENE && ./gradlew :lucene:queries:test --tests org.apache.lucene.queries.function.TestValueSourceBridgeTotalTermFreq -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same failure)
Expected
1 hit with score 2 (MatchAllDocsQuery scores 1 and ttf is 2; new FunctionQuery(ttf) scores 2.0 on the same index).
Observed
NullPointerException: "this.val$fv" is null at ValueSource.java:210, with assertions on and off.

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

More files, environment, full output250 words

The test indexes one document with TextField("text", "test test") in a ByteBuffersDirectory, wraps new TotalTermFreqValueSource("text", "test", "text", new BytesRef("test")) with asDoubleValuesSource(), and runs new IndexSearcher(reader).search(FunctionScoreQuery.boostByValue(new MatchAllDocsQuery(), ...), 1). Deterministic, single thread; the test framework's randomized codec plays no part (the bridge throws before ttf reads anything). TotalTermFreqValueSource is a public class but is marked @lucene.internal, so it is not a supported public API; the custom-source variant (a ValueSource that stores its state in createWeight, executed by the finder and the independent reproducer) uses only supported extension points and can replace or accompany this test in an issue. The NullPointerException is caught only to restate expected versus actual in the message; the cause is kept. It imports org.apache.lucene.tests.*, so it compiles on 9.1.0 and later; the study ran raw-Map ports of its own tests on 6.4.0 to 9.0.0. Through gradle, checkGoogleJavaFormat asks to wrap lines 45 and 46 (101 and 103 characters); ./gradlew tidy does it.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); also fails on the 10.6.0, 10.5.2, 9.12.3 and 9.1.0 release jars; 6.3.0 and older: no such API
jdkOpenJDK 25.0.3
osLinux aarch64

Observed output

TestValueSourceBridgeTotalTermFreq > testBoostByTotalTermFreq FAILED
    java.lang.AssertionError: expected score 2 (ttf, as FunctionQuery gives), got java.lang.NullPointerException: Cannot invoke "org.apache.lucene.queries.function.FunctionValues.doubleVal(int)" because "this.val$fv" is null
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:B63616D72850F488]:0)
        at org.apache.lucene.queries.function.TestValueSourceBridgeTotalTermFreq.testBoostByTotalTermFreq(TestValueSourceBridgeTotalTermFreq.java:51)

        Caused by:
        java.lang.NullPointerException: Cannot invoke "org.apache.lucene.queries.function.FunctionValues.doubleVal(int)" because "this.val$fv" is null
            at org.apache.lucene.queries.function.ValueSource$WrappedDoubleValuesSource$1.doubleValue(ValueSource.java:210)
            at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.DoubleValues$1.doubleValue(DoubleValues.java:79)
            at org.apache.lucene.queries.function.FunctionScoreQuery$MultiplicativeBoostValuesSource$1.doubleValue(FunctionScoreQuery.java:299)
            at org.apache.lucene.queries.function.FunctionScoreQuery$FunctionScoreWeight$1.score(FunctionScoreQuery.java:245)
            ...
            at org.apache.lucene.queries.function.TestValueSourceBridgeTotalTermFreq.testBoostByTotalTermFreq(TestValueSourceBridgeTotalTermFreq.java:49)

# the same with -Ptests.asserts=false; with the recommended fix the test passes

Why it looks unintended

  • getValues gets "the context that was previously passed to createWeight()" (ValueSource.java:44), and the bridge promises to "Expose this ValueSource", unrestricted.
  • FunctionQuery, FunctionRangeQuery, getSortField and the removed BoostedQuery all call createWeight first; boostByValue, the documented replacement, takes a ValueSource only through this bridge.
  • A custom source written as the createWeight javadoc says fails through public API alone.
The full argument271 words
  • The contract. getValues "Gets the values for this reader and the context that was previously passed to createWeight()" (ValueSource.java:43-47); createWeight: sources "can optionally store weight info in the context. The context object will be passed to getValues() where this info can be retrieved" (:65-69). The bridge is itself a caller of in.getValues and breaks that precondition; its javadoc promises to "Expose this ValueSource" (:111, :183) with no restriction.
  • The bridge has what it needs. DoubleValuesSource's class javadoc tells clients to call rewrite(IndexSearcher) against the top-level searcher before getValues (DoubleValuesSource.java:34-36); FunctionScoreQuery does, and the bridge keeps the searcher. Core's DoubleValuesSource.fromQuery does its per-query setup in rewrite, building its Weight there (:776-780).
  • Every other consumer calls it: FunctionQuery.java:71, FunctionRangeQuery.java:149, ValueSource.java:381, and the removed BoostedQuery (boostVal.createWeight(fcontext,searcher);, 7.2.0 BoostedQuery.java:74). LUCENE-8099 removed BoostedQuery and MIGRATE.md:1377-1381 maps it to boostByValue(), which reaches a ValueSource only through asDoubleValuesSource(); none of the 35 comments on #9147 discusses losing the call.
  • Tests. Upstream tests call createWeight before getValues (TestValueSources.java:697-701, :935-940) and use the bridge only with sources that need no createWeight; no test asserts that the bridge skips it.
  • History. The bridges were born without the call (LUCENE-7610, 2017), and no later change says it is skipped on purpose: not LUCENE-7736 (which gave the bridge the searcher), LUCENE-8726, LUCENE-10252 or #15406. In LUCENE-5335 (#6399, 2013) Yonik Seeley gave state keyed by this as an intended use of the context: 'SumTotalTermFreqValueSource is one example of where "this" is very conveniently used as a key'.
The devil's-advocate check258 words

The strongest case for "works as designed": the DoubleValuesSource protocol has no createWeight step; ttf and sttf are @lucene.internal, and LUCENE-7736 (#8787) added IndexReaderFunctions.totalTermFreq and sumTotalTermFreq so that such statistics work in FunctionScoreQuery; the facet code the bridge replaced also skipped createWeight; QueryValueSource and ScaleFloatFunction cope with a skipped createWeight, so robust sources should too; the bridge's javadoc is one line; and open issue #16820 proposes deprecating ValueSource in Lucene 11.

Why it fails: the replacement argument covers ttf and sttf only, not a custom source that follows the createWeight javadoc and fails through public API alone; nothing limits the bridges to sources that ignore createWeight, while the getValues javadoc makes it a precondition; LUCENE-7736 put the searcher into the bridge's context, so sources that read it (such as maxdoc()) work through the bridge, and it neither added nor ruled out createWeight-dependent sources (reading it as a sign that the bridge is meant to serve them is an inference); every native consumer and the removed BoostedQuery call it; two lazy fallbacks are defensive code, not a relaxed contract; and #16820 is a proposal, while 6.4.0 to 10.6.0 are affected. No reviewer accepted the intended case: the cross-model reviews rated it weak, the devil's advocate is highly confident it is not intended, and the independent reproducer and the adjudication found the call promised by the javadoc. Verdict: not intended, a defect. Expect "use IndexReaderFunctions" and "ValueSource is being deprecated" as replies; the custom-source case and the BoostedQuery migration answer both.

Suggested fix

Recommended B: call createWeight once in rewrite, copy the context per segment (validated)

Call in.createWeight(context, searcher) once in the bridges' rewrite(searcher), keep that context, and give each segment its own copy in getValues and explain (+20/-11 lines in ValueSource.java). The repro then scores 2 and the queries module's 512 upstream tests run with 0 failures (5 skipped); the bridge makes one createWeight call per query, like FunctionQuery.

All options and trade-offs4 options · 1,191 words

Option B was applied in a scratch worktree of 645d4fe14a20 and run with gradle (the worktree was removed after the runs). Option A, the study's sketch, was validated with a standalone runner by shadowing the patched class. Options C and D were not built.

B: call createWeight once in rewrite, copy the context per segment (validated) (recommended)

In both wrappers, rewrite(searcher) builds newContext(searcher), calls in.createWeight(context, searcher) and keeps the filled map; getValues and explain start from new IdentityHashMap<>(weightContext) instead of newContext(searcher). An un-rewritten wrapper keeps a newContext(null) map, whose only entry is "searcher" -> null, the same as today's per-segment context. WrappedLongValuesSource's now unused searcher field is replaced; WrappedDoubleValuesSource keeps its package-private searcher field, which TestValueSources checks is null before rewrite.

diff --git a/lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java b/lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java
index 8f31c9af02c..d7f3dcc547f 100644
--- a/lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java
+++ b/lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java
@@ -110,22 +110,23 @@ public abstract class ValueSource {
 
   /** Expose this ValueSource as a LongValuesSource */
   public LongValuesSource asLongValuesSource() {
-    return new WrappedLongValuesSource(this, null);
+    return new WrappedLongValuesSource(this, newContext(null));
   }
 
   private static class WrappedLongValuesSource extends LongValuesSource {
 
     private final ValueSource in;
-    private final IndexSearcher searcher;
+    // filled once per query by in.createWeight() in rewrite(), then only copied
+    private final Map<Object, Object> weightContext;
 
-    private WrappedLongValuesSource(ValueSource in, IndexSearcher searcher) {
+    private WrappedLongValuesSource(ValueSource in, Map<Object, Object> weightContext) {
       this.in = in;
-      this.searcher = searcher;
+      this.weightContext = weightContext;
     }
 
     @Override
     public LongValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException {
-      Map<Object, Object> context = newContext(searcher);
+      Map<Object, Object> context = new IdentityHashMap<>(weightContext);
 
       var scorer = new ScorableView(scores);
       context.put("scorer", scorer);
@@ -176,28 +177,34 @@ public abstract class ValueSource {
 
     @Override
     public LongValuesSource rewrite(IndexSearcher searcher) throws IOException {
-      return new WrappedLongValuesSource(in, searcher);
+      Map<Object, Object> context = newContext(searcher);
+      in.createWeight(context, searcher);
+      return new WrappedLongValuesSource(in, context);
     }
   }
 
   /** Expose this ValueSource as a DoubleValuesSource */
   public DoubleValuesSource asDoubleValuesSource() {
-    return new WrappedDoubleValuesSource(this, null);
+    return new WrappedDoubleValuesSource(this, null, newContext(null));
   }
 
   static class WrappedDoubleValuesSource extends DoubleValuesSource {
 
     final ValueSource in;
     final IndexSearcher searcher;
+    // filled once per query by in.createWeight() in rewrite(), then only copied
+    private final Map<Object, Object> weightContext;
 
-    private WrappedDoubleValuesSource(ValueSource in, IndexSearcher searcher) {
+    private WrappedDoubleValuesSource(
+        ValueSource in, IndexSearcher searcher, Map<Object, Object> weightContext) {
       this.in = in;
       this.searcher = searcher;
+      this.weightContext = weightContext;
     }
 
     @Override
     public DoubleValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException {
-      Map<Object, Object> context = newContext(searcher);
+      Map<Object, Object> context = new IdentityHashMap<>(weightContext);
 
       var scorer = new ScorableView(scores);
       context.put("scorer", scorer);
@@ -234,7 +241,7 @@ public abstract class ValueSource {
     @Override
     public Explanation explain(LeafReaderContext ctx, int docId, Explanation scoreExplanation)
         throws IOException {
-      Map<Object, Object> context = newContext(searcher);
+      Map<Object, Object> context = new IdentityHashMap<>(weightContext);
       context.put("scorer", new ScorableView(docId, scoreExplanation.getValue().floatValue()));
       FunctionValues fv = in.getValues(context, ctx);
       return fv.explain(docId);
@@ -242,7 +249,9 @@ public abstract class ValueSource {
 
     @Override
     public DoubleValuesSource rewrite(IndexSearcher searcher) throws IOException {
-      return new WrappedDoubleValuesSource(in, searcher);
+      Map<Object, Object> context = newContext(searcher);
+      in.createWeight(context, searcher);
+      return new WrappedDoubleValuesSource(in, searcher, context);
     }
 
     @Override

Trade-offs

  • One createWeight per query, like FunctionQuery. Each segment gets its own copy of the map, so getValues may add entries ("scorer", lazily built state) without writing to a shared map: the bridge itself writes the shared map only during rewrite, which avoids the one-context-shared-by-concurrent-slices problem of the native consumers for entries added in getValues. The copy is shallow: objects that createWeight stores (for example QueryValueSource's Weight) are shared across segments, as in FunctionQuery, and a source that keeps a reference to the createWeight map and writes to it later still shares it. Whether the context must be read-only after createWeight, copied deeper, or documented as holding shareable values is a maintainer decision.
  • A caller that calls getValues without rewrite behaves as today, so stateful sources still fail there. The DoubleValuesSource javadoc requires rewrite first (DoubleValuesSource.java:34-36); consumers that skip it are separate bugs with fixes in those consumers.
  • rewrite now does the per-query work (ttf and sttf loop over all segments once). Its javadoc says "Implementations should assume that this will only be called once" (DoubleValuesSource.java:94), and fromQuery already builds its Weight there (:776-780).
  • That work, and any exception a custom createWeight throws, now happens when FunctionScoreQuery.createWeight rewrites the source (FunctionScoreQuery.java:121), before it is known whether the inner query matches anything; today the bridge is reached only for segments where the inner query has a scorer (:236-239).
  • The rewritten wrapper's context holds the searcher; the rewritten wrapper held it before too (LUCENE-8726).
  • By code reading only: QueryValueSource would find its Weight in the context instead of rebuilding it once per segment. No benchmark was run.

What the checks validated

  • Gradle, stock 645d4fe14a20, seed DEADBEEF: the repro fails with assertions on and off.
  • Gradle with B: :lucene:queries:test with the repro and the study's three test classes added (the finder's 5 tests, the reproducer's 7, the devil's advocate's 6): 531 tests, 0 failures, 5 skipped, of which 512 are upstream tests. ecjLintMain passes; checkGoogleJavaFormat flags only the added test files, not ValueSource.java, so that gradle invocation (run with --continue) ends BUILD FAILED while its test task is SUCCESS. The whole module ran with assertions on only.
  • With -Ptests.asserts=false: the repro, the three study classes and TestValueSources, TestFunctionScoreQuery, TestFunctionScoreExplanations, TestFunctionQuerySort and TestKnnVectorSimilarityFunctions: 95 tests (76 upstream), 0 failures, 1 skipped.
  • The devil's advocate's call-counting probe: with B the bridge makes 1 createWeight call for 2 segments, as FunctionQuery does (stock: 0; option A: 2).

A: call createWeight on each per-segment context (the study's sketch; validated)

In each of the three methods, take the rewrite-time searcher (or, if the wrapper was never rewritten, a new top-level IndexSearcher), build the context from it and call in.createWeight(context, s) before in.getValues.

IndexSearcher s = searcher != null ? searcher : new IndexSearcher(ReaderUtil.getTopLevelContext(ctx));
Map<Object, Object> context = newContext(s);
in.createWeight(context, s);

Trade-offs

  • Simplest, and also covers callers that skip rewrite; that fallback builds a new IndexSearcher per segment and deserves its own review.
  • Repeats the per-query work per segment: 2 createWeight calls for 2 segments (executed), and ttf and sttf loop over all segments inside createWeight, so O(segments^2) term lookups per query (code reading).

What the checks validated

  • With the patched class shadowed: the repro passes with assertions on and off; the finder's 5, the reproducer's 7 and the devil's advocate's 6 tests pass; 76 upstream function tests (TestValueSources, TestFunctionScoreQuery, TestFunctionScoreExplanations, TestKnnVectorSimilarityFunctions, TestFunctionQuerySort) pass stock and with the fix. Not run through gradle.

C: make ttf and sttf compute lazily in getValues (not built)

Compute the statistic in getValues when the context has no entry, as ScaleFloatFunction and QueryValueSource do.

Trade-offs

Fixes the in-tree trigger only: a custom source that follows the createWeight javadoc stays broken, and the statistic is recomputed per segment.

D: document that the bridges skip createWeight (not built)

Say in the bridge javadoc that sources needing createWeight are not supported.

Trade-offs

Contradicts the getValues javadoc and helps no released user; a maintainer who prefers deprecating ValueSource (#16820) might still choose it.

Downstream note

The failure is a loud exception for that one query; a search server whose query language puts ttf() or sttf() inside a boost, as Apache Solr's boost parsers allow (read, not run), would answer such requests with an error.

How it was found

A bounded-exhaustive check of the createWeight contract over the queries module: 22 composites times 8 consumer entry points (176 checks), with probe children that flag getValues on a context their createWeight never saw. Under both bridges every composite's children broke it; under the native consumers only RangeMapFloatFunction (LFV-886) and the three vector similarity functions did.

The method in full264 words

Method: semi-formal (bounded-exhaustive translation validation of a documented contract on the stock jar, then an executed public-API test).

  • A hunt unit sweeping public APIs that no earlier hunt had named (core and queries) took the ValueSource#createWeight javadoc as a contract: every consumer must call createWeight on a context before calling getValues with it, and composites must propagate it.
  • An AI finder wrote a checker that, by reflection, enumerates the 46 concrete ValueSource classes of lucene-queries, builds the 22 composites (including anonymous subclasses of the public abstract composite bases) over strict probe children, and drives each through 8 consumer entry points: 176 checks. A probe child fails when getValues receives a context its createWeight never saw.
  • Under FunctionQuery, FunctionRangeQuery and ValueSource#getSortField (4 entry points) only the children of RangeMapFloatFunction's target and default sources (the sibling bug LFV-886) and of the three vector similarity functions (which do not override createWeight) saw getValues without createWeight: 8 violations per entry point. Under asDoubleValuesSource() and asLongValuesSource() (4 entry points) every composite's children did: 40 per entry point. With both fixes shadowed (this one and LFV-886's), every entry point, the bridges included, is left with the same 6 violations, the vector similarity functions' children, which this page does not analyse.
  • The AI tool then reduced the signal by hand to a JUnit with public API only (ttf, sttf, a sort and a contract-following custom source), and a later step reduced that to the one-method repro. An earlier hunt unit had noted the ttf NullPointerException as an unreported lead without filing it, because ttf is @lucene.internal.

Prior reports, history and checks

Prior reports

No prior report found (2026-10-28, re-checked 2026-10-30) in GitHub issues, PRs and commits incl. the JIRA archive, the Lucene lists, downstream trackers and the web, nor in the file lists of all 410 open PRs. The closest items are other bridge defects (#9772, #11288); #16820 proposes deprecating ValueSource.

Searches5 searches

No report of this bug was found, and upstream main still skips the call. The closest items are other defects of the same bridge or design context, not reports of this one.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs, commits (apache/lucene incl. the migrated JIRA archive; downstream repositories)2026-10-28two automated passes of 89 and 81 queries (identifiers, symptoms, consumers) and 34 manual GitHub searches, including created and updated windows for the last 48 h; the finder's, verifiers' and the adjudication's own searchesno report; the closest are #9772 (LUCENE-8726, the bridge leaked the searcher), #11288 (LUCENE-10252, eager score), #9786 (LUCENE-8741), #15406, #15328
Related items (read in full)2026-10-28bodies and comments of the closest threads#9147 (LUCENE-8099: the BoostedQuery removal; 35 comments, none about losing createWeight), #8787 (LUCENE-7736, IndexReaderFunctions), #6399 (LUCENE-5335, context design), #8661 (LUCENE-7610, introduced the bridge); none mentions this bug
Open PRs2026-10-28changed-file lists of all 410 open apache/lucene PRsonly #14775 (Sort#inverse) touches ValueSource.java, not the bridge
Mailing lists, ASF JIRA (LUCENE, SOLR), web2026-10-28dev@, java-user@, issues@, commits@ and the Solr lists; 24 JQL queries; 17 web queriesonly other bridge defects (SOLR-12407, SOLR-13126, SOLR-13303, SOLR-13315) and a newer downstream path through the bridge (SOLR-17951, read only)
Re-check for this page2026-10-30last commit on ValueSource.java on upstream main; GitHub searches for asDoubleValuesSource, asLongValuesSource, TotalTermFreqValueSource, createWeight ValueSource, FunctionScoreQuery NullPointerException; issues and PRs created since 2026-10-28unchanged (#15406, 2025-12-02, is still the last commit on the file; upstream main 716d6f216f52); nothing new

History

Introduced with the bridges by LUCENE-7610 (713b65d1dcc, 2017-01-06), first released in 6.4.0; no version ever called createWeight. It fails on every release run from 6.4.0 to 10.6.0 and on main; 6.3.0 has no such API. Not fixed upstream.

Versions tested7 rows

LUCENE-7610 "Deprecate ValueSource methods in facets module" (Alan Woodward, commits 713b65d1dcc on master and a238610bab1 on branch_6x, author date 2017-01-06; a patch on the issue, now #8661) added asDoubleValuesSource() and asLongValuesSource() without a createWeight call; CHANGES lists it under 6.4.0. Later changes reshaped the bridge without adding the call: FunctionScoreQuery (LUCENE-7623, 6.5.0), the bridge's explain (LUCENE-7741, 7.0.0), the searcher in the context (LUCENE-7736, 7.2.0), LUCENE-8099, which deprecated BoostedQuery in 7.3.0, added FunctionScoreQuery.boostByValue as its replacement (42154387d4f, 2018-01-03, first released in 7.3.0) and removed BoostedQuery in 8.0.0 (c27099b4d15, 2017-12-14), a new wrapper per rewrite (LUCENE-8726, 8.1.0 and 7.7.2), newContext(searcher) and a lazy score (LUCENE-10252, 9.1.0), and round-trip conversion (#15406, 10.4.0). Not a regression: a scan of all 77 tagged releases from 6.4.0 to 10.5.2 in the history clone (it has no tags for 8.8.2 to 8.11.4 or 10.6.0; 8.11.4 and 10.6.0 were run) finds no createWeight call in the bridge. The omission is older than the bridge: the facet module's ValueSource-based APIs (LUCENE-5297/5298, 4.6.0, 2013) already called getValues without createWeight, and on 6.3.0 ttf through LongRangeFacetCounts fails the same way (executed); LUCENE-7610 moved that pattern into the bridges and routed the facet methods through them.

The rows ran either the minimal repro (9.1.0 and later), the finder's 5-test class, or raw-Map ports of the same public-API tests for 6.4.0 to 9.0.0; every bridge test failed and every control (FunctionQuery, ValueSource#getSortField) passed. OpenJDK 25.0.3, Linux aarch64; release jars SHA-1-checked.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 (unreleased 11.0) affectedNullPointerException, -ea and -da, gradle and standalone
10.6.0 (release jars, built from 4ae946e5a9b9) affectedNullPointerException, -ea and -da (minimal repro)
10.5.2, 9.12.3, 9.1.0 affectedNullPointerException (minimal repro; the finder's class fails 4 of 5 on 10.5.2 and 9.12.3 with -ea and -da)
10.4.0, 10.0.0, branch_10_6 4ae946e5a9b affectedthe finder's class fails 4 of 5
9.0.0, 8.11.4, 8.1.0, 7.7.3, 7.2.0, 6.6.6, 6.5.0 affectedraw-Map ports fail 6 of 8 (sort and score)
6.4.0 (first release with the bridge) affectedraw-Map port fails 4 of 6, -ea and -da (sort only; FunctionScoreQuery arrived in 6.5.0)
6.3.0 n/adoes not compile: no asDoubleValuesSource()

Independent checks

The finder, an independent reproducer, an intended-behaviour devil's advocate, an adjudication, the registration check, the repro minimizer and this page's gradle runs all reproduce it, and the fixes pass. Cross-model reviews (OpenAI GPT-5.6) could run nothing: some said real, the others "not real" for procedure alone; separate re-runs failed as claimed.

Checks and verdicts375 words

Every executing check ran on the stock jars and voted real. The only dissent is procedural: one review that could not run any code voted not real at low confidence, while rating the intended-behaviour case weak; a re-run of the repro for it failed as claimed.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (finder) candidate176-check contract sweep: every composite's children see getValues without createWeight under both bridges; its JUnit fails 4 of 5 with -ea and -da; with the fix 5 of 5 pass.
Cross-model review, refute lens (OpenAI GPT-5.6) not real, low (procedural)It could not run any code, so it did not execute the repro; it rated the intended-behaviour argument weak. A re-run of the repro for it: 4 of 5 fail.
Independent reproducer realOwn JUnit (3 documents, 2 segments) fails 5 of 7 on main -ea/-da, 10.5.2, 9.12.3 and 8.11.4; controls pass; own fix 7 of 7; 76 upstream function tests pass stock and fixed.
Cross-model review, intended lens (OpenAI GPT-5.6) real, low (a read-only review: real, medium)Reading only; root cause agreed, inputs in contract, intended argument weak. Separate re-runs fail 4 of 5 on main -ea and -da.
Intended-behaviour devil's advocate real, high that it is not intendedBuilt and rejected the intended case; own 6-test probe fails 3 of 6 on main -ea and 10.5.2 -da (ProductFloatFunction, call counts, explain), 6 of 6 with the fix.
Adjudication confirmed, highRe-ran the independent reproduction's test with a new seed: 5 of 7 fail with -ea and -da.
Registration check still fails; fix passesThe finder's repro fails 4 of 5 with -ea and -da and passes 5 of 5 with the fix; the independent reproduction's test fails 5 of 7 with -da.
Repro minimizer verifiedThe one-method repro fails 1 of 1 on main 645d4fe14a20 with -ea and -da and passes with option A.
This page's runs reproduced; option B validatedGradle stock fails with assertions on and off; 10.6.0, 10.5.2, 9.12.3 and 9.1.0 fail; option B: 531 module tests with assertions on and 95 suite tests with assertions off, 0 failures; ecjLint clean, the format check flags only the four added test files.

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,432 words

Finding record

LFV-893, snapshot of 2026-11-06

Registry title
ValueSource#asDoubleValuesSource and #asLongValuesSource never call createWeight, so a ValueSource that keeps per-query state in its context (ttf, sttf, or a user ValueSource) fails with NullPointerException through FunctionScoreQuery, its explain, or a LongValuesSource sort
Area
queries / function queries (ValueSource to DoubleValuesSource/LongValuesSource bridge: asDoubleValuesSource, asLongValuesSource)
Priority
Low, rank 625: The ValueSource bridges skip createWeight and throw a loud NPE only for stateful sources, the same missing-call shape as LFV-776, and since the bridges feed FunctionScoreQuery and sorts they rank above the narrower map() sibling LFV-886.
Impact class
crash
Impact note
Hits applications that pass a ValueSource which keeps per-query state in createWeight (in-tree only TotalTermFreqValueSource (ttf) and SumTotalTermFreqValueSource (sttf), both @lucene.internal, or a function that contains them, or any user ValueSource that follows the createWeight javadoc) through asDoubleValuesSource into FunctionScoreQuery (scoring, boostByValue, explain) or through asLongValuesSource into a sort: every such search then throws, with -ea and -da, the default IndexWriterConfig and a plain IndexSearcher, at any index size (2 documents suffice), in every release from 6.4.0 to 10.6.0 and on main; the failure is loud and only that query fails, while stateless sources and the searcher-only statistics (maxdoc(), numdocs(), docfreq()) work through the bridge [exec].
Summary
You expose a ValueSource as a DoubleValuesSource with asDoubleValuesSource() and score with it through FunctionScoreQuery (for example FunctionScoreQuery.boostByValue, the documented replacement for the removed BoostedQuery), or sort by asLongValuesSource().getSortField(), and expect the per-document values the same ValueSource gives through FunctionQuery or its own getSortField (3 for ttf(text, test) in the repro). Instead the bridge builds a fresh context in getValues and explain and calls in.getValues without ever calling in.createWeight, so ttf and sttf, which compute their statistic in createWeight and return context.get(this) from getValues, throw NullPointerException ('this.val$fv' is null at ValueSource.java:210 or :144), and a user ValueSource that stores per-query state in createWeight, as the ValueSource javadoc describes, finds its context entry missing. FunctionQuery, FunctionRangeQuery and ValueSource#getSortField all call createWeight first, and the bridge's rewrite(searcher) already holds the searcher such a call needs.
Introduced
713b65d1dcc (master) / a238610bab1 (branch_6x backport), 'LUCENE-7610: Deprecate ValueSource methods in facets module' (issue LUCENE-7610, now #8661; pre-GitHub-PR era, patch on the issue) [read: git blame -w -C -C -M of main ValueSource.java:126-134 and :198-206 attributes the method skeletons to 713b65d1dcc (the in.getValues line to the LUCENE-7723 refactor 1a278ae89be, explain :235-241 to LUCENE-7741 fb24d451cc9); exec: 6.3.0 has no such API (compile error) and 6.4.0 fails]; not a regression: the bridge has never called createWeight, and none of the 77 tagged releases 6.4.0-10.5.2 adds the call (2017-01-06 (author date of 713b65d1dcc and a238610bab1; committed 2017-01-07); first released in 6.4.0)
First affected version
6.4.0 [exec] (the first release with the bridge API, CHANGES 'Lucene 6.4.0 / API Changes / LUCENE-7610'; a 6.x port fails 4 of 6 there with -ea and -da, through the bridges' getSortField, because FunctionScoreQuery only arrived in 6.5.0). 6.3.0 is not affected because the API is absent [exec: compile error 'cannot find symbol: method asDoubleValuesSource()'].
Affected versions
Every release from 6.4.0 to 10.5.2 (77 tagged releases) [read: no createWeight call in the bridge code of any of them], branch_10_6 4ae946e5a9b (10.6.0, released 2026-10-29) and main a9779ef3c193; not fixed upstream (ValueSource.java unchanged since #15406, author date 2025-12-02; upstream main c5ab3e57b9ba) [exec: the finder's JUnit or era ports fail on 6.4.0 (-ea and -da), 6.5.0, 6.6.6, 7.2.0, 7.7.3, 8.1.0, 8.11.4, 9.0.0, 9.1.0, 9.12.3 (-ea and -da), 10.0.0, 10.4.0, 10.5.2 (-ea and -da), branch_10_6 and main (-ea and -da), with exactly the bridge tests failing and every control passing; the independent reproduction's own JUnit fails 5 of 7 on main (-ea and -da), 10.5.2, 9.12.3 and 8.11.4].
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: exhaustive check
Upstream and prior reports
none found (the prior-report record verdict new, searched 2026-10-28, read-only, upstream main c5ab3e57b9ba): two scripted prior-report searches (89 and 81 queries: GitHub issues, PRs and commits, downstream repositories (Elasticsearch, OpenSearch, Solr), the dev@, java-user@, issues@ and commits@ lists, JIRA and web; 156 and 1,161 distinct hits, the only 'high' hit #8674 by term overlap) plus 110 own queries (GitHub with created/updated windows for the last 48 h, ASF JIRA LUCENE and SOLR, list archives, web) and the file lists of all 410 open PRs (only #14775, Sort#inverse, touches ValueSource.java, not the bridge); the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's searches found nothing either. Refreshed in the registration check (read-only gh): no apache/lucene issue or PR updated since 2026-10-26 matches createWeight ValueSource, TotalTermFreqValueSource, FunctionScoreQuery ValueSource or WrappedDoubleValuesSource; the only PR updated since 2026-10-26 whose files match queries/function or ValueSource is #16911 (unrelated Gradle build classes); issues and PRs created since 2026-10-26 (to #16928) are unrelated; the last commit to ValueSource.java is still #15406 (2025-12-02). Closest items, none the same bug: #9772 (LUCENE-8726, the bridge leaked the IndexSearcher), #11288 (LUCENE-10252, eager score), #9147 (LUCENE-8099, BoostedQuery deprecation; BoostedQuery's Weight called createWeight, and nobody discussed losing that call), #9786 (LUCENE-8741), #15406, #15328, SOLR-12407, SOLR-13126, SOLR-13303, SOLR-13315, SOLR-17951 (a newer downstream bridge path, read only), #6399 (LUCENE-5335, context design). Context, not a report: #16820 (open, 2026-10-12) proposes deprecating ValueSource in Lucene 11. Gaps: searches cannot prove absence; private reports are not searchable.
Lucene commit tested
main 645d4fe14a20 (2026-10-28; lucene/queries is identical to the study's a9779ef3c193, and upstream main 716d6f216f52 has the same ValueSource.java on 2026-10-30): the repro with gradle in a scratch worktree, stock with assertions on and off, and with the recommended fix together with the whole lucene/queries test suite (the worktree was removed after the runs); a standalone runner on main with -ea and -da and on the 10.6.0, 10.5.2, 9.12.3 and 9.1.0 release jars; the study's own tests and raw-Map ports on 13 releases from 6.4.0 to 10.5.2, branch_10_6 and main a9779ef3c193. JDK OpenJDK 25.0.3, Linux aarch64.

Lucene and upstream sources

Finding record
  • lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:43-47 (getValues gets 'the context that was previously passed to createWeight()'), :65-71 (createWeight: sources 'can optionally store weight info in the context. The context object will be passed to getValues()'), :111-114 and :183-186 ('Expose this ValueSource as a LongValuesSource / DoubleValuesSource'), :126-134 (WrappedLongValuesSource.getValues: newContext(searcher), in.getValues, no createWeight), :198-211 (WrappedDoubleValuesSource.getValues, doubleValue at :210), :235-241 (explain, same), :177-180 and :243-246 (rewrite(searcher) keeps the searcher), :378-383 (ValueSourceSortField.rewrite calls createWeight) @ a9779ef3c193
  • lucene/queries/src/java/org/apache/lucene/queries/function/FunctionQuery.java:71 and FunctionRangeQuery.java:149 (native consumers call createWeight before getValues); FunctionScoreQuery.java:121 (createWeight passes source.rewrite(searcher)), :182-190 (explain), :239 (scorerSupplier calls valueSource.getValues)
  • lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/TotalTermFreqValueSource.java:33 (@lucene.internal), :59-62 (getValues returns context.get(this)), :65-81 (createWeight computes the statistic and stores it); SumTotalTermFreqValueSource.java:32 (@lucene.internal)
The finding in 30 seconds
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:43-47, :65-71, :74-78, :111-114, :126-147, :177-180, :183-186, :198-222, :235-246, :379-382
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java:84-86, :121, :239, :244-245, :294-299, :315-317; FunctionQuery.java:70-71; FunctionRangeQuery.java:148-149
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/TotalTermFreqValueSource.java:33, :59-81; valuesource/SumTotalTermFreqValueSource.java:32, :51-74; valuesource/QueryValueSource.java:82-86, :114-126
  • Lucene main 645d4fe14a20: lucene/core/src/java/org/apache/lucene/search/DoubleValues.java:71-79
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/MaxDocValueSource.java:49, DocFreqValueSource.java:60 (context.get("searcher")), NumDocsValueSource.java:44-46 (ReaderUtil.getTopLevelContext(readerContext).reader().numDocs())
Who is affected
  • Lucene main 645d4fe14a20: lucene/MIGRATE.md:1377-1381
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java:177-178, :236-239 (no scorer or no match: the bridge is not evaluated)
Minimal repro
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/TotalTermFreqValueSource.java:33 (@lucene.internal)
Why it looks unintended
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:43-47, :65-69, :111, :183, :381
  • Lucene main 645d4fe14a20: lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java:34-36, :92-104, :776-780
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/FunctionQuery.java:71; FunctionRangeQuery.java:149
  • Lucene main 645d4fe14a20: lucene/MIGRATE.md:1377-1381
  • Lucene main 645d4fe14a20: lucene/queries/src/test/org/apache/lucene/queries/function/TestValueSources.java:697-701, :935-940
  • GitHub apache/lucene#6399 (LUCENE-5335), comment by Yonik Seeley, 2013-11-07 (read-only)
Suggested fix
  • Lucene main 645d4fe14a20: lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java:34-36, :94, :776-780
  • Lucene main 645d4fe14a20: lucene/queries/src/test/org/apache/lucene/queries/function/TestValueSources.java:829-834 (assertNull(source2.searcher))
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java:121, :236-239 (rewrite once per query; getValues only for segments with a scorer)
Prior reports
  • GitHub REST API, read-only, 2026-10-30: repos/apache/lucene/commits?path=lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java; search/issues for the queries listed
History
  • Lucene main 645d4fe14a20: lucene/CHANGES.txt:8612, :8633-8634

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 page: a summary card (PNG), every element checked against the code, the repro and an executed probe. Two differences from this page: the card's fix sketch is the study's per-segment fix (option A here), while this page recommends option B, which calls createWeight once at rewrite and copies the context per segment (the card's caveat line describes that trade-off); and the card was made before 10.6.0 was run, so it says "6.4.0 to 10.5.2" where this page says "6.4.0 to 10.6.0".

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.