All findings · Lucene finding · LFV-801 · knearestfuzzy-field-boost-syntax-unparsed

KNearestFuzzyClassifier reads a documented field boost such as title^10 as part of the field name, so that field is ignored

KNearestFuzzyClassifier documents field names with a boost such as title^10, but looks the whole string up as a field name, so that field adds nothing: alone it gives no class (null); next to other fields it is silently ignored (fruit becomes vehicle). Affects 7.0.0 through 10.6.0 (10.6.0 by reading) and main.

Summary

Priority
Low, rank 339In the experimental, little-used KNearestFuzzyClassifier, and the single-field case fails visibly (null).
Impact
silent wrong results
Since
7.0.0 (LUCENE-7838, 2017)introduced 2017-05-18 · about 9 years
Affected
7.0.0 to 10.5.2, 10.6.0 (by reading) and main
Status
documented
Found by
javadoc-versus-code check, confirmed by JUnit

AI-assisted page: this finding was first signalled by code reading or testing 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

The constructor javadoc says the text field names "can contain boosting indication e.g. title^10". The class never strips the ^10: it searches a field named title^10, which does not exist.

KNearestFuzzyClassifier built with the single text field "text^2": knnSearch passes the string unchanged to NearestFuzzyQuery, which finds no field named text^2, adds no terms, and the required empty clause matches nothing, so assignClass returns null while plain "text" gives fruit. With fields {"title^10", "body"} and the input "apple wheel", the title adds nothing and the answer is vehicle 0.971, the same as {"body"} alone, where {"title", "body"} gives fruit. The sibling KNearestNeighborClassifier parses the boost.
One boosted field: text^2 is looked up as a field of that name, so the fuzzy query gets no terms and assignClass returns null (plain text: fruit). Two fields: {"title^10", "body"} drops the title and answers vehicle (0.971), the body-only answer, where {"title", "body"} gives fruit. Select the still to open it at full size. Generated with AI, checked against the code.
  1. Index four documents in one field text: fruit "apple banana cherry" and "apple pear", vehicle "car truck bus" and "bus train". Build a KNearestFuzzyClassifier (k = 3) with the field written text^2, as the javadoc's title^10 example allows, and classify "apple banana".
  2. You expect fruit, the answer for plain text (score 0.848): with one field, a boost cannot change which documents are nearest.
  3. You get null, and getClasses returns an empty list. The classifier looks for a field literally named text^2; there is none, so its fuzzy query has no terms, and because that query is required, no document matches.
  4. With two fields it is silent. Titles point to fruit, bodies to vehicle. For "apple wheel", {"title", "body"} gives fruit (0.667), but {"title^10", "body"} gives vehicle (0.971), exactly the body-only answer. Even the no-op title^1 gives vehicle.
  5. The sibling KNearestNeighborClassifier, whose javadoc has the same sentence, parses the boost and answers fruit. Parsing field^boost here fixes both cases.

Who is affected

Applications that use the experimental KNearestFuzzyClassifier and write a boosted field name, the javadoc's own example. Then every call ignores that field, whatever the index settings: one boosted field returns null; with several, the answer comes from the other fields only, with no error. Plain field names are not affected.

Mechanism, terms and numbers540 words

The KNearestFuzzyClassifier constructor documents that its text field names "can contain boosting indication e.g. title^10" (KNearestFuzzyClassifier.java:83-84). The class never parses that syntax: knnSearch passes every entry unchanged to NearestFuzzyQuery.addTerms(text, fieldName) (:137-140), and NearestFuzzyQuery looks up a field literally named title^10, finds none and adds no terms for it (NearestFuzzyQuery.java:133-136). The fuzzy query is a required clause (:141). With one boosted text field nothing matches, so assignClass returns null and getClasses an empty list on every call; with several fields the boosted one is silently left out and the answer comes from the others: on a six-document index, {"title", "body"} classifies "apple wheel" as fruit (0.667) and {"title^10", "body"} as vehicle (0.971), the body-only answer. The sibling KNearestNeighborClassifier carries the same javadoc sentence and parses it. The gap dates from the class's introduction (LUCENE-7838, first released in 7.0.0), so every release from 7.0.0 to 10.6.0 (10.6.0 by reading) and main is affected. Passing the parsed boost into the one NearestFuzzyQuery the class already builds fixes it; the whole lucene/classification test suite passes with that change.

