Lucene.Net初識(1)

Lucene.Net 系列一本文介紹了什麼是Lucene,Lucene能做什麼.

如何從一個文件夾下的所有txt文件中查找特定的詞?

本文將圍繞該個實例介紹了lucene.net的索引的建立以及如何針對索引進行搜索.最後還將給出源代碼供大家學習.

What’s Lucene
Lucene是一個信息檢索的函數庫(Library),利用它你可以爲你的應用加上索引和搜索的功能.

Lucene的使用者不需要深入瞭解有關全文檢索的知識,僅僅學會使用庫中的一個類,你就爲你的應用實現全文檢索的功能.

不過千萬別以爲Lucene是一個象google那樣的搜索引擎,Lucene甚至不是一個應用程序,它僅僅是一個工具,一個Library.你也可以把它理解爲一個將索引,搜索功能封裝的很好的一套簡單易用的API.利用這套API你可以做很多有關搜索的事情,而且很方便.

What Can Lucene Do

Lucene可以對任何的數據做索引和搜索. Lucene不管數據源是什麼格式,只要它能被轉化爲文字的形式,就可以被Lucene所分析利用.也就是說不管是MS word, Html ,pdf還是其他什麼形式的文件只要你可以從中抽取出文字形式的內容就可以被Lucene所用.你就可以用Lucene對它們進行索引以及搜索.

How To Use Lucene --- A Simple Example
示例介紹:

爲作爲輸入參數的文件夾下的所有txt類型的文件做索引,做好的索引文件放入index文件夾.

然後在索引的基礎上對文件進行全文搜索.

1.       建立索引
IndexWriter writer = new IndexWriter("index", new StandardAnalyzer(), true);
IndexDocs(writer, new System.IO.FileInfo(args[0]));              
writer.Optimize();
writer.Close();

IndexWriter是對索引進行寫操作的一個類,利用它可以創建一個索引對象然後往其中添加文件.需要注意它並不是唯一可以修改索引的類.在索引建好後利用其他類還可以對其進行修改.

構造函數第一個參數是建立的索引所要放的文件夾的名字.第二個參數是一個分析對象,主要用於從文本中抽取那些需要建立索引的內容,把不需要參與建索引的文本內容去掉.比如去掉一些a the之類的常用詞,還有決定是否大小寫敏感.不同的選項通過指定不同的分析對象控制.第三個參數用於確定是否覆蓋原有索引的.

第二步就是利用這個writer往索引中添加文件.具體後面再說.

第三步進行優化.

第四步關閉writer.

下面具體看看第二步:

public static void IndexDirectory(IndexWriter writer, FileInfo file)
         {
              if (Directory.Exists(file.FullName))
              {
                   String[] files = Directory.GetFileSystemEntries(file.FullName);
                   // an IO error could occur
                   if (files != null)
                   {
                       for (int i = 0; i < files.Length; i++)
                       {
                            IndexDirectory(writer, new FileInfo(files[i]));  //這裏是一個遞歸
                       }
                   }
              }
              else if (file.Extension == ".txt")
              {
                   IndexFile(file, writer);
              }
         }

         private static void IndexFile(FileInfo file, IndexWriter writer)
         {
              Console.Out.WriteLine("adding " + file);
              try
              {
                   Document doc = new Document();                   
                   doc.Add(Field.Keyword("filename", file.FullName));

                   doc.Add(Field.Text("contents", new StreamReader(file.FullName)));

                   writer.AddDocument(doc);
              }
              catch (FileNotFoundException fnfe)
              {
              }
     }

主要就是兩個函數一個用於處理文件夾(不是爲文件夾建立索引),一個用於真正爲文件建立索引.

因此主要集中看一下IndexFile這個方法.首先建立Document對象,然後爲Document對象添加一些屬性Field.你可以把Document對象看成是虛擬文件,將來將從此獲取信息.而Field則看成是描述此虛擬文件的元數據(metadata).

其中Field包括四個類型:

Keywork

該類型的數據將不被分析,而會被索引並保存保存在索引中.

UnIndexed

該類型的數據不會被分析也不會被索引,但是會保存在索引.

UnStored

和UnIndexed剛好相反,被分析被索引,但是不被保存.

Text

和UnStrored類似.如果值的類型爲string還會被保存.如果值的類型Reader就不會被保存和UnStored一樣.

最後將每一個Document添加到索引當中.

需要注意的是索引不僅可以建立在文件系統上,也可以建立在內存中.

例如

IndexWriter writer = new IndexWriter("index", new StandardAnalyzer(), true);

在第一個參數不是指定文件夾的名字而是使用Directory對象,並使用它的子類RAMDirectory,就可以將索引建立在內存當中.

2.       對索引進行搜索

IndexSearcher indexSearcher= new IndexSearcher(indexDir);
Query query = QueryParser.Parse(queryString, "contents",new StandardAnalyzer());
Hits hits = indexSearcher.Search(query);

第一步利用IndexSearcher打開索引文件用於後面搜索,其中的參數是索引文件的路徑.

第二步使用QueryParser將可讀性較好的查詢語句(比如查詢的詞lucene ,以及一些高級方式lucene AND .net)轉化爲Lucene內部使用的查詢對象.

第三步執行搜索.並將結果返回到hits集合.需要注意的是Lucene並不是一次將所有的結果放入hits中而是採取一次放一部分的方式.出於空間考慮.

作者 idior

2005-03-16 22:36

Lucene.net 系列二 --- index 一詳細介紹了有關Lucene.net索引添加刪除更新的詳細內容.並給出了所有的TestCase供學習參考.

Lucene建立Index的過程:

1.       抽取文本.

比如將PDF以及Word中的內容以純文本的形式提取出來.Lucene所支持的類型主要爲String,爲了方便同時也支持Date 以及Reader.其實如果使用這兩個類型lucene會自動進行類型轉換.

2.       文本分析.

   Lucene將針對所給的文本進行一些最基本的分析,並從中去除一些不必要的信息,比如一些常用字a ,an, the 等等,如果搜索的時候不在乎字母的大小寫, 又可以去掉一些不必要的信息.總而言之你可以把這個過程想象成一個文本的過濾器,所有的文本內容通過分析, 將過濾掉一些內容,剩下最有用的信息.

3.       寫入index.

和google等常用的索引技術一樣lucene在寫index的時候都是採用的倒排索引技術(inverted index.) 簡而言之,就是通過某種方法(類似hash表?)將常見的”一篇文檔中含有哪些詞”這個問題轉成”哪篇文檔中有這些詞”. 而各個搜索引擎的索引機制的不同主要在於如何爲這張倒排表添加更準確的描述.比如google有名的PageRank因素.Lucene當然也有自己的技術,希望在以後的文章中能爲大家加以介紹.

在上一篇文章中,使用了最基本的建立索引的方法.在這裏將對某些問題加以詳細的討論.

1. 添加Document至索引
上次添加的每份文檔的信息是一樣的,都是文檔的filename和contents.

doc.Add(Field.Keyword("filename", file.FullName));
doc.Add(Field.Text("contents", new StreamReader(file.FullName)));

在Lucene中對每個文檔的描述是可以不同的,比如,兩份文檔都是描述一個人,其中一個添加的是name, age 另一個添加的是id, sex ,這種不規則的文檔描述在Lucene中是允許的.
還有一點Lucene支持對Field進行Append , 如下:

string baseWord = "fast";
string synonyms[] = String {"quick", "rapid", "speedy"};
Document doc = new Document();
doc.Add(Field.Text("word", baseWord));
for (int i = 0; i < synonyms.length; i++)
    doc.Add(Field.Text("word", synonyms[i]));

這點純粹是爲了方便用戶的使用.在內部Lucene自動做了轉化,效果和將它們拼接好再存是一樣.

2. 刪除索引中的文檔

這一點Lucene所採取的方式比較怪,它使用IndexReader來對要刪除的項進行標記,然後在Reader Close的時候一起刪除.
這裏簡要介紹幾個方法.

