All findings · Lucene finding · LFV-565 · smartchinese-normalize-skips-fullwidth-folding

SmartChineseAnalyzer.normalize skips the tokenizer's full-width folding, so full-width prefix, wildcard, fuzzy and range queries can miss

SmartChineseAnalyzer indexes full-width letters and digits as plain ASCII (Lucene as lucen), but its normalize() only lower-cases, so prefix, wildcard, fuzzy, regexp and range queries with full-width letters or digits can miss: Luc* finds 0 documents, Luc* finds 1. Every release since 6.2.0 has it.

Summary

Priority
Low, rank 302Needs full-width Latin letters or digits in a multi-term query on a SmartChineseAnalyzer field, rare because common input methods produce half-width Latin.
Impact
silent wrong results
Since
6.2.0 (LUCENE-7355, committed 2016-07-12)introduced 2016-07-12 · over 10 years
Affected
6.2.0 to 10.6.0 (10.6.0 by reading) and main
Status
documented
Found by
bounded-exhaustive metamorphic scan: normalize() vs the indexed token

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

Chinese text can contain full-width Latin letters and digits (Lucene, 2026). SmartChineseAnalyzer indexes them as plain ASCII, but the query-time shortcut that the parsers use for partial words leaves that step out.

  1. Index 我们使用Lucene ("we use Lucene", the Latin word in full-width letters, which Chinese text can contain) with the stock new SmartChineseAnalyzer(). Its tokenizer turns full-width letters and digits into ASCII and lower-cases them, so the index holds 我们, 使用 and lucen (Porter-stemmed).
  2. Search Luc* with QueryParser. A partial word cannot go through the full analyzer, so the parser calls Analyzer.normalize, documented to return "the representation that it would have in the index".
  3. You expect 1 hit, the same as Luc* and the plain term query Lucene get.
  4. You get 0 hits, no error. SmartChineseAnalyzer.normalize only lower-cases, so the query becomes luc*, still full-width, and no indexed term starts with it. Called directly, normalize("ABC") returns abc and normalize("123") returns 123, while the index holds abc and 123.
  5. The same miss shows for wildcard (Lu?en), fuzzy (Lucen~1), regexp and range ([Lua TO Luz]) terms in the classic and flexible parsers and for the simple parser's prefix and fuzzy terms, whichever width the document used; fuzzy terms can still hit when the edit distance absorbs the width difference. Parsed term and phrase queries run the full analyzer and are unaffected.

Who is affected

Applications that index Chinese or mixed Chinese-English text with the stock SmartChineseAnalyzer and accept prefix, wildcard, fuzzy, regexp or range syntax through Lucene's query parsers. The query term must hold full-width letters or digits next to an ASCII operator, for example pasted full-width text plus an added *. The only symptom is silently fewer hits.

Mechanism, terms and numbers742 words

SmartChineseAnalyzer does its case and width handling inside the tokenizer: HMMChineseTokenizer's SegTokenFilter turns full-width Latin letters and digits into ASCII and lower-cases A-Z (SegTokenFilter.java:39-62), and createComponents has no LowerCaseFilter (SmartChineseAnalyzer.java:124-134). So 我们使用Lucene is indexed as 我们, 使用, lucen. SmartChineseAnalyzer.normalize(), which the classic and flexible query parsers use for prefix terms, wildcard chunks, fuzzy terms, regexps and range bounds (the simple parser for its prefix and fuzzy terms), returns only new LowerCaseFilter(in) (:137-140), although Analyzer#normalize is documented to give "the representation that it would have in the index", without tokenizing or stemming (Analyzer.java:203-208). The result: Luc*, Lucen~1 and [Lua TO Luz] find nothing while Luc* and the term query Lucene find the document, and normalize("ABC") returns abc while the index holds abc. A secondary, narrower direction: normalize lower-cases non-ASCII capitals such as É that the index keeps. Parsed term and phrase queries and the indexed data are unaffected. Every release from 6.2.0 to 10.6.0 (10.6.0 by reading) and main has it. The fix makes normalize() apply the tokenizer's folding; it changes only the query side, so no reindex is needed.

Terms

  • Full-width forms: the letters A-Z, a-z (U+FF21-U+FF3A, U+FF41-U+FF5A) and digits 0-9 (U+FF10-U+FF19), each as wide as a Chinese character. Chinese input methods produce them in full-width mode, giving mixed text such as 我们使用Lucene or 2026年.
  • SegTokenFilter: part of HMMChineseTokenizer, so it runs inside the tokenizer, not as a token filter. For full-width tokens it subtracts 0xFEE0 from each character (full-width to ASCII) and then lower-cases A-Z; for ASCII tokens it lower-cases A-Z (SegTokenFilter.java:39-62; javadoc "Full-width latin will be converted to half-width, then all latin will be lowercased"). createComponents adds no LowerCaseFilter: "LowerCaseFilter is not needed, as SegTokenFilter lowercases Basic Latin text" (SmartChineseAnalyzer.java:128).
  • Analyzer.normalize(field, text): what the query parsers call for a partial word, because the full analyzer would tokenize or stem it. It runs the analyzer's normalize(String, TokenStream) override, which should hold the normalizing steps and leave out the tokenizer and the stemmer (Analyzer.java:53-58, :203-212).
  • Multi-term queries: prefix (Luc*), wildcard (Lu?en), fuzzy (Lucen~1), regexp (/luc.*/) and range ([Lua TO Luz]). Parsed term and phrase queries run the full analyzer and are not affected.

