Lucene學習筆記(二)--------構建索引

構建索引

對搜索內容建模

文檔和域

文檔是索引和搜索的原子單位,包含一個或多個域的容器,域則依次包含“真正的”被搜索內容。每個域都有一個標識名稱(即一個文本值或二進制值)。將文檔加入到索引中時,可以通過一系列選項控制Lucene的行爲。在對原始數據進行索引操作時,先將數據轉換成Lucene所能識別的文檔和域,搜索過程中被搜索對象爲閾值。

Lucene可以針對域進行3種操作:

  • 閾值可以被索引(或者不被索引),要搜索一個域就必須先進行索引。且被索引的域必須是文本格式的(二進制格式的域值只能被存儲不能被索引),索引一個域時,首先需要使用分析過程將域值轉換成語彙單元,然後將語彙單元加入到索引中

  • 域被索引後,還可以選擇性的存儲項向量,後者可以看作該域的一個小型反向索引集合,通過該向量能夠檢索該域的所有語彙單元,這個機制可以幫助實現一些高級功能,比如搜索與當前文檔相似的文檔

  • 域值可以被單獨存儲,即被分析前的域值備份也可以寫進索引中

當搜索程序通過索引檢索文檔時,只有被存儲的域纔會被作爲搜索結果展現。被索引但未被存儲於文檔的域是不會被作爲搜索結果展示的

與數據庫不同,Lucene沒有一個確定的全局模式,即加入索引的每個文檔都是獨立的,與此前加入的文檔完全沒有關係:它可以包含任意的域,以及任意的索引、存儲和項向量操作選項,不必包含與其他文檔相同的域,甚至可以內容相同僅是相關操作選項有所區別

這種特性保證了可以遞歸訪問文檔並建立對應的索引,可以隨時對文檔進行索引,不必提前設計文檔的數據結構表,若隨後想向文檔中添加域,可以完成添加後重新索引該文檔或重建索引即可。

Lucene與數據庫的第二個區別是,Lucene要求在進行索引操作時簡單化或反向規格化原始數據。

反向規格化(Denormalization)是爲了解決文檔真實結構和Lucene表示能力之間的“不匹配”問題。

提取文本和創建文檔

使用Lucene索引數據時,必須先從數據中提取純文本格式信息,以便Lucene識別該文本並建立對應的Lucene文檔。

分析文檔

建立起Lucene文檔和域,就可以調用IndexWriter對象的addDocument方法將數據傳遞給Lucene進行索引操作。索引操作時,首先分析文本,將文本數據分割成語彙單元串,然後對它們執行一些可選操作。
- LowerCaseFilter可以將詞彙單元在索引前統一轉換爲小寫,以使搜索不對大小寫敏感
- StopFilter類從輸入中去掉一些使用很頻繁卻沒有實際意義的詞(a、an、the、in、on、so on等)
- PorterStemFilter可以去掉英文詞的詞幹

這些將原始數據轉換爲語彙單元,隨後用一系列filter來修正該語彙單元的操作,一起構成了分析器。還可以通過鏈接Lucene的詞彙單元和filter自定義分析器或通過其他方式自定義分析器

向索引添加文檔

對輸入數據分析完成,Lucene將分析結果以倒排索引(inverted index)的方式寫入索引文件中存儲。在進行關鍵字快速查找時,倒排索引能有效利用磁盤空間

Lucene使用倒排數據結構的原因是:把文檔中提取出來的詞彙單元作爲查詢關鍵字。

索引段
  • segment_N: —->Segment0, Segment1, Segment2, Segment3,….

Lucene索引都包含一個或多個段,每個段都是一個獨立的索引,包含整個文檔索引的一個子集。每當writer刷新緩衝區增加的文檔,以及掛起目錄刪除操作時,索引文件都會建立一個新段。在搜索索引時,每個段都是單獨訪問的,但搜索結果是合併後返回的。

每個段都包含多個文件,文件格式爲_X.,X代表段名稱,是擴展名,用來標識該文件對應索引的某個部分。各個獨立的文件共同組成了索引的不同部分(項向量、存儲的域、倒排索引等)。

如果使用混合文件格式(Lucene的默認處理方式,但可以通過IndexWriter.setUseCompoundFile方法進行修改),上述索引文件會被壓縮成一個單一的文件:_X.cfs。這種方式的好處是能在搜索期間減少打開的文件數量。

