All findings · Lucene finding · LFV-750 · sweetspot-hyperbolictf-overflow-returns-max

SweetSpotSimilarity.hyperbolicTf returns max instead of min for frequencies far below its midpoint

SweetSpotSimilarity.hyperbolicTf, the S-shaped tf curve a subclass can use as its tf(), returns its maximum instead of its minimum for frequencies far below the midpoint. With factors (min 0, max 2, base 10, midpoint 400), tf(1) is 2.0 instead of 0.0, so one mention of a term outranks 400.

Summary

Priority
Low, rank 722SweetSpotSimilarity hyperbolicTf silently inverts ranking once configured, but only with the opt-in helper and a midpoint far above real term frequencies, a parameter-overflow profile like FeatureField's LFV-258 with less extreme parameters.
Impact
silent wrong results
Since
2.0.0 (2006, first version of the class)introduced 2006-05-25 · over 20 years
Affected
every release from 2.0.0 to 10.6.0, and main; opt-in helper only
Status
documented
Found by
semi-formal: bounded-exhaustive check of the helper against its javadoc formula

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

Background

  1. tf. In ClassicSimilarity (TF-IDF), a document's score for a term is multiplied by tf(freq), a function of how often the term occurs in the document, at search time. SweetSpotSimilarity extends it.
  2. hyperbolicTf is an optional S-shaped tf curve: about min well below the midpoint xoffset, halfway at it, about max well above. The stock tf() does not use it; a subclass opts in by returning hyperbolicTf(freq) from tf().
  3. Overflow. A double holds at most about 1.8e308, so for base > 1 base^t becomes Infinity once t passes about ln(1.8e308) / ln(base): about 308 at base 10, about 2705 at the default base 1.3.
Diagram of the bug: the tf curve of SweetSpotSimilarity.hyperbolicTf for min 0, max 2, base 10, midpoint 400. The documented curve is an S that is about 0 below the midpoint, 1.0 at 400 and 2.0 above. The actual values are 2.0 for every frequency from 1 to 91, because Math.pow(10, 400 - freq) overflows to Infinity, the ratio is NaN and the NaN guard returns max; at 92 the value drops to 0.0 and then follows the S-curve.
The tf curve for factors (0, 2, base 10, midpoint 400): the documented S-curve stays near 0 below the midpoint, while hyperbolicTf returns 2.0 for freq 1 to 91, falls to 0.0 at 92 and only then follows the curve. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You subclass SweetSpotSimilarity so that tf(freq) returns hyperbolicTf(freq), as its javadoc suggests, and call setHyperbolicTfFactors(0, 2, 10, 400): min 0, max 2, base 10, midpoint 400.
  2. You expect the documented S-curve: tf about 0 well below 400, 1.0 at 400, 2.0 well above, never decreasing as the frequency grows.
  3. Instead tf is 2.0, the maximum, for every frequency from 1 to 91, drops to 0.0 at 92, and only then follows the curve: 1.0 at 400, 2.0 at 800.
  4. Why: far below the midpoint Math.pow(10, 400 - freq) overflows to Infinity, the ratio -Infinity / Infinity is NaN, and the guard Float.isNaN(result) ? max : result, right only for the overflow above the midpoint, returns max.
  5. Effect: of two equally long documents (450 terms), the one with the term once scores 0.0953 and the one with it 400 times 0.0477. No error. It needs xoffset - freq above about 308 at base 10, or about 2705 at the default base 1.3.

Who is affected

Only applications that opt in: a SweetSpotSimilarity subclass whose tf() returns hyperbolicTf, with the midpoint far above their documents' term frequencies (more than about 2705 occurrences at the default base 1.3, 308 at base 10). The class defaults, the stock tf() and the documented example curves never trigger it.

Mechanism, terms and numbers550 words

SweetSpotSimilarity offers hyperbolicTf, a tanh-shaped tf curve "provided as a convenience for subclasses that want to use a hyperbolic tf function": about min below the midpoint xoffset, about max above it. It computes the tanh as (base^x - base^-x) / (base^x + base^-x) with x = freq - xoffset. Once |x| passes ln(Double.MAX_VALUE) / ln(base) (for base > 1: about 308 at base 10, 2705 at the default base 1.3) one power overflows to Infinity and the ratio is NaN, on either side of the midpoint. The guard return Float.isNaN(result) ? max : result; returns max for both: right above the midpoint, wrong below it, where the formula gives min. A subclass that uses hyperbolicTf as its tf() with a midpoint far above its documents' frequencies then gives the largest tf to the rarest mentions and inverts rankings, silently, in every release since 2.0.0. The class defaults, the stock tf() and the documented example curves are unaffected. A one-branch fix (use Math.tanh when the ratio is NaN) was validated.

The code (main 645d4fe14a20)

