- tf. In
ClassicSimilarity(TF-IDF), a document's score for a term is multiplied bytf(freq), a function of how often the term occurs in the document, at search time.SweetSpotSimilarityextends it. - hyperbolicTf is an optional S-shaped tf curve: about
minwell below the midpointxoffset, halfway at it, aboutmaxwell above. The stocktf()does not use it; a subclass opts in by returninghyperbolicTf(freq)fromtf(). - Overflow. A double holds at most about 1.8e308, so for
base > 1base^tbecomes Infinity oncetpasses aboutln(1.8e308) / ln(base): about 308 at base 10, about 2705 at the default base 1.3.
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.- You subclass
SweetSpotSimilarityso thattf(freq)returnshyperbolicTf(freq), as its javadoc suggests, and callsetHyperbolicTfFactors(0, 2, 10, 400): min 0, max 2, base 10, midpoint 400. - 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.
- 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.
- Why: far below the midpoint
Math.pow(10, 400 - freq)overflows to Infinity, the ratio -Infinity / Infinity is NaN, and the guardFloat.isNaN(result) ? max : result, right only for the overflow above the midpoint, returns max. - 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 - freqabove about 308 at base 10, or about 2705 at the default base 1.3.
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.
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- 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 istanh((x - xoffset) * ln(base)): -1 far below the midpoint, 0 at it, +1 far above it. - 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. - Far below the midpoint (freq 1: x = -399):
Math.pow(10, 399)is Infinity andMath.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. - Just inside the range (freq 92: x = -308):
10^308is 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. - Where it reaches scores. The TF-IDF scorer multiplies
tf(freq)by the query weight at search time (TFIDFSimilarity.java:469). The stockSweetSpotSimilarity.tfdelegates tobaselineTf(:122-130), so only a subclass whosetf()returnshyperbolicTfis affected; no non-test code in Lucene callshyperbolicTf.
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.
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'sSweetSpotSimilarityFactorybuilds one from thehyperbolicTfMin/Max/Base/Offsetschema settings, without a range check (read in Solr's source, not run). - When.
xoffset - freqabove 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.gnuplotand 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.
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.
/*
* 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);
}
}
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.
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.
| lucene | main 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 |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; pure Java arithmetic) |
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
# 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)- 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 whenfreqincreases"; 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);setHyperbolicTfFactorscallsxoffset"the midpoint of the hyperbolic function" (:75); the shippeddoc-files/ss.gnuplot:47plots 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 whenfreqincreases, ie. iffreq1 > freq2, thenscore(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 whenfreqis large". - The guard was only exercised on the correct side.
TestSweetSpotSimilarity.java:205-226uses 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 checks3.3 <= tf <= 7.7,tf(5)= 5.5 andtf(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.
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.
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
tanhgives +1, so the result ismin + (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 < 1side case (a decreasing curve), where the stock guard returns max far above the midpoint. The finalisNaNguard stays for NaN factors. - Cost. The extra
logandtanhrun 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
TestSweetSpotSimilaritypass with assertions on and off;TestSweetSpotSimilaritywith-Ptests.nightly=truepasses 3 of 3, including the nightlytestSweetSpotComputeNorm; - the whole
:lucene:misc:testpasses: 317 tests, 0 failures, 9 skipped; checkGoogleJavaFormat,ecjLintMain,ecjLintTestandforbiddenApisMainfor 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.
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 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.
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.
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
hyperbolicTfon the stock jar and the javadoc formula written asmin+(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.
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 (
lengthNormint overflow) and LFV-413 (lengthNormplateau and top-k pruning). Different methods. - Limits: keyword-based list search; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive, lucene-solr, solr, lucenenet, Elasticsearch, all of GitHub) | 2026-10-27 | three 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 PRs | hyperbolicTf and setHyperbolicTfFactors: 0 issues, PRs and commits; no open PR touches the file |
| Mailing lists (dev@, java-user@, issues@, commits@, general@, Solr lists), JIRA, web | 2026-10-27 | 60 tool list queries, 19 direct list searches, 12 + 7 JIRA queries, 9 web searches; 16 hits read | only 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-31 | read-only searches for hyperbolicTf, setHyperbolicTfFactors and SweetSpotSimilarity, and open PRs mentioning SweetSpotSimilarity | 0, 0 and 7 old closed items (last updated 2022), no open PR |
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).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails with assertions on and off (gradle and standalone); passes with the fix |
main a9779ef3c193 | affected | portable repro and the JUnit classes fail (assertions on and off); pass with the fix |
| 10.5.2, 9.12.3 | affected | the 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 | affected | portable repro fails with identical values; its four controls pass |
| 10.6.0 | affected | minimal 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 code | the file is identical to 645d4fe14a20 (read, not run) |
| 1.9.1 | no class | SweetSpotSimilarity does not exist; the repro does not compile |
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.
| Check | Verdict | What it found |
|---|---|---|
| Finder (bounded-exhaustive formula check, JUnit) | candidate | 720,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 | real | Own 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) | real | Stock fails; fix A passes the repro, TestSweetSpotSimilarity with nightly and the whole misc module; fix B passes the repro and TestSweetSpotSimilarity. |
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 354 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,465 words
- 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 withTestSweetSpotSimilarity(including its nightly test) and the whole misc module. Earlier runs: maina9779ef3c193and 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.javais identical in 10.6.0,branch_10_67e5aec4370e,branch_11x603af870375and upstream main2f424fd428d(2026-10-30) [read].
- 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, :178lucene/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-471lucene/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)
- TestSweetSpotHyperbolicTfFarBelowMidpoint.java (one test, public API):
repro/LFV-750/src/org/apache/lucene/misc/TestSweetSpotHyperbolicTfFarBelowMidpoint.java(29 lines)
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.
assets/LFV-750/bug.png(354 KB)
