Lucene單詞詞典
使用lucene進行查詢不可避免都會使用到其提供的單詞詞典功能,即根據給定的term找到該term所對應的倒排文檔id列表等信息。實際上lucene索引文件后綴名為tim和tip的文件實現的就是lucene的單詞詞典功能。
怎么實現一個單詞詞典呢?我們馬上想到排序數組,即term字典是一個已經按字母順序排序好的數組,數組每一項存放着term和對應的倒排文檔id列表。每次載入索引的時候只要將term數組載入內存,通過二分查找即可。這種方法查詢時間復雜度為Log(N),N指的是term數目,占用的空間大小是O(N*str(term))。排序數組的缺點是消耗內存,即需要完整存儲每一個term,當term數目多達上千萬時,占用的內存將不可接受。
單詞詞典是倒排索引中非常重要的組成部分,它用來維護文檔集合中出現過的所有單詞的相關信息,同時用來記載某個單詞對應的倒排列表在倒排文件中的位置信息,如圖1-2所示。在支持搜索時,根據用戶的查詢詞,去單詞詞典里查詢,就能夠獲得相應的倒排列表,並以此作為后續排序的基礎。
對於一個規模很大的文檔集合來說,可能包含幾十萬甚至上百萬的不同單詞,能否快速定位某個單詞,這直接影響搜索時的響應速度,所以需要高效的數據結構來對單詞詞典進行構建 和查找,常用的數據結構包括哈希加鏈表結構和樹形詞典結構和FST有窮狀態轉換器。
很多數據結構均能完成字典功能,總結如下。
數據結構 | 優缺點 |
排序列表Array/List | 使用二分法查找,不平衡 |
HashMap/TreeMap | 性能高,內存消耗大,幾乎是原始數據的三倍 |
Skip List | 跳躍表,可快速查找詞語,在lucene、redis、Hbase等均有實現。相對於TreeMap等結構,特別適合高並發場景(Skip List介紹) |
Trie | 適合英文詞典,如果系統中存在大量字符串且這些字符串基本沒有公共前綴,則相應的trie樹將非常消耗內存(數據結構之trie樹) |
Double Array Trie | 適合做中文詞典,內存占用小,很多分詞工具均采用此種算法(深入雙數組Trie) |
Ternary Search Tree | 三叉樹,每一個node有3個節點,兼具省空間和查詢快的優點(Ternary Search Tree) |
Finite State Transducers (FST) | 一種有限狀態轉移機,Lucene 4有開源實現,並大量使用 |
一:哈希加鏈表
圖 1-3 是這種詞典結構的示意圖。這種詞典結構主要由兩個部分構成,主體部分是哈希表, 每個哈希表項保存一個指針,指針指向沖突鏈表,在沖突鏈表里,相同哈希值的單詞形成鏈表結構。之所以會有沖突鏈表,是因為兩個不同單詞獲得相同的哈希值,如果是這樣,在哈希方法里被稱做是一次沖突,可以將相同哈希值的單詞存儲在鏈表里,以供后續查找。
注:
-》主體部分為什么使用哈希表?
數組、向量、集合等都可以存儲對象,但是對象的位置是隨機的,也就是說對象本身和位置並沒有必然的聯系,當要查找一個對象時,只能以某種順序(如順序查找或二分查找)與各個元素進行比較,當數組或向量或集合中的元素數量很多時,查找的效率會明顯的降低。Hash表利用key、value模式,散列無序存儲,效率高很多。
-》為什么會有Hash沖突?
哈希的原理就是抽樣,取信息的特征。兩個不同單詞通過hash算法獲得哈希值是有可能相同的(如果是這樣,在哈希方法里被稱做是一次沖突),而實際的哈希表,全部都要處理哈希值沖突的情況。而鏈表法就是解決hash沖突的一種手段(可以將相同哈希值的單詞存儲在鏈表里,以供后續查找),如:向哈希表中添加一個詞匯2時,如果哈希表中已經存在一個具有相同哈希值的詞匯1,則將詞匯2添加到以詞匯1為頭節點的鏈表中。
圖1-3 哈希加沖突鏈表詞典結構
在建立索引的過程中,詞典結構也會相應地被構建出來。比如在解析一個新文檔的時候,對於某個在文檔中出現的詞匯A:
第1步:首先利用哈希函數獲得其哈希值
第2步:之后根據哈希值對應的哈希表項讀取其中保存的指針,找到了對應的沖突鏈表。
第3步:如果沖突鏈表里已經存在這個單詞, 說明單詞在之前解析的文檔里已經出現過。
第4步:如果在沖突鏈表里沒有發現這個單詞,說明該單詞是首次碰到,則將其加入沖突鏈表里。
通過這種方式,當文檔集合內所有文檔解析完畢時,相應的詞典結構也就建立起來了。
在響應用戶查詢請求時,其過程與建立詞典類似,不同點在於即使詞典里沒出現過某個單詞, 也不會添加到詞典內。以圖 1-3 為例,
第1步:假設用戶輸入的查詢請求為單詞3,對這個單詞進行哈希。
第2步:定位到哈希表內的 2 號槽。
第3步:從其保留的指針可以獲得沖突鏈表。
第4步:依次將單詞 3 和沖突鏈表內的單詞比較,發現單詞 3 在沖突鏈表內,於是找到這個單詞。之后可以讀出這個單詞對應的倒排列表來進行后續的工作。
第5步:如果沒有找到這個單詞,說明文檔集合內沒有任何文檔包含單詞,則搜索結果為空。
二:樹形結構
B 樹(或者 B+樹)是另外一種高效查找結構,圖 1-4 是一個 B 樹結構示意圖。B樹與哈希方式查找不同,需要字典項能夠按照大小排序(數字或者字符序),而哈希方式則無須數據滿足此項要求。
B 樹形成了層級查找結構,中間節點用於指出一定順序范圍的詞典項目存儲在哪個子樹中, 起到根據詞典項比較大小進行導航的作用,最底層的葉子節點存儲單詞的地址信息,根據這個地址就可以提取出單詞字符串。
圖1-4 B樹查找結構
三:FST有窮狀態轉換器
Finite State Transducers 簡稱 FST, 中文名:有窮狀態轉換器。在自然語言處理等領域有很大應用,其功能類似於字典的功能(STL 中的map,C# 中的Dictionary),但其查找是O(1)的,僅僅等於所查找的key長度。目前Lucene4.0在查找Term時就用到了該算法來確定此Term在字典中的位置。
lucene從4開始大量使用的數據結構是FST(Finite State Transducer)。FST有兩個優點:1)空間占用小。通過對詞典中單詞前綴和后綴的重復利用,壓縮了存儲空間;2)查詢速度快。O(len(str))的查詢時間復雜度。
下面簡單描述下FST的構造過程。我們對“cat”、 “deep”、 “do”、 “dog” 、“dogs”這5個單詞進行插入構建FST(注:必須已排序)。
1)插入“cat”
插入cat,每個字母形成一條邊,其中t邊指向終點。
2)插入“deep”
與前一個單詞“cat”進行最大前綴匹配,發現沒有匹配則直接插入,P邊指向終點。
3)插入“do”
與前一個單詞“deep”進行最大前綴匹配,發現是d,則在d邊后增加新邊o,o邊指向終點。
4)插入“dog”
與前一個單詞“do”進行最大前綴匹配,發現是do,則在o邊后增加新邊g,g邊指向終點。
5)插入“dogs”
與前一個單詞“dog”進行最大前綴匹配,發現是dog,則在g后增加新邊s,s邊指向終點。
最終我們得到了如上一個有向無環圖。利用該結構可以很方便的進行查詢,如給定一個term “dog”,我們可以通過上述結構很方便的查詢存不存在,甚至我們在構建過程中可以將單詞與某一數字、單詞進行關聯,從而實現key-value的映射。
FST壓縮率一般在3倍~20倍之間,相對於TreeMap/HashMap的膨脹3倍,內存節省就有9倍到60倍!那FST在性能方面真的能滿足要求嗎?
下面是我在蘋果筆記本(i7處理器)進行的簡單測試,性能雖不如TreeMap和HashMap,但也算良好,能夠滿足大部分應用的需求。
參考博文:
sbp810050504 《lucene (42)源代碼學習之FST(Finite State Transducer)在SynonymFilter中的實現思想》
zhanlijun 《lucene字典實現原理》