1 引言(Introduction)
在Lucence中包括了幾個基礎的概念,分別是索引、段、文檔、域和項。其中索引由段構成,段由文檔構成,因此索引可以理解為包含了多個文檔的序列。文檔由域構成,域由項構成,項是索引中最小構成單位,其本質是一個字符串。段是索引數據存儲的基本單元,多個段之間彼此獨立,當添加新的文檔時將生成新的段,且段可以合并。
充分理解Lucence索引的內部結構,對于在當前互聯網大數據、云存儲應用環境下較好使用Lucence進行具有重要意義。
2 索引文件結構(Index file structure)
2.1 倒排索引
對文本信息進行檢查的系統索引可以分為包括正排索引和倒排索引[1].在實際使用中根據屬性值查找記錄的過程稱為倒排索引。倒排表中存儲了文檔中包含的單詞,文檔編號差值、單詞出現次數等,這些信息即組成了倒排索引項term.倒排索引入口為索引項term,通過倒排索引可以找到包含每個term的文檔集合稱為記錄表,記錄表中包含文檔號及term在該文檔中的出現頻率。
Lucene為了使得基于term的檢索效率更高,索引存儲terms的統計數據。Lucene的索引采用了倒排索引。它可以列舉,包含一個term的所有文檔。這與自然關聯規則相反,即由documents列舉它所包含的terms.
2.2 域Fields
Lucene在存儲域數據時,域中文本被逐字的以非倒轉方式進行轉換,這些被倒排存儲的域數據可以被索引[2].每個域的文本被拆分為多個索引項以便被高效索引,另一種方式為域中文本作為一個索引項進行索引。
2.3 片斷
Lucene索引可以由多個復合的子索引或片段構成。每個段是一個完全獨立的索引,它可以分離提取進行檢索。檢索過程如下:(1)當添加新的文檔時創建新的段;(2)對現有段進行合并。在索引檢索時,會涉及多個索引或段,因此每一個索引都隱含包括了一組段。段及文檔關系如圖1所示。
2.4 文檔編號
在Lucene的內部對于每個文檔使用一個整數編號進行標識。初始情況下,第一個被加進來的文檔其文檔編號為0,之后加進來的文檔其編號自動遞增,采用自動遞增方式對文檔進行編號,編號不會出現重復。但需注意的是,文檔編號可以被手動更改。因此在維護時若出現更改情況,需特別考慮此編號是否在更大的范圍內被使用。通常的做法是為每個段空間設置文檔編號的范圍,以保證段之間的文檔編號不重復。
綜上所述,文檔編號若發生修改可能導致錯誤,因此文檔編號應先進行設計,在應用時不輕易、不頻繁修改文檔編號,只有當出現文檔在某段空間是統一的,且需要在整個系統中使用時必須修改情況下才進行修改。
3 索引文件(Index file)
3.1 索引文件
Lucene索引文件包含多種,其使用不同的文件擴展名來進行標識。同時文件名稱可以標識不同的版本。常見的文件擴展名有:fnm文件存儲域名和域屬性、fdt文件存儲域數據、fdx文件存儲在fdt文件中的偏移位置、frq存儲文件中項的位置數據等。對于段文件命名通常為segments_x,其中x即為最新修改版本,文件segments.gen存儲當前版本值。以下文件存在于每個索引index中,并且只有一份。
(1)Segments文件
索引中活動的Segments被存儲在segment info文件中,segments_N,在索引中可能會包含一個或多個segments_N文件。然而,最大一代的那個文件是活動的片斷文件(這時更舊的segments_N文件依然存在是因為它們暫時還不能被刪除,或者,一個writer正在處理提交請求,或者一個用戶定義的IndexDeletionPolicy正被使用。這個文件按照名稱列舉每一個片斷,詳細描述分離的標準和要刪除的文件,并且還包含了每一個片斷的大小。
(2)Lock文件
寫鎖文件名為"write.lock",存儲在默認的索引目錄中。如果鎖目錄和索引目錄不一致的,寫鎖將被命名為"XXXX-write.lock",其中"XXXX"是一個特殊唯一的前綴,來源于索引目錄的路徑。
(3)Deletable文件
Lucene的刪除機制類似于操作系統回收站,在執行刪除操作時,相當于對索引進行了"刪除標記"并未真實刪除,當索引下次優化或合并時再從索引中真正刪除。
(4)Compound文件
Compound文件格式成一個簡單的容器,其用來服務所有Segment包含的文件(除了。del文件)。
3.2 Segment
每個段中的文件是由后綴名來區分的。
(1)域集合信息
域的信息以集合形式存儲,域集合文件后綴為。fnm.在當前的情況下,fieldbits僅用于低位,索引的域值是1,未索引的域值為0,文件中各域根據次序進行編號,即認為值0是第一個域文件,值1是下一個域文件等。Fields將使用它們在這個文件中的順序來編號。需要注意的是,就像文檔編號一樣,field編號與片斷是相關的。
(2)域索引和域值
域值存儲表使用兩種文件存儲 , 分別為 : 域索引表(。fdx文件)文件,這個文件包含指向域值的指針FieldvaluesPosition,類型為UInt64類型,此指針指向了某域值在域文件中的位置。因為域值文件包括定長的數據信息,較易隨機訪問;域值(。fdt文件)文件,每個文檔的域值信息包括了字段FieldData、DocFieldData、FieldCount、FieldNum、Bits和value.
(3)項字典
項Term字典使用如下兩種文件存儲,第一種是存儲term信息(TermInfoFile)的文件,即。tis文件,另一種是存儲term信息索引的文件,即。tii文件。TermInfoFile文件按照Term來排序,排序方法首先按照Term的field名稱(按照UTF-16字符編碼)排序,然后按照Term的Text字符串(UTF-16編碼)排序。項的命名上前綴通常上是相同的,然后以字的后綴組成。
(4)項頻率數據
Term頻率數據文件(。frq文件)存儲容納了每一個term的文檔列表,以及該term出現在該文檔中的頻率(出現次數frequency,如果omitTf設置為fals時才存儲)。
關 于 skiplevels , 每一個 term 可以有多個skip levels.一個term的skip levels的數目等于NumSkipLevels=Min(MaxSkipLevels,floor(log(DocFreq/log(SkipInterval))))。對一個skip level來說SkipData記錄的數目等于DocFreq/(SkipInterval^(Level+1))。然而最低的skip level等于Level=0.例如假設SkipInterval=4,MaxSkipLevels=2,DocFreq=35,則skip level 0有8個SkipData記錄,在TermFreqs序列中包含第3、7、11、15、19、23、27和31個文檔的編號。Skip level 1則有兩個SkipData記錄,在TermFreqs中包含了第15和第31個文檔的編號。在所有level>0之上的SkipData記錄中包含一個SkipChildLevelPointer,指向(referencing)level-1中相應)的SkipData記錄。在這個例子中,level 1中的記錄15有一個指針指向level 0中的記錄15,level 1中的記錄31有一個指針指向level 0中的記錄31.
(5)項位置
Positions位置信息數據文件(。prx文件)容納了每一個term出現在所有文檔中的位置的列表,可以利用這些信息來參與對索引結果的排序。如果在fields中的omitTf設置為true時將不會在此文件中存儲任何信息,并且如果索引中所有fields中的omitTf都設置為true,此。prx文件將不會存在。
(6)標準化因子文件
在Lucene 2.1版本之前,每一個索引都有一個norm文件給每一個文檔都保存了一個字節。對每一個文檔來說,那些。f[0-9]包含了一個字節容納一個被編碼的分數,值為對hits結果集來說在那個field中被相乘得出的分數。每一個分離的norm文件在適當的時候為復合的和非復合的segment片斷創建。在Lucene 2.1及以上版本,只有一個norm文件容納了所有norms數據:一個分離的norm文件在一個存在的segment的norm數據被更改的時候被創建,當field N被修改時,一個分離的norm文件。sN被創建,用來維護該field的norm數據。
(7)項向量文件
Term向量的支持是field基本組成中對一個field來說的可選項,它包含如下三種文件:a.文檔索引或。tvx文件:對每個文檔來說,它把偏移存儲進文檔數據(。tvd)文件和域field數據(。tvf)文件。b.文檔或。tvd文件:對每個文檔來說,它包含fields的數目,有term向量的fields的列表,還有指向term向量域文件(。tvf)中的域信息的指針列表。該文件用于映射出那些存儲了term向量的fields,以及這些field信息在。tvf文件中的位置。c.域field或。tvf文件:對每個存儲了term向量的field來說,該文件包含了一個term的列表,及它們的頻率,還有可選的位置和偏移信息。
(8)刪除的文檔
刪除的文檔(。del)文件是可選的,而且僅當一個segment存在有被刪除的文檔時才存在。即使對每一單個segment,它也是維護復合segment的外部數據。
4 索引創建過程(The index creation process)
為了使用Lucene的索引數據,必須先將其轉換為文本標記的數據流,并用它來創建一個文檔對象,其中包含的域成員來容納這些文本數據[3,4].一旦你準備的文檔對象,可以調用IndexWriter類的adddocument方法傳遞對象到Lucene寫索引。當這樣做時Lucene進行分析數據,以便更適合索引。
文檔的索引過程是通過DocumentsWriter的內部數據處理鏈完成的,DocumentsWriter可以實現同時添加多個文檔并將它們寫入一個臨時的segment中,完成后再由IndexWriter和SegmentMerger合并到統一的segment中去。
DocumentsWriter支持多線程處理,即多個線程同時添加文檔,它會為每個請求分配一個DocumentsWriterThreadState對象來監控此處理過程。處理時通過DocumentsWriter初始化時建立的DocFieldProcessor管理的索引處理鏈來完成的,依次處理為DocFieldConsumers、DocInverter、TermsHash、FreqProxTermsWriter、TermVectorsTermsWriter、NormsWriter以及StoredFieldsWriter等。
DocFieldProcessorPerThread.processDocument()方法是處理一個文檔的調度函數,負責整理文檔的各個fields數據,并創建相應的DocFieldProcessorPerField對象來依次處理每一個field.該方法首先調用索引鏈表的startDocument()來初始化各項數據,然后依次遍歷每一個fields,將它們建立一個以field名字計算的hash值為key的hash表,值為DocFieldProcessorPerField類型。如果hash表中已存在該field,則更新該FieldInfo(調用FieldInfo.update()方法),如果不存在則創建一個新的DocFieldProcessorPerField來加入hash表中。該hash表會存儲包括當前添加文檔的所有文檔的fields信息,并根據FieldInfo.update()來合并相同field名字的域設置信息。
建立hash表的同時,生成針對該文檔的fields[]數組(只包含該文檔的fields,但會共用相同的fields數組,通過lastGen來控制當前文檔),如果field名字相同,則將Field添加到DocFieldProcessorPerField中的fields數組中。建立完fields后再將此fields數組按field名字排序,使得寫入的vectors等數據也按此順序排序。之后開始正式的文檔處理,通過遍歷fields數組依次調用DocFieldProcessorPerField的processFields()方法進行,完成后調用finishDocument()完成后序工作,如寫入FieldInfos等。
5 結論(Conclusion)
Lucene自發布至今已有近15年時間,近幾年互聯網的快速發展更使得產生的數據呈指數級增長,Lucene面對海量數據進行處理,因此在檢索時針對索引的優化至關重要。牢固掌握Lucene索引原理對于解決當前大數據環境下數據檢索以及提高檢索效率打下良好基礎。
參考文獻(References):
[1] 王冬。一種增量倒排索引結構的設計與實現[J].吉林大學學報,2007,45(06):953-958.
[2] 黃軼文。搜索引擎原理與快速開發應用[J].科技信息,2010,(36):570-571.
[3] 何偉,等?;贚ucene的全文搜索引擎的設計與實現[J].2006,25(9):88-90.
[4] 李曉麗,杜振龍?;贚ucence的個性化搜索引擎研究[J].計算機工程,2010,36(19):258-260.