還有一個段文件,用段_標識,該文件指向所有激活的段。Lucene會首先打開該文件,然後打開它所指向的其他文件。值被稱爲“the generation”,它是一個整數,Lucene每次向索引提交更改時都會將這個數加1.

久而久之,索引會積聚很多段,尤其當程序打開和關閉writer較爲頻繁時。IndexWriter類會週期性地選擇一些段,然後將它們合併到一個新段中,然後刪除老的段。被合併的段選取策略由一個獨立的MergePolicy類主導。一旦選取好這些段,具體合併操作由MergeScheduler類實現。

基本索引操作

向索引添加文檔

添加文檔的方法有兩個:

  • addDocument(Document)——使用默認分析器添加文檔,在創建IndexWriter對象時指定分析器,用於詞彙單元化操作

  • addDocument(Document,Analyzer)——使用指定的分析器添加文檔和詞彙單元化操作,但要注意分析器在搜索時能夠匹配索引時生成的詞彙單元才能正常工作

IndexWriter類初始化方法並不顯式包含索引是否已創建的布爾值,它在初始化時會首先檢查傳入的Directory類是否已包含索引,如果索引存在,IndexWriter類則在該索引上追加內容,否則則向Directory類寫入新創建的索引。

IndexWriter類有多個初始化方法。其中一些方法會顯式包含創建索引的參數,這允許強制建立新的索引並覆蓋原來的索引。

一旦建立起索引,就可以用for循環來初始化文檔對象:首先創建一個新的Document空對象,然後根據需要向這個Documnet對象中逐個添加Field對象。每個文檔都有4個域,每個域都有各自不同的選項。最後調用writer.addDocument方法來索引文檔。

刪除索引中的文檔

IndexWriter類提供了各種方法從索引中刪除文檔:

  • deleteDocument(Term):刪除包含項的所有文檔
  • deleteDocument(Term[]):刪除包含項數組任一元素的所有文檔
  • deleteDocument(Query):刪除匹配查詢語句的所有文檔
  • deleteDocument(Query[]):刪除匹配查詢語句數組任一元素的所有文檔
  • deleteAll():刪除索引中所有文檔

如果需要通過Term類刪除單個文檔,需要確認在每個文檔中都已索引過對應的Field類,還要確認所有域值都是唯一的。還可以對這個域進行任意命名(通常用ID命名),該域需要被索引成未被分析的域以保證分析器不會將它分解成語彙單元,然後利用該域來刪除對應文檔:

writer.deleteDocument(new Term("ID", documentID));

在所有情況下,刪除操作不會馬上執行,而是放入內存緩衝區,與加入文檔的操作類似,Lucene會通過週期性刷新文檔目錄來執行該操作。不過即使刪除操作已經完成,存儲該文檔的磁盤空間也不會馬上釋放,Lucene只是將該文檔標記爲“刪除”。

  • writer.hasDeletions()方法用於檢查索引中是否包含被標記爲已刪除的文檔
  • IndexWriter.maxDoc()返回索引中被刪除和未被刪除的文檔數
  • IndexWriter.numDocs()返回索引中未被刪除的文檔總數

更新索引中的文檔

Lucene做不到只更新文檔中的部分域,只能刪除整個舊文檔,然後向索引中添加新文檔。這要求新文檔必須包含舊文檔中所有域,包括內容未發生改變的域。IndexWriter提供了兩個方法更新索引中的文檔

  • updateDocument(Term, Document)首先刪除包含Term變量的所有文檔,然後使用writer的默認分析器添加新文檔
  • updateDocument(Term, Document, Analyzer)功能與上述一致,區別在於可以指定分析器添加文檔

這兩個方法是通過調用deleteDocument(Term)和addDocument兩個方法合併實現的。

writer.updateDocument(new Term("ID", documentId), newDocument);

域選項

當創建好一個域時,可以指定多個域選項來控制Lucene在將文檔添加進索引後針對該域的行爲。域選項分爲三類:索引選項、存儲選項、項向量使用選項。

域索引選項(Field.Index.*)

