All findings · Lucene finding · LFV-886 · rangemapfloatfunction-createweight-skips-target-and-default

RangeMapFloatFunction (map()) never calls createWeight on its target and default, so ttf, sttf or any ValueSource that keeps per-query state there throws NullPointerException

map(x, min, max, target, default) passes createWeight only to x, so a target or default that prepares its value in createWeight, such as sttf(text), is read before it exists. One document: map(1, 0, 2, sttf(text)) should score 2 and throws NullPointerException instead.

Summary

Priority
Low, rank 626Map() fails loudly with an NPE only when its target or default is ttf, sttf or a custom stateful ValueSource, a narrower case of the same createWeight gap as LFV-893, so it follows that row.
Impact
crash
Since
4.7.0 (SOLR-1871, committed 2013-12-02)introduced 2013-12-02 · over 12 years
Affected
4.7.0 to 10.6.0 and main (14 versions run)
Status
documented
Found by
AI tools checking the createWeight contract of every composite ValueSource

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

map() skips createWeight for two of its three children, so a child that prepares its value there is read before it exists.

Background: function queries and createWeight

  1. A ValueSource gives every document a number; FunctionQuery uses that number as the score. Composites such as map(), if() or sum() combine child ValueSources.
  2. Before scoring, FunctionQuery creates one context map for the query and calls createWeight(context, searcher) on the root ValueSource. A ValueSource may compute a per-query value there, for example a statistic over the whole index, and store it in the context. Composites are expected to pass the call on to their children.
  3. Then, for each segment, it calls getValues(context, segment) on the root, and each ValueSource reads back what its createWeight stored.
Summary of the bug: one document with text "x x"; the query is FunctionQuery(map(1, 0, 2, sttf(text))), built as RangeMapFloatFunction(ConstValueSource(1), 0, 2, SumTotalTermFreqValueSource("text"), null). FunctionQuery calls createWeight on map, which passes it only to the constant 1, so sttf never computes and stores its value 2 in the query context. getValues then reads source, target and default from that context; sttf returns null and floatVal throws NullPointerException at RangeMapFloatFunction.java:84 instead of scoring 2. IfFunction forwards createWeight to all three children; the fix does the same for map's target and default.
map(1, 0, 2, sttf(text)) on one document: createWeight reaches only the first argument, so sttf never stores its value 2 in the context, and reading the target returns null. 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 field is "x x". sttf(text), the field's total number of tokens, is 2.
  2. Search FunctionQuery(map(1, 0, 2, sttf(text))), i.e. new RangeMapFloatFunction(new ConstValueSource(1), 0, 2, new SumTotalTermFreqValueSource("text"), null). The value 1 lies in [0, 2], so you expect the document to get the target's value: score 2.
  3. You get NullPointerException: ... "this.val$targets" is null at RangeMapFloatFunction.java:84, with assertions on and off.
  4. Why: sttf counts the tokens once per query in createWeight and stores the result in the query's context; its getValues reads it back. map() passes createWeight on only to its first argument, so sttf finds nothing and returns null.
  5. The same happens with ttf, with sttf as the default (line 85), and a custom ValueSource that stores per-query data in createWeight finds its entry missing. IfFunction with the same children works: it forwards createWeight to all three.

Who is affected

Code that builds map(x, min, max, target, default) with a target or default that keeps per-query state: ttf, sttf (internal classes) or a custom ValueSource that stores data in createWeight. With ttf or sttf every search throws once a document takes that branch, at any index size; affected: 4.7.0 to 10.6.0 and main.

Mechanism, terms and numbers697 words

RangeMapFloatFunction is the map() function query: a document whose source value lies in [min, max] gets the target's value, any other document the default's. Since 4.7.0 the target and default may be any ValueSource. Some ValueSources, in-tree TotalTermFreqValueSource (ttf) and SumTotalTermFreqValueSource (sttf), compute their value once per query in createWeight and keep it in the per-query context, where getValues reads it back, as the ValueSource javadoc describes. map() reads all three children on that context but passes createWeight only to the source, so ttf or sttf as the target or default returns null and the first document that takes that branch throws NullPointerException; a custom ValueSource that keeps per-query state finds its entry missing. One document is enough: FunctionQuery(map(1, 0, 2, sttf(text))) should score 2 and throws, with assertions on and off and the default configuration; every release from 4.7.0 to 10.6.0 and main is affected (14 versions run, the releases in between by reading). The fix is four lines that forward createWeight to the target and the default, as every other non-vector composite does.

