ElasticSearch 如何使用 ik 進行中文分詞?

image.png

你們好,我是歷小冰。在《爲何 ElasticSearch 比 MySQL 更適合複雜條件搜索》 一文中,咱們講解了 ElasticSearch 如何在數據存儲方面支持全文搜索和複雜條件查詢,本篇文章則着重分析 ElasticSearch 在全文搜索前如何使用 ik 進行分詞,讓你們對 ElasticSearch 的全文搜索和 ik 中文分詞原理有一個全面且深刻的瞭解。html

全文搜索和精確匹配

ElasticSearch 支持對文本類型數據進行全文搜索和精確搜索,可是必須提早爲其設置對應的類型:java

  • keyword 類型,存儲時不會作分詞處理,支持精確查詢和分詞匹配查詢;
  • text 類型,存儲時會進行分詞處理,也支持精確查詢和分詞匹配查詢。

好比,建立名爲 article 的索引(Index),併爲其兩個字段(Filed)配置映射(Mapping),文章內容設置爲 text 類型,而文章標題設置爲 keyword 類型。node

curl -XPUT http://localhost:9200/article
curl -XPOST http://localhost:9200/article/_mapping -H 'Content-Type:application/json' -d'
{
        "properties": {
            "content": {
                "type": "text"
            },
            "title": {
                "type": "keyword"
            }
        }

}'

Elasticsearch 在進行存儲時,會對文章內容字段進行分詞,獲取並保存分詞後的詞元(tokens);對文章標題則是不進行分詞處理,直接保存原值。git

image.png

上圖的右半邊展現了 keyword 和 text 兩種類型的不一樣存儲處理過程。而左半邊則展現了 ElasticSearch 相對應的兩種查詢方式:程序員

  • term 查詢,也就是精確查詢,不進行分詞,而是直接根據輸入詞進行查詢;
  • match 查詢,也就是分詞匹配查詢,先對輸入詞進行分詞,而後逐個對分詞後的詞元進行查詢。

舉個例子,有兩篇文章,一篇的標題和內容都是「程序員」,另一篇的標題和內容都是「程序」,那麼兩者在 ElasticSearch 中的倒排索引存儲以下所示(假設使用特殊分詞器)。github

image.png

這時,分別使用 term 和 match 查詢對兩個字段進行查詢,就會得出如圖右側的結果。算法

Analyzer 處理過程

可見,keyword 與 text 類型, term 與 match 查詢方式之間不一樣就在因而否進行了分詞。在 ElasticSearch 中將這個分詞的過程統稱了 Text analysis,也就是將字段從非結構化字符串(text)轉化爲結構化字符串(keyword)的過程。編程

Text analysis 不只僅只進行分詞操做,而是包含以下流程:json

  • 使用字符過濾器(Character filters),對原始的文本進行一些處理,例如去掉空白字符等;
  • 使用分詞器(Tokenizer),對原始的文本進行分詞處理,獲得一些詞元(tokens);
  • 使用詞元過濾器(Token filters),對上一步獲得的詞元繼續進行處理,例如改變詞元(小寫化),刪除詞元(刪除量詞)或增長詞元(增長同義詞),合併同義詞等。

image.png

ElasticSearch 中處理 Text analysis 的組件被稱爲 Analyzer。相應地,Analyzer 也由三部分組成,character filters、tokenizers 和 token filters。網絡

Elasticsearch 內置了 3 種字符過濾器、10 種分詞器和 31 種詞元過濾器。此外,還能夠經過插件機制獲取第三方實現的相應組件。開發者能夠按照自身需求定製 Analyzer 的組成部分。

"analyzer": {
    "my_analyzer": {
        "type":           "custom",
        "char_filter":  [ "html_strip"],
        "tokenizer":      "standard",
        "filter":       [ "lowercase",]
    }
}

按照上述配置,my_analyzer 分析器的功能大體以下:

  • 字符過濾器是 html_strip,會去掉 HTML 標記相關的字符;
  • 分詞器是 ElasticSearch 默認的標準分詞器 standard
  • 詞元過濾器是小寫化 lowercase 處理器,將英語單詞小寫化。

通常來講,Analyzer 中最爲重要的就是分詞器,分詞結果的好壞會直接影響到搜索的準確度和滿意度。ElasticSearch 默認的分詞器並非處理中文分詞的最優選擇,目前業界主要使用 ik 進行中文分詞。

ik 分詞原理

ik 是目前較爲主流的 ElasticSearch 開源中文分詞組件,它內置了基礎的中文詞庫和分詞算法幫忙開發者快速構建中文分詞和搜索功能,它還提供了擴展詞庫字典和遠程字典等功能,方便開發者擴充網絡新詞或流行語。

ik 提供了三種內置詞典,分別是:

  • main.dic:主詞典,包括平常的通用詞語,好比程序員和編程等;
  • quantifier.dic:量詞詞典,包括平常的量詞,好比米、公頃和小時等;
  • stopword.dic:停用詞,主要指英語的停用詞,好比 a、such、that 等。

