Lucene倒排索引原理與實現:Term Dictionary和Index檔案 (FST詳細解析)
我們來看最複雜的部分,就是Term Dictionary和Term Index檔案,Term Dictionary檔案的字尾名為tim,Term Index檔案的字尾名是tip,格式如圖所示。
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之外的所有資訊。
Lucene40PostingsWriter的start函式如下:
下面咱們具體討論,Term如何分塊,Block如何寫入,FSTIndex如何構造。
我們首先通過一個簡單的例子,來看一下一個普通的FST是如何構造的,Lucene的文件裡面給了類似下面這樣一個例子。
這裡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所示。
相關推薦
Lucene倒排索引原理與實現:Term Dictionary和Index檔案 (FST詳細解析)
我們來看最複雜的部分,就是Term Dictionary和Term Index檔案,Term Dictionary檔案的字尾名為tim,Term Index檔案的字尾名是tip,格式如圖所示。 Term Dictionary檔案首先是一個Header,接下來是Pos
Term Dictionary和Index檔案 (FST詳細解析)
有限自動機演算法(FST,Finite State Transducer):通過輸入有序字串構建最小有向無環圖。通過共享字首來節省空間,記憶體存放字首索引,磁碟存放字尾詞塊 1、緊湊的結構,通過對詞典中單詞字首和字尾的重複利用,壓縮了儲存空間。 2、O(len(st
Lucene 4.X 倒排索引原理與實現: (1) 詞典的設計
詞典的格式設計 詞典中所儲存的資訊主要是三部分: Term字串 Term的統計資訊,比如文件頻率(Document Frequency) 倒排表的位置資訊 其中Term字串如何儲存是一個很大的問題,根據上一章基本原理的表述中,我們知道,寫入檔案的Term是按照字典順序排好序的,那麼如何將這些
Lucene倒排索引原理(轉)
Lucene是一個高效能的java全文檢索工具包,它使用的是倒排檔案索引結構。該結構及相應的生成演算法如下:0)設有兩篇文章1和2文章1的內容為:Tom lives in Guangzhou,I live in Guangzhou too.文章2的內容為:He once li
Elasticsearch系列---倒排索引原理與分詞器
概要 本篇主要講解倒排索引的基本原理以及ES常用的幾種分詞器介紹。 倒排索引的建立過程 倒排索引是搜尋引擎中常見的索引方法,用來儲存在全文搜尋下某個單詞在一個文件中儲存位置的對映。通過倒排索引,我們輸入一個關鍵詞,可以非常快地獲取包含這個關鍵詞的文件列表。 我們先看英文的,假設我們有兩個文件: I have
倒排索引原理和實現
轉載https://blog.csdn.net/u011239443/article/details/60604017 倒排索引原理和實現 關於倒排索引 場景是:給定幾個關鍵詞,找出包含關鍵詞的文件 倒排索引: 不是由記錄來確定屬性值,而是由屬性值來確定記錄的位置
lucene倒排索引表搜尋原理
什麼是正排索引?什麼是倒排索引?搜尋的過程是什麼樣的?會用到哪些演算法與資料結構?前面的內容太巨集觀,為了照顧大部分沒有做過搜尋引擎的同學,資料結構與演算法部分從正排索引、倒排索引一點點開始。提問:什麼
ElasticSearch倒排索引原理揭祕——基於mapreduce實現自己的倒排索引
Elasticsearch簡單介紹 Elasticsearch (ES)是一個基於Lucene構建的開源、分散式、REST
Lucene倒排索引簡述 之倒排表
一、前言 上一篇《Lucene倒排索引簡述 之索引表》,已經對整個倒索引的結構進行大體介紹,並且詳細介紹了索引表(TermsDictionary)的內容。同時還詳細介紹了Lucene關於索引表的實現,相關檔案結構詳解,以及對索引表採用的資料結構進行剖析解讀。
Lucene倒排索引簡述 細說倒排索引構建
在《Lucene倒排索引簡述 之索引表》和《Lucene倒排索引簡述 之倒排表》兩篇文章中介紹了Lucene如何將倒排索引結構寫入索引檔案,如何為實現高效搜尋過程奠定了基礎。 Lucene需要收集每個Term在整個Segment的所有資訊(DocID/Term
倒排索引的分散式實現(MapReduce程式)
package aturbo.index.inverted; import java.io.IOException; import java.util.HashSet; import org.apache.commons.lang3.StringUtils; imp
lucene倒排索引--fst和SkipList的結合
1. 使用FST儲存詞典,FST可以實現快速的Seek,這種結構在當查詢可以表達成自動機時(PrefixQuery、FuzzyQuery、RegexpQuery等)效率很高。(可以理解成自動機取交集)此種場景主要用在對Query進行rewrite的時候。2. FST可以表達出
lucene 倒排索引、反向索引概念明晰
lucene中,一直在糾結什麼叫倒排索引,為什麼叫倒排索引,找了n個部落格沒有對該名詞很透徹的解析,重於在知乎上中找到需要的答案: ----------------------------------------------------------------------
倒排索引的java實現
假設有3篇文章,file1, file2, file3,檔案內容如下: 檔案內容程式碼 file1 (單詞1,單詞2,單詞3,單詞4....) file2 (單詞a,單詞b,單
Lucene倒排索引簡述 之索引表
一、前言 倒排索引是全文檢索的根基,理解了倒排索引之後才能算是入門了全文檢索領域。倒排索引的的概念很簡單,也很好理解。但如你知道在全文檢索領域Lucene可謂是獨領風騷。所以你真的瞭解Lucene的倒排了嗎?Lucene是如何實現這個結構的呢? 倒排索引如此重
Lucene倒排索引簡述 番外篇
一、前言 Lucene構建索引是一個非常複雜的過程,需要經過多道工序才能完成。那你知道Lucene在索引構建過程有哪些工序嗎?又是整體流程是怎麼樣的呢?這裡儘量從巨集觀的角度來介紹索引全過程,給大家一個全景的印象,且不失關鍵步驟細節的介紹。 在Lucene接使
elasticsearch-倒排索引原理
Term Doc_1 Doc_2 ------------------------- Quick | | X The | X | brown | X | X dog | X | dogs | | X fox
談談lucene倒排索引的儲存方式(一)
詞的位置具體包括每篇文件中的詞頻、位置以及附帶的payload(這裡先忽略掉norm資訊的儲存),這3塊lucene分別採用了3個輸出流進行寫入,具體寫入過程如下: 1、對於每個詞而言會記錄該次所屬的文件ID以及在該文件中的詞頻,由於文件ID已經排過序所以寫入時會進行差值壓縮儲存,而文件詞頻會直接儲存,
談談lucene倒排索引的儲存方式(二)
在談談lucene倒排索引的儲存方
談談lucene倒排索引的儲存方式(3-1)
現在開啟lucene倒排索引之詞的索引儲存之旅也就是對BlockTreeTermsWriter類分析,這是lucene中難啃的骨頭