1. 程式人生 > >LuceneInAction(第2版)學習筆記——第二章 構建索引

LuceneInAction(第2版)學習筆記——第二章 構建索引


1. 文件和域

1.1.文件和域的關係


文件是Lucene索引和搜尋的原子單位。
 文件為包含一個或多個域的容器,而域則依次包含“真正的”被搜尋內容。
 每個域都有一個標識名稱,該名稱為一個文字值或二進位制值。
 
 將一個文件加入到索引中時,可以通過一系列選項來控制Lucene的行為。

 在對原始資料進行索引時,得先將資料轉換成Lucene所能識別的文件和域。
 在隨後的搜尋過程中,被搜尋物件則為域值。

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


  1) 域值可以被索引或不被索引。

   要搜尋一個域,則要先對該域進行索引。
   被索引的域值必須是文字格式,二進位制格式的域值只能被儲存而不被索引。
   域值 ==>分析後得到語彙單元 ==> 將語彙單元加入到索引中


  2) 域被索引後,可以選擇性地儲存項向量


   項向量可以看做是該域的一個小型反向索引集合,
   通過該向量能夠檢索該域的所有語彙單元。
   這個機制可以實現一些高階功能,如搜尋與當前文件相似的文件。


  3) 域值可以被單獨儲存


   是指被分析前的域值備份可以寫進索引中,以便後續的檢索。
   這機制將使你可以將原始域值展現給使用者,如文件的標題或摘要。

 有時,可能會需要使用雜項域,即包含所有文字的一個獨立域以供搜尋。


2. Lucene與資料庫的區別


A. Lucene沒有一個確定的全域性模式

即:
 加入索引的每個文件都是獨立的,它與之前加入的文件完全沒有關係;
 文件可以包含任意的域,以及任意的索引、儲存和項的向量操作選項;
 文件也可以不必包含與其它文件相同的域,甚至跟其它文件可以只有相關操作選項有所區別。
 這種特性可以隨時對文件進行索引,而不必提前設計文件的資料結構表;
 如果隨後想向文件中新增域,則可以完成新增後重新索引該文件或重建索引即可;
 Lucene這種靈活的架構意味著單一的索引可以包含表示不同實體的多個文件。

B. Lucene要求你在進行索引操作時簡單化或反向規格化原始資料


反向規格化Donormalization: 解決有關文件真實結構和Lucene表示能力之間的“不匹配”問題


3. Lucene的索引過程


從原始文件 提取文字並建立文件 --> 分析文件 --> 文件索引(向索引新增文件)

A. 提取文字並建立文件


HTML ==> 【 Extract Text 】 ==> 文字
PDF  ==> 【 Extract Text 】 ==> 文字
Word ==> 【 Extract Text 】 ==> 文字

提關提取文字資訊的細節可結合Tika框架詳述,使用該框架能使你很輕易地從各種格式的檔案中提取文字資訊。
一旦提取出預想的文字資訊,並建立起對應的包含各個域的文件後,下一步就是對這些文字資訊進行分析了。

B. 分析文件


分析文件是呼叫IndexWriter物件的addDocument方法將資料傳遞給Lucene進行索引操作。
索引操作時,Lucene首先分析文字,將文字資料分割成語彙單元,然後對它們執行一些可選操作。

在索引前,對語彙單元的操作:
 1) 統一轉換為小寫,以使搜尋不對大小寫敏感;
 2) 呼叫StopFilter停詞過濾類,從輸入中去掉一些使用很頻繁即沒有實際意義的詞;
 3) 去掉詞幹;
 4) 然後呼叫一系列filter過濾類,以便修正語彙單元;
 以上這些操作,構成了分析器。

此外,還可以通過連結Lucene的語彙單元和filter來搭建自己的分析器,或通過其它自定義方式來搭建分析器。

分析過程會產生大批的語彙單元,隨後這些語彙單元將被寫入索引檔案中。

C. 文件索引(向索引新增文件)


對輸入資料分析完畢後,就可以將分析結果寫入索引檔案中。
Lucene將輸入資料以一種倒排索引(inverted index)的資料結構進行儲存。
在進行關鍵字快速查詢時,這種資料結構能夠有效利用磁碟空間。

Lucene使用倒排資料結構的原因是:把文件中提取出的語彙單元作為查詢關鍵字,而不是將文件作為中心實體。
倒排索引結構類似於圖書的索引與頁碼的對應關係。

倒排索引不是回答【這個文件中包含哪些單詞?】,而是經過優化後用來快速回答【哪些文件包含單詞x?】”。
現在所有的Web搜尋引擎核心都是採用倒排索引技術。