SweetSpotSimilarity.java:160-179:

final double x = (double) (freq - xoffset);                      // :167
final float result =
    min + (float) ((max - min) / 2.0f
        * (((Math.pow(base, x) - Math.pow(base, -x))
                / (Math.pow(base, x) + Math.pow(base, -x)))          // :174-175
            + 1.0d));
return Float.isNaN(result) ? max : result;                          // :178
  1. The formula (javadoc, :150-152): tf(x)=min+(max-min)/2*(((base**(x-xoffset)-base**-(x-xoffset))/(base**(x-xoffset)+base**-(x-xoffset)))+1). The ratio is tanh((x - xoffset) * ln(base)): -1 far below the midpoint, 0 at it, +1 far above it.
  2. Far above the midpoint (freq 800, xoffset 400, base 10: x = 400): Math.pow(10, 400) is Infinity, Math.pow(10, -400) is 0, the ratio is Infinity / Infinity = NaN, and the guard returns max = 2.0. The formula's limit is max too: this side is right.
  3. Far below the midpoint (freq 1: x = -399): Math.pow(10, 399) is Infinity and Math.pow(10, -399) is 0, so the ratio is (0 - Infinity) / (0 + Infinity) = NaN, and the same guard returns max = 2.0. The formula's limit here is min = 0.
  4. Just inside the range (freq 92: x = -308): 10^308 is still finite, the ratio is exactly -1 and the result is 0.0. So tf falls from 2.0 at freq 91 to 0.0 at freq 92.
  5. Where it reaches scores. The TF-IDF scorer multiplies tf(freq) by the query weight at search time (TFIDFSimilarity.java:469). The stock SweetSpotSimilarity.tf delegates to baselineTf (:122-130), so only a subclass whose tf() returns hyperbolicTf is affected; no non-test code in Lucene calls hyperbolicTf.

Overflow points

For base > 1, hyperbolicTf returns max wrongly when xoffset - freq is above about 709.78 / ln(base): about 2705 at base 1.3 (the default), 1024 at base 2, 710 at e, 308 at base 10, 154 at base 100. A verifier measured the first failing xoffset at freq 1 for 14 bases (2707 at base 1.3, 1025 at 2, 711 at e, 310 at 10, 156 at 100); each matched this threshold.

The numbers

Factors (0, 2, 10, 400): tf = 2.0 for freq 1 to 91, 0.0 at 92, 1.0 at 400, 2.0 at 800. The exhaustive check over 720,000 inputs found 37,209 mismatches with the formula, every one of them this fallback, and no other kind.

Exposure and likelihood262 words
  • Who. Opt-in only. The class javadoc says "baselineTf and hyperbolicTf functions are provided, which subclasses can choose between" (SweetSpotSimilarity.java:28-29), and the upstream test builds exactly such a subclass (TestSweetSpotSimilarity.java:207-213). Downstream, Solr's SweetSpotSimilarityFactory builds one from the hyperbolicTfMin/Max/Base/Offset schema settings, without a range check (read in Solr's source, not run).
  • When. xoffset - freq above about 2705 at the default base 1.3 (a midpoint of thousands of occurrences), or above 308 at base 10 (a steep, near step-like curve with a midpoint of hundreds). The class author's advice is to pick "the value that makes sense for your data", and that for books or encyclopedia articles "the sweetspots are going to be much larger" (java-user@, 2012), so very long documents are where such a setting is plausible.
  • Not affected (executed): the class defaults (0, 2, 1.3, 10), the four curves in the shipped doc-files/ss.gnuplot and the upstream test factors (3.3, 7.7, e, 5): 0 mismatches over freq 1..100000.
  • Effect. Once configured it is deterministic: every document whose frequency is below the overflow point gets the largest possible tf, documents just above it get about the smallest, and rankings invert. No exception, assertions on and off. Values stay inside [min, max], so range checks do not notice.
  • Nothing is written to the index. tf is applied at search time, so a fixed jar corrects existing indexes without reindexing.
  • Not claimed: an effect on impact-based top-k pruning (WAND/MaxScore). It is plausible, since scores must not decrease with freq, but it was not executed.
  • How likely: rare. No report in 20 years.

Minimal repro

One JUnit test, one assertion, no index and no randomness: setHyperbolicTfFactors(0, 2, 10, 400), then hyperbolicTf(1) must be 0.0, the documented value. It calls the helper directly, which is exactly what a subclass's tf() returns.

