假設現在有10W+份word文檔,讓你做個web頁面,給出關鍵詞能快速搜索結果,你會怎麼做?那至少有3種方案,
順序掃描,每次檢測文檔中是否包含關鍵詞,包含則加入結果列表,不包含繼續查找下一個,直到找完為止。
將文檔內容導入資料庫,用SQL的like關鍵詞搜索。
用Lucene做全文檢索。
你會選哪種?首先順序掃描在數據量大的時候太慢,數據量比較少的話可行;導入資料庫數據量太大用like會引起全表掃描也會很慢,那當然是第三種,因為搜尋引擎Lucene就是專門為這種場景設計的:大量的,分結構化數據的快速搜索
看看官網怎麼對Lucene的介紹
可擴展,高性能的索引過程
在現代化的硬體上面索引超過150G/小時
只需要1M的堆
增量索引和批量索引一樣快
索引大小大約是文本索引大小的20-30%
強大,準確和高效的搜索算法
全文檢索原理全文檢索原理很簡單,就拿新華字典做比喻,假設字典沒有索引頁,你要找一個字的解釋你就得一頁一頁翻,直到找到為止,這效率可想而知
現在有了索引頁,就可以根據拼音首字母或偏旁部首快速定位到目標字在哪一頁,這個索引頁,就是全文檢索核心。
簡而言之:在非結構化數據中,將一部分結構化信息抽取出來,重新組織,然後針對這部分有結構的數據進行索引建立,從而達到加速查詢的目的。那麼
索引存啥?怎麼存?(對應字典的索引頁部分)
數據怎麼存?(對應字典非索引頁部分)
反向索引在Lucene 中,關鍵詞就是Term(比如英文裡面的一個單詞,中文的詞語),字典就是Term的集合。下圖是Lucene 的索引的底層存儲結構,左邊是字典,右邊保存的是包含左邊字符串的文檔編碼鍊表,稱之為倒排表
上面這種就叫做反向索引,是相對正向索引而言的:
比如上圖中包含"lucene"這個關鍵詞的文檔編碼有3,7,15,30,35,67,其他同理
單個詞查詢查詢的時候直接根據關鍵詞返回與之對應的倒排表即可。
多個詞查詢將要查詢的字符串分詞,分成多個關鍵詞,根據關鍵詞返回倒排表,再根據邏輯條件(且或非)整合倒排表
且:很簡單,多個倒排表直接做交集運算
或:併集運算
非:差集運算
字典當數據量很大時候,字典的數據就會很多,想想就知道一篇文章關鍵詞是很多的,字典要放到內存裡面隨時讀取才快,那麼字典的數據結構就顯得十分重要了,既不能太大太佔空間,效率也不能太低,下圖列了一些常見詞典的優缺點:
FST是Lucene現在使用的索引結構
理論基礎: 《Direct construction of minimal acyclic subsequential transducers》,通過輸入有序字符串構建最小有向無環圖。
優點:內存佔用率低,壓縮率一般在3倍~20倍之間、模糊查詢支持好、查詢快
缺點:結構複雜、輸入要求有序、更新不易 Lucene裡有個FST的實現,從對外接口上看,它跟Map結構很相似,有查找,有迭代
100萬數據性能測試:
數據結構HashMapTreeMapFST構建時間(ms)1855001512查詢所有key(ms)106218890可以看出,FST性能基本跟HaspMap差距不大,但FST有個不可比擬的優勢就是佔用內存小,只有HashMap10分之一左右,這對大數據規模檢索是至關重要的,畢竟速度再快放不進內存也是沒用的。因此一個合格的詞典結構要求有:
查詢速度。
內存佔用。
內存+磁碟結合。
所以最終的樣子如下,Term dict index以FST的結構存緩存在內存中,從Term dict index查到關鍵詞對應的term dic的塊位置之後,再去磁碟上找term,大大減少了磁碟的IO次數。
數據分段搞定了索引之後,再來看看數據是怎麼存儲的,試想一下如果所有數據(新華字典中所有的字)都存在一個文件中,如果數據有更新或者刪除(比如新增加一個字或者刪除一個字),那麼所有的索引都需要全量重新創建,這種方式在數據量很大時效率很低,並且由於創建一次索引的成本很高,所以對數據的更新不能過於頻繁,也就不能保證時效性。
Lucene在搜索中引入了段(Segment)的概念(將一個索引文件拆分為多個子文件,每個子文件叫作段),每個段都是一個獨立的可被搜索的數據集,並且段具有不變性,一旦索引的數據被寫入硬碟,就不可再修改。
在分段的思想下,對數據寫操作的過程如下。
新增。當有新的數據需要創建索引時,由於段的不變性,所以選擇新建一個段來存儲新增的數據。
刪除。當需要刪除數據時,由於數據所在的段只可讀,不可寫,所以Lucene在索引文件下新增了一個.del的文件,用來專門存儲被刪除的數據id。當查詢時,被刪除的數據還是可以被查到的,只是在進行文檔鍊表合併時,才把已經刪除的數據過濾掉。被刪除的數據在進行段合併時才會真正被移除。
更新。更新的操作其實就是刪除和新增的組合,先在.del文件中記錄舊數據,再在新段中添加一條更新後的數據。
段不可變的優點段不變性的優點如下:
不需要鎖。因為數據不會更新,所以不用考慮多線程下的讀寫不一致情況。
可以常駐內存。段在被加載到內存後,由於具有不變性,所以只要內存的空間足夠大,就可以長時間駐存,大部分查詢請求會直接訪問內存,而不需要訪問磁碟,使得查詢的性能有很大的提升。
緩存友好。在段的生命周期內始終有效,不需要在每次數據更新時被重建。
增量創建。分段可以做到增量創建索引,可以輕量級地對數據進行更新,由於每次創建的成本很低,所以可以頻繁地更新數據,使系統接近實時更新。
當對數據進行刪除時,舊數據不會被馬上刪除,而是在.del文件中被標記為刪除。而舊數據只能等到段更新時才能真正被移除,這樣會有大量的空間浪費。
更新。更新數據由刪除和新增這兩個動作組成。若有一條數據頻繁更新,則會有大量的空間浪費。
由於索引具有不變性,所以每次新增數據時,都需要新增一個段來存儲數據。當段的數量太多時,對伺服器的資源(如文件句柄)的消耗會非常大,查詢的性能也會受到影響。
在查詢後需要對已經刪除的舊數據進行過濾,這增加了查詢的負擔。
延遲寫策略為了提升寫的性能,Lucene並沒有每新增一條數據就增加一個段,而是採用延遲寫的策略,每當有新增的數據時,就將其先寫入內存中,然後批量寫入磁碟中。若有一個段被寫到硬碟,就會生成一個提交點,提交點就是一個用來記錄所有提交後的段信息的文件。一個段一旦擁有了提交點,就說明這個段只有讀的權限,失去了寫的權限
相反,當段在內存中時,就只有寫數據的權限,而不具備讀數據的權限,所以也就不能被檢索了。從嚴格意義上來說,Lucene或者Elasticsearch並不能被稱為實時的搜尋引擎,只能被稱為準實時的搜尋引擎。
寫索引的流程如下:
新數據被寫入時,並沒有被直接寫到硬碟中,而是被暫時寫到內存中。Lucene默認是一秒鐘,或者當內存中的數據量達到一定階段時,再批量提交到磁碟中,當然,默認的時間和數據量的大小是可以通過參數控制的。通過延時寫的策略,可以減少數據往磁碟上寫的次數,從而提升整體的寫入性能。如下圖所示。
在達到觸發條件以後,會將內存中緩存的數據一次性寫入磁碟中,並生成提交點。清空內存,等待新的數據寫入。如圖8所示。
合併原因:雖然分段比每次都全量創建索引有更高的效率,但由於在每次新增數據時都會新增一個段,所以經過長時間的積累,會導致在索引中存在大量的段,當索引中段的數量太多時,不僅會嚴重消耗伺服器的資源,還會影響檢索的性能。
因為索引檢索的過程是:查詢所有段中滿足查詢條件的數據,然後對每個段裡查詢的結果集進行合併,所以為了控制索引裡段的數量,我們必須定期進行段合併操作。但是如果每次合併全部的段,則將造成很大的資源浪費,特別是"大段"的合併。
所以Lucene現在的段合併思路是:根據段的大小先將段進行分組,再將屬於同一組的段進行合併。但是由於對超級大的段的合併需要消耗更多的資源,所以Lucene會在段的大小達到一定規模,或者段裡面的數據量達到一定條數時,不會再進行合併。所以Lucene的段合併主要集中在對中小段的合併上,這樣既可以避免對大段進行合併時消耗過多的伺服器資源,也可以很好地控制索引中段的數量。
總結反向索引:Lucene 採用了基於反向索引的設計原理,可以非常高效地實現文本查找
數據分段:在底層採用了數據分段的存儲模式,分段是只讀的,使它在讀寫時幾乎完全避免了鎖的出現,大大提升了讀寫性能。
參考Apache Lucene Core
5分鐘了解lucene全文索引
Lucene段概念
lucene搜索原理
ElasticSearch底層搜尋引擎Lucene原理剖析
Lucene底層原理和優化經驗分享(1)-Lucene簡介和索引原理
掌握它才說明你真正懂 Elasticsearch - Lucene (一)
使用Lucene在知識圖譜上構建索引