A description "computed as <formula> from:" says the node's value is that formula of its children. The Axiomatic F1/F3 tf node states 1 + log(1 + log(freq)) over its freq child, but its value comes from tf(), which adds 1 to freq first. The printed formula is one step behind the value.
IndexSearcher.explain(query, doc)returns a tree that shows how a hit's score was computed: every node has a value, a description and child nodes.- A description ending in
computed as <formula> from:promises that the node's value is that formula applied to its children;Explanation.matchdocuments the description as "how value was computed". - The Axiomatic similarities score a term as tf x ln x tfln x idf (minus gamma; the rule is in
Axiomatic.score, the tree only says "computed from:"), and the tree lists each component, with its formula and inputs, under the score node.
tf() actually computes. Select the still to open it at full size. Generated with AI, checked against the code.- Index three documents with
AxiomaticF1EXP: fieldf= "a", "a a", "a a a". Searchf:aand callIndexSearcher.explainon doc 0, where the term occurs once. - The tf node reads
tf, term frequency computed as 1 + log(1 + log(freq)) from:with one child,1.0 = freq, number of occurrences of term in the document. You expect 1 + log(1 + log(1)) = 1.0. - Instead the node shows 1.526589, which is 1 + log(1 + log(2)):
tf()runsfreq += 1before the formula. At freq 2 and 3 it shows 1.7412763 and 1.8697417, the printed formula's values for 3 and 4. - Recomputing the score from the printed parts by
Axiomatic.score's rule gives tf 1.0 x ln 1.0 x tfln 1.0 x idf 1.1059324 = 1.106, but the tree and the hit say 1.6883042 (= 1.526589 x 1.1059324). The score, ranking and node value are unaffected; only the text is stale. Same in F1LOG, F3EXP and F3LOG.
Anyone who calls explain on searches that use AxiomaticF1EXP, F1LOG, F3EXP or F3LOG, which are non-default, @lucene.experimental similarities. Every explained hit shows the stale formula, whatever the term frequency. A reader who recomputes the node or the score from the printed text cannot make the numbers add up. Scores and ranking are unaffected.
Mechanism, terms and numbers506 words
AxiomaticF1EXP, AxiomaticF1LOG, AxiomaticF3EXP and AxiomaticF3LOG compute the term-frequency component as freq += 1; return 1 + Math.log(1 + Math.log(freq));, that is 1 + log(1 + log(freq + 1)) of the freq they are given (for a term query, the raw count). Their tfExplain shows that value but describes it as "tf, term frequency computed as 1 + log(1 + log(freq)) from:" over one child holding the unshifted freq, so the printed formula on the printed child does not give the printed value: 1.526589 instead of 1.0 at freq 1, 1.8697417 instead of 1.7412763 at freq 3. The defect changes no score, ranking or match and no value in the tree; only the formula text is stale. LUCENE-8011 wrote the text when tf() had no + 1; LUCENE-8010 added the + 1 four weeks later and left the text, so every release from 8.0.0 to 10.6.0 and main shows it. The fix is one string per class.
- The value.
AxiomaticF1EXP.tf(AxiomaticF1EXP.java:59-62):freq += 1; // otherwise gives negative scores for freqs < 1(:60), thenreturn 1 + Math.log(1 + Math.log(freq));. Natural log. The same inAxiomaticF1LOG.java:50-53,AxiomaticF3EXP.java:59-62andAxiomaticF3LOG.java:48-51. - The explanation.
AxiomaticF1EXP.tfExplain(:88-94) returnsExplanation.match((float) tf(stats, freq, docLen), "tf, term frequency computed as 1 + log(1 + log(freq)) from:", Explanation.match((float) freq, "freq, number of occurrences of term in the document")): the value carries the+ 1, the text (:92) and the child (:93, the unshifted freq) do not. The string is atAxiomaticF1LOG.java:83,AxiomaticF3EXP.java:92andAxiomaticF3LOG.java:81. - Where it shows.
Axiomatic.explainadds the node under "score(<class>, freq=...), computed from:" (Axiomatic.java:162,:129-137); the score node's value uses the sametf()(:125-127), so the top value equals the hit score. - Why no test noticed.
CheckHits.verifyExplanationstops at a node ending in "computed from:", which is the Axiomatic score node (CheckHits.java:411-413), and treats any "., computed as . from:" node as consistent with its children (:375,:437-438). No upstream test contains the string.
| raw freq (child) | tf node shows | printed formula on the child | 1 + log(1 + log(freq + 1)) |
|---|---|---|---|
| 1 | 1.526589 | 1.0000000 | 1.5265890 |
| 2 | 1.7412763 | 1.5265890 | 1.7412763 |
| 3 | 1.8697417 | 1.7412763 | 1.8697417 |
The ln and idf nodes of the same tree, and the F2 classes' tf node ("tf, term frequency, equals to 1"), match their own formulas and children.
The runs cover freq 1, 2, 3 and 7. By the math every legal freq is affected (Similarity.java:218: a "sloppy term frequency, must be finite and positive"): above 1/e both expressions are real and differ; at or below 1/e, which a sloppy PhraseQuery can reach (PhraseWeight.java:82-86, SloppyPhraseMatcher.java:200-202), the printed formula is not a real number while the value stays finite. That is the NaN rmuir found in LUCENE-7997 (#9045), which the + 1 avoids.
Exposure and likelihood145 words
- Who is hit: callers of
IndexSearcher.explain(or anything that prints an explanation) on a search with one of the four similarities, with defaultIndexWriterConfig, codec and searcher, assertions on or off. - Opt-in: non-default similarities; every class involved is
@lucene.experimental(AxiomaticF1EXP.java:25,AxiomaticF1LOG.java:25,AxiomaticF3EXP.java:27,AxiomaticF3LOG.java:27,Axiomatic.java:34). - What goes wrong: misleading diagnostic text. Checking the tf node against its child, or combining the printed components by
Axiomatic.score's rule (1.106 instead of 1.688 in the example), points at a scoring bug that does not exist. The hunt's evaluator, which recomputes explanation formulas from their children, flags it. - Scale: four string literals; the searches found no prior report.
- Not claimed: any effect on scores, ranking, matching, the tf node's value or the top explanation value; in every run the tf value is
tf()'s and the top value equals the hit score.
One JUnit test on the public API: one document with the term once, AxiomaticF1EXP, one IndexSearcher.explain and one assertion that the tf description states the formula of its value. Deterministic, single thread, under a second.
/*
* 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.search.similarities;
import java.util.Arrays;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestAxiomaticF1EXPTfExplain extends LuceneTestCase {
public void testTfDescriptionStatesTheFormulaOfItsValue() throws Exception {
Similarity sim = new AxiomaticF1EXP();
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig().setSimilarity(sim))) {
Document doc = new Document();
doc.add(new TextField("f", "a", Field.Store.NO));
w.addDocument(doc);
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
IndexSearcher searcher = new IndexSearcher(reader);
searcher.setSimilarity(sim);
Explanation score = searcher.explain(new TermQuery(new Term("f", "a")), 0).getDetails()[0];
Explanation tf =
Arrays.stream(score.getDetails())
.filter(e -> e.getDescription().startsWith("tf,"))
.findFirst()
.orElseThrow();
// freq = 1: expected the formula of the value 1.5266 = 1 + log(1 + log(freq + 1)); actual: 1 + log(1 + log(freq)), which is 1.0
assertEquals(
"tf value " + tf.getValue(),
"tf, term frequency computed as 1 + log(1 + log(freq + 1)) from:",
tf.getDescription());
}
}
}
}
cd $LUCENE # an apache/lucene checkout at 645d4fe14a20
cp $REPRO/TestAxiomaticF1EXPTfExplain.java lucene/core/src/test/org/apache/lucene/search/similarities/
./gradlew :lucene:core:test --tests org.apache.lucene.search.similarities.TestAxiomaticF1EXPTfExplain -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same failure); the outcome does not depend on the seed
- Expected
- The tf description states the formula of its value:
1 + log(1 + log(freq + 1)), which is the shown 1.526589 at freq 1. - Observed
ComparisonFailure: tf value 1.526589 expected:<...1 + log(1 + log(freq[ + 1])) from:> but was:<...1 + log(1 + log(freq[])) from:>: the text says1 + log(1 + log(freq)), which gives 1.0; assertions on and off.
More files, environment, full output225 words
The smallest input: one document, freq 1, where the printed formula gives 1.0 and the node shows 1.526589. The assertion pins the description to the formula tf() computes, so the test fails on stock and passes with the fix; its message carries the node's value. It covers AxiomaticF1EXP only; F1LOG, F3EXP and F3LOG carry the same string and are fixed by the same diff (the probe shows all four). It asserts nothing about scores, which this defect does not change. As a regression test it is the same test (it could sit in TestAxiomaticF1EXP). All runs were on Linux aarch64 with OpenJDK 25.0.3; the bug is a string literal, so the platform does not matter.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); the same failure on 10.6.0 |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux aarch64 |
# gradle, stock 645d4fe14a20, seed DEADBEEF, assertions on
TestAxiomaticF1EXPTfExplain > testTfDescriptionStatesTheFormulaOfItsValue FAILED
org.junit.ComparisonFailure: tf value 1.526589 expected:<...1 + log(1 + log(freq[ + 1])) from:> but was:<...1 + log(1 + log(freq[])) from:>
# assertions off (-Ptests.asserts=false): the same message
# 10.6.0 release jars, assertions on and off: the same message
# with the fix (four description strings), assertions on and off
classes=1 tests=1 failures=0 errors=0 skipped=0
# closest suites (lucene/core similarities package, TestSimpleExplanations*, TestComplexExplanations*), seed DEADBEEF
stock: classes=32 tests=353 failures=1 (the repro) errors=0 skipped=2
with fix: classes=32 tests=353 failures=0 errors=0 skipped=2Explanation.matchdocuments the description as "how value was computed", andAxiomatic.tfExplainpromises "how the tf component was computed".- The node's only child is labelled "number of occurrences of term in the document" and holds the unshifted freq.
- The text was exact when LUCENE-8011 wrote it; LUCENE-8010 added the
+ 1four weeks later without discussing the tf explanation.
The full argument187 words
- The contract.
Explanation.java:31,:43: "@param description how {@code value} was computed".Axiomatic.java:195: "@return Explanation of how the tf component was computed".SimilarityBase.java:106: subclasses add clauses "to explain details of their scoring formulae". - The operand. The child "freq, number of occurrences of term in the document" holds the unshifted freq (
AxiomaticF1EXP.java:93;Axiomatic.java:193documentstfExplain's freq with the same words), so the text cannot mean a shifted freq. - The history. In the LUCENE-8011 (#9059) thread jpountz asked that the similarities "include the formula that was used to compute the score"; the strings it added matched
tf()then. In LUCENE-8010 (#9058) jpountz asked "Axiomatic similarities add 1 to the freq, is it ok?" and rmuir answered with relevance numbers: the score change was deliberate. No #9058 comment mentions the explanation, and its issue body even lists "internal explanations calculations are sane" as a test requirement. - The neighbours. The
lnandidfnodes and the F2 tf node match their operands. - Precedent. GITHUB#13691 "Fix incorrect exponent value in explain of SigmoidFunction" (
0f5359a2ac6), an explain-only fix, is listed under Bug Fixes for 9.12.0.
The devil's-advocate check152 words
The strongest case for "intended" (the devil's-advocate checker): Explanation is an expert diagnostic API with human-readable descriptions, not an expression language; the classes are @lucene.experimental; 1 + log(1 + log(freq)) names the published F1/F3 tf function, so "freq" could mean the shifted local variable; CheckHits deliberately treats "computed as ... from:" nodes as opaque; no CHANGES or MIGRATE entry promises exact explain formulas.
Why it fails: the javadoc says the description is how the value was computed; the child is labelled and filled as the unshifted freq, which rules out the shifted reading; the text was exact until LUCENE-8010 changed the code without a word about the tf explanation; CheckHits' opacity explains why no test noticed, not why the text would be right; and an explain-only formula error was fixed as a bug before (GITHUB#13691). Every reviewer rated the intended case weak; it explains the low priority, not the behaviour.
Recommended State the formula the code computes (four strings)
Change the four description strings to tf, term frequency computed as 1 + log(1 + log(freq + 1)) from: and keep the child, the unshifted freq. Text only: no value, score or tree shape changes. Validated through gradle: the repro passes with assertions on and off, and all 353 tests of the closest suites pass.
- "tf, term frequency computed as 1 + log(1 + log(freq)) from:",
+ "tf, term frequency computed as 1 + log(1 + log(freq + 1)) from:",All options and trade-offs3 options ยท 341 words
The hunt's fix, unchanged, run through gradle in a scratch worktree of 645d4fe14a20 (OpenJDK 25.0.3, seed DEADBEEF) with the minimal repro and the closest suites.
Change the description in AxiomaticF1EXP.java:92, AxiomaticF1LOG.java:83, AxiomaticF3EXP.java:92 and AxiomaticF3LOG.java:81 to tf, term frequency computed as 1 + log(1 + log(freq + 1)) from:; the child stays the unshifted freq.
- "tf, term frequency computed as 1 + log(1 + log(freq)) from:",
+ "tf, term frequency computed as 1 + log(1 + log(freq + 1)) from:",Trade-offs
Text only: no value, score or tree shape changes. A downstream tool matching the exact old string would see the new one (none is known). Optional, not run: the child's label "number of occurrences of term in the document" (also Axiomatic.java:193) is exact for term queries only; "term frequency" would also fit sloppy phrase frequencies.
What the checks validated
Through gradle in a scratch worktree of 645d4fe14a20 (OpenJDK 25.0.3, seed DEADBEEF): the minimal test fails on stock and passes with the fix, assertions on and off. The closest suites (every test class in org.apache.lucene.search.similarities plus TestSimpleExplanations* and TestComplexExplanations*: 32 classes, 353 tests, the repro included) give 1 failure, the repro, on stock and 0 with the fix (2 skipped both times). Earlier, the hunt's shadow fix check and the refute reviewer's runs (15 Axiomatic tests, 118 of 118 similarity tests) passed too.
Make the child Explanation.match((float) (freq + 1), ...) with a relabelled description. Not run.
Trade-offs
The unshifted freq, which the score node's "freq=..." also shows, disappears from the tf subtree and the child's label must change: a larger, less obvious edit for the same result.
Keep the F1/F3 tf formula text in one place (for example a constant in Axiomatic) so the four copies cannot drift again. Not run.
Trade-offs
Touches four public experimental classes for a four-line fix; optional on top of the first option.
Apache Solr has no factory for these four similarities (SOLR-9953, open since 2017), so the stale text reaches mainly applications that use Lucene directly.
Semi-formal: an AI tool's sweep explained 3,178 hits over 79 similarity configurations and recomputed each of 14,554 "computed as" nodes from its own children. 200 tf nodes of the Axiomatic F1/F3 classes disagreed. Ten of ten injected mutants were caught, so the check is not vacuous; a public-API JUnit test then confirmed it.
The method in full124 words
Method: semi-formal, a differential explain-tree evaluation (a hunt unit, explanation formula strings checked against their own operand values), on the stock main a9779ef3c193 jars.
- A test program explained hits for 79 similarity configurations x
TermQuery,PhraseQuery,SynonymQueryand a 2-clauseBooleanQueryx 2 boosts, plus the scoring queries: 3,178 explained hits, 14,554 "computed as <formula> from:" nodes, each evaluated on its own labelled children, with assertions on and off. - 200 tf nodes over
AxiomaticF1EXP,F1LOG,F3EXPandF3LOGdisagreed with their stated formula;F2EXP/F2LOGand every other Axiomatic node agreed. - The oracle is not vacuous: 10 of 10 injected non-equivalent mutants were detected.
- The signal was reduced to a public-API JUnit test, then to the one-test repro.
No prior report found in GitHub issues, PRs and commits, open PRs' file lists, JIRA, the mailing lists, downstream projects and the web (2026-10-28), re-checked on GitHub on 2026-11-01. LUCENE-8010 (#9058) and LUCENE-8011 (#9059) are the origin, not reports.
Searches3 searches
Context only: #9058 (LUCENE-8010, the deliberate score change), #9059 (LUCENE-8011, the strings; during its review the LUCENE-7996 commit a8a63464e7d let a single-child "computed as ... from:" node, such as this tf node, differ from its child in CheckHits; multi-child ones were accepted unchecked since LUCENE-7524), #9045 (LUCENE-7997, why the shift), #9071 (LUCENE-8023, open, "Remove divide-by-zero formula hacks across sim impls", which would make "good explain()" possible; about docFreq guards in other similarities), #16267 (open, explanation-testing gaps).
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits; Elasticsearch, OpenSearch, Solr; dev@, java-user@, issues@, commits@; JIRA; the web | 2026-10-28 | about 168 queries of the hunt's search tool in four specs plus the finder's 45; manual GitHub searches including the formula text with and without + 1; the 9 LUCENE/SOLR JIRA issues and 85 list mails matching "axiomatic"; 18 web queries | no report; context #9058, #9059, #9045, #9071, #16267, SOLR-9953 |
| Open PRs (apache/lucene) | 2026-10-28; re-checked 2026-11-01 | file lists of all 413 open PRs (2026-10-28); file lists of the 36 open PRs updated since 2026-10-28 (2026-11-01) | no PR touches an Axiomatic file; #16949 touches only BM25Similarity and TFIDFSimilarity javadoc |
| GitHub issue and PR search (apache/lucene) | 2026-11-01 | Axiomatic (updated since 2026-10-28), tfExplain, "log(1 + log(freq))", AxiomaticF1EXP, AxiomaticF3LOG, explain similarity, explanation formula, "freq + 1" explain; the 33 items created since 2026-10-28 | no report |
LUCENE-8011 (6771886cf33, 2017) wrote the string when tf() had no + 1; LUCENE-8010 (b2f248164c1, 2017-12-28) added freq += 1 and left the text. Affected: 8.0.0 to 10.6.0 and main. Clean: 7.7.3 and earlier, whose tf node states no formula. Not fixed upstream.
Versions tested6 rows
String written by 6771886cf33, LUCENE-8011 "Improve similarity explanations" (#9059; apache/lucene-solr#280 by mayya-sharipova, merged by jpountz; author date 2017-11-30). Then tf() was if (freq <= 0.0) return 0.0; return 1 + Math.log(1 + Math.log(freq));, so the text was right.
Introduced by b2f248164c1, LUCENE-8010 "Fix similarities so that they pass tests." (#9058; jpountz; author date 2017-12-28; a JIRA patch whose hunks for the four subclasses were made against the files before LUCENE-8011, so they show no tfExplain or formula string). It replaced the guard with freq += 1 in all four classes. git log -S finds exactly one commit for each line. Commit isolation (the 2017 subclass sources compiled against the 8.0.0 jars): the classes from b2f248164c1^ are consistent, those from b2f248164c1 are not.
All 51 release tags from 8.0.0 to 10.5.2 in the history clone contain both lines, none of the 14 7.x tags contains either; the 10.6.0 tag and upstream main 2f424fd428da (2026-10-30) still have both. The clone has no tags for 8.8.2 to 8.11.4; those releases were run instead (8.11.4 by the verifiers, the other eight after the fact checks). The Axiomatic classes exist since 6.4.0.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the minimal test fails through gradle, assertions on and off; the probe shows all four classes |
| 10.6.0 | affected | the minimal test fails on the release jars, assertions on and off |
| 10.5.2, 10.0.0, 9.12.3, 9.0.0, 8.11.4, 8.0.0 | affected | the history probe and the verifiers' tests fail (8.0.0, 9.12.3 and 10.5.2 with assertions on and off) |
| 8.8.2, 8.9.0, 8.10.0, 8.10.1, 8.11.0, 8.11.1, 8.11.2, 8.11.3 | affected | the probe on the release jars prints the stale text: 12 of 12 tf nodes (four classes, freq 1-3) mismatch; top value = hit score (assertions on) |
| 7.7.3, 7.2.0, 6.6.6 | not affected | plain "tf" node with no formula (7.7.3 with assertions on and off) |
upstream main 2f424fd428da (2026-10-30) | read | both lines present in all four classes (read, not run) |
Confirmed with high confidence by the study's judge after an independent reproducer, an intended-behaviour devil's advocate and reviews by another model family (OpenAI GPT-5.6), none dissenting; confirmed by a credibility audit; re-run through gradle and on the 10.6.0 jars for this doc, with the fix validated.
Checks and verdicts307 words
Each checker that executed wrote its own test from the claim; the adjudication re-ran the independent reproducer's test with a new seed.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | 200 F1/F3 tf nodes of 14,554 formula nodes disagree; 10 of 10 mutants detected; JUnit fails on main (assertions on and off), 10.5.2 and 9.12.3 and passes with the fix. |
| refute review (OpenAI GPT-5.6) | real, high | Own JUnit fails with four seeds, assertions on and off; F2 control passes; the fix passes its test, 15 Axiomatic tests and 118 of 118 similarity tests. |
| Independent reproducer | real | Own JUnit at freq 1, 2, 3 and 7: 16 of 16 tf nodes mismatch on main, 10.5.2, 9.12.3, 8.11.4 and 8.0.0, every value equal to the freq + 1 formula; controls pass. |
| Works-as-designed review (OpenAI GPT-5.6) | real, high | Own JUnit fails on main and 10.5.2 (assertions on); intended argument weak. |
| Intended-behaviour devil's advocate | real | Strongest "by design" case rejected point by point; own JUnit fails on main (assertions on and off), 10.5.2 and 9.12.3. |
| adjudication | confirmed, high | Re-ran the independent reproduction with a new seed, assertions on and off: the same 16 mismatches. Impact minor. |
| credibility audit | confirmed | Finder's JUnit re-run on main with a new seed fails as expected; minor kept. |
| this doc | real, fix validated | The minimal test fails through gradle (assertions on and off) and on the 10.6.0 jars; the fix passes it and the 353 tests of the closest suites; a probe prints the trees of all four classes; a GitHub prior-art refresh (2026-11-01) found nothing new. |
| fact checks (independent checker, OpenAI GPT-5.6 reviewer, diagram checker) | real; wording fixed | Code, numbers, history, test and fix confirmed, the test and probe re-run; quote, citation and wording errors corrected and the 8.8.2-8.11.3 releases run. |
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 411 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,416 words
- Registry title
- AxiomaticF1EXP, F1LOG, F3EXP and F3LOG explain tf as 'computed as 1 + log(1 + log(freq))' of the raw freq, but the value they show is 1 + log(1 + log(freq + 1)); the score itself is correct
- Area
- core / search similarities (Axiomatic F1/F3 term-frequency explanation, tfExplain)
- Priority
- Low, rank 1043: The experimental Axiomatic F1/F3 similarities print a stale tf formula in explain while all values are correct, which is cosmetic.
- Impact class
- minor
- Impact note
- Anyone who calls IndexSearcher.explain on a search that uses one of the non-default, @lucene.experimental similarities AxiomaticF1EXP, AxiomaticF1LOG, AxiomaticF3EXP or AxiomaticF3LOG (public constructors, default IndexWriterConfig, codec and searcher, -ea and -da, one document) sees the stale formula on every explained hit, for every term frequency; scores, ranking, matching, the tf node's value and the top explanation value are all correct, and nothing throws; every release from 8.0.0 to 10.6.0; Solr has no factory for these similarities (SOLR-9953, open since 2017) and, by the finder's reading, Elasticsearch's similarity settings do not expose them, so reach is mostly direct Lucene users.
- Summary
- You search with AxiomaticF1EXP (or F1LOG, F3EXP, F3LOG) and call IndexSearcher.explain on a hit. The tf node says 'tf, term frequency computed as 1 + log(1 + log(freq)) from:' and its only child is 'freq, number of occurrences of term in the document', the raw count, so you expect its value to be that formula of that child. Instead tf() first adds 1 to freq (since LUCENE-8010, 2017), so for freq = 1 the node shows 1.5265890 where the stated formula gives 1.0, and for freq = 3 it shows 1.8697417 instead of 1.7412763; the score is right and only the formula text is stale.
- Introduced
- b2f248164c1a3ddf213a56778d55c9252a022f18 ('LUCENE-8010: Fix similarities so that they pass tests', by jpountz; JIRA patch era, no pull request; LUCENE-8010 is apache/lucene#9058): added 'freq += 1; // otherwise gives negative scores for freqs < 1' to tf() in all four classes and left the four tfExplain description strings unchanged. Those strings were written one month earlier by 6771886cf3303a9a2c3cb86c8d0329e55ee0feaa ('LUCENE-8011: Improve similarity explanations', by mayya-sharipova, committed by jpountz; apache/lucene-solr#280, apache/lucene#9059), when tf() was 'if (freq <= 0.0) return 0.0; return 1 + log(1 + log(freq))' and the text was correct; 6771886cf330 never shipped without b2f248164c1. [read: git log -S on both lines and blame in the history clone (later commits on the files change neither line); the LUCENE-8010.patch JIRA attachment was made against the file before LUCENE-8011, so its hunk carried no explain context; exec: commit isolation on 8.0.0 jars, the four subclasses from b2f248164c1^ give a self-consistent tf node and those from b2f248164c1 give the mismatch, -ea and -da] (2017-12-28 (author date of b2f248164c1; committed 2017-12-29); the stale string itself from 6771886cf330, author date 2017-11-30 (committed 2017-12-13))
- First affected version
- 8.0.0 [exec, -ea and -da] (both commits first appear in 8.0.0; no 7.x tag contains either); the last unaffected release is 7.7.3 [exec, -ea and -da], where the tf node is a plain 'tf' with no formula (also 7.2.0 and 6.6.6 [exec]); the Axiomatic classes exist since 6.4.0
- Affected versions
- Every release from 8.0.0 to 10.5.2 (the last release before 10.6.0), 10.6.0 (the branch_10_6 head 4ae946e5a9b9, the RC1 revision) and main a9779ef3c193; not fixed upstream. [exec] the follow-up's version-portable probe AxiomaticTfExplainHistoryProbe fails on 8.0.0 (-ea and -da), 8.11.4, 9.0.0, 9.12.3 (-ea and -da), 10.0.0, 10.5.2 (-ea and -da), branch_10_6 and main (-ea and -da) and passes (no formula node) on 6.6.6, 7.2.0 and 7.7.3 (-ea and -da); the finder's JUnit fails on main, 10.5.2 and 9.12.3 (each -ea and -da); the independent reproduction's JUnit fails on main (-ea and -da), 10.5.2, 9.12.3, 8.11.4 and 8.0.0; the intended-behaviour check's on main (-ea and -da), 10.5.2 and 9.12.3. [read] all 51 release tags 8.0.0-10.5.2 in the history clone contain both 'freq += 1' and the stale string in all four classes and none of the 14 7.x tags contains either (8.8.2-8.11.x are not tagged there; 8.11.4 executed); upstream main c5ab3e57b9b (2026-10-28) still has both lines (read-only GitHub contents API, re-checked 2026-10-28). 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 agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal
- Upstream and prior reports
- none found (verdict new, searched 2026-10-28, read-only: a scripted prior-report search in four specs (about 168 queries over apache/lucene issues and PRs, commits, Elasticsearch/OpenSearch/Solr, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web) plus the finder's earlier 45-query run; manual GitHub searches (issue/PR, commit, downstream and all-of-GitHub, including the formula text with and without '+ 1'); the changed-file lists of all 413 open apache/lucene PRs; the 86 items updated since 2026-10-25 listed and the 31 created since 2026-10-24 read by title; the 9 LUCENE/SOLR JIRA issues mentioning 'axiomatic', read; every subject of the 85 list mails matching 'axiomatic'; 18 web queries); 15 hits read, none the same bug. Refresh 2026-10-28: the changed files of the 49 open PRs updated since 2026-10-26 (only #16916 touches a similarities file, the IBSimilarity class javadoc), the 15 newest issues/PRs up to #16931, and GitHub searches 'Axiomatic' (updated since 2026-10-26), 'tfExplain', 'Axiomatic explain', 'AxiomaticF1EXP', the formula text, 'axiomatic tf freq', PRs 'similarities' and items 'explain' updated in the last 48 h: nothing new; upstream main c5ab3e57b9b still has both lines and the four files were last touched in 2021-2023. Related, not this bug: LUCENE-8010 (#9058: 'Axiomatic similarities add 1 to the freq, is it ok?', the deliberate score change that made the text stale; no #9058 comment mentions the explanation), LUCENE-8011 (#9059, added the formula strings; during its review the LUCENE-7996 commit a8a63464e7d let a single-child 'computed as ... from:' node differ from its child in CheckHits, and multi-child ones were accepted unchecked since LUCENE-7524), LUCENE-7997 (#9045, motivation for the shift), LUCENE-8023 (#9071, open: 'Remove divide-by-zero formula hacks across sim impls', which would make 'good explain()' possible; about docFreq guards, not Axiomatic), #16267 (open, explanation test gaps, does not list similarity formula strings), #16921 (BM25/TFIDF idfExplain javadoc type name), #16909/#16916 (IBSimilarity javadoc).
- Lucene commit tested
- main
645d4fe14a20through gradle in a scratch worktree: the minimal test stock and with the fix, assertions on and off, and the closest suites stock and with the fix; the minimal test on the 10.6.0 release jars (assertions on and off); a probe that prints the explanation trees of all four classes on645d4fe14a20; the verifiers' tests on maina9779ef3c193, 10.5.2, 9.12.3, 8.11.4 and 8.0.0; the history probe from 6.6.6 to main and the commit isolation on the 8.0.0 jars; after the fact checks, the probe on the 8.8.2-8.11.3 release jars. All runs on Linux aarch64 with OpenJDK 25.0.3. Upstream main2f424fd428da(2026-10-30) and the 10.6.0 tag have the same code.
- Finding record
lucene/core/src/java/org/apache/lucene/search/similarities/AxiomaticF1EXP.java:25 (@lucene.experimental), :59-62 (tf: 'freq += 1' at :60), :89-94 (tfExplain; string at :92) @ a9779ef3c193lucene/core/src/java/org/apache/lucene/search/similarities/AxiomaticF1LOG.java:25, :50-53 ('freq += 1' at :51), :80-85 (string at :83); AxiomaticF3EXP.java:27, :59-62 (:60), :89-94 (:92); AxiomaticF3LOG.java:27, :48-51 (:49), :78-83 (:81) @ a9779ef3c193lucene/core/src/java/org/apache/lucene/search/similarities/Axiomatic.java:34 (@lucene.experimental), :136 (root node 'computed from:'), :162 (explain adds tfExplain(stats, freq, docLen)), :195-197 (tfExplain javadoc: 'Explanation of how the tf component was computed') @ a9779ef3c193lucene/core/src/java/org/apache/lucene/search/Explanation.java:31,43 ('@param description how {@code value} was computed') @ a9779ef3c193GitHub REST and search (read-only), 2026-10-28: repos/apache/lucene/commits?path=<each file>, commits/main, contents of AxiomaticF1EXP.java on main, pulls?state=open sorted by update with pulls/N/files, issues sorted by creation, search/issues queries listed in
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/search/similarities/AxiomaticF1EXP.java:59-62 (at 645d4fe14a20) (:60), :88-94 (:92, :93); AxiomaticF1LOG.java:50-53, :80-85 (:83); AxiomaticF3EXP.java:59-62, :89-94 (:92); AxiomaticF3LOG.java:48-51, :78-83 (:81)lucene/core/src/java/org/apache/lucene/search/similarities/Axiomatic.java:125-137, (at 645d4fe14a20) :162; lucene/core/src/java/org/apache/lucene/search/Explanation.java:31, :43lucene/test-framework/src/java/org/apache/lucene/tests/search/CheckHits.java:375, (at 645d4fe14a20) :411-413, :437-438lucene/core/src/java/org/apache/lucene/search/similarities/AxiomaticF2EXP.java:89, (at 645d4fe14a20) AxiomaticF2LOG.java:79
- Who is affected
lucene/core/src/java/org/apache/lucene/search/similarities/AxiomaticF1EXP.java:25, (at 645d4fe14a20) AxiomaticF1LOG.java:25, AxiomaticF3EXP.java:27, AxiomaticF3LOG.java:27, Axiomatic.java:34
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/search/Explanation.java:31, (at 645d4fe14a20) :43; lucene/core/src/java/org/apache/lucene/search/similarities/Axiomatic.java:195; SimilarityBase.java:106; AxiomaticF1EXP.java:93a Lucene clone: git log -1 0f5359a2ac6 (#13691); lucene/CHANGES.txt:2689, (at 645d4fe14a20) :2705 (9.12.0, Bug Fixes)
- History
a Lucene clone: git show releases/lucene/10.6.0 (four files, both lines; identical to 645d4fe14a20)
- TestAxiomaticF1EXPTfExplain.java (one test, public API):
repro/LFV-920/src/org/apache/lucene/search/similarities/TestAxiomaticF1EXPTfExplain.java(61 lines)
Made from the code at 645d4fe14a20 and the executed runs.
assets/LFV-920/bug.png(411 KB)