The bug

  1. FunctionQuery.FunctionWeight creates the context and calls createWeight on the root (FunctionQuery.java:70-71); each segment's scorer then calls func.getValues(weight.context, context) (:116). A ValueSource sort does the same in ValueSourceSortField.rewrite (ValueSource.java:379-381), and so does FunctionRangeQuery (FunctionRangeQuery.java:148-149).
  2. RangeMapFloatFunction.createWeight calls only source.createWeight(context, searcher) (valuesource/RangeMapFloatFunction.java:105-108).
  3. RangeMapFloatFunction.getValues reads all three children on that context: source (:75), target (:76) and, when non-null, defaultVal (:77-78). floatVal returns targets.floatVal(doc) for an in-range value (:83-84), else defaults.floatVal(doc) (:85).
  4. SumTotalTermFreqValueSource.createWeight sums the field's token count over all segments and stores it with context.put(this, ...) (valuesource/SumTotalTermFreqValueSource.java:57-74); its getValues is return (FunctionValues) context.get(this); (:51-54). TotalTermFreqValueSource works the same way (:59-62, :65-81). Without the call the entry is missing, targets is null, and line 84 throws.

Which children fail

  • ValueSource.newContext already puts the searcher in the context (ValueSource.java:74-77), so docfreq, maxdoc and norm, which only read it, work as targets. QueryValueSource (query(...)) and ScaleFloatFunction (scale(...)) build their state lazily inside getValues when it is missing, so they do not crash.
  • In-tree, only ttf and sttf fail, and any composite that wraps one of them, because map() never calls the wrapper's createWeight either (run: scale(sttf) and sum(sttf, const(0)) as the target throw, sum(const(3), const(4)) scores 7); plus any custom ValueSource that stores per-query data in createWeight as the javadoc describes.
  • Masking: the context is an IdentityHashMap (ValueSource.java:75) and ttf/sttf key their entry by this. If the very same ttf/sttf object is both map's source and its target, the source's createWeight fills that object's entry and the bug is hidden; equal but distinct instances do not hide it.

The numbers

  • One document "x x", map(1, 0, 2, sttf(text)): expected score 2; NullPointerException "this.val$targets" is null at line 84 (main, -ea and -da; 10.6.0 release jars, -ea and -da).
  • Two documents, ttf(text, test) as the target: the same NPE; sttf(text) as the default of an out-of-range value: NPE "this.val$defaults" is null at line 85.
  • A custom ValueSource that stores the document count in createWeight, as target or as default: its getValues finds no entry.
  • A ValueSource sort by the same map(...) after Sort.rewrite (new Sort(map.getSortField(true)).rewrite(searcher); reverse false and true both run): the same NPE, from ValueSourceComparator.copy.
  • FunctionRangeQuery over the same map(...), and IndexSearcher.explain of its FunctionQuery: the same NPE at line 84; both work with the fix.
  • Controls on the same index pass: ttf, sttf and the custom source alone; IfFunction with the same stateful children; a stateful source as map's first argument.

Not claimed here

  • map(x, min, max, query(...)) does not crash: QueryValueSource builds its Weight inside getValues and writes it into the shared context, which adds one writer to a separate, known context-map race of concurrent searches (LFV-567).
  • The three vector-similarity composites (through VectorSimilarityFunction, which has no createWeight override) skip both children too, and the propagation harness flags them, but no in-tree vector ValueSource keeps per-query state. Every "only" or "every other" on this page means the non-vector composites.
Exposure and likelihood292 words
  • Who is hit. Callers of RangeMapFloatFunction with a ValueSource target or default that keeps per-query state set in createWeight: in-tree TotalTermFreqValueSource (ttf) and SumTotalTermFreqValueSource (sttf), both public classes marked @lucene.internal, or a custom ValueSource that follows the createWeight javadoc. Consumers that call createWeight on the root fail: FunctionQuery scoring and a ValueSource sort after Sort.rewrite (both run). The hunt's propagation harness also flags FunctionQuery.explain and FunctionRangeQuery for the same two children, and the fact check ran both on the repro's index: the same NPE at line 84.
  • Who is not. map() with constants, fields, docfreq, maxdoc, norm or query(...) as target or default; arithmetic or scale(...) whose whole child tree needs no createWeight (sum(sttf, ...) and scale(sttf, ...) fail); and stateful sources as the mapped (first) argument.
  • Default vs opt-in. Default IndexWriterConfig and a plain IndexSearcher, single thread; the main() programs and the diagram probe use Lucene's stock default codec, the JUnit runs the codec the test framework picks from the seed, and all fail the same way. The API is opt-in, and FunctionQuery is @lucene.experimental.
  • Scale. One document is enough. With ttf or sttf as the target or the default, every search throws as soon as one document takes that branch (in-range values read the target, the others the default); if no document takes it, nothing fails. For ttf and sttf the failure is loud and limited to that query, with no index damage. A custom ValueSource may throw, build its state lazily, or fall back to some value when its entry is missing; a fallback would give a wrong score silently (plausible, not tested).
  • How likely. Rare: most map() calls use constants or fields, and the searches found no report since 4.7.0 (2014).

Minimal repro

One JUnit class for lucene/queries/src/test/org/apache/lucene/queries/function/, one method, public API: one document, default IndexWriterConfig, plain IndexSearcher. It fails with assertions on and off; it is also the suggested regression test.