Lucene的索引檔案目錄有唯一一個段結構,即索引段(有待具體分析)。
可以將一個段看作一個子索引,儘管每個段都不是一個完全獨立的索引。
Lucene的優勢之一就是支援增量索引(Incremental indexing),而這個功能主要是靠索引分段實現。

4. 基本索引操作: 新增、刪除、更新索引文件Document


4.1. 新增索引文件


 Directory dir = null;
 //取Writer
 private IndexWriter getWriter() throws IOException{
  return new IndexWriter(dir, new WhitespaceAnalyzer(), IndexWriter.MaxFieldLength.UNLIMITED);
  //IndexWriter建構函式的三個引數:索引存放目錄,分析器,域的最大長度
 }


 protected void setup() throws Exception{
  dir = new RAMDirectory();
  IndexWriter writer = getWriter();
  for(...){
   Document doc = new Document();
   doc.add(new Field("id", "1", Field.Stores.YES, Field.Index.NOT_ANALYZED));
   //Field建構函式的四個引數:域名,域值,域的儲存狀態(是否儲存),域的索引狀態(是否分析)
   writer.addDocument(doc);
  }
  writer.commit();//或 writer.close();
 }

 protected int getHitCount(String fieldName, String searchString) throws Exception{
  IndexSearcher searcher = IndexSearcher(dir);
  Term t = new Term(fieldName, searchString);
  Query query = new TermQuery(t);
  int hitCount = searcher.search(query, 1).totalHits;
  searcher.close();
  return hitCount;
 }

4.2. 刪除索引中的文件

 protected void DelDoc() throws Exception{
  IndexWriter writer = getWriter();
  writer.deleteDocument(new Term("id", "1"));//刪除文件
  //writer.hasDeletions(); 是否包含被標記為已刪除的文件

  //writer.maxDoc(); //返回索引中被刪除和未被刪除的文件總數
  //writer.numDocs(); //返回索引中未被刪除的文件總數

  writer.optimize();//優化操作使用刪除生效,並強制Lucene在刪除一個文件後合併索引段
  writer.commit();

  //或 writer.close();
 }

4.3. 更新索引中的文件

 protected void ModDoc() throws Exception{
  IndexWriter writer = getWriter();

  Document doc = new Document();
  doc.add(new Field("id", "1", Field.Stores.YES, Field.Index.NOT_ANALYZED));
  writer.updateDocument(new Term("id", "1"), doc);//更新文件
  

  //或 writer.close();
 } 

 updateDocument = 先呼叫 deleteDocument() + 再呼叫 addDocument();

5. 域選項


 Field類是在文件索引期單最重要的類,該類控制著被索引的域值。
 域選項有:索引選項、儲存選項、項向量使用選項。


5.1. 域索引選項 Field.Index.*


 通過倒排索引來控制域文字是否可被搜尋。
 Field.Index.ANALYZED  【會分析域值】使用分析器將域值分解成獨立的語彙單元流,並使每個語彙單元能被搜尋。
 Field.Index.NOT_ANALYZED 【不分析域值】對域進行索引,但不對String值進行分析;
     將域值作為單一語彙單元並使之能被搜尋;
     適用於不能被分解的域值,如:URL、檔案路徑、日期、人名、社保號碼、電話號碼等。
 Field.Index.ANALYZED_NO_NORMS   【會分析域值,不儲存norms】不在索引中儲存norms資訊
 Field.Index.NOT_ANALYZED_NO_NORMS 【不分析域值,不儲存norms】不在索引中儲存norms資訊
 Field.Index.NO   【使對應的域值不被搜尋】

 norms資訊記錄了索引中的index-time boost 資訊,但是當搜尋時,可能會比較耗費記憶體。

5.2. 域儲存選項 Field.Store.*


 用來確定是否需要儲存域的真實值,以便後續搜尋時能恢復這個值。
 Field.Store.YES  【指定儲存域值】建議不要儲存太大的域值,因為會消耗掉索引的儲存空間
 Field.Store.NO  【指定不儲存域值】

 可以使用Lucene的CompressionTools類中的方法對要儲存的域值進行壓縮和解壓,但該方法會降低索引和搜尋速度。
 這其實就是通過消耗更多CPU計算能力來換取更多的磁碟空間,這要仔細權衡。如果域值較小,建議少用壓縮。

5.3. 域的項向量選項: 【索引域】 == 【項向量】 == 【儲存域】


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

