All findings · Lucene finding · LFV-653 · icu-default-rbbi-han-non-ideographic-status-zero

ICUTokenizer with cjkAsWords=false drops the Japanese iteration mark 々: 人々 is indexed as 人

With cjkAsWords=false, ICUTokenizer silently drops the Japanese iteration mark 々 (U+3005): 人々 ("people") is indexed as just 人, so 々 cannot be searched and 人々 can no longer be told apart from 人. Every release since 4.7.0 does this; one line in the break rules fixes it.

Summary

Priority
Low, rank 292Only the non-default cjkAsWords=false mode of the opt-in ICU tokenizer is affected, and same-analyzer search largely masks the precision loss.
Impact
silent wrong results
Since
4.7.0 (LUCENE-4381)introduced 2013-12-03 · over 12 years
Affected
4.7.0 to 10.5.2 and main
Status
documented
Found by
bounded-exhaustive check against StandardTokenizer

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

ICUTokenizer first splits text into script runs. With cjkAsWords=false, a Han run is cut into pieces by Lucene's own rules, Default.brk, and each piece gets a status number; pieces with status 0 (spaces, punctuation) are skipped, the others become tokens.

Summary of the bug in three panels. 1: ICUTokenizer with DefaultICUTokenizerConfig(false, true) and CJKBigramFilter, documents 人々 (people) and 人 (person); 々 (U+3005) repeats the kanji before it. 2: Default.rbbi line 95 takes Han characters out of the letter class and line 131 gives status 400 only to Ideographic characters, so 人 gets 400 (token) and 々 gets 0 (skipped), as for a space or a comma. 3: expected tokens 人, 々; actual 人 only; 様々な人々 gives 様, な, 人 and 佐々木 gives 佐, 木; with CJKBigramFilter, doc 0 人々 and doc 1 人 both get only the term 人, the same terms; TermQuery 人々 finds 0 documents and TermQuery 人 finds 2 (1 and 1 with the default mode or the fix); silent, no exception. A candidate fix at Default.rbbi:131 gives 1 and 1. Introduced by LUCENE-4381 in 4.7.0; 4.6.1 keeps 々; dropped on 19 releases from 4.7.0 to 10.5.2 and on main.
How 人々 is tokenized with cjkAsWords=false: 々 gets rule status 0 and is skipped, so 人々 is indexed as 人; the search counts that follow, and the one-line rule fix. Select the still to open it at full size. Generated with AI, checked against the code.
  1. Build ICUTokenizer with new DefaultICUTokenizerConfig(false, true) (or the icu factory with cjkAsWords="false"), the mode the module's own tests put in front of CJKBigramFilter. Analyze 人々 ("people"): the kanji 人 followed by 々, which repeats it.
  2. You expect [人, 々]. The javadoc promises "UAX#29 defaults" for this mode, and under UAX #29 々 is a letter. StandardTokenizer and Lucene 4.6.1 give [人, 々]; the default cjkAsWords=true gives [人々].
  3. You get [人]. 様々な人々 gives [様, な, 人] and 佐々木 gives [佐, 木]: every 々 is gone, with no error, with assertions on or off.
  4. Search changes too. With CJKBigramFilter, the documents 人々 and 人 are both indexed as the one term 人, so no search on the field can tell them apart and no term contains 々. With the default mode or the recommended fix, the 人々 document gets the bigram 人々; with <ALPHANUM> typing, as StandardTokenizer gives, it gets the term 々.
  5. Why: the rule file Default.rbbi takes every Han-script character out of its letter rule and gives a word status only to Ideographic ones. 々 is Han but not Ideographic, so its piece gets status 0, and ICUTokenizer skips status-0 pieces just as it skips punctuation.

Who is affected

Applications that tokenize Japanese with the ICU module's non-default cjkAsWords=false: DefaultICUTokenizerConfig(false, ...) in Java, or cjkAsWords="false" on the icu factory (for example in a Solr schema), usually to feed CJKBigramFilter. Every 々 in everyday words (人々, 時々, 様々) and names (佐々木) is lost. The default mode is not affected.

Mechanism, terms and numbers635 words

ICUTokenizer has a switch, cjkAsWords (default true). With cjkAsWords=false (new DefaultICUTokenizerConfig(false, true), or the icu factory with cjkAsWords="false"), CJK text is "segmented according to UAX#29 defaults" by Lucene's own break rules, Default.brk, instead of ICU's dictionary; the module's own tests use this mode, also in front of CJKBigramFilter. In this mode every U+3005 IDEOGRAPHIC ITERATION MARK vanishes: 人々 ("people") gives [人], 時々 ("sometimes") [時], 様々な人々 [様, な, 人], 佐々木 [佐, 木], where StandardTokenizer and Lucene 4.6.1 give [人, 々] and the default mode [人々]. The cause is Default.rbbi: it removes every Han-script character from the letter class and gives a status only to Ideographic characters; 々 is Han but not Ideographic, so its segment gets rule status 0, and ICUTokenizer skips status-0 segments like punctuation. The other two Han-script letters, U+303B and U+16FE3, are dropped the same way. Nothing throws; 々 is simply not in the index, and with CJKBigramFilter 人々 is indexed as the term 人. It came with LUCENE-4381 (4.7.0) and is in every release up to 10.5.2 and on main; a one-line rule change, with Default.brk regenerated, fixes it.

