lucene檢索機制和檢索效能優化
全文檢索:不管是結構檔案或者非機構檔案,先建立對其建立索引,在對索引進行搜尋就是全文檢索
Lucene 是一個基於 Java 的全文檢索工具包,你可以利用它來為你的應用程式加入索引和檢索功能。Lucene 目前是著名的 Apache Jakarta 家族中的一個開源專案,下面我們即將學習 Lucene 的索引機制以及它的索引檔案的結構。
在這篇文章中,我們首先演示如何使用 Lucene 來索引文件,接著討論如何提高索引的效能。最後我們來分析 Lucene 的索引檔案結構。需要記住的是,Lucene 不是一個完整的應用程式,而是一個資訊檢索包,它方便你為你的應用程式新增索引和搜尋功能。
架構概覽
圖一顯示了 Lucene 的索引機制的架構。Lucene 使用各種解析器對各種不同型別的文件進行解析。比如對於 HTML 文件,HTML 解析器會做一些預處理的工作,比如過濾文件中的 HTML 標籤等等。HTML 解析器的輸出的是文字內容,接著 Lucene 的分詞器(Analyzer)從文字內容中提取出索引項以及相關資訊,比如索引項的出現頻率。接著 Lucene 的分詞器把這些資訊寫到索引檔案中。
圖一:Lucene 索引機制架構
用Lucene索引文件
接下來我將一步一步的來演示如何利用 Lucene 為你的文件建立索引。只要你能將要索引的檔案轉化成文字格式,Lucene 就能為你的文件建立索引。比如,如果你想為 HTML 文件或者 PDF 文件建立索引,那麼首先你就需要從這些文件中提取出文字資訊,然後把文字資訊交給 Lucene 建立索引。我們接下來的例子用來演示如何利用 Lucene 為字尾名為 txt 的檔案建立索引。
1. 準備文字檔案
首先把一些以 txt 為字尾名的文字檔案放到一個目錄中,比如在 Windows 平臺上,你可以放到 C:\\files_to_index 下面。
2. 建立索引
清單1是為我們所準備的文件建立索引的程式碼。
清單1:用 Lucene 索引你的文件
package lucene.index; import java.io.File; import java.io.FileReader; import java.io.Reader; import java.util.Date; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.index.IndexWriter; /** * This class demonstrates the process of creating an index with Lucene * for text files in a directory. */ public class TextFileIndexer { public static void main(String[] args) throws Exception{ //fileDir is the directory that contains the text files to be indexed File fileDir = new File("C:\\files_to_index "); //indexDir is the directory that hosts Lucene's index files File indexDir = new File("C:\\luceneIndex"); Analyzer luceneAnalyzer = new StandardAnalyzer(); IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,true); File[] textFiles = fileDir.listFiles(); long startTime = new Date().getTime(); //Add documents to the index for(int i = 0; i < textFiles.length; i++){ if(textFiles[i].isFile() >> textFiles[i].getName().endsWith(".txt")){ System.out.println("File " + textFiles[i].getCanonicalPath() + " is being indexed"); Reader textReader = new FileReader(textFiles[i]); Document document = new Document(); document.add(Field.Text("content",textReader)); document.add(Field.Text("path",textFiles[i].getPath())); indexWriter.addDocument(document); } } indexWriter.optimize(); indexWriter.close(); long endTime = new Date().getTime(); System.out.println("It took " + (endTime - startTime) + " milliseconds to create an index for the files in the directory " + fileDir.getPath()); } }
正如清單1所示,你可以利用 Lucene 非常方便的為文件建立索引。接下來我們分析一下清單1中的比較關鍵的程式碼,我們先從下面的一條語句開始看起。
Analyzer luceneAnalyzer = new StandardAnalyzer();
這條語句建立了類 StandardAnalyzer 的一個例項,這個類是用來從文字中提取出索引項的。它只是抽象類 Analyzer 的其中一個實現。Analyzer 也有一些其它的子類,比如 SimpleAnalyzer 等。
我們接著看另外一條語句:
IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,true);
這條語句建立了類 IndexWriter 的一個例項,該類也是 Lucene 索引機制裡面的一個關鍵類。這個類能建立一個新的索引或者開啟一個已存在的索引併為該所引新增文件。我們注意到該類的建構函式接受三個引數,第一個引數指定了儲存索引檔案的路徑。第二個引數指定了在索引過程中使用什麼樣的分詞器。最後一個引數是個布林變數,如果值為真,那麼就表示要建立一個新的索引,如果值為假,就表示開啟一個已經存在的索引。
接下來的程式碼演示瞭如何新增一個文件到索引檔案中。
Document document = new Document(); document.add(Field.Text("content",textReader)); document.add(Field.Text("path",textFiles[i].getPath())); indexWriter.addDocument(document);
首先第一行建立了類 Document 的一個例項,它由一個或者多個的域(Field)組成。你可以把這個類想象成代表了一個實際的文件,比如一個 HTML 頁面,一個 PDF 文件,或者一個文字檔案。而類 Document 中的域一般就是實際文件的一些屬性。比如對於一個 HTML 頁面,它的域可能包括標題,內容,URL 等。我們可以用不同型別的 Field 來控制文件的哪些內容應該索引,哪些內容應該儲存。如果想獲取更多的關於 Lucene 的域的資訊,可以參考 Lucene 的幫助文件。程式碼的第二行和第三行為文件添加了兩個域,每個域包含兩個屬性,分別是域的名字和域的內容。在我們的例子中兩個域的名字分別是"content"和"path"。分別儲存了我們需要索引的文字檔案的內容和路徑。最後一行把準備好的文件新增到了索引當中。
當我們把文件新增到索引中後,不要忘記關閉索引,這樣才保證 Lucene 把新增的文件寫回到硬碟上。下面的一句程式碼演示瞭如何關閉索引。
indexWriter.close();
利用清單1中的程式碼,你就可以成功的將文字文件新增到索引中去。接下來我們看看對索引進行的另外一種重要的操作,從索引中刪除文件。
從索引中刪除文件
類IndexReader負責從一個已經存在的索引中刪除文件,如清單2所示。
清單2:從索引中刪除文件
File indexDir = new File("C:\\luceneIndex"); IndexReader ir = IndexReader.open(indexDir); ir.delete(1); ir.delete(new Term("path","C:\\file_to_index\lucene.txt")); ir.close();
在清單2中,第二行用靜態方法 IndexReader.open(indexDir) 初始化了類 IndexReader 的一個例項,這個方法的引數指定了索引的儲存路徑。類 IndexReader 提供了兩種方法去刪除一個文件,如程式中的第三行和第四行所示。第三行利用文件的編號來刪除文件。每個文件都有一個系統自動生成的編號。第四行刪除了路徑為"C:\\file_to_index\lucene.txt"的文件。你可以通過指定檔案路徑來方便的刪除一個文件。值得注意的是雖然利用上述程式碼刪除文件使得該文件不能被檢索到,但是並沒有物理上刪除該文件。Lucene 只是通過一個字尾名為 .delete 的檔案來標記哪些文件已經被刪除。既然沒有物理上刪除,我們可以方便的把這些標記為刪除的文件恢復過來,如清單 3 所示,首先開啟一個索引,然後呼叫方法 ir.undeleteAll() 來完成恢復工作。
清單3:恢復已刪除文件
File indexDir = new File("C:\\luceneIndex"); IndexReader ir = IndexReader.open(indexDir); ir.undeleteAll(); ir.close();
你現在也許想知道如何物理上刪除索引中的文件,方法也非常簡單。清單 4 演示了這個過程。
清單4:如何物理上刪除文件
File indexDir = new File("C:\\luceneIndex"); Analyzer luceneAnalyzer = new StandardAnalyzer(); IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,false); indexWriter.optimize(); indexWriter.close();
在清單 4 中,第三行建立了類 IndexWriter 的一個例項,並且打開了一個已經存在的索引。第 4 行對索引進行清理,清理過程中將把所有標記為刪除的文件物理刪除。
Lucene 沒有直接提供方法對文件進行更新,如果你需要更新一個文件,那麼你首先需要把這個文件從索引中刪除,然後把新版本的文件加入到索引中去。
提高索引效能
利用 Lucene,在建立索引的工程中你可以充分利用機器的硬體資源來提高索引的效率。當你需要索引大量的檔案時,你會注意到索引過程的瓶頸是在往磁碟上寫索引檔案的過程中。為了解決這個問題, Lucene 在記憶體中持有一塊緩衝區。但我們如何控制 Lucene 的緩衝區呢?幸運的是,Lucene 的類 IndexWriter 提供了三個引數用來調整緩衝區的大小以及往磁碟上寫索引檔案的頻率。
1.合併因子(mergeFactor)
這個引數決定了在 Lucene 的一個索引塊中可以存放多少文件以及把磁碟上的索引塊合併成一個大的索引塊的頻率。比如,如果合併因子的值是 10,那麼當記憶體中的文件數達到 10 的時候所有的文件都必須寫到磁碟上的一個新的索引塊中。並且,如果磁碟上的索引塊的隔數達到 10 的話,這 10 個索引塊會被合併成一個新的索引塊。這個引數的預設值是 10,如果需要索引的文件數非常多的話這個值將是非常不合適的。對批處理的索引來講,為這個引數賦一個比較大的值會得到比較好的索引效果。
2.最小合併文件數
這個引數也會影響索引的效能。它決定了記憶體中的文件數至少達到多少才能將它們寫回磁碟。這個引數的預設值是10,如果你有足夠的記憶體,那麼將這個值儘量設的比較大一些將會顯著的提高索引效能。
3.最大合併文件數
這個引數決定了一個索引塊中的最大的文件數。它的預設值是 Integer.MAX_VALUE,將這個引數設定為比較大的值可以提高索引效率和檢索速度,由於該引數的預設值是整型的最大值,所以我們一般不需要改動這個引數。
清單 5 列出了這個三個引數用法,清單 5 和清單 1 非常相似,除了清單 5 中會設定剛才提到的三個引數。
清單5:提高索引效能
/** * This class demonstrates how to improve the indexing performance * by adjusting the parameters provided by IndexWriter. */ public class AdvancedTextFileIndexer { public static void main(String[] args) throws Exception{ //fileDir is the directory that contains the text files to be indexed File fileDir = new File("C:\\files_to_index"); //indexDir is the directory that hosts Lucene's index files File indexDir = new File("C:\\luceneIndex"); Analyzer luceneAnalyzer = new StandardAnalyzer(); File[] textFiles = fileDir.listFiles(); long startTime = new Date().getTime(); int mergeFactor = 10; int minMergeDocs = 10; int maxMergeDocs = Integer.MAX_VALUE; IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,true); indexWriter.mergeFactor = mergeFactor; indexWriter.minMergeDocs = minMergeDocs; indexWriter.maxMergeDocs = maxMergeDocs; //Add documents to the index for(int i = 0; i < textFiles.length; i++){ if(textFiles[i].isFile() >> textFiles[i].getName().endsWith(".txt")){ Reader textReader = new FileReader(textFiles[i]); Document document = new Document(); document.add(Field.Text("content",textReader)); document.add(Field.Keyword("path",textFiles[i].getPath())); indexWriter.addDocument(document); } } indexWriter.optimize(); indexWriter.close(); long endTime = new Date().getTime(); System.out.println("MergeFactor: " + indexWriter.mergeFactor); System.out.println("MinMergeDocs: " + indexWriter.minMergeDocs); System.out.println("MaxMergeDocs: " + indexWriter.maxMergeDocs); System.out.println("Document number: " + textFiles.length); System.out.println("Time consumed: " + (endTime - startTime) + " milliseconds"); } }
通過這個例子,我們注意到在調整緩衝區的大小以及寫磁碟的頻率上面 Lucene 給我們提供了非常大的靈活性。現在我們來看一下程式碼中的關鍵語句。如下的程式碼首先建立了類 IndexWriter 的一個例項,然後對它的三個引數進行賦值。
int mergeFactor = 10; int minMergeDocs = 10; int maxMergeDocs = Integer.MAX_VALUE; IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,true); indexWriter.mergeFactor = mergeFactor; indexWriter.minMergeDocs = minMergeDocs; indexWriter.maxMergeDocs = maxMergeDocs;
下面我們來看一下這三個引數取不同的值對索引時間的影響,注意引數值的不同和索引之間的關係。我們為這個實驗準備了 10000 個測試文件。表 1 顯示了測試結果。
表1:測試結果
通過表 1,你可以清楚地看到三個引數對索引時間的影響。在實踐中,你會經常的改變合併因子和最小合併文件數的值來提高索引效能。只要你有足夠大的記憶體,你可以為合併因子和最小合併文件數這兩個引數賦儘量大的值以提高索引效率,另外我們一般無需更改最大合併文件數這個引數的值,因為系統已經預設將它設定成了最大。