5.4. 域的其它初始化選項: Reader/TokenStream/byte[]


 Field(String name, Reader value, TermVector termVector)
  Reader的域值不能被儲存 Store.NO,且該域會一直用於分析和索引 Index.ANALYZED
 Field(String name, Reader value)
  TermVector為TermVector.NO

 Field(String name, TokenStream tokenStream, TermVector termVector)
  允許程式對域值進行預分析並生成TokenStream物件。此域不會被儲存 Store.NO,會有索引Index.ANALYZED
 Field(String name, TokenStream tokerStream)
  TermVector為TermVector.NO

 Field(String name, byte[] value, Store store)
  採用二進位制域,不參與索引 Index.NO,沒有項向量 TermVector.NO,會被儲存 Store.YES
 Field(String name, byte[] value, int offset, int length, Store store)
  引用二進位制的部分片段

5.5. 域選項組合(常見組合)


索引選項      儲存選項 項向量      使用場合
Index.NOT_ANALYZED_NO_NORMS Store.NO TermVector.NO     識別符號,檔名,主鍵,URL,
           日期,用於排序的文字域
Index.ANALYZED      Store.YES TermVector.WITH_POSITIONS_OFFSETS 文件標題、摘要
Index.ANALYZED      Store.NO TermVector.WITH_POSITIONS_OFFSETS 文件正文(資料量大,故不儲存)
Index.NO      Store.YES TermVector.NO     文件型別、不要搜尋的資料庫主鍵
Index.NOT_ANALYZED     Store.NO TermVector.NO     隱藏的關鍵字

5.6. 域排序選項


當Lucene返回匹配搜尋條件的文件時,一般是按照預設評分對文件進行排序的。
當然,也可以自定義排序,前提是要先正確地完成對域的索引。

注意:用於排序的域是必須進行索引的,而且每個對應文件必須包含一個語彙單元。

5.7. 多值域


作者域,當作者數多於一個時,該如何處理?
方法一: 依次處理每個作者名字,將它們加入單個String,然後建立對應的域。
方法二: 向這個域中寫入幾個不同的值,如:
 Document doc = new Document();
 for(String author : authors){
  doc.add(new Field("author", author, Field.Store.YES, Field.Index.ANALYZED));
 }

鼓勵使用第二種方式,因為這是邏輯上具有多個域值的表達方式。

在Lucene中,只要文件出現同名的多值域,
倒排索引和項向量都會在邏輯上將這些域的語彙單元附加進去,
具體順序由新增該域的順序決定。


6. 對文件和域進行加權操作 : 提升文件或域的評分,預設的加權值為1.0


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


6.1. 文件加權操作


 對郵件進行索引和搜尋時,如果傳送人郵件是在本公司,則要排在更重要的位置,否則,排在不重要的位置。

 Document doc = new Document();
 doc.add(new Field("senderEmail", senderEmail, Field.Store.YES, Field.Index.Not_ANALYZED));
 //...
 if(isImportant(senderEmail)){
  doc.setBoost(1.5F);  //重要:加權因子為1.5
 }else if(isUnimportang(senderEmail)){
  doc.setBoost(0.1F);  //不重要:加權因子為0.1
 }
 //其它,採用預設的加權因子: 1.0
 writer.addDocument(doc);

6.2. 對域加權操作


 當加權一個文件時,Lucene在內部採用同一個加權因子來對該文件中的域進行加權。
 同上節的例子,如何才能使用郵件的主題變得比郵件的作者更重要呢?即:
 搜尋時,如何才能讓主題域變得比作者域更重要呢?為達到這個目地,可以使用Field類的setBoost()方法。

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


注意:當你改變一個域或一個文件的加權因子時,必須完全刪除並建立對應的文件,或者使用updateDocument方法來實現。
另外:較短的域有一個隱含的加權,這取決於Lucene的評分演算法具體實現。

加權操作是一項高階操作,很多搜尋程式沒有它也能正常執行,所以使用加權的時候要小心。
Lucene的評分機制包含大量的因子,其中就有加權因子。

Lucene如何將加權因子寫入索引呢?這就是屬於norms的範疇了。


