Lucene索引存儲結構

內存管理 與 數據存儲

索引文檔的總體結構

         索引(index):Lucene的索引由許多個文件組成,這些文件放在同一個目錄下

         段(segment):一個Lucene的索引由多個段組成,段與段之間是獨立的。添加新的文檔時可以生成新的段,達到閾值(段的個數,段中包含的文件數等)時,不同的段可以合併。在文件夾下,具有相同前綴的文件屬於同一個段segments.gen 和 segments_N(N表示一個具體數字,eg:segments_5)是段的元數據文件,他們保存了段的屬性信息。

        文檔(document):文檔時建索引的基本單位,一個段中可以包含多篇文檔。新添加的文檔時單獨保存在一個新生成的段中,隨着段的合併,不同的文檔會合併到至相同的段中。

         域(Field):一個文檔有可由多個域(Field)組成,比如一篇新聞,有 標題,作者,正文等多個屬性,這些屬性可以看作是文檔的域。不同的域可以指定不同的索引方式,比如指定不同的分詞方式,是否構建索引,是否存儲等

         詞(Term):詞 是索引的最小單位,是經過詞法分詞和語言處理後的字符串。

Lucene的索引結構中,即保存了正向信息,也保存了反向信息。

正向信息:

按層次保存了從索引,一直到詞的包含關係:索引(Index) –> 段(segment) –> 文檔(Document) –> 域(Field) –> 詞(Term)

也即此索引包含了那些段,每個段包含了那些文檔,每個文檔包含了那些域,每個域包含了那些詞。

既然是層次結構,則每個層次都保存了本層次的信息以及下一層次的元信息,也即屬性信息,比如一本介紹中國地理的書,應該首先介紹中國地理的概況,以及中國包含多少個省,每個省介紹本省的基本概況及包含多少個市,每個市介紹本市的基本概況及包含多少個縣,每個縣具體介紹每個縣的具體情況。

如上圖,包含正向信息的文件有:

          segments_N保存了此索引包含多少個段,每個段包含多少篇文檔。

          XXX.fnm保存了此段包含了多少個域,每個域的名稱及索引方式。

          XXX.fdx,XXX.fdt保存了此段包含的所有文檔,每篇文檔包含了多少域,每個域保存了那些信息。

          XXX.tvx,XXX.tvd,XXX.tvf保存了此段包含多少文檔,每篇文檔包含了多少域,每個域包含了多少詞,每個詞的字符串,位置等信息。

反向信息:

保存了詞典到倒排表的映射:詞(Term) –> 文檔(Document)

如上圖,包含反向信息的文件有:

          XXX.tis,XXX.tii保存了詞典(Term Dictionary),也即此段包含的所有的詞按字典順序的排序。

         XXX.frq保存了倒排表,也即包含每個詞的文檔ID列表。

         XXX.prx保存了倒排表中每個詞在包含此詞的文檔中的位置。

倒排

倒排存儲示例

文章1的所有關鍵詞爲:[tom] [live] [guangzhou] [i] [live] [guangzhou]     文章2的所有關鍵詞爲:[he] [live] [shanghai]

索引結構

通常有兩種位置:

a.字符位置,即記錄該詞是文章中第幾個字符(優點是關鍵詞亮顯時定位快);

b.關鍵詞位置,即記錄該詞是文章中第幾個關鍵詞(優點是節約索引空間、詞組(phase)查詢快),lucene中記錄的就是這種位置。

lucene將上面三列分別作爲詞典文件(Term Dictionary)、頻率文件(frequencies)、位置文件 (positions)保存。其中詞典文件不僅保存有每個關鍵詞,還保留了指向頻率文件和位置文件的指針,通過指針可以找到該關鍵字的頻率信息和位置信息。

倒排信息

參考鏈接:Lucene索引過程中的內存管理與數據存儲

 在Lucene的設計裏,IntBlockPool和ByteBlockPool的作用域是IndexChain,即每個IndexChain都會生成獨立的ByteBlockPool和IntBlockPool ,這樣就不會出現多線程間可變數據共享的問題,這種做法實際上是一種約定方式的線程封閉,即ByteBlockPool本身並不是線程安全的,不像ThreadLocal或者棧封閉。由於每個IndexChain都需要處理多個Field,所以IntBlockPool和ByteBlockPool是Field所共享的。需要注意的是ParallelPostingsArray的作用域是Field,即每個Field都有一個postingsArray

  ParallelPostingsArray的三個成員變量:

   textStarts存儲的是每一個term在ByteBlockPool裏面的起始位置,通過textStarts[termID]可以快速找到termID對應的term 。

   byteStarts存儲的是term在ByteBlockPool的結束位置的下一個位置。

   IntStarts存儲的是term在IntBlockPool的地址信息,而IntBlockPool則存儲着term在ByteBlockPool中的Slice位置信息。

ParallelPostingsArray成員變量關係示例

DOCID;Freq;Positions這三種信息都是隨着Term存儲在ByteBlockPool中,其存儲過程如下:

     第一步:把term.length存儲到ByteBlockPool.buffer中。這會佔用1或者2個byte,由term的大小決定。由於term的最大長度爲32766,所以term.length最多會佔用兩個byte。

    第二步:把term的byte數組形式存儲到ByteBlockPool.buffer中。

    第三步:緊接着term開闢5個byte大小的slice,用來存儲term在每個doc中的freq信息。

    第四步:再開闢一塊Slice用來存儲positions信息。

