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

四、具體格式

4.2. 反向信息

反向信息是索引文件的核心,也即反向索引。

反向索引包括兩部分,左面是詞典(Term Dictionary),右面是倒排表(Posting List)。

在Lucene中,這兩部分是分文件存儲的,詞典是存儲在tii,tis中的,倒排表又包括兩部分,一部分是文檔號及詞頻,保存在frq中,一部分是詞的位置信息,保存在prx中。

  • Term Dictionary (tii, tis)
    • –> Frequencies (.frq)
    • –> Positions (.prx)

4.2.1. 詞典(tis)及詞典索引(tii)信息

[圖]詞典及詞典索引信息


在詞典中,所有的詞是按照字典順序排序的。

  • 詞典文件(tis)
    • TermCount:詞典中包含的總的詞數
    • IndexInterval:爲了加快對詞的查找速度,也應用類似跳躍表的結構,假設IndexInterval爲4,則在詞典索引(tii)文件中保存第4個,第8個,第12個詞,這樣可以加快在詞典文件中查找詞的速度。
    • SkipInterval:倒排表無論是文檔號及詞頻,還是位置信息,都是以跳躍表的結構存在的,SkipInterval是跳躍的步數。
    • MaxSkipLevels:跳躍表是多層的,這個值指的是跳躍表的最大層數。
    • TermCount個項的數組,每一項代表一個詞,對於每一個詞,以前綴後綴規則存放詞的文本信息(PrefixLength + Suffix),詞屬於的域的域號(FieldNum),有多少篇文檔包含此詞(DocFreq),此詞的倒排表在frq,prx中的偏移量(FreqDelta, ProxDelta),此詞的倒排表的跳躍表在frq中的偏移量(SkipDelta),這裏之所以用Delta,是應用差值規則。
  • 詞典索引文件(tii)
    • 詞典索引文件是爲了加快對詞典文件中詞的查找速度,保存每隔IndexInterval個詞。
    • 詞典索引文件是會被全部加載到內存中去的。
    • IndexTermCount = TermCount / IndexInterval:詞典索引文件中包含的詞數。
    • IndexInterval同詞典文件中的IndexInterval。
    • SkipInterval同詞典文件中的SkipInterval。
    • MaxSkipLevels同詞典文件中的MaxSkipLevels。
    • IndexTermCount個項的數組,每一項代表一個詞,每一項包括兩部分,第一部分是詞本身(TermInfo),第二部分是在詞典文件中的偏移量(IndexDelta)。假設IndexInterval爲4,此數組中保存第4個,第8個,第12個詞。。。
  • 讀取詞典及詞典索引文件的代碼如下:

origEnum = new SegmentTermEnum(directory.openInput(segment + "." + IndexFileNames.TERMS_EXTENSION,readBufferSize), fieldInfos, false);//用於讀取tis文件

  • int firstInt = input.readInt();
  • size = input.readLong();
  • indexInterval = input.readInt();
  • skipInterval = input.readInt();
  • maxSkipLevels = input.readInt();

SegmentTermEnum indexEnum = new SegmentTermEnum(directory.openInput(segment + "." + IndexFileNames.TERMS_INDEX_EXTENSION, readBufferSize), fieldInfos, true);//用於讀取tii文件

  • indexTerms = new Term[indexSize];
  • indexInfos = new TermInfo[indexSize];
  • indexPointers = new long[indexSize];
  • for (int i = 0; indexEnum.next(); i++)
    • indexTerms[i] = indexEnum.term();
    • indexInfos[i] = indexEnum.termInfo();
    • indexPointers[i] = indexEnum.indexPointer;

4.2.2. 文檔號及詞頻(frq)信息

[圖]文檔號及詞頻信息


