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()); } } }