Lucene 4.X 倒排索引原理與實現: (3) Term Dictionary和Index文件 (FST詳細解析)


我們來看最復雜的部分,就是Term Dictionary和Term Index文件,Term Dictionary文件的后綴名為tim,Term Index文件的后綴名是tip,格式如圖所示。

image

 

Term Dictionary文件首先是一個Header,接下來是PostingsHeader,這兩個的格式一致,但是保存的是不同的信息。SkipInterval是跳躍表的跳的幅度,MaxSkipLevels是跳躍表的層數,SkipMinimun是應用跳躍表的最小倒排表長度,接下來就是Term的部分了。

在tim文件中,Term是分成Block進行保存的,如何將Term進行分塊,則需要和tip文件配合。Term Index文件對於每一個Field都保存一個FSTIndex來幫助快速定位tim文件中屬於這個Field的Term的位置,由於FSTIndex的長度不同,為了快速定位某個Field的位置,則應用指針列表規則,為每一個Field保存了指向這個Field的FSTIndex的指針。

這里比較令人困惑的一點就是,FST是什么,如何利用他來分塊呢?

FST全程是Finite State Transducers,是一個帶輸出的有限狀態機,看過前面有限狀態機規則的可以知道,有限狀態機邏輯上來講就是一顆樹,就像圖3-71中的那棵樹,從初始狀態輸入字符a到達狀態a,輸入字符b到達狀態b,輸入字符d到達狀態d,不同的是狀態d有輸出,所謂的輸出就是一個指針,指向tim文件中的位置。

Tim文件中Term的分塊就是按照FST來的,圖3-71中,Block 0中的所有的Term都是以abd為前綴的,Block 1中所有的Term都是以abe為前綴的。每一個Block都有一個Block Header,里面指明這個Block包含幾個Term,假設個數為N,Suffix里面包含了N個后綴,比如Block 0中包含Term “abdi”和”abdj”,則這里面保存”i”和”j”。Stats里面包含了N個統計信息,每個統計信息包含docFreq和totalTermFreq。Metadata里面包含了指向倒排表文件frq和prx文件的指針。

Tim和tip文件的寫入是由org.apache.lucene.codecs.BlockTreeTermsWriter來負責的,在它的構造函數中,生成了兩個OutputStream,並且寫入除了Block和FSTIndex之外的所有信息。

image

Lucene40PostingsWriter的start函數如下:

image

image

下面咱們具體討論,Term如何分塊,Block如何寫入,FSTIndex如何構造。

我們首先通過一個簡單的例子,來看一下一個普通的FST是如何構造的,Lucene的文檔里面給了類似下面這樣一個例子。

image

這里InputValues是構造FST的輸入,是根據這些字符串,構造出圖3-71中的那棵樹。

OutputValue是有限狀態機的輸出,由於在實際應用中,輸出是一個指向tim文件的一個指針,一般是byte[]類型,所以我們也在這里弄了三個byte[]作為輸出。

Builder就是有限狀態機的構造器,它支持多種輸出類型,我們這里用byte[]作為輸出,所以輸出類型我們選擇BytesRef,這是對byte[]的一個封裝。

下一步就是用Builder的add函數將輸入和輸出關聯起來,由於builder的輸入必須是IntsRef類型,所以需要從字符串轉換成為IntsRef類型,輸出也要將byte[]封裝為BytesRef。

Builder的finish函數真正構造一個FST,在內存中形成一個二進制結構,通過它可以通過輸入,快速查詢輸出,例如程序中的給出輸入”acf”就能得到輸出[5 6]。

從表面現象來看,我們甚至可以決定FST就是一個hash map,給出輸入,得到輸出。這就滿足了作為Term Dictionary的要求,給出一個字符串,我馬上能找到倒排表的位置。

Builder里面一個很重要的成員變量UnCompiledNode<T>[] frontier,在FST的構造過程中,它維護整棵FST樹,其中里面直接保存的是UnCompiledNode,是當前添加的字符串所形成的狀態節點,而前面添加的字符串形成的狀態節點通過指針相互引用。

Builder.add函數主要包括四個部分:

image

當第一個字符串abd加入之后,frontier的結構如圖3-72所示,圖中藍色的節點都是。

image

 

