Lucene學習總結之三:Lucene的索引文件格式(1)

Lucene的索引裏面存了些什麼,如何存放的,也即Lucene的索引文件格式,是讀懂Lucene源代碼的一把鑰匙。

當我們真正進入到Lucene源代碼之中的時候,我們會發現:

  • Lucene的索引過程,就是按照全文檢索的基本過程,將倒排表寫成此文件格式的過程。
  • Lucene的搜索過程,就是按照此文件格式將索引進去的信息讀出來,然後計算每篇文檔打分(score)的過程。

本文詳細解讀了Apache Lucene - Index File Formats(http://lucene.apache.org/java/2_9_0/fileformats.html) 這篇文章。

 

一、基本概念

下圖就是Lucene生成的索引的一個實例:

image

Lucene的索引結構是有層次結構的,主要分以下幾個層次:

  • 索引(Index):
    • 在Lucene中一個索引是放在一個文件夾中的。
    • 如上圖,同一文件夾中的所有的文件構成一個Lucene索引。
  • 段(Segment):
    • 一個索引可以包含多個段,段與段之間是獨立的,添加新文檔可以生成新的段,不同的段可以合併。
    • 如上圖,具有相同前綴文件的屬同一個段,圖中共兩個段 "_0" 和 "_1"。
    • segments.gen和segments_5是段的元數據文件,也即它們保存了段的屬性信息。
  • 文檔(Document):
    • 文檔是我們建索引的基本單位,不同的文檔是保存在不同的段中的,一個段可以包含多篇文檔。
    • 新添加的文檔是單獨保存在一個新生成的段中,隨着段的合併,不同的文檔合併到同一個段中。
  • 域(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保存了倒排表中每個詞在包含此詞的文檔中的位置。

在瞭解Lucene索引的詳細結構之前,先看看Lucene索引中的基本數據類型。

 

二、基本類型

Lucene索引文件中,用一下基本類型來保存信息:

  • Byte:是最基本的類型,長8位(bit)。
  • UInt32:由4個Byte組成。
  • UInt64:由8個Byte組成。
  • VInt:
    • 變長的整數類型,它可能包含多個Byte,對於每個Byte的8位,其中後7位表示數值,最高1位表示是否還有另一個Byte,0表示沒有,1表示有。
    • 越前面的Byte表示數值的低位,越後面的Byte表示數值的高位。
    • 例如130化爲二進制爲 1000, 0010,總共需要8位,一個Byte表示不了,因而需要兩個Byte來表示,第一個Byte表示後7位,並且在最高位置1來表示後面還有一個Byte,所以爲(1) 0000010,第二個Byte表示第8位,並且最高位置0來表示後面沒有其他的Byte了,所以爲(0) 0000001。

clip_image002[1]

  • Chars:是UTF-8編碼的一系列Byte。
  • String:一個字符串首先是一個VInt來表示此字符串包含的字符的個數,接着便是UTF-8編碼的字符序列Chars。

 

三、基本規則

Lucene爲了使的信息的存儲佔用的空間更小,訪問速度更快,採取了一些特殊的技巧,然而在看Lucene文件格式的時候,這些技巧卻容易使我們感到困惑,所以有必要把這些特殊的技巧規則提取出來介紹一下。

在下不才,胡亂給這些規則起了一些名字,是爲了方便後面應用這些規則的時候能夠簡單,不妥之處請大家諒解。

1. 前綴後綴規則(Prefix+Suffix)

Lucene在反向索引中,要保存詞典(Term Dictionary)的信息,所有的詞(Term)在詞典中是按照字典順序進行排列的,然而詞典中包含了文檔中的幾乎所有的詞,並且有的詞還是非常的長的,這樣索引文件會非常的大,所謂前綴後綴規則,即當某個詞和前一個詞有共同的前綴的時候,後面的詞僅僅保存前綴在詞中的偏移(offset),以及除前綴以外的字符串(稱爲後綴)。

prefixsuffix

比如要存儲如下詞:term,termagancy,termagant,terminal,

如果按照正常方式來存儲,需要的空間如下:

[VInt = 4] [t][e][r][m],[VInt = 10][t][e][r][m][a][g][a][n][c][y],[VInt = 9][t][e][r][m][a][g][a][n][t],[VInt = 8][t][e][r][m][i][n][a][l]

共需要35個Byte.

如果應用前綴後綴規則,需要的空間如下:

[VInt = 4] [t][e][r][m],[VInt = 4 (offset)][VInt = 6][a][g][a][n][c][y],[VInt = 8 (offset)][VInt = 1][t],[VInt = 4(offset)][VInt = 4][i][n][a][l]

共需要22個Byte。

大大縮小了存儲空間,尤其是在按字典順序排序的情況下,前綴的重合率大大提高。

2. 差值規則(Delta)

在Lucene的反向索引中,需要保存很多整型數字的信息,比如文檔ID號,比如詞(Term)在文檔中的位置等等。

由上面介紹,我們知道,整型數字是以VInt的格式存儲的。隨着數值的增大,每個數字佔用的Byte的個數也逐漸的增多。所謂差值規則(Delta)就是先後保存兩個整數的時候,後面的整數僅僅保存和前面整數的差即可。

delta

比如要存儲如下整數:16386,16387,16388,16389

如果按照正常方式來存儲,需要的空間如下:

[(1) 000, 0010][(1) 000, 0000][(0) 000, 0001],[(1) 000, 0011][(1) 000, 0000][(0) 000, 0001],[(1) 000, 0100][(1) 000, 0000][(0) 000, 0001],[(1) 000, 0101][(1) 000, 0000][(0) 000, 0001]

供需12個Byte。

如果應用差值規則來存儲,需要的空間如下:

[(1) 000, 0010][(1) 000, 0000][(0) 000, 0001],[(0) 000, 0001],[(0) 000, 0001],[(0) 000, 0001]

共需6個Byte。

大大縮小了存儲空間,而且無論是文檔ID,還是詞在文檔中的位置,都是按從小到大的順序,逐漸增大的。

3. 或然跟隨規則(A, B?)

Lucene的索引結構中存在這樣的情況,某個值A後面可能存在某個值B,也可能不存在,需要一個標誌來表示後面是否跟隨着B。

一般的情況下,在A後面放置一個Byte,爲0則後面不存在B,爲1則後面存在B,或者0則後面存在B,1則後面不存在B。

但這樣要浪費一個Byte的空間,其實一個Bit就可以了。

在Lucene中,採取以下的方式:A的值左移一位,空出最後一位,作爲標誌位,來表示後面是否跟隨B,所以在這種情況下,A/2是真正的A原來的值。

ab

如果去讀Apache Lucene - Index File Formats這篇文章,會發現很多符合這種規則的:

  • .frq文件中的DocDelta[, Freq?],DocSkip,PayloadLength?
  • .prx文件中的PositionDelta,Payload? (但不完全是,如下表分析)

當然還有一些帶?的但不屬於此規則的:

  • .frq文件中的SkipChildLevelPointer?,是多層跳躍表中,指向下一層表的指針,當然如果是最後一層,此值就不存在,也不需要標誌。
  • .tvf文件中的Positions?, Offsets?。
    • 在此類情況下,帶?的值是否存在,並不取決於前面的值的最後一位。
    • 而是取決於Lucene的某項配置,當然這些配置也是保存在Lucene索引文件中的。
    • 如Position和Offset是否存儲,取決於.fnm文件中對於每個域的配置(TermVector.WITH_POSITIONS和TermVector.WITH_OFFSETS)

爲什麼會存在以上兩種情況,其實是可以理解的:

  • 對於符合或然跟隨規則的,是因爲對於每一個A,B是否存在都不相同,當這種情況大量存在的時候,從一個Byte到一個Bit如此8倍的空間節約還是很值得的。
  • 對於不符合或然跟隨規則的,是因爲某個值的是否存在的配置對於整個域(Field)甚至整個索引都是有效的,而非每次的情況都不相同,因而可以統一存放一個標誌。
文章中對如下格式的描述令人困惑:

Positions --> <PositionDelta,Payload?> Freq

Payload --> <PayloadLength?,PayloadData>

PositionDelta和Payload是否適用或然跟隨規則呢?如何標識PayloadLength是否存在呢?

其實PositionDelta和Payload並不符合或然跟隨規則,Payload是否存在,是由.fnm文件中對於每個域的配置中有關Payload的配置決定的(FieldOption.STORES_PAYLOADS) 。

當Payload不存在時,PayloadDelta本身不遵從或然跟隨原則。

當Payload存在時,格式應該變成如下:Positions --> <PositionDelta,PayloadLength?,PayloadData> Freq

從而PositionDelta和PayloadLength一起適用或然跟隨規則。

 

4. 跳躍表規則(Skip list) 

爲了提高查找的性能,Lucene在很多地方採取的跳躍表的數據結構。

跳躍表(Skip List)是如圖的一種數據結構,有以下幾個基本特徵:

  • 元素是按順序排列的,在Lucene中,或是按字典順序排列,或是按從小到大順序排列。
  • 跳躍是有間隔的(Interval),也即每次跳躍的元素數,間隔是事先配置好的,如圖跳躍表的間隔爲3。
  • 跳躍表是由層次的(level),每一層的每隔指定間隔的元素構成上一層,如圖跳躍表共有2層。

skiplist

需要注意一點的是,在很多數據結構或算法書中都會有跳躍表的描述,原理都是大致相同的,但是定義稍有差別:

  • 對間隔(Interval)的定義: 如圖中,有的認爲間隔爲2,即兩個上層元素之間的元素數,不包括兩個上層元素;有的認爲是3,即兩個上層元素之間的差,包括後面上層元素,不包括前面的上層元素;有的認爲是4,即除兩個上層元素之間的元素外,既包括前面,也包括後面的上層元素。Lucene是採取的第二種定義。
  • 對層次(Level)的定義:如圖中,有的認爲應該包括原鏈表層,並從1開始計數,則總層次爲3,爲1,2,3層;有的認爲應該包括原鏈表層,並從0計數,爲0,1,2層;有的認爲不應該包括原鏈表層,且從1開始計數,則爲1,2層;有的認爲不應該包括鏈表層,且從0開始計數,則爲0,1層。Lucene採取的是最後一種定義。

跳躍表比順序查找,大大提高了查找速度,如查找元素72,原來要訪問2,3,7,12,23,37,39,44,50,72總共10個元素,應用跳躍表後,只要首先訪問第1層的50,發現72大於50,而第1層無下一個節點,然後訪問第2層的94,發現94大於72,然後訪問原鏈表的72,找到元素,共需要訪問3個元素即可。

然而Lucene在具體實現上,與理論又有所不同,在具體的格式中,會詳細說明。

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