All findings · Lucene finding · LFV-754 · sweetspot-lengthnorm-int-overflow

SweetSpotSimilarity returns a NaN length norm for fields longer than 2^30 terms

With the default SweetSpotSimilarity, a document whose field has 1,073,741,848 terms or more (one custom term frequency of 1.1 billion is enough) gets a NaN length norm, because lengthNorm adds abs(x-min) + abs(x-max) in int. That document is never returned, and top-k search also skips good hits stored near it.

Summary

Priority
Low, rank 972A field of 2^30 terms under the opt-in legacy SweetSpotSimilarity gives NaN norms that drop the document and break top-k pruning, a contrived trigger that one custom term frequency can reach.
Impact
silent wrong results
Since
2.0.0 (2006, LUCENE-577)introduced 2006-05-25 · over 20 years
Affected
2.0.0 to 10.6.0 and main
Status
documented
Found by
bounded-exhaustive formula check on the real jar

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

lengthNorm does its distance arithmetic in int, so with the default factors it wraps negative and returns NaN from length 2^30 + 1. The NaN norm then drops the document and breaks top-k pruning.

Background: length norms and top-k pruning

  1. A TF-IDF similarity multiplies a hit's score by a length norm that shrinks as the field gets longer. SweetSpotSimilarity documents it as 1/sqrt(steepness * (|x-min| + |x-max| - (max-min)) + 1), which is 1/sqrt(x) for the defaults (min = max = 1, steepness 0.5).
  2. The index stores each field's length in one byte. At search time the similarity computes the norm for each of the 255 decoded lengths. Byte 248 is the first above 2^30: it decodes to 1,073,741,848.
  3. Top-k search keeps the best possible score of each 256-document block (and of each 8,192-document range) and skips any block whose best score cannot beat the current k-th hit.
Diagram of the bug. SweetSpotSimilarity.lengthNorm with the default factors computes |x-1| + |x-1| in int: at x = 2^30 it is 2,147,483,646 and the norm is 3.05e-5, at x = 2^30 + 1 it wraps to -2,147,483,648 and the norm is NaN. Norm bytes 248 to 255 (field lengths from 1,073,741,848) are NaN. In a 10,000-document index, doc 2000 has custom term frequency 1.1e9 and scores NaN; its block maximum is NaN, so top-k search skips its 256-document block and 8,192-document skip range: search(q, 7) returns docs 50 and 9000 and fillers scoring 0.5, and misses doc 2000 (expected top hit, 1.0121535) and docs 2010, 3000 and 7000 (score 1.0). With long arithmetic the norm is 3.05e-5 and all hits come back.
How the norm breaks: the int sum |x-1| + |x-1| wraps negative above 2^30, norm bytes 248-255 become NaN, and top-k search skips the long document's block and skip range. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Score with new SweetSpotSimilarity() and index 10,000 documents with one DOCS_AND_FREQS field: five hold only "a" (score 1.0), the others "a" plus padding (score 0.5), and doc 2000 holds "a" with custom term frequency 1,100,000,000.
  2. You expect doc 2000's length norm to be 1/sqrt of its decoded length (norm byte 248, 1,073,741,848), 3.05e-5, as the javadoc and ClassicSimilarity say. Then it scores 1.0121535 and search(q, 7) returns it first, followed by the five documents that score 1.0.
  3. Instead lengthNorm computes |x-1| + |x-1| in int. From x = 2^30 + 1 the sum passes Integer.MAX_VALUE and wraps negative, so the square root is NaN. Every field length from 1,073,741,848 up gets a NaN norm: doc 2000 scores NaN, and a NaN hit is never collected (collecting every hit returns 9,999 of 10,000).
  4. The NaN also becomes the best possible score of its 256-document block and 8,192-document skip range, and NaN never beats the threshold, so top-k pruning skips them, doc 2000 included: search(q, 7) returns [50, 9000, 0, 1, 2, 3, 4] and misses docs 2010, 3000 and 7000 (score 1.0). No error, with assertions on and off.

Who is affected

Only applications that score a normed field with the opt-in SweetSpotSimilarity (lucene/misc; the default factors are affected) and index one document whose field length reaches 1,073,741,848: custom term frequencies summing past a billion on a DOCS_AND_FREQS field, or about a billion tokens. Contrived, but silent when it happens.

Mechanism, terms and numbers765 words

SweetSpotSimilarity (lucene/misc) documents its length norm as 1/sqrt( steepness * (abs(x-min) + abs(x-max) - (max-min)) + 1 ) and says it degrades to 1/sqrt(x) with the default factors (min = max = 1, steepness 0.5). lengthNorm evaluates the sum in int, so with the defaults 2·(x − 1) wraps negative from x = 2^30 + 1 and the norm is NaN.

Through an index, lengthNorm sees only the 255 decoded norm lengths, so every field length of 1,073,741,848 or more (norm bytes 248-255) scores NaN. Lucene accepts such lengths: the indexing chain rejects only an int overflow, and one token with custom term frequency 1.1e9 on a DOCS_AND_FREQS field reaches it (so do about a billion ordinary tokens). The NaN document is never collected, and because a NaN block maximum never passes the competitive check, top-k search also skips competitive documents in the same 256-document postings block and 8,192-document skip range. Computing the distance in long fixes it. The int expression dates from the class's first version (2006); it is silent even with assertions on since 10.1.0. Low impact: an opt-in class and a 2^30-scale field.

