咱們來看最複雜的部分,就是Term Dictionary和Term Index文件,Term Dictionary文件的後綴名爲tim,Term Index文件的後綴名是tip,格式如圖所示。apache
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是什麼,如何利用他來分塊呢?eclipse
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文件的指針。ui
Tim和tip文件的寫入是由org.apache.lucene.codecs.BlockTreeTermsWriter來負責的,在它的構造函數中,生成了兩個OutputStream,而且寫入除了Block和FSTIndex以外的全部信息。編碼
Lucene40PostingsWriter的start函數以下:3d
下面我們具體討論,Term如何分塊,Block如何寫入,FSTIndex如何構造。指針
咱們首先經過一個簡單的例子,來看一下一個普通的FST是如何構造的,Lucene的文檔裏面給了相似下面這樣一個例子。code
這裏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函數主要包括四個部分:
當第一個字符串abd加入以後,frontier的結構如圖3-72所示,圖中藍色的節點都是。
當新的字符串abe以後,首先(1)找出公共前綴ab,則prefixLenPlus1=3。而後調(2)用freezeTail將尾節點Sd進行冰封。爲何要進行冰封(一個形象的說法)呢?由於Sd節點不會再改變了。在實際應用中,字符串都是按照字母順序依次處理的,上一次的字符串是abd,下一個字符串多是abdm,再下一個字符串多是abdn,這都會致使Sd這個節點的變化。然而當abe出現後,說明abd*都不可能出現了,狀態Sd也不可能再有新的子節點了,因此Sd也就肯定下來了,須要冰封。那麼Sb節點要不要冰封呢?固然不行了,由於此次來了abe,下次還可能有abf, abg等等新的Sb的子節點出現,這就是爲何要計算公共前綴了,公共前綴以後的狀態節點都是能夠冰封的了,而這些冰封的節點都從尾部開始,因此這一步的函數叫freezeTail。
freezeTail的實現以下:
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>),代碼以下:
而後(3)將新的input添加到frontier以後,變成如圖3-73的數據結構。
依次類推,當添加acf以後,frontier變成以下的數據結構。
最後調用Builder的finish函數生成FST,代碼以下:
造成的二進制數組如圖3-75所示,因爲有內容翻轉,因此解析的時候須要從右向左解析。
瞭解了最基本的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中,有時候字符顯示的是十進制,有時候是十六進制,當看到這些數值的時候,知道是這些字符便可。
寫tim和tip文件的過程紛繁複雜,下面的流程圖3-77做爲一個線索
每來一個新的Term,都調用finishTerm。
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也如圖所示。
加入abei的時候,對Sd進行freeze的時候,發現Sd的出度爲3,大於min,則開始調用BlockTreeTermsWriter.TermsWriter.writeBlocks(IntsRef, int, int)函數。
因爲出度小於max,因此寫成一個non floor的block。
寫入一個Block的函數以下:
對於每個寫成的block,都要爲這個block生成一個FSTIndex,這個過程由函數BlockTreeTermsWriter.PendingBlock.compileIndex實現。
Block也寫入了,FSTIndex也生成了,這個時候frontier,pending和pendingTerms的結果以下圖所示。
這裏須要解釋一下的BLOCK:abd的FSTIndex裏面的映射關係[-38,2]是如何得出來的?這是由下面這個函數計算出來的。fp=86, hasTerm=true, isFloor=false,則二進制位101011010,表示成爲VInt爲11011010, 00000010,爲[-38,2],其實-38是補碼。
接下來添加abei, abej, abek, abel, abem, aben以後,這個時候frontier,pending和pendingTerms的結果以下圖3-80所示。
當全部的Term添加完畢後,BlockTreeTermsWriter.TermsWriter.finish被調用。
調用freezeTail(0)的時候,仍是調用FindBlocks.freeze函數,在freeze狀態Se的時候,出度爲6>min,因此調用writeBlocks,因爲6>max,於是寫入floor block。
寫入firstBlock和floorBlocks的函數仍是上面寫non floor block時調用的writeBlock函數,下面列出一些主要的變量的值。
寫入了層級block而且生成FSTIndex以後,frontier,pending和pendingTerms的結果以下圖所示。
這裏須要解釋的是[-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中,有這樣一段:
接着寫入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的變量
在compileIndex生成當前block的FSTIndex的時候,除了添加prefix=ab所對應的output以外,還會將子block,BLOCK:abd和BLOCK:abe的FSTIndex都添加過來,造成一個整的FSTIndex。
Freeze完狀態Sb以後,frontier,pending和pendingTerms的結果以下圖所示。
這裏pending只有一項,全部子Block的FSTIndex都合併到BLOCK:ab中來,多了一個[ab]的output爲[-30,4],這是由fp=152, hasTerm=true, isFloor=false編碼出來的。
接下來對於狀態Sa,出度爲1,並不作什麼。對於初始狀態S0,出度也爲1,按說不作什麼,可是在FindBlocks.freeze函數中,有這樣的代碼:
這裏除了判斷出度是否>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所示。
最後咱們再看一下FSTIndex的二進制內容,以下圖3-84所示。