Lucene存儲在索引中的並非真正的docId,而是docDelta,即兩個docId的差值.這樣存儲能夠起到節約空間的作用。

索引實現

參考鏈接:Lucene底層實現原理,它的索引結構

Lucene3.0之前使用的也是跳躍表結構,後換成了FST,但跳躍表在Lucene其他地方還有應用如倒排表合併和文檔號索引。

FST

理論基礎:《Direct constructionofminimal acyclic subsequential transducers》,通過輸入有序字符串構建最小有向無環圖。

優點:內存佔用率低,壓縮率一般在3倍~20倍之間、模糊查詢支持好、查詢快

缺點:結構複雜、輸入要求有序、更新不易

Lucene裏有個FST的實現,從對外接口上看,它跟Map結構很相似,有查找,有迭代。

與Map的性能對比

倒排索引

索引文件結構

往索引庫裏插入四個單詞abd、abe、acf、acg,看看它的索引文件內容如下:

索引結構圖

構建過程如下:

構建過程

Lucene的FST實現的主要優化策略有:

1. 最小後綴數。Lucene對寫入tip的前綴有個最小後綴數要求,默認25,這時爲了進一步減少內存使用。如果按照25的後綴數,那麼就不存在ab、ac前綴,將只有一個跟節點,abd、abe、acf、acg將都作爲後綴存在tim文件中。我們的10g的一個索引庫,索引內存消耗只佔20M左右。

2.前綴計算基於byte,而不是char,這樣可以減少後綴數,防止後綴數太多,影響性能。如對宇(e9 b8 a2)、守(e9 b8 a3)、安(e9 b8 a4)這三個漢字,FST構建出來,不是隻有根節點,三個漢字爲後綴,而是從unicode碼出發,以e9、b8爲前綴,a2、a3、a4爲後綴。

倒排表的docId壓縮

docId壓縮存儲

跳躍表加速合併,因爲布爾查詢時,and 和or 操作都需要合併倒排表,這時就需要快速定位相同文檔號,所以利用跳躍表來進行相同文檔號查找。

正排:

正向信息在Lucene中只有docId-document的映射,由CompressingStoredFieldsWriter類來完成。

Lucene的正向信息存儲比較簡單,按Field依次把內容寫入到bufferedDocs中,然後把偏移量寫入到endOffsets中就OK了。

當滿足flush條件或者執行了IndexWriter.commit()方法,則會進行一次flush操作,把內存中緩存的document及倒排信息flush到硬盤中。

正向索引

正向索引結構

fnm中爲元信息存放了各列類型、列名、存儲方式等信息。

段的元數據信息

fdt爲文檔值,裏面一個chunk就是一個塊,Lucene索引文檔時,先緩存文檔,緩存大於16KB時,就會把文檔壓縮存儲。一個chunk包含了該chunk起始文檔、多少個文檔、壓縮後的文檔內容。

域數據文件

fdx爲文檔號索引,倒排表存放的時文檔號,通過fdx才能快速定位到文檔位置即chunk位置,它的索引結構比較簡單,就是跳躍表結構,首先它會把1024個chunk歸爲一個block,每個block記載了起始文檔值,block就相當於一級跳錶。

 

域索引文件的作用示意

所以查找文檔,就分爲三步:

    第一步二分查找block,定位屬於哪個block。

    第二步就是根據從block里根據每個chunk的起始文檔號,找到屬於哪個chunk和chunk位置。

   第三步就是去加載fdt的chunk,找到文檔。這裏還有一個細節就是存放chunk起始文檔值和chunk位置不是簡單的數組,而是採用了平均值壓縮法。所以第N個chunk的起始文檔值由DocBase + AvgChunkDocs * n + DocBaseDeltas[n]恢復而來,而第N個chunk再fdt中的位置由StartPointerBase + AvgChunkSize * n + StartPointerDeltas[n]恢復而來。

     從上面分析可以看出,lucene對原始文件的存放是行式存儲,並且爲了提高空間利用率,是多文檔一起壓縮,因此取文檔時需要讀入和解壓額外文檔,因此取文檔過程非常依賴隨機IO,以及lucene雖然提供了取特定列,但從存儲結構可以看出,並不會減少取文檔時間。

列式存儲

詞向量(Term Vector)的數據信息

詞向量 信息是從索引(index)到文檔(document)到域(field)到詞(term)的正向信息,有了詞向量信息,就可以得到一篇文檔包含哪些詞的信息。

Lucene目前有五種類型的DocValues:NUMERIC、BINARY、SORTED、SORTED_SET、SORTED_NUMERIC,針對每種類型Lucene都有特定的壓縮方法。

如對NUMERIC類型即數字類型,數字類型壓縮方法很多,如:增量、表壓縮、最大公約數,根據數據特徵選取不同壓縮方法。

SORTED類型即字符串類型,壓縮方法就是表壓縮:預先對字符串字典排序分配數字ID,存儲時只需存儲字符串映射表,和數字數組即可,而這數字數組又可以採用NUMERIC壓縮方法再壓縮。

列式存儲

對DocValues的應用,ElasticSearch功能實現地更系統、更完整,即ElasticSearch的Aggregations——聚合功能,它的聚合功能分爲三類: Metric -> 統計 、 Bucket ->分組 、 Pipline -> 基於聚合再聚合 。

ElasticSearch基於倒排索引和DocValues實現SQL過程


參考:

Lucene原理與代碼分析完整版

https://www.iteye.com/blog/forfuture1978-691017

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章