The bug, step by step (main 645d4fe14a20)

  1. Int arithmetic. SweetSpotSimilarity.lengthNorm (SweetSpotSimilarity.java:110-120): final int l = ln_min; final int h = ln_max; (:112-113), then (float) (1.0f / Math.sqrt((s * (float) (Math.abs(numTerms - l) + Math.abs(numTerms - h) - (h - l))) + 1.0f)) (:116-119). The sum is int; the cast to float comes after it.
  2. It wraps. With the defaults (:36-38) the sum is 2·(x − 1): 2,147,483,646 at x = 2^30 (correct norm), -2,147,483,648 at x = 2^30 + 1, so 0.5 · sum + 1 is negative, Math.sqrt returns NaN and so does lengthNorm.
  3. Decoded lengths. computeNorm stores SmallFloat.intToByte4(length) (Similarity.java:151-161); TFIDFSimilarity.scorer fills normTable[i] = lengthNorm(byte4ToInt(i)) for i = 1..255 (TFIDFSimilarity.java:427-446) and score reads it (:470). Byte 247 decodes to 1,006,632,984 (finite), byte 248 to 1,073,741,848 (NaN), byte 255 to 2,013,265,944 (NaN). A field of exactly 2^30 + 1 terms encodes to byte 247 and is unaffected.
  4. Legal length. IndexingChain sums the field length with Math.addExact (1 per token for DOCS_AND_CUSTOM_FREQS, the custom frequency otherwise) and throws "too many tokens" only on an int overflow (IndexingChain.java:2053-2064). TermFrequencyAttribute names DOCS_AND_FREQS and sets no upper bound (TermFrequencyAttribute.java:23-34).
  5. Not collected. BatchScoreBulkScorer collects only if (score >= scorable.minCompetitiveScore) (BatchScoreBulkScorer.java:53-55): false for NaN.
  6. Block skipped. MaxScoreCache.computeMaxScore takes Math.max over the block's impacts (MaxScoreCache.java:84-88), and Math.max returns NaN if either argument is. ImpactsDISI keeps a block only if (maxScore >= minCompetitiveScore) (ImpactsDISI.java:80-82) and getSkipLevel climbs to the highest level whose maximum fails that test (MaxScoreCache.java:137-158), so the 256-doc block and the 8,192-doc level-1 range of the default postings format (Lucene104PostingsFormat.java:343-351) are skipped. In the 10,000-document example, doc 50 is collected before pruning starts (after 1,000 hits) and doc 9000 lies outside that range; docs 2010, 3000 and 7000 lie inside it. There the raised bar reaches the term scorer at doc 1024 and one skip goes from doc 1024 to doc 8192, so doc 2000 itself is skipped before it is scored; step 5 is what drops it when every hit is collected.

Exactly when the result is wrong

int arithmetic wraps consistently, so an intermediate overflow alone is harmless: the result is wrong only when the true value 2·(x − max) (for x ≥ max ≥ min) exceeds Integer.MAX_VALUE, i.e. x ≥ max + 2^30 (executed probe):

factors (min, max, steepness)xx − max ≥ 2^30lengthNormformula
1, 1, 0.5 (defaults)2^30no3.0517578E-53.0517578E-5
1, 1, 0.5 (defaults)2^30 + 1yesNaN3.0517578E-5
1, 30, 0.51,073,741,848 (byte 248)no (the sum overflows, the result does not)3.0517578E-53.0517578E-5
3, 5, 0.52^30 + 5yesNaN3.0517578E-5
100, 1000, 0.51,073,741,848 (byte 248)no3.0517593E-53.0517593E-5
100, 1000, 0.51,207,959,576 (byte 249)yesNaN2.877226E-5

For the decoded lengths the wrapped value lies between -2^31 and about -2.7e8, so the norm is NaN for any steepness above about 4e-9; steepness 0 never gives NaN. With the defaults, norm bytes 248-255 are NaN, and bytes 1-247 are finite and match ClassicSimilarity to within one float step (243 of 247 identical; bytes 226, 229, 234 and 241 differ by one float step). Byte 0 is not a legal norm ("norm is never 0", Similarity.java:205); its table entry 1f / normTable[255] is NaN too, but by that contract a scorer never receives norm 0.