TestRangeMapTargetCreateWeight.java (JUnit for Lucene's build)61 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 java.io.IOException;
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.ConstValueSource;
import org.apache.lucene.queries.function.valuesource.RangeMapFloatFunction;
import org.apache.lucene.queries.function.valuesource.SumTotalTermFreqValueSource;
import org.apache.lucene.search.IndexSearcher;
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;

public class TestRangeMapTargetCreateWeight extends LuceneTestCase {

  public void testStatefulTarget() throws IOException {
    try (Directory dir = new ByteBuffersDirectory()) {
      try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
        Document doc = new Document();
        doc.add(new TextField("text", "x x", Field.Store.NO));
        w.addDocument(doc);
      }
      try (DirectoryReader r = DirectoryReader.open(dir)) {
        // map(1, 0, 2, sttf(text)): 1 is in [0, 2], so expected score sttf(text) = 2
        Query q =
            new FunctionQuery(
                new RangeMapFloatFunction(
                    new ConstValueSource(1), 0, 2, new SumTotalTermFreqValueSource("text"), null));
        TopDocs td;
        try {
          td = new IndexSearcher(r).search(q, 1);
        } catch (NullPointerException e) {
          throw new AssertionError("expected score 2, got " + e, e);
        }
        assertEquals(2f, td.scoreDocs[0].score, 0f);
      }
    }
  }
}
Command
cp $REPRO/TestRangeMapTargetCreateWeight.java $LUCENE/lucene/queries/src/test/org/apache/lucene/queries/function/
cd $LUCENE && ./gradlew :lucene:queries:test --tests org.apache.lucene.queries.function.TestRangeMapTargetCreateWeight -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same failure)
Expected
1 hit with score 2: the value 1 lies in [0, 2], so the document gets sttf(text) = 2.
Observed
NullPointerException, "this.val$targets" is null at RangeMapFloatFunction.java:84, with assertions on and off.

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

More files, environment, full output229 words

The test indexes one document with TextField("text", "x x") in a ByteBuffersDirectory and runs new IndexSearcher(reader).search(q, 1) with q = FunctionQuery(RangeMapFloatFunction(ConstValueSource(1), 0, 2, SumTotalTermFreqValueSource("text"), null)). Single thread, deterministic for a given seed; the test draws no random values. The test framework still picks the codec, similarity, locale and time zone from the seed (the DEADBEEF run used codec=Asserting(Lucene104)); the failure does not depend on them: it fails with every seed run, and the main() programs on Lucene's stock default codec fail the same way. The NullPointerException is caught once and rethrown as an AssertionError so that the message states expected against actual; the NPE is kept as the cause. The test needs the org.apache.lucene.tests framework (9.1.0 and later); older releases were checked with an equivalent main() program.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); 10.6.0 and every release run back to 4.7.0 also fail (the releases in between are affected by reading); 4.6.1 and older: API absent
jdkOpenJDK 25.0.3
osLinux aarch64

Observed output

TestRangeMapTargetCreateWeight > testStatefulTarget FAILED
    java.lang.AssertionError: expected score 2, got java.lang.NullPointerException: Cannot invoke "org.apache.lucene.queries.function.FunctionValues.floatVal(int)" because "this.val$targets" is null
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:DC1CD76A43A01F29]:0)
        at org.apache.lucene.queries.function.TestRangeMapTargetCreateWeight.testStatefulTarget(TestRangeMapTargetCreateWeight.java:55)

        Caused by:
        java.lang.NullPointerException: Cannot invoke "org.apache.lucene.queries.function.FunctionValues.floatVal(int)" because "this.val$targets" is null
            at org.apache.lucene.queries.function.valuesource.RangeMapFloatFunction$1.floatVal(RangeMapFloatFunction.java:84)
            at org.apache.lucene.queries.function.FunctionQuery$AllScorer.score(FunctionQuery.java:131)
            at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.TopScoreDocCollector$1.collect(TopScoreDocCollector.java:95)
            ...
            at org.apache.lucene.core@11.0.0-SNAPSHOT/org.apache.lucene.search.IndexSearcher.search(IndexSearcher.java:624)
            at org.apache.lucene.queries.function.TestRangeMapTargetCreateWeight.testStatefulTarget(TestRangeMapTargetCreateWeight.java:53)
1 test completed, 1 failed

# the same with -Ptests.asserts=false and on the 10.6.0 release jars; with the fix the test passes

Why it looks unintended

  • The getValues javadoc says it receives "the context that was previously passed to createWeight()"; map() breaks that for its target and default.
  • Every other non-vector composite, e.g. IfFunction, forwards createWeight to all its children; a 176-check harness found no other non-vector exception.
  • SOLR-1871 (4.7.0) made target and default arbitrary ValueSources but left createWeight as written for float constants.