文檔號及詞頻文件裏面保存的是倒排表,是以跳躍表形式存在的。

  • 此文件包含TermCount個項,每一個詞都有一項,因爲每一個詞都有自己的倒排表。
  • 對於每一個詞的倒排表都包括兩部分,一部分是倒排表本身,也即一個數組的文檔號及詞頻,另一部分是跳躍表,爲了更快的訪問和定位倒排表中文檔號及詞頻的位置。
  • 對於文檔號和詞頻的存儲應用的是差值規則和或然跟隨規則,Lucene的文檔本身有以下幾句話,比較難以理解,在此解釋一下:

For example, the TermFreqs for a term which occurs once in document seven and three times in document eleven, with omitTf false, would be the following sequence of VInts:

15, 8, 3

If omitTf were true it would be this sequence of VInts instead:

7,4

首先我們看omitTf=false的情況,也即我們在索引中會存儲一個文檔中term出現的次數。

例子中說了,表示在文檔7中出現1次,並且又在文檔11中出現3次的文檔用以下序列表示:15,8,3.

那這三個數字是怎麼計算出來的呢?

首先,根據定義TermFreq --> DocDelta[, Freq?],一個TermFreq結構是由一個DocDelta後面或許跟着Freq組成,也即上面我們說的A+B?結構。

DocDelta自然是想存儲包含此Term的文檔的ID號了,Freq是在此文檔中出現的次數。

所以根據例子,應該存儲的完整信息爲[DocID = 7, Freq = 1] [DocID = 11,  Freq = 3](見全文檢索的基本原理章節)。

然而爲了節省空間,Lucene對編號此類的數據都是用差值來表示的,也即上面說的規則2,Delta規則,於是文檔ID就不能按完整信息存了,就應該存放如下:

[DocIDDelta = 7, Freq = 1][DocIDDelta = 4 (11-7), Freq = 3]

然而Lucene對於A+B?這種或然跟隨的結果,有其特殊的存儲方式,見規則3,即A+B?規則,如果DocDelta後面跟隨的Freq爲1,則用DocDelta最後一位置1表示。

如果DocDelta後面跟隨的Freq大於1,則DocDelta得最後一位置0,然後後面跟隨真正的值,從而對於第一個Term,由於Freq爲1,於是放在DocDelta的最後一位表示,DocIDDelta = 7的二進制是000 0111,必須要左移一位,且最後一位置一,000 1111 = 15,對於第二個Term,由於Freq大於一,於是放在DocDelta的最後一位置零,DocIDDelta = 4的二進制是0000 0100,必須要左移一位,且最後一位置零,0000 1000 = 8,然後後面跟隨真正的Freq = 3。

於是得到序列:[DocDleta = 15][DocDelta = 8, Freq = 3],也即序列,15,8,3。

如果omitTf=true,也即我們不在索引中存儲一個文檔中Term出現的次數,則只存DocID就可以了,因而不存在A+B?規則的應用。

[DocID = 7][DocID = 11],然後應用規則2,Delta規則,於是得到序列[DocDelta = 7][DocDelta = 4 (11 - 7)],也即序列,7,4.

  • 對於跳躍表的存儲有以下幾點需要解釋一下:
    • 跳躍表可根據倒排表本身的長度(DocFreq)和跳躍的幅度(SkipInterval)而分不同的層次,層次數爲NumSkipLevels = Min(MaxSkipLevels, floor(log(DocFreq/log(SkipInterval)))).
    • 第Level層的節點數爲DocFreq/(SkipInterval^(Level + 1)),level從零計數。
    • 除了最高層之外,其他層都有SkipLevelLength來表示此層的二進制長度(而非節點的個數),方便讀取某一層的跳躍表到緩存裏面。
    • 低層在前,高層在後,當讀完所有的低層後,剩下的就是最後一層,因而最後一層不需要SkipLevelLength。這也是爲什麼Lucene文檔中的格式描述爲 NumSkipLevels-1 , SkipLevel,也即低NumSKipLevels-1層有SkipLevelLength,最後一層只有SkipLevel,沒有SkipLevelLength。
    • 除最低層以外,其他層都有SkipChildLevelPointer來指向下一層相應的節點。
    • 每一個跳躍節點包含以下信息:文檔號,payload的長度,文檔號對應的倒排表中的節點在frq中的偏移量,文檔號對應的倒排表中的節點在prx中的偏移量。
    • 雖然Lucene的文檔中有以下的描述,然而實驗的結果卻不是完全準確的:

Example: SkipInterval = 4, MaxSkipLevels = 2, DocFreq = 35. Then skip level 0 has 8 SkipData entries, containing the 3rd , 7th , 11th , 15th , 19th , 23rd , 27th , and 31st document numbers in TermFreqs. Skip level 1 has 2 SkipData entries, containing the 15th and 31st document numbers in TermFreqs.

按照描述,當SkipInterval爲4,且有35篇文檔的時候,Skip level = 0應該包括第3,第7,第11,第15,第19,第23,第27,第31篇文檔,Skip level = 1應該包括第15,第31篇文檔。

然而真正的實現中,跳躍表節點的時候,卻向前偏移了,偏移的原因在於下面的代碼:

  • FormatPostingsDocsWriter.addDoc(int docID, int termDocFreq)
    • final int delta = docID - lastDocID;
    • if ((++df % skipInterval) == 0)
      • skipListWriter.setSkipData(lastDocID, storePayloads, posWriter.lastPayloadLength);
      • skipListWriter.bufferSkip(df);

從代碼中,我們可以看出,當SkipInterval爲4的時候,當docID = 0時,++df爲1,1%4不爲0,不是跳躍節點,當docID = 3時,++df=4,4%4爲0,爲跳躍節點,然而skipData裏面保存的卻是lastDocID爲2。

所以真正的倒排表和跳躍表中保存一下的信息:

[圖]倒排表的跳躍表

4.2.3. 詞位置(prx)信息

[圖]詞位置信息


詞位置信息也是倒排表,也是以跳躍表形式存在的。

  • 此文件包含TermCount個項,每一個詞都有一項,因爲每一個詞都有自己的詞位置倒排表。
  • 對於每一個詞的都有一個DocFreq大小的數組,每項代表一篇文檔,記錄此文檔中此詞出現的位置。這個文檔數組也是和frq文件中的跳躍表有關係的,從上面我們知道,在frq的跳躍表節點中有ProxSkip,當SkipInterval爲3的時候,frq的跳躍表節點指向prx文件中的此數組中的第1,第4,第7,第10,第13,第16篇文檔。
  • 對於每一篇文檔,可能包含一個詞多次,因而有一個Freq大小的數組,每一項代表此詞在此文檔中出現一次,則有一個位置信息。
  • 每一個位置信息包含:PositionDelta(採用差值規則),還可以保存payload,應用或然跟隨規則。

4.3. 其他信息

4.3.1. 標準化因子文件(nrm)

爲什麼會有標準化因子呢?從第一章中的描述,我們知道,在搜索過程中,搜索出的文檔要按與查詢語句的相關性排序,相關性大的打分(score)高,從而排在前面。相關性打分(score)使用向量空間模型(Vector Space Model),在計算相關性之前,要計算Term Weight,也即某Term相對於某Document的重要性。在計算Term Weight時,主要有兩個影響因素,一個是此Term在此文檔中出現的次數,一個是此Term的普通程度。顯然此Term在此文檔中出現的次數越多,此Term在此文檔中越重要。

這種Term Weight的計算方法是最普通的,然而存在以下幾個問題:

  • 不同的文檔重要性不同。有的文檔重要些,有的文檔相對不重要,比如對於做軟件的,在索引書籍的時候,我想讓計算機方面的書更容易搜到,而文學方面的書籍搜索時排名靠後。
  • 不同的域重要性不同。有的域重要一些,如關鍵字,如標題,有的域不重要一些,如附件等。同樣一個詞(Term),出現在關鍵字中應該比出現在附件中打分要高。
  • 根據詞(Term)在文檔中出現的絕對次數來決定此詞對文檔的重要性,有不合理的地方。比如長的文檔詞在文檔中出現的次數相對較多,這樣短的文檔比較吃虧。比如一個詞在一本磚頭書中出現了10次,在另外一篇不足100字的文章中出現了9次,就說明磚頭書應該排在前面碼?不應該,顯然此詞在不足100字的文章中能出現9次,可見其對此文章的重要性。

