IndexTank全文檢索引擎設計分析

  1. 簡介 IndexTank是一個託管的搜索基礎服務。他主要有如下幾個特色(從官網介紹翻譯過來的):java

    索引更新實時生效 地理位置搜索 支持多種客戶端語言 Ruby, Rails, Python, Java, PHP, .NET & more! 支持靈活的排序與評分控制 支持自動完成 支持面搜索(facet search) 支持匹配高亮 支持海量數據擴展(Scalable from a personal blog to hundreds of millions of documents! ) 支持動態數據數據庫

IndexTank在2011年10月被Linkedin收購,其原有提供的搜索託管服務將在2012年4月終止。目前Linkedin已經把IndexTank的索引引擎代碼以及部分上層服務的代碼開源。 2. 設計分析 因爲IndexTank是一個開源不久的項目,官方公佈的設計相關的資料以及網上對其分析測試能夠說是幾乎沒有,對其內部實現分析都是經過分析代碼得出,因爲目前只是大體看了一下代碼,部分細節分析可能有誤。 2.1. 索引數據結構 IndexTank在底層索引倒排表是基於lucene實現的,可是其索引數據的總體設計與lucene有較大的區別。其基本思想爲把索引數據分紅三大類:倒排表數據、動態數據、原始文檔數據。 2.1.1. 倒排表數據 這 部分數據主要經過lucene封裝實現,底層文件數據結構與lucene徹底相同,可是與lucene對外暴露docId不一樣,在倒排表獲取的docId 會被轉換爲全局惟一不變的id,這點相似於ZOIE,可是實現上有所區別,具體看後文實現分析。倒排表數據爲了支持實時搜索分紅文件倒排和內存倒排兩部分 數據 2.1.2. 動態數據 這部分數據是保存文檔中會常常動態修改的數據,主要包括用於計算評分的數據、facet search字段等。該部分數據經過全局惟一的主鍵進行訪問,數據徹底加載在內存中,定時dump到文件系統。 2.1.3. 原始文檔數據 這部分數據是保存文檔中不會動態修改的原始數據,主要用於在檢索時獲取原始文檔內容生成摘要等。該部分數據經過全局惟一的主鍵進行訪問。目前開源部分的 IndexTank-engine代碼只定義了一個DocumentStorage的接口,並無提供實現,用戶能夠本身實現所需的存儲控制器(如基於數 據庫等)。 2.2. 代碼分析 索引引擎的代碼結構以下圖: IndexTank全文檢索引擎設計分析 - 網易杭研後臺技術中心 - 網易杭研後臺技術中心的博客緩存

上圖爲IndexTank中索引引擎IndexEngine中的主要類結構,各主要類的設計以下:安全

LargeScaleIndex

大 規模數據倒排索引封裝類,用於存儲持久化到文件系統中的索引數據,底層經過lucene實現。該類對外提供建索引文檔的插入、刪除以及查找制定查詢對象匹 配的記錄的接口,這些接口都與lucene無關,是IndexTank本身封裝的接口,主要能夠抽象爲如下幾個接口(其中findMatches爲該類的 內部類中提供,爲解析方便抽象出來一塊兒介紹):數據結構

void add(String docId, Document document);併發

void del(String docId);分佈式

// 注意下面的TopMatches與Query並不是lucene中的,而是IndexTank自行封裝的函數

TopMatches findMatches(Query query, int limit, int scoringFunctionIndex);測試

能夠看出,LargeScaleIndex與lucene的IndexWriter與IndexReader不一樣,提供的接口都是以全局惟一且不變的docId爲依據進行操做的,而不是使用lucene內部生成且會變化的docId。編碼

在 建索引的時候會多創建一個特殊的字段,字段名和值都固定,並在payload中保存該文檔對應的全局惟一不變的docId,從而造成一個按lucene內 部docId連續排序的倒排表,其中每項payload中保存對應的全局惟一不變docId,經過這種方式保存lucene docId與全局docId的映射關係,實現方式與zoie相同。另外,全部字段在經過lucene建索引時參數都是分詞+不保存原值+不保存 TermVector+文本類型的字段。

在 檢索的時候,首先經過lucene IndexReader的底層接口獲取TermPositions封裝生成匹配結果的迭代器,用於獲取全部匹配Query對象的docId、位置偏移等信 息。而後遍歷該迭代器,並在迭代過程當中經過payload字段獲取匹配記錄對應的全局docId,而後經過 DynamicDataFacetingManager統計分類信息,經過BoostsScorer計算最終文檔評分,最終轉換生成TopMatches 返回。

RealTimeIndex