此外,開發者能夠經過配置擴展詞庫字典和遠程字典對上述詞典進行擴展。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment>IK Analyzer 擴展配置</comment>
  <!--用戶能夠在這裏配置本身的擴展字典 -->
  <entry key="ext_dict">custom/mydict.dic</entry>
   <!--用戶能夠在這裏配置本身的擴展中止詞字典-->
  <entry key="ext_stopwords">custom/ext_stopword.dic</entry>
   <!--用戶能夠在這裏配置遠程擴展字典 -->
  <entry key="remote_ext_dict">location</entry>
   <!--用戶能夠在這裏配置遠程擴展中止詞字典-->
  <entry key="remote_ext_stopwords">http://xxx.com/xxx.dic</entry>
</properties>

ik 跟隨 ElasticSearch 啓動時,會將默認詞典和擴展詞典讀取並加載到內存,並使用字典樹 tire tree (也叫前綴樹)數據結構進行存儲,方便後續分詞時使用。

image.png

字典樹的典型結構如上圖所示,每一個節點是一個字,從根節點到葉節點,路徑上通過的字符鏈接起來,爲該節點對應的詞。因此上圖中的詞包括:程序員、程門立雪、編織、編碼和工做。

1、加載字典

ik 的 Dictionary 單例對象會在初始化時,調用對應的 load 函數讀取字典文件,構造三個由 DictSegment 組成的字典樹,分別是 MainDictQuantifierDictStopWords。咱們下面就來看一下其主詞典的加載和構造過程。loadMainDict 函數較爲簡單,它會首先建立一個 DictSegment 對象做爲字典樹的根節點,而後分別去加載默認主字典,擴展主字典和遠程主字典來填充字典樹。

private void loadMainDict() {
    // 創建一個主詞典實例
    _MainDict = new DictSegment((char) 0);

    // 讀取主詞典文件
    Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_MAIN);
    loadDictFile(_MainDict, file, false, "Main Dict");
    // 加載擴展詞典
    this.loadExtDict();
    // 加載遠程自定義詞庫
    this.loadRemoteExtDict();
}

loadDictFile 函數執行過程當中,會從詞典文件讀取一行一行的詞,交給 DictSegmentfillSegment 函數處理。

fillSegment 是構建字典樹的核心函數,具體實現以下所示,處理邏輯大體有以下幾個步驟:

  • 1、按照索引,獲取詞中的一個字;
  • 2、檢查當前節點的子節點中是否有該字,若是沒有,則將其加入到 charMap中;
  • 3、調用 lookforSegment 函數在字典樹中尋找表明該字的節點,若是沒有則插入一個新的;
  • 4、遞歸調用 fillSegment 函數處理下一個字。
private synchronized void fillSegment(char[] charArray , int begin , int length , int enabled){
    //獲取字典表中的漢字對象
    Character beginChar = Character.valueOf(charArray[begin]);
    Character keyChar = charMap.get(beginChar);
    //字典中沒有該字,則將其添加入字典
    if(keyChar == null){
        charMap.put(beginChar, beginChar);
        keyChar = beginChar;
    }
    
    //搜索當前節點的存儲,查詢對應keyChar的keyChar,若是沒有則建立
    DictSegment ds = lookforSegment(keyChar , enabled);
    if(ds != null){
        //處理keyChar對應的segment
        if(length > 1){
            //詞元尚未徹底加入詞典樹
            ds.fillSegment(charArray, begin + 1, length - 1 , enabled);
        }else if (length == 1){
            //已是詞元的最後一個char,設置當前節點狀態爲enabled,
            //enabled=1代表一個完整的詞,enabled=0表示從詞典中屏蔽當前詞
            ds.nodeState = enabled;
        }
    }
}

ik 初始化過程大體如此,再進一步詳細的邏輯你們能夠直接去看源碼,中間都是中文註釋,相對來講較爲容易閱讀。

2、分詞邏輯

ik 中實現了 ElasticSearch 相關的抽象類,來提供自身的分詞邏輯實現:

  • IKAnalyzer 繼承了 Analyzer ,用來提供中文分詞的分析器;
  • IKTokenizer 繼承了 Tokenizer,用來提供中文分詞的分詞器,其 incrementToken 是 ElasticSearch 調用 ik 進行分詞的入口函數。

incrementToken 函數會調用 IKSegmenternext方法,來獲取分詞結果,它是 ik 分詞的核心方法。

image.png

如上圖所示,IKSegmenter 中有三個分詞器,在進行分詞時會遍歷詞中的全部字,而後將單字按照順序,讓三個分詞器進行處理:

  • LetterSegmenter,英文分詞器比較簡單,就是把連續的英文字符進行分詞;
  • CN_QuantifierSegmenter,中文量詞分詞器,判斷當前的字符是不是數詞和量詞,會把連起來的數詞和量詞分紅一個詞;
  • CJKSegmenter,核心分詞器,基於前文的字典樹進行分詞。

