一、Lucene源碼實現分析的說明
通過以上對Lucene系統結構的分析,我們已經大致的清楚了Lucene 系統的組成,以及在Lucene系統之上的開發步驟。接下來,我們試圖來分析Lucene項目(采用Lucene 1.2版本)的源碼實現,考察其實現的細節。這不僅僅是我們嘗試用C++語言重新實現Lucene的必須工作,也是進一步做Lucene開發工作的必要準備。因此,這一部分所涉及到的內容,對于Lucene上的應用開發也是有價值的,尤其是本部分所做的文件格式分析。
由于本文建立在我們的畢設項目之上,且同時我們需要實現cLucene項目,因此很遺憾的我們并沒有完全的完成Lucene的所有源碼實現的分析工作。接下來的部分,我們將涉及的部分為Lucene文件格式分析,Lucene中的存儲抽象模塊分析,以及Lucene中的索引構建邏輯模塊分析。這一部分,我們主要涉及到的是文件格式分析與存儲抽象模塊分析。
二、Lucene索引文件格式
在Lucene的web站點上,有關于 Lucene的文件格式的規范,其規定了Lucene的文件格式采取的存儲單位、組織結構、命名規范等等內容,但是它僅僅是一個規范說明,并沒有從實現者角度來衡量這個規范的實現。因此,我們以下的內容,結合了我們自己的分析與文件格式的定義規范,以期望給出一個更加清晰的文件格式說明。具體的文檔規范可以參考后面的文獻2。
首先在Lucene的文件格式中,以字節為基礎,定義了如下的數據類型:
表 3.1 Lucene文件格式中定義的數據類型
數據類型 |
所占字節長度(字節) |
說明 |
Byte |
1 |
基本數據類型,其他數據類型以此為基礎定義 |
UInt32 |
4 |
32位無符號整數,高位優先 |
UInt64 |
8 |
64位無符號整數,高位優先 |
VInt |
不定,最少1字節 |
動態長度整數,每字節的最高位表明還剩多少字節,每字節的低七位表明整數的值,高位優先。可以認為值可以為無限大。其示例如下
值 |
字節1 |
字節2 |
字節3 |
0 |
00000000 |
|
|
1 |
00000001 |
|
|
2 |
00000010 |
|
|
127 |
01111111 |
|
|
128 |
10000000 |
00000001 |
|
129 |
10000001 |
00000001 |
|
130 |
10000010 |
00000001 |
|
16383 |
10000000 |
10000000 |
00000001 |
16384 |
10000001 |
10000000 |
00000001 |
16385 |
10000010 |
10000000 |
00000001 |
|
Chars |
不定,最少1字節 |
采用UTF-8編碼[20]的Unicode字符序列 |
String |
不定,最少2字節 |
由VInt和Chars組成的字符串類型,VInt表示Chars的長度,Chars則表示了String的值 |
以上的數據類型就是Lucene索引文件格式中用到的全部數據類型,由于它們都以字節為基礎定義而來,因此保證了是平臺無關,這也是Lucene索引文件格式平臺無關的主要原因。接下來我們看看Lucene索引文件的概念組成和結構組成。

以上就是Lucene的索引文件的概念結構。Lucene索引index由若干段(segment)組成,每
一段由若干的文檔(document)組成,每一個文檔由若干的域(field)組成,每一個域由若干的項(term)組成。項是最小的索引概念單位,它直接代表了一個字符串以及其在文件中的位置、出現次數等信息。域是一個關聯的元組,由一個域名和一個域值組成,域名是一個字串,域值是一個項,比如將“標題”和實際標題的項組成的域。文檔是提取了某個文件中的所有信息之后的結果,這些組成了段,或者稱為一個子索引。子索引可以組合為索引,也可以合并為一個新的包含了所有合并項內部元素的子索引。我們可以清楚的看出,Lucene的索引結構在概念上即為傳統的倒排索引結構[21]。
從概念上映射到結構中,索引被處理為一個目錄(文件夾),其中含有的所有文件即為其內容,這些文件按照所屬的段不同分組存放,同組的文件擁有相同的文件名,不同的擴展名。此外還有三個文件,分別用來保存所有的段的記錄、保存已刪除文件的記錄和控制讀寫的同步,它們分別是segments, deletable和lock文件,都沒有擴展名。每個段包含一組文件,它們的文件擴展名不同,但是文件名均為記錄在文件segments中段的名字。讓我們看如下的結構圖3.2。
關于圖3.2中的各個文件具體的內部格式,在參考文獻3中,均可以找到詳細的說明。接下來我們從宏觀關系上說明一下這些文件組成。在這些宏觀上的關系理清楚之后,仔細閱讀參考文獻3,即可清楚的明白具體的Lucene文件格式。
每個段的文件中,主要記錄了兩大類的信息:域集合與項集合。這兩個集合中所含有的文件在圖3.2中均有表明。由于索引信息是靜態存儲的,域集合與項集合中的文件組采用了一種類似的存儲辦法:一個小型的索引文件,運行時載入內存;一個對應于索引文件的實際信息文件,可以按照索引中指示的偏移量隨機訪問;索引文件與信息文件在記錄的排列順序上存在隱式的對應關系,即索引文件中按照“索引項1、索引項2…”排列,則信息文件則也按照“信息項1、信息項2…”排列。比如在圖3.2所示文件中,segment1.fdx與segment1.fdt之間,segment1.tii與segment1.tis、 segment1.prx、segment1.frq之間,都存在這樣的組織關系。而域集合與項集合之間則通過域的在域記錄文件(比如 segment1.fnm)中所記錄的域記錄號維持對應關系,在圖3.2中segment1.fdx與segment1.tii中就是通過這種方式保持聯系。這樣,域集合和項集合不僅僅聯系起來,而且其中的文件之間也相互聯系起來。此外,標準化因子文件和被刪除文檔文件則提供了一些程序內部的輔助設施(標準化因子用在評分排序機制中,被刪除文檔是一種偽刪除手段)。這樣,整個段的索引信息就通過這些文檔有機的組成。
以上所闡述的,就是 Lucene所采用的索引文件格式。基本上而言,它是一個倒排索引,但是Lucene在文件的安排上做了一些努力,比如使用索引/信息文件的方式,從文件安排的形式上提高查找的效率。這是一種數據庫之外的處理方法,其有其優點(格式平**立、速度快),也有其缺點(獨立性帶來的共享訪問接口問題等等),具體如何衡量兩種方法之間的利弊,本文這里就不討論了。
三、一些公用的基礎類
分析完索引文件格式,我們接下來應該著手對存儲抽象也就是org.apache.lucenestore中的源碼做一些分析。我們先不著急分析這部分,而是分析圖2.1中基礎結構封裝那一部分,因為這是整個系統的基石,然后我們在下一部分再來分析存儲抽象。
基礎結構封裝,或者基礎類,由org.apache.lucene.util和org.apache.lucene.document兩個包組成,前者定義了一些常量和優化過的常用的數據結構和算法,后者則是對于文檔(document)和域(field)概念的一個類定義。以下我們用列表的方式來分析這些封裝類,指出其要點。
表 3.2 基礎類包org.apache.lucene.util
類 |
說明 |
Arrays |
一個關于數組的排序方法的靜態類,提供了優化的基于快排序的排序方法sort |
BitVector |
C/C++語言中位域的java實現品,但是加入了序列化能力 |
Constants |
常量靜態類,定義了一些常量 |
PriorityQueue |
一個優先隊列的抽象類,用于后面實現各種具體的優先隊列,提供常數時間內的最小元素訪問能力,內部實現機制是哈析表和堆排序算法 |
表 3.3 基礎類包org.apache.lucene.document
類 |
說明 |
Document |
是文檔概念的一個實現類,每個文檔包含了一個域表(fieldList),并提供了一些實用的方法,比如多種添加域的方法、返回域表的迭代器的方法 |
Field |
是域概念的一個實現類,每個域包含了一個域名和一個值,以及一些相關的屬性 |
DateField |
提供了一些輔助方法的靜態類,這些方法將java中Date和Time數據類型和String相互轉化 |
總的來說,這兩個基礎類包中含有的類都比較簡單,通過閱讀源代碼,可以很容易的理解,因此這里不作過多的展開。
四、存儲抽象
有了上面的知識,我們接下來來分析存儲抽象部分,也就是org.apache.lucene.store包。存儲抽象是唯一能夠直接對索引文件存取的包,因此其主要目的是抽象出和平臺文件系統無關的存儲抽象,提供諸如目錄服務(增、刪文件)、輸入流和輸出流。在分析其實現之前,首先我們看一下UML [22]圖。

圖 3.3 存儲抽象實現UML圖(一)

圖 3.4 存儲抽象實現UML圖(二)

圖 3.4 存儲抽象實現UML圖(三)
圖3.2到3.4展示了整個org.apache.lucene.store中主要的繼承體系。共有三個抽象類定義:Directory、 InputStream和OutputStrem,構成了一個完整的基于抽象文件系統的存取體系結構,在此基礎上,實作出了兩個實現品:(FSDirectory,FSInputStream,FSOutputStream)和(RAMDirectory,RAMInputStream和 RAMOutputStream)。前者是以實際的文件系統做為基礎實現的,后者則是建立在內存中的虛擬文件系統。前者主要用來永久的保存索引文件,后者的作用則在于索引操作時是在內存中建立小的索引,然后一次性的輸出合并到文件中去,這一點我們在后面的索引邏輯部分能夠看到。此外,還定以了 org.apache.lucene.store.lock和org.apache.lucene.store.with兩個輔助內部實現的類用在實現 Directory方法的makeLock的時候,以在鎖定索引讀寫之前來讓客戶程序做一些準備工作。
(FSDirectory, FSInputStream,FSOutputStream)的內部實現依托于java語言中的io類庫,只是簡單的做了一個外部邏輯的包裝。這當然要歸功于java語言所提供的跨平臺特性,同時也帶了一些隱患:文件存取的效率提升需要依耐于文件類庫的優化。如果需要繼續優化文件存取的效率,應該還提供一個文件與目錄的抽象,以根據各種文件系統或者文件類型來提供一個優化的機會。當然,這是應用開發者所不需要關系的問題。
(RAMDirectory,RAMInputStream和RAMOutputStream)的內部實現就比較直接了,直接采用了虛擬的文件 RAMFile類(定義于文件RAMDirectory.java中)來表示文件,目錄則看作一個String與RAMFile對應的關聯數組。 RAMFile中采用數組來表示文件的存儲空間。在此的基礎上,完成各項操作的實現,就形成了基于內存的虛擬文件系統。因為在實際使用時,并不會牽涉到很大字節數量的文件,因此這種設計是簡單直接的,也是高效率的。
這部分的實現在理清楚繼承體系后,相當的簡單。因此接下來的部分,我們可以通過直接閱讀源代碼解決。接下來我們看看這個部分的源代碼如何在實際中使用的。
一般來說,我們使用的是抽象類提供的接口而不是實際的實現類本身。在實現類中一般都含有幾個靜態函數,比如createFile,它能夠返回一個 OutputStream接口,或者openFile,它能夠返回一個InputStream接口,利用這些接口之中的方法,比如 writeString,writeByte等等,我們就能夠在抽象的層次上處理Lucene定義的數據類型的讀寫。簡單的說,Lucene中存儲抽象這部分設計時采用了工廠模式(Factory parttern)[23]。我們利用靜態類的方法也就是工廠來創建對象,返回接口,通過接口來執行操作。
五、關于cLucene項目
這一部分詳細的說明了Lucene系統中所采用的索引文件格式、一些基礎類和存儲抽象。接下來我們來敘述一下我們在項目cLucene中重新實現這些結構時候的一些考慮。
cLucene徹底的遵守了Lucene所定義的索引文件格式,這是Lucene對于各個兼容系統的基本要求。在此基礎上,cLucene系統和Lucene系統才能夠共享索引文件數據。或者說,cLucene生成的索引文件和Lucene生成的索引文件完全等價。
在基礎類問題上,cLucene同樣封裝了類似的結構。我們同樣列表描述,請和前面的表3.2與3.3對照比較。
表 3.4 基礎類包cLucene::util
類 |
說明 |
Arrays |
沒有實現,直接利用了STL庫中的快排序算法實現 |
BitVector |
C/C++語言版本的實現,與java實現版本類似 |
Constants |
常量靜態類,定義了一些常量,但是與java版本不同的是,這里主要定義了一些宏 |
PriorityQueue |
這是一個類型定義,直接利用STL庫中的std::priority_queue |
表 3.3 基礎類包cLucene::document
類 |
說明 |
Document |
C/C++語言版本的實現,與java實現版本類似 |
Field |
C/C++語言版本的實現,與java實現版本類似 |
DateField |
沒有實現,直接利用OpenTop庫中的ot::StringUtil |
存儲抽象的實現上,也同樣是類似于java實現。由于我們采用了OpenTop庫,因此同樣得以借助其中對于文件系統抽象的ot::io包來解決文件系統問題。這部分問題與前面一樣,存在優化的可能。在實現的類層次上、對外接口上,均與java版本的一樣。