Lucene -- 實時索引


 

 lucene的實時搜索可以分成:實時和近實時的搜索

實時只能依靠內存了。
近實時可以用lucene中提供org.apache.lucene.index.DirectoryReader.open(IndexWriter writer, boolean applyAllDeletes) throws IOException,可以在不十分影響性能的前提下,實現近實時的效果(比如每1s打開一次搜索,這類似於solr中的實現)。

 

 

 

 實時搜索

 lucene一般有ramdirectory和fsddirectory兩種方式存儲索引

第一個是內存方式,非常快,但沒有持久化;

第二個是硬盤方式,慢,但有持久化。  

 

 

Lucene 的事務性,使得Lucene 可以增量的添加一個段,我們知道,倒排索引是有一定的格式的,而這個格式一旦寫入是非常難以改變的,那么如何能夠增量建索引呢?
  Lucene 使用段這個概念解決了這個問題,對於每個已經生成的段,其倒排索引結構不會再改變,而增量添加的文檔添加到新的段中,段之間在一定的時刻進行合並,從而形成新的倒排索引結構。 Lucene 的事務性,使得Lucene 的索引不夠實時,如果想Lucene 實時,則必須新添加的文檔后IndexWriter 需要commit,在搜索的時候IndexReader 需要重新的打開,然而當索引在硬盤上的時候,尤其是索引非常大的時候,IndexWriter 的commit 操作和IndexReader 的open 操作都是非常慢的,根本達不到實時性的需要

 

 其實一般的應用,如果可以允許有1、2分鍾的延時,那么用fsddirectory就足夠了,每1分鍾增加索引並commit即可。
但是如果有需求,要實時搜索的話,那么就需要用ram和fsd兩種方式來組合使用了。

大致原理是用multireader組合多個索引的searcher即可
(multireader可以為實時搜索服務,也可用於分布式索引啊)
實時步驟是:

1、先打開fsdindex,用於搜索;如果新增文檔,則加入ramindex,並重打開ramsearcher。ram的重打開是很快的。
然后定時把ramindex寫入磁盤。
2、在寫入的時候,fsd需要commit並重新打開一個reader,這個時候需要新開一個ramindex。
在此時的搜索需要打開3個searcher,原ramsearcher,原fsdsearcher,新ramsearcher。
這個時候原ramindex寫入磁盤的時候,只要不commit就不會出現重復結果。
3、ramindex寫入磁盤結束,那么需要新打開一個fsdsearcher,這個過程是比較慢的。所以我們保持第2步的3個searcher先不變,繼續服務。
4、當心得fsdsearcher打開完畢,那么丟棄原fsdsearcher和原ramseacher。使用新的fsdsearcher和ramsearcher

這4步中的操作大多是原子性的,如果做了(2)但沒有做(3),如果來一個搜索,則將少看到一部分數據,如果做了(3)沒有做(2)則,多看到一部分數據。所以需要加一個同步鎖,以防數據異常。

 

 

 

 

 二、近實時搜索

 實現原理:
Near real time search的原理記錄在LUCENE-1313和LUCENE-1516里。

LUCENE-1313,在Index Writer內部維護了一個ram directory,在內存夠用前,flush和merge操作只是把數據更新到ram directory,只有Index Writer上的optimize和commit操作才會導致ram directory上的數據完全同步到文件。

LUCENE-1516,Index Writer提供了實時獲得reader的API,這個調用將導致flush操作,生成新的segment,但不會commit(fsync),從而減少 了IO。新的segment被加入到新生成的reader里。從返回的reader里,可以看到更新。所以,只要每次新的搜索都從Index Writer獲得一個新的reader,就可以搜索到最新的內容。這一操作的開銷僅僅是flush,相對commit來說,開銷很小。