The bug

  1. SmartChineseAnalyzer.normalize() is return new LowerCaseFilter(in); (SmartChineseAnalyzer.java:137-140). Because the folding lives in the tokenizer, which normalize skips, it is lost.
  2. The classic parser passes the prefix through it (QueryParserBase.java:712, BytesRef term = getAnalyzer().normalize(field, termStr);), and likewise the fuzzy term (:729), the regexp (:683), each wildcard chunk (:646, :656) and the range bounds (:550, :556; an open * bound is skipped, :547-557). The flexible parser's processors do the same (range bounds only when they do not parse as dates, TermRangeQueryNodeProcessor.java:100-139). The simple parser has only prefix and fuzzy syntax and normalizes both (SimpleQueryParser.java:573, :605).
  3. Luc* therefore becomes body:luc*. The index holds lucen, so the prefix cannot match any term. LowerCaseFilter also applies Character.toLowerCase to every code point, while the index lower-cases only A-Z, so normalize("É") is é but the index keeps É. This secondary direction matters for single-letter terms, because the tokenizer emits such letters one per token, and for range bounds: [Аz TO Яz] (Cyrillic capitals) built from normalize admits б instead of the indexed Б.
  4. The sibling analyzers fold width in normalization: CJKAnalyzer.normalize is CJKWidthFilter + LowerCaseFilter (CJKAnalyzer.java:101-106), and JapaneseAnalyzer applies CJKWidthCharFilter in initReaderForNormalization (JapaneseAnalyzer.java:130-133).

The numbers

One document 我们使用Lucene, default SmartChineseAnalyzer, classic QueryParser: Luc* and Lucen~1 find 0 documents; Luc*, Lucen~1 and the term query Lucene find 1. normalize("ABC") = abc (index abc), normalize("123") = 123 (index 123). The hunt's scan found 76 of 285 single-token words that disagree; every one of the 73 that the parser scan checked (the other 3 were not in its word set) was missed by classic and flexible prefix, wildcard, regexp and range queries and by simple-parser prefix queries, while fuzzy still hit 61 within the edit distance. An exhaustive scan of every BMP character indexed as one token found 1,141 of 61,527 disagreeing (the 62 full-width letters and digits and 1,079 non-ASCII capitals) and all 2,704 two-letter full-width words.

Exposure and likelihood410 words
  • Who is exposed. Applications that use the stock SmartChineseAnalyzer, the default constructor included, and pass user text through QueryParser, StandardQueryParser or SimpleQueryParser (prefix and fuzzy only), or call Analyzer.normalize themselves. Analyzer wrappers delegate normalize to the wrapped analyzer (AnalyzerWrapper.java:123-127), so wrapped uses inherit it unless the wrapper adds its own normalization step (as the workaround below does). The class targets "Chinese or mixed Chinese-English text" (SmartChineseAnalyzer.java:33).
  • Default, not opt-in. Default analyzer and default parser settings. The only choices involved are the smartcn analyzer (an optional module, marked @lucene.experimental) and multi-term syntax.
  • What it takes. Full-width letters or digits in the query term next to an ASCII *, ?, ~, / or range syntax: mixed-width input, or an application that appends * or builds a range around user text. By the classic grammar, a full-width * is an ordinary character, so Luc* typed entirely in full-width mode becomes the plain term query body:luc, not a prefix query, and does not match lucen (QueryParser.jj:183-187, :208-211; executed). Documents written in full width are still found by half-width queries that fit the stemmed term: Luc* finds both widths, Lucene* neither, because the index holds lucen. The registry rates the likelihood rare; no report was found in the searched public sources.
  • What the damage looks like. Prefix terms, and wildcard or regexp literals, that require a full-width letter or digit cannot match any indexed term: 0 hits, no error, whichever width the document used. Range bounds compare in the wrong form and can drop or add terms ([a TO z] admitted 7 of 8 probe documents; the indexed-form [a TO z] admits 3). Fuzzy terms can still hit within the edit distance, so say "miss", not "never match".
  • Scale. One document and one query are enough. All 62 full-width letters and digits and all 2,704 two-letter full-width words normalize differently from how they are indexed.
  • Downstream (code reading only, nothing run). Elasticsearch's analysis-smartcn plugin registers the stock analyzer, and its query_string parser normalizes range, fuzzy, prefix and wildcard terms with the field's analyzer, so it would inherit the miss; another downstream server's smartcn plugin also registers the stock class.
  • Limit. After the fix, prefixes that run past the Porter stem stay best effort, as with every stemming analyzer (Lu?ene misses even with the fix, because Lucene is indexed as lucen). This bug is only the width and case mismatch.

Minimal repro

The test shows that SmartChineseAnalyzer indexes the full-width word ABC as abc, but normalize("f", "ABC") (used by the query parsers for prefix, wildcard, regexp, range and fuzzy terms) keeps it full-width.

TestSmartChineseNormalizeFullWidth.java (JUnit test)31 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.analysis.cn.smart;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.tests.analysis.BaseTokenStreamTestCase;

public class TestSmartChineseNormalizeFullWidth extends BaseTokenStreamTestCase {

