Lucene的Smart CN實現(xiàn)分詞次兆、停用詞、擴展詞

0.png

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),可以查看一些源碼中的停用詞配置信息镜豹。

00.png

其中設(shè)定的默認(rèn)停用詞會讀取 stopwards.txt 文件中停用詞傲须,可以在引入的 jar 包中找到該文件。

01.png

其內(nèi)容主要是一些標(biāo)點符號作為停用詞趟脂。

[ , 泰讽、, 。, 昔期!, , (, ), 《, 》, 已卸,, -, 【, 】, —, :, ;, “, ”, 硼一?, !, ", #, $, &, ', (, ), *, +, ,, -, ., /, ·, :, [, <, ], >, ?, @, ●, [, \, ], ^, _, `, ;, =, {, |, }, ~]

在 stopwords.txt 中雖然只是提供了一些標(biāo)點符號作為停用詞但是其中定義了停用詞的三個類別:Punctuation tokens to remove累澡、English Stop Words、Chinese Stop Words般贼。

02.png

因此可以按照 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)。參考

很顯然愧杯,上面的代碼分為代碼部分:

  1. 生成 tokenizer 對象涎才;
  2. 生成 tokenStream 對象,并進(jìn)行停用詞過濾力九;
  3. 使用 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)停用詞、擴展詞等功能鸥印,并考慮擴展性勋功。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末坦报,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子狂鞋,更是在濱河造成了極大的恐慌片择,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,185評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件要销,死亡現(xiàn)場離奇詭異构回,居然都是意外死亡夏块,警方通過查閱死者的電腦和手機疏咐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,445評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脐供,“玉大人浑塞,你說我怎么就攤上這事≌海” “怎么了酌壕?”我有些...
    開封第一講書人閱讀 157,684評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長歇由。 經(jīng)常有香客問我卵牍,道長,這世上最難降的妖魔是什么沦泌? 我笑而不...
    開封第一講書人閱讀 56,564評論 1 284
  • 正文 為了忘掉前任糊昙,我火速辦了婚禮,結(jié)果婚禮上谢谦,老公的妹妹穿的比我還像新娘释牺。我一直安慰自己,他們只是感情好回挽,可當(dāng)我...
    茶點故事閱讀 65,681評論 6 386
  • 文/花漫 我一把揭開白布没咙。 她就那樣靜靜地躺著,像睡著了一般千劈。 火紅的嫁衣襯著肌膚如雪祭刚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,874評論 1 290
  • 那天墙牌,我揣著相機與錄音涡驮,去河邊找鬼。 笑死憔古,一個胖子當(dāng)著我的面吹牛遮怜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鸿市,決...
    沈念sama閱讀 39,025評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼锯梁,長吁一口氣:“原來是場噩夢啊……” “哼即碗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起陌凳,我...
    開封第一講書人閱讀 37,761評論 0 268
  • 序言:老撾萬榮一對情侶失蹤剥懒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后合敦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體初橘,經(jīng)...
    沈念sama閱讀 44,217評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,545評論 2 327
  • 正文 我和宋清朗相戀三年充岛,在試婚紗的時候發(fā)現(xiàn)自己被綠了保檐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,694評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡崔梗,死狀恐怖夜只,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蒜魄,我是刑警寧澤扔亥,帶...
    沈念sama閱讀 34,351評論 4 332
  • 正文 年R本政府宣布,位于F島的核電站谈为,受9級特大地震影響旅挤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜伞鲫,卻給世界環(huán)境...
    茶點故事閱讀 39,988評論 3 315
  • 文/蒙蒙 一粘茄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧榔昔,春花似錦驹闰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,778評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至诵肛,卻和暖如春屹培,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背怔檩。 一陣腳步聲響...
    開封第一講書人閱讀 32,007評論 1 266
  • 我被黑心中介騙來泰國打工褪秀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人薛训。 一個月前我還...
    沈念sama閱讀 46,427評論 2 360
  • 正文 我出身青樓媒吗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親乙埃。 傳聞我的和親對象是個殘疾皇子闸英,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,580評論 2 349