Terms

  • cjkAsWords (default true): with true, Han, Hiragana and Katakana text goes to ICU's own word iterator, which uses a dictionary and keeps 人々 as one word. With false, CJK text is "segmented according to UAX#29 defaults" (DefaultICUTokenizerConfig.java:94-96) by Lucene's rule file Default.rbbi, compiled to Default.brk. That is the mode for CJKBigramFilter users: the module's own TestWithCJKBigramFilter uses it.
  • Rule status: ICU break rules give each piece of text a number. In Default.rbbi, letters get 200, kana 300 and ideographs 400 (Default.rbbi:125-131); pieces that no status rule matches get 0, ICU's "none" status for spaces and punctuation.
  • 々 (U+3005): the Japanese iteration mark; it repeats the kanji before it (人々 = 人 + 人, "people"). It is Script=Han, Ideographic=No and Word_Break=ALetter, a letter under UAX #29.

The bug

  1. Default.rbbi defines $Han = [:Han:] (:54), puts it into $dictionaryCJK (:88-89) and removes $dictionaryCJK from the letter class: $ALetterPlus = [$ALetter-$dictionaryCJK ...] (:95). So no Han-script character gets the letter status 200.
  2. The only status rule left for Han is $Ideographic $ExFm* {400}; (:131): Lucene's tailoring removed ICU's rule that chains Han and kana (header :23). 人 is Ideographic and gets 400. 々 is not, so its piece gets status 0. The Han exclusion and this status rule are ICU's own word.txt text; in ICU, the final rule $KanaKanji $KanaKanji {400} and the dictionary keep a 々 after a kanji inside a status-400 word, and Lucene's mode removes both.
  3. ICUTokenizer.incrementTokenBuffer skips status-0 pieces (ICUTokenizer.java:209-214), the loop that skips spaces and punctuation. 々 is dropped exactly like a comma: in 486 of 486 tested contexts the tokens equal those of the same text with 、 in its place.
  4. The boundaries are right (人 | 々, as UAX #29 says); only the status is wrong.

The numbers

人々 -> [人]; 時々 -> [時]; 様々な人々 -> [様, な, 人]; 佐々木さん -> [佐, 木, さ, ん]; 人〻 (U+303B) and 人𖿣 (U+16FE3) -> [人]. StandardTokenizer gives [人, 々], the default mode [人々]. With CJKBigramFilter, 人々 gives the unigram [人] instead of the bigram [人々], and 様々な人々 gives [様, な人] instead of [様々, 々な, な人, 人々]. Indexing the documents 人々 and 人 through that chain leaves one term, 人 (df=2), for both documents: TermQuery 人々 finds 0 documents, 人 finds 2 and 々 finds 0. With the default mode or the recommended fix the 人々 document gets the bigram 人々 (1 and 1); with <ALPHANUM> typing, as StandardTokenizer gives, it gets the term 々 (人々 0, 人 2, 々 1).

Exposure and likelihood274 words
  • Who is exposed. Users of the opt-in analysis-icu module who choose cjkAsWords=false: new ICUTokenizer(new DefaultICUTokenizerConfig(false, ...)), or ICUTokenizerFactory (SPI name icu) with cjkAsWords="false" (the factory default is true, ICUTokenizerFactory.java:96). A maintainer described this as a supported setup on #13239: "the user can configure false here and use CJKBigramFilter if they prefer an n-gram approach". ICUTokenizer and DefaultICUTokenizerConfig are @lucene.experimental (the factory is not). All of this is with the stock rules: a factory rulefiles entry for the Han script replaces Default.brk for Han text (ICUTokenizerFactory.java:54-66, :86-95, :120-131).
  • Not exposed. The default ICUTokenizer() and the factory default (cjkAsWords=true). Elasticsearch and OpenSearch hard-code cjkAsWords=true in their icu_tokenizer.
  • What triggers it. Ordinary Japanese text: 々 is in common words (人々, 時々, 様々, 色々, 我々, 日々) and names (佐々木). Deterministic: every occurrence, every document, assertions on or off.
  • Effect. 々 is never indexed and cannot be searched. 人々 is indexed as 人: with CJKBigramFilter, the 人々 and 人 documents get the same single term 人, so no search on the field can tell them apart; a query for 人々 analysed the same way becomes 人, so it still finds the 人々 document but also every document with an isolated 人 (lower precision). Bigrams across the mark are lost.
  • Not affected. Other tokens and their offsets, index integrity (CheckIndex clean over 3,000 documents), cost (linear, the same order of growth as punctuation and no amplification; all-々 input was about 40% slower than all-、 input in the recorded run). Existing indexes keep the missing terms until reindexed.
  • Scale. Every release from 4.7.0 (2014) to 10.5.2, and main.

Minimal repro

The test shows that ICUTokenizer with new DefaultICUTokenizerConfig(false, true) (cjkAsWords=false) drops the Japanese iteration mark 々 (U+3005, a UAX #29 letter) from 人々, because Default.rbbi gives its segment rule status 0.

TestICUTokenizerIterationMark.java (JUnit test)42 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.icu.segmentation;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestICUTokenizerIterationMark extends LuceneTestCase {

  public void testIterationMarkKeptWithCjkAsWordsFalse() throws Exception {
    List<String> terms = new ArrayList<>();
    try (Tokenizer t = new ICUTokenizer(new DefaultICUTokenizerConfig(false, true))) {
      t.setReader(new StringReader("人々"));
      CharTermAttribute term = t.addAttribute(CharTermAttribute.class);
      t.reset();
      while (t.incrementToken()) {
        terms.add(term.toString());
      }
      t.end();
    }
    // expected [人, 々] (U+3005 is a UAX #29 letter, kept by StandardTokenizer); actual [人]
    assertEquals(List.of("人", "々"), terms);
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-653/, 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/icu/build/libs/lucene-analysis-icu-$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.icu.segmentation.TestICUTokenizerIterationMark
# the module(s) analysis-icu also need their third-party runtime jars
Expected vs actual
Expected tokens [人, 々] (as StandardTokenizer gives); actual [人] (expected:<[人, 々]> but was:<[人]>), with -ea and -da.

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

More files, environment, full output35 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:<[人, 々]> but was:<[人]>
       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.analysis.icu.segmentation.TestICUTokenizerIterationMark.testIterationMarkKeptWithCjkAsWordsFalse(TestICUTokenizerIterationMark.java:40)

Why it looks unintended

  • The javadoc says cjkAsWords=false segments text "according to UAX#29 defaults" (DefaultICUTokenizerConfig.java:94-96); under UAX #29, 々 is a letter.
  • Every reference keeps 々: StandardTokenizer, ICU's root iterator, the default mode, and Lucene 4.6.1.
  • The rule file's header lists two tailorings, neither about Han letters; no test, CHANGES entry or discussion found accepts the loss.
The full argument400 words
  • The contract. "@param cjkAsWords true if cjk text should undergo dictionary-based segmentation, otherwise text will be segmented according to UAX#29 defaults. If this is true, all Han+Hiragana+Katakana words will be tagged as IDEOGRAPHIC." (DefaultICUTokenizerConfig.java:94-96). The class javadoc: "Generally tokenizes Unicode text according to UAX#29 (BreakIterator.getWordInstance(ULocale.ROOT))" with a dictionary as the only CJK tailoring (:30-36); next to Default.brk: "the same as ROOT, except no dictionary segmentation for cjk" (:80). ICUTokenizer: "Breaks text into words according to UAX #29" (ICUTokenizer.java:30-32). Under UAX #29, U+3005 is Word_Break=ALetter, a letter. UAX #29 sets the boundary and the letter class, not ICU's rule statuses or which segments a tokenizer emits; ICUTokenizer drops only status-0 segments, ICU's status for spaces and punctuation.
  • The tailorings on record. The Default.rbbi header names two: "Prevent dictionary break: disable final rule that would chain han and kana" and "For kana rules, change from 400 to 300 status" (:23-24). A diff against ICU 77.1's word.txt shows exactly those two changes. Neither says Han-script letters stop being tokens.
  • ICU's convention. Status 0 is ICU's "none" status, documented for spaces and most punctuation; 々 is gc=Lm, a letter.
  • Every reference keeps it. StandardTokenizer ([人, 々]), ICU's root word iterator (人々 as one status-400 word), the default cjkAsWords=true ([人々]), and Lucene 4.6.1 ([人, 々]).
  • History. LUCENE-4381 (4.7.0) added cjkAsWords, changed the letter class to [$ALetter-$dictionaryCJK ...], and moved the module's existing tests to DefaultICUTokenizerConfig(false), which suggests that false was meant to keep the earlier UAX #29 behaviour (the commit message does not say so). Its only CHANGES entry is "Upgrade analysis/icu to 52.1" under Build. With the ICU version held fixed, the 4.6.1 rules give 々 status 200 and the 4.7.0 rules 0.
  • Maintainer on record. #13239 (2024): Default.rbbi "is used only when the user overrides defaults to disable ICU's dictionary-based CJK segmentation", quoting the "UAX#29 defaults" javadoc, with cjkAsWords=false plus CJKBigramFilter as the use case.
  • Tests. TestICUTokenizer (main analyzer at :78, used by testJapanese at :436) and TestWithCJKBigramFilter (:49) run in this mode. No test in analysis/icu contains 々; the only tests that do are in kuromoji (the iteration-mark char filter tests, TestJapaneseCompletionFilter, TestJapaneseAnalyzer).
  • Outside Lucene. Unicode CLDR decided in 2009 that 々 should behave as an ideograph for Japanese word break (CLDR-2009).
The devil's-advocate check258 words

The intended-behaviour checker and the cross-model lenses built the strongest case for "works as designed": Lucene only removed ICU's chain rule, and status 0 is what ICU's grammar then gives; UAX #29 defines boundaries, not which pieces are words, and the boundary is right; status-0 pieces are skipped on purpose; the broad Han exclusion survived every later rule change and is ICU's own grammar text; a TODO calls cjkAsWords "super-expert stuff" (DefaultICUTokenizerConfig.java:86); the classes are @lucene.experimental; ICU root itself drops a lone 々; kuromoji's JapaneseIterationMarkCharFilter can expand 々 first.

Why it fails: the javadoc promises UAX #29 defaults and 々 is a letter there; status 0 is ICU's status for spaces and punctuation, and ICUTokenizer turns it into a deletion. Every reference keeps the character, including the 4.6.1 behaviour that the test migration suggests cjkAsWords=false was meant to keep. No javadoc, CHANGES entry, test, commit message or discussion found accepts the loss. The lone-々 point holds only for the default mode (ICU root's own behaviour), which is not claimed here; with cjkAsWords=false even a lone 々 is in scope. The char filter is a partial workaround, not a contract. Not settled by this: the recommended fix is itself a tailoring (CLDR-2009's "treat 々 as ideograph"), and a maintainer may call this a long-standing limitation of an experimental mode. Verdict: not intended (the cross-model reviews rated the works-as-designed case weak and the independent reproduction, the intended-behaviour check and the adjudication found the bug real; the later cross-model fact check of this doc rated that case moderate).

Suggested fix

Recommended Give the Han-script letters the ideographic status (N)

Give the three Han-script letters the ideographic status: at Default.rbbi:131 use [$Ideographic [$Han & $ALetter]] $ExFm* {400}; and regenerate Default.brk with regenerateRbbi. Validated: the new test passes, and so do :lucene:analysis:icu:check and the module's 148 upstream tests that run, with assertions on and off.

--- a/lucene/analysis/icu/src/data/uax29/Default.rbbi
+++ b/lucene/analysis/icu/src/data/uax29/Default.rbbi
@@ -128,7 +128,7 @@ $HangulSyllable {200};
 $Hebrew_Letter $ExFm* {200};
 $Katakana $ExFm* {300};             # note:  these status values override those from rule 5
 $Hiragana $ExFm* {300};             #        by virtue of being numerically larger.
-$Ideographic $ExFm* {400};          #
+[$Ideographic [$Han & $ALetter]] $ExFm* {400};  # Han letters such as U+3005 are not Ideographic
 
 #
 # rule 5
All options and trade-offs4 options · 707 words

One fix was validated with gradle in a scratch worktree at a9779ef3c193, with Default.brk regenerated by the build's own task. A broader variant was validated earlier by compiling the rules at class init; a third variant was only probed. Default.brk is generated: change Default.rbbi, then run ./gradlew :lucene:analysis:icu:regenerateRbbi, which also updates the checksum file that check verifies and runs applyGoogleJavaFormat on the module's sources.

Give the Han-script letters the ideographic status (N) (validated by the checks) (recommended)

One line in Default.rbbi (:131), then regenerateRbbi. 々, 〻 and 𖿣 become <IDEOGRAPHIC> tokens, as the default mode types them ("all Han+Hiragana+Katakana words will be tagged as IDEOGRAPHIC", DefaultICUTokenizerConfig.java:95-96) and in line with CLDR-2009, so CJKBigramFilter forms 人々, 様々 and 佐々. Of the 335 Han-script code points that are not Ideographic, it changes exactly these 3 letters; the 330 radicals and 2 marks stay as on main. Boundaries do not change.

--- a/lucene/analysis/icu/src/data/uax29/Default.rbbi
+++ b/lucene/analysis/icu/src/data/uax29/Default.rbbi
@@ -128,7 +128,7 @@ $HangulSyllable {200};
 $Hebrew_Letter $ExFm* {200};
 $Katakana $ExFm* {300};             # note:  these status values override those from rule 5
 $Hiragana $ExFm* {300};             #        by virtue of being numerically larger.
-$Ideographic $ExFm* {400};          #
+[$Ideographic [$Han & $ALetter]] $ExFm* {400};  # Han letters such as U+3005 are not Ideographic
 
 #
 # rule 5

Trade-offs

  • Changes analysis output for cjkAsWords=false users (new tokens 々, new bigrams): needs a CHANGES line and a reindex note.
  • With cjkAsWords=true, Han-script text goes to ICU's root iterator, not Default.brk, so the default mode should not change (from the code; the cjkAsWords=true suite is @AwaitsFix and was not run).
  • Not plain UAX #29: each Han letter gets its own single-character rule, so 々々 (as in 部分々々) gives [々, 々], where StandardTokenizer gives one <ALPHANUM> token [々々] (fix B splits it too). The two Han Word_Break=Extend marks (U+16FF0, U+16FF1) stay outside every token, as on main and as with StandardTokenizer. It is a tailoring in the direction of CLDR-2009.

What the checks validated

  • Gradle, JDK 25, scratch worktree at a9779ef3c193: on stock the new test fails 4 of 5; regenerateRbbi rewrote Default.brk (22,792 to 22,808 bytes) and the checksum file, and left MyanmarSyllable.brk unchanged.
  • :lucene:analysis:icu:check passes: the checksum check, checkGoogleJavaFormat, ecjLint, forbiddenApis, checkLicenses and all module tests: 17 suites, 163 tests, 0 failed (the 5 new tests and 148 upstream tests; the 10 tests of TestICUTokenizerCJK are @AwaitsFix).
  • The module tests pass with assertions off and in nightly mode (163 tests, 0 failed each).
  • The finder's and the independent reproducer's tests pass with the fix (16 of 16 with the new test).
  • The regenerated Default.brk gives the same tokens as the rules compiled at runtime.

Give every Han-script character the ideographic status (A, the finder's sketch)

[$Ideographic $Han] $ExFm* {400}; at Default.rbbi:131. Same result for the three letters; the finder's run passed its repro (4 of 4), TestICUTokenizer (45) and TestWithCJKBigramFilter (12) with the rules compiled at class init. The diagram shows this variant; N gives the same tokens and counts for every input in it.

Trade-offs

Also turns the 330 Word_Break=Other radicals and the 2 Han marks into <IDEOGRAPHIC> tokens (332 of 332 kept after 人 in the probe), which goes beyond UAX #29 and beyond what 4.6.1 did.

Give the Han-script letters the letter status (B)

Add [$Han & $ALetter] $ExFm* {200}; after Default.rbbi:131. 々 becomes <ALPHANUM>, as in StandardTokenizer and 4.6.1; the radicals stay as on main. Probed with rules compiled at runtime; not run in gradle.

Trade-offs

CJKBigramFilter passes <ALPHANUM> tokens through, so 人々 gives [人, 々] rather than the bigram 人々; the new test's testBigramFilter fails with B by design.

Workarounds until a fix ships

Use the default cjkAsWords=true, which keeps 人々 as one word; or put kuromoji's JapaneseIterationMarkCharFilter in front of the tokenizer, in both the index and the query analyzer: it expands a 々 that follows a kanji (人々 -> 人人, tokens [人, 人]).

Trade-offs

The first changes CJK segmentation as a whole. The second needs the kuromoji module and is partial: it passes a lone or leading 々 through unchanged (JapaneseIterationMarkCharFilter.java:28-34), which the tokenizer still drops (々 -> [], 々人 -> [人]), and it does not handle U+303B or U+16FE3 (executed).

Downstream note

Elasticsearch and OpenSearch hard-code cjkAsWords=true in their icu_tokenizer, so their users cannot reach this mode; Solr exposes cjkAsWords on ICUTokenizerFactory, so a Solr schema can.

How it was found

Semi-formal: a bounded-exhaustive check ran the stock ICUTokenizer on 2,593,337 short strings in both cjkAsWords modes against one oracle: every letter that StandardTokenizer puts in a token must be inside some ICUTokenizer token. It flagged 々; an AI tool then traced it to the Default.rbbi status rule and wrote the JUnit.

The method in full197 words

Method: bounded-exhaustive translation validation (the hunt's "semi-formal" tier), hunt unit "ICU script iterator and ICUTokenizer against reference semantics", 2026-10-27, on main a9779ef3c193. No model checker, SMT solver or proof produced the signal.

  • A harness ran the stock ICUTokenizer on every string over a 35-character alphabet up to length 4 and a 16-character alphabet at length 5: 2,593,337 strings x 2 configurations (cjkAsWords true and false), 25.9 million checks, against reference semantics for the script iterator and the token loop (0 mismatches there).
  • A "content dropped" oracle compared it with StandardTokenizer, the core UAX #29 tokenizer: every letter or digit StandardTokenizer puts in a token must lie inside some ICUTokenizer token. It flagged three dropped-content classes; U+3005 was one, the other two became separate rows (a word joined to an emoji by ZWJ, and Hangul followed by a combining mark).
  • An AI tool listed the 335 Han-script code points that are not Ideographic with ICU's UnicodeSet, separated the cjkAsWords=false defect from ICU root's own handling of a lone 々 in the default mode, found the status rule in Default.rbbi, and wrote the public-API JUnit. Independent AI tools then reproduced and judged it (see the checks).

Prior reports, history and checks

Prior reports

No prior report: five prior-report tool runs (766 hits, 0 high), 32 logged manual GitHub search entries, the files of all 409 open PRs, JIRA, five mailing lists and 9 web queries in English and Japanese (2026-10-27). Closest: kuromoji's iteration-mark char filter (#5273), the introducing change (#5447) and Unicode's CLDR-2009.

Searches5 searches

No issue, PR, JIRA entry, list thread or web page reports ICUTokenizer dropping 々 (or U+303B, U+16FE3) with cjkAsWords=false, or Default.rbbi leaving Han-script letters with status 0. Searches are keyword-based (GitHub tokenizes single CJK characters loosely, so exact phrases were also run), JIRA attachments are not searched, and private reports are not visible, so this is "no report found". The upstream re-check read the main head, the newest ICU-module commit and the open PRs; its issue searches were rate-limited, so the open-issue state rests on the earlier searches.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene)2026-10-27five runs of the hunt's prior-report tool (three of 51 queries each, plus 46 and 50): identifiers (Default.rbbi, dictionaryCJK, ALetterPlus, cjkAsWords, LUCENE-4381), symptoms (々, 人々, iteration mark, odoriji, character dropped) and the wider character set; 766 distinct hits, every ICU, CJK and Japanese title triagedno report; #5447 (LUCENE-4381) is the introducing change and none of its comments mentions Han non-ideographs; #13239 holds the maintainer comment on cjkAsWords=false + CJKBigramFilter
GitHub, manual (apache/lucene and downstream)2026-10-2732 logged search entries (several bundle more than one search) in apache/lucene, Elasticsearch, OpenSearch, Solr, Lucene.NET, ICU4N, unicode-org/icu and global code searchno report; only #5273 (LUCENE-4201, the kuromoji iteration-mark char filter, a feature) mentions 々
Open PRs2026-10-27the changed files of all 409 open apache/lucene PRs; re-checkednone touches Default.rbbi, Default.brk, DefaultICUTokenizerConfig or ICUTokenizer (#15825 edits CJKBigramFilter positions only)
JIRA and mailing lists2026-10-27LUCENE and SOLR JIRA, the Unicode ICU and CLDR trackers; java-user@, dev@, issues@, solr-user@ and users@solrno report; CLDR-2009 (2009) decided that 々 should behave as an ideograph for Japanese word break, which supports the expected behaviour; ICU-10341 is an ICU-side dictionary split
Web2026-10-279 queries in English and Japaneseno blog, Q&A or forum report (one query was throttled)

History

A regression from LUCENE-4381 (#5447, the ICU 52.1 upgrade, committed 2013-12-03), first released in 4.7.0. 4.6.1 keeps 々; 19 releases from 4.7.0 to 10.5.2 and main drop it. With the ICU version held fixed, the rule change alone flips 々 from kept to dropped.

Versions tested6 rows

Introduced by LUCENE-4381, "upgrade ICU to icu4j 52.1" (JIRA LUCENE-4381, migrated as #5447; a direct SVN commit with the patch on the JIRA issue, no PR): trunk b5dbac5e3582 and branch_4x 2924e5286330 (the copy that shipped in 4.7.0), both 2013-12-03. It added cjkAsWords and changed the letter class from [$ALetter [$dictionary-$Extend-$Control]] to [$ALetter-$dictionaryCJK [$ComplexContext-$Extend-$Control]] with all of [:Han:] in $dictionaryCJK; it also moved the module's tests to DefaultICUTokenizerConfig(false). Its only CHANGES entry is "Upgrade analysis/icu to 52.1" under Build.

The rules are the cause, not the ICU upgrade: with ICU4J held at 52.1 and at 78.3, the 4.6.1 rules give 々 status 200 and the 4.7.0 rules give 0.

Never fixed: later Default.rbbi changes (7.4.0, 7.5.0, #13239 in 9.11.0/10.0.0, #14386 in 10.2.0) and every Default.brk rebuild since kept the line; on 2026-10-27 no commit after a9779ef3c193 touches lucene/analysis/icu (upstream main a95c754ee003).

Coverage: 19 releases and main were executed; the releases in between are affected by history (every release tag in the history clone contains the change and the line is unchanged), not by a run. The clone's 8.x tags stop at 8.8.1, so 8.8.2 and 8.9.0 to 8.11.4 were not checked by tag; the 8.11.4 jar was executed.

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthe brief's test 4 of 5 fail (gradle and the standalone runner, assertions on and off); the finder's 3 of 4; the independent reproducer's 4 of 7
10.5.2, 9.12.3 affectedthe brief's test 4 of 5 fail; the finder's 3 of 4; the independent reproducer's 4 of 7
4.7.0 to 10.2.0 (17 releases: 4.7.0, 4.10.4, 5.0.0, 5.5.5, 6.6.6, 7.2.1, 7.3.0, 7.4.0, 7.5.0, 7.7.3, 8.11.4, 9.0.0, 9.10.0, 9.11.0, 10.0.0, 10.1.0, 10.2.0) history probethe version-neutral probe drops 々 (人々 -> [人]); assertions on
4.6.1 last beforekeeps 々: 人々 -> [人 <IDEOGRAPHIC>, 々 <ALPHANUM>] (no cjkAsWords option yet; its only config is the UAX #29 behaviour that false replaced)
4.6.1 rules vs 4.7.0 rules on ICU4J 52.1 and 78.3 rule isolation4.6.1 rules: 々 status 200 (kept); 4.7.0 rules: status 0 (dropped)
fix N (scratch worktree) not affectedthe brief's test 5 of 5; :lucene:analysis:icu:check passes; 163 module tests pass with assertions on, off and in nightly mode

Independent checks

An independent reproducer, a devil's advocate, an adjudication and a later audit confirmed it with their own runs on main, assertions on and off. Two executed reviews by another model family (OpenAI GPT-5.6) found it real with high confidence and rated the works-as-designed case weak.

Checks and verdicts397 words

Every verifier wrote or ran its own tests against the stock jars; the cross-model reviews executed their own tests too (on ea57360912ed, whose analysis/icu is identical).

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateJUnit fails 3 of 4 on main (assertions on and off), 10.5.2 and 9.12.3; main() repros fail on 8.11.4, 6.6.6 and 4.7.0; passes 4 of 4 with fix A, and TestICUTokenizer (45) and TestWithCJKBigramFilter (12) pass.
cross-model review, refute lens real (high)An independent reviewer from another model family (OpenAI GPT-5.6), executed mode: its own test fails (expected [人, 々] but was [人]) with two more seeds; status probe 人:400, 々:0; the fix passes its test and the 57 upstream tests; intended case weak.
Independent reproducer realOwn 7-test JUnit from the claim: 4 of 7 fail on main (on and off), 10.5.2 and 9.12.3; all three Han-script letters dropped; controls pass; the boundaries match UAX #29 and only the status is wrong.
Cross-model review, intended lens real (high)OpenAI GPT-5.6, executed mode: its own test confirms 人々 -> [人] and status WORD_NONE; intended case weak. A separate run re-ran the finder's test on a9779ef3c193: 3 of 4 fail, 4 of 4 pass with the fix.
Devil's advocate realRan the real 4.6.1 and 4.7.0 jars (4.6.1 keeps 々), diffed Default.rbbi against ICU 77.1 (only the two documented tailorings), counted the 335 code points; limited the claim to the 3 Han-script letters.
adjudication confirmed (high)Re-ran the finder's test (3 of 4 fail) and the independent reproduction's (4 of 7 fail) with new seeds.
registry re-runs and audit keepRe-runs with new seeds fail as before and pass with the fix; the audit's own test (佐々木 -> [佐, 木]) fails 2 of 5, assertions on and off.
this brief fix validatedNew test fails 4 of 5 on main (gradle and standalone, on and off), 10.5.2 and 9.12.3; fix N passes it, :lucene:analysis:icu:check and the module tests.
checks of this doc corrections appliedAn independent fact check, a cross-model fact check (OpenAI GPT-5.6) and a diagram check found text errors only (a test-format claim, a test line number, an over-broad test claim, the search-count framing, the char-filter workaround, a search window); all are fixed, with re-runs, and the cross-model check rated the works-as-designed case moderate.

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

Finding record

LFV-653, snapshot of 2026-11-06

Registry title
ICUTokenizer with cjkAsWords=false silently drops the Japanese iteration mark 々 (U+3005), as in 人々, 時々 and 様々, and the other Han-script letters that are not Ideographic (U+303B, U+16FE3), because Default.rbbi gives them rule status 0
Area
analysis / ICU (DefaultICUTokenizerConfig break rules Default.rbbi / Default.brk, used for CJK text when cjkAsWords=false; ICUTokenizerFactory 'icu' with cjkAsWords="false")
Priority
Low, rank 292: Only the non-default cjkAsWords=false mode of the opt-in ICU tokenizer is affected, and same-analyzer search largely masks the precision loss.
Impact class
silent wrong results
Impact note
Applications that tokenize Japanese text with ICUTokenizer in its non-default cjkAsWords=false mode (new DefaultICUTokenizerConfig(false, ...), or ICUTokenizerFactory cjkAsWords="false", for example in a Solr schema; this is the setup the module's own TestWithCJKBigramFilter uses to feed CJKBigramFilter) lose every 々 from the token stream (after a kanji as in 人々, and in every other context probed: alone, doubled, before a kanji or a Latin word), deterministically and silently, with -ea and -da, at any size, in every release from 4.7.0 to 10.5.2: 人々 is indexed and queried as 人 and 佐々木 as 佐/木, so 々 cannot be searched and term and phrase matching on such words loses precision (a same-analyzer query for 人々 still finds 人々 but also matches an isolated 人); the default cjkAsWords=true mode and the Elasticsearch and OpenSearch icu_tokenizer (which hard-code true) are not affected, and nothing throws or damages the index.
Summary
You build ICUTokenizer with cjkAsWords=false, whose javadoc promises that text 'will be segmented according to UAX#29 defaults', and index Japanese text such as 人々 ('people'), 時々 ('sometimes') or 様々な人々; you expect 々, a Word_Break=ALetter letter under UAX #29, to end up in a token, as it does with StandardTokenizer, with the default cjkAsWords=true mode and with Lucene 4.6.1. Instead Default.rbbi takes every Han-script character out of its letter rule and gives a status only to Ideographic characters, and 々 is Han but not Ideographic, so its segment gets rule status 0 and ICUTokenizer skips it like punctuation: 人々 -> [人], 様々な人々 -> [様, な, 人], and with CJKBigramFilter 人々 becomes the unigram 人. Nothing throws; the character is simply missing from the index.
Introduced
b5dbac5e3582a1cfd59af34c1a41f88aed203da1 (trunk, svn r1547502) and its branch_4x copy 2924e528633022f2c68bbdd1db25b49bb13af4cb (svn r1547561, the copy that shipped in 4.7.0), both 'LUCENE-4381: upgrade ICU to icu4j 52.1' by GitHub rmuir (JIRA LUCENE-4381, migrated as apache/lucene#5447; direct SVN commit with the patch on the JIRA issue, no PR). The commit added DefaultICUTokenizerConfig(boolean cjkAsWords) and changed Default.rbbi from '$ALetterPlus = [$ALetter [$dictionary-$Extend-$Control]]' to '$ALetterPlus = [$ALetter-$dictionaryCJK [$ComplexContext-$Extend-$Control]]' with all of [:Han:] in $dictionaryCJK [read: git blame at 7cecf795236 attributes Default.rbbi:95 to b5dbac5e358; exec: with ICU4J held at 52.1 and at 78.3, the 4.6.1 rules give U+3005 status 200 and the 4.7.0 rules give it status 0, so the rule text causes it, not the ICU upgrade]. Later rule regenerations (LUCENE-8122/8175 in 7.4.0, LUCENE-8366 in 7.5.0, LUCENE-9773 in 9.0.0, #13239 in 9.11.0/10.0.0, #14386 in 10.2.0) kept the line (2013-12-03)
First affected version
4.7.0 [exec] (4.6.1, the last release before it, keeps U+3005 as <ALPHANUM> [exec]; 4.6.1 has no cjkAsWords option, and its only config, DefaultICUTokenizerConfig(), is the UAX #29 behaviour that cjkAsWords=false replaced: the introducing commit moved the module's existing tests to DefaultICUTokenizerConfig(false))
Affected versions
every release from 4.7.0 to 10.5.2 (the last release before 10.6.0; the latest 9.x is 9.12.3) and main a9779ef3c193; never fixed. [exec] the follow-up's version-independent main() probe drops U+3005 on 4.7.0, 4.10.4, 5.0.0, 5.5.5, 6.6.6, 7.2.1, 7.3.0, 7.4.0, 7.5.0, 7.7.3, 8.11.4, 9.0.0, 9.10.0, 9.11.0, 9.12.3, 10.0.0, 10.1.0, 10.2.0, 10.5.2 and main (-ea and -da) and keeps it on 4.6.1; the finder's JUnit fails 3 of 4 on main (-ea and -da), 10.5.2 and 9.12.3; the independent reproduction's JUnit fails 4 of 7 on main (-ea and -da), 10.5.2 and 9.12.3. [read] every release tag from 4.7.0 to 10.5.2 in the history clones contains one of the two introducing commits, and the line is unchanged in each line's latest release. Not affected: 4.6.1 and earlier. Not fixed upstream: no commit after a9779ef3c193 touches lucene/analysis/icu (upstream main a95c754ee003, 2026-10-26), Default.rbbi last changed with #14386 (2025-03, an unrelated sync), and no open PR touches Default.rbbi, Default.brk, DefaultICUTokenizerConfig or ICUTokenizer, so the unreleased 10.x and 11.0 lines are affected (main executed).
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal: exhaustive check
Upstream and prior reports
none found (searched 2026-10-27, plus the finder's and the intended-behaviour check's runs earlier that night, read-only; the prior-report record, verdict new): five hunt prior-report tool runs (three follow-up runs of 51 queries each plus the finder's 46 and the intended-behaviour check's 50, over apache/lucene GitHub issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive; 766 distinct hits, 0 high), 32 manual GitHub searches (apache/lucene, Elasticsearch, OpenSearch, Solr, Lucene.NET, ICU4N, unicode-org/icu and global code search, for 々, 'iteration mark', 'U+3005', cjkAsWords, Default.rbbi, dictionaryCJK, LUCENE-4381 and ICUTokenizer with Japanese, Han, Kangxi, radical, missing or dropped), the changed files of all 409 open apache/lucene PRs (none touches Default.rbbi, Default.brk, DefaultICUTokenizerConfig or ICUTokenizer), JIRA (LUCENE, SOLR, and the Unicode ICU and CLDR trackers), the java-user@, dev@, issues@, solr-user@ and users@solr lists, and 9 web queries in English and Japanese. Not fixed upstream: no commit after a9779ef3c193 touches lucene/analysis/icu (upstream main a95c754ee003), and no commit since LUCENE-4381 has changed the Han status rule. Nearest items, none a report of this bug: #5273 / LUCENE-4201 (the kuromoji JapaneseIterationMarkCharFilter, a feature and a possible workaround), #5447 / LUCENE-4381 (the introducing change; its discussion never mentions Han non-ideographs), #13239 (ICU 74.2; a maintainer's review comment describes cjkAsWords=false plus CJKBigramFilter as a supported setup), #14386 (last Default.rbbi change), #15812 / #15825 (CJKBigramFilter positions), #10793, #11976, #6174, #9223 (other ICUTokenizer defects), CLDR-2009 (Unicode CLDR, 2009: treat 々 as an ideograph for Japanese word break) and ICU-10341 (an ICU-side dictionary split). Gaps: keyword search only (GitHub search tokenizes single CJK characters loosely, so exact phrases were also run); JIRA attachments are not searched; one web query was throttled; private security reports are not visible.
Lucene commit tested
main a9779ef3c193 (commit of 2026-10-26; tested 2026-10-27) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the release jars 10.5.2 and 9.12.3 with the same test; the version-neutral probe on 4.6.1 (not affected) and on 19 releases from 4.7.0 to 10.5.2 (all affected). The recommended fix was validated in the scratch worktree with Default.brk regenerated by the build's regenerateRbbi task: the new test, :lucene:analysis:icu:check (163 tests, 148 of them upstream tests that ran, plus format, lint, forbidden-API and checksum checks), the module tests with assertions off and in nightly mode, and the finder's and the independent reproducer's tests.

Lucene and upstream sources

Finding record
  • lucene/analysis/icu/src/data/uax29/Default.rbbi:23 (header: 'disable final rule that would chain han and kana'), :54 ($Han = [:Han:]), :88-89 ($KanaKanji, $dictionaryCJK), :95 ($ALetterPlus = [$ALetter-$dictionaryCJK ...]), :131 ($Ideographic $ExFm* {400})
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/DefaultICUTokenizerConfig.java:80-82 (Default.brk: 'the same as ROOT, except no dictionary segmentation for cjk'), :86 ('super-expert' TODO), :94-96 (@param cjkAsWords: 'otherwise text will be segmented according to UAX#29 defaults'), :110-124 (getBreakIterator)
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizer.java:30-39 (class javadoc 'Breaks text into words according to UAX #29', @lucene.experimental), :69-71 (default constructor uses DefaultICUTokenizerConfig(true, true)), :209-214 (status-0 segments skipped)
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizerFactory.java:96 (cjkAsWords parsed, default true)
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ScriptIterator.java:66, :191-193 (combineCJ: with cjkAsWords=true, Han, Hiragana and Katakana runs become UScript.JAPANESE and use the root iterator)
  • lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/segmentation/TestICUTokenizer.java:42 and TestWithCJKBigramFilter.java:49 (the module's own tests run DefaultICUTokenizerConfig(false, true))
The finding in 30 seconds
  • lucene/analysis/icu/src/data/uax29/Default.rbbi:23, :54, :88-89, :95, :125-131
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizer.java:209-214
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/DefaultICUTokenizerConfig.java:94-96, :105-124
Who is affected
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizer.java:69-71; lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizerFactory.java:96
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizerFactory.java:54-66, :86-95, :120-131 (rulefiles); ICUTokenizer.java:38, DefaultICUTokenizerConfig.java:38 (@lucene.experimental)
Why it looks unintended
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/DefaultICUTokenizerConfig.java:30-36, :80, :86, :94-96
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizer.java:30-32
  • lucene/analysis/icu/src/data/uax29/Default.rbbi:23-24
  • lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/segmentation/TestICUTokenizer.java:70-81 (setUp, :78), :436; TestWithCJKBigramFilter.java:49
  • lucene/analysis/kuromoji/src/test/org/apache/lucene/analysis/ja/TestJapaneseCompletionFilter.java:126, :130-131; TestJapaneseAnalyzer.java (tests containing 々)
Suggested fix
  • lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/DefaultICUTokenizerConfig.java:95-96, :113-114; lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ScriptIterator.java:191-193
  • lucene/analysis/icu/build.gradle (regenerateRbbi task)
  • lucene/analysis/kuromoji/src/java/org/apache/lucene/analysis/ja/JapaneseIterationMarkCharFilter.java:28-34; kuromoji/src/test/org/apache/lucene/analysis/ja/TestJapaneseIterationMarkCharFilter.java:121-122

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

Repro files

About the diagrams

Made in parallel with this doc from executed probes on main a9779ef3c193, stock and with the finder's fix A compiled at class init. The picture names fix A; the recommended fix N gives the same tokens and counts for every input shown. The search rows mark only what holds for every reference as wrong (the two documents get the same terms); the TermQuery counts are shown as actual values, because with <ALPHANUM> typing (StandardTokenizer, fix B) 人々 finds 0 and 人 finds 2 as well.

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.