Terms

  • KNearestFuzzyClassifier: a k-nearest-neighbour text classifier in the opt-in lucene-classification module (@lucene.experimental). It turns the input text into a NearestFuzzyQuery over the configured text fields, takes the top k documents that have a class, and votes.
  • NearestFuzzyQuery: a trimmed copy of the sandbox FuzzyLikeThisQuery. For every field it was given, it analyses the text, collects fuzzy variants of each term (one edit; at most 50 per term and field), keeps the best 300 terms over all fields and rewrites to a BooleanQuery of SHOULD term clauses.
  • field^boost: the syntax the constructor javadoc documents for weighting a field, as in title^10. The sibling KNearestNeighborClassifier and both document classifiers parse it.

The bug

  1. The constructor stores the caller's textFieldNames unchanged (KNearestFuzzyClassifier.java:94); nothing parses or validates it.
  2. knnSearch calls nearestFuzzyQuery.addTerms(text, fieldName) with each entry as given (:137-140).
  3. NearestFuzzyQuery asks the index for the terms of the field title^10, gets null and returns: that field adds no terms (NearestFuzzyQuery.java:133-136). An index with a field literally named title^5 is read from that field, which shows the entry is used verbatim.
  4. The fuzzy query is added as MUST (KNearestFuzzyClassifier.java:141). With one text field it has no clauses and matches nothing: assignClass returns null (:104-117), getClasses an empty list. With several fields the other fields still match, so the top k come from them alone.

The numbers (stock main, k = 3, MockAnalyzer)

indextextFieldNames, inputresult
one field{"text"}, "apple banana"fruit 0.8476
one field{"text^2"}, "apple banana"null; getClasses []
one field{"text^2"}, "bus train"null; getClasses [] (plain text: vehicle 0.7899)
two fields{"title", "body"}, "apple wheel"fruit 0.6667, vehicle 0.3092
two fields{"title^10", "body"}, "apple wheel"vehicle 0.9712 (= {"body"})
two fields{"title^1", "body"}, "apple wheel"vehicle 0.9712
two fieldssibling KNearestNeighborClassifier, {"title^10", "body"}fruit 0.6667, vehicle 0.0778

NearestFuzzyQuery.addTerms("apple banana", "text") rewrites to (text:apple)^1.5108256 (text:banana)^1.9162908; with "text^2" it rewrites to an empty query. With either fix, {"text^2"} gives fruit 0.8135 and {"title^10", "body"} fruit 0.6667 (vehicle 0.0509).

Exposure and likelihood207 words
  • Who is hit. Users of KNearestFuzzyClassifier (opt-in lucene-classification module, @lucene.experimental) who write a text field name with a boost, as the constructor javadoc shows. Nothing else is needed: the lookup of the literal name does not depend on the index settings, the codec, the similarity or the index size; the outcomes shown here were run only on the small indexes of this doc.
  • Every call, deterministic. The lookup does not depend on data or seeds: the test fails 40 of 40 over 10 iterations.
  • One boosted field is loud: null on every call, so a user would probably notice and drop the boost.
  • Several fields are silent: the answer is plausible but comes from the other fields only, the opposite of what the user asked for (the field they weighted most counts least). Even ^1 changes the answer.
  • Reach. No downstream user is known: a GitHub-wide issue and PR search for the class name found no user report, and the searches found no public report of the bug since 7.0.0 (2017).
  • Not a resource problem. The faulty path does less work than the correct one.
  • Not counted here: getClasses(text, max) on the empty list throws IndexOutOfBoundsException (KNearestFuzzyClassifier.java:132), the known LUCENE-8110 / #9158.

Minimal repro

The test shows that KNearestFuzzyClassifier reads a documented boosted field name such as text^2 as a literal field name, so the field contributes nothing.

TestKNearestFuzzyClassifierBoostedField.java (JUnit test)58 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.classification;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
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.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.BytesRef;

public class TestKNearestFuzzyClassifierBoostedField extends LuceneTestCase {