Exposure and likelihood223 words
  • Who is hit. Applications that score a normed field with SweetSpotSimilarity and index at least one document whose field length is at least 1,073,741,848 (with the defaults; other factors from max + 2^30). Routes: custom term frequencies on a DOCS_AND_FREQS field (TermFrequencyAttribute, or DelimitedTermFrequencyTokenFilter input such as a|1100000000), or about a billion ordinary tokens (an independent verifier indexed 1,073,741,848 tokens in about 45 s and reproduced it). The 10.5 option DOCS_AND_CUSTOM_FREQS counts tokens instead of summing frequencies, so it closes the custom-frequency route, not the ordinary-token one.
  • What goes wrong. Silently: the long document scores NaN and never appears in scored results (in the example it should be the top hit), and default top-k search (pruning after 1,000 hits) also skips the competitive documents of the same term in its 256-document block and 8,192-document skip range, returning weaker hits in their place. The skipping reach depends on the postings format: on 9.12.3 a different set is lost.
  • Not affected. Non-scoring matching (filters, count(), constant-score queries). The stored norm byte is the encoded length, which is correct, so a fix needs no reindex (since 7.0).
  • Likelihood: contrived. A 2^30-scale length in one field does not happen by accident, and maintainers ask users of custom frequencies to keep "reasonable values" (#11086). Low priority, but a real defect in the documented formula.

Minimal repro

One JUnit test in lucene/misc, public API only, no index: it calls new SweetSpotSimilarity().lengthNorm(2^30 + 1) and compares with 1/sqrt(x). Deterministic; fails with assertions on and off on main and 10.6.0.

TestSweetSpotLengthNormOverflow.java (1 test)29 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.misc;

import org.apache.lucene.tests.util.LuceneTestCase;

public class TestSweetSpotLengthNormOverflow extends LuceneTestCase {

  public void testDefaultLengthNormAboveTwoToThe30() {
    int numTerms = (1 << 30) + 1;
    // javadoc: with the default factors lengthNorm degrades to 1/sqrt(x); actual: NaN
    assertEquals(
        (float) (1 / Math.sqrt(numTerms)), new SweetSpotSimilarity().lengthNorm(numTerms), 0f);
  }
}
Command
cd $LUCENE   # an apache/lucene checkout at 645d4fe14a20 (or any commit with the same SweetSpotSimilarity.java)
cp $REPRO/TestSweetSpotLengthNormOverflow.java lucene/misc/src/test/org/apache/lucene/misc/
./gradlew :lucene:misc:test --tests org.apache.lucene.misc.TestSweetSpotLengthNormOverflow -Ptests.seed=DEADBEEF
# assertions off fails the same way: add -Ptests.asserts=false
Expected
lengthNorm(1073741825) = 3.0517578E-5, i.e. 1/sqrt(x), as ClassicSimilarity returns.
Observed
NaN: expected:<3.0517578E-5> but was:<NaN>, on main and 10.6.0, assertions on and off.

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

More files, environment, full output258 words

The test calls the public lengthNorm with 2^30 + 1, the first length that overflows with the defaults. Through an index the first affected length is 1,073,741,848 (norm byte 248); the end-to-end effects (the NaN document dropped, other hits skipped) follow from the NaN norm and are shown by the larger tests of the study, summarised under "Observed" below.

As the regression test it can go in as is: with the fix and this file in place, :lucene:misc:check -x test (google-java-format, ecj lint, forbidden APIs, license checks) passes. It can also become a method of TestSweetSpotSimilarity.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): affected; also the 10.6.0 release jars
jdkOpenJDK 25.0.3
osLinux, aarch64

Observed output

TestSweetSpotLengthNormOverflow > testDefaultLengthNormAboveTwoToThe30 FAILED
    java.lang.AssertionError: expected:<3.0517578E-5> but was:<NaN>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:C95E8C6BB254070C]:0)
        at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
        at junit@4.13.2/org.junit.Assert.failNotEquals(Assert.java:835)
        at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:577)
        at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:701)
        at org.apache.lucene.misc.TestSweetSpotLengthNormOverflow.testDefaultLengthNormAboveTwoToThe30(TestSweetSpotLengthNormOverflow.java:26)
:lucene:misc:test (FAILURE): 1 test, 1  failure

# the same assertion with -Ptests.asserts=false, and on the 10.6.0 release jars (assertions on and off)

# end to end, the 10,000-document example (main 645d4fe14a20 jars, the same with -ea and -da):
SweetSpotSimilarity: count=10000 explain(2000)=NaN
  default search(q, 7):  [50:1.0 9000:1.0 0:0.5 1:0.5 2:0.5 3:0.5 4:0.5 ]
  exhaustive top 7:      [50:1.0 2010:1.0 3000:1.0 7000:1.0 9000:1.0 0:0.5 1:0.5 ]
  exhaustive hits:       9999
ClassicSimilarity (and SweetSpotSimilarity with the fix): count=10000 explain(2000)=1.0121535
  default search(q, 7):  [2000:1.0121535 50:1.0 2010:1.0 3000:1.0 7000:1.0 9000:1.0 0:0.5 ]
  exhaustive hits:       10000

# norm table of the default SweetSpotSimilarity
byte 247 decodes to 1006632984: SweetSpot 3.151842E-5, Classic 3.151842E-5
byte 248 decodes to 1073741848: SweetSpot NaN, Classic 3.0517578E-5
byte 255 decodes to 2013265944: SweetSpot NaN, Classic 2.2286888E-5