6.3. 加權基準(Norms)


 norms經常面臨的問題之一就是它在搜尋期間的高記憶體量。
 這是因為norms的全部陣列要載入到RAM,並且,要對被搜尋文件的每個域分配一個位元組空間。
 如果域較多,則載入操作會很快佔用大量的RAM空間。

 當然,你也可以關掉norms相關操作,方法是使用Field.Index中的NO_NORMS索引選項,
 或者,在對包含該域的文件進行索引前呼叫Field.setOmitNorms(true)方法,該操作會影響評分效果。
 因為上面的操作,使得搜尋期間程式不會處理索引時的加權資訊。

 注意,含Norms選項索引進行到一半時關閉Norms選項,則必須對整個索引進行重建。
 因為,即使只有一個文件域在索引時包含了norms,則在隨後的段合併操作中,這個情況會“擴散”,
 使得所有文件都會佔用一個位元組的norms空間,發生這種情況主要是因為Lucene並不針對norms進行鬆散儲存。

7. 索引數字、日期和時間


7.1. 索引數字


A. 數字內嵌在將要索引的文字中,而想保留這些數字,並將它們作為單獨的語彙單元處理,這樣搜尋過程就可以用到。


 只要選擇一個不丟棄數字的分析器即可,如: WhitespaceAnalyzer和StandardAnalyzer 。
 注意: SimpleAnalyzer和StopAnalyzer兩個類會將語彙單元流中的數字剔除。
 
 可以使用Luke來核實數字是否由分析器保留下來並寫入索引。
 Luke是一款用於檢查Lucene索引細節的優秀工具。

B. 某些域只包含數字,希望將它們作為數字域值來索引,並能在搜尋和排序中對它們進行精確匹配。


 從2.9版本開始,Lucene加入了對數字域的支援,即NumericField類。
 doc.add(new NumericField("price").setDoubleValue(19.99));
 NumericField類也能處理日期和時間,方法是將它們轉換成等效的int型或long型整數。

7.2. 索引日期和時間


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

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

8. 域擷取(Field truncation)


 一些應用程式需要對尺寸未知的文件進行索引,作為一個控制RAM和硬碟空間使用量的安全機制,
 我們需要對每個域進行索引時對輸入的文件尺寸進行限制。
 如:對每個文件的前面200個單詞進行索引。
 IndexWriter允許你對域進行擷取後再索引它們。

 在例項會IndexWriter後,必須向其傳入MaxFieldLength物件,以便傳遞具體的擷取數量。
  MaxFieldLength.UNLIMITED 不擷取
  MaxFieldLength.LIMITED  只擷取域中前1000個項
 當然,在例項化MaxFieldLength物件時,還可以設定自己所需要的擷取數。

 建立IndexWriter後,可以在任意時間調整擷取限制:
  setMaxFieldLength()方法: 設定擷取限制
  getMaxFieldLength()方法: 獲取擷取限制

 使用MaxFieldLength時,一定要謹慎!因為域擷取意味著程式會完全忽略一部分文件文字,
 使得這些文字無法被搜尋到,從而會讓使用者發現,有些文件找不到。
 【使用者信任是保護業務的最重要條件】

9. 近實時搜尋(Near-real-time Search)


 Lucene2.9開始新增了一項被稱為實時搜尋的重要功能,
 該功能解決了一個長期困擾搜尋引擎的問題:文件的即時索引和即時搜尋問題。
 實現方式:
  IndexReader IndexWriter.getReader()方法

 該方法能【實時重新整理緩衝區中新增或刪除的文件】,然後建立新的包含這些文件的只讀型IndexReader例項。

 請注意:getReader()方法,會降低索引效率,因為這會使得IndexWriter馬上重新整理段內容,
  而不是等到記憶體緩衝填滿再重新整理。

10. 優化索引


 優化索引就是將索引的多個段合併成一個或者少量段,同時,優化後的索引還可以在搜尋期間少使用一些檔案描述符。
 優化索引只能提高搜尋速度,而不是索引速度。

 IndexWriter的四個優化方法:
  optimize() 
   將索引壓縮至一個段,操作完成再返回
  optimize(int maxNumSegments)
   也稱為部分優化,將索引壓縮為最多maxNumSegments個段。
   因為將多個段合併為一個段的開銷最大,建議優化至5個段,它能比優化至一個段更快完成。
  optimize(boolean doWait)
   跟optimize()類似,doWait為false則不等待而立即執行,但合併工作是在後臺完成的。
   doWait=false只適用於後臺執行緒呼叫合併程式
  optimize(int maxNumSegments, boolean doWait)
   也是部分優化,灰doWait=false則也在後臺執行。

 請記住,索引優化會消耗大量的CPU和I/O資源,使用時一定要明確這點。
 這是以一次性大量系統開銷來換取更快的搜尋速度。
 搜尋期間,還要注意一項重要開銷就是磁碟臨時使用空間。
 合併期間,舊段不會被刪除,磁碟臨時空間會被用於儲存新段對應的檔案,
 這意味著,必須為程式預留大約3倍於優化用量的臨時磁碟空間。