  public void testNormalizeMatchesIndexedTerm() throws Exception {
    try (Analyzer a = new SmartChineseAnalyzer()) {
      assertAnalyzesTo(a, "ABC", new String[] {"abc"});
      // expected: normalize returns the indexed term "abc"; actual: "abc" (full-width kept)
      assertEquals("abc", a.normalize("f", "ABC").utf8ToString());
    }
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-565/, 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/analysis/smartcn/build/libs/lucene-analysis-smartcn-$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.analysis.cn.smart.TestSmartChineseNormalizeFullWidth
Expected vs actual
Expected: normalize returns the indexed term abc. Actual: abc.

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

More files, environment, full output32 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
    => org.junit.ComparisonFailure: expected:<[abc]> but was:<[abc]>
       org.junit.Assert.assertEquals(Assert.java:117)
       org.junit.Assert.assertEquals(Assert.java:146)
       org.apache.lucene.analysis.cn.smart.TestSmartChineseNormalizeFullWidth.testNormalizeMatchesIndexedTerm(TestSmartChineseNormalizeFullWidth.java:28)

Why it looks unintended

  • Analyzer#normalize must return "the representation that it would have in the index", minus only tokenizing and stemming. Width and case folding are neither.
  • The index-side folding is documented and tested: testFullWidth asserts Tests becomes test and 1234 becomes 1234.
  • The same 2016 commit gave CJKAnalyzer.normalize width folding; SmartChineseAnalyzer is the outlier.
The full argument243 words
  • The contract. Analyzer.java:203-208: "Normalize a string down to the representation that it would have in the index. This is typically used by query parsers in order to generate a query on a given term, without tokenizing or stemming". The class example (:53-58) keeps a filter that is "about normalization" and drops one that is "about stemming".
  • The analyzer documents its folding as index behaviour. SegTokenFilter's javadoc ("Full-width latin will be converted to half-width") and the SmartChineseAnalyzer.java:128 comment; TestSmartChineseAnalyzer.testFullWidth ("Full width alphas and numerics are folded to half-width", :160-170) asserts it.
  • Design intent. On LUCENE-7355 (#8409), jpountz, who added normalize, described it as applying "all normalization filters (lowercasing, ascii folding, etc.) but not transformations (stop word removal, stemming, etc.)".
  • Siblings. The same commit gave CJKAnalyzer.normalize CJKWidthFilter + LowerCaseFilter and JapaneseAnalyzer width folding (today through initReaderForNormalization). SmartChineseAnalyzer, whose folding is hidden in the tokenizer, got only LowerCaseFilter.
  • Tests. No smartcn test calls normalize; BaseTokenStreamTestCase only checks that it does not throw (:1478). Nothing pins the current output.
  • Precedents fixed as bugs. LUCENE-8186 (#9234, fixed in 7.4 and 8.0): CustomAnalyzer with a lower-casing tokenizer did not normalize multi-term queries, fixed so that normalize applies the tokenizer's normalization; LUCENE-7606 (#8657, fixed in 6.4 and 7.0): CustomAnalyzer.normalize applied only the last token filter. Separately, in LUCENE-8022 (#9070, closed as Invalid) a maintainer told a user that their analyzer must override normalize() to mirror its index-time lower-casing.
The devil's-advocate check313 words

The strongest "intended" case (the cross-model reviews rated it moderate; the independent reproducer rated it weak and the devil's-advocate verifier rejected it): SmartChineseAnalyzer is @lucene.experimental; normalize deliberately skips tokenization, and here the folding lives in the tokenizer; the classic parser's javadoc says "normal Analyzers are unlikely to work properly with wildcard templates" (QueryParserBase.java:611, :696; :667 says the same of "regular expression templates"), and a comment in its wildcard code calls keeping wildcard and escaped characters away from normalize a "best effort" (:638); and LowerCaseFilter keeps roughly what the parsers did before 6.2 (lowercaseExpandedTerms).

Why it fails: "experimental" marks API stability, not licence for query and index to disagree; the javadoc excludes tokenizing and stemming, not character folding, and LUCENE-8186 made normalize mirror a tokenizer's normalization; the wildcard javadoc predates LUCENE-7355 and is about splitting templates, while the prefix code a few lines below calls normalize (:712); back-compat with the old lower-casing makes the behaviour long-standing, not intended, since LUCENE-7355 aimed beyond lower-casing and added width folding wherever the index chain visibly folds width. No test, discussion or decision supporting the current output was found. Verdict: a defect. What the counter-case gets right: a fix changes long-standing query behaviour; the recommended fix copies the tokenizer's private folding rules into a second place; full-width text next to ASCII operators is unusual input; and the old parser lower-casing (String.toLowerCase(locale)) is close to, not identical with, LowerCaseFilter (per-code-point Character.toLowerCase; they differ for locale-sensitive and contextual mappings such as the Turkish dotless i and the Greek final sigma). These argue for a careful, tested change, not for keeping the mismatch. For the secondary É direction one could argue the index side deviates (SegTokenFilter's javadoc says "all latin will be lowercased" while the code lower-cases only A-Z), so present that direction as a question.

Suggested fix

Recommended normalize() applies the tokenizer's folding (validated)

Make normalize() apply the tokenizer's folding: full-width letters and digits to ASCII, then lower-case A-Z only (a small private filter replacing LowerCaseFilter). It changes only the query side, so no reindex is needed. With it the repro and all 23 smartcn tests pass, and normalize agrees with indexing on all 61,527 single-character BMP tokens tested.

All options and trade-offs4 options · 924 words

Two fixes were built and checked in a scratch worktree of main ea57360912ed; A mirrors the index exactly and is recommended. The other options are a larger refactoring and a workaround.

normalize() applies the tokenizer's folding (validated) (recommended)

Replace new LowerCaseFilter(in) with a private filter that does what SegTokenFilter does at index time, restricted to the three ranges that Utility.getCharType puts in the full-width classes (U+FF10-U+FF19, U+FF21-U+FF3A, U+FF41-U+FF5A). The LowerCaseFilter import goes; TokenFilter and CharTermAttribute are imported (+36/-2, formatted by tidy). Same logic as the finder's fix sketch. A regression test: the repro's class, or its assertions added to TestSmartChineseAnalyzer next to testFullWidth.

@Override
protected TokenStream normalize(String fieldName, TokenStream in) {
  return new SegTokenNormalizationFilter(in);
}

/**
 * Applies the case and width folding that SegTokenFilter applies at index time: full-width Latin
 * letters and digits become half-width, then only Basic Latin A-Z is lower-cased.
 */
private static final class SegTokenNormalizationFilter extends TokenFilter {
  private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);

  SegTokenNormalizationFilter(TokenStream in) {
    super(in);
  }

  @Override
  public boolean incrementToken() throws IOException {
    if (input.incrementToken() == false) {
      return false;
    }
    final char[] buffer = termAtt.buffer();
    for (int i = 0; i < termAtt.length(); i++) {
      char c = buffer[i];
      if ((c >= 0xFF10 && c <= 0xFF19)
          || (c >= 0xFF21 && c <= 0xFF3A)
          || (c >= 0xFF41 && c <= 0xFF5A)) {
        c -= 0xFEE0;
      }
      if (c >= 'A' && c <= 'Z') {
        c += 0x20;
      }
      buffer[i] = c;
    }
    return true;
  }
}

Trade-offs

  • Query side only: the index already holds the folded form, so no reindex.
  • Behaviour change: the tested full-width multi-term queries now hit (the intent); a full-width * or . stays a literal character (Lu*? becomes lu*?, 0 hits; executed). Non-ASCII capitals are no longer lower-cased in normalize, so É* matches the indexed É instead of é; consistent with the index, but worth raising as a question with maintainers.
  • Cost: one character loop per normalized query term.
  • Maintenance: the filter copies the tokenizer's private folding rules (Utility.getCharType, SegTokenFilter), so the two can drift apart again unless a test pins them together.
  • The override has not changed since 2016, so the same patch should apply to maintained branches; only main was built and tested.
  • Prefixes that run past the Porter stem stay best effort, as with every stemming analyzer.

What the checks validated

  • Scratch worktree at ea57360912ed, gradle, JDK 25: the new test fails 2 of 2 on stock and passes with the fix; :lucene:analysis:smartcn:check passes (checkGoogleJavaFormat, ecjLint, forbiddenApis, and 3 suites / 23 tests: TestSmartChineseAnalyzer 19, TestHMMChineseTokenizerFactory 2, the new 2); the module tests pass with assertions off (seed 77AA0565), and the new test 20 iterations (40 tests).
  • Standalone runner with the fix shadowed: the new test 2 of 2 (assertions on and off), the QueryParser test 3 of 3, the independent reproducer's 5 of 5, the security probe 11 of 11; the exhaustive scan finds 0 of 61,527 single-character tokens and 0 of 2,704 two-letter full-width words disagreeing, and a scan of U+FF00-U+FFEF 0 of 202 (not scanned: supplementary characters and mixed, escaped, wildcard or regexp inputs). The finder's equivalent sketch passed the upstream TestSmartChineseAnalyzer (19 of 19) earlier.
  • These standalone runs shadowed the fix before tidy re-wrapped its if condition (same logic). The fact check re-ran the new test (assertions on and off), the QueryParser test and the security probe with the final source (brief/fix/src-a); all pass.

Reuse CJKWidthFilter, as CJKAnalyzer does (validated, not recommended)

TokenStream result = new CJKWidthFilter(in); result = new LowerCaseFilter(result); return result; (+4/-1). The repro passes and :lucene:analysis:smartcn:check passes (23 tests).

TokenStream result = new CJKWidthFilter(in);
result = new LowerCaseFilter(result);
return result;

Trade-offs

Smaller and reuses an existing filter, but it does not mirror the index: with it, the exhaustive scan still finds 1,138 of 61,527 single-character tokens disagreeing. That is the 1,079 non-ASCII capitals as before, plus 59 new ones in U+FF65-U+FF9F, which the index keeps half-width: CJKWidthFilter maps the half-width katakana and middle dot to their full-width forms, and the two half-width sound marks U+FF9E and U+FF9F to the combining marks U+3099 and U+309A, or merges them into a preceding katakana (CJKWidthFilter.java:67-76). It also folds full-width punctuation (U+FF01-U+FF5E), so a full-width * or . inside a wildcard or regexp term becomes an ASCII operator: Lu*? becomes lu*? and /Luc.*/ becomes /luc.*/, and both match lucen (executed). The independent reproducer's É test still fails.

Move the folding out of the tokenizer into separate filters (not run)

The direction of an unmerged 2009 patch on LUCENE-2023 (#3098), written for speed: split SegTokenFilter's lower-casing and full-width conversion into filters, which normalize could then reuse.

Trade-offs

By inference (not run): it could change indexed terms for some inputs and would then need a back-compat story (reindex), so it is a larger change than the query-side fix.

Workaround for users until a fix ships (validated)

SmartChineseAnalyzer is final, so wrap it: an AnalyzerWrapper whose getWrappedAnalyzer returns the stock analyzer and whose wrapTokenStreamForNormalization returns new CJKWidthFilter(in) (it runs after the stock LowerCaseFilter). Indexing is unchanged. On the stock main jars, with assertions on and off, Luc*, Lucen~1 and [Lua TO Luz] then find the document. Or type multi-term queries in half-width (Luc*).

Trade-offs

No reindex. It carries the CJKWidthFilter caveats of the previous option (half-width katakana; full-width * and . become operators) and leaves the É direction as it is, because the stock LowerCaseFilter runs first (É* becomes é*, 0 hits; executed).

Downstream note

A restriction written as a parsed multi-term query goes through the same normalize(), so a full-width pattern never equals an indexed term, and a MUST_NOT rule such as -Luc* removes nothing. Lucene's SECURITY.md (lines 28, 50) leaves authorization to the application and promises no untrusted-input guarantee here, so it is out of scope; no exploit is known.

How it was found

A bounded-exhaustive metamorphic scan of the stock analyzers: normalize(w) must equal the single token indexing stores for w. SmartChineseAnalyzer disagreed on 76 of 285 single-token words (full-width letters and digits, and capitals such as É). A second scan showed the misses through the query parsers, and code reading found the cause.

The method in full227 words

Method: a bounded-exhaustive metamorphic scan on the stock main jars (the study's "semi-formal" tier). A hunt unit, "analyzer normalize consistency", 2026-10-26.

  • Relation N1: Analyzer.normalize(f, w) equals the single full-span token that indexing produces for w, checked over bounded word sets for 99 analyzer configurations (every public stock Analyzer with a no-arg constructor in the module jars, 5 hand-built ones, and stemmer-free variants of 38 language analyzers). SmartChineseAnalyzer disagreed on 76 of 285 single-token words: full-width Latin letters and digits, and non-ASCII capitals such as É, Ø, İ, ẞ.
  • Relation N2 (parser reachability): each disagreeing word, used as a prefix, wildcard, fuzzy, regexp and range query through the classic, flexible and simple parsers, must still find its document. All 73 checked disagreeing words were missed by classic and flexible prefix, wildcard, regexp and range queries and by simple-parser prefix queries; fuzzy still hit 61. Every agreeing word was found. The flexible parser's multi-field mode, which has a separate bug of its own, differs: there prefix and wildcard hit 19 of the 73 and missed 79 agreeing words.
  • Code reading then located the cause: the folding lives in the tokenizer's SegTokenFilter, which normalize skips.
  • An AI finder wrote the public-API JUnit; independent AI tools then reproduced and judged it, and an exhaustive scan over every BMP character confirmed the scope (see the checks).

Prior reports, history and checks

Prior reports

No prior report: 198 queries over GitHub issues, PRs and commits, the mailing lists, the JIRA archive (every smartcn issue title read) and the web in English and Chinese (2026-10-26); 28 hits read, none this bug. Closest: LUCENE-2023 (#3098), an unmerged 2009 speed refactoring of the same folding.

Searches6 searches

No issue, PR, commit, JIRA entry, mailing-list thread, downstream issue or web page reports this bug, by anyone. #8409 (LUCENE-7355), which added the override, has 34 comments and none mentions smartcn or width folding. None of the 406 open PRs changes normalize. Upstream main a95c754ee003 (2026-10-26) still returns new LowerCaseFilter(in).

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues and PRs (apache/lucene), commits2026-10-26four runs of a scripted prior-report search (160 queries in total, including downstream, list and JIRA queries) plus 7 manual GitHub searches and open-PR lists: class names, normalize with full-width, fullwidth, wildcard, SegTokenFilter, HMMChineseTokenizer, Analyzer#normalizeno report; #8409 added the code and never mentions smartcn
Open PRs2026-10-26the touched paths of all 406 open apache/lucene PRs#681 (module-info lint) and #202 (WordDelimiterFilter removal) touch smartcn; neither changes normalize
JIRA archive2026-10-266 JQL searches over LUCENE and SOLR; all 56 LUCENE and 16 SOLR smartcn issue titles readno report
Mailing lists (lists.apache.org)2026-10-2617 manual searches over java-user@, dev@ and issues@ (smartcn, SmartChineseAnalyzer, HMMChineseTokenizer, fullwidth, full-width, halfwidth), all distinct subjects readno report
Downstream repositories and the web2026-10-26downstream-repository queries; 8 web searches in English and Chineseno report; the closest downstream items concern normalizer wildcard escaping and Japanese numbers
Related items (read)2026-10-2612 issues read in full#3098 (LUCENE-2023, open speed refactoring), #9234 (LUCENE-8186) and LUCENE-7606 (same defect class in CustomAnalyzer, fixed), #9070 (LUCENE-8022, closed as Invalid with a maintainer's answer to a user), #8489 (open, wildcard tokenization), #10453 and #10892 (Japanese width folding)

History

Introduced by LUCENE-7355 (7c2e7a0fb80a and e92a38af90d1, committed 2016-07-12), which added Analyzer#normalize with this lower-case-only override; first released in 6.2.0, unchanged since. Fails on every release run from 6.2.0 to 10.5.2 and on main. Before 7.0 the default classic parser already missed these queries through its own lower-casing.

Versions tested8 rows

Introduced by LUCENE-7355 (#8409, "Leverage MultiTermAwareComponent in query parsers"; commit subject "Add Analyzer#normalize() and use it in query parsers"; jpountz; a JIRA patch, no PR): 7c2e7a0fb80a on branch_6x (first released in 6.2.0) and e92a38af90d1 on master (first released in 7.0.0), both committed 2016-07-12. The same diff gave CJKAnalyzer and JapaneseAnalyzer width folding in normalization. git log -S on the override lists only this commit. The index-side folding is from 2009 (e01aad89fe2, LUCENE-1629) and unchanged except for reformatting.

The symptom is older than the defect in the default classic parser. Before 7.0 that parser lower-cased multi-term text itself (lowercaseExpandedTerms, String.toLowerCase, no width folding), so it missed full-width multi-term queries already on 5.5.5, 6.0.0 and 6.1.0. From 7.0.0, which removed that option and routes every multi-term term through Analyzer#normalize, this override is the cause: the parser probe passes on main with the fix. In 6.2.0 it is a real regression only for AnalyzingQueryParser and setAnalyzeRangeTerms(true), which ran the full chain, width folding included, before.

Versions and branches executed, with results
Version or commitVerdictResult
main ea57360912ed affectedthis doc's test 2 of 2 fail (gradle and the standalone runner, assertions on and off); QueryParser test 3 of 3; independent 5-test JUnit 4 of 5, control passes
10.5.2, 9.12.3 affectedthis doc's test 2 of 2, the QueryParser test 3 of 3 and the independent JUnit 4 of 5 fail
9.1.0, 10.0.0 affectedQueryParser test 3 of 3 fail
6.2.0, 6.6.6, 7.0.0, 7.7.3, 8.11.4, 9.0.0 affectednormalize-vs-index probe 3 of 4 fail; the independent JUnit 4 of 5 on 8.11.4
8.8.2, 8.9.0, 8.10.0, 8.10.1, 8.11.0, 8.11.1, 8.11.2, 8.11.3 affectednormalize-vs-index probe 3 of 4 fail (run on 2026-10-27 for this doc's checks; these releases are not tagged in the history clone)
other tags 6.2.0 to 10.5.2 read onlythe introducing commits are in every tag from 6.2.0 to 6.6.6, 7.0.0 to 8.8.1 and 9.0.0 to 10.5.2; the untagged 8.8.2 to 8.11.4 were executed
5.5.5, 6.0.0, 6.1.0 older parser symptomno Analyzer#normalize; the default classic parser misses full-width multi-term queries through its own lower-casing (4 of 5 fail), AnalyzingQueryParser finds them (6 of 6 pass on 5.5.5 and 6.1.0; not run on 6.0.0)
main + fix not affectedall repros pass; smartcn check task and 23 tests pass

Independent checks

An independent reproducer, a devil's advocate and an adjudication found it real and re-ran it on main with assertions on and off. Reviews by another model family (reading only) agreed on the root cause; one said "not real" only because it could not run code. An audit kept it and lowered the likelihood to rare.

Checks and verdicts412 words

Every verifier wrote or ran its own test against the stock jars; the cross-model reviews could only read code, and a separate run re-ran the finder's test for each (3 of 3 fail).

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateQueryParser JUnit fails 3 of 3 on main (assertions on and off), 10.5.2 and 9.12.3; the fix sketch passes 3 of 3 and the upstream TestSmartChineseAnalyzer 19 of 19.
cross-model review, refute lens not real (low), proceduralAn independent reviewer from another model family (OpenAI GPT-5.6, reading only) could not run code, so it voted not real on procedure only; it agreed with the root cause, judged the inputs in contract and rated the intended case moderate.
Independent reproducer realOwn 5-test JUnit fails 4 of 5 on main, 10.5.2, 9.12.3 and 8.11.4, control passes; with the fix 5 of 5 pass. Exhaustive BMP scan: 1,141 of 61,527 characters disagree, 0 with the fix.
Cross-model review, intended lens real (medium)OpenAI GPT-5.6, reading only: the intended-behaviour case is moderate and rejected for full-width Latin. A separate probe fails 2 of 3 on main; its CJKAnalyzer control passes.
Devil's advocate realRe-ran the repro on main; its probe shows CJKAnalyzer folds width in normalize and that full-width prefix and fuzzy queries miss documents in either width while the term query finds both; no intended-behaviour argument survives.
security assessment out of scope of SECURITY.mdDeny rules written as full-width parsed prefix, wildcard, regexp or fuzzy terms exclude nothing; 10 of 11 probe checks fail on main, 10.5.2 and 9.12.3, 11 of 11 pass with the fix.
adjudication confirmed (high)Re-ran the independent reproduction's test on main with assertions on and off (4 of 5 fail, control passes) and with the fix (5 of 5 pass).
registry re-run and audit downgrade (kept, likelihood rare)New seeds on main: the finder's test 3 of 3 fail; an own probe (202*, flexible Lu?en) fails 2 of 3 on main and 10.5.2, passes 3 of 3 with the fix.
confirmed; wording fixedAn independent fact check re-ran the gradle command verbatim (assertions on and off: 2 of 2 fail; with fix A the smartcn check task and 23 tests pass) and every standalone repro with new seeds; a second fact check by the other model family (reading only) found no error in the root cause. Their wording corrections are applied.

Sources and provenance

Finding record, Lucene and upstream sources and repro files1,691 words

Finding record

LFV-565, snapshot of 2026-11-06

Registry title
SmartChineseAnalyzer.normalize only lower-cases, but indexing folds full-width Latin letters and digits to half-width (in the tokenizer's SegTokenFilter), so full-width prefix, wildcard, regexp, range and fuzzy terms from the query parsers miss documents that the same word as a term query finds
Area
analysis-smartcn / language analyzers (SmartChineseAnalyzer normalization: Analyzer#normalize vs the width and case folding inside HMMChineseTokenizer's SegTokenFilter, used by the query parsers for multi-term queries)
Priority
Low, rank 302: Needs full-width Latin letters or digits in a multi-term query on a SmartChineseAnalyzer field, rare because common input methods produce half-width Latin.
Impact class
silent wrong results
Impact note
Every application that indexes Chinese or mixed Chinese-English text with the stock SmartChineseAnalyzer and accepts prefix, wildcard, regexp, range or fuzzy syntax through Lucene's classic, flexible or simple query parser (or calls Analyzer.normalize itself) silently gets no hits whenever the query term contains full-width Latin letters or digits (for example 'Luc*', 'Lu?e*', '[lua TO luz]' or '202*'), because the index holds only the half-width, lower-cased form and normalize keeps the full-width one; fuzzy queries can still hit when the edit distance absorbs the difference, single-letter multi-term terms with a non-ASCII capital (É*, Б*) disagree in the other direction, term and phrase queries and the indexed data are unaffected, no setting or scale is needed, every release from 6.2.0 to 10.6.0 (10.6.0 by reading) and main has it (the default classic parser shows the same miss even before, through its own pre-7.0 lower-casing), and by code reading two downstream search servers' smartcn plugins register the stock class unchanged.
Summary
You index the text '我们使用Lucene' (or '我们使用Lucene') with the stock SmartChineseAnalyzer and search with the classic QueryParser: the term query 'Lucene' and the half-width prefix 'Luc*' find the document, so you expect the full-width prefix 'Luc*' to find it too. Instead it finds 0 documents, with no error: the tokenizer's SegTokenFilter folds full-width Latin letters and digits to half-width and lower-cases A-Z, so the index holds 'lucen', but SmartChineseAnalyzer.normalize, which the query parsers apply to prefix, wildcard, regexp, range and fuzzy terms, is only a LowerCaseFilter, so the query term stays full-width ('luc*'). Likewise Analyzer.normalize returns 'abc' for 'ABC' and '123' for '123' (the index holds 'abc' and '123') and 'é' for 'É' (the index keeps 'É').
Introduced
7c2e7a0fb80a5bf733cf710aee6cbf01d02629eb on branch_6x (first released in 6.2.0) and its master twin e92a38af90d12e51390b4307ccbe0c24ac7b6b4e (first released in 7.0.0): LUCENE-7355 'Add Analyzer#normalize() and use it in query parsers' (jpountz; GitHub issue apache/lucene#8409, JIRA patch with 8 versions, no PR), which added SmartChineseAnalyzer.normalize(String, TokenStream) { return new LowerCaseFilter(in); } (main :137-140) while giving CJKAnalyzer and JapaneseAnalyzer CJKWidthFilter + LowerCaseFilter [read: git blame of :137-140 and git log -S 'normalize(String fieldName, TokenStream in)' --follow on the file list only this commit; the later commits c07df196664, 7f8b7ffbcad, 2971f311a2b and cc342ea7407 did not touch normalize]. The index-side folding it fails to mirror dates from 2009 (SegTokenFilter full-width folding e01aad89fe2, LUCENE-1629; the createComponents comment 'SegTokenFilter lowercases Basic Latin text' 5265dc1bb20) and is unchanged except for reformatting (2016-07-12 (commit date of both commits; author date 2016-06-28))
First affected version
6.2.0 [exec] (the normalize-contract probe fails 3 of 4 on 6.2.0 and does not compile on 6.1.0, which has no Analyzer#normalize). In 6.2.0 it is a regression for AnalyzingQueryParser and QueryParser.setAnalyzeRangeTerms(true), which ran the analyzer's full chain, width folding included, before (probe C passes 6 of 6 on 5.5.5 and 6.1.0 and fails 5 of 6 on 6.2.0 and 6.6.6) [exec]. The default classic parser misses full-width multi-term queries already on 5.5.5, 6.0.0 and 6.1.0 through its own lowercaseExpandedTerms (String.toLowerCase, no width folding), a limitation of that parser rather than this defect; from 7.0.0, which removed that option and routes every multi-term term through Analyzer#normalize, this override is the cause (probe B passes 5 of 5 on main with the fix shadowed) [exec]
Affected versions
Every release 6.2.0 to 10.6.0 (10.6.0, released 2026-10-29, by reading) and main ea57360912ed (unreleased 11.0). [exec] probe A (normalize vs the indexed token for 'ABC', '123' and 'É', with a half-width control) fails 3 of 4 on 6.2.0, 6.6.6, 7.0.0, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da) and passes 4 of 4 on main with the fix; probe B (default classic parser end to end: 'Luc*', 'Lu?en', 'Lucen~1', '[Lua TO Luz]', with half-width controls) fails 4 of 5 on 5.5.5, 6.0.0, 6.1.0, 6.2.0, 6.6.6, 7.0.0, 8.11.4, 9.12.3, 10.5.2 and main (-ea and -da) and passes 5 of 5 on main with the fix; the finder's JUnit fails 3 of 3 on 9.1.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da); the independent reproduction's JUnit fails 4 of 5 on main (-ea and -da), 10.5.2, 9.12.3 and 8.11.4. [read] by tag containment 7c2e7a0fb80 is in the 15 tags 6.2.0 to 6.6.6 and e92a38af90d in the 65 tags 7.0.0 to 8.8.1 and 9.0.0 to 10.5.2; 8.8.2 to 8.11.3 are not tagged in the history clone (between 8.8.1 [read] and 8.11.4 [exec]). Not fixed upstream: the override is unchanged on upstream main a95c754ee003 (2026-10-26) and, in the local clone, on origin/main a9779ef3c19 and the branch_10x and branch_10_6 tips; no open PR changes it. Not affected: 6.1.0 and earlier (no Analyzer#normalize; for the older classic-parser symptom see first_affected_version). Not executed: x86
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-26, read-only; the prior-report record verdict new, confidence high): 198 queries: a scripted prior-report search 160 (the finder's run 45 and three follow-up runs 115: GitHub issue, PR and commit queries on apache/lucene, downstream-repository queries on Elasticsearch, OpenSearch and Solr, mailing-list queries over dev@, java-user@, issues@ and commits@, JIRA-archive queries), 7 manual GitHub searches and open-PR lists, 6 JQL queries over LUCENE and SOLR (all 56 LUCENE and 16 SOLR smartcn issue titles read), 17 lists.apache.org queries (all distinct subjects read) and 8 web queries in English and Chinese; 12 issues read in full; the touched paths of all 406 open PRs; git history. 28 hits judged, none the same bug. Closest: #3098 (LUCENE-2023, open since 2009, an unmerged patch by rmuir that moves SegTokenFilter's lower-casing and full-width conversion into separate filters for speed, written before Analyzer#normalize existed and saying nothing about queries); #8409 (LUCENE-7355, which added the override; its 34 comments never mention smartcn or width folding). Same defect class in other code, fixed as bugs (precedents, not reports): #9234 (LUCENE-8186, CustomAnalyzer with LowerCaseTokenizerFactory) and LUCENE-7606 (CustomAnalyzer.normalize); #9070 (LUCENE-8022: a user's analyzer must override normalize). Unrelated: #8489 (LUCENE-7437, open, wildcard tokenization), #10453 and #10892 (LUCENE-9413, LUCENE-9853, Japanese width folding), the other smartcn issues (segmentation, stemming, surrogates, offsets, reset, stopwords). Not fixed: upstream main a95c754ee003 (2026-10-26) still returns new LowerCaseFilter(in); SmartChineseAnalyzer.java last changed 2022-01-03; the only open PRs that touch the smartcn module (#681, module-info lint; #202, WordDelimiterFilter removal) do not change normalize. Gaps: GitHub search reads only the first 50-100 results per query and the broad 'normalize' query only to its first page; web search was a general engine and Chinese-language forums were sampled, not covered; private reports to security@lucene.apache.org are not visible
Lucene commit tested
main ea57360912ed (commit of 2026-10-24; tested 2026-10-26) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the Maven Central jars 9.12.3 and 10.5.2 (the doc's test), 9.1.0 and 10.0.0 (the finder's test), 8.11.4 (the independent reproducer's test), and 5.5.5 to 10.5.2 for the history probes (8.8.2 to 8.11.3 added on 2026-10-27 for this doc's checks). Fix A (recommended) and fix B were validated in the scratch worktree with the smartcn module's check task.

Lucene and upstream sources

Finding record
  • lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/SmartChineseAnalyzer.java:47 (at ea57360912ed) ('@lucene.experimental'), :124-134 (createComponents: HMMChineseTokenizer -> PorterStemFilter -> [StopFilter]; :128 'LowerCaseFilter is not needed, as SegTokenFilter lowercases Basic Latin text'), :137-140 (normalize returns new LowerCaseFilter(in))
  • lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/hhmm/SegTokenFilter.java:22-37 (at ea57360912ed) (javadoc 'Full-width latin will be converted to half-width, then all latin will be lowercased'), :39-62 (FULLWIDTH_NUMBER and FULLWIDTH_STRING: chars >= U+FF10 minus 0xFEE0, then A-Z lower-cased; STRING: A-Z lower-cased); lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/Utility.java:140-158 (getCharType: U+FF21-U+FF3A and U+FF41-U+FF5A FULLWIDTH_LETTER, U+FF10-U+FF19 FULLWIDTH_DIGIT)
  • lucene/core/src/java/org/apache/lucene/analysis/Analyzer.java:203-213 (at ea57360912ed) (normalize(String, String): 'the representation that it would have in the index', 'without tokenizing or stemming'); lucene/analysis/common/src/java/org/apache/lucene/analysis/cjk/CJKAnalyzer.java:101-106 (normalize: CJKWidthFilter + LowerCaseFilter); lucene/analysis/kuromoji/src/java/org/apache/lucene/analysis/ja/JapaneseAnalyzer.java:119-133 (normalize LowerCaseFilter; initReaderForNormalization CJKWidthCharFilter); lucene/analysis/smartcn/src/test/org/apache/lucene/analysis/cn/smart/TestSmartChineseAnalyzer.java:160-170 (testFullWidth: 'Tests' -> 'test', '1234' -> '1234')
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:550, (at ea57360912ed) :556, :646, :656, :683, :712, :729 (Analyzer.normalize calls for range bounds, wildcard chunks, regexp, prefix, fuzzy; follow-up the security record), :611, :638, :667, :696 ('normal Analyzers are unlikely to work properly with wildcard templates', 'best effort'); lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParser.jj:183-187, :208-211 (only ASCII '*' and '?' form PREFIXTERM and WILDTERM; reading for the likelihood note)
  • https://github.com/apache/lucene/issues/8409 (LUCENE-7355, introduces Analyzer#normalize and this override), https://github.com/apache/lucene/issues/3098 (LUCENE-2023, closest design context), https://github.com/apache/lucene/issues/9234 (LUCENE-8186, precedent fixed as a bug), https://issues.apache.org/jira/browse/LUCENE-7606 (precedent), https://github.com/apache/lucene/issues/9070 (LUCENE-8022)
The finding in 30 seconds
  • Lucene main ea57360912ed: lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/SmartChineseAnalyzer.java:124-134 (chain), :127-128 (comment), :137-140 (normalize)
  • Lucene main ea57360912ed: lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/hhmm/SegTokenFilter.java:22-37 (javadoc), :39-62 (filter)
  • Lucene main ea57360912ed: lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/Utility.java:140-158 (getCharType)
  • Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/analysis/Analyzer.java:53-58 (class example), :203-212 (normalize javadoc)
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:550, :556, :646, :656, :683, :712, :729; lucene/queryparser/src/java/org/apache/lucene/queryparser/simple/SimpleQueryParser.java:573, :605; flexible/standard/processors/WildcardQueryNodeProcessor.java:66, :76, FuzzyQueryNodeProcessor.java:59, RegexpQueryNodeProcessor.java:43, TermRangeQueryNodeProcessor.java:138-139
  • Lucene main ea57360912ed: lucene/analysis/common/src/java/org/apache/lucene/analysis/cjk/CJKAnalyzer.java:101-106; lucene/analysis/kuromoji/src/java/org/apache/lucene/analysis/ja/JapaneseAnalyzer.java:130-133
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:547-557, QueryParser.jj:350-362 (open range bounds); flexible/standard/processors/TermRangeQueryNodeProcessor.java:100-139 (date bounds); simple/SimpleQueryParser.java:108-136 (operators)
  • Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/analysis/LowerCaseFilter.java:38; CharacterUtils.java:54-69
Who is affected
  • Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/analysis/AnalyzerWrapper.java:123-127
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParser.jj:183-187, :208-211
  • Lucene main ea57360912ed: lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/SmartChineseAnalyzer.java:33, :47
Why it looks unintended
  • Lucene main ea57360912ed: lucene/core/src/java/org/apache/lucene/analysis/Analyzer.java:53-58, :203-208
  • Lucene main ea57360912ed: lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/hhmm/SegTokenFilter.java:22-37; lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/SmartChineseAnalyzer.java:47, :128
  • Lucene main ea57360912ed: lucene/analysis/smartcn/src/test/org/apache/lucene/analysis/cn/smart/TestSmartChineseAnalyzer.java:160-170
  • Lucene main ea57360912ed: lucene/test-framework/src/java/org/apache/lucene/tests/analysis/BaseTokenStreamTestCase.java:1478
  • Lucene main ea57360912ed: lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:611, :638, :667, :696, :712
  • Lucene main ea57360912ed: lucene/analysis/common/src/java/org/apache/lucene/analysis/cjk/CJKAnalyzer.java:101-106
  • https://github.com/apache/lucene/issues/8409; https://github.com/apache/lucene/issues/9234; https://github.com/apache/lucene/issues/8657 (LUCENE-7606, fixed 6.4/7.0); https://github.com/apache/lucene/issues/9070 (LUCENE-8022, resolution Invalid)
Suggested fix
  • Lucene main ea57360912ed: lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/SmartChineseAnalyzer.java:50 (final), :137-140; lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/Utility.java:153-156
  • Lucene main ea57360912ed: lucene/analysis/common/src/java/org/apache/lucene/analysis/cjk/CJKWidthFilter.java:67-76

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

Repro files

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.