Why it looks unintended

  • The javadoc formula has no length limit and "degrades to 1/sqrt(x)" with the default factors; ClassicSimilarity returns 3.0517578E-5 for the same length.
  • Lucene accepts the length: indexing rejects only an int overflow, the bound a maintainer set for custom frequencies (#9990).
  • Lucene's similarity tests require finite scores for every norm; TestSweetSpotSimilarity never runs them and checks lengths below 1,000 only.
The full argument224 words
  • The contract. "Implemented as: 1/sqrt( steepness * (abs(x-min) + abs(x-max) - (max-min)) + 1 )" and "This degrades to 1/sqrt(x) when min and max are both 1 and steepness is 0.5" (SweetSpotSimilarity.java:97-102), the defaults (:36-38).
  • The upstream test asserts that equivalence with ClassicSimilarity for lengths 1 to 999 only (@Nightly testSweetSpotComputeNorm, "base case, should degrade", TestSweetSpotSimilarity.java:72-86).
  • Finite scores. BaseSimilarityTestCase checks that "scores are non-negative and finite" for every norm value (LUCENE-6896, -7997, -8010), but TestSweetSpotSimilarity extends LuceneTestCase. SimScorer#score must not decrease when freq increases "for any value of norm that may be produced by computeNorm", and must not increase when the unsigned norm increases "for any legal freq" (Similarity.java:207-213); NaN satisfies neither, since every comparison with NaN is false. TopScoreDocCollector says NaN and NEGATIVE_INFINITY "are not valid scores" (TopScoreDocCollector.java:30-31).
  • Legal input. IndexingChain accepts the length (Math.addExact, :2053-2064). Michael McCandless on #9990 (LUCENE-8947): "users must not try to write too many, too large custom term frequencies such that their sum overflows a Java int in a single field / document"; 1.1e9 is inside that bound. The ordinary-token route needs no custom frequency at all.
  • History. The int expression is in the class's first version (LUCENE-577, 2006); no javadoc, CHANGES entry, test or issue sets a length limit below Integer.MAX_VALUE.
The devil's-advocate check132 words

The strongest case (verifiers and a cross-model review): SweetSpotSimilarity is an expert, legacy class in lucene/misc; a billion-term field is absurd; and custom term frequencies are the user's responsibility: "If you use custom term frequencies (expert feature), then it is your responsibility to have reasonable values. Otherwise, things can easily overflow, break scoring" (Robert Muir, #11086, won't fix). Once a score is NaN, the collector's behaviour is documented.

Why it fails: #11086 is about bypassing the int-sum check, and the same comment names the bound ("less than int32"); this trigger is inside it, and the ordinary-token route needs no expert feature. The NaN arises in the similarity, whose own javadoc promises a finite value that the parent class computes. Rarity lowers the priority; it does not make the NaN intended.

Suggested fix

Recommended Compute the distance in long

