One precomputed power, pivot^exp, leaves the double range, and the query's scores become NaN.
FeatureFieldstores a static per-document value S, such as a popularity or pagerank signal, andnewSigmoidQueryturns it into a score: weight * S^a / (S^a + pivot^a). The score grows from 0 to weight and is weight/2 when S equals the pivot.- The exponent a sets how sharply the score switches on around the pivot: a large exponent makes it almost a step, "slower before 'pivot' and faster after 'pivot'".
- A Java double holds values up to about 1.8e308, just under 2^1024. A larger result becomes +Infinity, and Infinity / Infinity is NaN; a result below about 2^-1074 becomes 0, and 0 / 0 is NaN too.
- Index one document with
new FeatureField("f", "x", 1 << 20)(feature value S = 1,048,576) and searchFeatureField.newSigmoidQuery("f", "x", 1, 1 << 20, 60): weight 1, pivot 2^20, exponent 60. The factory accepts both; it only rejects values that are not positive and finite. - You expect 0.5: the javadoc gives the score as weight * S^a / (S^a + pivot^a), which is weight/2 when S equals the pivot.
- You get NaN, and no exception. The sigmoid computes pivot^60 = 2^1200 once, as a double. The largest double is just under 2^1024, so it becomes +Infinity, and every document scores 1 - Infinity/Infinity = NaN.
- In a real query the ranking breaks: with
body:luceneas MUST and this sigmoid as a SHOULD boost, a document with feature value 1 (documented boost about 0) comes first with score NaN, ahead of a better text match that scores 0.130. - It needs a steep exponent: the first failing pivot is close to 2^(1024/exp), e.g. about 137,271 at exponent 60 or about 1,209.3 at exponent 100, and nothing fails for exponents up to 8. A tiny pivot can underflow to 0 and give 0/0 = NaN; the 4-argument
IntervalQueryrepeats the same arithmetic.
Applications that rank with the opt-in FeatureField.newSigmoidQuery, or the 4-argument IntervalQuery, and choose a steep exponent with a large pivot, or a tiny pivot with tiny feature values. It reproduces with one document and default index and searcher settings. Elasticsearch, OpenSearch and Solr pass a request's sigmoid pivot and exponent straight through. Typical exponents, below 2, are safe.
Mechanism, terms and numbers710 words
FeatureField.newSigmoidQuery(field, feature, weight, pivot, exp) documents the score weight * S^a / (S^a + pivot^a) and accepts any finite pivot and exponent above 0. Its SigmoidFunction evaluates it as weight * (1 - pivot^a / (S^a + pivot^a)) with pivot^a precomputed once per query as a double. When pivot^a overflows (above about 1.8e308) every document scores Inf/Inf = NaN; when it underflows to 0 and S^a does too, the score is 0/0 = NaN. With pivot 2^20 and exponent 60, a document with S = pivot, documented to score 0.5, scores NaN, silently. Since 10.2.0 NaN hits rank ahead of correctly scored ones; older releases return placeholder hits or throw an assertion from the collector. Every release since 7.4.0 and main are affected, and IntervalScoreFunction (behind the 4-argument IntervalQuery) copies the arithmetic. Scoring as weight / (1 + (pivot / S)^a) removes it.
SigmoidFunctionstorespivotPa = Math.pow(pivot, a)once per query (FeatureField.java:495,:500at645d4fe14a20).- Each document scores
(float) (weight * (1 - pivotPa / (Math.pow(f, a) + pivotPa)))(:531), whereweightis the scorer's boost:newSigmoidQuerypasses a weight other than 1 through aBoostQuery(:706-709). The comment above it (:528-530) explains the rewrite off^a / (f^a + k^a): "to make sure it doesn't decrease with f in spite of rounding". - Overflow. If
pivot^arounds pastDouble.MAX_VALUE(about 2^1024),pivotPais+Infinity; the denominator is+Infinityfor every finitef^a, so every document that has the feature scores1 - Inf/Inf = NaN, whatever its value. - Underflow. If
pivot^ais below about 2^-1074,pivotPais 0; documents whosef^aalso underflows score1 - 0/0 = NaN, the others scoreweight. - An SMT model of the final expression (cvc5, floating-point theory, with the two powers as independent non-negative doubles) finds no other way to NaN. It does not model
Math.powor howFeatureFieldencodes values. - The bulk scorer (
:540-550) andexplain()(:556-558) use the same arithmetic, andFeatureQuerypasses the scorer straight into aTermScorer(FeatureQuery.java:135-137); nothing on the way to the collector checks for NaN. newSigmoidQueryrejects onlypivot <= 0,exp <= 0and non-finite values (FeatureField.java:700-705). The weight is capped at 64 (MAX_WEIGHT,:576-582); the exponent is not.
- One document, S = pivot = 2^20 = 1,048,576, weight 1, exponent 60:
pivot^60 = 2^1200overflows and the hit scores NaN instead of 0.5, with assertions on and off. - Boundary at the same pivot: exponent 51 (
2^1020, finite) gives 0.5, exponent 52 (2^1040) gives NaN. - First failing float pivot, measured on the real scorer (S set to the pivot as
FeatureFieldstores it; the overflow does not depend on S), close to2^(1024/exp): exponent 9: 1.7804262e34; 20: 2.586639e15; 35: 6.416196e8; 50: 1,462,494.8; 60: 137,270.94; 100: 1,209.3365. Exponent 8 gives no NaN for any pivot up toFloat.MAX_VALUE, because a float is below 2^128 and 128 * 8 = 1024, and none through underflow either: a stored feature value is at least 2^-126 (FeatureField.java:162-171), so S^8 >= 2^-1008 > 0. - A finite wrong value right next to the overflow: where
pivot^ais finite butS^a + pivot^aoverflows (about2^1023 <= pivot^a < 2^1024; at pivot 2^20, exponents from about 51.15 to just under 51.2, measured at 51.16, 51.18 and 51.19) the score at S = pivot is 1.0 instead of 0.5. - Ranking:
body:luceneas MUST plusnewSigmoidQuery("f", "x", 1, 2^20, 60)as SHOULD, over doc0 "lucene apache apache" with feature value 1.0 (documented boost 1/(1+2^1200), about 0) and doc1 "lucene lucene lucene" without the feature: the result is[doc=0 score=NaN, doc=1 score=0.13022968]. With the fix doc1 ranks first. - Underflow (study test): pivot 2^-20 with exponent 60 over 4 documents gives 3 NaN scores, and the best document (score 1.0) is ranked last.
IntervalScoreFunction.SigmoidFunction (IntervalScoreFunction.java:109-131, :117, :128), behind new IntervalQuery(field, source, pivot, exp) (IntervalQuery.java:97-99), has the same arithmetic and the same checks (IntervalScoreFunction.java:33-41). Sloppy frequencies are small, so it needs exponents in the hundreds: with pivot 4 and exponent 600, all 4 documents score NaN.
Exposure and likelihood287 words
- Who is exposed. Users of the sigmoid feature function (
FeatureFieldis@lucene.experimental) who want the boost to switch on sharply around the pivot, for example to approximate a popularity threshold; a steep exponent is the documented way to get that ("higher values make the function grow slower before 'pivot' and faster after 'pivot'",FeatureField.java:689-690). - No special setup. One document, the default
IndexWriterConfigand codec and a plainIndexSearcherare enough; no special codec or concurrency setting was needed. The score depends only on the query's parameters and the document's feature value (FeatureField.java:527-532). - Silent. With assertions off there is no exception or warning.
TopScoreDocCollector's javadoc says NaN values "are not valid scores. This collector will not properly collect hits with such scores" (TopScoreDocCollector.java:30-31). - What users see depends on the release (4-document probe; the arithmetic never changed):
- 7.4.0-10.0.0: with
-ea,search()throws anAssertionErrorinTopScoreDocCollector; with-da,TopDocsholds placeholder hits (doc 2147483647, score -Infinity) in place of real documents; - 10.1.0: only 1 of 4 hits;
- 10.2.0 to main: every hit, with NaN scores ranked ahead of finite ones (overflow: all NaN, in doc-id order);
explain()reports NaN on every release.
- 7.4.0-10.0.0: with
- Downstream. Elasticsearch's and OpenSearch's
rank_featuresigmoid and Solr's{!rank function=sigm}pass the request's pivot and exponent tonewSigmoidQueryunchanged, so a search client can hit it with one query parameter. Their intervals queries use the 2-argumentIntervalQuery, so the interval copy needs an application that calls the 4-argument constructor itself. - How likely. Rare: the exponents involved are far above typical ones (Lucene's tests use 0.6 and 2; Elasticsearch documents the exponent as "typically in [0.5, 1]"), and nobody has reported it since 2018. Priority low.
One JUnit test, one document, default settings, public API: index S = 2^20, search newSigmoidQuery("f", "x", 1, 2^20, 60), and the hit must score 0.5.
/*
* 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.document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.IndexSearcher;
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 TestFeatureFieldSigmoidOverflow extends LuceneTestCase {
public void testScoreAtPivotWithSteepExponent() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
doc.add(new FeatureField("f", "x", 1 << 20));
w.addDocument(doc);
}
try (DirectoryReader r = DirectoryReader.open(dir)) {
TopDocs td =
new IndexSearcher(r).search(FeatureField.newSigmoidQuery("f", "x", 1, 1 << 20, 60), 1);
// expected weight/2 = 0.5 at S = pivot; actual NaN, because pivot^60 = 2^1200 overflows
assertEquals(0.5f, td.scoreDocs[0].score, 1e-6f);
}
}
}
}
cd $LUCENE && ./gradlew :lucene:core:test --tests TestFeatureFieldSigmoidOverflow -Ptests.seed=DEADBEEF -Ptests.asserts=false
- Expected
- The hit scores 0.5, weight/2 at S = pivot.
- Observed
AssertionError: expected:<0.5> but was:<NaN>, with assertions on and off.
More files, environment, full output288 words
ByteBuffersDirectory, the default IndexWriterConfig, a plain IndexSearcher; nothing random, one thread. 2^20 is a positive normal float that FeatureField stores exactly, and the fixed formula gives exactly 0.5 for it. The test asserts the documented value (weight/2 at S = pivot) rather than just "not NaN", so the failure reads expected:<0.5> but was:<NaN>.
It covers the overflow case of the FeatureField copy, the common one. The same root cause also gives NaN through underflow (tiny pivots with tiny feature values) and through the 4-argument IntervalQuery (for example pivot 4 with exponent 600); those were executed by the study and are described in the text, not added as more tests.
Regression test: this same test. It passes with the recommended fix and fits as one more method of the existing TestFeatureField.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; with the fix: passes. Release jars 10.5.2 and 10.6.0: fail |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; the code path is pure Java double arithmetic) |
cp $REPRO/TestFeatureFieldSigmoidOverflow.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests TestFeatureFieldSigmoidOverflow -Ptests.seed=DEADBEEF -Ptests.asserts=false
# assertions on (drop -Ptests.asserts=false): the same failure
# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF (assertions on, and again with -Ptests.asserts=false)
TestFeatureFieldSigmoidOverflow > testScoreAtPivotWithSteepExponent FAILED
java.lang.AssertionError: expected:<0.5> but was:<NaN>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:7964E766F43B5E25]:0)
at org.junit.Assert.fail(Assert.java:89)
at org.junit.Assert.failNotEquals(Assert.java:835)
at org.junit.Assert.assertEquals(Assert.java:577)
at org.junit.Assert.assertEquals(Assert.java:701)
at org.apache.lucene.document.TestFeatureFieldSigmoidOverflow.testScoreAtPivotWithSteepExponent(TestFeatureFieldSigmoidOverflow.java:41)
BUILD FAILED
# standalone runner, main 645d4fe14a20 jars, seed DEADBEEF: -ea and -da both tests_run=1 failures=1
# release jars 10.5.2 and 10.6.0, -ea and -da: FAIL 1/1, same message
# with the recommended fix, gradle, -Ptests.seed=DEADBEEF
:lucene:core:test (SUCCESS): 1 test # the minimal test, -Ptests.asserts=false
:lucene:core:test (SUCCESS): 499 tests, 26 skipped # --tests 'org.apache.lucene.document.*'
:lucene:queries:test (SUCCESS): 117 tests # --tests 'org.apache.lucene.queries.intervals.*'- The javadoc promises weight/2 at S = pivot for any pivot and exponent in (0, +Infinity); Lucene calls NaN "not valid scores".
- Upstream's own
TestFeatureFieldcheck demands a finite max score from the sigmoid; at (2^20, 60) it is NaN. - Nothing documents an exponent limit: the weight is capped at 64, the exponent is not.
The full argument289 words
- The contract is exact.
newSigmoidQuery: the score is "weight * S^a / (S^a + pivot^a)", pivot is the "feature value that would give a score contribution equal to weight/2, must be in (0, +Infinity)", exp "must be in (0, +Infinity)" (FeatureField.java:680-693). Class javadoc: "values are in the 0..1 range and 0.5 is obtained when S and k are equal" (:76-77); with a weight the formula's range is[0, weight], so the 0..1 wording holds for weight 1, as in the repro.IntervalQuery: "Scores are bounded to between 0 and 1" (IntervalQuery.java:56) and pivot is "the sloppy frequency value at which the score will be 0.5" (:92). - Lucene treats NaN as invalid.
TopScoreDocCollector.java:30-31; the test framework'sAssertingScorerasserts!Float.isNaN(score)(AssertingScorer.java:129). - Upstream's own oracle.
TestFeatureField.doTestSimScorer(:279-287) assertsFloat.isFinite(maxScore) // used to compute max scoresand that the score never decreases with the value. It is run forSigmoidFunction(20f, 0.6f)(:275-277); run on (2^20, 60) and (1e16, 20) it fails with max score NaN. - Robustness was a goal. The code comment (
FeatureField.java:528-530) says the formula was rewritten so the score does not decrease "in spite of rounding". - No limit or NaN in the recorded and archived material. No javadoc, test, CHANGES or MIGRATE entry or commit message at
645d4fe14a20, and no discussion the searches found, limits the exponent or presents NaN as intended. In the archived LUCENE-8197 review (#9244) reviewers asked for documented and validated parameter ranges, and the committed code has the(0, +Infinity)checks; no archived comment discussespivot^expoverflowing. - Tests never reach the region. Every upstream sigmoid test uses exponent 0.6 (
TestFeatureField) or 2 (TestIntervalQuery.java:439).
The devil's-advocate check156 words
The strongest case, built by the cross-model reviews and by the devil's-advocate verifier: FeatureField is @lucene.experimental, its constants "typically need training" (FeatureField.java:84), and exponents of 50 to 150 are unrealistic, so a committer could say the function was never meant for them.
Why it fails: @lucene.experimental is about API stability ("might change in incompatible ways in the next release"), not about returning invalid scores; the domain is documented as (0, +Infinity) and validated, so these inputs are legal; the authors capped the one parameter they found unreasonable (MAX_WEIGHT) but not the exponent; the javadoc describes high exponents as the way to make the function steeper; and an algebraically equal one-line rewrite removes the failure, at the price of one more division per document. The realistic-exponent point lowers the priority, not the reality, and maintainers may reasonably prefer validating the parameters (option B) to changing the formula. Verdict: not intended, a defect.
Recommended Score as weight / (1 + (pivot / S)^a) (validated)
Score as weight / (1 + (pivot / S)^a): the same function without a precomputed power, so it cannot form Infinity/Infinity or 0/0. The repro passes, and so do the document and intervals test packages (499 and 117 tests); on a test grid, scores of at least 1e-6 * weight move by at most 1 ulp.
// FeatureField.SigmoidFunction.doScore; the same line in IntervalScoreFunction.SigmoidFunction
return (float) (weight / (1 + Math.pow((double) pivot / f, a)));All options and trade-offs4 options · 684 words
Option A was built and run through gradle in both copies. B (validate the exponent) is a possible addition that turns NaN into an error, C (document a range) leaves NaN, and D (guard in the collector) treats the symptom. Workaround without a fix: use exponents up to 8, which never fail. With a larger exponent keep pivot^exp well inside the double range on both sides: no overflow (exp * log2(pivot) well below 1024) and, for a pivot below 1, no underflow (pivot 2^-14 with exponent 100 already underflows).
Drop pivotPa from FeatureField.SigmoidFunction and IntervalScoreFunction.SigmoidFunction and compute the same function as weight / (1 + (pivot / S)^a) (2 files, +7/-10). For positive finite inputs pivot / S lies in [0, +Inf], so does its power, 1 + x lies in [1, +Inf] and the result in [0, weight]: no NaN. At S = pivot it is exactly weight / 2. It stays non-decreasing in S, the property the old comment protects: the division is monotone, Math.pow is specified as semi-monotonic, and 1 + x and w / x are monotone.
// FeatureField.SigmoidFunction.doScore; the same line in IntervalScoreFunction.SigmoidFunction
return (float) (weight / (1 + Math.pow((double) pivot / f, a)));Trade-offs
- Ordinary scores barely change, but not bit for bit. A copy of the old and new expressions in plain double arithmetic (not the real scorer) on 1,320,480 points (7 pivots from 0.01 to 1e6, exponents 0.5 to 8, weights 1 and 3, S from
Float.MIN_NORMALto 1e30 in 1% steps): 73% bit-identical; where the old score is at least 1e-6 * weight, at most 1 ulp apart. The largest absolute difference, 9.1e-13 (2^-40), is a 1-ulp difference at a score of about 1e-5 (weight 1, pivot 4.5, exponent 2, S 0.014), and every score above about 1e-5 is bit-identical. Where the old form rounds a tiny score to 0 by cancellation, the new form keeps it (at most 3.3e-16); that gives the largest ulp distance. The new form gave no NaN, nothing outside[0, weight]and no decreasing step on the grid. A ranking that depends on exact score values can still change, so it is worth a CHANGES.txt line. - Cost. One more division per scored document (two instead of one) and no precomputed constant; no benchmark was run.
- Review. No reviewer has read the diff line by line yet.
What the checks validated
- Scratch worktree of
645d4fe14a20, gradle, seed DEADBEEF: the minimal test fails before the fix (assertions on and off) and passes after it (-Ptests.asserts=false). :lucene:core:test --tests 'org.apache.lucene.document.*': 60 classes, 499 tests, 26 skipped, 0 failures, including the upstreamTestFeatureField(14 of 14).:lucene:queries:test --tests 'org.apache.lucene.queries.intervals.*': 7 classes, 117 tests, 0 failures, includingTestIntervalQuery(34) andTestIntervals(43); the study's interval test (pivot 4, exponent 600), which fails on unfixed main, passes.- Earlier checks of the same diff: the study's and the independent reproducer's tests pass with it; the exhaustive sweep of the score functions (6.8e9 checks) shows no sigmoid violation; the tie-break's ranking probe and boundary tests pass.
In newSigmoidQuery and IntervalScoreFunction.sigmoidFunction, throw IllegalArgumentException when Math.pow(pivot, exp) is +Infinity or 0, or reject exponents above a documented bound.
Trade-offs
Turns silent NaN into an error that downstream engines would surface to the client. Leaves the finite band where S^a + pivot^a overflows (score weight instead of weight / 2) unless the bound is tighter, and rejects parameters option A handles. An exponent bound is a judgement call; none is documented today. Not run.
Say in the javadoc which exponents are safe for a given pivot.
Trade-offs
Leaves NaN scores for anyone who misses the note, and contradicts the documented (0, +Infinity) domain unless that changes too.
Make top-k collection tolerate NaN scores.
Trade-offs
TopScoreDocCollector already documents that it does not handle NaN; the related collector-side issue #3347 (LUCENE-2271) has been open since 2010. Fixing the producer is local and cheap.
Engines that pass a request's sigmoid pivot and exponent straight to newSigmoidQuery (Elasticsearch and OpenSearch rank_feature, Solr {!rank function=sigm}) let a search client trigger it, for its own scores and order only; matching, filters and deletions are unaffected. If an application itself fixes an underflowing pivot^exp, a document with a tiny feature value can rank first with NaN.
An exhaustive run of every FeatureField score function on the real jar, 6.8e9 checks of properties such as "never NaN", found NaN only in the sigmoid. An SMT model of the final expression then showed NaN needs pivot^exp to overflow, or to underflow together with S^exp, and a JUnit test was written from those parameters.
The method in full206 words
Method: bounded-exhaustive translation validation of the real jar (the study's "semi-formal" tier: the first signal came from executing the stock code against exact properties).
- A harness drove every
FeatureFieldscore function on the stock main jar over every integer frequency from 1 to 65,535 plusInteger.MAX_VALUEandFloat.MAX_VALUE(for the sigmoid: 287 pivots covering every float binade, 21 exponents, 2 weights), about 6.8e9 checks against seven properties: no NaN or negative score, non-decreasing in the value, within the max-score bound, a finite max score, bulk scoring equals single scoring, explanation equals score, at most the weight. Only the sigmoid produced NaN (as a score and as its max score). The only other failures were +Infinity max scores of the linear and log functions at a feature value ofFloat.MAX_VALUE, which were not reported. - cvc5 queries over the final score expression (floating-point theory, the two powers modelled as independent non-negative doubles) then showed NaN is possible only if
pivot^a = +Inf, orpivot^a = 0andS^a = 0(the "only if" query unsat, both vacuity variants sat), and a region probe mapped which accepted (pivot, exponent) pairs fail. - The JUnit repro was written by hand from those parameters; the one-test minimal repro came later.
No prior report. GitHub issues and PRs, commits, the mailing lists, the JIRA archive, Elasticsearch, OpenSearch, Solr and the web were searched on 2026-10-26 and 2026-10-31. Closest: #13691, a fix to the sigmoid's explanation text, with no NaN.
Searches5 searches
Verdict: new. No open PR touches either SigmoidFunction, and upstream has not fixed it.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues and PRs (incl. migrated JIRA issues) | 2026-10-31 | 40 tool queries (comments of the top 12 read) and 14 own searches, e.g. "sigmoid NaN", newSigmoidQuery, SigmoidFunction, "FeatureField sigmoid", pivotPa, IntervalScoreFunction | no report of this defect |
| Open and recently closed PRs | 2026-10-31 | file lists of all 408 open PRs and the 57 closed since 2026-10-20 | none touches SigmoidFunction or IntervalScoreFunction |
| Commits, downstream projects, mailing lists, JIRA, web | 2026-10-31 | 6 commit queries; Elasticsearch, OpenSearch and Solr (14); dev@, java-user@, issues@, commits@ (54); JIRA LUCENE and SOLR (14); web (14) | nothing on this defect |
| Closest items (read) | 2026-10-31 | threads and diffs | #13691 and OpenSearch#14921 (explain text printed the pivot as the exponent; no NaN); #3347 / LUCENE-2271 (open since 2010: NaN or -Infinity scores leave the collector's sentinel hits in results; context for the placeholder hits on old releases); #9244 / LUCENE-8197 and #9679 / LUCENE-8633 (the introducing changes) |
| Earlier search | 2026-10-26 | 120 queries over the same sources | 29 hits read, none the same bug |
Introduced with FeatureField itself in 7.4.0 (LUCENE-8197, 2018); the IntervalQuery copy came with LUCENE-8633 in 8.0.0. The arithmetic never changed: every release from 7.4.0 to 10.6.0 and main is affected.
Versions tested9 rows
Introduced by 710993435f36 (master) / 70cc7b68783b (branch_7x, shipped first, in 7.4.0), "LUCENE-8197: Efficient integration of static scoring factors." by jpountz, 2018-03-20, a JIRA patch with no PR (now #9244). The first SigmoidFunction already precomputed pivotPa, and newSigmoidQuery already accepted every finite positive pivot and exponent. Interval copy: a8266492416d / 18c3986cc4bd, "LUCENE-8633: Remove term weighting from IntervalQuery scores" by romseygeek, 2019-01-16 (now #9679), first in 8.0.0. Later commits (LUCENE-8313, the 2020 reformat, #13691, #15137, #15568, #15874) left the arithmetic unchanged.
What users see changed with the collector between 10.0.0 and 10.2.0 (#13939 and neighbours), not with the sigmoid.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails with assertions on and off (gradle and standalone runner); passes with the fix |
| 10.5.2, 10.6.0 | affected | minimal test fails, assertions on and off (release jars) |
| 10.2.0 - 10.5.1 | affected | every hit returned, NaN scores ranked ahead of finite ones |
| 10.1.0 | hits lost | only 1 of 4 hits returned |
| 7.4.0 - 10.0.0 | affected | assertions off: placeholder hits (doc 2147483647, score -Infinity) in place of real ones; assertions on: AssertionError in TopScoreDocCollector |
| 7.0.0 - 7.3.1 | not affected | no FeatureField (compile error) |
| IntervalQuery copy: 8.0.0 - 10.6.0 and main | affected | pivot 4, exponent 600: NaN; 61 releases |
| IntervalQuery copy: 7.7.3 and earlier | not affected | no 4-argument constructor (compile error) |
| fix (both copies) | not affected | the minimal test and the interval test pass; 499 + 117 tests pass |
Confirmed by an independent reproducer, a devil's-advocate verifier, a reviewer from another model family (OpenAI GPT-5.6), an adjudication, a credibility audit and a tie-break on main 645d4fe14a20. The only dissent was a procedural "not real" from a read-only review that could not run Java.
Checks and verdicts314 words
None of the checks found a flaw in the claim; the ones that could run code (the independent reproduction, the intended-behaviour check, the audit, the tie-break and this doc's runs) re-ran repros. The refute review could not execute anything, so its "not real" is procedural.
| Check | Verdict | What it found |
|---|---|---|
| Finder (exhaustive run, SMT, JUnit) | candidate | Overflow and underflow JUnit tests fail with assertions on and off on main and with assertions off on 10.5.2 and 9.12.3; the interval test (pivot 4, exponent 600) too. |
| Refute review (OpenAI GPT-5.6, read-only) | not real (procedural, low) | Could not run Java (twice, 2026-10-26 and 2026-10-31); agrees on the root cause, finds the inputs in contract and the intended-behaviour argument weak; the first review pointed out the IntervalQuery copy. |
| Independent reproducer | real | Own tests: pivot 1000 with exponent 150 gives 4 of 4 NaN; pivot 1e-4 with exponent 100 gives 2 of 3 NaN with the largest feature value ranked last; control (exponent 100, pivot 1000) passes. |
| Intended-behaviour lens (OpenAI GPT-5.6, read-only) | real (medium) | The experimental-API argument loses to the documented domain, the 0..1 range and the NaN javadoc. |
| Devil's advocate | real | Upstream's own sim-scorer oracle fails with max score NaN on accepted steep parameters. |
| Adjudication | confirmed (high) | Silent wrong results. |
| Credibility audit | keep | Re-ran the study's tests with fresh seeds: 3 of 3 fail with assertions on and off. |
| Current main and tie-break | real (high) | On 645d4fe14a20, 10.5.2 and 10.6.0; boundary control: exponent 51 passes, 52 fails; ranking probe; the fix makes all of them pass. |
| This doc | reproduced | Minimal test re-run on main (fails, assertions on and off); gradle before and after the fix; old-vs-new formula comparison. An independent fact check re-ran the gradle commands before and after the fix with the same results and located the largest old-vs-new difference. |
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 331 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,469 words
- Registry title
- FeatureField.newSigmoidQuery (and the IntervalQuery sigmoid) return NaN scores when pivot^exp overflows or underflows a double, for pivot and exp values the factories accept
- Area
- document / FeatureField sigmoid scoring (FeatureField.SigmoidFunction behind newSigmoidQuery; the same arithmetic is copied in queries / intervals IntervalScoreFunction.SigmoidFunction behind the 4-argument IntervalQuery constructor)
- Priority
- Low, rank 723: FeatureField sigmoid queries return NaN scores only for extreme pivot and exponent values, which is silent but boundary-value only.
- Impact class
- silent wrong results
- Impact note
- Applications on Lucene 7.4.0 through 10.5.2 or main that rank with a FeatureField sigmoid query (FeatureField.newSigmoidQuery, which downstream engines' rank_feature sigmoid and Solr's {!rank function=sigm} call with the request's pivot and exponent) and choose a steep exponent with a large or tiny pivot, so that pivot^exp leaves the double range (exp*log2(pivot) >= 1024, for example exp 50 with pivot >= 1.46e6 or exp 100 with pivot >= 1209, or pivot^exp below 2^-1074 together with small feature values; never for exp <= 8, and typical exponents are below 2), get NaN scores and no error at any index size with the default IndexWriterConfig, codec and searcher: on 10.2.0 and later the NaN hits come back ranked ahead of correctly scored documents (in the overflow case every hit is NaN, in doc-id order), on 7.4.0-10.0.0 TopDocs also holds placeholder hits (doc 2147483647, score -Infinity) in place of real ones and 10.1.0 returned 1 of 4 hits, with -ea a collector assertion fires instead on 7.4.0-10.0.0, and explain() reports NaN; the 4-argument IntervalQuery constructor has the same arithmetic but needs exponents in the hundreds (600 at pivot 4) because sloppy frequencies are small.
- Summary
- You rank documents with FeatureField.newSigmoidQuery (or IntervalQuery's pivot-and-exponent constructor) and pick a steep exponent with a large or tiny pivot, values the factory accepts because it only requires both to be positive and finite. You expect the documented score weight * S^exp / (S^exp + pivot^exp), a number between 0 and weight that equals weight/2 at S = pivot. Instead SigmoidFunction precomputes pivot^exp as a double, which becomes +Infinity or 0 for such parameters, so scores come out as Inf/Inf or 0/0 = NaN: with pivot 2^20 and exponent 60 every document scores NaN; since 10.2.0 NaN hits rank ahead of correctly scored ones, so the best document can come last, and on 9.x (assertions off) TopDocs even returns placeholder hits with doc id 2147483647.
- Introduced
- 710993435f365fce44a60b7c498ce6af8327f92c (master) / 70cc7b68783b313a845996dac9e28c3ea6ad61e3 (branch_7x, the copy that shipped first, in 7.4.0): 'LUCENE-8197: Efficient integration of static scoring factors.' by jpountz, a JIRA patch with no pull request (the issue is now #9244). The commit created FeatureField; its SigmoidFunction already precomputed pivotPa = Math.pow(pivot, a) and scored (float) (weight * (1 - pivotPa / (Math.pow(f, a) + pivotPa))), and newSigmoidQuery already accepted every finite pivot > 0 and exp > 0, so no earlier version of the code was correct [read: git show of both commits; git blame -w on main puts FeatureField.java:495, :500, :531 and the checks at :700-705 on 710993435f3; git log --all -S 'pivotPa' finds no other commit that changes the expression; exec: the release boundary, 7.3.1 has no FeatureField and 7.4.0 fails]. IntervalQuery copy: a8266492416dbc2fcf665983d1cac141e50fa9eb (master) / 18c3986cc4bdb38949fae14bd771621345d41776 (branch_8x), 'LUCENE-8633: Remove term weighting from IntervalQuery scores' by romseygeek (now #9679), which added IntervalScoreFunction.SigmoidFunction with the same arithmetic (IntervalScoreFunction.java:112, :117, :128 on main, blamed to a8266492416) [read]. Later commits that touched SigmoidFunction (9b9776a7143 LUCENE-8313, 2d6ad2fee6d LUCENE-9570 reformatting, 0f5359a2ac6 #13691 explain text, f59ff9220ec #15137 bulk scorer) left the arithmetic unchanged [read]. (2018-03-20 (FeatureField, LUCENE-8197); 2019-01-16 (IntervalQuery copy, LUCENE-8633))
- First affected version
- 7.4.0 [exec] (the first release with FeatureField; 7.3.1 does not have the API, the probe does not compile [exec]); IntervalQuery copy: 8.0.0 [exec] (in lucene-sandbox under org.apache.lucene.search.intervals; 7.7.3 has IntervalQuery but not the (field, source, pivot, exp) constructor [exec: compile error]; moved to lucene-queries in 8.2.0 by LUCENE-8856)
- Affected versions
- FeatureField copy: every lucene-core release from 7.4.0 to 10.5.2 (67 releases: 7.4.0-7.7.3, all 27 8.x, all 21 9.x and all 12 10.x up to 10.5.2) and main ea57360912ed [exec: history probe with -ea on all of them and -da on 7.4.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.1.0, 10.2.0, 10.5.2 and main; the finder's JUnit on main (-ea, -da), 10.5.2 and 9.12.3; the independent reproduction's JUnit on main, 10.5.2 and 9.12.3 (-ea and -da)]. IntervalQuery copy: every release from 8.0.0 to 10.5.2 (60 releases) and main [exec]. Not affected: 7.0.0-7.3.1 (API absent) [exec]. The NaN arithmetic never changed, but the visible symptom depends on the collector [exec, 4-document probe]: 7.4.0-10.0.0 with -ea, search() throws TopScoreDocCollector's AssertionError; 7.4.0-10.0.0 with -da, TopDocs holds placeholder hits (doc 2147483647, score -Infinity) in place of real documents; 10.1.0 returns only 1 of 4 hits; 10.2.0 to main return every hit, with NaN scores ranked ahead of finite ones; explain() reports NaN on every release. Not fixed on main, branch_10x or branch_10_5 [read, 2026-10-26]. All runs on aarch64 with JDK 25.
- 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 adjudication that ruled it real, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: exhaustive check; also reached by cvc5
- Upstream and prior reports
- none found (2026-10-26, the prior-report record: 120 recorded queries, namely 34 GitHub issue/PR searches plus 3 REST reads in apache/lucene, 3 commit-message searches, 5 downstream searches (Elasticsearch, OpenSearch, Solr), 52 mailing-list searches over dev@, java-user@, issues@ and commits@, 13 JIRA queries over LUCENE and SOLR, 8 web searches, 1 Q&A API read and a local history search; plus the changed-file lists of all 413 open apache/lucene PRs; 29 hits read and classified, none the same bug; the finder's earlier 45-query run, the independent reproduction and the intended-behaviour check found nothing either); not fixed upstream (main ea57360912ed, branch_10x and branch_10_5 still compute pivotPa / (Math.pow(f, a) + pivotPa), and no open PR touches FeatureField.java or IntervalScoreFunction.java); closest items, not reports: #13691 (merged 2024-08-27, fixed only the explain text of SigmoidFunction, which printed the pivot where the exponent belongs) and the downstream report behind it, opensearch-project/OpenSearch#14921 (no NaN); #9244 / LUCENE-8197 and #9679 / LUCENE-8633 (the introducing changes; their reviews asked for documented parameter ranges but never discussed pivot^exp overflowing)
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test fails with assertions on and off, through gradle in a scratch worktree and through a standalone runner; with the recommended fix it passes, together with theorg.apache.lucene.documenttests (499) and theorg.apache.lucene.queries.intervalstests (117). Release jars 10.5.2 and 10.6.0 fail; a port of the test fails on every release from 7.4.0. Upstream main2f424fd428dacarries the same code (read, 2026-10-31).
- Finding record
lucene/core/src/java/org/apache/lucene/document/FeatureField.java:74-77 (at ea57360912ed) (class javadoc: sigm(S) = w * S^a / (S^a + k^a) with k > 0, a > 0, 'values are in the 0..1 range and 0.5 is obtained when S and k are equal'), :104 (@lucene.experimental), :492-501 (SigmoidFunction; :500 pivotPa = Math.pow(pivot, a)), :527-532 (doScore; :531 (float) (weight * (1 - pivotPa / (Math.pow(f, a) + pivotPa))) and the comment on rounding), :540-550 (asBulkSimScorer; :547 calls the same doScore), :576-582 (MAX_WEIGHT cap and its rationale), :680-693 (newSigmoidQuery javadoc: the formula, pivot and exp 'must be in (0, +Infinity)'), :700-705 (the only checks on pivot and exp: <= 0 or not finite)lucene/queries/src/java/org/apache/lucene/queries/intervals/IntervalScoreFunction.java:33-41 (at ea57360912ed) (sigmoidFunction checks), :109-131 (SigmoidFunction; :117 pivotPa = Math.pow(pivot, a), :128 the same expression); IntervalQuery.java:56 ('Scores are bounded to between 0 and 1'), :87-99 (4-argument constructor javadoc: pivot 'must be within (0, +Infinity)', exp 'must be in (0, +Infinity)')lucene/core/src/java/org/apache/lucene/search/TopScoreDocCollector.java:30-31 (at ea57360912ed) ('The values Float#NaN and Float#NEGATIVE_INFINITY are not valid scores. This collector will not properly collect hits with such scores'); lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingScorer.java:129 (assert !Float.isNaN(score)); lucene/core/src/test/org/apache/lucene/document/TestFeatureField.java:275-287 (testSigmSimScorer with pivot 20, exp 0.6; doTestSimScorer asserts a finite max score and monotonicity)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/document/FeatureField.java (:74-77, :104, :492-501, :527-532, :540-550, :556-558, :576-582, :680-711)lucene/core/src/java/org/apache/lucene/document/FeatureQuery.java (:135-137)lucene/queries/src/java/org/apache/lucene/queries/intervals/IntervalScoreFunction.java (:33-41, :109-131); lucene/queries/src/java/org/apache/lucene/queries/intervals/IntervalQuery.java (:56, :87-99)
- Who is affected
lucene/core/src/java/org/apache/lucene/document/FeatureField.java (:84, :104, :689-690)lucene/core/src/java/org/apache/lucene/search/TopScoreDocCollector.java (:30-31)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/document/FeatureField.java (:76-77, :84, :104, :528-530, :576-582, :680-705)lucene/queries/src/java/org/apache/lucene/queries/intervals/IntervalQuery.java (:56, :92); lucene/core/src/java/org/apache/lucene/search/TopScoreDocCollector.java (:30-31); lucene/test-framework/src/java/org/apache/lucene/tests/search/AssertingScorer.java (:129)lucene/core/src/test/org/apache/lucene/document/TestFeatureField.java (:160, :240-258, :275-287, :445, :462, :578); lucene/queries/src/test/org/apache/lucene/queries/intervals/TestIntervalQuery.java (:439)
- Suggested fix
registry row LFV-258 notes
- TestFeatureFieldSigmoidOverflow.java (one test, public API):
repro/LFV-258/src/org/apache/lucene/document/TestFeatureFieldSigmoidOverflow.java(45 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the executed runs (the minimal test, the boundary at exponents 51 and 52, the ranking probe).
assets/LFV-258/bug.png(331 KB)