咱們只講解一下 CJKSegmenter 的實現,其 analyze 函數大體分爲兩個邏輯:

  • 根據單字去字典樹中進行查詢,若是單字是詞,則生成詞元;若是是詞前綴,則放入到臨時命中列表中;
  • 而後根據單字和以前處理時保存的臨時命中列表數據一塊兒去字典樹中查詢,若是命中,則生成詞元。
public void analyze(AnalyzeContext context) {
            
    //優先處理tmpHits中的hit,根據單字和 hit 一塊兒去查詢
    if(!this.tmpHits.isEmpty()){
        ....
        for(Hit hit : tmpArray){
            hit = Dictionary.getSingleton().matchWithHit(context.getSegmentBuff(), context.getCursor() , hit);
            if(hit.isMatch()){
                //輸出當前的詞
                Lexeme newLexeme = new Lexeme(context.getBufferOffset() , hit.getBegin() , context.getCursor() - hit.getBegin() + 1 , Lexeme.TYPE_CNWORD);
                context.addLexeme(newLexeme);
                ....                 
            }else if(hit.isUnmatch()){
                //hit不是詞,移除
                this.tmpHits.remove(hit);
            }                    
        }
    }            
    
    //*********************************
    //再對當前指針位置的字符進行單字匹配
    Hit singleCharHit = Dictionary.getSingleton().matchInMainDict(context.getSegmentBuff(), context.getCursor(), 1);
    if(singleCharHit.isMatch()){//首字成詞
        //輸出當前的詞
        Lexeme newLexeme = new Lexeme(context.getBufferOffset() , context.getCursor() , 1 , Lexeme.TYPE_CNWORD);
        context.addLexeme(newLexeme);

        //同時也是詞前綴
        if(singleCharHit.isPrefix()){
            //前綴匹配則放入hit列表
            this.tmpHits.add(singleCharHit);
        }
    }else if(singleCharHit.isPrefix()){//首字爲詞前綴
        //前綴匹配則放入hit列表
        this.tmpHits.add(singleCharHit);
    }
    .... // 判斷是否結束,清理工做
}

具體的代碼邏輯,如上所示。爲了方便你們理解,舉個例子,好比輸入的詞是 編碼工做

  • 首先處理
  • 由於當前 tmpHits 爲空,直接進行單字判斷;
  • 直接拿 字去前文示意圖的字典樹查詢(詳見 matchInMainDict 函數),發現可以命中,而且該字不是一個詞的結尾,因此將 和其在輸入詞中的位置生成 Hit 對象,存儲到 tmpHits 中。
  • 接着處理
  • 由於 tmpHits 不爲空,因此拿着 對應的 Hit 對象和 字去字典樹中查詢(詳見 matchWithHit 函數), 發現命中了 編碼 一詞,因此將這個詞做爲輸出詞元之一,存入 AnalyzeContext;可是由於 已是葉節點,並無子節點,表示不是其餘詞的前綴,因此將對應的 Hit 對象刪除掉;
  • 接着拿單字 去字典樹中查詢,看單字是否成詞,或者構成詞的前綴。
  • 依次類推,將全部字處理完。

3、消除歧義和結果輸出

經過上述步驟,有時候會生成不少分詞結果集合,好比說,程序員愛編程 會被分紅 程序員程序編程 五個結果。這也是 ik 的 ik_max_word 模式的輸出結果。可是有些場景,開發者但願只有 程序員編程 三個分詞結果,這時就須要使用 ik 的 ik_smart 模式,也就是進行消除歧義處理。

ik 使用 IKArbitrator 進行消除歧義處理,主要使用組合遍歷的方式進行處理。從上一階段的分詞結果中取出不相交的分詞集合,所謂相交,就是其在文本中出現的位置是否重合。好比 程序員程序 三個分詞結果是相交的,可是 編程 是不相交的。因此分歧處理時會將 程序員程序 做爲一個集合, 做爲一個集合,編碼 做爲一個集合,分別進行處理,將集合中按照規則優先級最高的分詞結果集選出來,具體規則以下所示:

  • 有效文本長度長優先;
  • 詞元個數少優先;
  • 路徑跨度大優先;
  • 位置越靠後的優先,由於根據統計學結論,逆向切分機率高於正向切分;
  • 詞長越平均優先;
  • 詞元位置權重大優先。

根據上述規則,在第一個集合中,程序員 明顯要比 程序 要更符合規則,因此消除歧義的結果就是輸出 程序員,而不是 程序

最後,對於輸入字來講,有些位置可能並不在輸出結果中,因此會以單字的方式做爲詞元直接輸出(詳見AnalyzeContextoutputToResult 函數)。好比 程序員是職業 字是不會被分詞出來的,可是在最終輸出結果時,要將其做爲單字輸出。

後記

ElasticSearch 和 ik 組合是目前較爲主流的中文搜索技術方案,理解其搜索和分詞的基礎流程和原理,有利於開發者更快地構建中文搜索功能,或基於自身需求,特殊定製搜索分詞策略。

image.png

相關文章
相關標籤/搜索