Lucene的index組織方式為一個index目錄下的多個segment。新的doc會加入新的segment里,這些新的小segment每隔一段時間就合並起來。因為合並,總的segment數量保持的較小,總體search速度仍然很快。為了防止讀寫沖突,lucene只創建新的 segment,並在任何active的reader不在使用后刪除掉老的segment。
flush是把數據寫入到操作系統的緩沖區,只要緩沖區不滿,就不會有硬盤操作。
commit是把所有內存緩沖區的數據寫入到硬盤,是完全的硬盤操作。
optimize是對多個segment進行合並,這個過程涉及到老segment的重新讀入和新segment的合並,屬於CPU和IO-bound的
重量級操作。這是因為,Lucene索引中最主要的結構posting通過VINT和delta的格式存儲並緊密排列。合並時要對同一個term的posting進行歸並排序,是一個讀出,合並再生成的過程。


代碼解讀:
在IndexWriter獲得reader的方法中,主要調用了兩個方法doflush()和maybeMerge()。doflush() 將調用DocumentsWriter的flush方法,生成新的segment,返回的reader將能訪問到新的segment。 DocumentsWriter接收多個document添加,並寫入到同一個segment里。每一個加入的doc會經過多個DocConsumer組 成的流水線,他們包括StoredFieldsWriter(內部調用 FieldsWriter),TermVectorsTermsWriter,FreqProxTermsWriter,NormsWriter等。在外 界沒有主動調用flush的情況下,RAM buffer全用完了或者加入的doc數足夠大后,才會創建新的segment並flush到目錄中。
FreqProxTermsWriter調用TermHashPerField負責term的索引過程,當索引某字段詞項時,使用對應 TermsHashPerField的add()函數完成(一個)詞項索引過程,並將索引內容(詞項字符串/指針信息/位置信息等)存儲於內存緩沖中。中 間的過程使用了CharBlockPool,IntBlockPool,ByteBlockPool,只要內存夠用,可以不斷往后添加。
特性試驗:
設計一個文檔檢索程序,進程管理一個index writer和兩個線程,線程A負責新文檔的索引,線程B負責處理搜索請求,其中搜索時使用IndexWriter的新API獲取新的reader。通過交替的生成index和search的請求,觀察search的結果和索引目錄的變化。實驗結果如下:

1 打開indexwriter時,會生成一個lock文件
2 每次調用reader時,如果發生了更新,會先進行一次flush,把上次積攢在內存中的更新數據寫成新的segment,多出一個.cfs。
3 從新的reader中,可以讀到之前新加入的doc信息。
4 當新生成的segment達到十次后,會發生一次optimize,生成8個文件,為.fdt, .fdx, .frq, .fnm,
    .nrm, .prx, .tii, .tis。
5 當然,外界也可以主動觸發optimize,結果是一樣的。optimize前的多個segment的文件以及此前optimize的文件不再有用。
6 因為optimize生成cfs要消耗雙倍磁盤空間,並增加額外的處理時間,當optimize的index大小較大,超過了index總大小的10或者一個規定大小時,即使index   writer指定了CFS格式,optimize仍然會保留為多個文件的格式(LUCENE-2773)。
7 調用indexwriter的close方法,lock文件會被釋放,但除了optimize的結果文件外,此前生成的文件並不會被刪除。只到下次打開此index目錄時,不需要的文件才會被刪除。
8 當三種情況下,indexwriter會試圖刪除不需要的文件,on open,on flushing a new    segment,On finishing a merge。但如果當前打開的reader正在使用文件,則不會刪除。
9 因此,reader使用完后,一定要調用close方法,釋放不需要的文件。

 

 

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.Version;

public class NRT1 {

    /**
     * 用IndexReader.open(writer, false);實現近實時效果
     * 
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        Directory dir = new RAMDirectory();
        IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_41,
                new StandardAnalyzer(Version.LUCENE_41));
        IndexWriter w = new IndexWriter(dir, iwc);
        w.commit();

        FieldType doctype = new FieldType();
        doctype.setIndexed(true);
        doctype.setStored(true);

        Document doc = new Document();
        doc.add(new Field("title", "haha", doctype));

        DirectoryReader r = DirectoryReader.open(dir);
        for (int i = 0; i < 3; i++) {
            w.addDocument(doc);
            r = DirectoryReader.open(w, false);
// true:使得刪除可見(並不是寫入磁盤);false:刪除操作不可見,這會使得性能比true要高一些。
            System.out.println(r.numDocs());
        }
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM