lucene索引研究

架構概覽

圖一顯示了 Lucene 的索引機制的架構。Lucene 使用各種解析器對各種不同類型的文檔進行解析。比如對於 HTML 文檔,HTML 解析器會做一些預處理的工作,比如過濾文檔中的 HTML 標籤等等。HTML 解析器的輸出的是文本內容,接着 Lucene 的分詞器(Analyzer)從文本內容中提取出索引項以及相關信息,比如索引項的出現頻率。接着 Lucene 的分詞器把這些信息寫到索引文件中。

圖一: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:測試結果

通過表 1,你可以清楚地看到三個參數對索引時間的影響。在實踐中,你會經常的改變合併因子和最小合併文檔數的值來提高索引性能。只要你有足夠大的內存,你可以爲合併因子和最小合併文檔數這兩個參數賦儘量大的值以提高索引效率,另外我們一般無需更改最大合併文檔數這個參數的值,因爲系統已經默認將它設置成了最大。

Lucene 索引文件結構分析

在分析 Lucene 的索引文件結構之前,我們先要理解反向索引(Inverted index)這個概念,反向索引是一種以索引項爲中心來組織文檔的方式,每個索引項指向一個文檔序列,這個序列中的文檔都包含該索引項。相反,在正向索引中,文檔佔據了中心的位置,每個文檔指向了一個它所包含的索引項的序列。你可以利用反向索引輕鬆的找到那些文檔包含了特定的索引項。Lucene正是使用了反向索引作爲其基本的索引結構。

索引文件的邏輯視圖

在Lucene 中有索引塊的概念,每個索引塊包含了一定數目的文檔。我們能夠對單獨的索引塊進行檢索。圖 2 顯示了 Lucene 索引結構的邏輯視圖。索引塊的個數由索引的文檔的總數以及每個索引塊所能包含的最大文檔數來決定。

圖2:索引文件的邏輯視圖 圖2:索引文件的邏輯視圖 

Lucene 中的關鍵索引文件

下面的部分將會分析Lucene中的主要的索引文件,可能分析有些索引文件的時候沒有包含文件的所有的字段,但不會影響到對索引文件的理解。

1.索引塊文件

這個文件包含了索引中的索引塊信息,這個文件包含了每個索引塊的名字以及大小等信息。表 2 顯示了這個文件的結構信息。

表2:索引塊文件結構 表2:索引塊文件結構

2.域信息文件

我們知道,索引中的文檔由一個或者多個域組成,這個文件包含了每個索引塊中的域的信息。表 3 顯示了這個文件的結構。

表3:域信息文件結構 表3:域信息文件結構

3.索引項信息文件

這是索引文件裏面最核心的一個文件,它存儲了所有的索引項的值以及相關信息,並且以索引項來排序。表 4 顯示了這個文件的結構。

表4:索引項信息文件結構 表4:索引項信息文件結構

4.頻率文件

這個文件包含了包含索引項的文檔的列表,以及索引項在每個文檔中出現的頻率信息。如果Lucene在索引項信息文件中發現有索引項和搜索詞相匹配。那麼 Lucene 就會在頻率文件中找有哪些文件包含了該索引項。表5顯示了這個文件的一個大致的結構,並沒有包含這個文件的所有字段。

表5:頻率文件的結構 表5:頻率文件的結構

5.位置文件

這個文件包含了索引項在每個文檔中出現的位置信息,你可以利用這些信息來參與對索引結果的排序。表 6 顯示了這個文件的結構

表6:位置文件的結構 表6:位置文件的結構

到目前爲止我們介紹了 Lucene 中的主要的索引文件結構,希望能對你理解 Lucene 的物理的存儲結構有所幫助。

總結

目前已經有非常多的知名的組織正在使用 Lucene,比如,Lucene 爲 Eclipse 的幫助系統,麻省理工學院的 OpenCourseWare 提供了搜索功能。通過閱讀這篇文章,希望你能對 Lucene 的索引機制有所瞭解,並且你會發現利用 Lucene 創建索引是非常簡單的事情。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章