11. 其它Directory子類


 Lucene的抽象類Directory主要是為我們提供一個簡單的檔案類儲存API,它隱藏了實現儲存的細節資訊。
 當Lucene需要對索引中的檔案進行讀寫操作時,它會呼叫Directory子類的對應方法來進行。

 五個核心子類如下:


 1) SimpleFSDirecotry


  最簡單的Directory子類,使用 java.io.* API將檔案存入檔案系統,不能很好地支援多執行緒操作。
  要支援多執行緒,必須在內部加入鎖,而java.io.*並不支援按位置讀取。

 2) NIOFSDirectory


  使用  java.nio.* API將檔案儲存至檔案系統。能很好地支援除Micfosoft Windows之外的多執行緒操作。
  它在Windows下的效能比較差,甚至可能比SimpleFSDirectory的效能還要差。

 3) MMapDirectory


  使用記憶體對映I/O進行檔案訪問,對64位JRE來說是一個很好選擇,
  對於32位JRE並且索引相對較小時也可以使用該類。
  建議最好使用64位的JRE。
  對於32位的JRE,程式可能由於記憶體碎片問題而遇到OutOfMemoryError異常。
  MMapDirectory提供setMaxChunkSize()方法來處理該問題。

 4) FileSwitchDirectory


  使用兩個檔案目錄,根據副檔名在兩個目錄之間切換使用


 5) RAMDirectory


  將所有檔案都存入RAM


 特別:所有的Directory子類在進行寫操作時,都共享相同的程式碼,程式碼來自於SimpleFSDirectory,使用java.io.*

 那麼,到底該採用哪個Directory子類呢?一個很好的方法就是使用靜態的FSDirectory.open()方法。
 該方法會根據當前的作業系統和平臺來嘗試選擇最合適的預設FSDirectory子類,
 具體選擇演算法會隨著Lucene版本的更新而改進。

12. 併發、執行緒安全及鎖機制


 索引檔案的併發訪問
 IndexReader和IndexWriter的執行緒安全性
 Lucene用於實現併發與執行緒安全的鎖機制


12.1. 執行緒安全和多虛擬機器安全


 1) 任意數量的只讀屬性的IndexReader類都可以同時開啟一個索引。


  不管這些Reader是否屬於同一個JVM,是否屬於同一臺計算機。
  記住: 在單個JVM內,利用資源和發揮效率的最好辦法是用多執行緒共享單個的IndexReader例項。
  如: 多個執行緒或程序並行搜尋同一個索引。


 2) 對一個索引來說,一次只能開啟一個Writer


  Lucene採用檔案鎖來提供保障。
  一旦建立起IndexWriter物件,系統即會分配一個鎖給它。
  該鎖只有當IndexWriter物件被關閉時才會釋放。

  如果使用IndexReader物件來改變索引的話(如修改norms或刪除文件),
  這時的IndexReader物件會作為Writer使用。
  它必須在修改上述內容之前成功地獲取Writer鎖,並在被關閉時釋放該鎖。


 3) IndexReader物件甚至可以在IndexWriter物件正在修改索引時開啟。


  每個IndexReader物件將向索引展示自己被開啟的時間點。
  該物件只有在IndexReader物件提交修改或自己被重新開啟後才能獲知索引的修改情況。

  在已經有IndexReader物件被開啟的情況下,開啟新的IndexReader時採用採數create=true,這樣,
  新的IndexReader會持續檢查索引的情況。

 4) 任意多個執行緒都可以共享同一個IndexReader類或IndexWriter類。


  這些類不僅是執行緒安全的,而且是執行緒友好的,即是說他們能夠很好地擴充套件到新增執行緒。

12.2. 通過遠端檔案系統訪問索引


 使用不同計算機上的不同虛擬機器JVM來訪問同一個索引,則得提供該索引的遠端訪問方式。
 但,最好的方式,是將索引複製到各臺計算機自己的檔案系統,再進行搜尋。Solr支援這種複製策略。

 四種遠端檔案系統:
 1) Samba/CIFS1.0 : Windows標準遠端檔案系統,能很好地共享Lucene索引
 2) Samba/CIFS2.0 : 新版本,由於不連貫的客戶端快取,Lucene不能很好地執行
 3) Networked File System(NFS) : 針對大多數UNIX作業系統的標準遠端檔案系統
     由於不連貫的客戶端快取,Lucene不能很好地執行
 4) Apple File Protocal(AFP) : Apple的標準遠端檔案協議。
     由於不連貫的客戶端快取,Lucene不能很好地執行

   
 針對併發訪問唯一的限制是不能同時開啟多於一個writer