[TestFixture]
public class DocumentDeleteTest : BaseIndexingTestCase   // BaseIndexingTestCase
中的SetUp方法                                               //建立了索引其中加入了兩個Document
{
    [Test]
    public void testDeleteBeforeIndexMerge()
    {
        IndexReader reader = IndexReader.Open(dir);  //
當前索引中有兩個Document

        Assert.AreEqual(2, reader.MaxDoc());   //文檔從0開始計數,MaxDoc表示下一個文檔的序號

        Assert.AreEqual(2, reader.NumDocs());  //NumDocs表示當前索引中文檔的個數
        reader.Delete(1);                   //
對標號爲1的文檔標記爲待刪除,邏輯刪除
        Assert.IsTrue(reader.IsDeleted(1));         //檢測某個序號的文檔是否被標記刪除
        Assert.IsTrue(reader.HasDeletions());       //檢測索引中是否有Document被標記刪除
        Assert.AreEqual(2, reader.MaxDoc());        //
當前下一個文檔序號仍然爲2
        Assert.AreEqual(1, reader.NumDocs());       //
當前索引中文檔數變成1
        reader.Close();                             //
此時真正從物理上刪除之前被標記的文檔
        reader = IndexReader.Open(dir);
        Assert.AreEqual(2, reader.MaxDoc());        
        Assert.AreEqual(1, reader.NumDocs());
        reader.Close();
    }

    [Test]
    public void DeleteAfterIndexMerge()    //
在索引重排之後
    {
        IndexReader reader = IndexReader.Open(dir);
        Assert.AreEqual(2, reader.MaxDoc());
        Assert.AreEqual(2, reader.NumDocs());
        reader.Delete(1);
        reader.Close();
        IndexWriter writer = new IndexWriter(dir, GetAnalyzer(), false);
        writer.Optimize();                 //
索引重排
        writer.Close();
        reader = IndexReader.Open(dir);
        Assert.IsFalse(reader.IsDeleted(1));
        Assert.IsFalse(reader.HasDeletions());
        Assert.AreEqual(1, reader.MaxDoc());       //
索引重排後,下一個文檔序號變爲1
        Assert.AreEqual(1, reader.NumDocs());
        reader.Close();
    }
}

當然你也可以不通過文檔序號進行刪除工作.採用下面的方法,可以從索引中刪除包含特定的內容文檔.

IndexReader reader = IndexReader.Open(dir);
reader.Delete(new Term("city", "Amsterdam"));
reader.Close();

你還可以通過reader.UndeleteAll()這個方法取消前面所做的標記,即在read.Close()調用之前取消所有的刪除工作

3. 更新索引中的文檔

這個功能Lucene沒有支持, 只有通過刪除後在添加來實現. 看看代碼,很好理解的.

[TestFixture]
public class DocumentUpdateTest : BaseIndexingTestCase
{
    [Test]
    public void Update()
    {
        Assert.AreEqual(1, GetHitCount("city", "Amsterdam"));
        IndexReader reader = IndexReader.Open(dir);
        reader.Delete(new Term("city", "Amsterdam"));
        reader.Close();
        Assert.AreEqual(0, GetHitCount("city", "Amsterdam"));
        IndexWriter writer = new IndexWriter(dir, GetAnalyzer(),false);
        Document doc = new Document();
        doc.Add(Field.Keyword("id", "1"));

        doc.Add(Field.UnIndexed("country", "Netherlands"));
        doc.Add(Field.UnStored("contents","Amsterdam has lots of bridges"));
        doc.Add(Field.Text("city", "Haag"));
        writer.AddDocument(doc);
        writer.Optimize();
        writer.Close();
        Assert.AreEqual(1, GetHitCount("city", "Haag"));
    }

    protected override Analyzer GetAnalyzer()
    {
        return new WhitespaceAnalyzer();  //
注意此處如果用SimpleAnalyzer搜索會失敗,因爲建立索引的時候使用的SimpleAnalyse它會將所有字母變成小寫.

    }

private int GetHitCount(String fieldName, String searchString)
    {
        IndexSearcher searcher = new IndexSearcher(dir);
        Term t = new Term(fieldName, searchString);
        Query query = new TermQuery(t);
        Hits hits = searcher.Search(query);
        int hitCount = hits.Length();
        searcher.Close();
        return hitCount;
    }
}

需要注意的是以上所有有關索引的操作,爲了避免頻繁的打開和關閉Writer和Reader.又由於添加和刪除是不同的連接(Writer, Reader)做的.所以應該儘可能的將添加文檔的操作放在一起批量執行,然後將刪除文檔的操作也放在一起批量執行.避免添加刪除交替進行.

Lucene.net 系列三 --- index 本文將進一步討論有關Lucene.net建立索引的問題:

主要包含以下主題:
1.索引的權重
2.利用IndexWriter 屬性對建立索引進行高級管理
3.利用RAMDirectory充分發揮內存的優勢
4.利用RAMDirectory並行建立索引
5.控制索引內容的長度
6.Optimize 優化的是什麼?

本文將進一步討論有關Lucene.net建立索引的問題:

索引的權重
根據文檔的重要性的不同,顯然對於某些文檔你希望提高權重以便將來搜索的時候,更符合你想要的結果. 下面的代碼演示瞭如何提高符合某些條件的文檔的權重.

比如對公司內很多的郵件做了索引,你當然希望主要查看和公司有關的郵件,而不是員工的個人郵件.這點根據郵件的地址就可以做出判斷比如包含@alphatom.com的就是公司郵件,而@gmail.com等等就是私人郵件.如何提高相應郵件的權重? 代碼如下:

     public static  String COMPANY_DOMAIN = "alphatom.com";
     Document doc = new Document();
     String senderEmail = GetSenderEmail();
     String senderName = getSenderName();
     String subject = GetSubject();
     String body = GetBody();
     doc.Add(Field.Keyword("senderEmail”, senderEmail));
     doc.Add(Field.Text("senderName", senderName));
     doc.Add(Field.Text("subject", subject));
     doc.Add(Field.UnStored("body", body));

     if (GetSenderDomain().EndsWith(COMPANY_DOMAIN)) 

    //如果是公司郵件,提高權重,默認權重是1.0
           doc.SetBoost(1.5);                      
     else                         //如果是私人郵件,降低權重.
           doc.SetBoost(0.1);

     writer.AddDocument(doc);

不僅如此你還可以對Field也設置權重.比如你對郵件的主題更感興趣.就可以提高它的權重.   

Field senderNameField = Field.Text("senderName", senderName);

     Field subjectField = Field.Text("subject", subject);
     subjectField.SetBoost(1.2);
lucene搜索的時候會對符合條件的文檔按匹配的程度打分,這點就和google的PageRank有點類似, 而SetBoost中的Boost就是其中的一個因素,當然還有其他的因素.這要放到搜索裏再說.

利用IndexWriter 變量對建立索引進行高級管理
在建立索引的時候對性能影響最大的地方就是在將索引寫入文件的時候, 所以在具體應用的時候就需要對此加以控制.

在建立索引的時候對性能影響最大的地方就是在將索引寫入文件的時候所以在具體應用的時候就需要對此加以控制

IndexWriter屬性

默認值

描述

MergeFactory

10

控制segment合併的頻率和大小

MaxMergeDocs

Int32.MaxValue

限制每個segment中包含的文檔數

MinMergeDocs

10

當內存中的文檔達到多少的時候再寫入segment

Lucene默認情況是每加入10份文檔就從內存往index文件寫入並生成一個segement,然後每10個segment就合併成一個segment.通過MergeFactory這個變量就可以對此進行控制.

MaxMergeDocs用於控制一個segment文件中最多包含的Document數.比如限制爲100的話,即使當前有10個segment也不會合並,因爲合併後的segmnet將包含1000個文檔,超過了限制.

MinMergeDocs用於確定一個當內存中文檔達到多少的時候才寫入文件,該項對segment的數量和大小不會有什麼影響,它僅僅影響內存的使用,進一步影響寫索引的效率.

爲了生動的體現這些變量對性能的影響,用一個小程序對此做了說明.

這裏有點不可思議.Lucene in Action書上的結果比我用dotLucene做的結果快了近千倍.這裏給出書中用Lucene的數據,希望大家比較一下看看是不是我的問題.

Lucene in Action書中的數據:

% java lia.indexing.IndexTuningDemo 100000 10 9999999 10
Merge factor: 10
Max merge docs: 9999999
Min merge docs: 10
Time: 74136 ms
% java lia.indexing.IndexTuningDemo 100000 100 9999999 10
Merge factor: 100
Max merge docs: 9999999
Min merge docs: 10
Time: 68307 ms
我的數據: 336684128 ms
可以看出MinMergeDocs(主要用於控制內存)和MergeFactory(控制合併的次數和合並後的大小) 對建立索引有顯著的影響.但是並不是MergeFactory越大越好,因爲如果一個segment的文檔數很多的話,在搜索的時候必然也會影響效率,所以這裏MergeFactory的取值是一個需要平衡的問題.而MinMergeDocs主要受限於內存.

利用RAMDirectory充分發揮內存的優勢

從上面來看充分利用內存的空間,減少讀寫文件(寫入index)的次數是優化建立索引的重要方法.其實在Lucene中提供了更強大的方法來利用內存建立索引.使用RAMDirectory來替代FSDirectory. 這時所有的索引都將建立在內存當中,這種方法對於數據量小的搜索業務很有幫助,同時可以使用它來進行一些小的測試,避免在測試時頻繁建立刪除索引文件.

在實際應用中RAMDirectory和FSDirectory協作可以更好的利用內存來優化建立索引的時間.

具體方法如下:

1.建立一個使用FSDirectory的IndexWriter

2 .建立一個使用RAMDirectory的IndexWriter

3 把Document添加到RAMDirectory中

4 當達到某種條件將RAMDirectory 中的Document寫入FSDirectory.

5 重複第三步

示意代碼:
private FSDirectory fsDir = FSDirectory.GetDirectory("index",true);

private RAMDirectory ramDir = new RAMDirectory();

       private IndexWriter fsWriter = IndexWriter(fsDir,new SimpleAnalyzer(), true);
       private IndexWriter ramWriter = new IndexWriter(ramDir,new SimpleAnalyzer(), true);
       while (there are documents to index)
      {
         ramWriter.addDocument(doc);
         if (condition for flushing memory to disk has been met)
         {
           fsWriter.AddIndexes(Directory[]{ramDir}) ;
           ramWriter.Close();          //why not support flush?
           ramWriter =new IndexWriter(ramDir,new SimpleAnalyzer(),true);
         }
     }

這裏的條件完全由用戶控制,而不是FSDirectory採用對Document計數的方式控制何時寫入文件.相比之下有更大的自由性,更能提升性能.

利用RAMDirectory並行建立索引

RAMDirectory還提供了使用多線程來建立索引的可能性.下面這副圖很好的說明了這一點.

甚至你可以在一個高速的網絡裏使用多臺計算機來同時建立索引.就像下面這種圖所示.

雖然有關並行同步的問題需要你自己進行處理,不過通過這種方式可以大大提高對大量數據建立索引的能力.

控制索引內容的長度.

在我的一篇速遞介紹過Google Desktop Search只能搜索到文本中第5000個字的.也就是google在建立索引的時候只考慮前5000個字,在Lucene中同樣也有這個配置功能.

Lucene對一份文本建立索引時默認的索引長度是10,000. 你可以通過IndexWriter 的MaxFieldLength屬性對此加以修改.還是用一個例子說明問題. 

[Test]
     public void FieldSize()      
     // AddDocuments 和 GetHitCount都是自定義的方法,詳見源代碼
     {
         AddDocuments(dir, 10);      
         //第一個參數是目錄,第二個配置是索引的長度
         Assert.AreEqual(1, GetHitCount("contents", "bridges"))
         //原文檔的contents爲”Amsterdam has lots of bridges”
         //當索引長度爲10個字時能找到bridge
         AddDocuments(dir, 1);
         Assert.AreEqual(0, GetHitCount("contents", "bridges"));
         //當索引長度限制爲1個字時就無法發現第5個字bridges
     }

對索引內容限長往往是處於效率和空間大小的考慮.能夠對此進行配置是建立索引必備的一個功能.

Optimize 優化的是什麼?

在以前的例子裏,你可能已經多次見過writer.Optimize()這段代碼.Optimize到底做了什麼?

讓你吃驚的是這裏的優化對於建立索引不僅沒有起到加速的作用,反而是延長了建立索引的時間.爲什麼?

因爲這裏的優化不是爲建立索引做的,而是爲搜索做的.之前我們提到Lucene默認每遇到10個Segment就合併一次,儘管如此在索引完成後仍然會留下幾個segmnets,比如6,7.

而Optimize的過程就是要減少剩下的Segment的數量,儘量讓它們處於一個文件中.

它的過程很簡單,就是新建一個空的Segmnet,然後把原來的幾個segmnet全合併到這一個segmnet中,在此過程中,你的硬盤空間會變大,因爲同時存在兩份一樣大小的索引.不過在優化完成後,Lucene會自動將原來的多份Segments刪除,只保留最後生成的一份包含原來所有索引的segment.

儘量減少segments的個數主要是爲了增加查詢的效率.假設你有一個Server,同時有很多的Client建立了各自不同的索引,如果此時搜索,那麼必然要同時打開很多的索引文件,這樣顯然會受到很大的限制,對性能產生影響.

當然也不是隨時做Optimize就好,如前所述做優化時要花費更多的時間和空間,而且在做優化的時候是不能進行查詢的.所以索引建立的後期,並且索引的內容不會再發生太多的變化的時候做優化是一個比較好的時段. 

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