分段儲存
在早期的全文檢索中為整個文件集合建立了一個很大的倒排索引,並將其寫入磁碟中,如果索引有更新,就需要重新全量建立一個索引來替換原來的索引。
這種方式在資料量很大時效率很低,並且由於建立一次索引的成本很高,所以對資料的更新不能過於頻繁,也就不能保證實效性。
現在,在搜尋中引入了段的概念(將一個索引檔案拆分為多個子檔案,則每個子檔案叫做段),每個段都是一個獨立的可被搜尋的資料集,並且段具有不變性,一旦索引的資料被寫入硬碟,就不可修改。
在分段的思想下,對資料寫操作的過程如下:
-
新增:當有新的資料需要建立索引時,由於段段不變性,所以選擇新建一個段來儲存新增的資料。
-
刪除:當需要刪除資料時,由於資料所在的段只可讀,不可寫,所以 Lucene 在索引檔案新增一個 .del 的檔案,用來專門儲存被刪除的資料 id。
當查詢時,被刪除的資料還是可以被查到的,只是在進行文件連結串列合併時,才把已經刪除的資料過濾掉。被刪除的資料在進行段合併時才會被真正被移除。
-
更新:更新的操作其實就是刪除和新增的組合,先在.del檔案中記錄舊資料,再在新段中新增一條更新後的資料。
段不可變性的優點如下:
-
不需要鎖:因為資料不會更新,所以不用考慮多執行緒下的讀寫不一致情況。
-
可以常駐記憶體:段在被載入到記憶體後,由於具有不變性,所以只要記憶體的空間足夠大,就可以長時間駐存,大部分查詢請求會直接訪問記憶體,而不需要訪問磁碟,使得查詢的效能有很大的提升。
-
快取友好:在段的宣告週期內始終有效,不需要在每次資料更新時被重建。
-
增量建立:分段可以做到增量建立索引,可以輕量級地對資料進行更新,由於每次建立的成本很低,所以可以頻繁地更新資料,使系統接近實時更新。
段不可變性的缺點如下:
-
刪除:當對資料進行刪除時,舊資料不會被馬上刪除,而是在 .del 檔案中被標記為刪除。而舊資料只能等到段更新時才能真正地被移除,這樣會有大量的空間浪費。
-
更新:更新資料由刪除和新增這兩個動作組成。若有一條資料頻繁更新,則會有大量的空間浪費。
-
新增:由於索引具有不變性,所以每次新增資料時,都需要新增一個段來儲存資料。當段段數量太多時,對伺服器的資源(如檔案控制程式碼)的消耗會非常大,查詢的效能也會受到影響。
-
過濾:在查詢後需要對已經刪除的舊資料進行過濾,這增加了查詢的負擔。
為了提升寫的效能,Lucene 並沒有每新增一條資料就增加一個段,而是採用延遲寫的策略,每當有新增的資料時,就將其先寫入記憶體中,然後批量寫入磁碟中。
若有一個段被寫到硬碟,就會生成一個提交點,提交點就是一個用來記錄所有提交後的段資訊的檔案。
一個段一旦擁有了提交點,就說明這個段只有讀的許可權,失去了寫的許可權;相反,當段在記憶體中時,就只有寫資料的許可權,而不具備讀資料的許可權,所以也就不能被檢索了。
從嚴格意義上來說,Lucene 或者 Elasticsearch 並不能被稱為實時的搜尋引擎,只能被稱為準實時的搜尋引擎。
寫索引的流程如下:
-
新資料被寫入時,並沒有被直接寫到硬碟中,而是被暫時寫到記憶體中。Lucene 預設是一秒鐘,或者當記憶體中資料量達到一定階段時,再批量提交到磁碟中。
當然,預設的時間和資料量的大小是可以通過引數控制的。通過延時寫的策略,可以減少資料往磁碟上寫的次數,從而提升整體的寫入效能,如圖 3。
-
在達到出觸發條件以後,會將記憶體中快取的資料一次性寫入磁碟中,並生成提交點。
-
清空記憶體,等待新的資料寫入,如下圖所示。
從上述流程可以看出,資料先被暫時快取在記憶體中,在達到一定的條件再被一次性寫入硬碟中,這種做法可以大大提升資料寫入的速度。
但是資料先被暫時存放在記憶體中,並沒有真正持久化到磁碟中,所以如果這時出現斷電等不可控的情況,就會丟失資料,為此,Elasticsearch 新增了事務日誌,來保證資料的安全。
段合併策略
雖然分段比每次都全量建立索引有更高的效率,但是由於在每次新增資料時都會新增一個段,所以經過長時間的的積累,會導致在索引中存在大量的段。
當索引中段的數量太多時,不僅會嚴重消耗伺服器的資源,還會影響檢索的效能。
因為索引檢索的過程是:查詢所有段中滿足查詢條件的資料,然後對每個段裡查詢的結果集進行合併,所以為了控制索引裡段的數量,我們必須定期進行段合併操作。
但是如果每次合併全部的段,則會造成很大的資源浪費,特別是“大段”的合併。
所以 Lucene 現在的段合併思路是:根據段的大小將段進行分組,再將屬於同一組的段進行合併。
但是由於對於超級大的段的合併需要消耗更多的資源,所以 Lucene 會在段的大小達到一定規模,或者段裡面的資料量達到一定條數時,不會再進行合併。
所以 Lucene 的段合併主要集中在對中小段的合併上,這樣既可以避免對大段進行合併時消耗過多的伺服器資源,也可以很好地控制索引中段的數量。
段合併的主要引數如下:
-
mergeFactor:每次合併時參與合併的最少數量,當同一組的段的數量達到此值時開始合併,如果小於此值則不合並,這樣做可以減少段合併的頻率,其預設值為 10。
-
SegmentSize:指段的實際大小,單位為位元組。
-
minMergeSize:小於這個值的段會被分到一組,這樣可以加速小片段的合併。
-
maxMergeSize:若有一段的文字數量大於此值,就不再參與合併,因為大段合併會消耗更多的資源。
段合併相關的動作主要有以下兩個:
-
對索引中的段進行分組,把大小相近的段分到一組,主要由 LogMergePolicy1 類來處理。
-
將屬於同一分組的段合併成一個更大的段。
在段合併前對段的大小進行了標準化處理,通過 logMergeFactorSegmentSize 計算得出。
其中 MergeFactor 表示一次合併的段的數量,Lucene 預設該數量為 10;SegmentSize 表示段的實際大小。通過上面的公式計算後,段的大小更加緊湊,對後續的分組更加友好。
段分組的步驟如下:
①根據段生成的時間對段進行排序,然後根據上述標準化公式計算每個段的大小並且存放到段資訊中,後面用到的描述段大小的值都是標準化後的值,如圖 4 所示:
圖 4:Lucene 段排序
②在陣列中找到最大的段,然後生成一個由最大段的標準化值作為上限,減去 LEVEL_LOG_SPAN(預設值為 0.75)後的值作為下限的區間,小於等於上限並且大於下限的段,都被認為是屬於同一組的段,可以合併。
③在確定一個分組的上下限值後,就需要查詢屬於這個分組的段了,具體過程是:建立兩個指標(在這裡使用指標的概念是為了更好地理解)start 和 end。
start 指向陣列的第 1 個段,end 指向第 start+MergeFactor 個段,然後從 end 逐個向前查詢落在區間的段。
當找到第 1 個滿足條件的段時,則停止,並把當前段到 start 之間的段統一分到一個組,無論段的大小是否滿足當前分組的條件。
如圖 5 所示,第 2 個段明顯小於該分組的下限,但還是被分到了這一組。
這樣做的好處如下:
-
增加段合併的概率,避免由於段的大小參差不齊導致段難以合併。
-
簡化了查詢的邏輯,使程式碼的執行效率更高。
④在分組找到後,需要排除不參加合併的“超大”段,然後判斷剩餘的段是否滿足合併的條件。
如圖 5 所示,mergeFactor=5,而找到的滿足合併條件的段的個數為 4,所以不滿足合併的條件,暫時不進行合併,繼續找尋下一個組的上下限。
⑤由於在第 4 步並沒有找到滿足段合併的段的數量,所以這一分組的段不滿足合併的條件,繼續進行下一分組段的查詢。
具體過程是:將 start 指向 end,在剩下的段(從 end 指向的元素開始到陣列的最後一個元素)中尋找最大的段,在找到最大的值後再減去 LEVEL_LOG_SPAN 的值,再生成一下分組的區間值。
然後把 end 指向陣列的第 start+MergeFactor 個段,逐個向前查詢第 1 個滿足條件的段:重複第 3 步和第 4 步。
⑥如果一直沒有找到滿足合併條件的段,則一直重複第 5 步,直到遍歷完整個陣列,如圖所示:
⑦在找到滿足條件的 mergeFactor 個段時,就需要開始合併了。但是在滿足合併條件的段大於 mergeFactor 時,就需要進行多次合併。
也就是說每次依然選擇 mergeFactor 個段進行合併,直到該分組的所有段合併完成,再進行下一分組的查詢合併操作。
⑧通過上述幾步,如果找到了滿足合併要求的段,則將會進行段的合併操作。
因為索引裡面包含了正向資訊和反向資訊,所以段合併的操作分為兩部分:
-
一個是正向資訊合併,例如儲存域、詞向量、標準化因子等。
-
一個是反向資訊的合併,例如詞典、倒排表等。
在段合併時,除了需要對索引資料進行合併,還需要移除段中已經刪除的資料。
Lucene 相似度打分
我們在前面瞭解到,Lucene 的查詢過程是:首先在詞典中查詢每個 Term,根據 Term 獲得每個 Term 所在的文件連結串列;然後根據查詢條件對連結串列做交、並、差等操作,連結串列合併後的結果集就是我們要查詢的資料。
這樣做可以完全避免對關係型資料庫進行全表掃描,可以大大提升查詢效率。
但是,當我們一次查詢出很多資料時,這些資料和我們的查詢條件又有多大關係呢?其文字相似度是多少?
本節會回答這個問題,並介紹 Lucene 最經典的兩個文字相似度演算法:基於向量空間模型的演算法和基於概率的演算法(BM25)。
如果對此演算法不太感興趣,那麼只需瞭解對文字相似度有影響的因子有哪些,哪些是正向的,哪些是逆向的即可,不需要理解每個演算法的推理過程。但是這兩個文字相似度演算法有很好的借鑑意義。
本作品採用《CC 協議》,轉載必須註明作者和本文連結