12.3. 索引鎖機制


 為實現單一的writer,即一個用於刪除或修改norms的IndexWriter類或IndexReader類,Lucene採用了基於檔案的鎖。
 如果鎖檔案 writer.lock 存在於你的索引所在目錄,writer會馬上開啟該索引。
 這時,若企圖針對同一索引建立其它writer,將產生一個LockObtainFailedException異常。
 這是很重要的保護機制,因為若針對同一索引開啟兩個 writer 的話,會導致索引損壞。

 索引允許你修改鎖實現方法: 可以通過呼叫Directory.setLockFactory將任何LockFactory的子類設定為你自己的鎖實現。
 注意,在完成該操作之後,才能在Directory例項中開啟IndexWriter類。
 正常情況下,不用擔心程式正在使用哪個鎖實現,通常只有那些採用多臺電腦或者多虛擬機器的搜尋程式,
 才可能需要自定義鎖實現,以便能輪流進行索引操作。

 Lucene提供的鎖實現:
 1) NativeFSLockFactory
  FSDirectory的預設鎖,使用java.nio本地作業系統鎖,在JVM還存在的情況下不會釋放餘下的被鎖檔案。
  該鎖可能無法與一些共享檔案系統很好地協同,特別是NFS檔案系統。
 2) SimpleFSLockFactory
  使用Java的File.createNewFile API,它比NativeFSLockFactory更易於在不同檔案系統間移植。
  如果JVM崩潰或IndexWriter物件並未在JVM退出之前關尊重,則這會導致遺留一個write.lock檔案,要手動刪除。
 3) SingleInstanceLockFactory
  在記憶體中建立一個完全的鎖,它是RAMDirectory預設的鎖實現子類。
  在程式知道所有IndexWriter將在同一個JVM例項化時使用該類。
 4) NoLockFactory
  完全關閉鎖機制,要小心使用。只有在程式確認不需要使用Lucene的鎖保護機制時才能使用它。
  如: 使用帶有單個IndexWriter例項的私有RAMDirectory.

 注意: 這些鎖在實現上都是不“公平”的。如果該鎖已被某writer持有,則後續的writer只是簡單地重複申請獲取該鎖,
  預設情況是申請兩次。當該鎖被最初持有者釋放時,系統並沒有提供佇列機制讓後續writer持有該鎖。
  如果搜尋程式需要採用佇列機制的話,最好是自己實現它,同時,要確認該機制能夠正確執行。

 IndexWriter類的isLocked(dir)方法,可以在建立一個新的writer物件前檢查索引是否被鎖住。
 IndexWriter類的unLock(dir)方法,在任意刻對任意的Lucene索引進行解鎖,貿然使用它很危險。
  在索引正被修改期間對它進行解鎖的話,會立即導致該鎖索引被毀壞並變得不可使用。

 最好不要直接操作鎖檔案,而應該通過Lucene提供的API來進行操作。

   
13. 除錯索引


 如果需要對Lucene的寫索引操作進行除錯的話,可以通過呼叫 IndexWriter類的setInfoStream()方法,
 通過列印諸如 System.out 等輸出流(PrintStream)的方式獲取Lucene操作索引的相關輸出資訊。

 IndexWriter writer = new IndexWriter(dir, nanlyzer, IndexWriter.MaxFieldLength.UNLIMITED);
 writer.setInfoStream(System.out);

 該程式碼能夠揭示有關段重新整理和段合併的診斷資訊,它可以幫助你調整相關的索引引數。
 如果在索引期間遇到問題,並估計是Lucene的bug導致,可以將該問題寫入Apache的Lucene的使用者清單中,
 然後,通過設定 infoStream 貼出相關係統資訊。

14. 高階索引概念


