1. 程式人生 > >Lucene倒排索引原理與實現:Term Dictionary和Index檔案 (FST詳細解析)

Lucene倒排索引原理與實現: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

相關推薦

Lucene索引原理實現:Term DictionaryIndex檔案 (FST詳細解析)

我們來看最複雜的部分,就是Term Dictionary和Term Index檔案,Term Dictionary檔案的字尾名為tim,Term Index檔案的字尾名是tip,格式如圖所示。 Term Dictionary檔案首先是一個Header,接下來是Pos

Term DictionaryIndex檔案 (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索引--fstSkipList的結合

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中難啃的骨頭