實 時索引數據管理類,用於保存最近加入的文檔的索引,數據保存在內存中,用於在數據刷寫到文件系統前經過內存索引提供實時檢索支持。內部保存兩份倒排表數 據,分別爲markedIndex和index,全部新數據進入index,而後在LargeScaleIndex開始刷寫新數據的時候對 RealTimeIndex進行標記(mark),此時index數據放到markedIndex中,index新開一份空間用於保存新數據,在刷寫完畢 的時候丟棄markedIndex,刷寫過程當中檢索數據同時讀取兩份倒排數據進行合併。每份倒排數據相似於經過一個Map>的結構保存全部倒排數據,以及DocId[]保存原始id與全局DocId的映射關係。該類提供了跟 LargeScaleIndex同樣的接口。檢索時重用了LargeScaleIndex的大部分代碼。

DynamicDataManager

用 於保存文檔動態數據的管理類,保存如推薦數等容易發生變化的數據,另外facet search中使用的分類信息(可選值數量比較小的字符串)也經過該類保存,在訪問該類時,經過全局惟一不變docId定位文檔。相關數據所有保存在內存 中並定時dump到文件系統,在保存分類信息的時候,會對分類字符串值進行相似哈弗曼編碼以數值進行保存,避免重複保存相同的字符串佔用內存。

DynamicDataFacetingManager

處理面搜索相關邏輯的管理器,在檢索過程當中被Blender調用統計匹配結果中不一樣類別的記錄數,底層經過DynamicDataManager獲取記錄的分類信息。

BoostsScorer

記錄評分計算器,內部保存各類用戶評分計算接口實例,在計算記錄評分的時候,經過計算接口對象從DynamicDataManager獲取動態數據計算最終評分。

UserFunctionsManager

評 分接口管理類,IndexTank定義了一套評分函數,UserFunctionsManager負責對用戶設置的評分函數字符串進行解析,並動態生成繼 承評分計算接口(ScoreFunction)java.class對象進行加載並保存到BoostsScorer中。

IndexEngineParser

保存分詞器並用於解析用戶查詢條件字符串。

Dealer

IndexEngine 中用於處理建索引請求的對象,在建索引的時候,會把同一個請求經過LargeScaleIndex和RealTimeIndex進行處理。其中 LargeScaleIndex只有在刷寫新加數據到文件系統中才可以被檢索出來,新加記錄經過RealTimeIndex進行檢索。

Blender

IndexEngine 中用於處理檢索請求的對象,在檢索的時候,會經過LargeScaleIndex和RealTimeIndex進行處理,其中 LargeScaleIndex獲取老數據,RealTimeIndex獲取新加記錄,由Blender負責把兩部分進行合併。

DocumentStorage

負責保存文檔不變字段的原始內容,在訪問該類時,經過全局惟一不變docId定位文檔。IndexTank並無開源DocumentStorage的實現,只給出了其接口定義,由使用者自行定義實現。

Suggester

實現自動完成的相關功能,內部在內存經過相似Tri Tree的數據結構保存相關自動數據,並定時dump到文件系統。 2.3. 重要流程分析 2.3.1. 實時索引 IndexTank的實時索引的實現方式與ZOIE和LuceneNRT都不一樣,其原理以下:

首先經過RealTimeIndex創建內存中的反向索引,該部分數據直接經過java的map、list等內存對象結構存儲,沒有經過lucene的RamDirectory實現,沒有數據壓縮。
而後把相同的文檔經過LargeScaleIndex內部的lucene IndexWriter再次建一份反向索引,可是IndexWriter不commit,至關與只是在內存中進行了反向表的相關編碼等,尚未寫到文件系統。
在 調用Dealer對象的dump接口的時候,會經過LargeScaleIndex內IndexWriter的commit方法把內存中的數據刷到文件系 統,並根據合併策略進行merge操做。這個過程有可能會比較長,爲了讓實時索引保持正常,會對RealTimeIndex進行mark操做,原來的內存 Map對象的反向數據會保存到markIndex引用中,並新開一個Map對象保存新記錄的反向索引,而LargeScaleIndex的新記錄會緩存到 一個隊列中等合併完畢在進行處理。這段時間內全部檢索都要同時經過LargeScaleIndex中的老IndexReader和 RealTimeIndex中經過兩個Map保存的方向索引數據進行檢索併合並。等merge操做完成後,會同步reopen LargeScaleIndex中的IndexReader並丟棄RealTimeIndex中的markIndex。設計思想與ZOIE相似。

2.3.2. 創建索引

在創建索引使用者須要首先把索引文檔字段劃分爲三類:須要建反向索引的靜態字段、須要存儲的靜態字段、不須要反向索引的動態字段。
經過IndexEngine獲取DocumentStorage對象,經過其接口保存須要存儲的靜態字段
經過IndexEngine獲取Dealer對象,經過updateBoosts和updateCategories保存動態的評分字段與分類信息字段
經過Dealer對象add接口對給須要建反向索引的靜態字段創建反向索引
定時調用Dealer對象的dump接口把內存中的實時反向索引數據、動態字段數據、自動完成數據持久化到文件系統

