map() skips createWeight for two of its three children, so a child that prepares its value there is read before it exists.
- A
ValueSourcegives every document a number;FunctionQueryuses that number as the score. Composites such asmap(),if()orsum()combine child ValueSources. - Before scoring,
FunctionQuerycreates one context map for the query and callscreateWeight(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. - Then, for each segment, it calls
getValues(context, segment)on the root, and each ValueSource reads back what itscreateWeightstored.
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.- Index one document whose
textfield is "x x".sttf(text), the field's total number of tokens, is 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. - You get
NullPointerException: ... "this.val$targets" is nullatRangeMapFloatFunction.java:84, with assertions on and off. - Why:
sttfcounts the tokens once per query increateWeightand stores the result in the query's context; itsgetValuesreads it back.map()passescreateWeighton only to its first argument, sosttffinds nothing and returns null. - The same happens with
ttf, withsttfas the default (line 85), and a custom ValueSource that stores per-query data increateWeightfinds its entry missing.IfFunctionwith the same children works: it forwardscreateWeightto all three.
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.
FunctionQuery.FunctionWeightcreates the context and callscreateWeighton the root (FunctionQuery.java:70-71); each segment's scorer then callsfunc.getValues(weight.context, context)(:116). A ValueSource sort does the same inValueSourceSortField.rewrite(ValueSource.java:379-381), and so doesFunctionRangeQuery(FunctionRangeQuery.java:148-149).RangeMapFloatFunction.createWeightcalls onlysource.createWeight(context, searcher)(valuesource/RangeMapFloatFunction.java:105-108).RangeMapFloatFunction.getValuesreads all three children on that context:source(:75),target(:76) and, when non-null,defaultVal(:77-78).floatValreturnstargets.floatVal(doc)for an in-range value (:83-84), elsedefaults.floatVal(doc)(:85).SumTotalTermFreqValueSource.createWeightsums the field's token count over all segments and stores it withcontext.put(this, ...)(valuesource/SumTotalTermFreqValueSource.java:57-74); itsgetValuesisreturn (FunctionValues) context.get(this);(:51-54).TotalTermFreqValueSourceworks the same way (:59-62,:65-81). Without the call the entry is missing,targetsis null, and line 84 throws.
ValueSource.newContextalready puts the searcher in the context (ValueSource.java:74-77), sodocfreq,maxdocandnorm, which only read it, work as targets.QueryValueSource(query(...)) andScaleFloatFunction(scale(...)) build their state lazily insidegetValueswhen it is missing, so they do not crash.- In-tree, only
ttfandsttffail, and any composite that wraps one of them, becausemap()never calls the wrapper'screateWeighteither (run:scale(sttf)andsum(sttf, const(0))as the target throw,sum(const(3), const(4))scores 7); plus any custom ValueSource that stores per-query data increateWeightas the javadoc describes. - Masking: the context is an
IdentityHashMap(ValueSource.java:75) and ttf/sttf key their entry bythis. If the very same ttf/sttf object is both map's source and its target, the source'screateWeightfills that object's entry and the bug is hidden; equal but distinct instances do not hide it.
- 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: itsgetValuesfinds no entry. - A ValueSource sort by the same
map(...)afterSort.rewrite(new Sort(map.getSortField(true)).rewrite(searcher);reversefalse and true both run): the same NPE, fromValueSourceComparator.copy. FunctionRangeQueryover the samemap(...), andIndexSearcher.explainof itsFunctionQuery: the same NPE at line 84; both work with the fix.- Controls on the same index pass: ttf, sttf and the custom source alone;
IfFunctionwith the same stateful children; a stateful source as map's first argument.
map(x, min, max, query(...))does not crash:QueryValueSourcebuilds its Weight insidegetValuesand 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 nocreateWeightoverride) 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
RangeMapFloatFunctionwith a ValueSource target or default that keeps per-query state set increateWeight: in-treeTotalTermFreqValueSource(ttf) andSumTotalTermFreqValueSource(sttf), both public classes marked@lucene.internal, or a custom ValueSource that follows thecreateWeightjavadoc. Consumers that callcreateWeighton the root fail:FunctionQueryscoring and a ValueSource sort afterSort.rewrite(both run). The hunt's propagation harness also flagsFunctionQuery.explainandFunctionRangeQueryfor 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,normorquery(...)as target or default; arithmetic orscale(...)whose whole child tree needs nocreateWeight(sum(sttf, ...)andscale(sttf, ...)fail); and stateful sources as the mapped (first) argument. - Default vs opt-in. Default
IndexWriterConfigand a plainIndexSearcher, single thread; themain()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, andFunctionQueryis@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).
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.
/*
* 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);
}
}
}
}
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.
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.
| lucene | main 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 |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux aarch64 |
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- The
getValuesjavadoc 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, forwardscreateWeightto 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
createWeightas 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.createWeightcalls all three children (valuesource/IfFunction.java:155-159), and so doDualFloatFunction,MultiFloatFunction,MultiFunction,MultiBoolFunction,ComparisonBoolFunctionand the single-child composites. A harness over all 46 concrete ValueSource classes, 22 composites and 8 consumers (176 checks) foundRangeMapFloatFunctionthe 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.
createWeightwas 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 updatedgetValues,equals,hashCodeanddescription, but notcreateWeight. Its JIRA thread never mentionscreateWeight, and no commit ever added the forwarding. - The tests.
TestValueSources.testRangeMap(:564) uses only constant, field and sum operands, which need nocreateWeight;testSumTotalTermFreq(:613-617) andtestTotalTermFreq(:686-690) score sttf and ttf throughFunctionQuery, 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.
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.
+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
getValuescontract for both children and matches every other non-vector composite. Stateless children are unaffected (theircreateWeightis the base no-op).defaultValmay be null, hence the guard. - Cost: one
createWeightcall 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 repeatedquery(...)overwrites its Weight entry). - Side effect worth naming: with
query(...)as target or default,QueryValueSourcenow builds its Weight increateWeightinstead of lazily insidegetValues, 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(includingTestValueSources,TestFunctionQuerySort,TestFunctionRangeQueryandTestFunctionQueryExplanations): 15 classes, 112 tests (111 upstream), 0 failures, 0 skipped. - Gradle with the fix,
-Ptests.asserts=false: the repro andTestValueSources, 42 tests, 0 failures;:lucene:queries:checkGoogleJavaFormatpasses. - 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
RangeMapFloatFunctionor any other non-vector composite (48 vector-similarity violations remain, not claimed).
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.
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.
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.
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#createWeightpropagation contract, the finder noticed thatRangeMapFloatFunction.createWeightreaches onlysource. - 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
getValueson a context theircreateWeightnever saw, and ran each under 8 consumer entry points: 176 checks. Apart from the three vector-similarity composites,RangeMapFloatFunctionwas the only composite that skipped children under the consumers that callcreateWeight. 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.
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.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs, commits (apache/lucene incl. the migrated JIRA archive; apache/solr, apache/lucenenet, OpenSearch, Elasticsearch) | 2026-10-28 | three 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 lists | 2026-10-28 | JQL for RangeMapFloatFunction, map + NPE, map + ttf/totaltermfreq, createWeight + ValueSource, with comment scans of every map() issue; dev@, java-user@, issues@, commits@ and the Solr lists | context only: SOLR-1871 (introducing change), SOLR-1432, SOLR-3963, SOLR-5614, #6399 (LUCENE-5335), SOLR-7046 (different fix site) |
| Open PRs and git history | 2026-10-28 | changed-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 page | 2026-10-30 | last commits on the file and on lucene/queries function code; issues mentioning RangeMapFloatFunction; ValueSource and createWeight issues created since 2026-10-27 | unchanged (last change 2020-12-28, LUCENE-9570 formatting); nothing new; #16820 still open with no comments |
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.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 (unreleased 11.0) | affected | fails, -ea and -da (gradle and standalone runner) |
10.6.0 (4ae946e5a9b9) | affected | fails, -ea and -da (minimal repro on the release jars; the finder's test on the same commit: 3 of 4 fail) |
| 10.5.2 | affected | fails, -ea and -da (3 of 4; an independent test 5 of 8) |
| 9.12.3, 9.1.0 | affected | fail (3 of 4; on 9.12.3 an independent test 5 of 8) |
| 10.0.0, 9.0.0 | affected | fail (main() program: the 4 bug checks) |
| 8.11.4, 7.7.3, 6.6.6, 5.5.5, 5.0.0 | affected | fail (main() program: the 4 bug checks) |
| 4.10.4, 4.7.0 | affected | fail (4.x main() program; 4.7.0 with -ea and -da) |
| 4.6.1 | n/a | API absent: the probe does not compile; a float-API control passes 9 of 9 |
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.
| Check | Verdict | What it found |
|---|---|---|
| Finder (finder) | candidate | 176-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 | real | Own 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, low | Intended-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, high | Built 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 affected | Era-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 lowered | Re-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 | verified | The 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 validated | Gradle 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. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 491 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,468 words
- 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.javais the same blob on upstream main716d6f216f52, 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 oforg.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 maina9779ef3c193(no change underlucene/queriessince), 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.
- 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) @ a9779ef3c193lucene/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-149Lucene 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-100Lucene main 645d4fe14a20: lucene/CHANGES.txt:12745-12746; lucene/queries/src/test/org/apache/lucene/queries/function/TestValueSources.java:564, :613-617, :686-690GitHub 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.0GitHub 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
- TestRangeMapTargetCreateWeight.java (JUnit for Lucene's build):
repro/LFV-886/src/org/apache/lucene/queries/function/TestRangeMapTargetCreateWeight.java(61 lines)
Made in parallel with this page: a summary card (PNG), built from facts checked against the code and the minimal repro.
assets/LFV-886/bug.png(491 KB)