  public void testBoostedFieldName() throws Exception {
    try (Directory dir = new ByteBuffersDirectory();
        Analyzer analyzer = new StandardAnalyzer()) {
      try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig(analyzer))) {
        Document doc = new Document();
        doc.add(new TextField("text", "apple", Field.Store.NO));
        doc.add(new StringField("cls", "fruit", Field.Store.YES));
        w.addDocument(doc);
      }
      try (DirectoryReader reader = DirectoryReader.open(dir)) {
        ClassificationResult<BytesRef> plain =
            new KNearestFuzzyClassifier(reader, null, analyzer, null, 1, "cls", "text")
                .assignClass("apple");
        ClassificationResult<BytesRef> boosted =
            new KNearestFuzzyClassifier(reader, null, analyzer, null, 1, "cls", "text^2")
                .assignClass("apple");
        // expected: documented "text^2" assigns the same class as "text" (fruit); actual: null
        assertEquals(
            plain.assignedClass().utf8ToString(),
            boosted == null ? null : boosted.assignedClass().utf8ToString());
      }
    }
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-801/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble   (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/analysis/common/build/libs/lucene-analysis-common-$V.jar
CP=$CP:$LUCENE/lucene/classification/build/libs/lucene-classification-$V.jar
CP=$CP:$LUCENE/lucene/grouping/build/libs/lucene-grouping-$V.jar
CP=$CP:$LUCENE/lucene/queries/build/libs/lucene-queries-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar   # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar   # JUnit Platform console launcher (Maven Central)

javac -d classes -cp "$CP" $(find src -name '*.java')

# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
     -cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
     execute --select-class org.apache.lucene.classification.TestKNearestFuzzyClassifierBoostedField
Expected vs actual
Expected assignClass("apple") with "text^2" to give the same class as with "text" (fruit); actual: null (-ea and -da).

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

More files, environment, full output34 words

Environment

luceneLucene main e1faeba08bde (2026-11-02)
jdk25.0.3+9-LTS

Observed output

# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
    => java.lang.AssertionError: expected:<fruit> but was:<null>
       org.junit.Assert.fail(Assert.java:89)
       org.junit.Assert.failNotEquals(Assert.java:835)
       org.junit.Assert.assertEquals(Assert.java:120)
       org.junit.Assert.assertEquals(Assert.java:146)
       org.apache.lucene.classification.TestKNearestFuzzyClassifierBoostedField.testBoostedFieldName(TestKNearestFuzzyClassifierBoostedField.java:52)

Why it looks unintended

  • The constructor javadoc promises the syntax: the text field names "can contain boosting indication e.g. title^10".
  • The module writes "NO boosting supported per field" where boosts are unsupported; the three other classifiers with this sentence parse it.
  • Following the documented example gives null or another class, silently: the contract and the code disagree, so one of them must change.
The full argument183 words
  • Contract. KNearestFuzzyClassifier.java:83-84: "@param textFieldNames the name of the fields used as the inputs for the classifier, they can contain boosting indication e.g. title^10". It is the only documentation of the parameter; no test, CHANGES entry or MIGRATE note says the syntax is unsupported here, and the searches found no issue or list thread that does.
  • Module convention. BM25NBClassifier.java:76-77 and SimpleNaiveBayesClassifier.java:84-85 say "NO boosting supported per field". KNearestNeighborClassifier (:161-168), KNearestNeighborDocumentClassifier (:128-129) and SimpleNaiveBayesDocumentClassifier (:157-158) carry the boost sentence and parse it; the last one writes the stripped name back into the caller's array (:170), so only its first call is boosted (a separate bug, LFV-382).
  • Tests. The upstream tests build this class with plain field names only, so nothing caught it; the document classifiers' tests use title^100.
  • Precedent. LUCENE-7991 (7.2.0) fixed a field-boost bug in KNearestNeighborDocumentClassifier as a bug.
  • History. The sentence and its parsing came to KNearestNeighborClassifier together (LUCENE-6654, 2015). LUCENE-7838 (2017) added this class with the same javadoc block but passed the raw names to the query; it never parsed ^.
The devil's-advocate check183 words

The strongest case for "not a bug" (the intended-behaviour checker and the cross-model reviews): the whole constructor javadoc was copied from KNearestNeighborClassifier when the class was created (the neighbouring "@param k ... in the MLT results" names MoreLikeThis, which this class never used), so the boost sentence may be a copy too; the class is @lucene.experimental; and ^ is legal in a field name, so a field literally named text^2 works today.

Why it fails: the copy explains where the sentence came from, not that the behaviour is wanted; the published contract says the syntax works, and the module labels the classes without boost support. @lucene.experimental allows API changes, not a documented input that silently returns no class. The javadoc reserves ^ for boosts, as the siblings do. Whether the code or the sentence is the wrong half is for the maintainers to decide; either way the two disagree. Verdict: a defect, at the least a contract mismatch; the copy-paste origin, the experimental status and the absence of any public report since 2017 make the javadoc-only fix a reasonable maintainer choice.

Suggested fix

Recommended Pass the boost into the one shared NearestFuzzyQuery (recommended)

Parse field^boost in knnSearch and pass the boost into the one NearestFuzzyQuery the class already builds, through a new addTerms(text, field, boost) that scales that field's term scores. Unboosted configurations rewrite to the same query as today. The new test, the whole classification test suite and the module's checks pass.

// KNearestFuzzyClassifier.knnSearch
for (String fieldName : textFieldNames) {
  int caret = fieldName.indexOf('^');
  if (caret >= 0) { // "field^boost", e.g. title^10
    float boost = Float.parseFloat(fieldName.substring(caret + 1));
    nearestFuzzyQuery.addTerms(text, fieldName.substring(0, caret), boost);
  } else {
    nearestFuzzyQuery.addTerms(text, fieldName);
  }
}
// NearestFuzzyQuery: new addTerms(String, String, float); FieldVals keeps the boost
st.score = (st.score * st.score) * idf(df, corpusNumDocs) * f.boost;
All options and trade-offs3 options · 782 words

Two code fixes were built and validated; they give the same classes and scores on every probed configuration and, by the code (not run), can differ only when an input yields more than 300 candidate terms. A javadoc-only fix is the third option.

Pass the boost into the one shared NearestFuzzyQuery (recommended) (validated by the checks)

+29/-4 in KNearestFuzzyClassifier.java and utils/NearestFuzzyQuery.java. knnSearch splits field^boost and calls a new public overload NearestFuzzyQuery.addTerms(String, String, float boost); the old two-argument method delegates with 1f. The new method rejects a negative or non-finite boost; FieldVals keeps the boost (also in equals/hashCode), and each term's score is multiplied by it.

// KNearestFuzzyClassifier.knnSearch
for (String fieldName : textFieldNames) {
  int caret = fieldName.indexOf('^');
  if (caret >= 0) { // "field^boost", e.g. title^10
    float boost = Float.parseFloat(fieldName.substring(caret + 1));
    nearestFuzzyQuery.addTerms(text, fieldName.substring(0, caret), boost);
  } else {
    nearestFuzzyQuery.addTerms(text, fieldName);
  }
}
// NearestFuzzyQuery: new addTerms(String, String, float); FieldVals keeps the boost
st.score = (st.score * st.score) * idf(df, corpusNumDocs) * f.boost;

Trade-offs

  • One query and one shared 300-term budget, as today; the boost scales the field's term weights, like mlt.setBoostFactor in the sibling. Without ^ the only scoring change is a multiplication by 1f, which is exact, so the rewritten query is unchanged (FieldVals also carries the boost, in equals and hashCode, so its hash values change); title^1 equals title.
  • The scaled score is also used for the 300-term cut, so a boosted field wins more of the budget when an input has more candidate terms than that. The earlier cap of 50 variants per term and field is applied before the boost, as today. Neither cap was reached in any run (the probe indexes have at most 8 distinct terms per field), neither was tested with a fix, and the new overload has no direct unit test.
  • Adds one public overload to NearestFuzzyQuery (an exported package of the module).
  • It parses on every call, like KNearestNeighborClassifier (SimpleNaiveBayesDocumentClassifier parses on its first call only), and a field literally named title^5 can no longer be addressed (the mechanism probe now gets fruit instead of tool). A malformed boost (title^x, title^) throws NumberFormatException on every call; parsing once in the constructor would fail fast instead (not run).
  • The accepted grammar is that of Float.parseFloat: title^1e1 is accepted, title^2^3 throws (the sibling reads it as boost 2), and a literal ^ cannot be escaped. The new overload rejects negative (also -0.0) and non-finite boosts but accepts 0, while its message says "positive": reword the message, or reject 0, in the PR. None of these inputs was run.

What the checks validated

  • stock: the new test fails 4 of 4 through gradle with assertions on (seed 5EED0811) and off (5EED0812), and again with the final test file (5EED0816);
  • fix B: the whole :lucene:classification:test passes, 13 suites, 47 tests (the new class's 4 included), 1 skipped (@Monster 20 Newsgroups), 0 failures, with assertions on (5EED0813) and off (5EED0814);
  • fix B: :lucene:classification:check passes (google-java-format, ecj lint, forbidden APIs and the tests, 5EED0817);
  • standalone runner with fix B shadowed: the new test (assertions on and off), the independent reproduction's test (4 of 4), the finder's test, and the probe (the tables above) pass.

One boosted NearestFuzzyQuery per field when any entry has a boost (the hunt's sketch) (validated by the checks)

+26/-3 in KNearestFuzzyClassifier.java only. Without ^ today's code runs unchanged; otherwise each field gets its own NearestFuzzyQuery, wrapped in a BoostQuery, as a SHOULD clause of a required BooleanQuery. No API change; it mirrors the sibling, which builds one MoreLikeThis query per field.

Trade-offs

Two code paths. With any boost, each field gets its own 300-term budget, so the query can grow to 300 terms per field; and title^1 takes the boosted path, so for long inputs it can differ from title. Raised by the reviewers; not measured.

What the checks validated

  • the whole :lucene:classification:test passes, 13 suites, 47 tests, 1 skipped, 0 failures (assertions on, 5EED0815), and so does :lucene:classification:check (5EED0818);
  • standalone runner with fix A shadowed: the new test passes with assertions on and off; earlier runs of the hunt: the finder's test, the 3 upstream TestKNearestFuzzyClassifier tests, the independent reproduction's test (4 of 4) and the portable history probe pass.

Delete the javadoc sentence

Replace "they can contain boosting indication e.g. title^10" with the module's wording for classes without boost support, "NO boosting supported per field".

Trade-offs

Not run. Removes the broken promise but keeps the silent result for anyone who already writes a boosted name; rejecting entries with ^ would make it loud but would break a field literally named with ^.

Downstream note

A product that labels or routes content with this classifier and a boosted field list silently gets wrong labels; no product that uses this class was found.

How it was found

An AI tool building a test baseline for the classification module compared the constructor javadoc's field^boost example with the code and saw that the boost is never parsed. A public-API JUnit test confirmed it on the stock jars; independent AI tools then reproduced, reviewed and judged it.

The method in full127 words

Method: a documented-syntax check (the hunt's "non-formal" tier), 2026-10-26, stock main jars.

  • While a hunt unit built a property-based baseline for the classification module, it checked the constructor javadoc's documented field^boost syntax against knnSearch. Its fuzz harness never passes a boosted field name, so the harness itself could not find it. The lead was left unfiled.
  • On 2026-10-27 a later unit turned the lead into a public-API JUnit test with a differential oracle (the unboosted call and the sibling classifier) and re-ran it on main a9779ef3c193: it failed with assertions on and off and on 10.5.2.
  • Then independent AI tools reproduced, reviewed and judged it (see the checks), and the history step ran one portable probe on eight releases from 7.0.0 to 10.5.2 plus main.

Prior reports, history and checks

Prior reports

No prior report found (best-effort search, 2026-10-27): the scripted prior-report search's queries over GitHub, commits, downstream projects, four mailing lists, JIRA and the web, plus manual searches. Nearest: #9039 (LUCENE-7991), a different boost bug in a sibling classifier, fixed in 7.2.0.

Searches4 searches

The searches below found no issue, PR, review comment, JIRA entry, list thread, commit or web page that says that KNearestFuzzyClassifier does not parse the field^boost syntax, or reports its symptom (a null class or a dropped field for a boosted name). It is not fixed upstream: KNearestFuzzyClassifier.java last changed on 2024-09-11 (#13762) on main and branch_10_6, and no open PR touches it.

Related, not this bug: #9039 / LUCENE-7991 (a boost leak in KNearestNeighborDocumentClassifier, fixed in 7.2.0: precedent); #8889 / LUCENE-7838 (introduced the class; the thread is about the sandbox dependency); #7689 / LUCENE-6631 and #7712 / LUCENE-6654 (added field boosting, with parsing, to the siblings). Gaps: token-based search may not match title^10 literally; part of the tool's GitHub results came from a cache written earlier the same day (the key class-name queries were rerun live by hand); list and JIRA search is keyword-based; private reports are not visible. These searches cannot prove that nobody ever reported it.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene, apache/solr, all of GitHub)2026-10-27the scripted prior-report search's 24 issue/PR, 3 commit and 2 downstream queries; by hand: the class names in titles, bodies and comments, all 68 module:classification items, everything created in the last 48 hours, the class name across all of GitHub, the changed files of every open PR touching lucene/classification and of every PR opened 2026-10-26/27no report; the related items above; a refresh found no new item touching lucene/classification
JIRA archive (LUCENE, SOLR)2026-10-274 tool queries (1 failed on quoting and was rerun by hand) and 8 by hand (class names, fuzzy classifier, classification boosts, all classification-component issues since 2017)no report
Mailing lists (lists.apache.org)2026-10-2720 tool queries over dev@, java-user@, issues@ and commits@, plus the Lucene and Solr lists by handno report
Web2026-10-279 tool queries planned (7 run; the 2 left pending were searched by hand with other wording) and 7 by handnothing relevant

History

Present since the class was added by LUCENE-7838 (bd9e32d3583, 2017-05-18) and first released in 7.0.0; never changed in this respect. One portable probe fails on 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main; 6.6.6 has no such class.

Versions tested6 rows

Introduced by bd9e32d358399af7c31e732314e1ef1dd89bcfa1 "LUCENE-7838 - added knn classifier based on flt" (2017-05-18, @tteofili; JIRA LUCENE-7838 = apache/lucene#8889; committed directly, no PR). It added the javadoc sentence and passed each name unchanged to FuzzyLikeThisQuery.addTerms(text, fieldName, 1f, 2). 92e460389dc (2017-06-29, same issue) replaced that with NearestFuzzyQuery and kept the raw lookup. The sentence and its parsing had come to KNearestNeighborClassifier together in 35810f4c77d (LUCENE-6654, 2015); this class copied the sentence only. Not a regression. First release 7.0.0 (CHANGES.txt:8003). All 65 release tags from 7.0.0 to 10.5.2 in the history clone have the sentence and no ^ parsing. The releases 8.8.2 to 8.11.4 have no tag there (the last 8.x tag is 8.8.1): 8.11.4 was run from its release jars and fails, and 8.8.2-8.11.3, between the two, are inferred, not checked. KNearestFuzzyClassifier.java last changed in f0fd51c4d5c4 (#13762, 2024-09-11) on main and on branch_10_6 (checked read-only on 2026-10-27 and 2026-10-28), so 10.6.0, released from that branch on 2026-10-29, is affected. All release runs used OpenJDK 25.0.3; the bug is a field-name lookup and does not depend on the JVM.

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthis doc's test: 4 of 4 fail (gradle and the standalone runner, assertions on and off); the portable probe fails
10.5.2 affectedthis doc's test: 4 of 4 fail (assertions on and off); the portable probe fails
7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0 release sweepthe portable probe fails in every version: text = fruit, text^2 = null; [title, body] = tool, [title^10, body] = null
6.6.6 API absentno KNearestFuzzyClassifier (compile error)
main + fix A controlthe portable probe passes
main + fix B not affectedevery repro passes; the whole lucene/classification suite passes

Independent checks

An independent reproducer and a devil's advocate found it real with their own executed tests; an adjudication and an audit re-ran it with fresh seeds. Read-only reviews by another model family agreed on the root cause (one gave a procedural "not real": it could not run code). This doc's gradle runs reproduced it on main.

Checks and verdicts385 words

Every reproducing test (finder, the independent reproduction, the cross-model review stage, adjudication, audit, this doc) failed on the stock main jars with assertions on and off; the mechanism probes and the two separate runs ran with assertions on only. Fix checks shadowed the patched class (standalone runner) or applied the diff (gradle); fix A's gradle suite and check ran with assertions on only. The reviews by the other model family ran read-only (they could not execute code), and a separate run re-ran the finder's test each time, which failed.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidatePublic-API JUnit fails on main (assertions on and off) and 10.5.2: text^2 gives null where text gives fruit; fix A passes it and the 3 upstream TestKNearestFuzzyClassifier tests.
cross-model review, refute lens not real (low, procedural)An independent reviewer from another model family (OpenAI GPT-5.6), read-only: could not run Java, so it voted not real on procedure only; root cause agreed, inputs in contract, intended argument moderate (a javadoc-only fix is plausible); flagged fix A's per-field term budget; a separate re-run failed as claimed.
Independent reproducer realOwn 4-test JUnit fails 3 of 4 on main (assertions on and off, 20 iterations), 10.5.2 and 9.12.3; a mechanism probe shows a field literally named title^5 is read; fix A passes 4 of 4.
Cross-model review, intended lens real (medium)OpenAI GPT-5.6, read-only: intended argument weak, root cause agreed, inputs in contract; strongest counter-case @lucene.experimental and a copied javadoc. A separate run re-ran the finder's test on main and 10.5.2: fails.
Devil's advocate realNo intended-behaviour case survives (copied javadoc, experimental, literal ^ names); found that the finder's sibling assertion does not discriminate and that multi-field configurations drop the boosted field.
adjudication confirmed (high)Re-ran the independent reproduction's test with seed 1A2B3C4D, assertions on and off: 3 of 4 fail.
credibility audit keepRe-ran the independent reproduction's test on main (fresh seeds, assertions on and off) and on 10.5.2: 3 of 4 fail; mechanism probe passes; score and dedup checked.
this doc reproducedNew 4-test JUnit: 4 of 4 fail through gradle and the standalone runner on main (assertions on and off) and on 10.5.2; fix B and fix A pass it and the whole classification suite.

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

Finding record

LFV-801, snapshot of 2026-11-06

Registry title
KNearestFuzzyClassifier ignores its documented 'field^boost' syntax: an entry such as 'title^10' in textFieldNames is looked up as a literal field name, so that field contributes nothing, and with a single boosted field assignClass returns null
Area
classification / KNearestFuzzyClassifier (textFieldNames 'field^boost' syntax; NearestFuzzyQuery field lookup)
Priority
Low, rank 339: In the experimental, little-used KNearestFuzzyClassifier, and the single-field case fails visibly (null).
Impact class
silent wrong results
Impact note
Hits only applications that use the experimental KNearestFuzzyClassifier and follow its constructor javadoc by passing a boosted field name such as 'title^10'; then on every assignClass/getClasses call, with no error, at any scale and with the default IndexWriterConfig, codec and searcher: a single boosted field gives null (getClasses an empty list), and in a multi-field configuration the boosted field is silently dropped while the other fields still work; every release from 7.0.0 to 10.6.0 (10.6.0 by reading) and main.
Summary
You build a KNearestFuzzyClassifier with a text field written as 'text^2' or 'title^10', as its constructor javadoc says you may, and expect the same classes as with the plain field name (with that field weighted more). Instead the entry is used verbatim as the name of a field called 'text^2', which does not exist, so that field adds no terms to the query: with one text field assignClass returns null and getClasses an empty list, and with several fields the boosted one is silently ignored. The sibling KNearestNeighborClassifier, whose javadoc has the same sentence, parses the boost and classifies normally.
Introduced
bd9e32d358399af7c31e732314e1ef1dd89bcfa1 ('LUCENE-7838 - added knn classifier based on flt', by @tteofili; JIRA LUCENE-7838 = apache/lucene#8889; committed directly, no pull request): the commit that added the class already had the javadoc sentence 'they can contain boosting indication e.g. title^10' and passed each name unchanged to FuzzyLikeThisQuery.addTerms [read: git show]; 92e460389dc (2017-06-29, same issue, in 7.0.0) replaced it with NearestFuzzyQuery and kept the raw-name lookup [read]. The sentence and its parsing came together to KNearestNeighborClassifier in 35810f4c77d (LUCENE-6654, 2015); this class copied the sentence but not the parsing [read: git log -S]. Not a regression. (2017-05-18 (bd9e32d3583); 2017-06-29 (92e460389dc, NearestFuzzyQuery form))
First affected version
7.0.0 [exec: the version-portable history probe fails]; 6.x is not affected because it has no KNearestFuzzyClassifier [exec: 6.6.6 compile error, class absent]
Affected versions
every release that has the class: 7.0.0 to 10.5.2, 10.6.0 (by reading) and main a9779ef3c193 (unreleased 11.0) [exec: the history probe fails ('text^2' -> null vs 'text' -> fruit; ['title^10','body'] -> null vs ['title','body'] -> tool) on 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main; the finder's JUnit fails on 9.12.3, 10.0.0, 10.5.2 and main (-ea; main also -da); the independent reproduction's JUnit fails 3 of 4 on main, 10.5.2 and 9.12.3 (-ea, -da)]; the releases in between by inference: all 65 release tags 7.0.0..10.5.2 carry the javadoc sentence and no '^' parsing [read]. branch_10_6 and upstream main head have no change to KNearestFuzzyClassifier.java since f0fd51c4d5c4 (2024-09-11) [read], so 10.6.0 (released 2026-10-29) ships it. Executed on Linux aarch64 with JDK 25 only (pre-10 releases too).
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
non-formal: code reading
Upstream and prior reports
none found (the prior-report record verdict new, confidence high, searched 2026-10-27, read-only; the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's searches found nothing either): scripted prior-report search (24 GitHub issue/PR queries on apache/lucene, 3 commit, 2 downstream (Elasticsearch, OpenSearch, Solr), 20 mailing-list queries over dev@, java-user@, issues@ and commits@, 4 JIRA and 9 web queries; 116 distinct hits, 0 high), manual GitHub searches (class names in title/body/comments, all 68 module:classification items, everything created in the last 48 h, GitHub-wide class-name searches, Solr classification boosts), the changed files of every open PR that touches lucene/classification (#16847, #681, #12295) and of every PR opened 2026-10-26/27, 8 JIRA queries (LUCENE and SOLR), lists.apache.org and 7 web searches. Nearest items, none the same bug: #9039 / LUCENE-7991 (2017, fixed: a field-boost leak in KNearestNeighborDocumentClassifier, another class and mechanism), #8889 / LUCENE-7838 (introduced the class; nothing on boosts), #7689 / LUCENE-6631 and #7712 / LUCENE-6654 (added field boosting with parsing to the siblings). Refresh (2026-10-27, read-only): GitHub GraphQL search on apache/lucene for 'KNearestFuzzyClassifier', 'KNearestFuzzy boost', '"boosting indication"' and 'classification created:>=2026-10-25' gives 0 hits, 'NearestFuzzyQuery' only #8889; none of the 19 issues and PRs created since 2026-10-25 (#16905-#16923, incl. #16913) touches lucene/classification; KNearestFuzzyClassifier.java last changed in f0fd51c4d5c4 (#13762, 2024-09-11) on upstream main and branch_10_6. Not fixed upstream. Gaps: GitHub search is token-based and may not match 'title^10' literally; list and JIRA search is keyword-based; private reports are not visible.
Lucene commit tested
main a9779ef3c193 (commit of 2026-10-26; tested 2026-10-27/28) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the 10.5.2 release jars (this doc's test, assertions on and off). Earlier checks: the finder's and the independent reproduction's tests on main, 10.5.2 and 9.12.3 (the adjudication re-ran the independent reproduction's test on main, the audit on main and 10.5.2), and one portable history probe on the releases 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0 and 10.5.2 (6.6.6 has no such class). Every run used OpenJDK 25.0.3 on Linux aarch64. Both fixes were validated in the scratch worktree: the new test, the whole lucene/classification suite (13 suites, 47 tests) and :lucene:classification:check; fix B also with assertions off.

Lucene and upstream sources

Finding record
  • lucene/classification/src/java/org/apache/lucene/classification/KNearestFuzzyClassifier.java:44-48 (at a9779ef3c193) (class javadoc, @lucene.experimental), :83-84 (constructor javadoc '@param textFieldNames the name of the fields used as the inputs for the classifier, they can contain boosting indication e.g. title^10'), :135-148 (knnSearch passes each name unchanged to NearestFuzzyQuery.addTerms and adds the query as MUST; fix site)
  • lucene/classification/src/java/org/apache/lucene/classification/utils/NearestFuzzyQuery.java:130-136 (at a9779ef3c193) (addTerms(IndexReader, FieldVals, PriorityQueue): MultiTerms.getTerms(reader, f.fieldName) == null -> return, no terms)
  • lucene/classification/src/java/org/apache/lucene/classification/KNearestNeighborClassifier.java:88-89 (at a9779ef3c193) (same javadoc sentence), :157-172 (knnSearch splits 'field^boost' and applies the boost)
The finding in 30 seconds
  • lucene/classification/src/java/org/apache/lucene/classification/KNearestFuzzyClassifier.java:45-49 (at a9779ef3c193) (@lucene.experimental), :83-84 (javadoc), :94, :104-125, :135-148 (knnSearch)
  • lucene/classification/src/java/org/apache/lucene/classification/utils/NearestFuzzyQuery.java:58 (at a9779ef3c193) (MAX_NUM_TERMS), :118-136 (addTerms; getTerms null -> return), :217-263 (rewrite)
Who is affected
  • lucene/classification/src/java/org/apache/lucene/classification/KNearestFuzzyClassifier.java:48, (at a9779ef3c193) :132
Why it looks unintended
  • lucene/classification/src/java/org/apache/lucene/classification/KNearestFuzzyClassifier.java:81, (at a9779ef3c193) :83-84
  • lucene/classification/src/java/org/apache/lucene/classification/BM25NBClassifier.java:76-77; (at a9779ef3c193) SimpleNaiveBayesClassifier.java:84-85; KNearestNeighborClassifier.java:88-89, :161-168; document/KNearestNeighborDocumentClassifier.java:66-67, :128-129; document/SimpleNaiveBayesDocumentClassifier.java:63-64, :157-158, :170 (writes the stripped name back)
  • lucene/classification/src/test/org/apache/lucene/classification/TestKNearestFuzzyClassifier.java (at a9779ef3c193) (plain names only); document/TestKNearestNeighborDocumentClassifier.java:149, document/TestSimpleNaiveBayesDocumentClassifier.java:122 (title^100)
  • lucene/CHANGES.txt:7763-7765 (at a9779ef3c193) (LUCENE-7991, Lucene 7.2.0)
Suggested fix
  • lucene/classification/src/java/org/apache/lucene/classification/utils/NearestFuzzyQuery.java:58, (at a9779ef3c193) :217-224
  • lucene/classification/src/java/org/apache/lucene/classification/KNearestNeighborClassifier.java:157-173 (at a9779ef3c193)
  • lucene/classification/src/java/org/apache/lucene/classification/BM25NBClassifier.java:76-77 (at a9779ef3c193) ("NO boosting supported per field")
History
  • lucene/CHANGES.txt:8003 (at a9779ef3c193) (LUCENE-7838 under Lucene 7.0.0)

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

Repro files

About the diagrams

A still, made in parallel with this doc from an executed probe on main a9779ef3c193 with the same two indexes (stock, and the per-field fix sketch as the control).

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.