The full argument247 words
  • The contract. ValueSource.java:43-44: getValues "Gets the values for this reader and the context that was previously passed to createWeight()". :65-68: "Implementations should propagate createWeight to sub-ValueSources which can optionally store weight info in the context. The context object will be passed to getValues() where this info can be retrieved."
  • The siblings. IfFunction.createWeight calls all three children (valuesource/IfFunction.java:155-159), and so do DualFloatFunction, MultiFloatFunction, MultiFunction, MultiBoolFunction, ComparisonBoolFunction and the single-child composites. A harness over all 46 concrete ValueSource classes, 22 composites and 8 consumers (176 checks) found RangeMapFloatFunction the only non-vector composite that skips a child. With this fix and the separate bridge fix (LFV-893) both shadowed it reports no non-vector violation; the 48 left are all the three vector-similarity composites, which skip both children too and are not claimed.
  • The promise. lucene/CHANGES.txt:12745: "SOLR-1871: The RangeMapFloatFunction accepts an arbitrary ValueSource as target and default values."
  • The history. createWeight was written by SOLR-1432 (2009) when target and default were float constants, so forwarding to the source was complete. SOLR-1871 (4109c6db336, 2013-12-02) made them ValueSources and updated getValues, equals, hashCode and description, but not createWeight. Its JIRA thread never mentions createWeight, and no commit ever added the forwarding.
  • The tests. TestValueSources.testRangeMap (:564) uses only constant, field and sum operands, which need no createWeight; testSumTotalTermFreq (:613-617) and testTotalTermFreq (:686-690) score sttf and ttf through FunctionQuery, as legal inputs.
The devil's-advocate check172 words

