Before it finds words, SmartChineseAnalyzer gives every character a class. SPACE_LIKE characters are skipped; OTHER characters become one-character terms. Fourteen whitespace characters land in OTHER.
SmartChineseAnalyzer(module analysis-smartcn) splits Chinese text into words with a dictionary, and keeps runs of Latin letters or digits together as one term.- First
Utility.getCharTypegives each character a class: Chinese character, letter, digit, punctuation (DELIMITER), SPACE_LIKE ("Characters that act as a space"), full-width letter or digit, or OTHER. - The segmenter skips SPACE_LIKE characters and joins Chinese characters across them; punctuation becomes a token that the default stop list removes; each OTHER character becomes a term of its own.
hello world and hello<U+2009>world through SmartChineseAnalyzer: the space is skipped, the THIN SPACE becomes a term of its own. Select the still to open it at full size. Generated with AI, checked against the code.- Analyze
hello worldandhello<U+2009>world(a THIN SPACE between the words) with the stocknew SmartChineseAnalyzer(). - You expect [hello, world] for both: the segmenter has a class for "Characters that act as a space" and skips them, as it does a space, a tab or the ideographic space U+3000.
- The second gives [hello, U+2009, world]. Only five characters are in that class; THIN SPACE falls into "Other" and becomes a term of its own. The same holds for VERTICAL TAB, FORM FEED, OGHAM SPACE MARK and all of U+2000-U+200A: 14 characters.
- Search changes with it: the exact phrase "hello world" matches 3 of 7 documents that differ only in the separator, a
TermQueryfor U+2003 alone finds its document, and中<U+2009>国gives [中, U+2009, 国] where中 国gives [中国]. - No error. HTML does the same when an application puts Lucene's
HTMLStripCharFilterin front of the analyzer:  adds two U+2003 terms ( decodes to a plain space and is fine).
Users of the optional smartcn module (SmartChineseAnalyzer with any constructor, or HMMChineseTokenizer) whose text contains one of the 14 characters, literally or via  ,   or   in HTML. Usually it only adds a term; results go wrong when a phrase or Chinese word spans the character, or a query targets it. Rare and silent; a fix needs reindexing.
Mechanism, terms and numbers655 words
SmartChineseAnalyzer's segmenter has a character class for "Characters that act as a space" (CharType.SPACE_LIKE), which it skips, but Utility.getCharType puts only space, tab, CR, LF and U+3000 in it. VERTICAL TAB, FORM FEED, OGHAM SPACE MARK and the eleven spaces U+2000-U+200A (EN QUAD to HAIR SPACE, among them EN, EM and THIN SPACE) fall through to OTHER, and every OTHER character is emitted as a one-character term that the default stop list does not remove. So hello<U+2009>world is indexed as [hello, U+2009, world], an exact phrase typed with a normal space misses it, a TermQuery for the space character alone finds it, and 中<U+2009>国 is no longer segmented as the word 中国. Nothing throws. It has been there since the original contribution (2.9.0, 2009) and is still on main.
- The class table.
Utility.getCharType(Utility.java:140-159) returnsSPACE_LIKEonly for' ','\t','\r','\n'and U+3000 (:146-147). The "Punctuation Marks" ranges come next:0x0021-0x00BB,0x2010-0x2642,0x3001-0x301E(:148-151), then full-width letters and digits and0xFE30-0xFF63(:153-157). U+000B and U+000C lie below0x0021, U+1680 is in no range, and U+2000-U+200A lie just below0x2010, so all 14 reachreturn CharType.OTHER(:158). - What the classes mean.
CharType.SPACE_LIKE: "Characters that act as a space" (CharType.java:38-39);CharType.OTHER: "Other (not fitting any of the other categories)" (:47-48). - The segmenter.
HHMMSegmenter.createSegGraphskipsSPACE_LIKEcharacters (HHMMSegmenter.java:56-58) and steps over them while it extends a Chinese word (:92), which is why中 国becomes 中国. Any other class ends the word. Thedefaultbranch, whichOTHERreaches, makes a one-characterWordType.STRINGtoken ("Treat the unrecognized char symbol as unknown string. For example, any symbol not in GB2312 is treated as one of these.",:156-165). - The term text.
WordSegmenter.convertSegTokencopies the original character back into aSTRINGtoken (WordSegmenter.java:69-75), so the term is the whitespace character itself. - No stop entry.
SmartChineseAnalyzer.createComponentschainsHMMChineseTokenizer,PorterStemFilterand aStopFilterwith the default stop set (SmartChineseAnalyzer.java:124-135). That set "is simply a list of punctuation" (:103-104); it lists U+3000 with the comment "Used as a space in Chinese" (stopwords.txt:54-55) and none of the 14 characters, so the term is indexed. - Why exactly 14. Lucene's pinned Unicode 17 White_Space set (
UnicodeProps.WHITESPACE,UnicodeProps.java:33-40) has 25 code points: 5 areSPACE_LIKE; U+0085, U+00A0, U+2028, U+2029, U+202F and U+205F fall into the DELIMITER ranges and are removed like a comma; the other 14 fall through toOTHER.
U+000B VERTICAL TAB, U+000C FORM FEED, U+1680 OGHAM SPACE MARK, U+2000 EN QUAD, U+2001 EM QUAD, U+2002 EN SPACE, U+2003 EM SPACE, U+2004 THREE-PER-EM SPACE, U+2005 FOUR-PER-EM SPACE, U+2006 SIX-PER-EM SPACE, U+2007 FIGURE SPACE, U+2008 PUNCTUATION SPACE, U+2009 THIN SPACE, U+200A HAIR SPACE.
| input | terms (position increment) | expected |
|---|---|---|
hello world, hello<TAB>world, hello<U+3000>world | [hello/1, world/1] | the same |
hello<U+2009>world, and the same for each of the 14 | [hello/1, U+2009/1, world/1] | [hello/1, world/1] |
中 国 | [中国/1] | the same |
中<U+2009>国, and the same for each of the 14 | [中/1, U+2009/1, 国/1] | [中国/1] |
| exact phrase "hello world" over 7 documents that differ only in the separator (space, tab, U+3000, FF, U+1680, U+2003, U+2009) | 3 hits | 7 hits |
TermQuery for U+000C, U+2003 or U+2009 alone | finds its document | finds nothing |
<p>  我购买了道具和服装。</p> through HTMLStripCharFilter | [U+2003, U+2003, 我, 购买, 了, 道具, 和, 服装] | the words only |
Not claimed: the no-break spaces U+00A0 and U+202F, U+205F MEDIUM MATHEMATICAL SPACE, and U+0085, U+2028, U+2029 are removed exactly like a comma (hello<U+00A0>world gives [hello/1, world/2], as hello,world does). That position gap after removed punctuation is tested design (TestSmartChineseAnalyzer.java:119-128), and Lucene has no single rule for no-break spaces. (The third no-break space, U+2007 FIGURE SPACE, is one of the 14.)
Exposure and likelihood403 words
- Who is hit. Users of the optional
analysis-smartcnmodule:SmartChineseAnalyzerwith any constructor, orHMMChineseTokenizer/ thehmmChinesefactory with the SmartChinese stop list. Default settings, any scale. Only text that contains one of the 14 characters, in documents or queries. - What goes wrong. (1) A whitespace-only term is indexed, and a
TermQueryfor that character finds the document. (2) An exact phrase typed with an ordinary space misses words on both sides of the character; slop 1 still matches, because the extra term takes one position. (3) Chinese characters are no longer joined into a word across it:中<U+2009>国is indexed as 中, U+2009, 国, so the term 中国 is not indexed for that text. A FORM FEED inside a word splits it the same way (道<U+000C>具gives 道, U+000C, 具, where a newline keeps 道具). (4) Each extra term also counts toward the field length that the norms store (IndexingChain.java:2055), so such a document's scores shift slightly (read from the code, not run). - How the characters arrive. As literal characters (typographic spacing, text extracted with form feeds), or through HTML when an application puts Lucene's
HTMLStripCharFilterin front of the analyzer (it is not part of the stockSmartChineseAnalyzer): the char filter decodes , and to U+2003, U+2002 and U+2009, so an  paragraph indent adds two U+2003 terms. decodes to U+0020 and is not a trigger. - Silent. Nothing throws, the index stays valid, and assertions on or off behave the same.
- Likelihood: rare. Most occurrences, such as a paragraph indent, only add an extra term; a wrong match needs a query that spans the character (an exact phrase across it, or a Chinese word split by it) or a query for the character itself. No report in the 17 years since 2.9.0.
- Not affected. Space, tab, CR, LF and U+3000 are skipped as intended.
StandardAnalyzer,CJKAnalyzerandWhitespaceAnalyzeremit no whitespace-only term for the same inputs. - After a fix. Indexed terms change for text with these characters, so affected fields need reindexing.
- Workaround today (from the code, not run): map these characters to U+0020 with a char filter (for example a
MappingCharFilter) in front ofHMMChineseTokenizerin a custom analyzer. A custom stop set that lists the 14 characters removes the terms too, but, like fix D, it leaves a position gap and does not join Chinese characters across them.
One JUnit test, one method, public API: the default SmartChineseAnalyzer() on hello<U+2009>world. It fails on main and on the 10.6.0, 10.5.2, 9.12.3 and 9.1.0 jars, with assertions on and off, and in 20 of 20 iterations.
/*
* 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 TestSmartChineseThinSpace extends BaseTokenStreamTestCase {
public void testThinSpaceIsSkippedLikeASpace() throws Exception {
try (Analyzer analyzer = new SmartChineseAnalyzer()) {
// "hello world" gives [hello, world]; THIN SPACE (U+2009) should act as a space too
assertAnalyzesTo(analyzer, "hello\u2009world", new String[] {"hello", "world"});
}
}
}
cd $LUCENE
cp $REPRO/TestSmartChineseThinSpace.java lucene/analysis/smartcn/src/test/org/apache/lucene/analysis/cn/smart/
./gradlew :lucene:analysis:smartcn:test --tests org.apache.lucene.analysis.cn.smart.TestSmartChineseThinSpace -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false
- Expected
hello<U+2009>worldanalyzes to [hello, world], likehello world.- Observed
- It analyzes to [hello, U+2009, world]:
term 1 expected:<[world]> but was:<[ ]>.
More files, environment, full output217 words
The test sits in org.apache.lucene.analysis.cn.smart like the module's own tests but uses only public API. One 11-character input, the default constructor (so the default stop list), single thread, deterministic. The source is plain ASCII: the THIN SPACE is written as a Java Unicode escape. It compiles from 9.1.0 on (org.apache.lucene.tests). For a release, compile and run the same file against that release's lucene-core, lucene-analysis-common, lucene-analysis-smartcn and lucene-test-framework jars.
Pass -Ptests.asserts explicitly: on current main its default is drawn from the seed and the module path ("Run with assertions for ~75% of all seeds"), and seed DEADBEEF turns assertions off for this module.
The [ ] in the failure message is the THIN SPACE term. The same test is the suggested regression test; it passes with the recommended fix.
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT), 10.6.0, 10.5.2, 9.12.3, 9.1.0: fail |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
# gradle, main 5adff78bb168, seed DEADBEEF, -Ptests.asserts=true
TestSmartChineseThinSpace > testThinSpaceIsSkippedLikeASpace FAILED
org.junit.ComparisonFailure: term 1 expected:<[world]> but was:<[ ]>
at org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:262)
...
at org.apache.lucene.analysis.cn.smart.TestSmartChineseThinSpace.testThinSpaceIsSkippedLikeASpace(TestSmartChineseThinSpace.java:27)
# the same failure: gradle with -Ptests.asserts=false; the JUnit runner on main (-ea and -da), 10.6.0 (-ea and -da), 10.5.2, 9.12.3 and 9.1.0
# main, 20 iterations (seed 5C0C2009): 20 of 20 fail
# main with the recommended fix: passes (assertions on and off)CharType.SPACE_LIKEis documented as "Characters that act as a space"; THIN SPACE and FORM FEED act as spaces, yet land inOTHER.- The stop-list design points the same way: the stop list even lists the already-skipped U+3000, "Used as a space in Chinese".
- Lucene fixed an earlier
getCharTypemisclassification as a bug (LUCENE-8325, 7.4.0).
The full argument314 words
This is the case for calling it a bug. It is medium confidence: no public contract promises how Unicode whitespace is handled, and maintainers may reasonably treat the change as an enhancement (see the devil's advocate below).
- Contract.
CharType.SPACE_LIKEis "Characters that act as a space" andOTHERis "Other (not fitting any of the other categories)" (CharType.java:38-39,:47-48). THIN SPACE, EM SPACE and FORM FEED act as spaces, soOTHERcontradicts the documented meaning of both classes. - The stop-list design points the same way. The default stop list lists U+3000 "Used as a space in Chinese" (
stopwords.txt:54-55) although U+3000 is already skipped (perhaps only redundancy, but it fits an intent to keep spaces out of the index);HMMChineseTokenizerFactorysays the tokenizer "will currently emit tokens for punctuation" and tells users to remove them, for example with the SmartChinese stop list (:27-30); and in #3089 (LUCENE-2014, 2009) rmuir described smartcn as creating "individual tokens for each piece of punctuation (including things like whitespace)" and putting "these in the stopword list". None of the 14 characters is in that list. - The list was meant to be open.
Utility.SPACESreads "Space-like characters that need to be skipped: such as space, tab, newline, carriage return." (Utility.java:41-44); the original Chinese javadoc ine01aad89fe2ends in "等等" ("etc."), and the originalCharTypecomment says "space, \t\r\n and other space or line-break characters". - Not a GB2312 rule. VT and FF are ASCII and encodable in GB2312, yet they are not
SPACE_LIKEand become terms. - Other Lucene analyzers tried do not do this.
StandardAnalyzer,CJKAnalyzerandWhitespaceAnalyzeremit no whitespace-only term for the same inputs. - Precedent. #9372 (LUCENE-8325, fixed in 7.4.0) fixed an earlier
getCharTypemisclassification (surrogates turned into bad one-character tokens) as a bug. - Nothing pins it. No upstream test, CHANGES or MIGRATE entry asserts or mentions a whitespace term.
The devil's-advocate check194 words
The strongest case for "intended" came from an independent reviewer from another model family (OpenAI GPT-5.6, refute lens), which reproduced the behaviour, agreed on the cause and the fix, and still called the change an enhancement: Utility.SPACES lists exactly five characters; HHMMSegmenter says "any symbol not in GB2312" becomes an unknown-string token; testNonChinese says other text is split per character "(regardless of Unicode category)" (TestSmartChineseAnalyzer.java:182-185); the classes are @lucene.experimental; and where Lucene promises Unicode whitespace splitting it says so (UnicodeWhitespaceTokenizer).
Why it fails: Utility.SPACES has no reader on main (its last one, SentenceTokenizer, was removed in 6.0.0 by LUCENE-5969) and introduces its examples with "such as"; the GB2312 comment explains the fallback for unrecognized characters, not why VT and FF are unrecognized; testNonChinese is about letters of other writing systems (it asserts Arabic letters), not separators; the claim is "do not index whitespace as a term", not "split like UnicodeWhitespaceTokenizer"; and @lucene.experimental did not stop LUCENE-8325 from being fixed as a bug. What is left is that the contract is descriptive, not a sentence saying "whitespace is never indexed"; that is why the confirmation is medium confidence.
Recommended A. Treat the remaining Unicode whitespace as SPACE_LIKE at the OTHER fall-through (validated)
Just before return CharType.OTHER in Utility.getCharType, return SPACE_LIKE for UnicodeProps.WHITESPACE, Lucene's pinned White_Space set: +3 lines, and only the 14 characters change. The repro passes with assertions on and off, and so do the smartcn module's 22 tests and its static checks. Indexed terms change, so affected fields need reindexing.
import org.apache.lucene.analysis.util.UnicodeProps;
...
if (ch >= 0xFE30 && ch <= 0xFF63) return CharType.DELIMITER;
// Other Unicode whitespace (VT, FF, U+1680, U+2000-U+200A) also acts as a space
if (UnicodeProps.WHITESPACE.get(ch)) return CharType.SPACE_LIKE;
return CharType.OTHER;All options and trade-offs4 options · 566 words
Four options. A was validated with gradle on main 5adff78bb168; B in standalone runs only; C was run by an earlier check and is incomplete; D was not run.
A. Treat the remaining Unicode whitespace as SPACE_LIKE at the OTHER fall-through (validated) (recommended)
UnicodeProps.WHITESPACE is Lucene's pinned Unicode 17 White_Space set. At the fall-through it can only catch characters no earlier branch classified, which are exactly the 14. The six White_Space characters in the DELIMITER ranges (U+0085, U+00A0, U+2028, U+2029, U+202F, U+205F) keep today's behaviour; U+2007 FIGURE SPACE, a no-break space, is one of the 14 and changes (see the trade-offs). smartcn already requires analysis-common (module-info.java:21, build.gradle:24).
import org.apache.lucene.analysis.util.UnicodeProps;
...
if (ch >= 0xFE30 && ch <= 0xFF63) return CharType.DELIMITER;
// Other Unicode whitespace (VT, FF, U+1680, U+2000-U+200A) also acts as a space
if (UnicodeProps.WHITESPACE.get(ch)) return CharType.SPACE_LIKE;
return CharType.OTHER;Trade-offs
- Changes indexed terms for text with these characters: reindex, and a CHANGES entry.
- The set follows Lucene's pinned Unicode version, not the JDK's.
- U+2007 FIGURE SPACE, a no-break space, becomes skipped like any space, so Chinese characters are joined across it (
中<U+2007>国: [中/1, U+2007/1, 国/1] today, [中国/1] with the fix); the other no-break spaces, U+00A0 and U+202F, stay DELIMITER and still split, with a position gap (中<U+00A0>国-> [中/1, 国/2] today and with the fix). In Latin text all three keep separating the words. Treating U+2007 like an ordinary space is a policy choice to flag for the maintainers.
What the checks validated
- Gradle in a scratch worktree of
5adff78bb168(the script reverts its changes and records an emptygit statusafterwards), OpenJDK 25.0.3, seed DEADBEEF: stock, the repro fails with assertions on and off; with the fix it passes with assertions on and off. - The whole module with the fix,
:lucene:analysis:smartcn:testwith assertions on: 22 tests, 0 failures (TestSmartChineseAnalyzer19,TestHMMChineseTokenizerFactory2, the new test 1). :lucene:analysis:smartcn:check -x test(formatting, ecj lint, forbidden APIs) passes with the fix and the test.- On
a9779ef3c193the same change made the broader 7-test check pass 7 of 7 (assertions on and off) and the version driver report the bug absent.
Same result as A without the UnicodeProps dependency. With the patched class applied, the minimal test passes and the broader 7-test check passes 7 of 7 on 5adff78bb168; no gradle module run.
// VT, FF, OGHAM SPACE MARK and EN QUAD to HAIR SPACE also act as a space
if (ch == 0x000B || ch == 0x000C || ch == 0x1680 || (ch >= 0x2000 && ch <= 0x200A))
return CharType.SPACE_LIKE;Trade-offs
A hand-kept list instead of Lucene's maintained Unicode set.
The finder's first sketch. With it the broader 7-test check still fails 4 of 7.
Trade-offs
Misses VT and FF (it covers only Zs, Zl and Zp); also turns U+00A0, U+202F, U+205F, U+2028 and U+2029 from DELIMITER into SPACE_LIKE; depends on the runtime JDK's Unicode tables. LUCENE-6874 (#7932) chose the White_Space property over Character.isSpaceChar for the same reasons.
Removes the terms for users of the default stop list only.
Trade-offs
Leaves a position gap like removed punctuation, so the exact phrase still misses, and 中<U+2009>国 is still not joined into 中国 (reasoning from the code, not run).
Products that bundle Lucene's smartcn module, such as the Elasticsearch Smart Chinese analysis plugin, inherit this behaviour, and a fix changes their indexed terms as well.
Semi-formal: an exhaustive sweep ran every BMP character through the stock Utility.getCharType and compared its class with its Unicode category. Of the 17 space separators, 2 are SPACE_LIKE, 3 DELIMITER and 12 OTHER; each was then run through SmartChineseAnalyzer. A later check restated the scope over Unicode White_Space: 14 characters become terms.
The method in full124 words
Method: semi-formal, exhaustive over the BMP character table.
- A sweep ran all 65,536 BMP characters through the stock
Utility.getCharTypeon the main jars and cross-tabulated the class against the JDK 25 Unicode general category. For the space separators (Zs, 17 characters): SPACE_LIKE 2, DELIMITER 3, OTHER 12. - Each space separator was then driven through the stock
SmartChineseAnalyzerand its terms printed; the OTHER ones came out as one-character terms. - The finding was reduced to a public-API JUnit test on the real jars.
- The confirming check restated the scope over the Unicode White_Space property (Lucene's pinned
UnicodeProps.WHITESPACE, checked over the whole BMP): that added VERTICAL TAB and FORM FEED and left out U+00A0, U+202F, U+205F, U+0085, U+2028 and U+2029, which behave like punctuation.
New: no Lucene issue, PR, JIRA entry, list thread or commit, and nothing outside Lucene, describes this defect (re-checked 2026-11-03). Related items concern other things: #3089, a position bug where rmuir described the stop-list design; #2802, a refactoring wish; and 2017 Wikimedia notes about ZERO WIDTH SPACE, which is not whitespace.
Searches5 searches
No report of this defect (re-checked 2026-11-03 in two independent passes, and again for this doc). Related prior items, none of them this defect:
- #3089 (LUCENE-2014, 2009, fixed): a position-increment bug (a missing
clearAttributes). In it rmuir described the design: smartcn "creates individual tokens for each piece of punctuation (including things like whitespace), and puts these in the stopword list". It names no character and reports no whitespace term; space, tab, CR, LF and U+3000 produce no token, then and now. - #2802 (LUCENE-1728, 2009, closed): moving smartcn to its own module; rmuir listed "use of unicode categories (java Character class, etc) versus Utility.getCharType()" as a refactoring wish. No defect is named.
- The Wikimedia Foundation search team's 2017 SmartCN evaluation notes (mediawiki.org, "User:TJones (WMF)/Notes/Chinese Analyzer Analysis", section added 2017-04-27): they note that SmartCN splits words at ZERO WIDTH SPACE U+200B (台[ZWSP]儿庄 -> 台 + 儿 + 庄). U+200B is a format character, not White_Space, and the fix here does not change it; the notes name none of the 14 characters, no whitespace term, no search effect and no cause, and they call the no-break-space handling "Good".
- #3098 (LUCENE-2023, open): a performance patch, never committed, that keeps the same five space characters.
- #7932 / #6160 (LUCENE-6874, LUCENE-5096):
WhitespaceTokenizerand the no-break space, resolved by addingUnicodeWhitespaceTokenizer; the precedent for using the White_Space property instead ofCharacter.isSpaceChar. - #9372 (LUCENE-8325, fixed in 7.4.0): surrogates in
getCharType, a different character class; a precedent for fixing this table as a bug. - #5795 (LUCENE-4730): offsets with trailing ASCII spaces, fixed; unrelated.
LUCENE-6874 is the precedent for the choice of character set. LFV-707 is a different bug in the same method (its DELIMITER ranges).
| Where | When | How | Found |
|---|---|---|---|
| GitHub apache/lucene issues, PRs and commits (incl. the migrated JIRA archive); downstream repositories; dev@, java-user@, issues@, commits@; JIRA | 2026-10-27 | the scripted prior-report search, 6 runs: 154 GitHub issue and PR, 15 commit, 10 downstream, 100 mailing-list and 20 JIRA queries | only the related items above |
| GitHub by hand: apache/lucene-solr, apache/solr, Lucene.NET, Elasticsearch, OpenSearch, the Elasticsearch smartcn plugin, all of GitHub; JIRA LUCENE and SOLR; lists.apache.org | 2026-10-27 | 90 read-only GitHub searches; 48 JQL queries with 21 issues read in full; 65 list-archive queries with 6 threads read | nothing about these characters in smartcn |
| Open PRs; local full history; web and Stack Overflow | 2026-10-27 | the changed-file lists of all 410 open apache/lucene PRs; pickaxe and message searches; 16 web and 4 Stack Overflow searches | no open PR touching smartcn code found; the SPACE_LIKE test never changed; the 2017 Wikimedia notes |
| Re-check for this doc: GitHub issues, PRs and commits, downstream trackers, the four lists, JIRA, web | 2026-11-03 | the scripted prior-report search, 51 queries (16 GitHub, 3 commit, 2 downstream, 20 list, 4 JIRA, 6 web planned), 178 hits; open smartcn PRs and smartcn issues or PRs since 2026-10-01; the latest upstream smartcn commits; 3 web searches | nothing new; the latest smartcn commit upstream, 54283150a81 (2026-06-14), is in 5adff78bb168 |
| GitHub apache/lucene issues and PRs | 2026-11-04 | gh search for smartcn (latest updated first), THIN SPACE, EM SPACE, SPACE_LIKE, getCharType, "smartcn whitespace" and "SmartChineseAnalyzer space"; open smartcn PRs | nothing new (only #2802 and #9372, both above) |
In the original contribution (LUCENE-1629, e01aad89fe2, 2009-05-14), first released in 2.9.0; the SPACE_LIKE test has never changed. A version driver finds the bug on 24 releases from 2.9.0 to 10.5.2; the minimal test fails on 9.1.0, 9.12.3, 10.5.2, 10.6.0 and main. Not fixed upstream.
Versions tested8 rows
Introduced by the original contribution, LUCENE-1629 "contrib intelligent Analyzer for Chinese" (GitHub #2703), a JIRA patch committed on 2009-05-14 as e01aad89fe2 (svn r774718); no GitHub PR exists. git log -S/-G on the SPACE_LIKE condition, return CharType.OTHER, the segmenter's SPACE_LIKE case and its STRING default branch each return only this commit. Not a regression: the later commits on getCharType (5265dc1bb20 LUCENE-1722 comment translation, 55858d7ba72 LUCENE-8325 surrogate check in 7.4.0, 7f8b7ffbcad LUCENE-9570 reformatting) left the test and the fall-through unchanged. 2.4.1, the release before 2.9.0, has no cn.smart package, so no release ever shipped an unaffected SmartChineseAnalyzer.
The version-portable driver checks three things for each of the 14 characters c: hello+c+world and 中+c+国 give a whitespace-only term (28 inputs), world is not right after hello (14), and 中+c+国 does not give 中国 (14); its controls are space, tab, CR, LF and U+3000. Its term lists are byte-identical on every release run. The driver checks terms and positions; the phrase and TermQuery searches themselves ran in the confirming 7-test check on main, 10.5.2 and 9.12.3.
| Version or commit | Verdict | Result |
|---|---|---|
| 2.4.1 | no API | no org.apache.lucene.analysis.cn.smart package (the probe does not compile) |
| 2.9.0 | first release | driver: 28/28 whitespace terms, 14/14 with world not right after hello (so the phrase breaks), 14/14 words not joined; controls hold |
| 2.9.4, 3.0.0, 3.0.3, 3.1.0, 3.6.2, 4.0.0, 4.7.2, 4.8.0, 4.10.4, 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.3.1, 7.4.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0 | affected | driver: the same, identical term lists (both sides of the 4.8.0, 6.0.0 and 7.4.0 pipeline changes) |
| 9.1.0, 9.12.3 | affected | minimal test fails; driver (9.12.3, assertions on and off) the same |
| 10.0.0, 10.5.2 | affected | driver the same; minimal test fails on 10.5.2 |
| 10.6.0 | affected | minimal test fails, assertions on and off |
main 5adff78bb168 | affected | minimal test fails (gradle and the JUnit runner, assertions on and off, 20 of 20 iterations); 7-test check 4 of 7 fail |
| main + fix A | not affected | repro passes; smartcn module 22 tests pass; driver (on a9779ef3c193) reports the bug absent |
Before this doc: the finder's cross-model review (OpenAI GPT-5.6) and an independent reproducer rated it real; a GPT-5.6 refute review reproduced it but called it an enhancement; a devil's-advocate verifier rated it real; the adjudication sent it to a tie-breaker, which confirmed it (medium confidence) for the 14 characters. This doc re-ran it on 5adff78bb168.
Checks and verdicts339 words
The reviews and verifiers ran on main a9779ef3c193 (the cross-model reviews on ea57360912ed, whose smartcn sources are identical); a re-verification on e1faeba08bde (2026-11-02) and this doc's runs on 5adff78bb168 reproduced it.
| Check | Verdict | What it found |
|---|---|---|
| finder | found (scope later narrowed) | BMP sweep, then a 2-test JUnit: both fail with assertions on and off, also on 10.5.2 and 9.12.3. Its claim also covered U+00A0, U+202F and U+205F, which are removed like punctuation; that part was not confirmed. |
| cross-model review (OpenAI GPT-5.6, intended lens, executed) | real (high) | Reproduced on main, 10.5.2 and 9.12.3; rated the intended-behaviour case moderate; advised a pinned Unicode set over Character.isSpaceChar. |
| independent reproducer | real | Own public-API test: 3 of 3 fail on main (assertions on and off), 10.5.2 and 9.12.3, controls pass; StandardAnalyzer, CJKAnalyzer and WhitespaceAnalyzer emit no whitespace-only term. |
| refute review (OpenAI GPT-5.6, executed) | not real (high): an enhancement | Reproduced the behaviour and agreed on root cause and fix, but argued no contract promises Unicode-space handling (see the devil's advocate above). |
| devil's-advocate verifier | real (medium-high) | No surviving intended reading for the whitespace terms; found that decodes to U+0020, so it is not a trigger. |
| adjudication | disputed (medium) | Re-ran the independent test (fails with assertions on and off, passes with a fix applied) and sent the case to a tie-breaker because the refute argument was substantive. |
| tie-breaker | confirmed (medium), narrowed | Own 7-test JUnit: 4 of 7 fail on main, 10.5.2 and 9.12.3, controls pass; answered the refute review point by point; fix A passes 7 of 7 and the 21 upstream smartcn tests. |
| re-checks | still reproduces | Clean re-runs on a9779ef3c193 and main e1faeba08bde (2026-11-02): 4 of 7 fail with assertions on and off; fix A passes. |
| this doc | reproduced; fix validated | One-method repro fails on main 5adff78bb168 (gradle and the JUnit runner, assertions on and off, 20 of 20 iterations) and on the 10.6.0, 10.5.2, 9.12.3 and 9.1.0 jars; fix A passes it and the smartcn module. |
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 491 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,583 words
- Registry title
- SmartChineseAnalyzer indexes whitespace characters as terms: VERTICAL TAB, FORM FEED, OGHAM SPACE MARK and the eleven spaces U+2000-U+200A (EN QUAD to HAIR SPACE, among them EN, EM and THIN SPACE) each become a one-character term, because Utility.getCharType classifies them OTHER instead of SPACE_LIKE
- Area
- analysis-smartcn / HHMM segmenter character classes (Utility.getCharType SPACE_LIKE test -> HHMMSegmenter OTHER branch -> one-character STRING term; SmartChineseAnalyzer and HMMChineseTokenizer)
- Priority
- Medium, rank 146: The stock SmartChineseAnalyzer indexes 14 rare Unicode spaces as terms, which breaks phrases that cross one of them, a rare-input relevance error.
- Impact class
- silent wrong results
- Impact note
- Only users of the optional smartcn module (SmartChineseAnalyzer with any constructor, or the HMMChineseTokenizer it wraps; default settings, no scale) whose documents or queries contain one of the 14 characters are hit: a whitespace-only term is indexed and a TermQuery for that character finds the document, an exact phrase typed with an ordinary space misses the words on both sides of it (slop 1 still matches), and Hanzi are no longer joined into a word across it ('中 国' -> [中国], '中<U+2009>国' -> [中, U+2009, 国]); HTML run through Lucene's own HTMLStripCharFilter triggers it with     and   while does not (it decodes to U+0020); nothing throws, the index stays valid, -ea and -da behave the same, and because a fix changes indexed terms, affected fields need reindexing after it.
- Summary
- You index text with the stock SmartChineseAnalyzer, whose segmenter has a character class for 'Characters that act as a space' (CharType.SPACE_LIKE) and skips those characters, so you expect any space to separate words without a trace, as an ordinary space, a tab or the ideographic space U+3000 does. Instead Utility.getCharType knows only those five, and VERTICAL TAB, FORM FEED, OGHAM SPACE MARK and U+2000-U+200A fall through to OTHER and are each indexed as a one-character term: 'hello<THIN SPACE>world' gives [hello, U+2009, world], a TermQuery for U+2009 alone finds the document, and '中<U+2009>国' gives [中, U+2009, 国] where '中 国' gives [中国]. So the exact phrase "hello world" matches only 3 of 7 documents that differ only in the separator (space, tab and U+3000 match; FORM FEED, U+1680, EM SPACE and THIN SPACE miss), and HTML indented with    gets two whitespace terms, with no error.
- Introduced
- e01aad89fe24f2482eb2e8c0e39f8272704abe80 (LUCENE-1629 'contrib intelligent Analyzer for Chinese', GitHub #2703: the original SmartChineseAnalyzer contribution, a JIRA patch committed as svn r774718; no GitHub PR exists) [read: git log -S and -G on the SPACE_LIKE condition, 'return CharType.SPACE_LIKE', 'return CharType.OTHER', Utility.SPACES, 'case CharType.SPACE_LIKE', the HHMMSegmenter join loop and the default branch's STRING token each return only this commit; git blame and git log -L of getCharType in the full-history clone]. Not a regression: the later commits on getCharType (5265dc1bb20 LUCENE-1722 comment translation, 55858d7ba72 LUCENE-8325 surrogate check in 7.4.0, 7f8b7ffbcad LUCENE-9570 reformatting) left the SPACE_LIKE test and the OTHER fall-through unchanged (2009-05-14 (first release 2.9.0))
- First affected version
- 2.9.0 [exec] (the first release with the org.apache.lucene.analysis.cn.smart package; 2.4.1, the release before, has no such package [exec: the API probe does not compile], so no release ever shipped an unaffected SmartChineseAnalyzer)
- Affected versions
- Every release from 2.9.0 to 10.6.0 and main a9779ef3c193; not fixed. [exec] the minimal one-method test fails on the released 10.6.0 jars with -ea and -da (2026-11-03); a version-portable driver reports the bug fully present (28 of 28 inputs give a whitespace-only term, 14 of 14 break the exact phrase, 14 of 14 stop 中国 from being joined; controls hold) with byte-identical term lists on 24 releases (2.9.0, 2.9.4, 3.0.0, 3.0.3, 3.1.0, 3.6.2, 4.0.0, 4.7.2, 4.8.0, 4.10.4, 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.3.1, 7.4.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2) and on main, with -ea and (9.12.3, 10.5.2, main) -da, and reports it absent on main with the tie-breaker's fix applied; the result is the same on both sides of the boundaries 4.7.2/4.8.0 (LUCENE-4984, HMMChineseTokenizer), 5.5.5/6.0.0 (LUCENE-5969, SentenceTokenizer removed) and 7.3.1/7.4.0 (LUCENE-8325); the tie-breaker's JUnit fails 4 of 7 (its 3 controls pass) and the finder's 2 of 2 on 9.1.0, 9.12.3, 10.0.0, 10.5.2 and main (neither compiles on 9.0.0, which predates org.apache.lucene.tests). [read] branch_10_6, branch_10x, branch_10_5 and upstream main a95c754ee00 have the same SPACE_LIKE test and OTHER fall-through (read 2026-10-27).
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that disputed it, an adjudication that rated it disputed and a tie-breaker that confirmed it (medium confidence), 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
- no Lucene report found (searched 2026-10-27, read-only; re-checked 2026-11-03): six scripted prior-report searches (154 GitHub issue and PR, 15 commit, 10 downstream, 100 mailing-list and 20 JIRA queries), 90 manual read-only GitHub searches (apache/lucene-solr, apache/solr, apache/lucenenet, Elasticsearch, OpenSearch, the Elasticsearch smartcn plugin and all of GitHub), 48 JQL queries over LUCENE and SOLR with 21 issues read in full, 65 lists.apache.org queries, the changed-file lists of all 410 open apache/lucene PRs, the local full history, and 16 web plus 4 Stack Overflow searches. No issue, PR, JIRA ticket, list thread or commit reports or fixes how smartcn handles VERTICAL TAB, FORM FEED, U+1680 or U+2000-U+200A (nor the no-break spaces), and no open PR touches smartcn code (#202 changes a javadoc word, #681 module-info). Related, outside Lucene's trackers (a different defect): the Wikimedia Foundation search team's 2017 SmartCN evaluation notes (mediawiki.org, User:TJones (WMF)/Notes/Chinese Analyzer Analysis, section added 2017-04-27) say SmartCN splits words at ZERO WIDTH SPACE U+200B, a format character that is not White_Space and that this fix does not change, and that no-break spaces 'get tokenized separately' (judged 'Good'). Related, not the same bug: #3089 (LUCENE-2014, fixed 2009: rmuir wrote that smartcn makes tokens for punctuation 'including things like whitespace' and puts these in the stop list), #2802 (LUCENE-1728, closed: 'use of unicode categories ... versus Utility.getCharType()' as a refactoring wish), #3098 (LUCENE-2023, open: an uncommitted patch that keeps the same five space characters), #7932 and its duplicate #6160 (LUCENE-6874, LUCENE-5096: WhitespaceTokenizer and NBSP, resolved by adding UnicodeWhitespaceTokenizer; precedent for the White_Space property over Character.isSpaceChar), #5795 (LUCENE-4730: offsets with trailing ASCII spaces, fixed). Precedent: #9372 (LUCENE-8325, fixed in 7.4.0), another getCharType misclassification fixed as a bug.
- Lucene commit tested
- main
5adff78bb168(gradle in a scratch worktree and the JUnit runner, assertions on and off) and the released 10.6.0, 10.5.2, 9.12.3 and 9.1.0 jars, 2026-11-03. Earlier runs: maina9779ef3c193(identical smartcn sources) ande1faeba08bde; a version-portable driver on 24 releases from 2.9.0 to 10.5.2.
- Finding record
lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/Utility.java:41-44 (at a9779ef3c193) (SPACES javadoc 'Space-like characters that need to be skipped: such as space, tab, newline, carriage return.'), :140-159 (getCharType; SPACE_LIKE only for ' ', TAB, CR, LF, U+3000 at :146-147; '// Punctuation Marks' DELIMITER ranges at :148-151 and :157; 'return CharType.OTHER' at :158)lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/CharType.java:38-39 (at a9779ef3c193) (SPACE_LIKE 'Characters that act as a space'), :47-48 (OTHER 'Other (not fitting any of the other categories)'), :22 (@lucene.experimental)lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/hhmm/HHMMSegmenter.java:56-58 (at a9779ef3c193) (SPACE_LIKE skipped), :92 (Hanzi prefix walk joins across SPACE_LIKE), :156-165 (default branch: one-character WordType.STRING token; comment 'any symbol not in GB2312 is treated as one of these')lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/WordSegmenter.java:69-75 (at a9779ef3c193) (convertSegToken copies the original character back into a STRING token)lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/SmartChineseAnalyzer.java:33 (at a9779ef3c193) ('an analyzer for Chinese or mixed Chinese-English text'), :47 (@lucene.experimental), :124-135 (HMMChineseTokenizer + PorterStemFilter + StopFilter)lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/HMMChineseTokenizerFactory.java:27-30 (at a9779ef3c193) (punctuation tokens are to be removed with the SmartChinese stop list)lucene/analysis/smartcn/src/resources/org/apache/lucene/analysis/cn/smart/stopwords.txt:54-55 (at a9779ef3c193) ('the line below contains an IDEOGRAPHIC SPACE character (Used as a space in Chinese)'; no entry for the 14 characters)lucene/analysis/common/src/java/org/apache/lucene/analysis/util/UnicodeProps.java:33-40 (at a9779ef3c193) (UNICODE_VERSION 17.0.0.0; WHITESPACE, 25 code points)lucene/analysis/common/src/java/org/apache/lucene/analysis/charfilter/HTMLCharacterEntities.jflex:109 (at a9779ef3c193) ('emsp' -> U+2003, 'ensp' -> U+2002), :124 ('nbsp' -> ' '), :145 ('thinsp' -> U+2009)lucene/analysis/smartcn/src/test/org/apache/lucene/analysis/cn/smart/TestSmartChineseAnalyzer.java:119-128 (at a9779ef3c193) (testChineseStopWords2, posInc 2 after removed punctuation), :182-186 (testNonChinese comment '(regardless of Unicode category)')SECURITY.md (In-scope reports; Untrusted query and analysis input; Scope and Threat Model)
- The finding in 30 seconds
lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/Utility.java:140-159 (at 5adff78bb168) (:146-147 SPACE_LIKE, :148-151 and :157 DELIMITER ranges, :158 OTHER)lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/CharType.java:38-39, (at 5adff78bb168) :47-48lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/hhmm/HHMMSegmenter.java:56-58, (at 5adff78bb168) :92, :156-165lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/WordSegmenter.java:69-75 (at 5adff78bb168)lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/SmartChineseAnalyzer.java:103-104, (at 5adff78bb168) :124-135lucene/analysis/smartcn/src/resources/org/apache/lucene/analysis/cn/smart/stopwords.txt:54-55 (at 5adff78bb168)lucene/analysis/common/src/java/org/apache/lucene/analysis/util/UnicodeProps.java:33-40 (at 5adff78bb168)lucene/analysis/smartcn/src/test/org/apache/lucene/analysis/cn/smart/TestSmartChineseAnalyzer.java:119-128 (at 5adff78bb168) (testChineseStopWords2, posInc 2 after removed punctuation)
- Who is affected
lucene/analysis/common/src/java/org/apache/lucene/analysis/charfilter/HTMLCharacterEntities.jflex:109 (at 5adff78bb168) (emsp, ensp), :124 (nbsp -> ' '), :145 (thinsp)lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/HMMChineseTokenizerFactory.java:27-30 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2055 (at 5adff78bb168) (each token adds 1 to the field length)
- Minimal repro
Lucene main 5adff78bb168 build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/TestsAndRandomizationPlugin.java:240-248 (tests.asserts default drawn from the project seed); build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/globals/RegisterBuildGlobalsPlugin.java:137-140 (project seed = root seed XOR project path hash; for :lucene:analysis:smartcn and DEADBEEF the default is false, computed in jshell)
- Why it looks unintended
lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/CharType.java:38-39, (at 5adff78bb168) :47-48; Utility.java:41-44lucene/analysis/smartcn/src/resources/org/apache/lucene/analysis/cn/smart/stopwords.txt:54-55 (at 5adff78bb168)lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/HMMChineseTokenizerFactory.java:27-30 (at 5adff78bb168)lucene/analysis/smartcn/src/test/org/apache/lucene/analysis/cn/smart/TestSmartChineseAnalyzer.java:182-185 (at 5adff78bb168) (testNonChinese)https://github.com/apache/lucene/issues/3089 (LUCENE-2014, rmuir's design remark)https://github.com/apache/lucene/issues/9372 (LUCENE-8325, fixed in 7.4.0)
- Suggested fix
lucene/analysis/smartcn/src/java/module-info.java:21; (at 5adff78bb168) lucene/analysis/smartcn/build.gradle:24https://github.com/apache/lucene/issues/7932 (LUCENE-6874)JDK java.lang.Character#isWhitespace javadoc (names U+00A0, U+2007 and U+202F as non-breaking spaces) and #isSpaceChar (Zs, Zl, Zp only)
- Prior reports
https://github.com/apache/lucene/issues/3089; https://github.com/apache/lucene/issues/2802; https://www.mediawiki.org/wiki/User:TJones_(WMF)/Notes/Chinese_Analyzer_Analysis
- Diagrams
lucene/analysis/smartcn/src/java/org/apache/lucene/analysis/cn/smart/Utility.java:146-147, (at 5adff78bb168) :158; hhmm/HHMMSegmenter.java:56-58, :156-165
- TestSmartChineseThinSpace.java (one test method):
repro/LFV-728/TestSmartChineseThinSpace.java(30 lines)
A walk-through of the minimal repro's input, made from the code at 5adff78bb168 and the executed runs; the PNG is the summary still.
assets/LFV-728/bug.png(491 KB)
