【搜索引擎】lucene事务
本文分兩部份,第一部份為譯:是對(duì)是對(duì)于lucene事務(wù)的一篇佳作《Transactional Lucene》的翻譯。第二部份為解:是本人對(duì)一文中提到一些概念在源碼層次的一些理解分析,參考lucene源碼版本為4.10.4。《Transactional Lucene》中還提到了多commit在實(shí)際生產(chǎn)中的一些妙用,值得參考。
一、譯
很多用戶并不了解Lucene API的事務(wù)語義及其在搜索應(yīng)用中的用途。對(duì)于初學(xué)者應(yīng)當(dāng)了解的Lucene ACID特性如下:
- Atomiciy 原子性
當(dāng)你在一次IndexWriter session中做操作(增加,刪除文檔),然后commit,要么你的所有的操作修改都是可見的(commit成功),要么所有的操作修改都不可見(commit失敗),絕不會(huì)處于某種中間狀態(tài)。有些方法有它自身的原子操作:如果你調(diào)用updateDocument方法,其內(nèi)在實(shí)現(xiàn)是先刪除后添加文檔,即使你打開了一個(gè)近實(shí)時(shí)(NRT)reader或者使用另一個(gè)線程做commit,絕不會(huì)出現(xiàn)只有刪除而沒有添加的情況。與此類似,如果使用addDocuments方法添加一組文檔,對(duì)于任何reader而言,要么所有的文檔可見,要么所有文檔不可見。
(對(duì)原文未提到的一點(diǎn)補(bǔ)充:通過Indexwriter.getReader獲得的Reader是能看到上次commit之后,IndexWriter執(zhí)行至當(dāng)前的所有變化的,在解的部份中將對(duì)其進(jìn)行詳細(xì)說明。) - Consistency 一致性
如果計(jì)算機(jī)或者OS奔潰,或者jvm掛掉或被殺死,亦或是電源被拔掉了,你的索引都會(huì)保持完好。注意,像RAM故障,cpu位翻轉(zhuǎn)或者文件系統(tǒng)損壞之類的問題,還是容易造成索引破壞的。 - Isolation 隔離性
當(dāng)IndexWriter正在做更改的時(shí)候,所有更改都不會(huì)對(duì)當(dāng)前搜索該索引的IndexReader可見,直到你commit或者打開了一個(gè)新的NRT reader。一次只能有一個(gè)IndexWriter實(shí)例對(duì)索引進(jìn)行更改。 - Durablity 持久性
一旦commit操作返回,所有變更都會(huì)被寫入到持久化存儲(chǔ)。如果計(jì)算機(jī)或者OS奔潰,或者jvm掛掉或被殺死,亦或是電源被拔掉了,所有的變更都已在索引中保存。
Lucene提供了二階段提交API: 調(diào)用prepareCommit方法,完成主要的提交工作(應(yīng)用緩存的delete操作,寫入被緩存中的文檔,fsync文件)。如果發(fā)生故障(例如,硬盤占滿),基本可以肯定它會(huì)發(fā)生在第一階段(prepareCommit階段)。然后,調(diào)用commit方法完成事務(wù)。
當(dāng)你調(diào)用IndexWriter的close方法時(shí),會(huì)自動(dòng)調(diào)用commit方法。如果你想要丟棄自上次commit以來的所有修改,可以調(diào)用rollback方法。你甚至可以對(duì)一次CREATE進(jìn)行rollback: 如果你有已經(jīng)有了一個(gè)index,你使用OpenMode.CREATE選項(xiàng)打開一個(gè)IndexWriter,然后再調(diào)用rollback,index會(huì)保持不變。同樣,你也可以調(diào)用deleteAll方法,然后rollback回來。
注意,僅在一個(gè)新的目錄上打開一個(gè)IndexWriter并不會(huì)產(chǎn)生一個(gè)空的commit操作,即你無法在這個(gè)目錄上打開一個(gè)IndexReader,直到你完成一次commit。
Lucene自身并未實(shí)現(xiàn)事務(wù)日志,但在更高層上可以方便實(shí)地現(xiàn)。例發(fā),Solr與ElasticSearch就實(shí)現(xiàn)了事務(wù)日志。
同一索引的多Commit
單個(gè)lucene索引可以保存多個(gè)commit,這是一個(gè)強(qiáng)大的特性,但往往容易被人們所忽視。每次commit都持有該commit被創(chuàng)建的時(shí)間點(diǎn)的過引索視圖。
這個(gè)特點(diǎn)類似于ZFS和新興的Btrfs這樣的現(xiàn)代文件系統(tǒng)的快照和寫克隆功能。事實(shí)上,Lucene是能夠暴露多次commit狀態(tài),其內(nèi)在的原理是:所有的索引分片和文件只會(huì)被寫一次,正如ZFS和Btrfs上的塊。
為了保存多次commit到你的索引中,只需要實(shí)現(xiàn)你自己的IndexDeletionPolicy,并將其傳遞給IndexWriter。Lucene正是通過這個(gè)類才知站定哪些commit是要被刪除的:IndexWriter會(huì)在打開索引和任何一次完成commit的時(shí)候調(diào)用它。KeepOnlyLastCommitDeletionPolicy是默認(rèn)的刪除機(jī)制實(shí)現(xiàn),它會(huì)刪除掉除了最近一次commit以外的所有commit。如果采用NoDeletionPolicy,那么每一次commit都會(huì)保存。
你可以在commit的時(shí)候傳送userData (Map<String,String>),用于記錄關(guān)于本次commit的一些用戶自定義信息(對(duì)Lucene不透明),然后使用IndexReader.listCommits方法獲得索引的所有commit信息。一旦你找到了一次commit,你可以在其上找開一個(gè)IndexReader對(duì)commit點(diǎn)上的索引數(shù)據(jù)進(jìn)行搜索。
你也可以基于之前的commit打開一個(gè)IndexWriter,回滾之后的所有變動(dòng)。這有點(diǎn)類似于rollback方法,不同之處在于它允許你在多個(gè)commit間rollback,而不僅是回滾當(dāng)前IndexWriter會(huì)話中所做的變動(dòng)。
當(dāng)你使用OpenMode.CREATE參數(shù)打開一個(gè)索引的時(shí)候,老的commit仍會(huì)保存。你也可使用OpenMode.CREATE,同時(shí)還在老的commit上用IndexReader進(jìn)行搜索。這使得一些有趣的應(yīng)用情影成為可能,例如,你可以在不影響當(dāng)前任何打開的reader前提下,在各個(gè)commit間做過索引全量重建工作。
組合上這些有意思的事務(wù)特征,你可以完成一些很酷的工作:
-
利用SnapShotDeletionPolicy或者PersistentSnapshotDeletionPolicy實(shí)現(xiàn)熱備:
these deletion policies make it trivial to take a "live" backup of the index without blocking ongoing changes with IndexWriter. 備份可以輕易的實(shí)現(xiàn)增量(只要復(fù)制新的文件,并去掉刪除文件),而你可以減少IO消耗以減輕對(duì)于搜索的干擾。 -
搜索不同版本的目錄
也許你在運(yùn)行一個(gè)商業(yè)網(wǎng)站,卻要輾轉(zhuǎn)于不同版本的目錄之間。在這種情況下,你就可以保存更老的commit,允許用戶選擇搜索哪個(gè)版本的目錄。 -
在同一個(gè)初始索引上進(jìn)行多次索引實(shí)驗(yàn):也許你想要在同一個(gè)大的索引上,運(yùn)行一系列的性能測試實(shí)驗(yàn),例如嘗試不的RAM緩存大小或者merge因子。要想如此,你可以在運(yùn)行完每次測試之后,不要關(guān)閉IndexWriter,而使用回滾方法快迅地恢得到初始狀態(tài),以備下次測試。
-
強(qiáng)制merge所有的片段到單一片段中,但依然保存之前的多片段時(shí)候的commit。如此,你就可以做多個(gè)段vs單一段的性能比較實(shí)驗(yàn)。
-
在NFS文件系統(tǒng)上做索引與搜索:
由于NFS無法保證當(dāng)前打開的文件不被刪除,你必須使用IndexDeletionPolicy來保存每次提交,直至所有的reader都不再使用該commit(即,重新在一個(gè)更新的commit上打開)。一種簡單的解決方案是基本于時(shí)間的,例如:不刪除該commit,直到創(chuàng)建該commit15分鐘之后,然后每5分鐘重新打開reader一次。不然你在NFS上進(jìn)行搜索時(shí),將會(huì)遇到各種可怕的異常。 -
分布式commit:
如果你有其他的資源,需要有l(wèi)ucene索引發(fā)生變化的同時(shí)提交,你就可以使用二階段提交API。這樣的做法很簡單,但容易在第二階段發(fā)生失敗; 例如lucene在其第二階段完成了提交,而數(shù)據(jù)庫在第二階段中發(fā)生了一些錯(cuò)誤、奔潰或者是斷電,這時(shí),你就可以通過在一個(gè)較早的commit上重新打開一個(gè)IndexWriter來rollback Lucene的這次commit。 -
進(jìn)行實(shí)驗(yàn)性的索引更新:也許你只想對(duì)索引的一個(gè)子集做一次re-indexing操作,但你又不確定這樣的操作是否會(huì)成功。這種情況下,只要保存老的commit,如果操作失敗,就執(zhí)行rollback,如果成功則刪除老的commit。
-
基于時(shí)間的快照:也許你想能夠更自由地回滾到1天前,一周前,一個(gè)月之前...的索引狀態(tài),就保可以根據(jù)這些時(shí)間點(diǎn),保存這些commit。
注意:保存多個(gè)版本的commit必然會(huì)帶來更多的磁盤空間消耗。然而,這些消耗往往會(huì)比較小,因?yàn)槎鄠€(gè)commit往往會(huì)共享一些索引片段,尤其是那些更大更早的片段。
二、 解
對(duì)于Lucene事務(wù)的原子性、隔離性以及近時(shí)實(shí)(NRT)搜索
上文在原子性的描述中提到“要么你的所有的操作修改都是可見的(commit成功),要么所有的操作修改都不可見(commit失敗),絕不會(huì)處于某種中間狀態(tài)。” 但沒有提到,通過Indexwriter.getReader獲得的Reader是能看到上次commit之后IndexWriter執(zhí)行到當(dāng)前的所有變化的。
When you ask for the IndexReader from the IndexWriter, the IndexWriter will be flushed (docs accumulated in RAM will be written to disk) but not committed (fsync files, write new segments file, etc).The returned IndexReader will search over previously committed segments, as well as the new, flushed but not committed segment. Because flushing will likely be processor rather than IO bound, this should be a process that can be attacked with more processor power if found to be too slow.
還有人們常使用openIfChanged方法來實(shí)現(xiàn)近實(shí)時(shí)搜索。我們來看看Lucene DirectoryReader的標(biāo)準(zhǔn)實(shí)現(xiàn)StandardDirectoryReader中的oldOpenIfChanged方法的實(shí)現(xiàn)。可以發(fā)現(xiàn)所謂的doOpenIfChanged內(nèi)部也是靠優(yōu)先嘗試從IndexWriter獲得DirectoryReader來實(shí)現(xiàn)的。
@Overrideprotected DirectoryReader doOpenIfChanged() throws IOException {return doOpenIfChanged((IndexCommit) null);}@Overrideprotected DirectoryReader doOpenIfChanged(final IndexCommit commit) throws IOException {ensureOpen();// If we were obtained by writer.getReader(), re-ask the// writer to get a new reader.if (writer != null) {return doOpenFromWriter(commit);} else {return doOpenNoWriter(commit);}}現(xiàn)在拋開近實(shí)時(shí)搜索,不使用該新特性,那么上文中作者提到的原子性特征是完備的。在不考慮merge的情況下,Lucene的每一次commit將內(nèi)存中積累的索引變更寫入到硬盤,形成新的索引分段,包括文檔刪除操作。這樣的好處在于,每一次commit不用去復(fù)雜又耗時(shí)地修改之前的索引分段,只要累加新文件即可。但這樣的后果是越來越多的分片存在,影響查詢效率,所以才需要merge機(jī)制的存在,不斷地去合并分段,這是另話了。
上文中所提的隔離性就是基于這種機(jī)制實(shí)現(xiàn)的,非NRT的正常reader打開時(shí),只能獲得當(dāng)前已commit到磁盤的分段文件信息的索引數(shù)據(jù),無法得到兩次commit的中間狀態(tài)。 而NRT的reader,可以獲得打開時(shí)IndexWriter所應(yīng)用的所有變更,但也無法感知到之后的IndexWriter所做的索引變化,除非重新打開。
至于“一次只能有一個(gè)IndexWriter實(shí)例對(duì)索引進(jìn)行更改”,這是靠Lucene實(shí)現(xiàn)的排它鎖實(shí)現(xiàn)的,你將會(huì)在你的索引目錄下看到write.lock文件的存在。
Lucene commit
Lucene多個(gè)commit的保存與刪除到底是怎么回事?如何基于commit打開索引? 這一系列的問題,還是得需要通過了解lucene如何保存每次commit才能理解。
我們首先看看一個(gè)IndexCommit的子類一般都包含哪些信息。一個(gè)IndexCommit實(shí)例就代表了一次索引commit。對(duì)索引內(nèi)容的任何變更只有在segments_N文件完成寫入之后才可見。
public abstract class IndexCommit implements Comparable<IndexCommit> {/*** Get the segments file (<code>segments_N</code>) associated * with this commit point.*/public abstract String getSegmentsFileName();/*** Returns all index files referenced by this commit point.*/public abstract Collection<String> getFileNames() throws IOException;/*** Returns the {@link Directory} for the index.*/public abstract Directory getDirectory();/*** Delete this commit point. This only applies when using* the commit point in the context of IndexWriter's* IndexDeletionPolicy.*/public abstract void delete();... }兩個(gè)重要的方法getSegmentsFileName與getFileNames。segmentsFileName是與當(dāng)前commit關(guān)聯(lián)的segments_N文件名。N是一個(gè)遞增的值,每次commit對(duì)應(yīng)的N都會(huì)增大。所以如果保存多個(gè)commit的話,自然會(huì)出現(xiàn)多個(gè)segments_N文件。segments_N具體格式內(nèi)容在此不作具體解釋,總體它記錄了lucene索引的所有分段的元數(shù)據(jù)信息,根據(jù)segments_N文件,可以清楚地知道當(dāng)前commit的索引內(nèi)容分布于哪些分段之中。getFileNames返回的是當(dāng)前commit所引用的所有文件。
如此每次某個(gè)commit上打開的IndexReader或IndexWriter就知道應(yīng)該去加載哪些文件。也正因如此,多個(gè)commit能夠共享一些老的索引分段,而不至于每個(gè)commit占用太大的存儲(chǔ)空間。當(dāng)涉及到commit刪除時(shí),由于lucene對(duì)索引文件的刪除是通過引用計(jì)數(shù)的方式實(shí)現(xiàn)的,只要對(duì)commit引用的文件調(diào)用一次IndexFileDeleter.decRef(Collection?files)方法即可。只有引用計(jì)數(shù)為0的文件才會(huì)真正地被刪除。
Luncene IndexDeletionPolicy
IndexDeletionPolicy它所能做的是在兩個(gè)方法時(shí)間結(jié)點(diǎn)上對(duì)IndexCommit做刪除。定義如下:
public abstract class IndexDeletionPolicy {protected IndexDeletionPolicy() {}public abstract void onInit(List<? extends IndexCommit> commits) throws IOException;public abstract void onCommit(List<? extends IndexCommit> commits) throws IOException; }onInit方法只在IndexWriter初始化時(shí)被調(diào)用,onCommit在每次commit操作的時(shí)候被調(diào)用。commits列表中包含了當(dāng)前所有的commit點(diǎn),按從老到新的順序排列。
默認(rèn)實(shí)現(xiàn)KeepOnlyLastCommitDeletionPolicy刪除上一次commit以外的所有commit相關(guān)文件(減少引用計(jì)數(shù))。
SnapshotDeletionPolicy是采用wrapper模式,對(duì)現(xiàn)有IndexDeletionPolicy的一層封裝。除了onInit和onCommit方法外,它還提供了snapshot和release兩個(gè)方法。snapshot得到的IndexCommit將不會(huì)被刪除,直到其被release,所以比較適用于備份的場影,在備份之前調(diào)用snapshot,直到備份完成,再調(diào)用release。
SnapshotDeletionPolicy只在內(nèi)存中保存snapShot信息,如果要保證數(shù)據(jù)持久化不丟失,可使用PersistentSnapshotDeletionPolicy。
Lucene二階段提交的實(shí)現(xiàn)
IndexWirter繼承了TwoPhaseCommit接口,實(shí)現(xiàn)三個(gè)方法:prepareCommit,commit與rollback。
- prepareCommit,完成二階段提交第一階段的工作,它會(huì)盡可能多的完成更新工作,但又避免完成真實(shí)的提交。你可以輕松地利用rollback廢棄掉當(dāng)前階段完成的所有工作。 事實(shí)上本次commit所產(chǎn)生的段文件,已寫入存儲(chǔ)。
- commit方法是完成第二階段的工作,它只作很少的工作,只有該方滿返回,調(diào)用者才能確認(rèn)索引相應(yīng)操作已完成,并持久化到存儲(chǔ)。跟蹤代碼直至SegmentIfnos的finishCommit方法,可見commit成功的情況下,只做了兩件事情,一是在segments_N未填入4byte的校驗(yàn)合,還有就是close寫入流完成fsync。新的commit,只有在校驗(yàn)和正確的情況下對(duì)IndexReader可見。
- rollback:廢棄掉上次commit以來的所有變更操作。
20151005首發(fā)于3dobe.com
鏈接地址:http://3dobe.com/archives/172/
本文分兩部份,第一部份為譯:是對(duì)是對(duì)于lucene事務(wù)的一篇佳作《Transactional Lucene》的翻譯。第二部份為解:是本人對(duì)一文中提到一些概念在源碼層次的一些理解分析,參考lucene源碼版本為4.10.4。《Transactional Lucene》中還提到了多commit在實(shí)際生產(chǎn)中的一些妙用,值得參考。
一、譯
很多用戶并不了解Lucene API的事務(wù)語義及其在搜索應(yīng)用中的用途。對(duì)于初學(xué)者應(yīng)當(dāng)了解的Lucene ACID特性如下:
- Atomiciy 原子性
當(dāng)你在一次IndexWriter session中做操作(增加,刪除文檔),然后commit,要么你的所有的操作修改都是可見的(commit成功),要么所有的操作修改都不可見(commit失敗),絕不會(huì)處于某種中間狀態(tài)。有些方法有它自身的原子操作:如果你調(diào)用updateDocument方法,其內(nèi)在實(shí)現(xiàn)是先刪除后添加文檔,即使你打開了一個(gè)近實(shí)時(shí)(NRT)reader或者使用另一個(gè)線程做commit,絕不會(huì)出現(xiàn)只有刪除而沒有添加的情況。與此類似,如果使用addDocuments方法添加一組文檔,對(duì)于任何reader而言,要么所有的文檔可見,要么所有文檔不可見。
(對(duì)原文未提到的一點(diǎn)補(bǔ)充:通過Indexwriter.getReader獲得的Reader是能看到上次commit之后,IndexWriter執(zhí)行至當(dāng)前的所有變化的,在解的部份中將對(duì)其進(jìn)行詳細(xì)說明。) - Consistency 一致性
如果計(jì)算機(jī)或者OS奔潰,或者jvm掛掉或被殺死,亦或是電源被拔掉了,你的索引都會(huì)保持完好。注意,像RAM故障,cpu位翻轉(zhuǎn)或者文件系統(tǒng)損壞之類的問題,還是容易造成索引破壞的。 - Isolation 隔離性
當(dāng)IndexWriter正在做更改的時(shí)候,所有更改都不會(huì)對(duì)當(dāng)前搜索該索引的IndexReader可見,直到你commit或者打開了一個(gè)新的NRT reader。一次只能有一個(gè)IndexWriter實(shí)例對(duì)索引進(jìn)行更改。 - Durablity 持久性
一旦commit操作返回,所有變更都會(huì)被寫入到持久化存儲(chǔ)。如果計(jì)算機(jī)或者OS奔潰,或者jvm掛掉或被殺死,亦或是電源被拔掉了,所有的變更都已在索引中保存。
Lucene提供了二階段提交API: 調(diào)用prepareCommit方法,完成主要的提交工作(應(yīng)用緩存的delete操作,寫入被緩存中的文檔,fsync文件)。如果發(fā)生故障(例如,硬盤占滿),基本可以肯定它會(huì)發(fā)生在第一階段(prepareCommit階段)。然后,調(diào)用commit方法完成事務(wù)。
當(dāng)你調(diào)用IndexWriter的close方法時(shí),會(huì)自動(dòng)調(diào)用commit方法。如果你想要丟棄自上次commit以來的所有修改,可以調(diào)用rollback方法。你甚至可以對(duì)一次CREATE進(jìn)行rollback: 如果你有已經(jīng)有了一個(gè)index,你使用OpenMode.CREATE選項(xiàng)打開一個(gè)IndexWriter,然后再調(diào)用rollback,index會(huì)保持不變。同樣,你也可以調(diào)用deleteAll方法,然后rollback回來。
注意,僅在一個(gè)新的目錄上打開一個(gè)IndexWriter并不會(huì)產(chǎn)生一個(gè)空的commit操作,即你無法在這個(gè)目錄上打開一個(gè)IndexReader,直到你完成一次commit。
Lucene自身并未實(shí)現(xiàn)事務(wù)日志,但在更高層上可以方便實(shí)地現(xiàn)。例發(fā),Solr與ElasticSearch就實(shí)現(xiàn)了事務(wù)日志。
同一索引的多Commit
單個(gè)lucene索引可以保存多個(gè)commit,這是一個(gè)強(qiáng)大的特性,但往往容易被人們所忽視。每次commit都持有該commit被創(chuàng)建的時(shí)間點(diǎn)的過引索視圖。
這個(gè)特點(diǎn)類似于ZFS和新興的Btrfs這樣的現(xiàn)代文件系統(tǒng)的快照和寫克隆功能。事實(shí)上,Lucene是能夠暴露多次commit狀態(tài),其內(nèi)在的原理是:所有的索引分片和文件只會(huì)被寫一次,正如ZFS和Btrfs上的塊。
為了保存多次commit到你的索引中,只需要實(shí)現(xiàn)你自己的IndexDeletionPolicy,并將其傳遞給IndexWriter。Lucene正是通過這個(gè)類才知站定哪些commit是要被刪除的:IndexWriter會(huì)在打開索引和任何一次完成commit的時(shí)候調(diào)用它。KeepOnlyLastCommitDeletionPolicy是默認(rèn)的刪除機(jī)制實(shí)現(xiàn),它會(huì)刪除掉除了最近一次commit以外的所有commit。如果采用NoDeletionPolicy,那么每一次commit都會(huì)保存。
你可以在commit的時(shí)候傳送userData (Map<String,String>),用于記錄關(guān)于本次commit的一些用戶自定義信息(對(duì)Lucene不透明),然后使用IndexReader.listCommits方法獲得索引的所有commit信息。一旦你找到了一次commit,你可以在其上找開一個(gè)IndexReader對(duì)commit點(diǎn)上的索引數(shù)據(jù)進(jìn)行搜索。
你也可以基于之前的commit打開一個(gè)IndexWriter,回滾之后的所有變動(dòng)。這有點(diǎn)類似于rollback方法,不同之處在于它允許你在多個(gè)commit間rollback,而不僅是回滾當(dāng)前IndexWriter會(huì)話中所做的變動(dòng)。
當(dāng)你使用OpenMode.CREATE參數(shù)打開一個(gè)索引的時(shí)候,老的commit仍會(huì)保存。你也可使用OpenMode.CREATE,同時(shí)還在老的commit上用IndexReader進(jìn)行搜索。這使得一些有趣的應(yīng)用情影成為可能,例如,你可以在不影響當(dāng)前任何打開的reader前提下,在各個(gè)commit間做過索引全量重建工作。
組合上這些有意思的事務(wù)特征,你可以完成一些很酷的工作:
-
利用SnapShotDeletionPolicy或者PersistentSnapshotDeletionPolicy實(shí)現(xiàn)熱備:
these deletion policies make it trivial to take a "live" backup of the index without blocking ongoing changes with IndexWriter. 備份可以輕易的實(shí)現(xiàn)增量(只要復(fù)制新的文件,并去掉刪除文件),而你可以減少IO消耗以減輕對(duì)于搜索的干擾。 -
搜索不同版本的目錄
也許你在運(yùn)行一個(gè)商業(yè)網(wǎng)站,卻要輾轉(zhuǎn)于不同版本的目錄之間。在這種情況下,你就可以保存更老的commit,允許用戶選擇搜索哪個(gè)版本的目錄。 -
在同一個(gè)初始索引上進(jìn)行多次索引實(shí)驗(yàn):也許你想要在同一個(gè)大的索引上,運(yùn)行一系列的性能測試實(shí)驗(yàn),例如嘗試不的RAM緩存大小或者merge因子。要想如此,你可以在運(yùn)行完每次測試之后,不要關(guān)閉IndexWriter,而使用回滾方法快迅地恢得到初始狀態(tài),以備下次測試。
-
強(qiáng)制merge所有的片段到單一片段中,但依然保存之前的多片段時(shí)候的commit。如此,你就可以做多個(gè)段vs單一段的性能比較實(shí)驗(yàn)。
-
在NFS文件系統(tǒng)上做索引與搜索:
由于NFS無法保證當(dāng)前打開的文件不被刪除,你必須使用IndexDeletionPolicy來保存每次提交,直至所有的reader都不再使用該commit(即,重新在一個(gè)更新的commit上打開)。一種簡單的解決方案是基本于時(shí)間的,例如:不刪除該commit,直到創(chuàng)建該commit15分鐘之后,然后每5分鐘重新打開reader一次。不然你在NFS上進(jìn)行搜索時(shí),將會(huì)遇到各種可怕的異常。 -
分布式commit:
如果你有其他的資源,需要有l(wèi)ucene索引發(fā)生變化的同時(shí)提交,你就可以使用二階段提交API。這樣的做法很簡單,但容易在第二階段發(fā)生失敗; 例如lucene在其第二階段完成了提交,而數(shù)據(jù)庫在第二階段中發(fā)生了一些錯(cuò)誤、奔潰或者是斷電,這時(shí),你就可以通過在一個(gè)較早的commit上重新打開一個(gè)IndexWriter來rollback Lucene的這次commit。 -
進(jìn)行實(shí)驗(yàn)性的索引更新:也許你只想對(duì)索引的一個(gè)子集做一次re-indexing操作,但你又不確定這樣的操作是否會(huì)成功。這種情況下,只要保存老的commit,如果操作失敗,就執(zhí)行rollback,如果成功則刪除老的commit。
-
基于時(shí)間的快照:也許你想能夠更自由地回滾到1天前,一周前,一個(gè)月之前...的索引狀態(tài),就保可以根據(jù)這些時(shí)間點(diǎn),保存這些commit。
注意:保存多個(gè)版本的commit必然會(huì)帶來更多的磁盤空間消耗。然而,這些消耗往往會(huì)比較小,因?yàn)槎鄠€(gè)commit往往會(huì)共享一些索引片段,尤其是那些更大更早的片段。
二、 解
對(duì)于Lucene事務(wù)的原子性、隔離性以及近時(shí)實(shí)(NRT)搜索
上文在原子性的描述中提到“要么你的所有的操作修改都是可見的(commit成功),要么所有的操作修改都不可見(commit失敗),絕不會(huì)處于某種中間狀態(tài)。” 但沒有提到,通過Indexwriter.getReader獲得的Reader是能看到上次commit之后IndexWriter執(zhí)行到當(dāng)前的所有變化的。
When you ask for the IndexReader from the IndexWriter, the IndexWriter will be flushed (docs accumulated in RAM will be written to disk) but not committed (fsync files, write new segments file, etc).The returned IndexReader will search over previously committed segments, as well as the new, flushed but not committed segment. Because flushing will likely be processor rather than IO bound, this should be a process that can be attacked with more processor power if found to be too slow.
還有人們常使用openIfChanged方法來實(shí)現(xiàn)近實(shí)時(shí)搜索。我們來看看Lucene DirectoryReader的標(biāo)準(zhǔn)實(shí)現(xiàn)StandardDirectoryReader中的oldOpenIfChanged方法的實(shí)現(xiàn)。可以發(fā)現(xiàn)所謂的doOpenIfChanged內(nèi)部也是靠優(yōu)先嘗試從IndexWriter獲得DirectoryReader來實(shí)現(xiàn)的。
@Overrideprotected DirectoryReader doOpenIfChanged() throws IOException {return doOpenIfChanged((IndexCommit) null);}@Overrideprotected DirectoryReader doOpenIfChanged(final IndexCommit commit) throws IOException {ensureOpen();// If we were obtained by writer.getReader(), re-ask the// writer to get a new reader.if (writer != null) {return doOpenFromWriter(commit);} else {return doOpenNoWriter(commit);}}現(xiàn)在拋開近實(shí)時(shí)搜索,不使用該新特性,那么上文中作者提到的原子性特征是完備的。在不考慮merge的情況下,Lucene的每一次commit將內(nèi)存中積累的索引變更寫入到硬盤,形成新的索引分段,包括文檔刪除操作。這樣的好處在于,每一次commit不用去復(fù)雜又耗時(shí)地修改之前的索引分段,只要累加新文件即可。但這樣的后果是越來越多的分片存在,影響查詢效率,所以才需要merge機(jī)制的存在,不斷地去合并分段,這是另話了。
上文中所提的隔離性就是基于這種機(jī)制實(shí)現(xiàn)的,非NRT的正常reader打開時(shí),只能獲得當(dāng)前已commit到磁盤的分段文件信息的索引數(shù)據(jù),無法得到兩次commit的中間狀態(tài)。 而NRT的reader,可以獲得打開時(shí)IndexWriter所應(yīng)用的所有變更,但也無法感知到之后的IndexWriter所做的索引變化,除非重新打開。
至于“一次只能有一個(gè)IndexWriter實(shí)例對(duì)索引進(jìn)行更改”,這是靠Lucene實(shí)現(xiàn)的排它鎖實(shí)現(xiàn)的,你將會(huì)在你的索引目錄下看到write.lock文件的存在。
Lucene commit
Lucene多個(gè)commit的保存與刪除到底是怎么回事?如何基于commit打開索引? 這一系列的問題,還是得需要通過了解lucene如何保存每次commit才能理解。
我們首先看看一個(gè)IndexCommit的子類一般都包含哪些信息。一個(gè)IndexCommit實(shí)例就代表了一次索引commit。對(duì)索引內(nèi)容的任何變更只有在segments_N文件完成寫入之后才可見。
public abstract class IndexCommit implements Comparable<IndexCommit> {/*** Get the segments file (<code>segments_N</code>) associated * with this commit point.*/public abstract String getSegmentsFileName();/*** Returns all index files referenced by this commit point.*/public abstract Collection<String> getFileNames() throws IOException;/*** Returns the {@link Directory} for the index.*/public abstract Directory getDirectory();/*** Delete this commit point. This only applies when using* the commit point in the context of IndexWriter's* IndexDeletionPolicy.*/public abstract void delete();... }兩個(gè)重要的方法getSegmentsFileName與getFileNames。segmentsFileName是與當(dāng)前commit關(guān)聯(lián)的segments_N文件名。N是一個(gè)遞增的值,每次commit對(duì)應(yīng)的N都會(huì)增大。所以如果保存多個(gè)commit的話,自然會(huì)出現(xiàn)多個(gè)segments_N文件。segments_N具體格式內(nèi)容在此不作具體解釋,總體它記錄了lucene索引的所有分段的元數(shù)據(jù)信息,根據(jù)segments_N文件,可以清楚地知道當(dāng)前commit的索引內(nèi)容分布于哪些分段之中。getFileNames返回的是當(dāng)前commit所引用的所有文件。
如此每次某個(gè)commit上打開的IndexReader或IndexWriter就知道應(yīng)該去加載哪些文件。也正因如此,多個(gè)commit能夠共享一些老的索引分段,而不至于每個(gè)commit占用太大的存儲(chǔ)空間。當(dāng)涉及到commit刪除時(shí),由于lucene對(duì)索引文件的刪除是通過引用計(jì)數(shù)的方式實(shí)現(xiàn)的,只要對(duì)commit引用的文件調(diào)用一次IndexFileDeleter.decRef(Collection?files)方法即可。只有引用計(jì)數(shù)為0的文件才會(huì)真正地被刪除。
Luncene IndexDeletionPolicy
IndexDeletionPolicy它所能做的是在兩個(gè)方法時(shí)間結(jié)點(diǎn)上對(duì)IndexCommit做刪除。定義如下:
public abstract class IndexDeletionPolicy {protected IndexDeletionPolicy() {}public abstract void onInit(List<? extends IndexCommit> commits) throws IOException;public abstract void onCommit(List<? extends IndexCommit> commits) throws IOException; }onInit方法只在IndexWriter初始化時(shí)被調(diào)用,onCommit在每次commit操作的時(shí)候被調(diào)用。commits列表中包含了當(dāng)前所有的commit點(diǎn),按從老到新的順序排列。
默認(rèn)實(shí)現(xiàn)KeepOnlyLastCommitDeletionPolicy刪除上一次commit以外的所有commit相關(guān)文件(減少引用計(jì)數(shù))。
SnapshotDeletionPolicy是采用wrapper模式,對(duì)現(xiàn)有IndexDeletionPolicy的一層封裝。除了onInit和onCommit方法外,它還提供了snapshot和release兩個(gè)方法。snapshot得到的IndexCommit將不會(huì)被刪除,直到其被release,所以比較適用于備份的場影,在備份之前調(diào)用snapshot,直到備份完成,再調(diào)用release。
SnapshotDeletionPolicy只在內(nèi)存中保存snapShot信息,如果要保證數(shù)據(jù)持久化不丟失,可使用PersistentSnapshotDeletionPolicy。
Lucene二階段提交的實(shí)現(xiàn)
IndexWirter繼承了TwoPhaseCommit接口,實(shí)現(xiàn)三個(gè)方法:prepareCommit,commit與rollback。
- prepareCommit,完成二階段提交第一階段的工作,它會(huì)盡可能多的完成更新工作,但又避免完成真實(shí)的提交。你可以輕松地利用rollback廢棄掉當(dāng)前階段完成的所有工作。 事實(shí)上本次commit所產(chǎn)生的段文件,已寫入存儲(chǔ)。
- commit方法是完成第二階段的工作,它只作很少的工作,只有該方滿返回,調(diào)用者才能確認(rèn)索引相應(yīng)操作已完成,并持久化到存儲(chǔ)。跟蹤代碼直至SegmentIfnos的finishCommit方法,可見commit成功的情況下,只做了兩件事情,一是在segments_N未填入4byte的校驗(yàn)合,還有就是close寫入流完成fsync。新的commit,只有在校驗(yàn)和正確的情況下對(duì)IndexReader可見。
- rollback:廢棄掉上次commit以來的所有變更操作。
總結(jié)
以上是生活随笔為你收集整理的【搜索引擎】lucene事务的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【链接保存】十分钟上手sklearn:安
- 下一篇: 【转载保存】修改IK分词器源码实现动态加