1.分詞 全文檢索必須要分詞,所謂分詞就是把一句話切分成一個個單獨的詞。分詞有很多演算法,比如自然分詞、n-gram分詞、字典分詞等等。對中文來說沒有自然分隔符,一般採用字典分詞,再加上對人名、地名的特殊處理,提高分詞的準確性。 我們使用ik分片語件,ik有兩種分詞策略:smart策略、max wor ...
1.分詞
全文檢索必須要分詞,所謂分詞就是把一句話切分成一個個單獨的詞。分詞有很多演算法,比如自然分詞、n-gram分詞、字典分詞等等。對中文來說沒有自然分隔符,一般採用字典分詞,再加上對人名、地名的特殊處理,提高分詞的準確性。
我們使用ik分片語件,ik有兩種分詞策略:smart策略、max word策略。
例如這個句子:
1939年的德國,9歲的小女孩莉賽爾和弟弟被迫送往慕尼黑遠郊的寄養家庭。6歲的弟弟不幸死在了路途中。在冷清的葬禮後,莉賽爾意外得到她的第一本書《掘墓人手冊》。 |
看一下分詞的結果,先看smart策略:
1939年/德國/9歲/小女孩/莉/賽/爾/和/弟弟/被迫/送往/慕尼黑/遠郊/寄養/家庭/6歲/弟弟/不幸/死/路/途中/冷清/葬禮/後/莉/賽/爾/意外/得/到她/第一/本書/掘墓人/手冊 |
Elasticsearch支持分詞介面,比如這個介面: http://localhost:9200/index/_analyze?text=1939年的德國&analyzer=ik_smart 可以執行一個分詞計算,使用的分詞器是ik_smart。在分詞結果中可以看到每個詞的位置和類型。 |
句子按照中文語法被分割成一個個詞,仔細觀察一下可以看到兩個現象:
1. 標點符號都不見了
2. 少了一些字,比如“的”、“在”、“了”
這個處理叫做“預處理”,預處理過程會去掉句子中的符號、停止詞(stop word),這兩種元素在檢索中沒有什麼作用,對排序還會造成很大的干擾,在索引的時候就被去除掉了。
對於英文等西方文字,由於詞之間存在自然分割,所以分詞的難度低,準確性高。拼音文字一般存在單複數、時態、詞性等詞尾的變化,比如make、made、makes,索引的時候還需要做詞形變換,全部處理成原型。
從分詞結果中還可以看到,一些分詞結果不准確,比如“1939年”、“得/到她”,因為分詞不准確,當用戶搜索“1939”、“得到”就檢索不到這個文檔,儘管文檔中出現了這些詞。還有“莉/賽/爾”,ik不能識別這個詞,分割成了三個獨立的字。如果用戶搜索“賽莉爾”,也能搜出文檔。
為了避免分詞不准確影響召回率的情況,可以使用max word策略:
1939/年/德國/9/歲/小女孩/小女/女孩/莉/賽/爾/和/弟弟/被迫/迫/送往/慕尼黑/慕/尼/黑/遠郊/郊/寄養/寄/養家/家庭/家/庭/6/歲/弟弟/不幸/死/路途/途中/途/中/冷清/冷/清/葬禮/葬/禮/後/莉/賽/爾/意外/得到/到她/第一本/第一/一本書/一本/一/本書/本/書/掘墓人/墓/人手/手冊/冊 |
max word多分出了很多詞,這樣就增加了檢索的召回率,但是正確率有所降低,也增加了索引的體積。
我們的策略是:對fulltext欄位使用max_word分詞策略進行索引。term檢索不對查詢詞做分詞處理,輸入什麼詞就檢索什麼詞。match_query檢索對查詢詞用smart策略。span_near_query檢索對查詢詞使用max_word策略,確保分詞結果一致。這樣做準確率和召回率都比較合適。
2.反向索引
反向索引也叫倒排索引,存儲了詞在文檔中的位置。比如有下麵三個文檔,已經做好了分詞:
doc1: A/BC/D/EF
doc2: BC/D/XY/Z/Z
doc3: BC/E/MN/XY
首先統計這三個文檔中出現的所有的詞,給這些詞編號,並且統計詞頻:
詞 | 編號 | 詞頻 |
A | 1 | 1 |
BC | 2 | 3 |
D | 3 | 2 |
EF | 4 | 1 |
XY | 5 | 2 |
Z | 6 | 2 |
E | 7 | 1 |
MN | 8 | 1 |
然後統計這些詞出現在哪些文檔中,以及出現的位置:
詞編號 | 文檔和位置 |
1 | (doc1,0) |
2 | (doc1,1),(doc2,0),(doc3,0) |
3 | (doc1,2),(doc2,1) |
4 | (doc1,3) |
5 | (doc2,2),(doc3,3) |
6 | (doc2,3,4) |
7 | (doc3,1) |
8 | (doc3,2) |
這樣反向索引就建立起來了。現在進行檢索,輸入“BDC”。首先對輸入條件進行分詞,得到“BC/D”,分別檢索BC和D的集合,查反向索引:
{doc1, doc2, doc3} AND {doc1, doc2} = {doc1, doc2} |
得到搜索結果集合doc1和doc2。
Elasticsearch是一個分散式的全文檢索資料庫,封裝了Lucene的功能,倒排索引的功能是在Lucene中實現的。Lucene的數據目錄中保存的就是倒排索引數據,包括了詞典和文檔數據,壓縮存儲。Lucene的索引是分域存儲的,比純粹的文本索引複雜的多,具體的原理可以看Lucene代碼。
3.排序
把關鍵詞從文檔的海洋中檢索出來只是萬里長征走完了第一步,後面還有一件更重要、難度也更大的事情:排序。對於海量數據來說,排序不合理和檢索不正確造成的後果其實沒有多大的區別(甚至要更嚴重,個人觀點)。
Lucene預設的排序方式是根據關鍵詞文檔相關性,預設的演算法是TF-IDF。TF詞頻(Term Frequency),IDF逆向文件頻率(Inverse Document Frequency)。
具體的公式這裡就不寫了,維基百科上有個例子,https://zh.wikipedia.org/wiki/TF-IDF,這裡說明一下:
首先計算單詞的TF-IDF:假如一篇文件的總詞語數是100個,而詞語“母牛”出現了3次,那麼“母牛”一詞在該文件中的詞頻就是3/100=0.03。一個計算文件頻率(DF)的方法是測定有多少份文件出現過“母牛”一詞,然後除以文件集里包含的文件總數。所以,如果“母牛”一詞在1,000份文件出現過,而文件總數是10,000,000份的話,其逆向文件頻率就是log(10,000,000 / 1,000)=4。最後的TF-IDF的分數為0.03 * 4=0.12。
再計算關鍵詞與文檔的相關性:根據關鍵字k1,k2,k3進行搜索結果的相關性就變成TF1 * IDF1 + TF2 * IDF2 + TF3 * IDF3。比如document1的term總量為1000,k1,k2,k3在document1出現的次數是100,200,50。包含了k1, k2, k3的document總量分別是 1000,10000,5000。document set的總量為10000。 TF1 = 100/1000 = 0.1 TF2 = 200/1000 = 0.2 TF3 = 50/1000 = 0.05 IDF1 = In(10000/1000) = In (10) = 2.3 IDF2 = In(10000/10000) = In (1) = 0; IDF3 = In(10000/5000) = In (2) = 0.69 這樣關鍵字k1,k2,k3與document1的相關性 = 0.1 * 2.3 + 0.2 * 0 + 0.05 * 0.69 = 0.2645 其中k1比k3的比重在document1要大,k2的比重是0.
這是理論上的相關性演算法,實際上可以根據其他因素來修正。比如Google的page rank演算法,會提前根據連接數和引用數算出網頁的page rank得分,計算關鍵詞相關性的時候還會考慮關鍵詞在網頁上出現的位置(title、meta、正文標題、正文內容、側邊欄等等),給出不同的相關度。也可以根據某個業務屬性強制排序(比如create_time等等)。
Elasticsearch是一個分散式的Lucene,排序工作實際上是Lucene做的,Elasticsearch做了一個分步執行、集中排序。
4.檢索方式
4.1 term檢索
term檢索的條件寫法,用SQL表達就是:
SELECT * FROM index/type WHERE status='yes' |
term是不分詞檢索,也就是對檢索條件不分詞。所以一般對非文本欄位、不分詞的文本欄位使用這樣的檢索。對分詞文本欄位用term檢索要慎重。比如對這樣的文檔:
fulltext: AB/CD/E/FG |
如果用term檢索:
WHERE fulltext='CDE' |
由於文檔的fulltext欄位中不存在“CDE”這個詞,所以檢索不到文檔。
4.2 matchQuery檢索
matchQuery條件寫法:
SELECT * FROM index/type WHERE fulltext=matchQuery('ABE') |
matchQuery是分詞檢索,先對檢索條件進行分詞,得到“AB/E”,然後尋找fulltext中同時包含“AB”和“E”的文檔。我們對fulltext進行ik max word分詞,matchQuery使用ik smart分詞,這樣能夠最大限度的避免分詞不准確對效果的影響。
4.3 spanNearQuery檢索
spanNearQuery條件寫法:
SELECT * FROM index/type WHERE fulltext=spanNearQuery('ABE') |
spanNearQuery是分詞檢索,同時要求條件在文檔中一定要連續出現。所以spanNearQuery要求索引和檢索一定要使用完全相同的分詞策略。比如對“遠郊寄養家庭”,分詞後是“遠郊/郊/寄養/寄/養家/家庭/家/庭”,如果檢索條件是:
WHERE fulltext=spanNearQuery('寄養家庭') |
搜索時先對條件使用同樣的分詞策略,然後判斷fulltext中是否連續出現搜索詞。
4.4 query檢索
query條件寫法:
SELECT * FROM index/type WHERE fulltext=query('chin*') |
query也是分詞檢索,首先在字典中尋找符合“chin*”通配符的詞,比如“china”、“chinese”等等。然後尋找含有任一詞的文檔。
5.自定義詞典
ik分片語件使用詞典分詞,可以修改詞典定義提高分詞的準確性。
ik組件可以使用自定義詞典,配置文件:/server/elasticsearch/config/ik/IKAnalyzer.cfg.xml
<?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;custom/single_word_low_freq.dic</entry> <!--用戶可以在這裡配置自己的擴展停止詞字典--> <entry key="ext_stopwords">custom/ext_stopword.dic</entry> <!--用戶可以在這裡配置遠程擴展字典 --> <entry key="remote_ext_dict">http://localhost/ext_dict.php</entry> <!--用戶可以在這裡配置遠程擴展停止詞字典--> <entry key="remote_ext_stopwords">http://localhost/ext_stopwords.php</entry> </properties> |
修改本地詞典和停止詞文件,需要在每個Elasticsearch節點上修改文件,修改後重啟。
使用遠程字典可以集中維護字典文件,並且不需要重啟Elasticsearch。ik每分鐘向遠程地址發出請求,請求有一個“If-Modified-Since”消息頭。如果字典沒有變動,返回“304 Not Modified”,ik不會更新字典。如果字典內容有變動,返回字典內容和新的“Last-Modified”,ik更新字典。