Lucene高性能索引之道

      網(wǎng)友投稿 704 2022-05-29

      在Lucene倒排索引原理探秘(1)和Lucene倒排索引原理探秘(2)兩篇文章中詳細介紹了Lucene的倒排索引文件組織結(jié)構(gòu),這為高效的搜索過程奠定了良好的基礎(chǔ)。

      我們已經(jīng)知道,Lucene的倒排索引有兩種格式:一種是用于搜索的Postings;另一種是TermsVectors。這兩種索引的構(gòu)建過程基本相同,只是寫文件時在編排方式上有所差異,在《番外篇:Lucene索引流程與倒排索引實現(xiàn)》一文中已經(jīng)做了詳細介紹。

      在構(gòu)建倒排索引的過程中,Lucene需要收集每個Term在整個Segment中的相關(guān)信息(DocID、TermFreq、Position、Offset、Payload),并且將這些信息存儲下來,如下圖所示。因此,在索引階段,如何高效的收集這些信息,顯得至關(guān)重要,本文將以Postings索引格式為例,詳細探討Lucene索引過程中所采用的數(shù)據(jù)結(jié)構(gòu)與技術(shù)手段。

      數(shù)據(jù)結(jié)構(gòu)

      Lucene設(shè)計了一系列內(nèi)存高效的數(shù)據(jù)結(jié)構(gòu),通過對象復用和內(nèi)存分頁的思想,來優(yōu)化Java GC問題。這部分內(nèi)容將圍繞ByteBlockPool、ByteRefHash、PostingsArray三個結(jié)構(gòu)展開。

      ByteBlockPool

      ByteBlockPool是Lucene實現(xiàn)高效的可變長的基本類型數(shù)組,但實際上數(shù)組一旦初始化之后長度是固定的,因為數(shù)組申請的內(nèi)存必須是連續(xù)分配的,以致能夠提供快速隨機訪問的能力。那么ByteBlockPool是如何實現(xiàn)的?

      Buffer結(jié)構(gòu)

      在JDK中,以數(shù)組為底層存儲的數(shù)據(jù)結(jié)構(gòu),如ArrayList/HashMap,實現(xiàn)可增長時都需要花費一定的代價。數(shù)組增長的過程分兩步,首先申請更大數(shù)組,然后將原數(shù)組復制到新數(shù)組中。即使JVM已經(jīng)對數(shù)組拷貝做了很多優(yōu)化,但隨著數(shù)據(jù)量的不斷增大,拷貝的性能開銷問題也會越來越凸顯,同時,數(shù)組的頻繁創(chuàng)建也都會加大JVM的GC負擔。

      ByteBlockPool是一個可動態(tài)增長的結(jié)構(gòu),如下圖所示:

      ByteBlockPool是一個由多個Buffer構(gòu)成的數(shù)組,如上圖右側(cè)所示,左側(cè)的數(shù)字則代表了每一個Buffer在這個二維數(shù)組中的Index位置。Buffer的長度是固定的,當一個Buffer被寫滿以后,需要申請一個新的Buffer,如果這個Buffer數(shù)組要擴展,僅僅是將已有Buffer的引用拷貝了一次,而不需要拷貝數(shù)據(jù)本身。Buffer本質(zhì)上是一個byte[],因此,ByteBlockPool其實是一個byte的二維數(shù)組,用Buffer數(shù)組來表達則更易理解。

      Slice鏈表

      索引構(gòu)建過程需要為每個Term分配一塊相對獨立的空間來存儲Posting信息。一個Term可能會出現(xiàn)在幾個文檔中,而且在每個文檔出現(xiàn)的次數(shù)和位置都無法確定,所以Lucene無法預知Term需要多大的數(shù)組來存儲Posting信息。

      為此Lucene在ByteBlockPool之上設(shè)計了可變長的邏輯結(jié)構(gòu),這結(jié)構(gòu)就是Slice鏈表。它的節(jié)點稱之為Slice,Lucene將Slice分成十個級別,逐層遞增,十層之后長度恒定。Slice的最后一個位置用于存儲下個節(jié)點Offset,對于最后一個Slice,則存儲了下個Slice的層級數(shù)。

      Slice節(jié)點可以跨越多個Buffer,Slice鏈表為我們提供了一個邏輯上連續(xù)的內(nèi)存塊。如果將Slice鏈表理解成類分布式文件系統(tǒng)上的文件,每個Slice則是文件的數(shù)據(jù)塊,不過文件系統(tǒng)的數(shù)據(jù)塊的大小是固定的,而Slice的長度則是分層級的。

      這種設(shè)計方案的一個好處:Buffer是相對比較緊湊的結(jié)構(gòu),能夠更高效的利用Buffer內(nèi)存。按Zipf定律可知一個單詞出現(xiàn)的次數(shù)與它在頻率表里的排名成反比,也就是說可能會有很多Term的頻率是很低的,同樣也有小部分Term的頻率則非常高,Slice的設(shè)計正是考慮到了這一分布特點。

      ByteBlockPool與IntBlockPool設(shè)計思想完全一樣,IntBlockPool只能存儲int,ByteBlockPool存儲byte,這里我們不再贅述。Lucene僅實現(xiàn)這兩種基礎(chǔ)的數(shù)據(jù)類型,其它的類型可以通過編碼之后用ByteBlockPool來存儲。

      BytesRefHash

      Lucene在構(gòu)建Postings的時候,采用一種類似HashMap結(jié)構(gòu),這個類HashMap的結(jié)構(gòu)便是BytesRefHash,它是一個非通用的Map實現(xiàn)。?它的非通用性表現(xiàn)在BytesRefHash存儲的鍵值對分別是Term和TermID,其次它并沒有實現(xiàn)Map接口,也沒有實現(xiàn)Map的相關(guān)操作。

      Term在Lucene中通常會被表示為BytesRef,而BytesRef的內(nèi)部是一個byte[],這是一個可以復用的對象。當通過TermID在BytesRefHash獲取詞元的時候,便將ByteBlockPool的byte[]拷貝到BytesRef的byte[],同時指定有效長度。整個BytesRefHash生存周期中僅持有一個BytsRef,所以該BytesRef的byte[]長度是詞元的長度。

      BytesRefHash用來存儲Term和TermID之間的映射關(guān)系,如果Term已經(jīng)存在,返回對應(yīng)TermID;否則將Term存儲并且生成TermID后返回。Term在存儲過程BytesRefHash將BytesRef的有效數(shù)據(jù)拷貝在ByteBlockPool上,從而實現(xiàn)緊湊的key值存儲。TermID是從0開始自增長的連續(xù)數(shù)值,存儲在int[]上。BytesRefHash非散列哈希表,從而TermID的存儲也是緊湊的。

      因為BytesRefHash為了盡可能避免用到對象類型,所以直接采用int[]存儲TermID,實際上也就很難直接采用散列表的數(shù)據(jù)結(jié)構(gòu)來解決HashCode沖突的問題。

      Lucene構(gòu)建倒排索引的過程分了兩步操作,構(gòu)建Postings和TermVectors。它們倆過程共享一個ByteBlockPool,也就是在每個DocumentsWriter共用同一個ByteRefHash(因為BytesRefHash以ByteBlockPool都不是線程安全的)。它為Postings收集過程提供去重和Term與TermID對應(yīng)關(guān)系的存儲及檢索等功能。

      PostingsArrays

      從PostingsArrays名字上容易被誤以為是存儲Postings數(shù)據(jù)的結(jié)構(gòu),實則不然。在Postings構(gòu)建過程中,Lucene將各項信息寫到ByteBlockPool的Slice鏈表上。鏈表是單向鏈表,它的表頭和表尾存儲到PostingsArrays,從而能夠快速寫入,并且可以從頭開始遍歷。這個結(jié)構(gòu)曾在《番外篇:Lucene索引流程與倒排索引實現(xiàn)》一文中介紹過。

      PostingsArrays除了記錄了Slice鏈表的索引之外,它還存儲上個文檔的DocID和TermFreq,還有Term上次出現(xiàn)的位置和偏移信息。PostingsArrays由幾個int[]組成,其下標都是TermID(TermID是連續(xù)分配的整數(shù)),對應(yīng)的值便是記錄TermID上一次出現(xiàn)的各種信息。

      Lucene為了能夠直接使用基本類型數(shù)據(jù),所以才有了PostingsArrays結(jié)構(gòu)。方便理解你可以理解成是Postings[],每個Postings對象含有DocId,TermFreq,intStarts,lastPosition等屬性。

      索引構(gòu)建過程

      在索引構(gòu)過程中,Term由TextField經(jīng)過TokenStream處理之后產(chǎn)生,它由一個可復用對象BytesRef表示。在建構(gòu)索引的鏈路上,Lucene更多是用TermID來表示Term。

      當Term第一次出現(xiàn)時,Lucene嘗試在BytesRefHash取到TermID失敗,此時Lucene將它的狀態(tài)標記"新人"。新出現(xiàn)的Term作為"新人",需要在BytesRefHash上為它分配一個"證件號碼"(TermID)。在PostingsArrays中會登記他的"戶籍信息",包含他的名字(BytesRef的byte[])在哪個位置(前面提過,BytesRefHash直接把Term存儲在ByteBlockPool上,所以需要把位置記下來);還會為他建立一個履歷檔案(第一Slice鏈表),記錄他將來在每個年級(DocID)的考試次數(shù)(TermFreq)。

      Lucene高性能索引之道

      在每一個"地區(qū)",還可以為Term建立另外一份檔案(第二Slice鏈表)用于記錄每次考試的情況,比如班級(Position),座位號(Offset),以及成績(Payload)。這些數(shù)據(jù)是Term成長過程的產(chǎn)生,經(jīng)歷一次記一次。關(guān)于每次考試的情況,第一次考試Lucene直接把它寫入ByteBlockPool的Slice鏈表上,同時會記錄增量信息,為了節(jié)省內(nèi)存空間,Position/Offset第二次及以后都用VInt來記錄這個增量值。

      這就是PostingsArrays需要記錄lastPosition/lastOffset的原因,而lastDocID和lastTermFreq不僅可用計算增量,還可以用來計算Term每次出現(xiàn)后TermFreq的累加值。需要說明的一點:每個信息在Slice中沒有索引,不方便回溯和修改。

      構(gòu)建索引的過程是ByteBlockPool(IntBlockPool)、BytesRefHash和PostingsArrays三者之間的協(xié)作,如下圖所示:

      這里為了簡化流程,圖中將IntBlockPool簡化成為int[],也就是說它也是Slice的方式實現(xiàn)連續(xù)的鏈表。

      PostingsArrays的byteStarts[TermID]記錄Term的兩個鏈表的表頭在ByteBlockPool的絕對位置,intStarts[TermID]記錄下次要寫的位置,則textStarts[TermID]則記錄BytesRefHash把Term存儲在ByteBlockPool的哪個位置上。

      為什么byteStart和intStart需要先指向IntBlockPool呢??主要是因為TermID可能對應(yīng)了兩條Slice鏈表,以TermID為索引的數(shù)組不方便存儲。通過IntBlockPool可以方便處理,僅需要IntBlockPool連續(xù)兩個位置,下一個位置用于存儲第兩個Slice鏈表。IntBlockPool的引入雖然讓這個過程變得更復雜了,但也更體現(xiàn)了Lucene的設(shè)計之精湛和巧妙。

      在索引提交的時候,Lucene將這兩個Slice鏈表的數(shù)據(jù)通過PostingsEnum遍歷出來,交由BlockTreeTermsWriter完成索引文件的輸出。至此,已經(jīng)完成了一輪構(gòu)建索引的流程。

      本文轉(zhuǎn)載自微信公眾號【Nosql漫談】。

      NoSQL數(shù)據(jù)庫

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔相應(yīng)法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。

      上一篇:【七天入門Go語言】 文件 && 包 | 第五天
      下一篇:【嵌入式Linux基礎(chǔ)入門】3、設(shè)置虛擬機和實體機的共享文件夾
      相關(guān)文章
      亚洲综合无码无在线观看| 亚洲人成高清在线播放| 亚洲大码熟女在线观看| 亚洲va成无码人在线观看| 亚洲人成电影在线天堂| 亚洲av鲁丝一区二区三区| 久久精品国产亚洲av成人| 亚洲成av人片天堂网| 国产AV无码专区亚洲A∨毛片| 国产亚洲精品观看91在线| 亚洲精品~无码抽插| 亚洲国产精彩中文乱码AV| 亚洲av伊人久久综合密臀性色| 亚洲成AV人片在线观看无| 亚洲成AV人片在线观看无| 亚洲色图在线播放| 久久亚洲精品无码VA大香大香| 久久久久久亚洲Av无码精品专口| 91精品国产亚洲爽啪在线观看| 亚洲国产一区二区三区青草影视| 久久久久亚洲Av片无码v| 亚洲综合在线视频| 亚洲国产美女在线观看| 久久亚洲最大成人网4438| 在线观看亚洲AV日韩AV| 亚洲av成人一区二区三区在线播放| 久久亚洲精品高潮综合色a片| 内射无码专区久久亚洲| 国产亚洲日韩一区二区三区| 久久亚洲综合色一区二区三区| 亚洲av不卡一区二区三区| 亚洲精品在线电影| 激情亚洲一区国产精品| 亚洲欧美成人一区二区三区| 国产精品亚洲一区二区三区久久| 亚洲精品视频免费观看| 日本亚洲欧洲免费天堂午夜看片女人员| 久久精品国产亚洲香蕉| 亚洲一卡二卡三卡| 亚洲αⅴ无码乱码在线观看性色| 亚洲精品456播放|