14.1. 用IndexReader刪除文件


  1) IndexReader能夠根據文件號刪除文件


  IndexWriter則不能根據文件號刪除文件


  2) IndexReader可以通過Term物件刪除文件


  IndexReader通過Term物件刪除文件與IndexWriter通過Term物件刪除文件的區別:
  IndexReader會返回被刪除的文件號,而IndexWriter則不能。
  原因: IndexReader可以立即決定刪除哪個文件,能對這些文件數量進行計算
   IndexWriter則只是將被刪除的Term進行快取,後續再進行實際的刪除操作

  3) 如果程式使用相同的reader進行搜尋,則IndexReader的刪除操作會即時生效


  而IndexWriter的刪除操作必須等到程式開啟一個新的Reader時才能被感知

  4) IndexWriter可以通過Query物件執行刪除操作,但IndexReader則不行


  5) IndexReader提供了undeleteAll()方法,能反向操作索引中所有被掛起的刪除


  注意,只能對還未進行段合併後的文件進行反刪除操作。
  該方法之所以能實現反刪除,是因為IndexWriter只是將被刪除文件標記為刪除狀態,並未真正移除這些文件。
  最終的刪除操作是在該文件對應的段進行合併時才執行。

 如果試圖使用IndexReader刪除文件,則Lucene只允許一個writer開啟一次。
 但實施刪除操作的IndexReader只能算作一作一個writer。
 這意昧著在使用reader進行刪除操作之前,必須關閉已開啟的IndexWriter,反之亦然。

 如果程式正在交叉進行文件的新增和刪除操作,則會極大地降低索引吞吐量。
 更好的辦法是,將新增和刪除操作以批量的形式讓IndexWriter完成,這樣可以獲得更好的效能。

 一般而言,最好是隻用IndexWriter完成所有刪除操作。

14.2. 回收被刪除文件所使用過的磁碟空間


 Lucene使用一個簡單辦法來記錄索引中被刪除的文件:
 用bit陣列的形式來標識它們,該操作速度很快,但對應的文件資料仍然會佔用磁碟空間。

 該技術是必要的,因為對於一個倒排索引來說,給定的文件項是分散在各處的,
 因此,在刪除文件時試圖回收它們佔用的磁碟空間是不切實際的。

1) 段合併操作時回收


 只有在段合併操作時(正常的合併操作,或顯示呼叫optimize方法),這些磁碟空間才能被回收。

2) 顯式呼叫expungeDeletes方法回收


 還可以顯式呼叫expungeDeletes方法,來回收被刪除文件所佔用的磁碟空間。
 該呼叫會對被掛起的刪除操作相關的所有段進行合併。
 這個操作的開銷比優化小,但仍然會導致較大開銷,一般只有在完成刪除操作較長一段時間後才值得這樣做。
 最壞的情況,如果刪除操作分散在所有段中,則expungeDeletes所做的工作就與optimize方法一致:將所有段進行合併。

14.3. 緩衝和重新整理


 當一個新文件被新增至Lucene索引時,或當掛起一個刪除操作時,
 這些操作首先被快取至記憶體,而不是立即在磁碟中進行。
 這種緩衝技術主要是出於降低磁碟I/O操作等效能原因而使用的,
 它們會以新段的形式週期性寫入索引的Diretory目錄。

 IndexWriter根據3個可能的標準來觸發實際上的重新整理,這三個標準是由程式控制的:

 1)  setRAMBufferSizeMB()
  當快取所佔用的空間超過預設的RAM比例時進行實施重新整理,預設方法為setRAMBufferSizeMB
  RAM快取大小不能被視為最大記憶體用量,因為還要考慮到影響測量JVM記憶體容量的其它因素。
  此外,IndexWriter並不佔用所有的RAM使用空間,如段合併操作所佔用的記憶體空間。
 2)  setMaxBufferedDocs()
  還可以在指定文件號所對應的文件被新增進索引之後通過呼叫 setMaxBufferedDocs 來完成重新整理操作。
 3)  setMaxBufferedDeleteTerm()
  在刪除項和查詢語句等操作所佔用的快取總量超過預設值時,
  可以通過呼叫 setMaxBufferedDeleteTerm 方法來觸發重新整理操作。

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

 當發生重新整理操作時,writer會在Directory目錄建立新的段和被刪除檔案。
 但是,這些檔案對於新開啟的IndexReader來說既不可視也不可用,至到Writer向索引提交更改以及重新開啟reader。

 【重新整理操作】是用來釋放被快取的更改;
 【提交操作】是用來讓所有的更改在索引中保持可視。
 這意味著,IndexReader所看到的一直是索引的起始狀態(IndexWriter開啟時的索引狀態),直到writer提交更改為止。