通過倒排索引來控制域文本是否可被搜索

  • Index.ANALYZED:使用分析器將域值分解成獨立的詞彙單元流,並使每個詞彙單元能被搜索。該選項適用於普通文本域(如正文、標題、摘要等)

  • Index.NOT_ANALYZED:對域進行索引,但不對String值進行分析。該操作實際上將域值作爲單一詞彙單元並使之能被搜索。該選項適用於索引那些不能被分解的域值,如URL、文件路徑、日期、人名、社保號碼和電話號碼等。該選項尤其適用於“精確匹配”搜索。

  • Index.ANALYZED_NO_NORMS:這是Index.ANALYZED的一個變體,它不會在索引中存儲norms信息。norms記錄了索引中的index-time boost信息,但是當進行搜索時可能會比較耗費內存。

  • Index.NOT_ANALYZED_NO_NORMS:不存儲norms信息。該選項用於在搜索期間節省索引空間和減少內存耗費,因爲single-token域並不需要norms信息,除非它們已經被進行加權操作

  • Index.NO:使對應的域值不被搜索

當Lucene建立起倒排索引後,默認情況下會保存所有必要信息以實施Vector Space Model。該Model需要計算文檔中出現的term數,以及它們出現的位置(通過詞組搜索時用到)
但有些時候這些域只是在布爾搜索時用到,並不爲相關評分做貢獻,例如,域只是被用作過濾,如權限過濾和日期過濾。這時,可以通過調用Field.setOmitTermFreqAndPositions(true)方法讓Lucene跳過對該項的出現頻率和出現位置的索引。該方法可以節省一些索引在磁盤上的存儲空間,還可以加速搜索和過濾過程,但會悄悄地阻止需要位置的搜索,如PhraseQuery和SpanQuery類的運行

域存儲選項

域存儲選項(Field.Store.*)用來確定是否需要存儲域的真實值,以便後續搜索時能恢復這個值。

  • Stroe.YES——存儲域值,此時,原始的字符串值全部被保存在索引中,並可以由IndexReder類恢復。該選項對需要展示搜索結果的一些域很有用(如URL、標題或數據庫主鍵)。不要存儲太大的值,會消耗掉索引的存儲空間。
  • Store.NO——不存儲域值。該選項通常跟Index.ANALYZED選項共同用來索引大的文本域值,通常這些域值不用恢復爲初始格式,如Web頁面的正文,或其他類型的文本文檔

可以使用CompressionTools在存儲域值之前對它進行壓縮,該類提供靜態方法壓縮和解壓字節數組。該類運行時會在後臺調用Java內置的java.util.Zip類。該方法可以爲索引節省一些空間,但節省幅度有限,且會降低索引和搜索速度。

域的項向量選項

項向量是介於索引域和存儲域的一箇中間結構

索引完文檔,如果希望在搜索期間該文檔所有的唯一項能完全從文檔域中檢索,可以在存儲的域中加快高亮顯示匹配的詞彙單元,還可以使用鏈接“找到類似的文檔”,當運行一個新的點擊搜索時,使用原始文檔中突出的項。其他解決方法是對文檔進行自動分類。

Reader、TokenStream和byte[]域值

Field對象還有幾個其他初始化方法,允許傳入除String以外的其他參數。
- Field(String name, Reader value, TermVector termVector)方法使用Reader而不是String對象來表示域值。這時是不能存儲域值的(域存儲項被硬編碼成Store.NO),並且該域會一直用於分析和索引(Index.ANALYZED)。

  • Field(String name, Reader value),與前述方法類似,使用Reader而不是String對象來表示域值,但使用該方法時,默認的termVector爲TermVector.NO

  • Field(String name, TokenStream tokenStream, TermVector termVector):允許程序對域值進行預分析並生成TokenStream對象,這個域不會被存儲並將一直用於分析和索引

  • Field(String name, TokenStream tokenStream):允許程序對域值進行預分析並生成TokenStream對象,但使用該方法時默認的termVector爲TermVector.NO

  • Field(String name, byte[] value, Store store):用來存儲二進制域,例如不用參與索引的域(Index.NO)和沒有項向量的域(TermVector.NO)。其中store參數必須設置爲Store.YES

  • Field(String name, byte[] value, int offset, int length, Store store):也能對二進制域進行索引,與前一個的區別在於該方法允許對這個二進制的部分片段進行引用,該片段的起始位置可以用offset參數表示,處理長度可以用參數length對應的字節數來表示