2.3.3. 檢索記錄

從IndexEngine中獲取Blender對象,經過search接口傳入query對象進行檢索直接獲取匹配結果的DocId與動態字段數據,具體處理步驟以下:

經過LargeScaleIndex和RealTimeIndex獲取與query對象匹配的文檔記錄的迭代器
在 上一步生成的遍歷迭代器基礎上封裝TopMatches迭代器,該迭代器在迭代的時候,經過DynamicDataManager獲取須要的動態數據,由 DynamicDataFacetManager統計facet search的結果,由BoostsScorer計算文檔評分
遍歷上一步生成的迭代器獲取全部匹配文檔記錄,在遍歷的同時,根據制定獲取的動態字段列表,從DynamicDataManager獲取該文檔的動態字段值。最終返回匹配結果。

從IndexEngine中獲取DocumentStorage對象,根據須要獲取文檔的原始記錄

2.3.4. 數據恢復

IndexTank在創建文檔索引的時候會記錄操做日誌
IndexTank會定時把內存中的數據(包括實時反向索引、自動完成數據、動態字段數據等)dump到文件系統
若是系統崩潰,IndexTank會首先加載最後一次dump到文件系統的數據,而後在根據操做日誌從dump的時間點開始重建索引數據。恢復的原理跟數據庫相似。
  1. IndexTank的優點 IndexTank的索引結構確實是一個挺有意思的設計,經過把整個索引劃分爲三種類型分別處理可以帶來必定的優點。 3.1. 加快Merge 因爲把文檔原始記錄與動態變化數據獨立於倒排表數據,經過全局不變的惟一DocId進行聯繫,因此在每次merge數據的時候,只涉及lucene的倒排表部分,數據量相對較小,能夠加快Merge速度。 3.2. 減小IndexReader的reopen操做 在 ZOIE和LuceneNRT中,對外提供索引訪問都是經過IndexReader實現的,爲了實現實時索引的功能,須要常常調用IndexReader 的reopen操做(或新建IndexReader),由於在lucene的設計思想中IndexReader在構建以後,其能讀取的數據就不會變化。然 而IndexReader的構建與reopen是一個不算太簡單的操做,其內部須要合併計算各個Segment的docId映射爲對外的docId、 delete文檔bitVector緩存等等,會佔用cpu並加大gc負擔。然而,在IndexTank中反向索引結構並不是經過IndexReader對 外提供訪問,只有LargeScaleIndex的內部實現中經過IndexReader實現反向索引讀取,那部分數據只有在dump的時候才須要 reopen IndexReader,頻率至關低,而RealTimeIndex中數據保存在支持同步修改、訪問的Map結構中,可以直接訪問當前最新的索引數據。通 過這種設計,從根本上解決了IndexReader須要頻繁構建與reopen的問題。 3.3. 提升索引的實時性 IndexTank的實時索引本質上是經過一個線程安全的Map來實現的,處理相對比較簡單,不須要像ZOIE或LuceneNRT那樣通過大量的編碼以及零散索引合併還有reopen的過程,實時建索引的效率更加高,缺點是數據沒有通過壓縮,內存佔用更大。 3.4. 解決緩存失效問題 在 Lucene中,在排序、自定義評分的時候,須要經過倒排表緩存全部文檔的相關字段的值,然而這種緩存是跟IndexReader關聯的,由於是經過內部 docId進行訪問,docId會伴隨segment的合併發生變化,所以常常會發生緩存失效須要從新加載的狀況。而IndexTank把全部動態數據單 獨保存,經過全局docId進行關聯,使緩存不會常常失效。 3.5. 減小原始文檔查詢數量 在 處理分佈式檢索的時候咱們的SearchDispatcher是從多個IndexStore中獲取,因爲目前原始文檔記錄是在lucene索引中保存,只 能在發往IndexStore的請求中同時查詢分片的全部匹配記錄的完整文檔信息,然而,在最終結果中,只有其中一部分有效,致使了大量的無用查詢。若是 像IndexTank那樣把原始文檔單獨存儲並經過全局DocId進行關聯,能夠在IndexStore的查詢中只返回匹配相關信息(如全局docId、 匹配評分等),在SearchDispatcher合併抽取出最終返回的記錄集合以後在經過全局DocId查詢原始記錄,最終輸出給用戶。 3.6. 隱藏內部Lucene DocId 在IndexTank的設計中,Lucene內部的DocId只有LargeScaleIndex知道,對外的接口都是經過全局DocId進行關聯,把Lucene內部實現隱藏掉了,設計上更加優雅,也更方便維護擴展其餘功能。
相關文章
相關標籤/搜索