14.4. 索引提交


 索引提交的兩個方法: commit()和close()
 注意: 新開啟或重啟的IndexReader或IndexSearcher只能看到上次提交後的索引狀態,
  而IndexWriter在兩次提交之間所完成的所有更改對於reader來說都是不可見的。
  唯一的例外是近實時搜尋功能,
  它可以在不用首次向磁碟提交更改的情況下,對IndexWriter所作的更改進行搜尋。

 此外:  提交操作的開銷較大,如果頻繁進行該操作,會降低索引吞吐量。
  如果要取消更改,則在上一次項索引提交更改後呼叫rollback()方法,
  來刪除當前writer上下文中包含的所有更改操作。

 1) IndexWriter 的提交步驟


  (1) 重新整理所有快取的文件和文件刪除操作;
  (2) 對所有新建立的檔案進行同步,包括重新整理新的檔案,
   還包括上一次呼叫commit()方法或者從開啟writer後已完成的段合併操作所生成的所有檔案。
   writer呼叫Directory.sync()來實現這一目標。
   該方法會在所有掛起的寫操作都通過I/O系統寫入穩定儲存器之後才返回結果。
  (3) 寫入和同步下一個segments_N檔案。一旦完成操作,reader會立即看到上一次提交後的所有變化。
  (4) 呼叫 IndexDeletionPolicy 刪除舊的提交,可以繼承該類來實現自定義提交的內容和時間。

  上一次提交中包含的舊索引檔案引用,只有在新的提交完成後才會被刪除。

 2) 兩階段提交 two-phrase commit


  prepareCommit()方法,會完成提交步驟1和步驟2,
  大多數還會完成步驟3,但它不能使新的segments_N檔案對Reader可視。

  呼叫prepareCommit()方法後,只能呼叫rollback()方法終止提交,或commit()方法來完成提交。
  如果已呼叫prepareCommit()方法,再呼叫commit()方法則會很快。

 3) 索引刪除策略


  IndexDeletionPolicy類負責通知IndexWriter何時能夠安全刪除舊的提交。
  預設的策略是 KeepOnlyLastCommitDeletionPolicy,該策略會在每次建立完新的提交後刪除先前的提交。

 4) 管理多個索引提交


  通常情況下,Lucene索引只有一個當前提交,它是最近的提交。
  如實現自定義的提交策略,則可以很容易地在索引中聚集多個提交。

  IndexReader.listCommits()方法,可檢索索引中當前所有的提交。

14.5. ACID事務和索引連續性


 Lucene實現了ACID事務模型,其限制是一次只能開啟一個事務(writer)。


 (1) Actomic: 原子性


  所有針對writer的變更,要麼全部提交至索引,要麼全不提提,沒有中間狀態。


 (2) Consistency: 一致性


  索引必須是連續的。


 (3) Isolation: 隔離性


  當使用writer進行索引變更時,只有進行後續提交時,新開啟的reader才能看到上一次提交的索引變化。
  即使是在新開啟writer時傳入引數create=true也是如此。
  IndexReader只能看到上一次成功提交所帶來的索引變化。


 (4) Durability: 永續性


  如果程式遇到無法處理的異常,如JVM崩潰、作業系統崩潰、計算機掉電等,
  則索引會保持連續性,並會保留上一次成功提交的所有變更內容。

14.6. 合併段


 如果索引包含太多的段,writer會選擇其中一些段,並將它們合併成一個單一的、更大的段。
 合併操作會帶來兩個重要的好處:
  一是會減少索引中的段數量,能加快搜索速度;
  二是會減小索引大小。


 1) 段合併策略


  writer依賴於抽象基類 MergePolicy 的子類來決定何時進行段合併。
  Lucene提供了兩個核心的合併策略,都是 LogMergePolicy 的子類:
   A. LogByteSizeMergePolicy
    該策略由writer使用,它會測量段大小,即:該段所包含的所有檔案總位元組數。
   B. LogDocMergePolicy
    它完成與上一個子類相同的段合併策略,區別在於:
    它對段大小的測量是用段中文件數量來表示。
  注意:這兩個策略都不會執行真正的刪除操作。
  如果段中文件大小差別較大,最好是使用 LogByteSizeMergePolicy子類。

  當然,也可以繼承 MergePolicy 後,自定義相關策略。

 ***** 控制 LogByteSizeMergePolicy 合併策略的引數


  
       2) MergeScheduler


  選取要被合併的段只是第一步。
  第二步,是實施實際上的合併。
  IndexWriter需要通過一個 MergeScheduler 子類來實成這個工作。
  預設使用 ConcurrentMergeScheduler 進行,該類利用後臺執行緒完成段的合併。

  另外,SerialMergeScheduler 可以由呼叫它的執行緒來完成段合併,
  這意味著可以看到addDocument和deleteDocuments等方法正在進行的段合併操作。

  此外,還可以繼承MergeScheduler,來自定義自己的合併策略。

  如果出於某些原因,需要等待所有的段合併操作完成,再進行下一步操作,
  則可以呼叫IndexWriter的waitForMerges方法。