TestSweetSpotHyperbolicTfFarBelowMidpoint.java (one test, public API)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 TestSweetSpotHyperbolicTfFarBelowMidpoint extends LuceneTestCase {

  public void testFarBelowMidpointGivesMin() {
    SweetSpotSimilarity sim = new SweetSpotSimilarity();
    sim.setHyperbolicTfFactors(0f, 2f, 10, 400f);
    // javadoc formula: 0 + (2-0)/2 * (tanh((1-400)*ln 10) + 1) = 0 (min); actual 2.0 (max)
    assertEquals("hyperbolicTf(1), factors (0, 2, 10, 400)", 0f, sim.hyperbolicTf(1), 1e-6f);
  }
}
Command
cd $LUCENE && ./gradlew :lucene:misc:test --tests TestSweetSpotHyperbolicTfFarBelowMidpoint -Ptests.seed=DEADBEEF -Ptests.asserts=true
Expected
hyperbolicTf(1) = 0.0, the javadoc formula: 0 + 1 × (tanh(-399 × ln 10) + 1) = 0.
Observed
hyperbolicTf(1) = 2.0, the maximum: expected:<0.0> but was:<2.0>, with assertions on and off.

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

More files, environment, full output222 words

Public API only; the test sits in org.apache.lucene.misc next to TestSweetSpotSimilarity but needs no package access. Base 10 and midpoint 400 are small round values safely past the overflow point: at freq 1 the first failing integer midpoint is 310 at base 10 and 2707 at the default base 1.3 (so, for example, 3000 there).

The ranking inversion (two 450-term documents, 0.0953 for the single mention against 0.0477 for 400 mentions) is a consequence of this value, shown by the finder's longer test; it is not part of this repro.

Regression test: this same test; it passes with the recommended fix. It could also go into TestSweetSpotSimilarity as one more method.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; with the fix: passes. The same code is in every release from 2.0.0 to 10.6.0
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (the only platform run; pure Java arithmetic)
Command (environment)
cp $REPRO/TestSweetSpotHyperbolicTfFarBelowMidpoint.java $LUCENE/lucene/misc/src/test/org/apache/lucene/misc/
cd $LUCENE && ./gradlew :lucene:misc:test --tests TestSweetSpotHyperbolicTfFarBelowMidpoint -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false

Observed output

# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true (stack trimmed; -Ptests.asserts=false identical)
TestSweetSpotHyperbolicTfFarBelowMidpoint > testFarBelowMidpointGivesMin FAILED
    java.lang.AssertionError: hyperbolicTf(1), factors (0, 2, 10, 400) expected:<0.0> but was:<2.0>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:82178102E9400FED]: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 org.apache.lucene.misc.TestSweetSpotHyperbolicTfFarBelowMidpoint.testFarBelowMidpointGivesMin(TestSweetSpotHyperbolicTfFarBelowMidpoint.java:27)

1 test completed, 1 failed

# with the recommended fix: BUILD SUCCESSFUL (assertions on and off)

Why it looks unintended

  • The javadoc writes the formula out, min+(max-min)/2*(tanh((x-xoffset)*ln(base))+1); below the midpoint it tends to min.
  • SimScorer#score: "Score must not decrease when freq increases"; this tf falls from 2.0 at freq 91 to 0.0 at 92.
  • The NaN guard is right only above the midpoint, the only side the upstream test reaches (xoffset 5, freq up to 1000).