域排序選項

Lucene返回匹配搜索條件的文檔時,一般是按照默認評分對文檔進行排序的。如果域是數值類型的,在將它加入文檔進行排序時,要用NumericField類來表示。如果域是文本類型的,如郵件發送者姓名,得用Field類表示它和索引它,並且要用Field.Index.NOT_ANALYZED選項避免對它進行分析。如果域未進行加權操作,那麼索引時就不能帶有norm選項,使用Field.Index.NOT_ANALYZED_NO_NORMS,可以節省磁盤空間和內存空間

new Field("auth", "Arthur C.Clark", Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS);

用於排序的域必須進行索引,而且每個對應文檔必須包含一個詞彙單元,通常這意味着使用Field.Index.NOT_ANALYZED或者Field.Index.NOT_ANALYZED_NO_NORMS(如果沒對文檔或域進行加權的話)選項,但若分析器只生成一個詞彙單元,比如KeywordAnalyzer、Field.Index.ANALYZED或Field.Index.ANALYZED_NO_NORMS選項也可以使用

多值域

對於存在多個值的域,可以向這個域中寫入幾個不同的值:

Document doc = new Document();
for(String author : authors){
    doc.add(new Field("author", author, Field.Store.YES, Field.Index.ANALYZED));
}

在程序內部,只要文檔中出現同名的多值域,倒排索引和項向量都會在邏輯上將這些域的詞彙單元附加進去,具體順序由添加該域的順序決定。然而,與索引操作不同,它們在文檔中的存儲順序是分離的,因此搜索期間對文檔進行檢索時,會發現有多個Field實例

對文檔和域進行加權操作

加權操作可以在索引期間完成也可以在搜索期間完成。

文檔加權操作

通過修改文檔的加權因子,就能指示Lucene在計算相關性時或多或少考慮到該文檔針對索引中其他文檔的重要程度。

調用加權操作的API只包含一個方法:setBoost(float):