Compute the distance in long: final long l = ln_min; final long h = ln_max; in lengthNorm (two tokens). Validated on main: the repro passes with assertions on and off; TestSweetSpotSimilarity with its nightly length-norm test, the whole lucene/misc test task and check pass. No reindex needed.

   public float lengthNorm(int numTerms) {
-    final int l = ln_min;
-    final int h = ln_max;
+    // long, not int: abs(x-min) + abs(x-max) can exceed Integer.MAX_VALUE for long fields
+    final long l = ln_min;
+    final long h = ln_max;
     final float s = ln_steep;
All options and trade-offs3 options · 533 words

The recommended fix was run in a scratch worktree of 645d4fe14a20 (OpenJDK 25.0.3), removed afterwards. "Closest suite" means TestSweetSpotSimilarity, the class's own test, run with -Ptests.nightly=true so that its @Nightly length-norm test runs too. LFV-413 (a plateau breaks norm monotonicity in the same method) is a different defect: this fix does not fix it, and its redesign would remove this overflow as a side effect.

Compute the distance in long (validated by the checks) (recommended)

Declare l and h as long in lengthNorm, so the whole expression is evaluated in long; numTerms is an int, so abs(x-min) + abs(x-max) stays below 2^33. The cast to float follows as before. Since 7.0 the norm byte stores the encoded length, which was always correct, so the fix changes only the search-time norm table: no reindex.

   public float lengthNorm(int numTerms) {
-    final int l = ln_min;
-    final int h = ln_max;
+    // long, not int: abs(x-min) + abs(x-max) can exceed Integer.MAX_VALUE for long fields
+    final long l = ln_min;
+    final long h = ln_max;
     final float s = ln_steep;

Trade-offs

  • Values. With the fix the default norm table equals ClassicSimilarity at 250 of 255 bytes; bytes 226, 229, 234 and 241 (unchanged from today) and byte 252 (2.49175E-5 vs 2.4917497E-5) differ by one float step, from the existing float conversion of the sum; lengths 1-999 are all identical.
  • Cost: negligible: 255 evaluations when a scorer is built, as today (TFIDFSimilarity.java:442-446); not measured.

What the checks validated

  • Gradle on a scratch worktree of 645d4fe14a20: stock fails the repro with assertions on and off; with the fix it passes with assertions on and off.
  • TestSweetSpotSimilarity with -Ptests.nightly=true: 3 of 3 pass, including the @Nightly testSweetSpotComputeNorm (16 s), which no earlier run had enabled (stock passes it too).
  • The whole lucene/misc test task: 317 tests, 9 skipped, 0 failures (the repro included). :lucene:misc:check -x test passes with the repro in place.
  • With the fixed class shadowed into the jars: the independent reproduction's end-to-end test passes 8 of 8; the 10,000-document example gives the ClassicSimilarity hits; the study's 6-test JUnit and the ordinary-token test passed with the same fix on a9779ef3c193.
  • Not run: other JDKs, x86, the full Lucene test suite.

Compute in double

Cast to double before the subtraction, e.g. Math.abs((double) numTerms - l) + ....

Trade-offs

Also overflow-free and closer to the javadoc formula. For the default factors, keeping the sum in double (dropping the (float) cast) reproduces ClassicSimilarity exactly at all 255 bytes and all lengths 1-999, since 0.5 · 2(x − 1) + 1 = x is exact in double; keeping the (float) cast gives the same values as the long fix (a standalone probe of both variants). Neither changes the nightly comparison for lengths 1-999; for other factors a norm may move by about one float step versus today. Not run through gradle.

Reject a non-finite norm in TFIDFSimilarity (defensive, not a fix)

TFIDFSimilarity.scorer could throw when lengthNorm returns a non-finite value.

Trade-offs

Catches any subclass with the same kind of bug, but turns silent wrong results into a search-time failure, and still needs the long fix. Not run.

Downstream note

Downstream, an application that lets document authors set custom term frequencies (for example with DelimitedTermFrequencyTokenFilter) on a field scored with SweetSpotSimilarity lets one such document drop itself and nearby relevant hits from ranked results; filters and non-scoring queries are unaffected.

How it was found

A harness compared Lucene's scoring components with their documented formulas. For SweetSpotSimilarity.lengthNorm it checked the real jar against the javadoc formula, evaluated exactly, over lengths 1 to 1,000 and all 255 decoded norm lengths for 48 factor settings: 305 NaN results, all at norm bytes 248-255. A JUnit then showed the effect end to end.

The method in full124 words

Method: semi-formal (bounded-exhaustive translation validation of the real jar against exact semantics).

  • Harness FormulaTV, part A1: lengthNorm on the stock jar against the javadoc formula evaluated in BigDecimal, for n = 1..1000 and the 255 decoded norm lengths x 8 plateaus x 6 steepness values: 60,240 checks, 334 mismatches, 305 of them NaN, all at norm bytes 248-255; the rest are rounding differences of at most 1.2 ulp. Part A1b ("degrades to 1/sqrt(x)"): 8 mismatches in 1,255, all NaN at bytes 248-255.
  • The finder read the root cause in the code and bridged it end to end with one custom term frequency; an independent reproducer wrote its own test from the claim alone, and a devil's advocate reproduced it with ordinary tokens.

Prior reports, history and checks

Prior reports

No prior report found: 68 tool queries over GitHub, commits, downstream projects, the mailing lists, the JIRA archive and the web, 51 manual GitHub, JIRA and list searches, and the changed files of all 408 open PRs (2026-10-27); GitHub re-checked 2026-10-31. #11086 and #9990 discuss custom-frequency bounds in general.

Searches4 searches

Verdict new: no prior report was found in the searches below; 26 hits were read, none the same bug. Context, not reports: #11086 / LUCENE-10048 and #9990 / LUCENE-8947 (the user-side bound for custom frequencies is the int sum); #15584 (adds DOCS_AND_CUSTOM_FREQS, which counts tokens); LUCENE-6896, -7997, -8010 (finite scores for every norm value, core similarities only); #3347 / LUCENE-2271 (collectors do not handle NaN scores). Limits: keyword-based search, one page per GitHub query; private channels and security@ were not searched.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub (apache/lucene issues, PRs, commits), downstream repositories, mailing lists, JIRA archive, web2026-10-27a scripted prior-report search, 68 queries (20 GitHub, 3 commit, 2 downstream, 28 mailing-list over dev@, java-user@, issues@ and commits@, 6 JIRA, 9 web)350 distinct hits; the only high-overlap one (#11086) is context, not this bug
GitHub, JIRA and lists, manual2026-10-2733 GitHub searches (apache/lucene, apache/solr, apache/lucenenet, Elasticsearch, OpenSearch, all of GitHub), 5 JIRA and 13 list queries; 26 hits readno report of an overflow, NaN norm or hidden hits in SweetSpotSimilarity
Open PRs and the file's upstream log2026-10-27changed files of all 408 open apache/lucene PRs; log of SweetSpotSimilarity.java on main and the 10.x branchesno open PR touches the file; last change a 2024 refactor (#13757)
GitHub, refresh2026-10-31SweetSpotSimilarity issues and PRs; "lengthNorm NaN"; #11086 and #9990 comments re-readstill only the same old SweetSpotSimilarity issues (newest #8547, 2016; "SweetSpotSimilarity" returns 7 issues, "SweetSpot" 5); no PR, no "lengthNorm NaN" issue

History

In the class since its first check-in (LUCENE-577, 2006); first released in 2.0.0. NaN score and a document missing from top-k results since 7.0.0, skipped neighbours since 8.0.0, silent with assertions on since 10.1.0. The minimal test fails on main and 10.6.0. Not fixed upstream.

Versions tested8 rows

Introduced by d2b63d328be0 ("LUCENE-577: initial checkin of SweetSpotSimilarity", by Chris M. Hostetter, 2006-05-25, #1655; no PR). git log -S finds no other commit that changed the int expression; later edits (LUCENE-1420, LUCENE-2912 a01e9cbb861, which wrote today's final int l = ln_min; final int h = ln_max;, LUCENE-7730, LUCENE-9570 reformatting) kept it. Not a regression.

How the symptom changed: 2.0.0-6.6.6 stored the NaN norm in the index byte (on the aarch64 test host it became the largest norm, so the long document ranked first); LUCENE-7730 (06a6034d9bc, 7.0.0) moved lengthNorm to search time over decoded lengths and LUCENE-7854 (d276acfbbcb, 7.0.0) added custom term frequencies: score NaN, document dropped; LUCENE-4198 (f410df81136, 8.0.0, impacts) added the skipping; #13939 (f8ea1305144, 10.1.0) removed the collector assertion that used to trip with -ea.

Not fixed upstream: SweetSpotSimilarity.java is byte-identical on upstream main 2f424fd428da, branch_10_6 and the 10.6.0 tag (read, 2026-10-31).

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails (gradle, assertions on and off); 10,000-document example loses 4 of the top 6 hits
10.6.0 affectedminimal test fails (release jars, assertions on and off)
10.1.0 - 10.5.2 silent10.5.2 executed: the finder's and the independent reproduction's JUnits fail with assertions on and off; the rest by code identity
8.0.0 - 10.0.0 skippingNaN score, missing from top-k results (on 9.12.3 an exhaustive collection still returns it), skipped neighbours (reach varies by postings format); -ea trips assert score >= 0 first
7.0.0 - 7.7.3 droppedNaN score, document dropped, top-k of the others correct; -ea trips the collector assertion
2.0.0 - 6.6.6 first affected 2.0.0NaN stored in the norm byte at index time: on the aarch64 test host the long document ranked first
1.9.1 API absentno SweetSpotSimilarity: the probe does not compile
fix on 645d4fe14a20 not affectedrepro passes (assertions on and off); TestSweetSpotSimilarity incl. nightly, lucene/misc tests and check pass

Independent checks

An independent reproducer, a devil's advocate and an adjudication rated it real; cross-model reviews (OpenAI GPT-5.6, reading only) agreed on the root cause, one rating it real and one "not real" on procedure only. An audit re-ran it with fresh seeds and lowered the likelihood to contrived.

Checks and verdicts285 words

Neither cross-model review could run code; separate runs re-ran the repro. This brief re-ran the minimal test with gradle on 645d4fe14a20 and on 10.6.0, the independent reproduction's end-to-end test on 645d4fe14a20, and corrected the overflow condition (x ≥ max + 2^30, not an intermediate overflow of |x-min| + |x-max|).

Each independent check and its verdict
CheckVerdictWhat it found
Finder (bounded-exhaustive formula check, then a JUnit) candidate60,240 checks, 305 NaN, all at norm bytes 248-255; the JUnit fails on main, 10.5.2 and 9.12.3.
Cross-model review, refute lens (OpenAI GPT-5.6, reading only) not real (low, procedural)Could not run code; agreed with the root cause and rated the intended case weak; a separate re-run failed 4 of 6.
Independent reproducer realOwn test from the claim: 6 of 8 fail on main and 10.5.2 with assertions on and off; whole level-1 range lost; 8 of 8 pass with long.
Cross-model adversary, intended lens (OpenAI GPT-5.6, reading only) real (medium)Intended argument weak; inputs in contract; separate runs fail on main, 10.5.2 and 9.12.3 and pass with the fix.
Devil's advocate real (high)Reproduced with 1,073,741,848 ordinary tokens, no custom frequency; rebutted #11086 with #9990.
Adjudication confirmed (high)Re-ran the independent reproduction's test (6 of 8 fail with assertions on and off; 0 with the fix); adopted the wording corrections.
Credibility audit downgrade (likelihood contrived)Fresh seeds: fails 4 of 6, passes 6 of 6 with the fix.
Minimal repro and this brief still reproduces; fix validatedOne test; fails on 645d4fe14a20 (gradle, assertions on and off) and 10.6.0; the fix passes the repro, the class's suite incl. nightly, the misc tests and check.

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

Finding record

LFV-754, snapshot of 2026-11-06

Registry title
SweetSpotSimilarity.lengthNorm adds abs(x-min) + abs(x-max) in int, so a field of 1,073,741,848 or more terms (reachable with one custom term frequency) gets a NaN norm: the document is never collected, and default top-k search also skips competitive documents in the same postings block and skip range
Area
misc / similarities (SweetSpotSimilarity lengthNorm; NaN effect on impacts-based top-k pruning in core search)
Priority
Low, rank 972: A field of 2^30 terms under the opt-in legacy SweetSpotSimilarity gives NaN norms that drop the document and break top-k pruning, a contrived trigger that one custom term frequency can reach.
Impact class
silent wrong results
Impact note
Hits only applications that score a normed field with the opt-in SweetSpotSimilarity (lucene/misc; the default factors min = max = 1, steepness 0.5 are affected, other factors with steepness > 0 once |x-min| + |x-max| exceeds Integer.MAX_VALUE) and index one document whose field length is at least 1,073,741,848 (the smallest length that encodes to norm byte 248), in practice a custom term frequency of about 1.1e9 on a DOCS_AND_FREQS field or about 1.07e9 ordinary tokens: that document scores NaN and is never collected, and default top-k search silently skips competitive documents in its 256-doc postings block and, on main and 10.5.2, its whole 8192-doc level-1 skip range [exec: main and 10.5.2 with -ea and -da; 8.0.0 to 10.0.0 with -da, where -ea trips Lucene's own collector assertion instead; the NaN norm itself in every release since 2.0.0]; the 10.5 option DOCS_AND_CUSTOM_FREQS counts tokens and so closes the custom-frequency route, not the ordinary-token one.
Summary
You score with SweetSpotSimilarity (default factors) and index one document whose DOCS_AND_FREQS field has a length of at least 1,073,741,848, for example one token with custom term frequency 1.1e9 or about 1.07e9 ordinary tokens, expecting the documented lengthNorm 1/sqrt(x) (about 3.05e-5, the ClassicSimilarity value) and an ordinary, low-ranked hit. Instead lengthNorm's int sum |x-1| + |x-1| wraps negative, so the norm and the score are NaN and the document is never collected (exhaustive collection returns 5,999 of 6,000 matches); because the NaN also makes the postings block's maximum score NaN, default top-k search skips that block and the skip range above it, so search(q, 7) on a 10,000-document index misses three documents that score 1.0 and returns documents that score 0.5 instead. ClassicSimilarity on the same index is correct.
Introduced
d2b63d328be030bfd659319cdeea8b3adeb6f10e (LUCENE-577 'SweetSpotSimiliarity', the initial check-in of SweetSpotSimilarity, SVN r409472, mirrored as apache/lucene#1655; no pull request) [read: git log -S 'Math.abs(numTerms - l)' over the full history finds only this commit, git blame of today's lines; exec: 1.9.1 has no such class, 2.0.0 shows the NaN]: the int expression is in the class's first version, so this is not a regression, and every later change to the method (LUCENE-1420 24d76157a06, LUCENE-2912 a01e9cbb861, LUCENE-7730 06a6034d9bc, LUCENE-9570 2cbf261032d) kept it. Today's symptoms became reachable later: 06a6034d9bc (LUCENE-7730, #8781, 7.0.0) moved the NaN from the stored norm byte into the search-time norm table, d276acfbbcb (LUCENE-7854, #8905, 7.0.0) added custom term frequencies (the cheap trigger), and f410df81136 (LUCENE-4198, 8.0.0) added the impacts-based block skipping (2006-05-25)
First affected version
2.0.0 [exec: lengthNorm(1073741825) = NaN and a wrong stored norm byte, end to end; 2.0.0 was tagged the day after the introducing commit and its SweetSpotSimilarity.java is identical]; 1.9.1 is not affected because SweetSpotSimilarity does not exist there (the probe does not compile) [exec]. The present symptoms start later: NaN score and dropped hit in 7.0.0, skipping of other competitive documents in 8.0.0 [exec]
Affected versions
Every release from 2.0.0 to 10.6.0 and main a9779ef3c193 and 645d4fe14a20 [exec 2026-10-31: the minimal test fails on the 10.6.0 release jars (SHA-1 checked) and on main 645d4fe14a20 with -ea and -da]; never fixed (SweetSpotSimilarity.java last changed upstream in 7c056ab88c7, #13757, 2024-09-13, a refactor; unchanged at upstream main f7bcc0abd81, branch_10x and branch_10_6; no open PR touches it; read 2026-10-27). [exec] a public-API probe shows the NaN length norm on 18 releases (2.0.0, 2.4.1, 2.9.4, 3.0.3, 3.1.0, 3.6.2, 4.0.0, 4.10.4, 5.5.5, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2) and main; end to end on 11 of them (2.0.0, 2.4.1, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2) and main. Symptoms by era: 2.0.0-6.6.6 the NaN is encoded into the stored norm byte at index time (on this aarch64 host it becomes the largest norm, so the long document ranks first; the x86 outcome, norm 0, is reasoning only); 7.0.0-7.7.3 the score is NaN and the long document is dropped (-ea: Lucene's own collector assertion throws); 8.0.0-10.0.0 top-k also skips competitive documents (-ea: TopScoreDocCollector's 'assert score >= 0' throws first; the distant lost hit depends on the postings format: lost on 8.0.0, 8.11.4 and 9.0.0, found on 9.12.3 and 10.0.0); 10.1.0-10.5.2 and main silent with -ea and -da (the collector assertion was removed by #13939, f8ea1305144): the finder's JUnit fails 6 run / 4 failed. [read] all 144 release tags from 2.0.0 on in the history clone contain the int expression. All runs with JDK 25.0.3 on aarch64
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal: exhaustive check
Upstream and prior reports
none found (2026-10-27, read-only; the prior-report record, verdict new): a scripted prior-report search of 68 queries (20 GitHub issue/PR, 3 commit, 2 downstream-repository, 28 mailing-list queries over dev@, java-user@, issues@ and commits@, 6 JIRA-archive and 9 web queries; 350 distinct hits, the only high-overlap one #11086, context), 33 manual GitHub searches (apache/lucene, apache/solr, apache/lucenenet, Elasticsearch, OpenSearch and all of GitHub), 5 JIRA queries, 13 list queries, the changed files of all 408 open apache/lucene PRs and the upstream commit log of SweetSpotSimilarity.java on main, branch_10x and branch_10_6; the finder's own scripted prior-report search (374 hits, 0 high) agrees; 26 hits read, none the same bug. Closest items, context not reports: #11086 (LUCENE-10048, won't fix: a maintainer says custom term frequencies need 'reasonable values' or 'things can easily overflow, break scoring'; it is about bypassing the int-sum check, not about lengths inside it); #9990 (LUCENE-8947: the user-side bound is that the frequency sum fits in an int, which this trigger satisfies); #15584 (merged 2026-04-24: adds DOCS_AND_CUSTOM_FREQS, which counts tokens; a maintainer notes remaining search-time concerns in general terms, nothing on SweetSpotSimilarity or NaN norms); #7876, #7954, #9045, #9058 (LUCENE-6818, -6896, -7997, -8010: finite, non-negative scores for every norm value, tested for core similarities only; TestSweetSpotSimilarity never runs that check); #3347 (LUCENE-2271, open: collectors do not support NaN scores, the downstream mechanism). Not fixed upstream: no open PR touches SweetSpotSimilarity.java, TestSweetSpotSimilarity.java, TFIDFSimilarity.java or ClassicSimilarity.java. Gaps: keyword-based search, GitHub's search rate limit, web search throttling, private channels and security@ not searched
Lucene commit tested
main 645d4fe14a20: gradle in a scratch worktree (the minimal test with assertions on and off, stock and fixed; TestSweetSpotSimilarity with its @Nightly length-norm test; the whole lucene/misc test task; :lucene:misc:check -x test), and the standalone runner (the independent reproduction's end-to-end test, a main() of the 10,000-document index and norm-table probes, stock and with the fixed class shadowed into the jars); the 10.6.0 release jars (assertions on and off). The study's runs: main a9779ef3c193, whose relevant code is identical, 10.5.2 and 9.12.3, and a probe over 18 releases from 2.0.0 to 10.5.2 (end to end on 11). Only OpenJDK 25.0.3 on Linux aarch64. Upstream main 2f424fd428da, branch_10_6 and the 10.6.0 tag have the same SweetSpotSimilarity.java (read, 2026-10-31).

Lucene and upstream sources

Finding record
  • lucene/misc/src/java/org/apache/lucene/misc/SweetSpotSimilarity.java:36-38 (at a9779ef3c193) (defaults ln_min = 1, ln_max = 1, ln_steep = 0.5), :98-102 (lengthNorm javadoc: 'Implemented as: 1/sqrt( steepness * (abs(x-min) + abs(x-max) - (max-min)) + 1 )' and 'This degrades to 1/sqrt(x) when min and max are both 1 and steepness is 0.5'), :111-119 (lengthNorm: 'final int l = ln_min; final int h = ln_max;' and Math.abs(numTerms - l) + Math.abs(numTerms - h) - (h - l) evaluated in int)
  • lucene/core/src/java/org/apache/lucene/search/similarities/TFIDFSimilarity.java:427-446 (at a9779ef3c193) (LENGTH_TABLE = SmallFloat.byte4ToInt of every byte; normTable[i] = lengthNorm(LENGTH_TABLE[i]) for i = 1..255; normTable[0] = 1f / normTable[255])
  • lucene/core/src/java/org/apache/lucene/search/similarities/Similarity.java:200-221 (at a9779ef3c193) (SimScorer#score: score must not increase when the unsigned norm increases, for any norm computeNorm may produce)
  • lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2054-2063 (at a9779ef3c193) (field length summed with Math.addExact, custom term frequencies included; only an int overflow is rejected, as 'too many tokens')
  • lucene/core/src/java/org/apache/lucene/analysis/tokenattributes/TermFrequencyAttribute.java:23-34 (at a9779ef3c193) (custom term frequency, for DOCS_AND_FREQS fields)
  • lucene/core/src/java/org/apache/lucene/search/BatchScoreBulkScorer.java:54 (at a9779ef3c193) (collect only if score >= minCompetitiveScore: false for NaN), MaxScoreCache.java:86 (block max = Math.max over impacts: NaN propagates), ImpactsDISI.java:80 (a block is kept only if maxScore >= minCompetitiveScore), TopScoreDocCollector.java:30-31 (NaN and NEGATIVE_INFINITY 'are not valid scores')
  • lucene/misc/src/test/org/apache/lucene/misc/TestSweetSpotSimilarity.java:73-86 (at a9779ef3c193) (@Nightly testSweetSpotComputeNorm, 'base case, should degrade': equality with ClassicSimilarity for lengths 1..999 only)
The finding in 30 seconds
  • lucene/misc/src/java/org/apache/lucene/misc/SweetSpotSimilarity.java:36-38, (at 645d4fe14a20) 97-120
  • lucene/core/src/java/org/apache/lucene/search/similarities/Similarity.java:151-161; (at 645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/similarities/TFIDFSimilarity.java:417-446, 470
  • lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2053-2064; (at 645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/FieldInfo.java:718-723; lucene/core/src/java/org/apache/lucene/analysis/tokenattributes/TermFrequencyAttribute.java:23-34
  • lucene/core/src/java/org/apache/lucene/search/BatchScoreBulkScorer.java:53-55; (at 645d4fe14a20) MaxScoreCache.java:84-88, 137-158; ImpactsDISI.java:80-97; lucene/core/src/java/org/apache/lucene/codecs/lucene104/Lucene104PostingsFormat.java:343-351
Who is affected
  • lucene/core/src/java/org/apache/lucene/search/similarities/Similarity.java:151-161 (at 645d4fe14a20) (the norm byte stores the encoded length)
Why it looks unintended
  • lucene/misc/src/java/org/apache/lucene/misc/SweetSpotSimilarity.java:36-38, (at 645d4fe14a20) 97-102
  • lucene/misc/src/test/org/apache/lucene/misc/TestSweetSpotSimilarity.java:39, (at 645d4fe14a20) 72-86
  • lucene/test-framework/src/java/org/apache/lucene/tests/search/similarities/BaseSimilarityTestCase.java:256, (at 645d4fe14a20) 281, 383-387
  • lucene/core/src/java/org/apache/lucene/search/similarities/Similarity.java:207-213; (at 645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/TopScoreDocCollector.java:30-31; lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2053-2064
  • github.com/apache/lucene/issues/9990 and /issues/11086 (comments read with gh, read-only, 2026-10-31)
Prior reports
  • gh read-only queries, 2026-10-31 (gh search issues/prs --repo apache/lucene SweetSpotSimilarity; "lengthNorm NaN"; gh issue view 11086 and 9990 --comments)
History
  • a Lucene clone (SweetSpotSimilarity.java md5 at 645d4fe14a20, origin/main 2f424fd428da, origin/branch_10_6 7e5aec4370e, releases/lucene/10.6.0, releases/lucene/10.5.2: identical)

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 from the code at 645d4fe14a20 and the executed runs.

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.