四、具體格式
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文件
SegmentTermEnum indexEnum = new SegmentTermEnum(directory.openInput(segment + "." + IndexFileNames.TERMS_INDEX_EXTENSION, readBufferSize), fieldInfos, true);//用於讀取tii文件
|
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篇文檔。 然而真正的實現中,跳躍表節點的時候,卻向前偏移了,偏移的原因在於下面的代碼:
從代碼中,我們可以看出,當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來了解文件結構,將更爲透徹。