當新的字符串abe之后,首先(1)找出公共前綴ab,則prefixLenPlus1=3。然后調(2)用freezeTail將尾節點Sd進行冰封。為什么要進行冰封(一個形象的說法)呢?因為Sd節點不會再改變了。在實際應用中,字符串都是按照字母順序依次處理的,上一次的字符串是abd,下一個字符串可能是abdm,再下一個字符串可能是abdn,這都會導致Sd這個節點的變化。然而當abe出現后,說明abd*都不可能出現了,狀態Sd也不可能再有新的子節點了,所以Sd也就確定下來了,需要冰封。那么Sb節點要不要冰封呢?當然不行了,因為這次來了abe,下次還可能有abf, abg等等新的Sb的子節點出現,這就是為什么要計算公共前綴了,公共前綴之后的狀態節點都是可以冰封的了,而這些冰封的節點都從尾部開始,所以這一步的函數叫freezeTail。

freezeTail的實現如下:

image

freezeTail主要有兩個分支,在Builder構造的時候,用戶可以傳進自己的freezeTail,如果用戶指定了,則調用它的freeze函數,如果沒有指定,則執行else部分默認的行為。在這里,我們使用默認行為,在后面的代碼分析中,我們還能看到使用自己的freezeTail的情況。

默認行為中,從尾部到公共前綴節點,對於每個狀態節點,調用compileNode函數。在這之前,frontier里面保存的都是UnCompiledNode,經過compileNode函數后,就變成了CompiledNode,並從frontier摘下來,parent.replaceLast函數將父節點的指針指向新的CompiledNode。所謂compile過程,就是將內存中的數據結構變成二進制。

compileNode最終調用org.apache.lucene.util.fst.FST.addNode(UnCompiledNode<T>),代碼如下:

image

image

然后(3)將新的input添加到frontier之后,變成如圖3-73的數據結構。

image

 

依次類推,當添加acf之后,frontier變成如下的數據結構。

image

 

最后調用Builder的finish函數生成FST,代碼如下:

image

image

形成的二進制數組如圖3-75所示,由於有內容翻轉,所以解析的時候需要從右向左解析。

image

 

了解了最基本的FST的原理之后,讓我們來一步一步通過代碼,了解tim和tip文件的block和FSTIndex是如何生成的。

我們以下圖3-76為例子。默認情況下,BlockTreeTermsWriter有兩個靜態變量,DEFAULT_MIN_BLOCK_SIZE=25,DEFAULT_MAX_BLOCK_SIZE=48,MIN的意思是當某個狀態節點的子節點個數超過25個的時候,可以寫成一個Block,MAX的意思是當個數超過48的時候,則寫成多個Block,多個Block構成一個層級Block。為了能夠清晰的解析代碼,我們設DEFAULT_MIN_BLOCK_SIZE=2,DEFAULT_MAX_BLOCK_SIZE=4。我們僅僅添加一篇文檔,里面的Term依次為 abc abdf abdg abdh abei abej abek abel abem aben。所形成的狀態樹如圖所示,根據MIN和MAX的設置,f, g, h會寫成一個Block,i, j, k, l, m, n寫成一個層級Block,c, d, e寫成一個Block。我們之所以把從a到n的十進制和十六進制列在這里,是因為在eclipse中,有時候字符顯示的是十進制,有時候是十六進制,當看到這些數值的時候,知道是這些字符即可。

image

 

寫tim和tip文件的過程紛繁復雜,下面的流程圖3-77作為一個線索

image

 

每來一個新的Term,都調用finishTerm。

image

image

finishTerm的blockBuilder是沒有output的,這個blockBuilder是用來進行Term分塊的,而不是用來生成FSTIndex的。blockBuilder.add函數的流程和上面的敘述過的FST基本原理中的過程基本一致,不同的是blockBuilder是被用戶指定了freezeTail的,為org.apache.lucene.codecs.BlockTreeTermsWriter.TermsWriter.FindBlocks,所以freezeTail調用的是FindBlocks.freeze函數。這個freeze函數僅僅處理子節點的個數大於min的節點,調用writeBlocks函數將子節點寫成block,對於不滿足這個條件的節點,僅僅從frontier上摘下來,不做其他操作。

在整個過程中,維護兩個成員變量,一個是List<PendingEntry> pending保存尚未處理的Term或者block,對於Term,里面保存這個Term的text,docFreq,totalTermFreq信息。另一個是pendingTerms,保存尚未處理的Term的freqStart和proxStart信息。

