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.
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.- Build
ICUTokenizerwithnew DefaultICUTokenizerConfig(false, true)(or theicufactory withcjkAsWords="false"), the mode the module's own tests put in front ofCJKBigramFilter. Analyze 人々 ("people"): the kanji 人 followed by 々, which repeats it. - 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 defaultcjkAsWords=truegives[人々]. - You get
[人]. 様々な人々 gives[様, な, 人]and 佐々木 gives[佐, 木]: every 々 is gone, with no error, with assertions on or off. - 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 々. - Why: the rule file
Default.rbbitakes 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, andICUTokenizerskips status-0 pieces just as it skips punctuation.
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.
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 fileDefault.rbbi, compiled toDefault.brk. That is the mode forCJKBigramFilterusers: the module's ownTestWithCJKBigramFilteruses 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.
Default.rbbidefines$Han = [:Han:](:54), puts it into$dictionaryCJK(:88-89) and removes$dictionaryCJKfrom the letter class:$ALetterPlus = [$ALetter-$dictionaryCJK ...](:95). So no Han-script character gets the letter status 200.- 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 ownword.txttext; 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. ICUTokenizer.incrementTokenBufferskips 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.- The boundaries are right (人 | 々, as UAX #29 says); only the status is wrong.
人々 -> [人]; 時々 -> [時]; 様々な人々 -> [様, な, 人]; 佐々木さん -> [佐, 木, さ, ん]; 人〻 (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-icumodule who choosecjkAsWords=false:new ICUTokenizer(new DefaultICUTokenizerConfig(false, ...)), orICUTokenizerFactory(SPI nameicu) withcjkAsWords="false"(the factory default is true,ICUTokenizerFactory.java:96). A maintainer described this as a supported setup on #13239: "the user can configurefalsehere and useCJKBigramFilterif they prefer an n-gram approach".ICUTokenizerandDefaultICUTokenizerConfigare@lucene.experimental(the factory is not). All of this is with the stock rules: a factoryrulefilesentry for the Han script replacesDefault.brkfor 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-codecjkAsWords=truein theiricu_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 (
CheckIndexclean 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.
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.
/*
* 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);
}
}
# 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-eaand-da.
More files, environment, full output35 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# 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)- The javadoc says
cjkAsWords=falsesegments 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 cjkAsWordstrue 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 toDefault.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;ICUTokenizerdrops only status-0 segments, ICU's status for spaces and punctuation. - The tailorings on record. The
Default.rbbiheader 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'sword.txtshows 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 defaultcjkAsWords=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 toDefaultICUTokenizerConfig(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, withcjkAsWords=falseplusCJKBigramFilteras the use case. - Tests.
TestICUTokenizer(main analyzer at:78, used bytestJapaneseat:436) andTestWithCJKBigramFilter(:49) run in this mode. No test inanalysis/icucontains 々; 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).
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 5All 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.
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 5Trade-offs
- Changes analysis output for
cjkAsWords=falseusers (new tokens 々, new bigrams): needs a CHANGES line and a reindex note. - With
cjkAsWords=true, Han-script text goes to ICU's root iterator, notDefault.brk, so the default mode should not change (from the code; thecjkAsWords=truesuite is@AwaitsFixand 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;regenerateRbbirewroteDefault.brk(22,792 to 22,808 bytes) and the checksum file, and leftMyanmarSyllable.brkunchanged. :lucene:analysis:icu:checkpasses: 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 ofTestICUTokenizerCJKare@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.brkgives the same tokens as the rules compiled at runtime.
[$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.
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.
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).
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.
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
ICUTokenizeron 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 (cjkAsWordstrue 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=falsedefect from ICU root's own handling of a lone 々 in the default mode, found the status rule inDefault.rbbi, and wrote the public-API JUnit. Independent AI tools then reproduced and judged it (see the checks).
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.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene) | 2026-10-27 | five 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 triaged | no 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-27 | 32 logged search entries (several bundle more than one search) in apache/lucene, Elasticsearch, OpenSearch, Solr, Lucene.NET, ICU4N, unicode-org/icu and global code search | no report; only #5273 (LUCENE-4201, the kuromoji iteration-mark char filter, a feature) mentions 々 |
| Open PRs | 2026-10-27 | the changed files of all 409 open apache/lucene PRs; re-checked | none touches Default.rbbi, Default.brk, DefaultICUTokenizerConfig or ICUTokenizer (#15825 edits CJKBigramFilter positions only) |
| JIRA and mailing lists | 2026-10-27 | LUCENE and SOLR JIRA, the Unicode ICU and CLDR trackers; java-user@, dev@, issues@, solr-user@ and users@solr | no 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 |
| Web | 2026-10-27 | 9 queries in English and Japanese | no blog, Q&A or forum report (one query was throttled) |
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.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | the 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 | affected | the 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 probe | the version-neutral probe drops 々 (人々 -> [人]); assertions on |
| 4.6.1 | last before | keeps 々: 人々 -> [人 <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 isolation | 4.6.1 rules: 々 status 200 (kept); 4.7.0 rules: status 0 (dropped) |
| fix N (scratch worktree) | not affected | the brief's test 5 of 5; :lucene:analysis:icu:check passes; 163 module tests pass with assertions on, off and in nightly mode |
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).
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | JUnit 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 | real | Own 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 | real | Ran 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 | keep | Re-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 validated | New 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 applied | An 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. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 474 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,468 words
- 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 withDefault.brkregenerated by the build'sregenerateRbbitask: 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.
- 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-131lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizer.java:209-214lucene/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:96lucene/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-96lucene/analysis/icu/src/java/org/apache/lucene/analysis/icu/segmentation/ICUTokenizer.java:30-32lucene/analysis/icu/src/data/uax29/Default.rbbi:23-24lucene/analysis/icu/src/test/org/apache/lucene/analysis/icu/segmentation/TestICUTokenizer.java:70-81 (setUp, :78), :436; TestWithCJKBigramFilter.java:49lucene/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-193lucene/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
- TestICUTokenizerIterationMark.java (JUnit test):
repro/LFV-653/src/org/apache/lucene/analysis/icu/segmentation/TestICUTokenizerIterationMark.java(42 lines)
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.
assets/LFV-653/bug.png(474 KB)