Document doc = new Document();
String senderEmail = getSenderEmail();
String senderName = getSenderName();
String subject = getSubject();
String body = getBody();
doc.add(new Field("senderEmail", senderEmail, Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("senderName", senderName, Field.Store.YES, Field.Index.ANALYZED));
doc.add(new Field("body", body, Field.Store.NO, Field.Index.ANALYZED));
String lowerDomain = getSenderDomain().toLowerCase();
if(isImportant(lowerDomain)){
    doc.setBoost(1.5F);
}else if(isUnimportant(lowerDomain)){
    doc.setBoost(0.1F);
}
writer.addDocument(doc);
域加權操作

匹配搜索時,如何才能讓主題域比senderName域更重要呢?
可以使用Field類的setBoost(float)方法:

Field subjectField = new Field("subject", subject, Field.Store.YES, Field.Index.ANALYZED);
subjectField.setBoost(1.2F);

當你改變一個域或者一個文檔的加權因子時,必須完全刪除並創建對應的文檔,或者使用updateDocument方法達到同樣效果。

較短的域有一個隱含的加權,這取決於Lucene的評分算法具體實現。當進行索引操作時,IndexWriter對象會調用Similarity.lengthNorm方法來實現該算法。也可以用自定義邏輯覆蓋它,具體可以實現自己的Similarity類並且告訴IndexWriter類通過調用自己的setSimilarity類來覆蓋。

加權基準(Norms)

索引期間,文檔中域的所有加權都被合併成一個單一的浮點數。除了域,文檔也有自己的加權值,Lucene會基於域的詞彙單元數量自動計算出這些加權值(更短的域有更高的加權),將加權值合併,並編碼(量化)成一個單一的字節值,作爲域或文檔信息的一部分存儲起來。搜索期間,被搜索域的norms都被加載到內存,並被解碼還原爲浮點數,然後用於計算相關性得分(relevance score)

雖然norms是在索引期間首次進行計算的,後續還是可以使用IndexReader的setNorms方法(要求程序驗算自身norms因子,用於動態計算加權因子的強大方法,如文檔更新或用點擊表示受歡迎程度等)對它進行修改的。

爲了防止norms在搜索期間佔用大量內存,可以關閉norms相關操作,方法是使用Field.Index中的NO_NORMS索引選項,或是對包含該域的文檔進行索引前調用Field.setOmitNorms(true)。因爲norms的全部數組需要加載到RAM時,需要對被搜索文檔的每個域都分配一個字節空間。如果是文檔中包含多個域的大索引,這個加載操作會很快佔用大量RAM空間。

Lucene並不對norms進行鬆散存儲,如果索引過程中有一個包含了norms選項,隨後的段合併中所有文檔都會有至少一個字節的norms空間。

索引數字、日期和時間

索引數字

當lucene索引數字時,會在索引中建立一個複雜數據結構(Rich Data Structure)。

索引數字

索引數字分兩種情況,一種是數字內嵌在將要索引的文本中,要想保留這些數字,並將它們作爲單獨的詞彙單元處理,這樣就可以在隨後的搜索過程中用到,實現這樣的索引,需要選擇一個不丟棄數字的分析器。WhitespaceAnalyzer和StandardAnalyzed兩個類均可以做到。SimapleAnalyzed和StopAnalyzer兩個類會將詞彙單元流中的數字剔除。

另一種場景是域中只包含數字,希望可以作爲數字域值來索引,並能在搜索和排序中對它們進行精確(相等)匹配。創建一個NumericField對象,使用其中一個setValue方法(該方法支持的數字類型有int、long、float和double,然後返回自身)記錄數值,然後將NumericField類加入到文檔:

doc.add(new NumericField("price").setDoubleValue(19.99));

每個數值都用trie structure進行索引,邏輯上爲越來越大的預定義括號數分配了一個單一的數值。針對每個括號都在索引中分配了一個唯一的項,因此能夠很快地在所有文檔中檢索這個單一的括號。搜索期間,搜索請求的範圍被轉換成等效的括號並集,這樣就能實現高效的範圍搜索或過濾功能。

每個NumericField實例只接受單一數值,但還是可以向文檔中添加多個帶有相同域名的實例。最後生成的NumericRangeQuery和NumericRangeFilter實例會將所有值用邏輯“or”連接起來。但這會對排序造成不確定影響。如果需要針對一個域進行排序,那就必須對只出現一次該域的各個NumericField進行索引。

NumericField類還能處理日期和時間,方法是將它們轉換成等效的int型或long型。

索引日期和時間

首先將日期和時間轉換成相等的int或long型值,然後將這些值作爲數字進行索引。具體可以使用Date.getTime獲取精確到毫秒的數字值:

doc.add(new NumericField("timestamp").setLongValue(new Date().getTime()));

如果並不需要精確到毫秒的日期,可以直接用除法向下量化到秒、分、小時或天。

doc.add(new NumericField("day").setIntValue(new Date().getTime()/24/3600));

如果要進一步量化到年月,或需要索引一天中的小時或一週中的日期,可以創建一個Calendar實例,並從中獲取相關值:

Calendar cal = Calendar.getInstance();
cal.setTime(date);
doc.add(new NumericField("dayofMonth").setIntValue(cal.get(Calendar.DAY_OF_MONTH)));

域截取(Field truncation)

你可能只想對每個文檔的前面200個單詞進行索引。
爲了支持這些不同的索引需求,IndexWriter允許對域進行截取後再索引它們,被分析的域只有前面N個項會被編入索引。實例化IndexWriter後,必須向其傳入MaxFieldLength實例向程序傳遞具體的截取數量。MaxFieldLength類提供兩個易用的默認實例:MaxFieldLength.UNLIMITED(不採取截取策略)和MaxFieldLength.LIMITED(截取域中前1000個項),實例化MaxFieldLength時還可以設置所需的截取數。

建立IndexWriter之後,可以調用setMaxFieldLength方法在任意時刻調整截取限制,getMaxFieldLength可以檢索當前的截取限制。

如果文檔中包含具有相同域名的多個域實例,那麼截取操作會在所有同名域中全部生效。

Lucene通過調用IndexWriter中的對應方法:

IndexReader getReader();

該方法能實時刷新緩衝區中新增或刪除的文檔,然後創建新的包含這些文檔的只讀型IndexReader實例

優化索引

當索引文檔尤其索引多個文檔或使用IndexWriter類的多個session索引文檔時,總會建立一個包含多個獨立段的索引。這樣搜索索引時,Lucene必須分別搜索每個段,然後合併各段的搜索結果。

對於處理大量索引的程序來說,優化索引能夠提高搜索效率,優化索引就是將索引的多個段合併成一個或者少量段。同時優化後的索引還可以在搜索期間少使用一些文件描述符。

優化索引只能提高搜索速度,而不是索引速度。

IndexWriter提供了4個優化方法:
- optimize()將索引壓縮至一個段,操作完成再返回

  • optimize(int manNumSegment)也稱作部分優化(Partial Optimize),將索引壓縮爲最多maxNumSegment個段。由於將多個段合併到一個段的開銷最大,建議優化至5個段,它能比優化至一個段更快完成

  • optimize(boolean doWait)doWait參數傳入false值,調用會立即執行,但是合併工作是在後臺運行的。doWait=false只適用於後臺線程調用合併程序,如默認的ConcurrentMergeScheduler

-optimize(int maxNumSegments,boolean doWait)部分優化,doWait=false時在後臺運行

索引優化會消耗大量的CPU和I/O資源,Lucene將多個段進行合併,合併操作期間,磁盤臨時空間會被用於保存新段對應的文件。但在合併完成並通過調用IndexWriter.commit或關閉IndexWriter進行提交之前,舊段並不能被刪除。即必須爲程序預留大約3倍於優化用量的臨時磁盤空間。完成優化操作並調用commit()方法後,磁盤用量會降低到較低水平。且索引中任何打開的reader都會潛在影響磁盤空間。

Directory子類

當Lucene需要對索引中的文件進行讀寫操作時,它會調用Directory子類的對應方法。

Lucene的幾個核心Directory子類(父類FSDirectory):
- SimpleFSDirectory:使用java.io.* API將文件存入文件系統,不能很好的支持多線程
- NIOFSDirectory:使用java.io.*將文件保存至文件系統,能支持windows之外的多線程
- MMapDirectory:使用內存映射I/O進行文件訪問,不需要使用鎖機制就能很好的支持多線程讀操作。但是由於java並沒有提供方法“取消”文件在內存中的映射關係,這意味着只有在JVM進行垃圾回收時纔會關閉文件和釋放內存空間,這樣索引文件就會佔用大量地址空間。
- RAMDirectory:將所有文件都存入RAM
- FileSwitchDirectory:使用兩個文件目錄,根據文件擴展名在兩個目錄之間切換使用

使用靜態的FSDirectory.open方法,會根據當前的操作系統和平臺選擇合適的默認FSDirectory子類,具體選擇算法會隨着Lucene版本更新而改進。

併發、線程安全及鎖機制

Lucene的併發處理規則:
- 任意數量的只讀屬性的IndexReader類都可以同時打開一個索引,無論這些Reader是否屬於同一個JVM,以及是否屬於同一臺計算機都無關緊要。在單個JVM內,最好是用多線程共享單個的IndexReader實例,例如,多個線程並行搜索同一個索引

  • 對於一個索引一次只能打開一個Writer。Lucene採用文件鎖來提供保障。一旦建立起IndexWriter對象,系統即會分配一個鎖給它,該鎖只有當IndexWriter對象被關閉時纔會釋放。

  • IndexReader對象甚至可以在IndexWriter對象正在修改索引時打開。每個IndexReader對象將向索引展示自己被打開的時間點,該對象只有在IndexWriter對象提交修改或自己被重新打開後才能獲知索引的修改情況。在已經有IndexReader對象被打開的情況下,打開新IndexReader時採用參數create=true:這樣,新的IndexReader會持續檢查索引的情況。

  • 任意多個線程都可以共享同一個IndexReader類或IndexWriter類,這些類不僅是線程安全的,而且時線程友好的,即它們能夠很好的擴展到新增線程

爲了實現單一的writer,即一個用於刪除或修改norms的IndexWriter類或IndexReader類,Lucene採用了基於文件的鎖:若鎖文件(默認爲writer.lock)存在於索引目錄內,writer會馬上打開該索引。若企圖對同一索引創建其他writer的話,將產生一個LockObtainFailException異常。

Lucene允許修改鎖實現方法,可以通過調用Directory.setLockFactory將任何LockFactory的子類設置爲自定義的鎖實現。在完成該操作後才能在Directory實例中打開IndexWriter類。

  • IndexWriter類的isLocked(Directory)————該方法會返回參數目錄所指定的索引是否已被鎖住,在程序試圖創建一個新的IndexWriter對象前可以通過該方法檢測索引是否已被鎖住

  • IndexWriter類的unlock(Directory)————該方法能夠在任意時刻對任意的Lucene索引進行解鎖,

高級索引

用IndexReader刪除文檔
  • IndexReader能夠根據文檔號刪除文檔。IndexWriter可能因爲段合併而改變文檔號

  • IndexReader可以通過Term對象刪除文檔並返回被刪除的文檔號,IndexWriter可以通過Term刪除文檔,但不能返回文檔號

  • 如果程序使用相同的reader進行搜索的話,IndexReader的刪除操作會即時生效。

  • IndexWriter通過Query對象執行刪除操作,但IndexReader則不行

  • IndexReader的undeleteAll方法能反向操作索引中所有被掛起的刪除,但只能對還未進行段合併的文檔進行反刪除操作。該方法之所以能實現反刪除,是因爲IndexWriter只是將被刪除文檔標記爲刪除狀態,但事實上並未真正移除這些文檔,最終刪除操作是在該文檔對應的段進行合併時才執行。

Lucene只允許一個“writer”打開一次,且實施刪除操作的IndexReader只能算作一個writer,即使用IndexReader進行刪除操作之前必須關閉已打開的任何IndexWriter。

IndexWriter批量執行添加和刪除可以獲得更好的性能。

回收被刪除文檔所使用過的磁盤空間

Lucene使用bit數組的形式標識被刪除的文檔,該操作速度很快,但對應的文檔數據仍會佔用磁盤空間。只有在發生段合併操作時這些磁盤空間才能被回收(可以通過正常的合併操作也可以通過顯示調用optimize方法進行)

還可以通過顯式調用expungeDeletes方法來回收被刪除文檔所佔用的磁盤空間。該調用會對被掛起的刪除操作相關的所有段進行合併。

緩衝和刷新

爲了降低磁盤I/O操作,當一個新文檔被添加至Lucene索引時,或者掛起一個刪除操作時,這些操作首先被緩存至內存,而不是立即在磁盤中進行。

IndexWriter觸發刷新操作的標準:
- 當緩存所佔用的空間超過預設的RAM比例時進行實施刷新,預設方法爲setRAMBufferSizeMB。RAM緩存的尺寸不能被視爲最大內存用量。IndexWriter並不佔用所有的RAM使用空間,如段合併操作所佔用的內存空間。
- 在指定文檔號所對應的文檔被添加進索引之後通過調用setMaxBufferedDocs完成刷新操作
- 在刪除項和查詢語句等操作所佔用的緩存總量超過預設值時可以通過調用setMaxBufferedDeleteTerm方法來觸發刷新操作

這幾個觸發器只要其中之一被觸發都會啓動刷新操作,與觸發事件的順序沒有關係。
常量IndexWriter.DISABLE_AUTO_FLUSH可以傳遞給以上任一方法,用以阻止發生刷新操作。默認情況下,IndexWriter只在RAM用量爲16MB時啓動刷新操作。

當發生刷新操作時,Writer會在Directory目錄創建新的段和被刪除文件。但是,這些文件對於新打開的IndexReader既不可見也不可用,直到Writer向索引提交更改以及重新打開reader之後

刷新操作是用來釋放被緩存的更改的。而提交操作是用來讓所有的更改(被緩存的更改或已經刷新的更改)在索引中保持可見。即IndexReader看到的一直是索引的起始狀態(IndexWriter被打開時的索引狀態),直到Writer提交更改爲止。

索引提交

IndexWriter的commit方法有兩個:commit()創建一個新的索引提交,commit(Map

兩階段提交(TWO-PHRASE COMMIT)

對於需要提交包括Lucene索引和其他外部資源(如數據庫)等事務的應用程序來說,Lucene提供了prepareCommit()方法和prepareCommit(Map

索引刪除策略

IndexDeletionPolicy類負責通知IndexWriter何時能夠安全刪除舊的提交。默認策略是KeepOnlyLastCommitDeletionPolicy,即在每次創建完新的提交後刪除先前的提交。

但在有些場景下,例如,通過NFS共享索引時,就需要自定義刪除策略,只有當所有使用索引的reader都切換到最近的提交時纔會刪除此前的刪除。

無論選擇什麼時候保留提交,都會不可避免地佔用索引中額外的磁盤空間。

管理多個索引提交

通常,Lucene索引只有一個當前提交,即最近的提交。自定義提交可以實現在索引中聚集多個提交。可以使用靜態的IndexReader.listCommits()方法檢索索引中當前所有的提交。

ACID事務和索引連續性

Lucene實現了ACID事務模型,其限制是一次只能打開一個事務(writer)。
- Atomic(原子性)——所有針對writer的變更要麼全部提交至索引,要麼全都不提交;沒有中間狀態

  • Consistency(一致性)——索引必須是連續的;

  • Isolation(隔離性)——當使用IndexWriter進行索引變更時,只有進行後續提交時,新打開的IndexReader才能看到上一次提交的索引變化。即使新打開IndexWriter時傳入參數create=true也是如此。IndexReader只能看到上一次成功提交所帶來的索引變化。

  • Durability(持久性)——如果應用程序遇到無法處理的異常,如JVM崩潰、操作系統崩潰或計算機突然掉電,那麼索引會保持連續性,並會保留上次成功提交的所有變更內容。此後的變更則會丟失。但硬盤錯誤、RAM或CPU錯誤會導致索引毀壞

索引段合併

合併索引段的好處:
- 該操作會減少索引中的段數量,能加快搜索速度,因爲被搜索的段數量變小了,還能使搜索程序避免達到由操作系統限制的文件描述符使用上限
- 該操作會減小索引尺寸。比如釋放文檔刪除標識所佔用的數據位,即使沒有掛起的刪除操作,單一的合併段通常會佔用更小的存儲空間。

MergePolicy決定什麼時候合併以及合併哪些段,而真正的合併操作由MergeScheduler完成。

段合併策略

IndexWriter依賴於抽象基類MergePolicy的子類決定何時進行段合併。當程序對變更操作進行刷新時,或者上一個合併操作已經完成時,程序將詢問MergePolicy以確定當前是否需要進行新的合併操作,若是,MergePolicy會精確提供將被合併的段。除了選擇一般的合併段,MergePolicy還會選擇索引中需要進行優化的段,然後運行expungeDelete方法

Lucene提供了兩個核心的合併策略,都是LogMergePolicy的子類,

  • LogByteSizeMergePolicy,默認由IndexWriter使用,該策略通過測量段尺寸,具體爲該段包含的所有文件總字節數。

  • LogDocMergePolicy,同樣是測量段尺寸,用段中文檔數量表示尺寸。

也可以自定義合併策略,如基於時間或是儘量找到帶有很多刪除操作的段。

對於每個段來說,是否進行合併取決於公式:

(int)log(max(minMergeMB, size)) / log(mergeFactor)

這樣可以將段按照尺寸級別分組。尺寸小於minMergeMB的小段通常會被強制轉換成更低級別的段,避免索引中出現太多小段。

每個級別包含的段尺寸都爲前一個級別段的mergeFactor倍。當使用LogDocMergePolicy策略時,段尺寸由段包含的文檔數表示。

mergeFactor值不但要控制如何將段按照尺寸分配給各個級別以用於出發合併操作,還要控制一次合併的段數量。對於索引中指定數量的文檔來說,mergeFactor越大,索引中會存在更多的段,合併頻率越低,該值設置的越大,通常會獲得更高的索引吞吐量,同時也可能會導致太多打開的文件描述符。

最好的辦法是使用默認的值10,要避免大段的合併,可以通過maxMergeMB或maxMergeDocs進行設置。如果某個段的字節尺寸超過maxMergeMB,或段內文檔數超過了maxMergeDocs,該段將永不被合併。

除了選擇合併策略以維持索引正常運行狀態以外,MergePolicy還要在程序調用optimize或expungeDeletes時選擇將要被合併的段,

MergeScheduler子類完成合並工作

默認情況下,IndexWriter使用concurrentMergeScheduler利用後臺線程完成段的合併,還有一個SerialMergeScheduler可以由調用它的線程完成段合併

IndexWriter.setInfoStream可以獲取刷新和合並的相關信息

IndexWriter.waitForMerges方法要等待所有的段合併操作完成再進行下一步操作。

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