The full argument226 words
  • The formula is the spec. The method javadoc gives it exactly (SweetSpotSimilarity.java:150-152); setHyperbolicTfFactors calls xoffset "the midpoint of the hyperbolic function" (:75); the shipped doc-files/ss.gnuplot:47 plots the same formula. The class author: "The functions are pretty clearly spelled out in the javadocs" (java-user@, 2012), and of the default curve on dev@ (2006): "~2 for all values greater then 15, and ~0 for all values less then 5".
  • Monotonicity is a contract. Similarity.java:207-209: "Score must not decrease when freq increases, ie. if freq1 > freq2, then score(freq1, norm) >= score(freq2, norm)". The TF-IDF scorer multiplies tf by a fixed positive query weight and norm (TFIDFSimilarity.java:469-471), so a tf that falls from 2.0 at freq 91 to 0.0 at 92 makes the score fall too. TFIDFSimilarity.java:342-344: tf implementations "usually return larger values when freq is large".
  • The guard was only exercised on the correct side. TestSweetSpotSimilarity.java:205-226 uses factors (3.3, 7.7, e, 5) and freq 1..1000; with base e the power overflows only for freq >= 715, above the midpoint. It checks 3.3 <= tf <= 7.7, tf(5) = 5.5 and tf(0) = 0, nothing below the midpoint.
  • History. The guard is in the first check-in (2006, LUCENE-577 / #1655), which describes hyperbolicTf as "available for subclasses to call from their own tf functions" and never discusses overflow.
The devil's-advocate check113 words

The strongest case for "intended": the NaN-to-max guard is deliberate code, it keeps the value inside the documented bounds (:71-72: min and max are "the minimum/maximum tf value to ever be returned"), and the trigger factors are unusual.

Why it fails: staying inside [min, max] is necessary, not sufficient. The documented formula, the plotted curve and the author's own description all put the value near min there; returning max makes tf decrease with freq, which SimScorer#score forbids; and the only evidence of the guard's purpose, the upstream test, reaches only the upper overflow. Rarity lowers the impact; it does not make the behaviour intended. All verifiers rated the intended-behaviour argument weak.

Suggested fix

Recommended Fall back to Math.tanh when the ratio is NaN (validated)

When the Math.pow ratio is NaN (a power overflowed on either side of the midpoint), use Math.tanh(x * Math.log(base)), the same quantity computed without overflow. Values that are finite today do not change; the repro, TestSweetSpotSimilarity and the whole misc module (317 tests) pass.

double ratio =
    (Math.pow(base, x) - Math.pow(base, -x)) / (Math.pow(base, x) + Math.pow(base, -x));
if (Double.isNaN(ratio)) {
  // base^x or base^-x overflowed to Infinity, far above or far below xoffset; the ratio is
  // tanh(x * ln(base)), which goes to +1 above the midpoint and to -1 below it (for base > 1)
  ratio = Math.tanh(x * Math.log(base));
}
final float result = min + (float) ((max - min) / 2.0f * (ratio + 1.0d));
All options and trade-offs3 options · 635 words

Option A was built and run through gradle on 645d4fe14a20; option B passed the repro and the closest suite; option C was not run.

Fall back to Math.tanh when the ratio is NaN (validated) (recommended)

In hyperbolicTf (SweetSpotSimilarity.java:169-176), compute the ratio first and, when it is NaN, replace it with Math.tanh(x * Math.log(base)) (+8/-8 lines).

double ratio =
    (Math.pow(base, x) - Math.pow(base, -x)) / (Math.pow(base, x) + Math.pow(base, -x));
if (Double.isNaN(ratio)) {
  // base^x or base^-x overflowed to Infinity, far above or far below xoffset; the ratio is
  // tanh(x * ln(base)), which goes to +1 above the midpoint and to -1 below it (for base > 1)
  ratio = Math.tanh(x * Math.log(base));
}
final float result = min + (float) ((max - min) / 2.0f * (ratio + 1.0d));

Trade-offs

  • No change where the code works today. Only the NaN branch changes, so every finite value stays bit-identical: over freq 1..100000 the stock and fixed values differ only at the overflow points (91, 294 and 76 of them in three trigger configurations) and nowhere for the defaults, the gnuplot curves and the upstream test factors.
  • Upper side unchanged in effect. Above the midpoint tanh gives +1, so the result is min + (max - min) in float, the value the finite path already returns just below the overflow point; no tf above max and no decreasing step in the eight probed configurations.
  • Also covers the base < 1 side case (a decreasing curve), where the stock guard returns max far above the midpoint. The final isNaN guard stays for NaN factors.
  • Cost. The extra log and tanh run only on overflow; no benchmark was run.

What the checks validated

Scratch worktree of 645d4fe14a20, gradle, OpenJDK 25.0.3, seed DEADBEEF (2026-10-31):

  • stock: the minimal test fails with assertions on and off;
  • with the fix: the minimal test and TestSweetSpotSimilarity pass with assertions on and off; TestSweetSpotSimilarity with -Ptests.nightly=true passes 3 of 3, including the nightly testSweetSpotComputeNorm;
  • the whole :lucene:misc:test passes: 317 tests, 0 failures, 9 skipped;
  • checkGoogleJavaFormat, ecjLintMain, ecjLintTest and forbiddenApisMain for misc pass;
  • earlier, the finder's version of the same logic made the finder's 5-test class, an independent verifier's 5-test class and the portable history repro pass.

Always compute Math.tanh(x * Math.log(base)) (validated on the repro and the closest suite)

Replace the Math.pow ratio everywhere with Math.tanh(x * Math.log(base)), which cannot overflow (+2/-7 lines).

// (base^x - base^-x) / (base^x + base^-x) is tanh(x * ln(base)); Math.tanh does not overflow
final float result =
    min + (float) ((max - min) / 2.0f * (Math.tanh(x * Math.log(base)) + 1.0d));

Trade-offs

Simpler, and no Math.pow at all, but a few finite values change: by up to 4.4e-16 absolute, at values near min (the largest: 4.3785993E-9 becomes 4.3785997E-9), at 3, 28 and 1 points of the three trigger configurations; none for the defaults, gnuplot curves and upstream test factors over freq 1..100000. Not benchmarked.

What the checks validated

Same worktree: the minimal test and TestSweetSpotSimilarity (-Ptests.nightly=true, 3 of 3) pass, and checkGoogleJavaFormat passes. The whole module was not run with this option.

Return min or max by the sign of x (not recommended, not run)

return Float.isNaN(result) ? (x > 0 ? max : min) : result;

Trade-offs

The smallest diff, but right only for base > 1: for base < 1 the limits swap, so it would need (x > 0) == (base > 1). It keeps two hand-coded limits next to a formula that already has them; A and B follow the formula for every finite base > 0. A javadoc-only limit (xoffset below ln(Double.MAX_VALUE) / ln(base), about 308 at base 10) would avoid the overflow, since freq is always positive, but it would forbid long-document midpoints and still return max silently above the limit; the code fix is one branch.

Downstream note

Solr's SweetSpotSimilarityFactory builds a hyperbolicTf subclass from the hyperbolicTfMin/Max/Base/Offset schema settings without a range check (read in its source, not run), so there the four hyperbolicTf* schema settings alone, with no Java code, can enable this.

How it was found

A bounded-exhaustive check compared hyperbolicTf on the stock jar with its javadoc formula over 720,000 inputs (freq 1..5000, 6 bases, 8 midpoints, 3 min/max pairs): 37,209 mismatches, every one this NaN fallback returning max where the formula gives min, and no other kind. A public-API JUnit then confirmed it, including the ranking inversion.

The method in full141 words

Method: semi-formal (the hunt's tier for bounded-exhaustive checks of the real jar against exact semantics). The hunt unit checked scoring components against their documented formulas.

  • Translation validation. The harness evaluated hyperbolicTf on the stock jar and the javadoc formula written as min+(max-min)/2*(tanh((freq-xoffset)*ln(base))+1) over freq 1..5000 × base {1.01, 1.3, 2, e, 10, 100} × xoffset {0, 5, 10, 100, 300, 400, 1000, 3000} × 3 (min, max) pairs: 720,000 checks, 37,209 mismatches, all the fallback (got max, formula about min), 0 others.
  • JUnit. A 5-test public-API class failed 4 of 5 on main (assertions on and off), 10.5.2 and 9.12.3 (assertions on): tf(1) = 2.0 for (0, 2, 1.3, 3000) and (0, 2, 10, 400), hyperbolicTf(92)=0.0 < hyperbolicTf(91)=2.0, and the ranking inversion; its control just above the overflow point passed. The minimal repro above is one assertion of that class.

Prior reports, history and checks

Prior reports

No prior report found: 216 recorded queries over GitHub issues and PRs (including the JIRA archive), commits, mailing lists and the web on 2026-10-27, re-checked on 2026-10-31; hyperbolicTf and setHyperbolicTfFactors appear in no issue or PR.

Searches3 searches
  • Closest items, none a report of this bug: the 2012 java-user@ thread "SweetSpotSimilarity" (a usage question), the 2006 dev@ thread describing the default curve, #1655 (LUCENE-577, added the class and the guard, never discussed), SOLR-1365 (Solr's factory exposes the four factors without a range check), #9045 (LUCENE-7997, tf monotonicity for core similarities only) and #5997 (LUCENE-4933, a different SweetSpot tf bug).
  • Same class, the hunt's own findings: LFV-754 (lengthNorm int overflow) and LFV-413 (lengthNorm plateau and top-k pruning). Different methods.
  • Limits: keyword-based list search; private reports are not visible.
What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive, lucene-solr, solr, lucenenet, Elasticsearch, all of GitHub)2026-10-27three scripted prior-report search runs (48 issue/PR, 9 commit, 6 downstream queries) plus 44 manual searches, a code search for hyperbolicTf and the changed files of all 408 open apache/lucene PRshyperbolicTf and setHyperbolicTfFactors: 0 issues, PRs and commits; no open PR touches the file
Mailing lists (dev@, java-user@, issues@, commits@, general@, Solr lists), JIRA, web2026-10-2760 tool list queries, 19 direct list searches, 12 + 7 JIRA queries, 9 web searches; 16 hits readonly SweetSpot usage and contribution threads (2006, 2008, 2012) and commit mails; none mentions overflow or a wrong value
GitHub issues and PRs (apache/lucene)2026-10-31read-only searches for hyperbolicTf, setHyperbolicTfFactors and SweetSpotSimilarity, and open PRs mentioning SweetSpotSimilarity0, 0 and 7 old closed items (last updated 2022), no open PR

History

In the first version of the class (d2b63d328be, LUCENE-577, 2006) and never changed since, so every release from 2.0.0 to 10.6.0 and current main have it; executed on 18 releases from 2.0.0 to 10.6.0 and on main.

Versions tested7 rows

Introduced by d2b63d328be030bfd659319cdeea8b3adeb6f10e (2006-05-25, "LUCENE-577: initial checkin of SweetSpotSimilarity", SVN r409472; issue LUCENE-577, mirrored as #1655). The Math.pow ratio and the guard were both in the first version: not a regression. 2cbf261032d (LUCENE-9570, 2021) only reformatted the body and 9089a6c0fb5 (2012) changed only javadoc. 1.9.1 has no SweetSpotSimilarity.

Now: SweetSpotSimilarity.java is identical to 645d4fe14a20 in 10.6.0, branch_10_6, branch_11x and upstream main (refs fetched 2026-10-30).

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails with assertions on and off (gradle and standalone); passes with the fix
main a9779ef3c193 affectedportable repro and the JUnit classes fail (assertions on and off); pass with the fix
10.5.2, 9.12.3 affectedthe finder's and an independent verifier's JUnit fail 4 of 5; the portable repro fails
2.0.0, 2.4.1, 2.9.4, 3.0.3, 3.6.2, 4.0.0, 4.10.4, 5.0.0, 5.5.5, 6.6.6, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 10.0.0 affectedportable repro fails with identical values; its four controls pass
10.6.0 affectedminimal test fails (the repro runner, release jars, assertions off, 2026-10-31); the file is identical to 645d4fe14a20
branch_10_6, branch_11x, upstream main 2f424fd428d same codethe file is identical to 645d4fe14a20 (read, not run)
1.9.1 no classSweetSpotSimilarity does not exist; the repro does not compile

Independent checks

Reproduced by an independent reproducer, a devil's-advocate verifier and an adjudication with fresh seeds (assertions on and off; main, 10.5.2, 9.12.3); read-only reviews by OpenAI GPT-5.6 agree on the root cause; this doc's gradle run confirms the repro and the fix.

Checks and verdicts261 words

Neither cross-model review could run code; all execution evidence comes from the Claude-family AI tools and separate runs.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (bounded-exhaustive formula check, JUnit) candidate720,000 checks against the javadoc formula: 37,209 mismatches, all the NaN fallback; the 5-test JUnit fails 4 of 5 on main (assertions on and off), 10.5.2 and 9.12.3 (assertions on); the fix passes it.
Refute review (OpenAI GPT-5.6, read-only) not real on procedure only (low)Could not execute; calls its verdict "not a substantive refutation", agrees with the root cause and rates the intended case weak; a separate re-run fails 4 of 5.
Independent reproducer realOwn 5-test JUnit fails 4 of 5 on main, 10.5.2 and 9.12.3; 19,798 grid mismatches of 192,000, exactly the analytic overflow count; end-to-end ranking inverted (0.0618 vs 0.0309); passes with the fix.
Works-as-designed lens (OpenAI GPT-5.6, read-only) real (medium)Intended argument weak; root cause agrees; separate runs fail with assertions off and pass with the fix.
Intended-behaviour devil's advocate real (high; low reach)Overflow thresholds for 14 bases as predicted on main, 10.5.2 and 9.12.3; the documented parameter sets give 0 mismatches over freq 1..100000.
Adjudication confirmed (high)Re-ran the independent reproduction's tests with seed 1A2B3C4D (4 of 5 fail, assertions on and off) and the finder's; the independent reproduction's pass with the fix.
This doc (gradle on 645d4fe14a20) realStock fails; fix A passes the repro, TestSweetSpotSimilarity with nightly and the whole misc module; fix B passes the repro and TestSweetSpotSimilarity.

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

Finding record

LFV-750, snapshot of 2026-11-06

Registry title
SweetSpotSimilarity.hyperbolicTf returns max instead of about min for frequencies far below xoffset, because its NaN fallback is right only for the Math.pow overflow above the midpoint, so a hyperbolic-tf subclass is not monotone in freq and ranks a document with one occurrence above one with hundreds
Area
misc / similarities (SweetSpotSimilarity hyperbolicTf helper)
Priority
Low, rank 722: SweetSpotSimilarity hyperbolicTf silently inverts ranking once configured, but only with the opt-in helper and a midpoint far above real term frequencies, a parameter-overflow profile like FeatureField's LFV-258 with less extreme parameters.
Impact class
silent wrong results
Impact note
Hits only applications that score with a SweetSpotSimilarity subclass whose tf() returns hyperbolicTf (the use its javadoc names; in Solr, SweetSpotSimilarityFactory with the hyperbolicTf* settings builds exactly such a subclass [read]) and that set the midpoint far above the term frequencies their documents have (xoffset - freq above about 2705 at the default base 1.3, 1024 at base 2, 710 at base e, 308 at base 10): every such low frequency then gets the maximum tf, tf falls to about min just above the overflow point, and rankings invert, with no error, with -ea and -da, in every release since 2.0.0; the stock SweetSpotSimilarity.tf (baselineTf), the class defaults, the shipped gnuplot curves and the upstream test factors are unaffected, nothing is written to the index, and any effect on impact-based top-k pruning was not executed.
Summary
You subclass SweetSpotSimilarity so that tf() returns hyperbolicTf(freq), as its javadoc suggests, call setHyperbolicTfFactors(0, 2, 10, 400) and search, expecting the documented curve min+(max-min)/2*(tanh((freq-xoffset)*ln(base))+1): a tf near min (0) for frequencies far below the midpoint 400, rising to max (2) above it. Instead hyperbolicTf returns the maximum 2.0 for every frequency from 1 to 91 and drops to 0.0 at 92, so a document that contains the term once outranks an equally long one that contains it 400 times. Far below the midpoint Math.pow(base, xoffset - freq) overflows to Infinity (once xoffset - freq exceeds about 308 at base 10, about 2705 at the default base 1.3), the ratio becomes NaN, and the guard 'return Float.isNaN(result) ? max : result', which is right only for the overflow above the midpoint, returns max.
Introduced
d2b63d328be030bfd659319cdeea8b3adeb6f10e (LUCENE-577 'SweetSpotSimiliarity', initial check-in of SweetSpotSimilarity by hossman, SVN r409472, mirrored as apache/lucene#1655; no pull request) [read: git log --follow, git log -L on hyperbolicTf and git blame -w -M -C -C in the history clone; exec: 1.9.1 has no such class, 2.0.0 fails]: the Math.pow ratio and the NaN guard were both in the first check-in, so this is not a regression; the only later commit touching the method body, 2cbf261032dc (LUCENE-9570, 2021-01-05), is a whitespace-only reformat (the body is token-identical to 2006 on main), and 9089a6c0fb5 (2012) changed only javadoc (gnuplot/SVG links, documented default base) (2006-05-25)
First affected version
2.0.0 [exec on the lucene-misc 2.0.0 artifact from Maven Central, whose entries carry 2007-06-17 timestamps, plus read of the releases/lucene/2.0.0 tag source, which has the same code]; 1.9.1 is not affected because SweetSpotSimilarity does not exist there (the portable repro does not compile) [exec]
Affected versions
Every release that ships SweetSpotSimilarity, 2.0.0 through 10.5.2, 10.6.0 [exec: the minimal test fails on the release jars, -da, 2026-10-31], and main a9779ef3c193; never fixed (upstream main f7bcc0abd816, branch_10x and branch_10_6 still have the guard at SweetSpotSimilarity.java:178; no open PR touches the file; read 2026-10-27). [exec] the portable main() HyperbolicTfHistoryRepro fails with identical values, its four controls passing, on 17 releases (2.0.0, 2.4.1, 2.9.4, 3.0.3, 3.6.2, 4.0.0, 4.10.4, 5.0.0, 5.5.5, 6.6.6, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2) and on main (-ea and -da), and passes on main with the fix shadowed; the finder's JUnit fails 4 of 5 on 9.12.3, 10.5.2 and main (-ea and -da); the independent reproduction's JUnit fails 4 of 5 on 9.12.3, 10.5.2 and main (-ea and -da); the intended-behaviour check's probe shows the same overflow thresholds on 9.12.3, 10.5.2 and main. [read] the hyperbolicTf body (whitespace stripped) is identical at every other checked tag (2.9.2, 3.0.1, 8.8.1) and is the same in the releases in between. All runs with JDK 25.0.3 on aarch64; pure Java arithmetic (Math.pow overflow to Infinity is specified behaviour, and the trigger inputs sit far past the overflow point)
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: 216 recorded queries, namely three scripted prior-report searches (the finder's run plus follow-up runs A and B: 48 GitHub issue/PR, 9 commit, 6 downstream-repository, 60 mailing-list queries over dev@, java-user@, issues@ and commits@, and 12 JIRA-archive queries), 44 manual GitHub issue/PR searches (apache/lucene, apache/lucene-solr, apache/solr, apache/lucenenet, Elasticsearch, OpenSearch and all of GitHub), a GitHub code search, the changed files of all 408 open apache/lucene PRs, 19 direct mailing-list searches (including the Solr lists and general@), 7 JIRA REST queries over LUCENE and SOLR, 9 web searches and the full history of the file; 16 hits read, none the same bug; hyperbolicTf and setHyperbolicTfFactors return 0 issues, PRs, commits and JIRA tickets). Closest items, not reports: the 2012 java-user@ thread 'SweetSpotSimilarity' (a usage question; the class author says xoffset is 'the center of your sweetspot' and that sweet spots for books are 'much larger'); the 2006 dev@ thread in which the class author describes the intended default curve ('~2 for all values greater then 15, and ~0 for all values less then 5'); #1655 (LUCENE-577, added the class and the guard, which was never discussed); SOLR-1365 (Solr's SweetSpotSimilarityFactory exposes the four hyperbolicTf factors without a range check); #9045 (LUCENE-7997, tf monotonicity stated and tested for core similarities only); #5997 (LUCENE-4933, a different SweetSpot tf bug). Not fixed upstream: SweetSpotSimilarity.java last changed on main, branch_10x and branch_10_6 in 7c056ab88c75 (#13757, 2024-09-13, a computeNorm refactor), line 178 is unchanged at upstream main f7bcc0abd816, and no open PR touches SweetSpotSimilarity.java or its test. Gaps: shared GitHub search throttling (6 manual queries and one paging request rate-limited, all re-run successfully), keyword-based list search, private reports not visible
Lucene commit tested
main 645d4fe14a20 (2026-10-28): the minimal test fails with assertions on and off through gradle in a scratch worktree (2026-10-31) and through a standalone runner (2026-10-29); with the recommended fix it passes, together with TestSweetSpotSimilarity (including its nightly test) and the whole misc module. Earlier runs: main a9779ef3c193 and 17 releases from 2.0.0 to 10.5.2 (portable repro), 9.12.3 and 10.5.2 (JUnit); on the 10.6.0 release the minimal test fails too (the repro runner, assertions off, 2026-10-31). SweetSpotSimilarity.java is identical in 10.6.0, branch_10_6 7e5aec4370e, branch_11x 603af870375 and upstream main 2f424fd428d (2026-10-30) [read].

Lucene and upstream sources

Finding record
  • lucene/misc/src/java/org/apache/lucene/misc/SweetSpotSimilarity.java:28-29 (at a9779ef3c193) (class javadoc: baselineTf and hyperbolicTf 'are provided, which subclasses can choose between'), :68-83 (setHyperbolicTfFactors: min/max 'the minimum/maximum tf value to ever be returned', base default 1.3, xoffset 'the midpoint of the hyperbolic function' default 10.0, no validation), :122-130 (tf delegates to baselineTf), :149-159 (hyperbolicTf javadoc: 'allows for a hard max', the exact formula, 'provided as a convenience for subclasses'), :160-178 (x = freq - xoffset; Math.pow ratio; :178 'return Float.isNaN(result) ? max : result')
  • lucene/core/src/java/org/apache/lucene/search/similarities/Similarity.java:207-209 (at a9779ef3c193) (SimScorer#score: 'Score must not decrease when freq increases')
  • lucene/core/src/java/org/apache/lucene/search/similarities/TFIDFSimilarity.java:343-349 (at a9779ef3c193) (tf: implementations 'usually return larger values when freq is large')
  • lucene/misc/src/test/org/apache/lucene/misc/TestSweetSpotSimilarity.java:205-225 (at a9779ef3c193) (testHyperbolicSweetSpot: factors 3.3, 7.7, e, 5 and freq 1..1000, which reach only the overflow above the midpoint)
The finding in 30 seconds
  • lucene/misc/src/java/org/apache/lucene/misc/SweetSpotSimilarity.java:150-152 (at 645d4fe14a20) (javadoc formula), :160-179 (hyperbolicTf; :167 x, :174-175 Math.pow ratio, :178 the NaN guard), :122-130 (tf delegates to baselineTf)
  • lucene/core/src/java/org/apache/lucene/search/similarities/TFIDFSimilarity.java:469 (at 645d4fe14a20) (tf(freq) * queryWeight at search time)
Who is affected
  • lucene/misc/src/java/org/apache/lucene/misc/SweetSpotSimilarity.java:28-29 (at 645d4fe14a20) (class javadoc), :122-130 (stock tf)
  • lucene/misc/src/test/org/apache/lucene/misc/TestSweetSpotSimilarity.java:205-226 (at 645d4fe14a20) (testHyperbolicSweetSpot subclass and factors)
  • lucene/core/src/java/org/apache/lucene/search/similarities/TFIDFSimilarity.java:469 (at 645d4fe14a20)
Why it looks unintended
  • lucene/misc/src/java/org/apache/lucene/misc/SweetSpotSimilarity.java:71-72, (at 645d4fe14a20) :75, :150-152, :154-155, :178
  • lucene/core/src/java/org/apache/lucene/search/similarities/Similarity.java:207-209 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/search/similarities/TFIDFSimilarity.java:342-344, (at 645d4fe14a20) :469-471
  • lucene/misc/src/test/org/apache/lucene/misc/TestSweetSpotSimilarity.java:205-226 (at 645d4fe14a20)
  • lucene/misc/src/java/org/apache/lucene/misc/doc-files/ss.gnuplot:47 (at 645d4fe14a20) (htf formula), :49-52 (the four plotted curves)
History
  • a Lucene clone refs origin/main 2f424fd428d, origin/branch_11x 603af870375, origin/branch_10_6 7e5aec4370e, tag releases/lucene/10.6.0 (git diff of SweetSpotSimilarity.java against 645d4fe14a20: empty)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Made in parallel with this doc from the code at 645d4fe14a20 and a probe run of hyperbolicTf over the frequencies, stock and with the fix.

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.