當加入abc,abdf,abdg,abdh之后,frontier成為如下的結構,在這個過程FindBlock.freeze什么都不做。這個時候的pending和pendingTerms也如圖所示。

image

 

加入abei的時候,對Sd進行freeze的時候,發現Sd的出度為3,大於min,則開始調用BlockTreeTermsWriter.TermsWriter.writeBlocks(IntsRef, int, int)函數。

image

由於出度小於max,所以寫成一個non floor的block。

寫入一個Block的函數如下:

image

image

image

對於每一個寫成的block,都要為這個block生成一個FSTIndex,這個過程由函數BlockTreeTermsWriter.PendingBlock.compileIndex實現。

image

image

Block也寫入了,FSTIndex也生成了,這個時候frontier,pending和pendingTerms的結果如下圖所示。

image

 

這里需要解釋一下的BLOCK:abd的FSTIndex里面的映射關系[-38,2]是如何得出來的?這是由下面這個函數計算出來的。fp=86, hasTerm=true, isFloor=false,則二進制位101011010,表示成為VInt為11011010, 00000010,為[-38,2],其實-38是補碼。

image

接下來添加abei, abej, abek, abel, abem, aben之后,這個時候frontier,pending和pendingTerms的結果如下圖3-80所示。

image

 

當所有的Term添加完畢后,BlockTreeTermsWriter.TermsWriter.finish被調用。

image

image

調用freezeTail(0)的時候,還是調用FindBlocks.freeze函數,在freeze狀態Se的時候,出度為6>min,所以調用writeBlocks,由於6>max,因而寫入floor block。

image

image

image

寫入firstBlock和floorBlocks的函數還是上面寫non floor block時調用的writeBlock函數,下面列出一些主要的變量的值。

image

寫入了層級block並且生成FSTIndex之后,frontier,pending和pendingTerms的結果如下圖所示。

image

 

這里需要解釋的是[-77,3,1,107,33]代表的什么呢?首先abe指向的是層級Block,其中firstBlock的起始地址為108,fp=108, hasTerm=true, isFloor=true,則二進制為110110011,表示成為VInt為 [10110011, 00000011],為[-77,3],接下來是floorblock信息。

在函數BlockTreeTermsWriter.PendingBlock.compileIndex中,有這樣一段:

image

接着寫入floorBlock的個數,為1。接着寫這個floorBlock的首字符k(107)。最后寫floorBlock的首地址和firstBlock的首地址的差,sub.fp=124, fp=108, sub.hasTerms=true,所以為33。所以[abe]的output為[-77,3,1,107,33]。

在freeze狀態Se之后,下面應該freeze狀態Sb了,它的出度為3,所以先調用writeBlock寫入一個non floor block的,然后調用compileIndex來為這個block產生新的FSTIndex。

寫入Block的時候,一些重要的變量如下表所示。

表3-17 freeze狀態Sb時writeBlock的變量

image

 

在compileIndex生成當前block的FSTIndex的時候,除了添加prefix=ab所對應的output之外,還會將子block,BLOCK:abd和BLOCK:abe的FSTIndex都添加過來,形成一個整的FSTIndex。

Freeze完狀態Sb之后,frontier,pending和pendingTerms的結果如下圖所示。

image

 

這里pending只有一項,所有子Block的FSTIndex都合並到BLOCK:ab中來,多了一個[ab]的output為[-30,4],這是由fp=152, hasTerm=true, isFloor=false編碼出來的。

接下來對於狀態Sa,出度為1,並不做什么。對於初始狀態S0,出度也為1,按說不做什么,但是在FindBlocks.freeze函數中,有這樣的代碼:

image

這里除了判斷出度是否>min,還有idx==0,對於狀態S0,還是需要調用writeBlocks,將BLOCK:ab寫入tim中。

BlockTreeTermsWriter.TermsWriter.finish函數的blockBuilder.finish()就此結束。接下來從pending.get(0)得到根節點的FSTIndex,由於在compileIndex中,所有的子節點的FSTIndex都會加入到父節點中,最終根節點的FSTIndex是整個狀態機的FSTIndex,然后將它寫入在indexOut,也即tip文件中。

最終,tip和tim文件中Block和FSTIndex的格式和關系如圖3-83所示。

image

 

最后我們再看一下FSTIndex的二進制內容,如下圖3-84所示。

image


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM