Lucene入門與使用
本文主要面向具體使用,適用於已熟悉java編程的lucene初學者。
1. Lucene的簡介
1.1 Lucene 歷史
org.apache.lucene包是純java語言的全文索引檢索工具包。
Lucene的作者是資深的全文索引/檢索專家,最開始發佈在他本人的主頁上,2001年10月貢獻給APACHE,成爲APACHE基金jakarta的一個子項目。
目前,lucene廣泛用於全文索引/檢索的項目中。
lucene也被翻譯成C#版本,目前發展爲Lucene.Net(不過最近好象有流產的消息)。
1.2 Lucene 原理
lucene的檢索算法屬於索引檢索,即用空間來換取時間,對需要檢索的文件、字符流進行全文索引,在檢索的時候對索引進行快速的檢索,得到檢索位置,這個位置記錄檢索詞出現的文件路徑或者某個關鍵詞。
在使用數據庫的項目中,不使用數據庫進行檢索的原因主要是:數據庫在非精確查詢的時候使用查詢語言“like %keyword%”,對數據庫進行查詢是對所有記錄遍歷,並對字段進行“%keyword%”匹配,在數據庫的數據龐大以及某個字段存儲的數據量龐大的時候,這種遍歷是致命的,它需要對所有的記錄進行匹配查詢。因此,lucene主要適用於文檔集的全文檢索,以及海量數據庫的模糊檢索,特別是對數據庫的xml或者大數據的字符類型。
2.Lucene的下載和配置
2.1 Lucene的下載
lucene在jakarta項目中的發佈主頁:http://jakarta.apache.org/lucene/docs/index.html。以下主要針對windows用戶,其它用戶請在上面的地址中查找相關下載。
lucene的.jar包的下載(包括.jar和一個範例demo):
http://apache.oregonstate.edu/jakarta/lucene/binaries/lucene-1.4-final.zip
lucene的源代碼下載:
http://www.signal42.com/mirrors/apache/jakarta/lucene/source/lucene-1.4-final-src.zip
lucene的api地址:http://jakarta.apache.org/lucene/docs/api/index.html
本文使用lucene版本:lucene-1.4-final.jar。
2.2 lucene的配置
首先請確定你的機子已經進行了java使用環境的基本配置,即確保在某個平臺下能夠運行java源代碼,否則請查閱相關文檔進行配置。
接下來進入lucene的配置:
普通使用者:在環境變量的CLASSPATH中添加lucene的位置。比如:“D:/java /lucene-1.4-final/lucene-1.4-final.jar;”。
jbuilder使用者:在“Project”--“Project Properties”--“Required Libraries”進行添加。
Jsp使用者:也可以直接將lucene-1.4-final.jar文件放到/WEB-INF/classes下。
3. Lucene 的範例(Demo )
3.1 Demo說明
可以得到的Demo包括:lucene-demos-1.4-final、XMLIndexingDemo,lucene-demos-1.4-final中包括對普通文件和html文件的兩種索引,XMLIndexingDemo針對xml文件的索引。他們的區別主要在於:對普通文件進行索引時只要對文件的全文進行索引,而針對html、xml文件時,對標籤類型不能進行索引,在實現上:html、xml的索引需要額外的數據流分析器,以分析哪些內容有用哪些無用。因此,在後兩者實現上,索引的時間額外開支,甚至超過索引本身時間,而檢索時間沒有區別。
以上Demo中,lucene-demos-1.4-final自帶於lucene-1.4-final.zip中,XMLIndexingDemo的下載地址:
http://cvs.apache.org/viewcvs.cgi/jakarta-lucene-sandbox/contributions/XML-Indexing-Demo/
3.2 Demo的運行
首先將demo.jar的路徑添加如環境變量的CLASSPATH中,例如:“D:/java/lucene-1.4-final/lucene-demos-1.4-final.jar;”,同時確保已經添加lucene-1.4-final.jar。
然後進行文件的全文索引,在dos控制檯中,輸入命令“java org.apache.lucene.demo.IndexFiles {full-path-to-lucene}/src”,後面的路徑爲所要進行索引的文件夾,例如:“java org.apache.lucene.demo.IndexFiles c:/test”。
接着對索引進行檢索,敲入“java org.apache.lucene.demo.SearchFiles”,在提示“Query:”後輸入檢索詞,程序將進行檢索列出檢索得到的結果(檢索詞出現的文件路徑)。
其他Demo的運行請參考/docs/demo.html。
在運行Demo後請閱讀Demo的源代碼以便深入學習。
4. 利用Lucene進行索引
進行lucene的熟悉後,我們將學習如何使用Lucene。
一段索引的應用實例:
//需要捕捉IOException異常
//建立一個IndexWriter,索引保存目錄爲“index”
String[] stopStrs = {
"他奶奶的", "***"};
StandardAnalyzer analyzer = new StandardAnalyzer(stopStrs);
IndexWriter writer = new IndexWriter("index", analyzer, true);
//添加一條文檔
Document doc = new Document();
doc.add(Field.UnIndexed("id", "1"));//“id”爲字段名,“
doc.add(Field.Text("text", "***,他奶奶的,入門與使用"));
writer.addDocument(doc);
//索引完成後的處理
writer.optimize();
writer.close();
看完這段實例後,我們開始熟悉lucene的使用:
4.1 Lucene的索引接口
在學習索引的時候,首先需要熟悉幾個接口:
分析器主要工作是篩選,一段文檔進來以後,經過它,出去的時候只剩下那些有用的部分,其他則剔除。而這個分析器也可以自己根據需要而編寫。
org.apache.lucene.analysis.Analyzer:這是一個虛構類,以下兩個藉口均繼承它而來。
org.apache.lucene.analysis.SimpleAnalyzer:分析器,支持最簡單拉丁語言。
org.apache.lucene.analysis.standard.StandardAnalyzer:標準分析器,除了拉丁語言還支持亞洲語言,並在一些匹配功能上進行完善。在這個接口中還有一個很重要的構造函數:StandardAnalyzer(String[] stopWords),可以對分析器定義一些使用詞語,這不僅可以免除檢索一些無用信息,而且還可以在檢索中定義禁止的政治性、非法性的檢索關鍵詞。
IndexWriter的構造函數有三種接口,針對目錄Directory、文件File、文件路徑String三種情況。
例如IndexWriter(String path, Analyzer a, boolean create),path爲文件路徑,a爲分析器,create標誌是否重建索引(true:建立或者覆蓋已存在的索引,false:擴展已存在的索引。)
一些重要的方法:
接口名 |
備註 |
addDocument(Document doc) |
索引添加一個文檔 |
addIndexes(Directory[] dirs) |
將目錄中已存在索引添加到這個索引 |
addIndexes(IndexReader[] readers) |
將提供的索引添加到這個索引 |
optimize() |
合併索引並優化 |
close() |
關閉 |
IndexWriter爲了減少大量的io維護操作,在每得到一定量的索引後建立新的小索引文件(筆者測試索引批量的最小單位爲10),然後再定期將它們整合到一個索引文件中,因此在索引結束時必須進行wirter. optimize(),以便將所有索引合併優化。
以下介紹兩種主要的類:
a)org.apache.lucene.document.Document:
Document文檔類似數據庫中的一條記錄,可以由好幾個字段(Field)組成,並且字段可以套用不同的類型(詳細見b)。Document的幾種接口:
接口名 |
備註 |
add(Field field) |
添加一個字段(Field)到Document中 |
String get(String name) |
從文檔中獲得一個字段對應的文本 |
Field getField(String name) |
由字段名獲得字段值 |
Field[] getFields(String name) |
由字段名獲得字段值的集 |
b)org.apache.lucene.document.Field
即上文所說的“字段”,它是Document的片段section。
Field的構造函數:
Field(String name, String string, boolean store, boolean index, boolean token)。
Indexed:如果字段是Indexed的,表示這個字段是可檢索的。
Stored:如果字段是Stored的,表示這個字段的值可以從檢索結果中得到。
Tokenized:如果一個字段是Tokenized的,表示它是有經過Analyzer轉變後成爲一個tokens序列,在這個轉變過程tokenization中,Analyzer提取出需要進行索引的文本,而剔除一些冗餘的詞句(例如:a,the,they等,詳見org.apache.lucene.analysis.StopAnalyzer.ENGLISH_STOP_WORDS和org.apache.lucene.analysis.standard.StandardAnalyzer(String[] stopWords)的API)。Token是索引時候的基本單元,代表一個被索引的詞,例如一個英文單詞,或者一個漢字。因此,所有包含中文的文本都必須是Tokenized的。
Field的幾種接口:
Name |
Stored |
Indexed |
Tokenized |
use |
Keyword(String name, String value) |
Y |
Y |
N |
date,url |
Text(String name, Reader value) |
N |
Y |
Y |
short text fields: title,subject |
Text(String name, String value) |
Y |
Y |
Y |
longer text fields, like “body” |
UnIndexed(String name, String value) |
Y |
N |
N |
|
UnStored(String name, String value) |
N |
Y |
Y |
|
?
5. 利用Lucene進行檢索
5.1 一段簡單的檢索代碼
//需要捕捉IOException,ParseException異常
//處理檢索條件
Query query = QueryParser.parse("入門", "text", analyzer);
//檢索
Searcher searcher = new IndexSearcher("./index");//"index"指定索引文件位置
Hits hits = searcher.search(query);
//打印結果值集
for (int i = 0; i < hits.length(); i++) {
doc = hits.doc(i);
String id = doc.get("id");
System.out.println("found " + "入門" + " on the id:" + id);
}
5.2 利用Lucene的檢索接口
主要使用方法:
QueryParser .parse(String query, String field, Analyzer analyzer),例如:
Query query = QueryParser.parse("入門", "text", analyzer);
"入門"爲檢索詞, "text"爲檢索的字段名, analyzer爲分析器
Hits的主要使用接口:
接口名 |
備註 |
Doc(int n) |
返回第n個的文檔的所有字段 |
length() |
返回這個集中的可用個數 |
6. Lucene的其他使用
6.1 Lucene 的索引修改
下面給出一段修改索引的代碼,請根據Lucene的API解讀:
/**
* 對已有的索引添加新的一條索引
* @param idStr String:要修改的id
* @param doc Document:要修改的值
*/
public void addIndex(String idStr, String valueStr) {
StandardAnalyzer analyzer = new StandardAnalyzer();
IndexWriter writer = null;
try {
writer = new IndexWriter(indexPath, analyzer, false);
writer.mergeFactor = 2; //修正lucene
Document doc = new Document();
doc.add(Field.UnIndexed("id", idStr));//“id”爲字段名,“
doc.add(Field.Text("text", valueStr));
writer.addDocument(doc);
writer.optimize();
writer.close();
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}
/**
* 刪除索引
*
* @param idStr String
*/
public void deleteIndex(String idStr) {
try {
Directory dirt = FSDirectory.getDirectory(indexPath, false);
IndexReader reader = IndexReader.open(dirt);
IndexXML.deleteIndex(idStr, reader);
reader.close();
dirt.close();
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}
6.2 Lucene 的檢索結果排序
Lucene的排序主要是對org.apache.lucene.search.Sort的使用。Sort可以直接根據字段Field生成,也可以根據標準的SortField生成,但是作爲Sort的字段,必須符合以下的條件:唯一值以及Indexed。可以對Integers, Floats, Strings三種類型排序。
對整數型的ID檢索結果排序只要進行以下的簡單操作:
Sort sort = new Sort("id");
Hits hits = searcher.search(query, sort);
用戶還可以根據自己定義更加複雜的排序,詳細請參考API。
7 總結
Lucene給java的全文索引檢索帶來了非常強大的力量,以上僅對Lucene進行簡單的入門說明。
參考資料:
1. Overview (Lucene 1.4-final API)
2. 車東 《在應用中加入全文檢索功能--基於JAVA的全文索引引擎Lucene簡介》
3. http://www.mail-archive.com/[email protected]/index.html
Lucene原理
Lucene是一個高性能的java全文檢索工具包,它使用的是倒排文件索引結構。該結構及相應的生成算法如下:
0)設有兩篇文章1和2
文章1的內容爲:Tom lives in
文章2的內容爲:He once lived in
1)由於lucene是基於關鍵詞索引和查詢的,首先我們要取得這兩篇文章的關鍵詞,通常我們需要如下處理措施
a.我們現在有的是文章內容,即一個字符串,我們先要找出字符串中的所有單詞,即分詞。英文單詞由於用空格分隔,比較好處理。中文單詞間是連在一起的需要特殊的分詞處理。
b.文章中的”in”, “once” “too”等詞沒有什麼實際意義,中文中的“的”“是”等字通常也無具體含義,這些不代表概念的詞可以過濾掉
c.用戶通常希望查“He”時能把含“he”,“HE”的文章也找出來,所以所有單詞需要統一大小寫。
d.用戶通常希望查“live”時能把含“lives”,“lived”的文章也找出來,所以需要把“lives”,“lived”還原成“live”
e.文章中的標點符號通常不表示某種概念,也可以過濾掉
在lucene中以上措施由Analyzer類完成
經過上面處理後
文章1的所有關鍵詞爲:[tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2的所有關鍵詞爲:[he] [live] [shanghai]
2) 有了關鍵詞後,我們就可以建立倒排索引了。上面的對應關係是:“文章號”對“文章中所有關鍵詞”。倒排索引把這個關係倒過來,變成:“關鍵詞”對“擁有該關鍵詞的所有文章號”。文章1,2經過倒排後變成
關鍵詞 文章號
guangzhou 1
he 2
i 1
live 1,2
shanghai 2
tom 1
通常僅知道關鍵詞在哪些文章中出現還不夠,我們還需要知道關鍵詞在文章中出現次數和出現的位置,通常有兩種位置:a)字符位置,即記錄該詞是文章中第幾個字符(優點是關鍵詞亮顯時定位快);b)關鍵詞位置,即記錄該詞是文章中第幾個關鍵詞(優點是節約索引空間、詞組(phase)查詢快),lucene中記錄的就是這種位置。
加上“出現頻率”和“出現位置”信息後,我們的索引結構變爲:
關鍵詞 文章號[出現頻率] 出現位置
guangzhou 1[2] 3,6
he 2[1] 1
i 1[1] 4
live 1[2],2[1] 2,5,2
shanghai 2[1] 3
tom 1[1] 1
以live 這行爲例我們說明一下該結構:live在文章1中出現了2次,文章2中出現了一次,它的出現位置爲“2,5,
以上就是lucene索引結構中最核心的部分。我們注意到關鍵字是按字符順序排列的(lucene沒有使用B樹結構),因此lucene可以用二元搜索算法快速定位關鍵詞。
實現時 lucene將上面三列分別作爲詞典文件(Term Dictionary)、頻率文件(frequencies)、位置文件 (positions)保存。其中詞典文件不僅保存有每個關鍵詞,還保留了指向頻率文件和位置文件的指針,通過指針可以找到該關鍵字的頻率信息和位置信息。
Lucene中使用了field的概念,用於表達信息所在位置(如標題中,文章中,url中),在建索引中,該field信息也記錄在詞典文件中,每個關鍵詞都有一個field信息(因爲每個關鍵字一定屬於一個或多個field)。
爲了減小索引文件的大小,Lucene對索引還使用了壓縮技術。首先,對詞典文件中的關鍵詞進行了壓縮,關鍵詞壓縮爲<前綴長度,後綴>,例如:當前詞爲“阿拉伯語”,上一個詞爲“阿拉伯”,那麼“阿拉伯語”壓縮爲<3,語>。其次大量用到的是對數字的壓縮,數字只保存與上一個值的差值(這樣可以減小數字的長度,進而減少保存該數字需要的字節數)。例如當前文章號是16389(不壓縮要用3個字節保存),上一文章號是16382,壓縮後保存7(只用一個字節)。
下面我們可以通過對該索引的查詢來解釋一下爲什麼要建立索引。
假設要查詢單詞 “live”,lucene先對詞典二元查找、找到該詞,通過指向頻率文件的指針讀出所有文章號,然後返回結果。詞典通常非常小,因而,整個過程的時間是毫秒級的。
而用普通的順序匹配算法,不建索引,而是對所有文章的內容進行字符串匹配,這個過程將會相當緩慢,當文章數目很大時,時間往往是無法忍受的。
Lucene講座
5. IndexReader類與IndexWirter類··· 23
第一節 全文檢索系統與Lucene簡介
一、 什麼是全文檢索與全文檢索系統?
全文檢索是指計算機索引程序通過掃描文章中的每一個詞,對每一個詞建立一個索引,指明該詞在文章中出現的次數和位置,當用戶查詢時,檢索程序就根據事先建立的索引進行查找,並將查找的結果反饋給用戶的檢索方式。這個過程類似於通過字典中的檢索字表查字的過程。
全文檢索的方法主要分爲按字檢索和按詞檢索兩種。按字檢索是指對於文章中的每一個字都建立索引,檢索時將詞分解爲字的組合。對於各種不同的語言而言,字有不同的含義,比如英文中字與詞實際上是合一的,而中文中字與詞有很大分別。按詞檢索指對文章中的詞,即語義單位建立索引,檢索時按詞檢索,並且可以處理同義項等。英文等西方文字由於按照空白切分詞,因此實現上與按字處理類似,添加同義處理也很容易。中文等東方文字則需要切分字詞,以達到按詞索引的目的,關於這方面的問題,是當前全文檢索技術尤其是中文全文檢索技術中的難點,在此不做詳述。
全文檢索系統是按照全文檢索理論建立起來的用於提供全文檢索服務的軟件系統。一般來說,全文檢索需要具備建立索引和提供查詢的基本功能,此外現代的全文檢索系統還需要具有方便的用戶接口、面向WWW[1]的開發接口、二次應用開發接口等等。功能上,全文檢索系統核心具有建立索引、處理查詢返回結果集、增加索引、優化索引結構等等功能,外圍則由各種不同應用具有的功能組成。結構上,全文檢索系統核心具有索引引擎、查詢引擎、文本分析引擎、對外接口等等,加上各種外圍應用系統等等共同構成了全文檢索系統。圖1.1展示了上述全文檢索系統的結構與功能。
在上圖中,我們看到:全文檢索系統中最爲關鍵的部分是全文檢索引擎,各種應用程序都需要建立在這個引擎之上。一個全文檢索應用的優異程度,根本上由全文檢索引擎來決定。因此提升全文檢索引擎的效率即是我們提升全文檢索應用的根本。另一個方面,一個優異的全文檢索引擎,在做到效率優化的同時,還需要具有開放的體系結構,以方便程序員對整個系統進行優化改造,或者是添加原有系統沒有的功能。比如在當今多語言處理的環境下,有時需要給全文檢索系統添加處理某種語言或者文本格式的功能,比如在英文系統中添加中文處理功能,在純文本系統中添加XML[2]或者HTML[3]格式的文本處理功能,系統的開放性和擴充性就十分的重要。
二、 什麼是Lucene?
Lucene是apache軟件基金會[4] jakarta項目組的一個子項目,是一個開放源代碼[5]的全文檢索引擎工具包,即它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎,部分文本分析引擎(英文與德文兩種西方語言)。Lucene的目的是爲軟件開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能,或者是以此爲基礎建立起完整的全文檢索引擎。
Lucene的原作者是Doug Cutting,他是一位資深全文索引/檢索專家,曾經是V-Twin搜索引擎[6]的主要開發者,後在Excite[7]擔任高級系統架構設計師,目前從事於一些Internet底層架構的研究。早先發布在作者自己的http://www.lucene.com/,後來發佈在SourceForge[8],2001年年底成爲apache軟件基金會jakarta的一個子項目:http://jakarta.apache.org/lucene/。
三、 Lucene的應用、特點及優勢
作爲一個開放源代碼項目,Lucene從問世之後,引發了開放源代碼社羣的巨大反響,程序員們不僅使用它構建具體的全文檢索應用,而且將之集成到各種系統軟件中去,以及構建Web應用,甚至某些商業軟件也採用了Lucene作爲其內部全文檢索子系統的核心。apache軟件基金會的網站使用了Lucene作爲全文檢索的引擎,IBM的開源軟件eclipse[9]的2.1版本中也採用了Lucene作爲幫助子系統的全文索引引擎,相應的IBM的商業軟件Web Sphere[10]中也採用了Lucene。Lucene以其開放源代碼的特性、優異的索引結構、良好的系統架構獲得了越來越多的應用。
Lucene作爲一個全文檢索引擎,其具有如下突出的優點:
(1)索引文件格式獨立於應用平臺。Lucene定義了一套以8位字節爲基礎的索引文件格式,使得兼容系統或者不同平臺的應用能夠共享建立的索引文件。
(2)在傳統全文檢索引擎的倒排索引的基礎上,實現了分塊索引,能夠針對新的文件建立小文件索引,提升索引速度。然後通過與原有索引的合併,達到優化的目的。
(3)優秀的面向對象的系統架構,使得對於Lucene擴展的學習難度降低,方便擴充新功能。
(4)設計了獨立於語言和文件格式的文本分析接口,索引器通過接受Token流完成索引文件的創立,用戶擴展新的語言和文件格式,只需要實現文本分析的接口。
(5)已經默認實現了一套強大的查詢引擎,用戶無需自己編寫代碼即使系統可獲得強大的查詢能力,Lucene的查詢實現中默認實現了布爾操作、模糊查詢(Fuzzy Search[11])、分組查詢等等。
面對已經存在的商業全文檢索引擎,Lucene也具有相當的優勢。首先,它的開發源代碼發行方式(遵守Apache Software License[12]),在此基礎上程序員不僅僅可以充分的利用Lucene所提供的強大功能,而且可以深入細緻的學習到全文檢索引擎製作技術和麪相對象編程的實踐,進而在此基礎上根據應用的實際情況編寫出更好的更適合當前應用的全文檢索引擎。在這一點上,商業軟件的靈活性遠遠不及Lucene。其次,Lucene秉承了開放源代碼一貫的架構優良的優勢,設計了一個合理而極具擴充能力的面向對象架構,程序員可以在Lucene的基礎上擴充各種功能,比如擴充中文處理能力,從文本擴充到HTML、PDF[13]等等文本格式的處理,編寫這些擴展的功能不僅僅不復雜,而且由於Lucene恰當合理的對系統設備做了程序上的抽象,擴展的功能也能輕易的達到跨平臺的能力。最後,轉移到apache軟件基金會後,藉助於apache軟件基金會的網絡平臺,程序員可以方便的和開發者、其它程序員交流,促成資源的共享,甚至直接獲得已經編寫完備的擴充功能。最後,雖然Lucene使用Java語言寫成,但是開放源代碼社區的程序員正在不懈的將之使用各種傳統語言實現(例如.net framework[14]),在遵守Lucene索引文件格式的基礎上,使得Lucene能夠運行在各種各樣的平臺上,系統管理員可以根據當前的平臺適合的語言來合理的選擇。
四、 本文的重點問題與cLucene項目
作爲中國人民大學信息學院99級本科生的一個畢業設計項目,我們對Lucene進行了深入的研究,包括系統的結構,索引文件結構,各個部分的實現等等。並且我們啓動了cLucene項目,做爲一個Lucene的C++語言的重新實現,以期望帶來更快的速度和更加廣泛的應用範圍。我們先分析了系統結構,文件結構,然後在研究各個部分的具體實現的同時開始進行的cLucene實現。限於時間的限制,到本文完成爲止,cLucene項目並沒有完成,對於Lucene的具體實現部分也僅僅完成到了索引引擎部分。
接下來的部分,本文將對Lucene的系統結構、文件結構、索引引擎部分做一個徹底的分析。以期望提供對Lucene全文檢索引擎的系統架構和部分程序實現的清晰的瞭解。cLucene項目則作爲一個開放源代碼的項目,繼續進行的開發。
有關cLucene項目的一些信息:
n 開發語言:ISO C++[15],STLport
n 目標平臺:Win32,POSIX
n 授權協議:GNU General Public License (GPL)[18]
第二節 Lucene系統結構分析
一、 系統結構組織
Lucene作爲一個優秀的全文檢索引擎,其系統結構具有強烈的面向對象特徵。首先是定義了一個與平臺無關的索引文件格式,其次通過抽象將系統的核心組成部分設計爲抽象類,具體的平臺實現部分設計爲抽象類的實現,此外與具體平臺相關的部分比如文件存儲也封裝爲類,經過層層的面向對象式的處理,最終達成了一個低耦合高效率,容易二次開發的檢索引擎系統。
以下將討論Lucene系統的結構組織,並給出系統結構與源碼組織圖:
從圖中我們清楚的看到,Lucene的系統由基礎結構封裝、索引核心、對外接口三大部分組成。其中直接操作索引文件的索引核心又是系統的重點。Lucene的將所有源碼分爲了7個模塊(在java語言中以包即package來表示),各個模塊所屬的系統部分也如上圖所示。需要說明的是org.apache.lucene.queryPaser是做爲org.apache.lucene.search的語法解析器存在,不被系統之外實際調用,因此這裏沒有當作對外接口看待,而是將之獨立出來。
從面象對象的觀點來考察,Lucene應用了最基本的一條程序設計準則:引入額外的抽象層以降低耦合性。首先,引入對索引文件的操作org.apache.lucene.store的封裝,然後將索引部分的實現建立在(org.apache.lucene.index)其之上,完成對索引核心的抽象。在索引核心的基礎上開始設計對外的接口org.apache.lucene.search與org.apache.lucene.analysis。在每一個局部細節上,比如某些常用的數據結構與算法上,Lucene也充分的應用了這一條準則。在高度的面向對象理論的支撐下,使得Lucene的實現容易理解,易於擴展。
Lucene在系統結構上的另一個特點表現爲其引入了傳統的客戶端服務器結構以外的的應用結構。Lucene可以作爲一個運行庫被包含進入應用本身中去,而不是做爲一個單獨的索引服務器存在。這自然和Lucene開放源代碼的特徵分不開,但是也體現了Lucene在編寫上的本來意圖:提供一個全文索引引擎的架構,而不是實現。
二、 數據流分析
理解Lucene系統結構的另一個方式是去探討其中數據流的走向,並以此摸清楚Lucene系統內部的調用時序。在此基礎上,我們能夠更加深入的理解Lucene的系統結構組織,以方便以後在Lucene系統上的開發工作。這部分的分析,是深入Lucene系統的鑰匙,也是進行重寫的基礎。
我們來看看在Lucene系統中的主要的數據流以及它們之間的關係圖:
|
|
|
|
|
圖2.2很好的表明了Lucene在內部的數據流組織情況,並且沿着數據流的方向我們也可以對與Lucene內部的執行時序有一個清楚的瞭解。現在將圖中的涉及到的流的類型與各個邏輯對應系統的相關部分的關係說明一下。
圖中共存在4種數據流,分別是文本流、token流、字節流與查詢語句對象流。文本流表示了對於索引目標和交互控制的抽象,即用文本流表示了將要索引的文件,用文本流向用戶輸出信息;在實際的實現中,Lucene中的文本流採用了UCS-2[19]作爲編碼,以達到適應多種語言文字的處理的目的。Token流是Lucene內部所使用的概念,是對傳統文字中的詞的概念的抽象,也是Lucene在建立索引時直接處理的最小單位;簡單的講Token就是一個詞和所在域值的組合,後面在敘述文件格式時也將繼續涉及到token,這裏不詳細展開。字節流則是對文件抽象的直接操作的體現,通過固定長度的字節(Lucene定義爲8比特位長,後面文件格式將詳細敘述)流的處理,將文件操作解脫出來,也做到了與平臺文件系統的無關性。查詢語句對象流則是僅僅在查詢語句解析時用到的概念,它對查詢語句抽象,通過類的繼承結構反映查詢語句的結構,將之傳送到查找邏輯來進行查找的操作。
圖中的涉及到了多種邏輯,基本上直接對應於系統某一模塊,但是也有跨模塊調用的問題發生,這是因爲Lucene的重用程度非常好,因此很多實現直接調用了以前的工作成果,這在某種程度上其實是加強了模塊耦合性,但是也是爲了避免系統的過於龐大和不必要的重複設計的一種折衷體現。詞法分析邏輯對應於org.apache.lucene.analysis部分。查詢語句語法分析邏輯對應於org.apache.lucene.queryParser部分,並且調用了org.apache.lucene.analysis的代碼。查詢結束之後向評分排序邏輯輸出token流,繼而由評分排序邏輯處理之後給出文本流的結果,這一部分的實現也包含在了org.apache.lucene.search中。索引構建邏輯對應於org.apache.lucene.index部分。索引查找邏輯則主要是org.apache.lucene.search,但是也大量的使用了org.apache.lucene.index部分的代碼和接口定義。存儲抽象對應於org.apache.lucene.store。沒有提到的模塊則是做爲系統公共基礎設施存在。
三、 基於Lucene的應用開發
通過以上的系統結構分析和數據流分析,我們已經很清楚的瞭解了Lucene的系統的結構特徵。在此基礎上,我們可以通過擴充Lucene系統來完成一個完備的全文檢索引擎,緊接着還可以在全文檢索引擎的基礎上構建各種應用系統。鑑於本文的目的並不在此,以下我們只是略爲敘述一下相關的步驟,從而給出應用開發的一些思路。
首先,我們需要的是按照目標語言的詞法結構來構建相應的詞法分析邏輯,實現Lucene在org.apache.lucene.analysis中定義的接口,爲Lucene提供目標系統所使用的語言處理能力。Lucene默認的已經實現了英文和德文的簡單詞法分析邏輯(按照空格分詞,並去除常用的語法詞,如英語中的is,am,are等等)。在這裏,主要需要參考實現的接口在org.apache.lucene.analysis中的Analyzer.java和Tokenizer.java中定義,Lucene提供了很多英文規範的實現樣本,也可以做爲實現時候的參考資料。其次,需要按照被索引的文件的格式來提供相應的文本分析邏輯,這裏是指除開詞法分析之外的部分,比如HTML文件,通常需要把其中的內容按照所屬於域分門別類加入索引,這就需要從org.apache.lucene.document中定義的類document繼承,定義自己的HTMLDocument類,然後就可以將之交給org.apache.lucene.index模塊來寫入索引文件。完成了這兩步之後,Lucene全文檢索引擎就基本上完備了。這個過程可以用下圖表示:
當然,上面所示的僅僅只是對於Lucene的基本擴充過程,它將Lucene由不完備的變成完備的(尤其是對於非英語的語言檢索)。除此之外我們還可以在很多方面對Lucene進行改造。第一個方面即爲按照文檔索引的域,比如標題,作者之類的信息對返回的查詢結果排序,這即需要改造Lucene的評分排序邏輯。默認的,Lucene採用其內部的相關性方法來處理評分和排序,我們可以根據需要改變它。遺憾的是,這部分Lucene並沒有做到如同擴充詞法解析和文檔類型那樣的條理清晰,沒有留下很好的接口,因此需要仔細的分析其源代碼的實現,自行擴充等等。其他的方面,比如改進其索引的效率,改進其返回結果時候的緩衝機制等等,都是加強Lucene系統的方面,在此也不再敘述。
完成了Lucene系統,之後就可以開始考慮其上的應用系統開發。如果應用系統也使用java語言開發,那麼Lucene系統能夠方便的嵌入到整個系統中去,作爲一個API集來調用。這個過程十分簡單,以下便是一個示例程序,配合註釋理解起來很容易。
|
或者,Lucene全文檢索引擎也可作爲服務器程序啓動,但是這就需要用戶自行擴充其他應用與Lucene的接口。這個可以通過傳統的包裝方式,比如客戶服務器結構,或者採用現在流行的Web方式。諸如此類的應用方案,本文也不再繼續敘述。參考Lucene的項目網站中的用戶郵件列表能找到更多的信息。
第三節 Lucene索引文件格式分析
一、 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字節 |
動態長度整數,每字節的最高位表明還剩多少字節,每字節的低七位表明整數的值,高位優先。可以認爲值可以爲無限大。其示例如下
|
||||||||||||||||||||||||||||||||||||||||||||
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版本的一樣。
第四節 Lucene索引構建邏輯模塊分析
一、 緒論
這一個部分,我們將分析Lucene中的索引構建邏輯模塊。它與前面介紹的存儲抽象一起構成了Lucene的索引核心部分。無論是對外接口中的查詢,還是分析各種文本以進一步生成索引,都需要直接調用這部分來獲得對索引文件的訪問能力,因此,這部分在系統中至關重要。構建一個高效的、易使用的索引構建邏輯,即是Lucene在這一部分需要達到的目的。
從面向對象的經典思考方式出發來看,我們只需要使用繼承體系來表達圖3.1中的各個概念,就可以通過這個繼承體系來控制索引文件的結構,然後設計合適的永久化方法,以及接受分析token流的操作,即可將索引構建邏輯完成。原理上就是這樣的簡單。由於兩個關鍵的概念document和field都已經在org.apache.lucene.document中當作基礎類定義過了,因此實際上Lucene在這部分需要完善的概念結構還有segment和term。在此基礎上繼續編寫各個邏輯結構的永久化方法,然後提供一個進入的接口方法,即是宣告完成了這個過程。其中永久化的部分,Lucene使用了另外實現一個代理類的方式來實現,即對於某個類X,存在XWriter類和XReader類來負責寫出和讀入的功能;用作永久化功能的類是被永久化的類的友元。
在接下來的分析過程中,我們按照這樣一個思路,以UML圖和對象體系的描述來敘述這部分的設計和實現,然後通過內部的數據流理清楚調用時序。
二、 對象體系與UML圖
1. 項(Term)
這部分主要是分析針對項(Term)這個概念所做的設計,包括概念所實際涉及的類、永久化類。首先,我們從圖3.2和閱讀參考文獻3知道,項(Term)所表示的是一個字符串,它擁有域、頻數和位置信息等等屬性。因此,Lucene中設計了兩個類來表示這個概念,如下圖
圖 4.1 UML圖(-)
上圖中,有意的突出了類Term和TermInfo中的數據成員,因爲它反映了對於項(Term)這個概念的具體表示。同時上圖中也同時列出了用於永久化項(Term)的代理類TermInfosWriter和TermInfosReader,它們完成永久化的功能,需要注意的是,TermInfosReader內部使用了數組indexTerms和indexInfos來存儲一系列項;而TermInfosWriter則是一個類似於鏈表的結構,通過一個other指向下一個TermInfosWriter,每一個TermInfosWriter只負責本身那個lastTerm和lastTi的永久化工作。這是一個設計上的技巧,通過批量讀取(或者稱爲緩衝的方式)來獲得讀入時候的效率優化;而通過一個鏈表式的、各負其責的方式,來獲得寫出時候的設計簡化。
項(term)這部分的設計中,還有一些重要的接口和類,我們先介紹如下,同樣我們也先展示UML圖
圖 4.2 UML圖(二)
圖4.2中,我們看到三個類:TermEnum、TermDocs與TermPositions,第一個是抽象類,後兩個都是接口。TermEnum的設計主要用在後面Segment和Document等等的實現中,以提供枚舉其中每一個項(Term)的能力。TermDocs是一個接口,用來繼承以提供返回<document, frequency>值對的能力,通過這個接口就可以獲得某個項(Term)在某個文檔中出現的頻數。TermPositions則是在TermDocs上的擴展,將項(Term)在文檔中的位置信息也表示出來。TermDocs(TermPositions)接口的使用方式類似於java中的Enumration接口,即通過next方法跳轉,通過doc,freq等方法獲得當前的屬性值。
2. 域(Field)
由於Field的基本概念在org.apache.lucene.document中已經做了定義,因此在這部分主要是針對項文件(.fnm文件、.fdx文件、.fdt文件)所需要的信息再來設計一些類。
圖 4.3 UML圖(三)
圖 4.3中展示的,就是表示與域(Field)所關聯的屬性信息的類。其中isIndexed表示的這個域的值是否被索引過,即值是否被分詞然後索引;另外兩個屬性所表示的意思則很明顯:一個是域的名字,一個是域的編號。
接下來我們來看關於域表和存取邏輯的UML圖。
圖 4.4 UML圖(四)
FieldInfos即爲域表的概念表示,內部採用了冗餘的方式以獲取在通過域的編號訪問或者通過域的名字來訪問時候的高效率。FieldsReader與FieldsWriter則分別是寫出和讀入的代理類。在功能和實現上,這兩個類都比較簡單。至於FieldInfos中採用的冗餘方式,則是基於域的數目相對比較少而做出的一種折衷處理。
3. 文檔(document)
文檔(document)同樣也是在org.apache.lucene.document中定義過的結構。由於對於這部分比較重要,我們也來看看其UML圖。
圖 4.5 UML圖(五)
在圖4.5中我們看到,Document的設計基本上沿用了鏈表的處理方法。左邊的Document類作爲一個數據外包類,用來提供對於內部結構DocumentFieldList的增加刪除訪問操作等等。DocumentFieldList纔是實際上的數據存儲單位,它用了鏈表的處理方法,直接指向一個當前的Field對象和下一個DocumentFieldList對象,這個與前面的類似。爲了能夠逐個訪問鏈表中的節點,還設計了DocumentFieldEnumeration枚舉類。
圖 4.6 UML圖(六)
實際上定義於org.apache.lucene.index中的有關於Document的就是永久化的代理類。在圖4.6中給出了其UML圖。需要說明的是爲什麼沒有出現讀入的方法:這個方法已經隱含在圖4.5中Document類中的add方法中了,結合圖2.4中的程序代碼段,我們就能夠清楚的理解這種設計。
4. 段(segment)
段(Segment)這一部分設計的比較特殊,在實現簡單的對象結構之上,還特意的設計了用於段之間合併的類。接下來,我們仍然採取對照UML分析的方式逐個敘述。接下來我們看Lucene中如何表示段這個概念。
圖 4.7 UML圖(七)
Lucene定義了一個類SegmentInfo用來表示每一個段(Segment)的信息,包括名字(name)、含有的文檔的數目(docCount)和段所位於的目錄的位置(dir)。根據索引文件中的段的意義,有了這三點,就能唯一確定一個段了。SegmentInfos這個類則是用來表示一個段的鏈表(從標準的java.util.Vector繼承而來),實際上,也就是索引(index)的意思了。需要注意的是,這裏並沒有在SegmentInfo中安插一個文檔(document)的鏈表。這樣做的原因牽涉到Lucene內部對於文檔(相當於一個被索引文件)的處理;Lucene內部採用了賦予文檔編號,給域賦值的方式來處理文檔,即加入的文檔順次編號,以後用文檔號表示文檔,而路徑信息,文件名字等等在以後索引查找需要的屬性,都作爲域存儲下來;因此SegmentInfo中並沒有另外存儲一個文檔(document)的鏈表,對於這些的寫出和讀入,則交給了永久化的代理類來做。
圖 4.8 UML圖(八)
圖4.8給出了負責段(segment)的讀入操作的代理類,而負責段(segment)的寫出操作也同樣沒有定義,這些操作都直接實現在了類IndexWriter類中(後面會詳細分析)。段的操作同樣採用了之前的數組或者說是緩衝的處理方式,相關的細節也不在這裏詳細敘述了。
然後,針對前面項(term)那部分定義的幾個接口,段(segment)這部分也需要做相應的接口實現,因爲提供直接遍歷訪問段中的各個項的能力對於檢索來說,無疑是十分重要的。即這部分的設計,實際上都是在爲了檢索在服務。
圖 4.9 UML圖(九)
圖 4.10 UML圖(十)
圖4.9和圖4.10分別展示了前面項(term)那裏定義的接口是如何在這裏通過繼承實現的。Lucene在處理這部分的時候,也是分成兩部分(Segment與Segments開頭的類)來實現,而且很合理的運用了數組的技法,以及注意了繼承重用。但是細化到局部,終歸是比較簡單的按照語義來獲得結果而已了,因此關於更多的也就不多做分析了,我們完全可以通過閱讀源代碼來解決。
接下來所介紹的,就是在Lucene的設計過程中比較特殊的一個部分:段合併類(SegmentMerger)。這首先需要介紹Lucene中的建立索引時的段合併策略。
Lucene爲了兼顧建立索引時的效率和讀取索引查找的速度,引入了分小段建立索引的方式,即每一次批量建立索引時,先在內存中的虛擬文件系統中爲每一個文檔單獨建立一個段,然後在輸出的時候將這些段合併之後輸出成爲索引文件,這時僅僅存在一個段。多次建立的索引後,如果想優化索引文件,也可採取合併段的方法,將索引中的段合併成爲一個段。我們來看一下在IndexWriter類中相應的方法的實現,來了解一下這中建立索引的實現。
對於上面的代碼,我們不做過多註釋了,結合源碼中的註解應該很容易理解。在最後那個mergeSegments函數中,將用到幾個重要的類結構,它們記錄了合併時候的一些重要信息,完成合並時候的工作。接下來,我們來看這幾個類的UML圖。
圖 4.12 UML圖(十一)
從圖4.12中,我們看到Lucene設計一個類SegmentMergeInfo用來保存每一個被合併的段的信息,也保存能夠訪問其內部的接口句柄,也就是說合並時的操作使用這個類作爲對被合併的段的操作代理。類SegmentMergeQueue則設計爲org.apache.lucene.util.PriorityQueue的子類,做爲SegmentMergeInfo的容器類,而且附帶能夠自動排序。SegmentMerger是主要進行操作的類,裏面各個方法環環相扣,分別完成合並各個數據項的問題。
5. IndexReader類與IndexWirter類
最後剩下的,就是整個索引邏輯部分的使用接口類了。外界通過這兩個類以及文檔(document)類的構造函數調用之,比如圖2.4中的代碼示例所示。下面我們來看一下這部分最後兩個類的UML圖。
圖 4.13 UML圖(十二)
IndexWriter的設計與IndexReader的設計很不相同,前者是一個實現類,而後者是一個抽象類,帶有沒有實現的接口。IndexWriter的主要作用就是接收新加入的文檔(document),然後在內部爲之生成相應的小段,最後再合併並向索引文件中輸出,圖4.11中已經給出了一些實現的代碼。由於Lucene在面向對象上封裝的努力,通過各個構造函數就已經完成了對於各個概念的構造過程,剩下部分的代碼主要是依據各個數組或者是鏈表中的信息,逐個逐個的將信息寫出到相應的文件中去了。IndexReader部分則只是做了接口設計,沒有具體的實現,這個和本部分所完成的主要功能有關:索引構建邏輯。設計這個抽象類的目的是,預先完成一些函數,爲以後的檢索(search)部分的各種形式的IndexReader鋪平道路,也是利用了在同一個包內可以方便訪問其它類的保護變量這個java語言的限制。
到此,在索引構建邏輯部分出現的類我們就分析完畢了,需要說明主要是做的一個宏觀上的組成結構上的分析,並指出一些實現上的要點。具體的實現,由於Lucene的開放源碼而顯得並不是非常的重要,因爲Lucene在做到良好的面相對象設計之後,實際帶來的是局部複雜性的減小,因此某一些單獨的函數或者實現就比較容易編寫,也容易讓人閱讀。本文不再繼續敘述這方面的細節,作爲一個總結,下一個部分我們通過索引構建邏輯的數據流圖的方式,再來理清楚一下索引構建邏輯這部分的調用時序。
三、 數據流邏輯
從宏觀上明白一個系統的設計,理清楚其中的運行規律,最好的方式應該是通過數據流圖。在分析了各個位於索引構建邏輯部分的類的設計之後,我們接下來就通過分析數據流圖的方式來總結一下。但是由於之前提到的原因:索引讀入部分在這一部分並沒有完全實現,所以我們在數據流圖中主要給出的是索引構建的數據流圖。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
對於圖4.14中所描述的內容,結合Lucene源代碼中的一些文件看,能夠加深理解。準備階段可以參考demo文件夾中的org.apache.lucene.demo.IndexFiles類和java文件夾中的org.apache.lucene.document文件包。索引構建階段的主要源碼位於java文件夾中org.apache.lucene.index.IndexWriter類,因此這部分可以結合這個類的實現來看。至於內存文件系統,比較複雜,但是這時的邏輯相對簡單,因此也不難理解。
上面的數據流圖十分清楚的勾畫除了整個索引構建邏輯這部分的設計:通過層層嵌套的類結構,在構建時候即分步驟有計劃的生成了索引結構,將之存儲到內存中的文件系統中,然後通過對內存中的文件系統優化合並輸出到實際的文件系統中。
四、 關於cLucene項目
前面的三個部分,已經完成了分析索引構建邏輯的任務,這裏我們還是有針對性的談談我們這次的畢業設計項目cLucene在這一部分的情況。
在實現這部分的時候,爲了將一些java語法中比較特殊的部分,比如內隱類、同步函數、同步對象等等,我們不得不採用了一些比較晦澀和艱深的C++語法,在OpenTop這個類庫所提供的類似於java語言的設施上來實現。這個尤其體現在實現Segment相關類時,爲了處理原來java源代碼中用內隱類實現的Lock文件創建機制的時候,我們不得不定義了大量的cLucene::store::With的子類,併爲之傳入調用類的指針,設置它爲調用類的友元,才得以精確的模擬了原有的語義。陷於我們這次的重寫以移植爲主,系統結構基本上沒有大的變化,不得不產生這種重複而且大量的工作。如果需要改進這中狀況,我們應該考慮按照C++語言的特點來設計索引構建部分的類庫繼承結構,但是很可惜在本文成文之前,時間不允許我們這樣做。
來自java語法的特殊性只是我們解決問題的一個方面,我們還需要處理引用的調用方式。由於java語言擁有了垃圾收集機制,因此得以將一切的參數形式看作爲引用,而不考慮其分配與消亡的問題。C++語言並不具備這種機制,它需要程序員自行管理分配空間與銷燬對象的問題。在這裏,我們使用的是來自OpenTop中所引入的計數指針RefPtr<>模板,它能夠模擬指針的語義,並且計算指針被引用的次數,在引用次數爲0時就自動釋放資源:這是一種類似於java語言中引用的方式,不過它顯得更加高效率。我們在cLucene的實現中大量的使用了計數指針模板。
除此之外,我們沒有改變Lucene所定義的索引構建邏輯的結構和語義,我們實現的是一個完全和java版本Lucene兼容的版本。
Lucene的系統結構
本文主要討論Lucene的系統結構,希望對其結構的初步分析,更深入的瞭解Lucene的運作機制,從而實現對Lucene的功能擴展。
1. Lucene的包結構
如上圖所示,Lucene源碼中共包括7個子包,每個包完成特定的功能:
Lucene包結構功能表 |
|
包名 |
功能 |
org.apache.lucene.analysis |
語言分析器,主要用於的切詞,支持中文主要是擴展此類 |
org.apache.lucene.document |
索引存儲時的文檔結構管理,類似於關係型數據庫的表結構 |
org.apache.lucene.index |
索引管理,包括索引建立、刪除等 |
org.apache.lucene.queryParser |
查詢分析器,實現查詢關鍵詞間的運算,如與、或、非等 |
org.apache.lucene.search |
檢索管理,根據查詢條件,檢索得到結果 |
org.apache.lucene.store |
數據存儲管理,主要包括一些底層的I/O操作 |
org.apache.lucene.util |
一些公用類 |
1. 2. Lucene的主要邏輯圖
Lucene功能強大,但從根本上說,主要包括兩塊:一是文本內容經切詞後索引入庫;二是根據查詢條件返回結果。
以下是上述兩大功能的邏輯圖:
STORAGE (存儲器) ACCESS INDEX (訪問索引) SERACHER (查詢器) ANALYZER (語言分析器) QUERY PARSER (查詢分析器) DOCUMENT (文檔結構) SEARCHER (查詢) INDEXER (入庫) FS BDD RAM Lucene功能邏輯圖
查詢邏輯
按先後順序,查詢邏輯可分爲如下幾步:
1. 1. 查詢者輸入查詢條件
條件之間可以通過特定運算符進行運算,比如查詢希望查詢到與“中國”和“北京”相關的記錄,但不希望結果中包括“海淀區中關村”,於是輸入條件爲“中國+北京-海淀區中關村”;
2. 2. 查詢條件被傳達到查詢分析器中,分析器將將對“中國+北京-海淀區中關村”進行分析,首先分析器解析字符串的連接符,即這裏的加號和減號,然後對每個詞進行切詞,一般最小的詞元是兩個漢字,則中國和北京兩個詞不必再切分,但對海淀區中關村需要切分,假設根據切詞算法,把該詞切分爲“海淀區”和“中關村”兩部分,則最後得到的查詢條件可以表示爲:“中國” AND “北京” AND NOT(“海淀區” AND “中關村”)。
3. 3. 查詢器根據這個條件遍歷索引樹,得到查詢結果,並返回結果集,返回的結果集類似於JDBC中的ResultSet。
4. 4. 將返回的結果集顯示在查詢結果頁面,當點擊某一條內容時,可以鏈接到原始網頁,也可以打開全文檢索庫中存儲的網頁內容。
這就是查詢的邏輯過程,需要說明的是,Lucene默認只支持英文,爲了便於說明問題,以上查詢過程採用中文舉例,事實上,當Lucene被擴充支持中文後就是這麼一個查詢過程。
入庫邏輯
入庫將把內容加載到全文檢索庫中,按順序,入庫邏輯包括如下過程:
1. 1. 入庫者定義到庫中文檔的結構,比如需要把網站內容加載到全文檢索庫,讓用戶通過“站內檢索”搜索到相關的網頁內容。入庫文檔結構與關係型數據庫中的表結構類似,每個入庫的文檔由多個字段構成,假設這裏需要入庫的網站內容包括如下字段:文章標題、作者、發佈時間、原文鏈接、正文內容(一般作爲網頁快照)。
2. 2. 包含N個字段的文檔(DOCUMENT)在真正入庫前需要經過切詞(或分詞)索引,切詞的規則由語言分析器(ANALYZER)完成。
3. 3. 切分後的“單詞”被註冊到索引樹上,供查詢時用,另外也需要也其它不需要索引的內容入庫,所有這些是文件操作均由STORAGE完成。
以上就是記錄加載流程,索引樹是一種比較複雜的數據存儲結構,將在後續章節陸續介紹,這裏就不贅述了,需要說明的一點是,Lucene的索引樹結構非常優秀,是Lucene的一大特色。
接下來將對Lucene的各個子包的結構進行討論。
2. 3. 語言分析包org.apache.lucene.analysis
Analyzer是一個抽象類,司職對文本內容的切分詞規則。
切分後返回一個TokenStream,TokenStream中有一個非常重要方法next(),即取到下一個詞。簡單點說,通過切詞規則,把一篇文章從頭到尾分成一個個的詞,這就是org.apache.lucene.analysis的工作。
對英文而言,其分詞規則很簡單,因爲每個單詞間都有一個空格,按空格取單詞即可,當然爲了提高英文檢索的準確度,也可以把一些短語作爲一個整體,其間不切分,這就需要一個詞庫,對德文、俄文也是類似,稍有不同。
對中文而言,文字之間都是相連的,沒有空格,但我們同樣可以把字切分,即把每個漢字作爲一個詞切分,這就是所謂的“切字”,但切字方式方式的索引沒有意義,準確率太低,要想提高準確度一般都是切詞,這就需要一個詞庫,詞庫越大準確度將越高,但入庫效率越低。
若要支持中文切詞,則需要擴展Analyzer類,根據詞庫中的詞把文章切分。
簡單點說,org.apache.lucene.analysis就是完成將文章切分詞的任務。
3. 4. 文檔結構包org.apache.lucene.document
document包相對而言比較簡單,該包下面就3個類,Document相對於關係型數據庫的記錄對象,主要負責字段的管理,字段分兩種,一是Field,即文本型字段,另一個是日期型字段DateField。這個包中關鍵需要理解的是Field中字段存儲方式的不同,這在上一篇中已列表提到,下面我們可以參見一下其詳細的類圖:
4. 5. 索引管理包org.apache.lucene.index
索引包是整個系統核心,全文檢索的的根本就爲每個切出來的詞建索引,查詢時就只需要遍歷索引,而不需要去正文中遍歷,從而極大的提高檢索效率,索引建設的質量關鍵整個系統的質量。Lucene的索引樹是非常優質高效的,具體的索引樹細節,將在後續章節中重要探討。
在這個包中,主要學習IndexWriter和IndexReader這個類。
通過上一篇的初步應用可知,全文檢索庫的初始化和記錄加載均需要通過該類來完成。
初始化全文庫的語句爲:
IndexWriter indexWriter = new IndexWriter(“全文庫的目錄位置”,new StandardAnalyzer(),true);
記錄加載的語句爲:indexWriter.addDocument(doc);
IndexWriter主要用於寫庫,當需要讀取庫內容時,就需要用到IndexReader這個類了。
5. 6. 查詢分析包org.apache.lucene.queryParser和檢索包org.apache.lucene.search
通過查詢分析器(queryParser)解析後,將返回一個查詢對象(query),根據查詢對象就可進行檢索了。上圖描述了query對象的生成,下圖描述了查詢結果集(Hits)的生成。
6. 7. 存儲包org.apache.lucene.store
一些底層的文件I/O操作。
7. 8. 工具包org.apache.lucene.util
該包中包括4個工具類。
8. 9. 總結
通過對Lucene源碼包的分析,我們可以初步認識到Lucene的核心類包主要有3個:
l l org.apache.lucene.analysis
l l org.apache.lucene.index
l l org.apache.lucene.search
其中org.apache.lucene.analysis 主要用於切分詞,切分詞的工作由Analyzer的擴展類來實現,Lucene自帶了StandardAnalyzer類,我們可以參照該寫出自己的切詞分析器類,如中文分析器等。
org.apache.lucene.index主要提供庫的讀寫接口,通過該包可以創建庫、添加刪除記錄及讀取記錄等。
org.apache.lucene.search主要提供了檢索接口,通過該包,我們可以輸入條件,得到查詢結果集,與org.apache.lucene.queryParser包配合還可以自定義的查詢規則,像google一樣支持查詢條件間的與、或、非、屬於等複合查詢。
參考資料
1. 1. http://www-igm.univ-mlv.fr/~dr/XPOSE2003/lucene/node1.html
Lucene實踐
Lucene 全文檢索實踐(1)
Lucene 是 Apache Jakarta 的一個子項目,是一個全文檢索的搜索引擎庫。其提供了簡單實用的 API,通過這些 API,可以自行編寫對文件(TEXT/XML/HTML等)、目錄、數據庫的全文檢索程序。
Features:
* Very fast indexing, minimal RAM required
* Index compression to 30% of original text
* Indexes text and HTML, document classes available for XML, PDF and RTF
* Search supports phrase and Boolean queries, plus, minus and quote marks, and parentheses
* Allows single and multiple character wildcards anywhere in the search words, fuzzy search, proximity
* Will search for punctuation such as + or ?
* Field searches for title, author, etc., and date-range searching
* Supports most European languages
* Option to store and display full text of indexed documents
* Search results in relevance order
* APIs for file format conversion, languages and user interfaces
實踐任務:
1) 編寫 Java 程序 MyIndexer.java,使用 JDBC 取出 MySQL 數據表內容(以某一論壇數據做測試),然後通過 org.apache.lucene.index.IndexWriter 創建索引。
2) 編寫 Java 程序 MySearcher.java,通過 org.apache.lucene.search.IndexSearcher 等查詢索引。
3) 實現支持中文查詢及檢索關鍵字高亮顯示。
4) 通過 PHP / Java Integration 實現對 MySearch.java 的調用。
5) 實現對 PHP 手冊(簡體中文) 的全文檢索。
Lucene 全文檢索實踐(2)
Java 的程序基本編寫完成,實現了對中文的支持。下一步是將其放到 WEB 上運行,首先想到的是使用 JSP,安裝了Apache Tomcat/4.1.24,默認的發佈端口是 8080。現在面臨的一個問題是:Apache httpd 的端口是 80,並且我的機器對外只能通過 80 端口進行訪問,如果將 Tomcat 的發佈端口改成 80 的話,httpd 就沒法對外了,而其上的 PHP 程序也將無法在 80 端口運行。
對於這個問題,我想到兩種方案:
1、使用 PHP 直接調用 Java。需要做的工作是使用 --with-java 重新編譯 PHP;
2、使用 mod_jk 做橋接的方式,將 servlet 引擎結合到 httpd 中。需要做的工作是編譯 jakarta-tomcat-connectors-jk-1.2.5-src,生成 mod_jk.so 給 httpd 使用,然後按照 Howto 文檔 進行 Tomcat、httpd 的配置。
對於第一個方案的嘗試:使用 PHP 直接調用 Java
環境
* PHP 4.3.6 prefix=/usr
* Apache 1.3.27 prefix=/usr/local/apache
* j2sdk1.4.1_01 prefix=/usr/local/jdk
配置步驟
1) 安裝 JDK,這個就不多說了,到 GOOGLE 可以搜索出這方面的大量文章。
2) 重新編譯 PHP,我的 PHP 版本是 4.3.6:
cd php-
./configure --with-java=/usr/local/jdk
make
make install
完成之後,會在 PHP 的 lib 下(我的是在 /usr/lib/php)有個 php_java.jar,同時在擴展動態庫存放的目錄下(我的是在 /usr/lib/php/20020429)有個 java.so 文件。到這一步需要注意一個問題,有些 PHP 版本生成的是 libphp_java.so 文件,extension 的加載只認 libphp_java.so,直接加載 java.so 可能會出現如下錯誤:
PHP Fatal error: Unable to load Java Library /usr/local/jdk/jre/lib/i386/libjava.so, error: libjvm.so:
cannot open shared object file: No such file or directory in /home/nio/public_html/java.php on line 2
所以如果生成的是 java.so,需要創建一個符號連接:
ln -s java.so libphp_java.so
3) 修改 Apache Service 啓動文件(我的這個文件爲 /etc/init.d/httpd),在這個文件中加入:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/jdk/jre/lib/i386/server:/usr/local/jdk/jre/lib/i386
正如你所看到的,我的 JDK 裝在 /usr/local/jdk 目錄下,如果你的不是在此目錄,請做相應改動(下同)。
4) 修改 PHP 配置文件 php.ini,找到 [Java] 部分進行修改:
[Java]
java.class.path = /usr/lib/php/php_java.jar
java.home = /usr/local/jdk
;java.library =
;java.library.path =
extension_dir=/usr/lib/php/20020429/
extension=java.so
我將 java.library 及 java.library.path 都註釋掉了,PHP 會自動認爲 java.library=/usr/local/jdk/jre/lib/i386/libjava.so。
5) 重新啓動 Apache httpd 服務:
service httpd restart
測試
測試腳本 java.php 源代碼:
getProperty('java.version').'<br />';
print 'Java vendor=' . $system->getProperty('java.vendor').'<br />';
print 'OS=' . $system->getProperty('os.name') . ' ' .
$system->getProperty('os.version') . ' on ' .
$system->getProperty('os.arch') . '<br />';
?>
總結
安裝配置還算簡單,但是在 PHP 運行 Java 的速度感覺較慢,所以下定決心開始實踐第二個方案。(待續)
Lucene 全文檢索實踐(3)
今天總算有些空閒時間,正好說說第二種方案:使用 mod_jk 做橋接的方式,將 servlet 引擎結合到 httpd 中。
環境
* PH
* Apache 1.3.27 prefix=/usr/local/apache
* j2sdk1.4.1_01 prefix=/usr/local/jdk
* jakarta-tomcat-4.1.24 prefix=/usr/local/tomcat
* 另外需要下載 jakarta-tomcat-connectors-jk-1.2.5-src.tar.gz
配置步驟
1) 安裝 JDK 與 Tomcat,這些安裝步驟就不多說了。
2) 編譯 jakarta-tomcat-connectors-jk-1.2.5-src,生成 mod_jk.so,並將其複製到 apache 的 modules 存放目錄:
tar xzf jakarta-tomcat-connectors-jk-
cd jakarta-tomcat-connectors-jk-
./configure --with-apxs=/usr/local/apache/bin/apxs
make
cp apache-1.3/mod_jk.so /usr/local/apache/libexec
3) 編輯 Apache 配置文件 /usr/local/apache/conf/httpd.conf,加入:
LoadModule jk_module libexec/mod_jk.so
AddModule mod_jk.c
這個 LoadModule 語句最好放在其他 LoadModule 語句後邊。
同時在配置文件後邊加入:
# workers.properties 文件所在路徑,後邊將對此文件進行講解
JkWorkersFile /usr/local/apache/conf/workers.properties
# jk 的日誌文件存放路徑
JkLogFile /usr/local/apache/log/mod_jk.log
# 設置 jk 的日誌級別 [debug/error/info]
JkLogLevel info
# 選擇日誌時間格式
JkLogStampFormat "[%a %b %d %H:%M:%S %Y] "
# JkOptions 選項設置
JkOptions +ForwardKeySize +ForwardURICompat -ForwardDirectories
# JkRequestLogFormat 設置日誌的請求格式
JkRequestLogFormat "%w %V %T"
# 映射 /examples/* 到 worker1,worker1 在 workers.properties 文件中定義
JkMount /examples/* worker1
4) 在 /usr/local/apache/conf/ 目錄下創建 workers.properties 文件,其內容如下:
# 定義使用 ajp13 的 worker1
worker.list=worker1
# 設置 worker1 的屬性(ajp13)
worker.worker1.type=ajp13
worker.worker1.host=localhost
worker.worker1.port=8009
worker.worker1.lbfactor=50
worker.worker1.cachesize=10
worker.worker1.cache_timeout=600
worker.worker1.socket_keepalive=1
worker.worker1.socket_timeout=300
5) 好了,啓動 Tomcat,重啓一下 Apache HTTPD Server,訪問:http://localhost/examples/index.jsp,看看結果如何,和 http://localhost:8080/examples/index.jsp 是一樣的。
提示:如果不想讓別人通過 8080 端口訪問到你的 Tomcat,可以將 /usr/lcoal/tomcat/conf/server.xml 配置文件中的如下代碼加上註釋:
<!--
<Connector className="org.apache.coyote.tomcat4.CoyoteConnector"
port="8080" minProcessors="5" maxProcessors="75"
enableLookups="false" redirectPort="8443"
acceptCount="100" debug="0" connectionTimeout="20000"
useURIValidationHack="false" disableUploadTimeout="true" />
-->
然後重新啓動 Tomcat 即可。
總結
此方案安裝配置稍微複雜些,但執行效率要比第一種方案要好很多。所以決定使用這種方案來完成我的 Lucene 全文檢索實踐任務。
Tomcat Service 腳本
在 Linux (我用的是 Redhat)中,如果經常需要啓動/關閉 Tomcat 的話,還是創建一個 daemon 來得比較方便,創建步驟如下:
1) 在 /etc/init.d/ 目錄下創建文件 tomcat,代碼如下:
# chkconfig: 345 91 10
# description: Tomcat daemon.
#
# 包含函數庫
. /etc/rc.d/init.d/functions
# 獲取網絡配置
. /etc/sysconfig/network
# 檢測 NETWORKING 是否爲 "yes"
[ "${NETWORKING}" = "no" ] && exit 0
# 設置變量
# $TOMCAT 指向 Tomcat 的安裝目錄
TOMCAT=/usr/local/tomcat
# $STARTUP 指向 Tomcat 的啓動腳本
STARTUP=$TOMCAT/bin/startup.sh
# $SHUTDOWN 指向 Tomcat 的關閉腳本
SHUTDOWN=$TOMCAT/bin/shutdown.sh
# 設置 JAVA_HOME 環境變量,指向 JDK 安裝目錄
export JAVA_HOME=/usr/local/jdk
# 啓動服務函數
start() {
echo -n $"Starting Tomcat service: "
$STARTUP
RETVAL=$?
echo
}
# 關閉服務函數
stop() {
action $"Stopping Tomcat service: " $SHUTDOWN
RETVAL=$?
echo
}
# 根據參數選擇調用
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
*)
echo $"Usage: $0 start|stop|restart"
exit 1
esac
exit 0
2) 修改 tomcat 文件的屬性
chown a+x tomcat
3) 生成 service
chkconfig --add tomcat
好了,現在可以通過 service tomcat start 命令啓動 Tomcat 了,關閉及重啓服務的命令也類似,只是將 start 換成 stop 或 restart。
Lucene 全文檢索實踐(4)
在幾天的研究中,瞭解了 Lucene 全文檢索的一些原理,同時進行了實踐,編寫了一個論壇的全文檢索創建索引程序及用於搜索的 JSP 程序,另外還寫了一個 PHP 手冊(簡體中文)的全文檢索,可以進行多關鍵字搜索。基本完成了最初定下的實踐任務。
Lucene 全文檢索實踐(5)
對於 Lucene 的初步研究已經過去一段時間,自己感覺還不是很深入,但由於時間的關係,一直也沒再拿起。應網友的要求,將自己實踐中寫的一些代碼貼出來,希望能對大家有用。程序沒有做進一步的優化,只是很簡單的實現功能而已,僅供參考。
在實踐中,我以將 PHP 中文手冊中的 HTML 文件生成索引,然後通過一個 JSP 對其進行全文檢索。
生成索引的 Java 代碼:
/**
* PHPDocIndexer.java
* 用於對 PHPDoc 的 HTML 頁面生成索引文件。
*/
import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Date;
import java.text.DateFormat;
import java.lang.*;
import org.apache.lucene.analysis.cjk.CJKAnalyzer;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.DateField;
class PHPDocIndexer
{
public static void main(String[] args) throws ClassNotFoundException, IOException
{
try {
Date start = new Date();
IndexWriter writer = new IndexWriter("/home/nio/indexes-phpdoc", new CJKAnalyzer(), true); //索引保存目錄,必須存在
indexDocs(writer, new File("/home/nio/phpdoc-zh")); //HTML 文件保存目錄
System.out.println("Optimizing ....");
writer.optimize();
writer.close();
Date end = new Date();
System.out.print("Total time: ");
System.out.println(end.getTime() - start.getTime());
} catch (Exception e) {
System.out.println("Class " + e.getClass() + " throws error!/n errmsg: " + e.getMessage());
} //end try
} //end main
public static void indexDocs(IndexWriter writer, File file) throws Exception
{
if (file.isDirectory()) {
String[] files = file.list();
for (int i = 0; i < files.length; i++) {
indexDocs(writer, new File(file, files[i]));
} //end for
} else if (file.getPath().endsWith(".html")) { //只對 HTML 文件做索引
System.out.print("Add file:" + file + " ....");
// Add html file ....
Document doc = new Document();
doc.add(Field.UnIndexed("file", file.getName())); //索引文件名
doc.add(Field.UnIndexed("modified", DateFormat.getDateTimeInstance().format(new Date(file.lastModified())))); //索引最後修改時間
String title = "";
String content = "";
String status = "start";
FileReader fReader = new FileReader(file);
BufferedReader bReader = new BufferedReader(fReader);
String line = bReader.readLine();
while (line != null) {
content += line;
//截取 HTML 標題 <title>
if ("start" == status && line.equalsIgnoreCase("><TITLE")) {
status = "match";
} else if ("match" == status) {
title = line.substring(1, line.length() - 7);
doc.add(Field.Text("title", title)); //索引標題
status = "end";
} //end if
line = bReader.readLine();
} //end while
bReader.close();
fReader.close();
doc.add(Field.Text("content", content.replaceAll("<[^<>]+>", ""))); //索引內容
writer.addDocument(doc);
System.out.println(" [OK]");
} //end if
}
} //end class
索引生成完之後,就需要一個檢索頁面,下邊是搜索頁面(search.jsp)的代碼:
<%@ page language="java" import="javax.servlet.*, javax.servlet.http.*, java.io.*, java.util.Date, java.util.ArrayList, java.util.regex.*, org.apache.lucene.analysis.*, org.apache.lucene.document.*, org.apache.lucene.index.*, org.apache.lucene.search.*, org.apache.lucene.queryParser.*, org.apache.lucene.analysis.Token, org.apache.lucene.analysis.TokenStream, org.apache.lucene.analysis.cjk.CJKAnalyzer, org.apache.lucene.analysis.cjk.CJKTokenizer, com.chedong.weblucene.search.WebLuceneHighlighter" %>
<%@ page contentType="text/html;charset=GB2312" %>
<!DOCTYPE html PUBLIC "-//W
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<title>PHPDoc - PHP 簡體中文手冊全文檢索</title>
<base target="main"><!-- 由於使用了 Frame,所以指定 target 到 main 窗口顯示 -->
<style>
body {background-color: white; margin: 4px}
body, input, div {font-family: Tahoma; font-size:
body, div {line-height: 18px}
u {color: red}
b {color: navy}
form {padding: 0px; margin: 0px}
.txt {border: 1px solid black}
.f {padding: 4px; margin-bottom: 16px; background-color: #E5ECF9; border-top: 1px solid #3366CC; border-bottom: 1px solid #3366CC; text-align: center;}
.d, .o {padding-left: 16px}
.d {color: gray}
.o {color: green}
.o a {color: #7777CC}
</style>
<script language="JavaScript">
function gotoPage(i)
{
document.frm.page.value = i;
document.frm.submit();
} //end function
</script>
</head>
<body>
<%
String keyVal = null;
String pageVal = null;
int offset = 0;
int curPage = 0;
int pages;
final int ROWS = 50;
//獲取 GET 參數
try {
byte[] keyValByte = request.getParameter("key").getBytes("ISO8859_1"); //查找關鍵字
keyVal = new String(keyValByte);
pageVal = request.getParameter("page"); //頁碼
} catch (Exception e) {
//do nothing;
}
if (keyVal == null)
keyVal = new String("");
%>
<div class="f">
<form name="frm" action="./index.jsp" method="GET" οnsubmit="this.page.value='0';return true;" target="_self">
<input type="text" name="key" class="txt" size="40" value="<%=keyVal%>" />
<input type="hidden" name="page" value="<%=pageVal%>" />
<input type="submit" value="搜 索" /><br />
<font color="green">提示:可使用多個關鍵字(使用空格隔開)提高搜索的準確率。</font>
</form>
<script language="JavaScript">
document.frm.key.focus();
</script>
</div>
<%
if (keyVal != null && keyVal.length() > 0) {
try {
curPage = Integer.parseInt(pageVal); //將當前頁轉換成整數
} catch (Exception e) {
//do nothing;
} //end try
try {
Date startTime = new Date();
keyVal = keyVal.toLowerCase().replaceAll("(or|and)", "").trim().replaceAll("//s+", " AND ");
Searcher searcher = new IndexSearcher("/home/nio/indexes-phpdoc"); //索引目錄
Analyzer analyzer = new CJKAnalyzer();
String[] fields = {"title", "content"};
Query query = MultiFieldQueryParser.parse(keyVal, fields, analyzer);
Hits hits = searcher.search(query);
StringReader in = new StringReader(keyVal);
TokenStream tokenStream = analyzer.tokenStream("", in);
ArrayList al = new ArrayList();
for (Token token = tokenStream.next(); token != null; token = tokenStream.next()) {
al.add(token.termText());
} //end for
//總頁數
pages = (new Integer(hits.length()).doubleValue() % ROWS != 0) ? (hits.length() / ROWS) + 1 : (hits.length() / ROWS);
//當前頁碼
if (curPage < 1)
curPage = 1;
else if (curPage > pages)
curPage = pages;
//起始、終止下標
offset = (curPage - 1) * ROWS;
int end = Math.min(hits.length(), offset + ROWS);
//循環輸出查詢結果
WebLuceneHighlighter hl = new WebLuceneHighlighter(al);
for (int i = offset; i < end; i++) {
Document doc = hits.doc(i);
%>
<div class="t"><a href="/~nio/phpdoc-zh/<%=doc.get("file")%>"><%=hl.highLight(doc.get("title"))%></a></div>
<div class="d"><%=hl.highLight(doc.get("content").replaceAll("/n", " "), 100)%> ……</div>
<div class="o">
/~nio/phpdoc-zh/<%=doc.get("file")%>
-
<%=doc.get("modified")%>
</div>
<br />
<%
} //end for
searcher.close();
Date endTime = new Date();
%>
<div class="f">
檢索總共耗時 <b><%=((endTime.getTime() - startTime.getTime()) / 1000.0)%></b> 秒,約有 <b><%=hits.length()%></b> 項符合條件的記錄,共 <b><%=pages%></b> 頁
<%
if (curPage > 1 && pages > 1) {
%>
| <a href="javascript:gotoPage(<%=(curPage-1)%>);" target="_self">上一頁</a>
<%
} //end if
if (curPage < pages && pages > 1) {
%>
| <a href="javascript:gotoPage(<%=(curPage+1)%>)" target="_self">下一頁</a>
<%
} //end if
} catch (Exception e) {
%>
<!-- <%=e.getClass()%> 導致錯誤:<%=e.getMessage()%> -->
<%
} //end if
} //end if
%>
</body>
</html>
在線示例:PHP 手冊(簡體中文)。
在應用中加入全文檢索功能
——基於Java的全文索引引擎Lucene簡介
作者: 車東 Email: chedongATbigfoot.com/chedongATchedong.com
寫於:2002/08 最後更新:
<script language="JavaScript" src="http://www.chedong.com/referer.js" type="text/javascript">
</script>
11/29/2006 17:23:30
Feed Back >> (Read this before you ask question)
版權聲明:可以任意轉載,轉載時請務必以超鏈接形式標明文章原始出處和作者信息及本聲明
http://www.chedong.com/tech/lucene.html
關鍵詞:Lucene java full-text search engine Chinese word segment
內容摘要:
Lucene是一個基於Java的全文索引工具包。
- 基於Java的全文索引引擎Lucene簡介:關於作者和Lucene的歷史
- 全文檢索的實現:Luene全文索引和數據庫索引的比較
- 中文切分詞機制簡介:基於詞庫和自動切分詞算法的比較
- 具體的安裝和使用簡介:系統結構介紹和演示
- Hacking Lucene:簡化的查詢分析器,刪除的實現,定製的排序,應用接口的擴展
- 從Lucene我們還可以學到什麼
Lucene不是一個完整的全文索引應用,而是是一個用Java寫的全文索引引擎工具包,它可以方便的嵌入到各種應用中實現針對應用的全文索引/檢索功能。
Lucene的作者:Lucene的貢獻者Doug Cutting是一位資深全文索引/檢索專家,曾經是V-Twin搜索引擎(Apple的Copland操作系統的成就之一)的主要開發者,後在Excite擔任高級系統架構設計師,目前從事於一些INTERNET底層架構的研究。他貢獻出的Lucene的目標是爲各種中小型應用程序加入全文檢索功能。
Lucene的發展歷程:早先發布在作者自己的www.lucene.com,後來發佈在SourceForge,2001年年底成爲APACHE基金會jakarta的一個子項目:http://jakarta.apache.org/lucene/
已經有很多Java項目都使用了Lucene作爲其後臺的全文索引引擎,比較著名的有:
- Jive:WEB論壇系統;
- Eyebrows:郵件列表HTML歸檔/瀏覽/查詢系統,本文的主要參考文檔“TheLucene search engine: Powerful, flexible, and free”作者就是EyeBrows系統的主要開發者之一,而EyeBrows已經成爲目前APACHE項目的主要郵件列表歸檔系統。
- Cocoon:基於XML的web發佈框架,全文檢索部分使用了Lucene
· Eclipse:基於Java的開放開發平臺,幫助部分的全文索引使用了Lucene
對於中文用戶來說,最關心的問題是其是否支持中文的全文檢索。但通過後面對於Lucene的結構的介紹,你會瞭解到由於Lucene良好架構設計,對中文的支持只需對其語言詞法分析接口進行擴展就能實現對中文檢索的支持。
Lucene的API接口設計的比較通用,輸入輸出結構都很像數據庫的表==>記錄==>字段,所以很多傳統的應用的文件、數據庫等都可以比較方便的映射到Lucene的存儲結構/接口中。總體上看:可以先把Lucene當成一個支持全文索引的數據庫系統。
比較一下Lucene和數據庫:
Lucene |
數據庫 |
索引數據源:doc(field1,field2...) doc(field1,field2...) |
索引數據源:record(field1,field2...) record(field1..) |
Document:一個需要進行索引的“單元” |
Record:記錄,包含多個字段 |
Field:字段 |
Field:字段 |
Hits:查詢結果集,由匹配的Document組成 |
RecordSet:查詢結果集,由多個Record組成 |
全文檢索 ≠ like "%keyword%"
通常比較厚的書籍後面常常附關鍵詞索引表(比如:北京:12, 34頁,上海:3,77頁……),它能夠幫助讀者比較快地找到相關內容的頁碼。而數據庫索引能夠大大提高查詢的速度原理也是一樣,想像一下通過書後面的索引查找的速度要比一頁一頁地翻內容高多少倍……而索引之所以效率高,另外一個原因是它是排好序的。對於檢索系統來說核心是一個排序問題。
由於數據庫索引不是爲全文索引設計的,因此,使用like "%keyword%"時,數據庫索引是不起作用的,在使用like查詢時,搜索過程又變成類似於一頁頁翻書的遍歷過程了,所以對於含有模糊查詢的數據庫服務來說,LIKE對性能的危害是極大的。如果是需要對多個關鍵詞進行模糊匹配:like"%keyword1%" and like "%keyword2%" ...其效率也就可想而知了。
所以建立一個高效檢索系統的關鍵是建立一個類似於科技索引一樣的反向索引機制,將數據源(比如多篇文章)排序順序存儲的同時,有另外一個排好序的關鍵詞列表,用於存儲關鍵詞==>文章映射關係,利用這樣的映射關係索引:[關鍵詞==>出現關鍵詞的文章編號,出現次數(甚至包括位置:起始偏移量,結束偏移量),出現頻率],檢索過程就是把模糊查詢變成多個可以利用索引的精確查詢的邏輯組合的過程。從而大大提高了多關鍵詞查詢的效率,所以,全文檢索問題歸結到最後是一個排序問題。
由此可以看出模糊查詢相對數據庫的精確查詢是一個非常不確定的問題,這也是大部分數據庫對全文檢索支持有限的原因。Lucene最核心的特徵是通過特殊的索引結構實現了傳統數據庫不擅長的全文索引機制,並提供了擴展接口,以方便針對不同應用的定製。
可以通過一下表格對比一下數據庫的模糊查詢:
|
Lucene全文索引引擎 |
數據庫 |
索引 |
將數據源中的數據都通過全文索引一一建立反向索引 |
對於LIKE查詢來說,數據傳統的索引是根本用不上的。數據需要逐個便利記錄進行GREP式的模糊匹配,比有索引的搜索速度要有多個數量級的下降。 |
匹配效果 |
通過詞元(term)進行匹配,通過語言分析接口的實現,可以實現對中文等非英語的支持。 |
使用:like "%net%" 會把netherlands也匹配出來, |
匹配度 |
有匹配度算法,將匹配程度(相似度)比較高的結果排在前面。 |
沒有匹配程度的控制:比如有記錄中net出現5詞和出現1次的,結果是一樣的。 |
結果輸出 |
通過特別的算法,將最匹配度最高的頭100條結果輸出,結果集是緩衝式的小批量讀取的。 |
返回所有的結果集,在匹配條目非常多的時候(比如上萬條)需要大量的內存存放這些臨時結果集。 |
可定製性 |
通過不同的語言分析接口實現,可以方便的定製出符合應用需要的索引規則(包括對中文的支持) |
沒有接口或接口複雜,無法定製 |
結論 |
高負載的模糊查詢應用,需要負責的模糊查詢的規則,索引的資料量比較大 |
使用率低,模糊匹配規則簡單或者需要模糊查詢的資料量少 |
全文檢索和數據庫應用最大的不同在於:讓最相關的頭100條結果滿足98%以上用戶的需求
Lucene的創新之處:
大部分的搜索(數據庫)引擎都是用B樹結構來維護索引,索引的更新會導致大量的IO操作,Lucene在實現中,對此稍微有所改進:不是維護一個索引文件,而是在擴展索引的時候不斷創建新的索引文件,然後定期的把這些新的小索引文件合併到原先的大索引中(針對不同的更新策略,批次的大小可以調整),這樣在不影響檢索的效率的前提下,提高了索引的效率。
Lucene和其他一些全文檢索系統/應用的比較:
|
Lucene |
其他開源全文檢索系統 |
增量索引和批量索引 |
可以進行增量的索引(Append),可以對於大量數據進行批量索引,並且接口設計用於優化批量索引和小批量的增量索引。 |
很多系統只支持批量的索引,有時數據源有一點增加也需要重建索引。 |
數據源 |
Lucene沒有定義具體的數據源,而是一個文檔的結構,因此可以非常靈活的適應各種應用(只要前端有合適的轉換器把數據源轉換成相應結構), |
很多系統只針對網頁,缺乏其他格式文檔的靈活性。 |
索引內容抓取 |
Lucene的文檔是由多個字段組成的,甚至可以控制那些字段需要進行索引,那些字段不需要索引,近一步索引的字段也分爲需要分詞和不需要分詞的類型: |
缺乏通用性,往往將文檔整個索引了 |
語言分析 |
通過語言分析器的不同擴展實現: |
缺乏通用接口實現 |
查詢分析 |
通過查詢分析接口的實現,可以定製自己的查詢語法規則: |
|
併發訪問 |
能夠支持多用戶的使用 |
|
對於中文來說,全文索引首先還要解決一個語言分析的問題,對於英文來說,語句中單詞之間是天然通過空格分開的,但亞洲語言的中日韓文語句中的字是一個字挨一個,所有,首先要把語句中按“詞”進行索引的話,這個詞如何切分出來就是一個很大的問題。
首先,肯定不能用單個字符作(si-gram)爲索引單元,否則查“上海”時,不能讓含有“海上”也匹配。
但一句話:“北京天安門”,計算機如何按照中文的語言習慣進行切分呢?
“北京 天安門” 還是“北 京 天安門”?讓計算機能夠按照語言習慣進行切分,往往需要機器有一個比較豐富的詞庫才能夠比較準確的識別出語句中的單詞。
另外一個解決的辦法是採用自動切分算法:將單詞按照2元語法(bigram)方式切分出來,比如:
"北京天安門" ==> "北京 京天 天安 安門"。
這樣,在查詢的時候,無論是查詢"北京" 還是查詢"天安門",將查詢詞組按同樣的規則進行切分:"北京","天安安門",多個關鍵詞之間按與"and"的關係組合,同樣能夠正確地映射到相應的索引中。這種方式對於其他亞洲語言:韓文,日文都是通用的。
基於自動切分的最大優點是沒有詞表維護成本,實現簡單,缺點是索引效率低,但對於中小型應用來說,基於2元語法的切分還是夠用的。基於2元切分後的索引一般大小和源文件差不多,而對於英文,索引文件一般只有原文件的30%-40%不同,
|
自動切分 |
詞表切分 |
實現 |
實現非常簡單 |
實現複雜 |
查詢 |
增加了查詢分析的複雜程度, |
適於實現比較複雜的查詢語法規則 |
存儲效率 |
索引冗餘大,索引幾乎和原文一樣大 |
索引效率高,爲原文大小的30%左右 |
維護成本 |
無詞表維護成本 |
詞表維護成本非常高:中日韓等語言需要分別維護。 |
適用領域 |
嵌入式系統:運行環境資源有限 |
對查詢和存儲效率要求高的專業搜索引擎 |
目前比較大的搜索引擎的語言分析算法一般是基於以上2個機制的結合。關於中文的語言分析算法,大家可以在Google查關鍵詞"wordsegment search"能找到更多相關的資料。
下載:http://jakarta.apache.org/lucene/
注意:Lucene中的一些比較複雜的詞法分析是用JavaCC生成的(JavaCC:JavaCompilerCompiler,純Java的詞法分析生成器),所以如果從源代碼編譯或需要修改其中的QueryParser、定製自己的詞法分析器,還需要從https://javacc.dev.java.net/下載javacc。
lucene的組成結構:對於外部應用來說索引模塊(index)和檢索模塊(search)是主要的外部應用入口
org.apache.Lucene.search/ |
搜索入口 |
org.apache.Lucene.index/ |
索引入口 |
org.apache.Lucene.analysis/ |
語言分析器 |
org.apache.Lucene.queryParser/ |
查詢分析器 |
org.apache.Lucene.document/ |
存儲結構 |
org.apache.Lucene.store/ |
底層IO/存儲結構 |
org.apache.Lucene.util/ |
一些公用的數據結構 |
簡單的例子演示一下Lucene的使用方法:
索引過程:從命令行讀取文件名(多個),將文件分路徑(path字段)和內容(body字段)2個字段進行存儲,並對內容進行全文索引:索引的單位是Document對象,每個Document對象包含多個字段Field對象,針對不同的字段屬性和數據輸出的需求,對字段還可以選擇不同的索引/存儲字段規則,列表如下:
方法 |
切詞 |
索引 |
存儲 |
用途 |
Field.Text(String name, String value) |
Yes |
Yes |
Yes |
切分詞索引並存儲,比如:標題,內容字段 |
Field.Text(String name, Reader value) |
Yes |
Yes |
No |
切分詞索引不存儲,比如:META信息, |
Field.Keyword(String name, String value) |
No |
Yes |
Yes |
不切分索引並存儲,比如:日期字段 |
Field.UnIndexed(String name, String value) |
No |
No |
Yes |
不索引,只存儲,比如:文件路徑 |
Field.UnStored(String name, String value) |
Yes |
Yes |
No |
只全文索引,不存儲 |
public class IndexFiles {
//使用方法:: IndexFiles [索引輸出目錄] [索引的文件列表] ...
public static void main(String[] args) throws Exception {
String indexPath = args[0];
IndexWriter writer;
//用指定的語言分析器構造一個新的寫索引器(第3個參數表示是否爲追加索引)
writer = new IndexWriter(indexPath, new SimpleAnalyzer(), false);
for (int i=1; i<args.length; i++) {
System.out.println("Indexing file " + args[i]);
InputStream is = new FileInputStream(args[i]);
//構造包含2個字段Field的Document對象
//一個是路徑path字段,不索引,只存儲
//一個是內容body字段,進行全文索引,並存儲
Document doc = new Document();
doc.add(Field.UnIndexed("path", args[i]));
doc.add(Field.Text("body", (Reader) new InputStreamReader(is)));
//將文檔寫入索引
writer.addDocument(doc);
is.close();
};
//關閉寫索引器
writer.close();
}
}
索引過程中可以看到:
- 語言分析器提供了抽象的接口,因此語言分析(Analyser)是可以定製的,雖然lucene缺省提供了2個比較通用的分析器SimpleAnalyser和StandardAnalyser,這2個分析器缺省都不支持中文,所以要加入對中文語言的切分規則,需要修改這2個分析器。
- Lucene並沒有規定數據源的格式,而只提供了一個通用的結構(Document對象)來接受索引的輸入,因此輸入的數據源可以是:數據庫,WORD文檔,PDF文檔,HTML文檔……只要能夠設計相應的解析轉換器將數據源構造成成Docuement對象即可進行索引。
- 對於大批量的數據索引,還可以通過調整IndexerWrite的文件合併頻率屬性(mergeFactor)來提高批量索引的效率。
檢索過程和結果顯示:
搜索結果返回的是Hits對象,可以通過它再訪問Document==>Field中的內容。
假設根據body字段進行全文檢索,可以將查詢結果的path字段和相應查詢的匹配度(score)打印出來,
public class Search {
public static void main(String[] args) throws Exception {
String indexPath = args[0], queryString = args[1];
//指向索引目錄的搜索器
Searcher searcher = new IndexSearcher(indexPath);
//查詢解析器:使用和索引同樣的語言分析器
Query query = QueryParser.parse(queryString, "body",
new SimpleAnalyzer());
//搜索結果使用Hits存儲
Hits hits = searcher.search(query);
//通過hits可以訪問到相應字段的數據和查詢的匹配度
for (int i=0; i<hits.length(); i++) {
System.out.println(hits.doc(i).get("path") + "; Score: " +
hits.score(i));
};
}
}
在整個檢索過程中,語言分析器,查詢分析器,甚至搜索器(Searcher)都是提供了抽象的接口,可以根據需要進行定製。
簡化的查詢分析器
個人感覺lucene成爲JAKARTA項目後,畫在了太多的時間用於調試日趨複雜QueryParser,而其中大部分是大多數用戶並不很熟悉的,目前LUCENE支持的語法:
Query ::= ( Clause )*
Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")")
中間的邏輯包括:and or + - &&||等符號,而且還有"短語查詢"和針對西文的前綴/模糊查詢等,個人感覺對於一般應用來說,這些功能有一些華而不實,其實能夠實現目前類似於Google的查詢語句分析功能其實對於大多數用戶來說已經夠了。所以,Lucene早期版本的QueryParser仍是比較好的選擇。
添加修改刪除指定記錄(Document)
Lucene提供了索引的擴展機制,因此索引的動態擴展應該是沒有問題的,而指定記錄的修改也似乎只能通過記錄的刪除,然後重新加入實現。如何刪除指定的記錄呢?刪除的方法也很簡單,只是需要在索引時根據數據源中的記錄ID專門另建索引,然後利用IndexReader.delete(Termterm)方法通過這個記錄ID刪除相應的Document。
根據某個字段值的排序功能
lucene缺省是按照自己的相關度算法(score)進行結果排序的,但能夠根據其他字段進行結果排序是一個在LUCENE的開發郵件列表中經常提到的問題,很多原先基於數據庫應用都需要除了基於匹配度(score)以外的排序功能。而從全文檢索的原理我們可以瞭解到,任何不基於索引的搜索過程效率都會導致效率非常的低,如果基於其他字段的排序需要在搜索過程中訪問存儲字段,速度回大大降低,因此非常是不可取的。
但這裏也有一個折中的解決方法:在搜索過程中能夠影響排序結果的只有索引中已經存儲的docID和score這2個參數,所以,基於score以外的排序,其實可以通過將數據源預先排好序,然後根據docID進行排序來實現。這樣就避免了在LUCENE搜索結果外對結果再次進行排序和在搜索過程中訪問不在索引中的某個字段值。
這裏需要修改的是IndexSearcher中的HitCollector過程:
...
scorer.score(new HitCollector() {
private float minScore =0.0f ;
public final void collect(int doc, float score) {
if (score >0.0f && // ignore zeroed buckets
(bits==null || bits.get(doc))) { // skip docs not in bits
totalHits[0]++;
if (score >= minScore) {
/* 原先:Lucene將docID和相應的匹配度score例入結果命中列表中:
* hq.put(new ScoreDoc(doc, score)); // update hit queue
* 如果用doc 或 1/doc 代替 score,就實現了根據docID順排或逆排
* 假設數據源索引時已經按照某個字段排好了序,而結果根據docID排序也就實現了
* 針對某個字段的排序,甚至可以實現更復雜的score和docID的擬合。
*/
hq.put(new ScoreDoc(doc, (float) 1/doc ));
if (hq.size() > nDocs) { // if hit queue overfull
hq.pop(); // remove lowest in hit queue
minScore = ((ScoreDoc)hq.top()).score; // reset minScore
}
}
}
}
}, reader.maxDoc());
更通用的輸入輸出接口
雖然lucene沒有定義一個確定的輸入文檔格式,但越來越多的人想到使用一個標準的中間格式作爲Lucene的數據導入接口,然後其他數據,比如PDF只需要通過解析器轉換成標準的中間格式就可以進行數據索引了。這個中間格式主要以XML爲主,類似實現已經不下4,5個:
數據源: WORD PDF HTML DB other
/ | | | /
XML中間格式
|
Lucene INDEX
目前還沒有針對MSWord文檔的解析器,因爲Word文檔和基於ASCII的RTF文檔不同,需要使用COM對象機制解析。這個是我在Google上查的相關資料:http://www.intrinsyc.com/products/enterprise_applications.asp
另外一個辦法就是把Word文檔轉換成text:http://www.winfield.demon.nl/index.html
索引過程優化
索引一般分2種情況,一種是小批量的索引擴展,一種是大批量的索引重建。在索引過程中,並不是每次新的DOC加入進去索引都重新進行一次索引文件的寫入操作(文件I/O是一件非常消耗資源的事情)。
Lucene先在內存中進行索引操作,並根據一定的批量進行文件的寫入。這個批次的間隔越大,文件的寫入次數越少,但佔用內存會很多。反之佔用內存少,但文件IO操作頻繁,索引速度會很慢。在IndexWriter中有一個MERGE_FACTOR參數可以幫助你在構造索引器後根據應用環境的情況充分利用內存減少文件的操作。根據我的使用經驗:缺省Indexer是每20條記錄索引後寫入一次,每將MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。
搜索過程優化
lucene支持內存索引:這樣的搜索比基於文件的I/O有數量級的速度提升。
http://www.onjava.com/lpt/a/3273
而儘可能減少IndexSearcher的創建和對搜索結果的前臺的緩存也是必要的。
Lucene面向全文檢索的優化在於首次索引檢索後,並不把所有的記錄(Document)具體內容讀取出來,而起只將所有結果中匹配度最高的頭100條結果(TopDocs)的ID放到結果集緩存中並返回,這裏可以比較一下數據庫檢索:如果是一個10,000條的數據庫檢索結果集,數據庫是一定要把所有記錄內容都取得以後再開始返回給應用結果集的。所以即使檢索匹配總數很多,Lucene的結果集佔用的內存空間也不會很多。對於一般的模糊檢索應用是用不到這麼多的結果的,頭100條已經可以滿足90%以上的檢索需求。
如果首批緩存結果數用完後還要讀取更後面的結果時Searcher會再次檢索並生成一個上次的搜索緩存數大1倍的緩存,並再重新向後抓取。所以如果構造一個Searcher去查1-120條結果,Searcher其實是進行了2次搜索過程:頭100條取完後,緩存結果用完,Searcher重新檢索再構造一個200條的結果緩存,依此類推,400條緩存,800條緩存。由於每次Searcher對象消失後,這些緩存也訪問那不到了,你有可能想將結果記錄緩存下來,緩存數儘量保證在100以下以充分利用首次的結果緩存,不讓Lucene浪費多次檢索,而且可以分級進行結果緩存。
Lucene的另外一個特點是在收集結果的過程中將匹配度低的結果自動過濾掉了。這也是和數據庫應用需要將搜索的結果全部返回不同之處。
- 支持中文的Tokenizer:這裏有2個版本,一個是通過JavaCC生成的,對CJK部分按一個字符一個TOKEN索引,另外一個是從SimpleTokenizer改寫的,對英文支持數字和字母TOKEN,對中文按迭代索引。
- 基於XML數據源的索引器:XMLIndexer,因此所有數據源只要能夠按照DTD轉換成指定的XML,就可以用XMLIndxer進行索引了。
- 根據某個字段排序:按記錄索引順序排序結果的搜索器:IndexOrderSearcher,因此如果需要讓搜索結果根據某個字段排序,可以讓數據源先按某個字段排好序(比如:PriceField),這樣索引後,然後在利用這個按記錄的ID順序檢索的搜索器,結果就是相當於是那個字段排序的結果了。
Luene的確是一個面對對象設計的典範
- 所有的問題都通過一個額外抽象層來方便以後的擴展和重用:你可以通過重新實現來達到自己的目的,而對其他模塊而不需要;
- 簡單的應用入口Searcher, Indexer,並調用底層一系列組件協同的完成搜索任務;
- 所有的對象的任務都非常專一:比如搜索過程:QueryParser分析將查詢語句轉換成一系列的精確查詢的組合(Query),通過底層的索引讀取結構IndexReader進行索引的讀取,並用相應的打分器給搜索結果進行打分/排序等。所有的功能模塊原子化程度非常高,因此可以通過重新實現而不需要修改其他模塊。
- 除了靈活的應用接口設計,Lucene還提供了一些適合大多數應用的語言分析器實現(SimpleAnalyser,StandardAnalyser),這也是新用戶能夠很快上手的重要原因之一。
這些優點都是非常值得在以後的開發中學習借鑑的。作爲一個通用工具包,Lunece的確給予了需要將全文檢索功能嵌入到應用中的開發者很多的便利。
此外,通過對Lucene的學習和使用,我也更深刻地理解了爲什麼很多數據庫優化設計中要求,比如:
- 儘可能對字段進行索引來提高查詢速度,但過多的索引會對數據庫表的更新操作變慢,而對結果過多的排序條件,實際上往往也是性能的殺手之一。
- 很多商業數據庫對大批量的數據插入操作會提供一些優化參數,這個作用和索引器的merge_factor的作用是類似的,
- 20%/80%原則:查的結果多並不等於質量好,尤其對於返回結果集很大,如何優化這頭幾十條結果的質量往往纔是最重要的。
- 儘可能讓應用從數據庫中獲得比較小的結果集,因爲即使對於大型數據庫,對結果集的隨機訪問也是一個非常消耗資源的操作。
參考資料:
Apache: Lucene Project
http://jakarta.apache.org/lucene/
Lucene開發/用戶郵件列表歸檔
[email protected]
[email protected]
The Lucene search engine: Powerful, flexible, and free
http://www.javaworld.com/javaworld/jw-09-2000/jw-0915-Lucene_p.html
Lucene Tutorial
http://www.darksleep.com/puff/lucene/lucene.html
Notes on distributed searching with Lucene
http://home.clara.net/markharwood/lucene/
中文語言的切分詞
http://www.google.com/search?sourceid=navclient&hl=zh-CN&q=chinese+word+segment
搜索引擎工具介紹
http://searchtools.com/
Lucene作者Cutting的幾篇論文和專利
http://lucene.sourceforge.net/publications.html
Lucene的.NET實現:dotLucene
http://sourceforge.net/projects/dotlucene/
Lucene作者Cutting的另外一個項目:基於Java的搜索引擎Nutch
http://www.nutch.org/ http://sourceforge.net/projects/nutch/
關於基於詞表和N-Gram的切分詞比較
http://china.nikkeibp.co.jp/cgi-bin/china/news/int/int200302100112.html
特別感謝:
前網易CTO許良傑(Jack Xu)給我的指導:是您將我帶入了搜索引擎這個行業。
原文出處:<ahref="http://www.chedong.com/tech/lucene.html">http://www.chedong.com/tech/lucene.html</a>
文章類別: 程序 技術 — SuperTaoer @ 2:40 am
一,weblucene的簡介
Web Lunce是中國人車東在sourceforge裏面的一個項目,詳細的地址在http://www.chedong.com/tech/weblucene.html。這個項目很好的解決對中文的支持
二, 基本原理我就不用介紹了,參照作者的文檔。
三,碰到的問題
1,編譯的時候需要指定javacc的路徑,並且只支持javacc2.0,由於發佈的是linux版,所以需要修改build.property裏面的值對應的路徑。
2,生存index目錄
發佈的index的命令在dump目錄中的index.sh,如果是windows的話就需要按照裏面內容重新建立index.cmd文件,並且修改相關的環境變量和值
3,把生成的index目錄拷貝到tomcat的webapps路徑下面
4,在查詢的時候會出現中文問題,產生的原因主要是由於字符集引起的,解決這個辦法可以把提交的方法修改爲post就可以了。需要修改幾個地方
1),WebLuceneServlet.java增加doPost方法
2),修改index.html文件,把method中的get修改爲post
3),修改var/searchbox.xsl文件,把method中的get修改爲post
XML 與 Java 技術: 用 Castor 進行數據綁定 #
對於主要關心文檔數據內容的應用程序,Java XML 數據綁定是一種代替 XML 文檔模型的強大機制。本文中,企業 Java 專家 Dennis Sosnoski 介紹數據綁定,並討論什麼使它如此令人矚目。然後,他向讀者展示瞭如何利用 Java 數據綁定的開放源代碼 Castor 框架來處理日益複雜的文檔。如果您的應用程序更多的把 XML 作爲數據而不是文檔,您就會願意瞭解這種處理 XML 和 Java 技術的簡單有效的方法。
.LUNCE有幾種語言分析器,其特點見另外一篇文章。import org.apache.lucene.analysis.standard.StandardAnalyzer;這個類可以進行中文分詞,但是效果不是很好。