The strongest case for "works as designed": the javadoc says "should", not "must", and the base createWeight is a no-op, so a ValueSource that depends on it is fragile; the in-tree triggers are @lucene.internal; QueryValueSource and ScaleFloatFunction cope lazily, which looks like a convention; FunctionQuery is experimental and an open issue (#16820) proposes deprecating ValueSource for Lucene 11; and the class is not final, so a user can override createWeight.

Why it fails: "should" does leave some design discretion (and the vector-similarity composites show that propagation is not enforced everywhere), but the getValues javadoc itself says it gets the context "previously passed to createWeight()", which map() breaks for two of its three children; a contract-following custom ValueSource fails through public constructors alone; every other non-vector composite forwards; nothing documents that children must tolerate a skipped createWeight; the SOLR-1871 diff shows an oversight; and ValueSource is not deprecated, while 9.x and 10.x are affected. A subclass that overrides createWeight is a workaround, not a contract. Verdict: not intended, a defect.

Suggested fix

Recommended A: forward createWeight to every child (validated)

Forward createWeight to target and, when it is not null, to defaultVal, as IfFunction does with its three children: 4 lines in RangeMapFloatFunction.createWeight. With it the repro and all 111 tests of the upstream function-query test classes pass.

public void createWeight(Map<Object, Object> context, IndexSearcher searcher) throws IOException {
  source.createWeight(context, searcher);
  target.createWeight(context, searcher);
  if (defaultVal != null) {
    defaultVal.createWeight(context, searcher);
  }
}
All options and trade-offs3 options · 468 words

Option A was applied in a scratch worktree of 645d4fe14a20 and run with gradle (the worktree was removed after the runs); the hunt's earlier checks validated the same diff. Options B and C were not built as fixes.

A: forward createWeight to every child (validated) (recommended)

+4 lines in RangeMapFloatFunction.createWeight (valuesource/RangeMapFloatFunction.java:105-108): also call target.createWeight and, when defaultVal is not null, defaultVal.createWeight.

public void createWeight(Map<Object, Object> context, IndexSearcher searcher) throws IOException {
  source.createWeight(context, searcher);
  target.createWeight(context, searcher);
  if (defaultVal != null) {
    defaultVal.createWeight(context, searcher);
  }
}

Trade-offs

  • Restores the getValues contract for both children and matches every other non-vector composite. Stateless children are unaffected (their createWeight is the base no-op). defaultVal may be null, hence the guard.
  • Cost: one createWeight call per child occurrence per query, not per document, as in the sibling composites. It runs even when no document takes that branch, and a child object that appears in two positions gets it twice (a repeated ttf/sttf sums its statistic again; a repeated query(...) overwrites its Weight entry).
  • Side effect worth naming: with query(...) as target or default, QueryValueSource now builds its Weight in createWeight instead of lazily inside getValues, which removes that lazy write into the shared context (the separate context-map race, LFV-567, remains).

What the checks validated

  • Gradle, stock 645d4fe14a20 (seed DEADBEEF): the repro fails with assertions on and off.
  • Gradle with the fix, assertions on: the repro and all 14 upstream test classes of org.apache.lucene.queries.function (including TestValueSources, TestFunctionQuerySort, TestFunctionRangeQuery and TestFunctionQueryExplanations): 15 classes, 112 tests (111 upstream), 0 failures, 0 skipped.
  • Gradle with the fix, -Ptests.asserts=false: the repro and TestValueSources, 42 tests, 0 failures; :lucene:queries:checkGoogleJavaFormat passes.
  • The standalone runner with the fixed class shadowed into the stock main jars: the repro passes with -ea and -da. The hunt's earlier checks validated the same diff: the finder's 4 tests and an independent 8-test suite pass, and with this fix and the separate bridge fix (LFV-893) both shadowed, the 176-check harness reports no violation for RangeMapFloatFunction or any other non-vector composite (48 vector-similarity violations remain, not claimed).

B: workaround without a Lucene change (not a fix)

The class is not final and its fields are protected: a subclass can override createWeight the same way. The version probes ran exactly this as a control group, and it passes on every version run.

Trade-offs

Helps users on old releases; leaves the bug in place for everyone else.

C: make ttf and sttf compute lazily when the context entry is missing (not built)

Like ScaleFloatFunction, compute the statistic inside getValues when createWeight was skipped and cache it in the shared context (ScaleFloatFunction computes its ScaleInfo once, on the first getValues call, and stores it there).

Trade-offs

Fixes only the two in-tree triggers; custom ValueSources that follow the javadoc still fail.

Downstream note

Solr's function-query parser builds map() from this class and ttf() / sttf() from the two failing sources, so a request such as map(x,0,2,ttf(f,t)) reaches this code there (read, not run); only that request fails.

How it was found

AI tools sweeping public APIs no earlier hunt had named checked, for every composite ValueSource in lucene-queries, whether createWeight reaches each child whose values it reads: 176 composite and consumer checks with probe children. map() was the only non-vector composite that skips children; the case was then reduced to a one-document test.

The method in full147 words

Method: semi-formal (bounded-exhaustive check of one API contract on the stock jars).

  • While writing the reference for the ValueSource#createWeight propagation contract, the finder noticed that RangeMapFloatFunction.createWeight reaches only source.
  • A harness then used reflection over all 46 concrete public ValueSource classes of lucene-queries, built 22 composites (including anonymous subclasses of the 6 abstract composite bases) with strict probe children that flag getValues on a context their createWeight never saw, and ran each under 8 consumer entry points: 176 checks. Apart from the three vector-similarity composites, RangeMapFloatFunction was the only composite that skipped children under the consumers that call createWeight. With this fix and the separate bridge fix (LFV-893) both shadowed, no non-vector composite is flagged; the vector-similarity violations remain and are not claimed.
  • The case was reduced to a public-API JUnit with ttf, sttf and a custom stateful source, and later to the one-document test.

Prior reports, history and checks

Prior reports

No prior report found in GitHub issues, PRs and commits (incl. the JIRA archive), Solr JIRA, the mailing lists, downstream repositories, the web and all 410 open PRs; re-checked 2026-10-30. Not fixed upstream.

Searches4 searches

No report of this bug was found, and upstream main still forwards createWeight only to the source: the file's last change is a 2020 formatting commit. The closest items are context, not reports: SOLR-1871 introduced it without discussing createWeight; #7946 (LUCENE-6888) is about the explain text of map(); #16820 (open) proposes deprecating ValueSource in Lucene 11.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs, commits (apache/lucene incl. the migrated JIRA archive; apache/solr, apache/lucenenet, OpenSearch, Elasticsearch)2026-10-28three automated passes of 51 queries each (identifiers and exact error text, the trigger as a user would hit it, the contract wording) plus manual searches for RangeMapFloatFunction, map function, createWeight ValueSource, ttf/sttf class names and 'val$targets'no report; the issue search for the class name returned only #7946 (LUCENE-6888, explain of map())
JIRA (SOLR, LUCENE) and mailing lists2026-10-28JQL for RangeMapFloatFunction, map + NPE, map + ttf/totaltermfreq, createWeight + ValueSource, with comment scans of every map() issue; dev@, java-user@, issues@, commits@ and the Solr listscontext only: SOLR-1871 (introducing change), SOLR-1432, SOLR-3963, SOLR-5614, #6399 (LUCENE-5335), SOLR-7046 (different fix site)
Open PRs and git history2026-10-28changed-file lists of all 410 open apache/lucene PRs; git log -S 'target.createWeight' and -S 'defaultVal.createWeight'no PR touches RangeMapFloatFunction.java; no commit ever added the forwarding
Re-check for this page2026-10-30last commits on the file and on lucene/queries function code; issues mentioning RangeMapFloatFunction; ValueSource and createWeight issues created since 2026-10-27unchanged (last change 2020-12-28, LUCENE-9570 formatting); nothing new; #16820 still open with no comments

History

Introduced by SOLR-1871 (committed 2013-12-02), first released in 4.7.0. Every release from 4.7.0 to 10.6.0 and main is affected (14 versions run, the releases in between by reading); 4.6.1 and older lack the ValueSource target/default constructor.

Versions tested8 rows

SOLR-1871 "Function Query 'map' variant that allows 'target' to be an arbitrary ValueSource" (patch by Chris Harris, committed by Shalin Shekhar Mangar; trunk 4109c6db3365, branch_4x backport b7ac9803c826, both 2013-12-02; pre-GitHub, no PR) turned target and default from float constants into ValueSources and made getValues read them on the query context, but left createWeight, written by SOLR-1432 (1eb46979775, 2009) for constant targets, forwarding only to the source. Later edits (LUCENE-5569, LUCENE-6888, LUCENE-7013, LUCENE-7407, LUCENE-9394, LUCENE-9570) never touch the forwarding; the file is the same blob from 9.0.0 through 10.6.0 and upstream main.

JUnit tests ran on 9.1.0, 9.12.3, 10.5.2, the 10.6.0 commit and main; an era-ported main() program with 13 checks (reference values, controls, the 4 bug checks and the same 4 queries through a forwarding subclass) ran on 11 releases from 4.7.0 to 10.5.2 and on main, and only the 4 bug checks fail on each. All on OpenJDK 25.0.3, Linux aarch64. Releases between the executed ones are affected by reading.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 (unreleased 11.0) affectedfails, -ea and -da (gradle and standalone runner)
10.6.0 (4ae946e5a9b9) affectedfails, -ea and -da (minimal repro on the release jars; the finder's test on the same commit: 3 of 4 fail)
10.5.2 affectedfails, -ea and -da (3 of 4; an independent test 5 of 8)
9.12.3, 9.1.0 affectedfail (3 of 4; on 9.12.3 an independent test 5 of 8)
10.0.0, 9.0.0 affectedfail (main() program: the 4 bug checks)
8.11.4, 7.7.3, 6.6.6, 5.5.5, 5.0.0 affectedfail (main() program: the 4 bug checks)
4.10.4, 4.7.0 affectedfail (4.x main() program; 4.7.0 with -ea and -da)
4.6.1 n/aAPI absent: the probe does not compile; a float-API control passes 9 of 9

Independent checks

The finder, an independent reproducer, an intended-behaviour devil's advocate, an adjudication, a credibility audit and this page's gradle runs all reproduce it. Cross-model reviews (OpenAI GPT-5.6) could run nothing: one said real, the other not real for procedure only; 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: a cross-model refute review that could run no Java voted not real on procedure only at low confidence while agreeing with the root cause, and a separate re-run of the repro failed as claimed.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (finder) candidate176-check propagation harness; JUnit fails 3 of 4 on main (-ea and -da), 10.5.2, 9.12.3 and the 10.6.0 commit (the version-history runs re-ran the same test on 9.1.0 and on 10.5.2 with -da); passes 4 of 4 with the fix.
Cross-model review, refute lens (OpenAI GPT-5.6, read-only) not real, low (procedural)Could run no Java; root cause agreed, inputs in contract, counter-argument weak. A separate run re-ran the repro: 3 of 4 fail.
Independent reproducer realOwn JUnit with the sub-source alone as oracle: 5 of 8 fail on main (-ea and -da), 10.5.2 and 9.12.3, including the sort path; 3 controls pass; 8 of 8 with the fix.
Cross-model review, intended lens (OpenAI GPT-5.6, read-only) real, lowIntended-behaviour argument weak; root cause agreed; separate re-runs fail on main, 10.5.2 and 9.12.3 and pass with the fix.
Intended-behaviour devil's advocate real, highBuilt the 'should, internal, experimental, deprecate' case and rejected it; re-runs fail on main and 10.5.2.
Adjudication confirmed, high (severity Low)Re-ran the independent reproduction's test with a new seed: 5 of 8 fail with -ea and -da; 8 of 8 pass with the fix.
History follow-up 4.7.0 first affectedEra-ported main() programs on 11 releases from 4.7.0 to 10.5.2 and on main; 4.6.1 compile check and controls.
Credibility audit real, new; score loweredRe-ran the finder's test (3 of 4 fail, -da) and the independent reproduction's (5 of 8 fail, 8 of 8 with the fix).
Minimal repro verifiedThe one-document test fails on 645d4fe14a20 with -ea, -da and a second seed (5 iterations); passes with the fix.
This page's own runs reproduced; fix validatedGradle in a scratch worktree: the repro fails stock with assertions on and off; with the fix it and 111 upstream function-query tests pass. The 10.6.0 release jars fail with -ea and -da.

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

Finding record

LFV-886, snapshot of 2026-11-06

Registry title
RangeMapFloatFunction (map()) never calls createWeight on its target and default value sources, so a target or default that keeps per-query state in the context (ttf, sttf, or a user ValueSource) fails with NullPointerException
Area
queries / function queries (RangeMapFloatFunction, map(): createWeight propagation to sub-ValueSources)
Priority
Low, rank 626: Map() fails loudly with an NPE only when its target or default is ttf, sttf or a custom stateful ValueSource, a narrower case of the same createWeight gap as LFV-893, so it follows that row.
Impact class
crash
Impact note
Hits applications that build map(x, min, max, target, default), i.e. RangeMapFloatFunction with a ValueSource target or default, where that target or default keeps per-query state that its createWeight puts in the context (in-tree only TotalTermFreqValueSource (ttf) and SumTotalTermFreqValueSource (sttf), both @lucene.internal, or a composite that wraps them; or any user ValueSource that follows the createWeight javadoc), and run it through a consumer that calls createWeight on the root (FunctionQuery scoring and ValueSource#getSortField after Sort.rewrite, both executed): then every such search throws as soon as a document reaches the target (or default) branch, with -ea and -da, the default IndexWriterConfig and a plain IndexSearcher, at any index size (2 documents suffice), in every release from 4.7.0 to 10.6.0 and on main; the failure is loud (a NullPointerException for ttf/sttf), only that query fails, and map(x, min, max, query(...)) does not crash because QueryValueSource builds its Weight lazily.
Summary
You build map(x, 0, 2, ttf(text, test)) as new RangeMapFloatFunction(source, 0, 2, new TotalTermFreqValueSource(...), null) and score it with FunctionQuery, expecting every document whose x lies in [0, 2] to get the target's value (3 in the repro, the score FunctionQuery(ttf) gives on its own). Instead the search throws NullPointerException ('this.val$targets' is null at RangeMapFloatFunction.java:84); sttf as the default fails the same way (:85), and a user ValueSource that stores a per-query statistic in createWeight, as the ValueSource javadoc describes, finds its context entry missing. The cause: RangeMapFloatFunction.createWeight forwards only to source, while getValues also asks target and default for their values on the same context, whereas every other composite ValueSource forwards createWeight to all of its children.
Introduced
4109c6db3365a901de47c97dcb41e673ee62ec95 (trunk, svn r1546926; branch_4x backport b7ac9803c82619b77529af906f36e149e1d53cae, svn r1546927), SOLR-1871 'Function Query "map" variant that allows "target" to be an arbitrary ValueSource' (pre-GitHub, no PR; CHANGES 4.7.0: 'SOLR-1871: The RangeMapFloatFunction accepts an arbitrary ValueSource as target and default values.') [read: diff, git blame and git log -S in the full-history clone; release boundary executed]: it turned target and default from float constants into ValueSources and made getValues call target.getValues and defaultVal.getValues on the query context, but left createWeight forwarding only to source. That method dates from SOLR-1432 (1eb46979775, 2009-09-17), when target and default were constants and forwarding to source was complete. git log -S 'target.createWeight' and -S 'defaultVal.createWeight' find no commit that ever added the forwarding. (2013-12-02 (trunk 4109c6db336 at 08:33:33Z, branch_4x b7ac9803c82 at 08:35:09Z; author date = commit date for both); first released in 4.7.0)
First affected version
4.7.0 [exec] (the first release with the ValueSource target/default constructor; an era-ported main() probe fails the same 4 of 13 checks there with -ea and -da). 4.6.1 and earlier are not affected because the API is absent [exec: the 4.x probe does not compile on 4.6.1, 'ValueSource cannot be converted to float'; a float-API control passes 9 of 9 on 4.6.1 and 4.7.0].
Affected versions
Every release from 4.7.0 to 10.5.2 (119 releases listed in the follow-up), branch_10_6 4ae946e5a9b (10.6.0, released 2026-10-29) and main a9779ef3c193; no upstream fix. [exec: the era-ported main() probe (13 checks: reference, controls, the 4 bug checks and the same 4 queries through a subclass that forwards createWeight) fails only the 4 bug checks on 4.7.0 (-ea and -da), 4.10.4, 5.0.0, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main; the finder's JUnit fails 3 of 4 on 9.1.0, 9.12.3, 10.5.2 (-ea and -da), branch_10_6 and main (-ea and -da); the independent reproduction's own JUnit fails 5 of 8 on main (-ea and -da), 10.5.2 and 9.12.3] [read: every later change to RangeMapFloatFunction.java (LUCENE-5569 rename, LUCENE-6888 explain text, LUCENE-7013 header, LUCENE-7407 throws IOException, LUCENE-9394 generics, LUCENE-9570 formatting) leaves createWeight forwarding only to source; the file is byte-identical (blob ec6a67c678e2) at 9.0.0, 9.12.3, 10.5.2, a9779ef3c193, branch_10x, branch_10_6 and upstream main c5ab3e57b9b (2026-10-28)].
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal: code reading; also reached by exhaustive check
Upstream and prior reports
none found (the prior-report record verdict new, searched 2026-10-28 plus the finder's run, the independent reproduction's and the adjudication's checks, read-only; refreshed in the registration check by listing the newest apache/lucene PRs, issues and commits through the REST API, as the search API was rate-limited then): three scripted prior-report searches of 51 queries each (GitHub issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, JIRA, web; 1,818, 1,644 and 1,173 distinct hits, none rated high) plus the finder's 51-query run; manual GitHub searches (RangeMapFloatFunction, 'map function', createWeight ValueSource, TotalTermFreqValueSource, SumTotalTermFreqValueSource, 'val$targets', sub-ValueSources) on apache/lucene, apache/solr, apache/lucenenet, OpenSearch and Elasticsearch; JIRA SOLR and LUCENE JQL (RangeMapFloatFunction, map + ttf/totaltermfreq + NullPointerException, createWeight + ValueSource) with comment scans of SOLR-1871, SOLR-3963, LUCENE-6888, SOLR-5614, SOLR-7046 and SOLR-13179; Lucene and Solr list archives; git log -S over the full history. No issue, PR, commit, JIRA entry, list thread or downstream report describes RangeMapFloatFunction (map()) skipping createWeight on its target or default. Closest items, none the same bug: SOLR-1871 (the introducing change, no createWeight discussion), SOLR-3963 (an earlier ValueSource-target patch, reviewed for description and performance), #7946 (LUCENE-6888, explain of map() does not show the default), #6399 (LUCENE-5335, context Map type), SOLR-7046 (a Solr grouping NPE from a shadowed context, different fix site). Context, not a report: #16820 (open, 2026-10-12, no comments) proposes deprecating ValueSource in Lucene 11. Not fixed: all 410 open apache/lucene PRs were listed with their files and none touches RangeMapFloatFunction.java (5 touch lucene/queries/.../function, none about createWeight propagation); newer items (#16926-#16928) are unrelated; the last commit under lucene/queries/.../function is #16814 (author date 2026-10-13, removes VectorValueSource and MultiValueSource), and the file last changed on 2020-12-28 (LUCENE-9570 formatting). Gaps: searches cannot prove absence; private reports are not searchable.
Lucene commit tested
main 645d4fe14a20 (commit of 2026-10-28; RangeMapFloatFunction.java is the same blob on upstream main 716d6f216f52, 2026-10-29): the repro with gradle in a scratch worktree, stock with assertions on and off, and with the fix together with the 14 upstream test classes of org.apache.lucene.queries.function (the worktree was removed after the runs); a standalone runner on main (two seeds) and on the 10.6.0 release jars with -ea and -da; the earlier test runs of the finder, an independent reproducer, a devil's advocate, an adjudication and an audit on main a9779ef3c193 (no change under lucene/queries since), 10.5.2, 9.12.3, 9.1.0 and the 10.6.0 commit; era-ported main() programs on releases back to 4.7.0, and 4.6.1 as the boundary. OpenJDK 25.0.3, Linux aarch64.

Lucene and upstream sources

Finding record
  • lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/RangeMapFloatFunction.java:73-86 (getValues: source, target, defaultVal on one context; floatVal :84-85), :105-108 (createWeight forwards only to source) @ a9779ef3c193
  • lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:43-45 (getValues gets 'the context that was previously passed to createWeight()'), :65-69 ('Implementations should propagate createWeight to sub-ValueSources ...')
  • lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/TotalTermFreqValueSource.java:33 (@lucene.internal), :59-61 (getValues returns context.get(this)), :65-81 (createWeight stores the FunctionValues); SumTotalTermFreqValueSource.java:32 (@lucene.internal)
The finding in 30 seconds
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/FunctionQuery.java:38, :70-71, :116, :131; ValueSource.java:43-47, :65-69, :74-77, :379-381, :429; FunctionRangeQuery.java:148-149
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/RangeMapFloatFunction.java:73-86, :105-108; SumTotalTermFreqValueSource.java:51-54, :57-74; TotalTermFreqValueSource.java:59-62, :65-81; QueryValueSource.java:82-86, :114-126; ScaleFloatFunction.java:106; DocFreqValueSource.java:60; MaxDocValueSource.java:49; NormValueSource.java:66; VectorSimilarityFunction.java (no createWeight override)
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:75 (newContext: IdentityHashMap); valuesource/TotalTermFreqValueSource.java and SumTotalTermFreqValueSource.java (context.put(this, ...))
Who is affected
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/TotalTermFreqValueSource.java:33, SumTotalTermFreqValueSource.java:32 (@lucene.internal); FunctionQuery.java:38 (@lucene.experimental)
Why it looks unintended
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:43-47, :65-69; valuesource/IfFunction.java:155-159; DualFloatFunction.java:82-85; MultiFloatFunction.java:100-104; MultiFunction.java:165-169; MultiBoolFunction.java:107-111; ComparisonBoolFunction.java:97-100
  • Lucene main 645d4fe14a20: lucene/CHANGES.txt:12745-12746; lucene/queries/src/test/org/apache/lucene/queries/function/TestValueSources.java:564, :613-617, :686-690
  • GitHub apache/lucene#16820 (open, 0 comments, read 2026-10-30)
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/VectorSimilarityFunction.java (getValues reads vector1 and vector2; no createWeight override)
Suggested fix
  • Lucene main 645d4fe14a20: lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/QueryValueSource.java:82-86, :114-126; ScaleFloatFunction.java:98, :106-109
History
  • a Lucene clone: git merge-base --is-ancestor b7ac9803c82 releases/lucene-solr/4.7.0
  • GitHub apache/lucene refs/tags/releases/lucene/10.6.0 (tag object e29d290eeb05 -> commit 4ae946e5a9b9, tagged 2026-10-29); contents API sha of RangeMapFloatFunction.java at 10.6.0 and main: ec6a67c678e2

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), built from facts checked against the code and the minimal repro.

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.