Lucene 中提供了 SmartCN 為中文提供分詞功能锹锰,實際應(yīng)用中還會涉及到停用詞芥炭、擴展詞(特殊詞、專業(yè)詞)等城须,因此本文將聚焦在 SmartCN 而暫時不考慮其他中文分詞類庫蚤认。
1 簡介
analyzers-smartcn
是一個用于簡體中文索引詞的 Analyzer。但是需要注意的它提供的 API 是試驗性的糕伐,后續(xù)版本中可能進(jìn)行更改砰琢。
可以它包含了如下兩部分:
org.apache.lucene.analysis.cn.smart
用于簡體中文的分析器,用來建立索引良瞧。
org.apache.lucene.analysis.cn.smart.hhmm
SmartChineseAnalyzer 隱藏了 Hidden Model 包陪汽。
analyzers-smartcn
中包含了 3 種分析器,它們用不同的方式來分析中文:
-
StandardAnalyzer
會單個漢字來作為標(biāo)記褥蚯。例如:“中臺的作用”分析后成為:中-臺-的-作-用 -
CJKAnalyzer
它在 analyzers/cjk 包中挚冤,使用相鄰兩個漢字作為標(biāo)記≡奘“中臺的作用”分析后成為:中臺-的作-用 -
SmartChineseAnalyzer
嘗試使用中文文本分割成單詞作為標(biāo)記训挡。“中臺的作用”分析后成為:中臺-的-作用
很顯然 SmartChineseAnalyzer
更符合日常搜索的使用場景歧强。
2 SmartChineseAnalyzer
上面的例子展示了 SmartChineseAnalyzer 對中文的處理澜薄,實際上 SmartChineseAnalyzer 同時還支持中英文混合排版。
SmartChineseAnalyzer 使用了概率知識來獲取最佳的中文分詞摊册。文本會首先被分割成字句肤京,再將字句分割成單詞。分詞基于 Hidden Markov Model茅特。使用大型訓(xùn)練語料來計算中文單詞頻率概率忘分。
SmartChineseAnalyzer 需要一個詞典來提供統(tǒng)計數(shù)據(jù)。它自帶了一個現(xiàn)成的詞典白修。包含的詞典數(shù)據(jù)來自 ICTCLAS1.0妒峦。
SamrtChineseAnalyzer 提供了 3 種構(gòu)造函數(shù),通過構(gòu)造函數(shù)能夠控制是否使用 SmartChineseAnalyzer 自帶的停用詞熬荆,以及使用外部的停用詞舟山。
通過實際測試绸狐,我們可以了解分詞結(jié)果卤恳,分詞代碼如下:
String text = "K8s 和 YARN 都不夠好累盗,全面解析 Facebook 自研流處理服務(wù)管理平臺";
Analyzer analyzer = new SmartChineseAnalyzer();
TokenStream tokenStream = analyzer.tokenStream("testField", text);
OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
tokenStream.reset();
List<String> tokens = new ArrayList<>();
while (tokenStream.incrementToken()) {
tokens.add(offsetAttribute.toString());
}
tokenStream.end();
String format = String.format("tokens:%s", tokens);
System.out.println(format);
通過下面下面 3 個例子分析結(jié)果,我們可以大致了解 SmartChineseAnalyzer突琳。
"K8s 和 YARN 都不夠好若债,全面解析 Facebook 自研流處理服務(wù)管理平臺"
分詞結(jié)果是:
[k, 8, s, 和, yarn, 都, 不, 夠, 好, 全面, 解析, facebook, 自, 研, 流, 處理, 服務(wù), 管理, 平臺]
“極客時間學(xué)習(xí)心得:用分類和聚焦全面夯實技術(shù)認(rèn)知"
分詞結(jié)果如下:
[極, 客, 時間, 學(xué)習(xí), 心得, 用, 分類, 和, 聚焦, 全面, 夯實, 技術(shù), 認(rèn)知]
"交易中臺架構(gòu)設(shè)計:海量并發(fā)的高擴展,新業(yè)務(wù)秒級接入"
分詞結(jié)果如下:
[交易, 中, 臺, 架構(gòu), 設(shè)計, 海量, 并發(fā), 的, 高, 擴展, 新, 業(yè)務(wù), 秒, 級, 接入]
很顯然拆融,停用詞中沒有沒有過濾掉“的”蠢琳。為了更加了解 SmartChineseAnalyzer 中實現(xiàn),可以查看一些源碼中的停用詞配置信息镜豹。
其中設(shè)定的默認(rèn)停用詞會讀取 stopwards.txt 文件中停用詞傲须,可以在引入的 jar 包中找到該文件。
其內(nèi)容主要是一些標(biāo)點符號作為停用詞趟脂。
[ , 泰讽、, 。, 昔期!, , (, ), 《, 》, 已卸,, -, 【, 】, —, :, ;, “, ”, 硼一?, !, ", #, $, &, ', (, ), *, +, ,, -, ., /, ·, :, [, <, ], >, ?, @, ●, [, \, ], ^, _, `, ;, =, {, |, }, ~]
在 stopwords.txt 中雖然只是提供了一些標(biāo)點符號作為停用詞但是其中定義了停用詞的三個類別:Punctuation tokens to remove累澡、English Stop Words、Chinese Stop Words般贼。
因此可以按照 lucene 源文件中的 stopwords.txt 的格式定義并引入自定義停用詞愧哟。
3 為 SmartChineseAnalyzer 自定義停用詞
這里可以在默認(rèn)的停用詞中添加一個停用詞“的”,然后重新對“交易中臺架構(gòu)設(shè)計:海量并發(fā)的高擴展哼蛆,新業(yè)務(wù)秒級接入” 進(jìn)行分詞并查看分詞結(jié)果蕊梧。
創(chuàng)建一個 stopwords.txt 文件,并將 smartcn 中停用詞內(nèi)容拷貝過來人芽,并追加如下內(nèi)容
//////////////// Chinese Stop Words ////////////////
的
在構(gòu)建 SmartChineseAnalyzer 是通過構(gòu)造函數(shù)指定停用詞望几。
CharArraySet stopWords = CharArraySet.unmodifiableSet(WordlistLoader.getWordSet(
IOUtils.getDecodingReader(
new ClassPathResource("stopwords.txt").getInputStream(),
StandardCharsets.UTF_8),
STOPWORD_FILE_COMMENT));
Analyzer analyzer = new SmartChineseAnalyzer(stopWords);
完整代碼執(zhí)行代碼如下:
String text = "交易中臺架構(gòu)設(shè)計:海量并發(fā)的高擴展,新業(yè)務(wù)秒級接入";
CharArraySet stopWords = CharArraySet.unmodifiableSet(WordlistLoader.getWordSet(
IOUtils.getDecodingReader(
new ClassPathResource("stopwords.txt").getInputStream(),
StandardCharsets.UTF_8),
STOPWORD_FILE_COMMENT));
Analyzer analyzer = new SmartChineseAnalyzer(stopWords);
TokenStream tokenStream = analyzer.tokenStream("testField", text);
OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
tokenStream.reset();
List<String> tokens = new ArrayList<>();
while (tokenStream.incrementToken()) {
tokens.add(offsetAttribute.toString());
}
tokenStream.end();
System.out.println(String.format("tokens:%s", tokens));
結(jié)果顯示:
[交易, 中, 臺, 架構(gòu), 設(shè)計, 海量, 并發(fā), 高, 擴展, 新, 業(yè)務(wù), 秒, 級, 接入]
停用詞 “的” 已經(jīng)成功被去掉萤厅。
4 為 SmartChineseAnalyzer 實現(xiàn)擴展詞
但是結(jié)果中顯示的”中臺“一次被拆分了”中“橄抹、”臺“兩個詞,而當(dāng)前”中臺“已經(jīng)是大家所熟知的一個術(shù)語惕味。如果能夠 SmartChineseAnalyzer 實現(xiàn)擴展詞楼誓,那么可以則能夠像停用詞一樣方便。
SmartChineseAnalyzer.createComponents() 方法的是最關(guān)鍵的實現(xiàn)名挥,代碼如下:
@Override
public TokenStreamComponents createComponents(String fieldName) {
final Tokenizer tokenizer = new HMMChineseTokenizer();
TokenStream result = tokenizer;
// result = new LowerCaseFilter(result);
// LowerCaseFilter is not needed, as SegTokenFilter lowercases Basic Latin text.
// The porter stemming is too strict, this is not a bug, this is a feature:)
result = new PorterStemFilter(result);
if (!stopWords.isEmpty()) {
result = new StopFilter(result, stopWords);
}
return new TokenStreamComponents(tokenizer, result);
}
但是 SmartChineseAnalyzer 類被關(guān)鍵字 final 修飾疟羹,也就意味著無法通過繼承來沿用 SmartChineseAnalyzer 的功能,但是可以通過繼承抽象類 Analyzer 并復(fù)寫 createComponents()、normalize() 方法榄融。實現(xiàn)代碼如下:
public class MySmartChineseAnalyzer extends Analyzer {
private CharArraySet stopWords;
public MySmartChineseAnalyzer(CharArraySet stopWords) {
this.stopWords = stopWords;
}
@Override
public Analyzer.TokenStreamComponents createComponents(String fieldName) {
final Tokenizer tokenizer = new HMMChineseTokenizer();
TokenStream result = tokenizer;
this is a feature:)
result = new PorterStemFilter(result);
if (!stopWords.isEmpty()) {
result = new StopFilter(result, stopWords);
}
return new TokenStreamComponents(tokenizer, result);
}
@Override
protected TokenStream normalize(String fieldName, TokenStream in) {
return new LowerCaseFilter(in);
}
}
通過上面的代碼我們既能實現(xiàn) SmartChineseAnalyzer 相同的功能参淫,但是仍不能實現(xiàn)對擴展詞的實現(xiàn)。參考
很顯然愧杯,上面的代碼分為代碼部分:
- 生成 tokenizer 對象涎才;
- 生成 tokenStream 對象,并進(jìn)行停用詞過濾力九;
- 使用 tokenizer耍铜、tokenStream 來構(gòu)建 TokenStreamComponents 對象。
因此跌前,我們可以參考 SmartChineseAnalyzer 中對停用詞的實現(xiàn)棕兼,來實現(xiàn)擴展詞。
StopFilter 是抽象類 TokenStream 的子類抵乓。其繼承鏈條如下:
StopFilter -> FilteringTokenFilter -> TokenFilter ->TokenStream
且 FilteringTokenFilter伴挚、TokenFilter、TokenStream 都是抽象類臂寝,其子類都需要復(fù)寫 incrementToken() 和實現(xiàn) accept()章鲤。
因此可自定義 ExtendWordFilter 來實現(xiàn)擴展詞的功能,ExtendWordFilter 繼承 TokenFilter咆贬,并復(fù)寫 IncrementToken()败徊,代碼如下:
public class ExtendWordFilter extends TokenFilter {
private int hadMatchedWordLength = 0;
private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
private final PositionIncrementAttribute posIncrAtt = addAttribute(PositionIncrementAttribute.class);
private final List<String> extendWords;
public ExtendWordFilter(TokenStream in, List<String> extendWords) {
super(in);
this.extendWords = extendWords;
}
@Override
public final boolean incrementToken() throws IOException {
int skippedPositions = 0;
while (input.incrementToken()) {
if (containsExtendWord()) {
if (skippedPositions != 0) {
posIncrAtt.setPositionIncrement(posIncrAtt.getPositionIncrement() + skippedPositions);
}
return true;
}
skippedPositions += posIncrAtt.getPositionIncrement();
}
return false;
}
protected boolean containsExtendWord() {
Optional<String> matchedWordOptional = extendWords.stream()
.filter(word -> word.contains(termAtt.toString()))
.findFirst();
if (matchedWordOptional.isPresent()) {
hadMatchedWordLength += termAtt.length();
if (hadMatchedWordLength == matchedWordOptional.get().length()) {
termAtt.setEmpty();
termAtt.append(matchedWordOptional.get());
return true;
}
} else {
hadMatchedWordLength = 0;
}
return matchedWordOptional.isEmpty();
}
}
incrementToken() 中主要是調(diào)用 setPositionIncrement() 設(shè)置數(shù)據(jù)讀取位置。containsExtendWord() 用來判斷是否包含擴展詞掏缎,以此為根據(jù)來合并和實現(xiàn)擴展詞皱蹦。
修改剛剛自定義的 CustomSmartChineseAnalyzer 中 createComponents() 方法,添加如下邏輯:
if (!words.isEmpty()) {
result = new ExtendWordFilter(result, words);
}
修改后的 CustomSmartChineseAnalyzer 完整代碼如下:
public class CustomSmartChineseAnalyzer extends Analyzer {
private CharArraySet extendWords;
private List<String> words;
private CharArraySet stopWords;
public CustomSmartChineseAnalyzer(CharArraySet stopWords, List<String> words) {
this.stopWords = stopWords;
this.words = words;
}
@Override
public Analyzer.TokenStreamComponents createComponents(String fieldName) {
final Tokenizer tokenizer = new HMMChineseTokenizer();
TokenStream result = tokenizer;
result = new LowerCaseFilter(result);
result = new PorterStemFilter(result);
if (!stopWords.isEmpty()) {
result = new StopFilter(result, stopWords);
}
if (!words.isEmpty()) {
result = new ExtendWordFilter(result, words);
}
return new TokenStreamComponents(tokenizer, result);
}
@Override
protected TokenStream normalize(String fieldName, TokenStream in) {
return new LowerCaseFilter(in);
}
}
最后調(diào)用測試代碼眷蜈,查看分詞結(jié)果沪哺。
@Test
void test_custom_smart_chinese_analyzer() throws IOException {
String text = "交易中臺架構(gòu)設(shè)計:海量并發(fā)的高擴展,新業(yè)務(wù)秒級接入";
CharArraySet stopWords = CharArraySet.unmodifiableSet(WordlistLoader.getWordSet(
IOUtils.getDecodingReader(
new ClassPathResource("stopwords.txt").getInputStream(),
StandardCharsets.UTF_8),
STOPWORD_FILE_COMMENT));
List<String> words = Collections.singletonList("中臺");
Analyzer analyzer = new CustomSmartChineseAnalyzer(stopWords, words);
TokenStream tokenStream = analyzer.tokenStream("testField", text);
OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
tokenStream.reset();
List<String> tokens = new ArrayList<>();
while (tokenStream.incrementToken()) {
tokens.add(offsetAttribute.toString());
}
tokenStream.end();
System.out.println(String.format("tokens:%s", tokens));
}
執(zhí)行結(jié)果如下:
[交易, 中臺, 架構(gòu), 設(shè)計, 海量, 并發(fā), 高, 擴展, 新, 業(yè)務(wù), 秒, 級, 接入]
上面的代碼只是初稿酌儒,存在著部分 Code Smell辜妓,感興趣的可以嘗試消除那些 Code Smell。
雖然實現(xiàn)了擴展詞的功能忌怎,但是是在叫高層的地方修改數(shù)據(jù)籍滴,且效率也并不佳,但是較容易擴展且擁有較好的可讀性榴啸。
如想提升性能孽惰,可以參考 HHMMSegmenter.process() 方法在分詞過程中實現(xiàn)停用詞、擴展詞等功能鸥印,并考慮擴展性勋功。