由於以上原因,Lucene在計算Term Weight時,都會乘上一個標準化因子(Normalization Factor),來減少上面三個問題的影響。

標準化因子(Normalization Factor)是會影響隨後打分(score)的計算的,Lucene的打分計算一部分發生在索引過程中,一般是與查詢語句無關的參數如標準化因子,大部分發生在搜索過程中,會在搜索過程的代碼分析中詳述。

標準化因子(Normalization Factor)在索引過程總的計算如下:

[圖]標準化因子公式


它包括三個參數:

  • Document boost:此值越大,說明此文檔越重要。
  • Field boost:此域越大,說明此域越重要。
  • lengthNorm(field) = (1.0 / Math.sqrt(numTerms)):一個域中包含的Term總數越多,也即文檔越長,此值越小,文檔越短,此值越大。

從上面的公式,我們知道,一個詞(Term)出現在不同的文檔或不同的域中,標準化因子不同。比如有兩個文檔,每個文檔有兩個域,如果不考慮文檔長度,就有四種排列組合,在重要文檔的重要域中,在重要文檔的非重要域中,在非重要文檔的重要域中,在非重要文檔的非重要域中,四種組合,每種有不同的標準化因子。

於是在Lucene中,標準化因子共保存了(文檔數目乘以域數目)個,格式如下:

[圖]標準化因子文件


  • 標準化因子文件(Normalization Factor File: nrm):
    • NormsHeader:字符串“NRM”外加Version,依Lucene的版本的不同而不同。
    • 接着是一個數組,大小爲NumFields,每個Field一項,每一項爲一個Norms。
    • Norms也是一個數組,大小爲SegSize,即此段中文檔的數量,每一項爲一個Byte,表示一個浮點數,其中0~2爲尾數,3~8爲指數。

4.3.2. 刪除文檔文件(del)

[圖]刪除文檔文件


  • 被刪除文檔文件(Deleted Document File: .del)
    • Format:在此文件中,Bits和DGaps只能保存其中之一,-1表示保存DGaps,非負值表示保存Bits。
    • ByteCount:此段中有多少文檔,就有多少個bit被保存,但是以byte形式計數,也即Bits的大小應該是byte的倍數。
    • BitCount:Bits中有多少位被至1,表示此文檔已經被刪除。
    • Bits:一個數組的byte,大小爲ByteCount,應用時被認爲是byte*8個bit。
    • DGaps:如果刪除的文檔數量很小,則Bits大部分位爲0,很浪費空間。DGaps採用以下的方式來保存稀疏數組:比如第十,十二,三十二個文檔被刪除,於是第十,十二,三十二位設爲1,DGaps也是以byte爲單位的,僅保存不爲0的byte,如第1個byte,第4個byte,第1個byte十進制爲20,第4個byte十進制爲1。於是保存成DGaps,第1個byte,位置1用不定長正整數保存,值爲20用二進制保存,第2個byte,位置4用不定長正整數保存,用差值爲3,值爲1用二進制保存,二進制數據不用差值表示。

五、總體結構

[圖]總體結構

  • 圖示爲Lucene索引文件的整體結構:
    • 屬於整個索引(Index)的segment.gen,segment_N,其保存的是段(segment)的元數據信息,然後分多個segment保存數據信息,同一個segment有相同的前綴文件名。
    • 對於每一個段,包含域信息,詞信息,以及其他信息(標準化因子,刪除文檔)
    • 域信息也包括域的元數據信息,在fnm中,域的數據信息,在fdx,fdt中。
    • 詞信息是反向信息,包括詞典(tis, tii),文檔號及詞頻倒排表(frq),詞位置倒排表(prx)。

大家可以通過看源代碼,相應的Reader和Writer來了解文件結構,將更爲透徹。

發佈了33 篇原創文章 · 獲贊 1 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章