大家好,我是歷小冰。在《爲什麼 ElasticSearch 比 MySQL 更適合複雜條件搜索》 一文中,我們講解了 ElasticSearch 如何在數據存儲方面支持全文搜索和複雜條件查詢,本篇文章則着重分析 ElasticSearch 在全文搜索前如何使用 ik 進行分詞,讓大家對 ElasticSearch 的全文搜索和 ik 中文分詞原理有一個全面且深入的瞭解。
全文搜索和精確匹配
ElasticSearch 支持對文本類型數據進行全文搜索和精確搜索,但是必須提前爲其設置對應的類型:
keyword 類型,存儲時不會做分詞處理,支持精確查詢和分詞匹配查詢;
text 類型,存儲時會進行分詞處理,也支持精確查詢和分詞匹配查詢。
比如,創建名爲 article
的索引(Index),併爲其兩個字段(Filed)配置映射(Mapping),文章內容設置爲 text 類型,而文章標題設置爲 keyword 類型。
Elasticsearch 在進行存儲時,會對文章內容字段進行分詞,獲取並保存分詞後的詞元(tokens);對文章標題則是不進行分詞處理,直接保存原值。
上圖的右半邊展示了 keyword 和 text 兩種類型的不同存儲處理過程。而左半邊則展示了 ElasticSearch 相對應的兩種查詢方式:
term 查詢,也就是精確查詢,不進行分詞,而是直接根據輸入詞進行查詢;
match 查詢,也就是分詞匹配查詢,先對輸入詞進行分詞,然後逐個對分詞後的詞元進行查詢。
舉個例子,有兩篇文章,一篇的標題和內容都是“程序員”,另外一篇的標題和內容都是“程序”,那麼二者在 ElasticSearch 中的倒排索引存儲如下所示(假設使用特殊分詞器)。
這時,分別使用 term 和 match 查詢對兩個字段進行查詢,就會得出如圖右側的結果。
Analyzer 處理過程
可見,keyword 與 text 類型, term 與 match 查詢方式之間不同就在於是否進行了分詞。在 ElasticSearch 中將這個分詞的過程統稱了 Text analysis,也就是將字段從非結構化字符串(text)轉化爲結構化字符串(keyword)的過程。
Text analysis 不僅僅只進行分詞操作,而是包含如下流程:
使用字符過濾器(Character filters),對原始的文本進行一些處理,例如去掉空白字符等;
使用分詞器(Tokenizer),對原始的文本進行分詞處理,得到一些詞元(tokens);
使用詞元過濾器(Token filters),對上一步得到的詞元繼續進行處理,例如改變詞元(小寫化),刪除詞元(刪除量詞)或增加詞元(增加同義詞),合併同義詞等。
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 等。
此外,開發者可以通過配置擴展詞庫字典和遠程字典對上述詞典進行擴展。
ik 跟隨 ElasticSearch 啓動時,會將默認詞典和擴展詞典讀取並加載到內存,並使用字典樹 tire tree (也叫前綴樹)數據結構進行存儲,方便後續分詞時使用。
字典樹的典型結構如上圖所示,每個節點是一個字,從根節點到葉節點,路徑上經過的字符連接起來,爲該節點對應的詞。所以上圖中的詞包括:程序員、程門立雪、編織、編碼和工作。
一、加載字典
ik 的 Dictionary
單例對象會在初始化時,調用對應的 load 函數讀取字典文件,構造三個由 DictSegment
組成的字典樹,分別是 MainDict
、QuantifierDict
和 StopWords
。我們下面就來看一下其主詞典的加載和構造過程。loadMainDict
函數較爲簡單,它會首先創建一個 DictSegment
對象作爲字典樹的根節點,然後分別去加載默認主字典,擴展主字典和遠程主字典來填充字典樹。
複製代碼
在 loadDictFile
函數執行過程中,會從詞典文件讀取一行一行的詞,交給 DictSegment
的fillSegment
函數處理。
fillSegment
是構建字典樹的核心函數,具體實現如下所示,處理邏輯大致有如下幾個步驟:
一、按照索引,獲取詞中的一個字;
二、檢查當前節點的子節點中是否有該字,如果沒有,則將其加入到
charMap
中;三、調用
lookforSegment
函數在字典樹中尋找代表該字的節點,如果沒有則插入一個新的;四、遞歸調用
fillSegment
函數處理下一個字。
ik 初始化過程大致如此,再進一步詳細的邏輯大家可以直接去看源碼,中間都是中文註釋,相對來說較爲容易閱讀。
二、分詞邏輯
ik 中實現了 ElasticSearch 相關的抽象類,來提供自身的分詞邏輯實現:
IKAnalyzer
繼承了Analyzer
,用來提供中文分詞的分析器;IKTokenizer
繼承了Tokenizer
,用來提供中文分詞的分詞器,其incrementToken
是 ElasticSearch 調用 ik 進行分詞的入口函數。
incrementToken
函數會調用 IKSegmenter
的 next
方法,來獲取分詞結果,它是 ik 分詞的核心方法。
如上圖所示,IKSegmenter
中有三個分詞器,在進行分詞時會遍歷詞中的所有字,然後將單字按照順序,讓三個分詞器進行處理:
LetterSegmenter
,英文分詞器比較簡單,就是把連續的英文字符進行分詞;CN_QuantifierSegmenter
,中文量詞分詞器,判斷當前的字符是否是數詞和量詞,會把連起來的數詞和量詞分成一個詞;CJKSegmenter
,核心分詞器,基於前文的字典樹進行分詞。
我們只講解一下 CJKSegmenter
的實現,其 analyze
函數大致分爲兩個邏輯:
根據單字去字典樹中進行查詢,如果單字是詞,則生成詞元;如果是詞前綴,則放入到臨時命中列表中;
然後根據單字和之前處理時保存的臨時命中列表數據一起去字典樹中查詢,如果命中,則生成詞元。
具體的代碼邏輯,如上所示。爲了方便大家理解,舉個例子,比如輸入的詞是 編碼工作
:
首先處理
編
字;因爲當前
tmpHits
爲空,直接進行單字判斷;直接拿
編
字去前文示意圖的字典樹查詢(詳見matchInMainDict
函數),發現能夠命中,並且該字不是一個詞的結尾,所以將編
和其在輸入詞中的位置生成Hit
對象,存儲到tmpHits
中。接着處理
碼
字;因爲
tmpHits
不爲空,所以拿着編
對應的Hit
對象和碼
字去字典樹中查詢(詳見matchWithHit
函數), 發現命中了編碼
一詞,所以將這個詞作爲輸出詞元之一,存入AnalyzeContext
;但是因爲碼
已經是葉節點,並沒有子節點,表示不是其他詞的前綴,所以將對應的Hit
對象刪除掉;接着拿單字
碼
去字典樹中查詢,看單字是否成詞,或者構成詞的前綴。依次類推,將所有字處理完。
三、消除歧義和結果輸出
通過上述步驟,有時候會生成很多分詞結果集合,比如說,程序員愛編程
會被分成 程序員
、程序
、員
、愛
和 編程
五個結果。這也是 ik 的 ik_max_word 模式的輸出結果。但是有些場景,開發者希望只有 程序員
、愛
和 編程
三個分詞結果,這時就需要使用 ik 的 ik_smart 模式,也就是進行消除歧義處理。
ik 使用 IKArbitrator
進行消除歧義處理,主要使用組合遍歷的方式進行處理。從上一階段的分詞結果中取出不相交的分詞集合,所謂相交,就是其在文本中出現的位置是否重合。比如 程序員
、程序
和 員
三個分詞結果是相交的,但是 愛
和 編程
是不相交的。所以分歧處理時會將 程序員
、程序
和 員
作爲一個集合,愛
作爲一個集合,編碼
作爲一個集合,分別進行處理,將集合中按照規則優先級最高的分詞結果集選出來,具體規則如下所示:
有效文本長度長優先;
詞元個數少優先;
路徑跨度大優先;
位置越靠後的優先,因爲根據統計學結論,逆向切分概率高於正向切分;
詞長越平均優先;
詞元位置權重大優先。
根據上述規則,在第一個集合中,程序員
明顯要比 程序
和 員
要更符合規則,所以消除歧義的結果就是輸出 程序員
,而不是 程序
和 員
。
最後,對於輸入字來說,有些位置可能並不在輸出結果中,所以會以單字的方式作爲詞元直接輸出(詳見AnalyzeContext
的 outputToResult
函數)。比如 程序員是職業
,是
字是不會被分詞出來的,但是在最終輸出結果時,要將其作爲單字輸出。
後記
ElasticSearch 和 ik 組合是目前較爲主流的中文搜索技術方案,理解其搜索和分詞的基礎流程和原理,有利於開發者更快地構建中文搜索功能,或基於自身需求,特殊定製搜索分詞策略。
-關注我
推薦閱讀
AbstractQueuedSynchronizer超詳細原理解析
本文分享自微信公衆號 - 程序員歷小冰(gh_a1d0b50d8f0a)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。