Lucene學習總結之四:Lucene索引過程分析(2)

3、將文檔加入IndexWriter

代碼:

writer.addDocument(doc); 
-->IndexWriter.addDocument(Document doc, Analyzer analyzer) 
     -->doFlush = docWriter.addDocument(doc, analyzer); 
          --> DocumentsWriter.updateDocument(Document, Analyzer, Term) 
注:--> 代表一級函數調用

IndexWriter繼而調用DocumentsWriter.addDocument,其又調用DocumentsWriter.updateDocument。

4、將文檔加入DocumentsWriter

代碼:

DocumentsWriter.updateDocument(Document doc, Analyzer analyzer, Term delTerm) 
-->(1) DocumentsWriterThreadState state = getThreadState(doc, delTerm); 
-->(2) DocWriter perDoc = state.consumer.processDocument(); 
-->(3) finishDocument(state, perDoc);

DocumentsWriter對象主要包含以下幾部分:

  • 用於寫索引文件
    • IndexWriter writer;
    • Directory directory;
    • Similarity similarity:分詞器
    • String segment:當前的段名,每當flush的時候,將索引寫入以此爲名稱的段。
IndexWriter.doFlushInternal() 
--> String segment = docWriter.getSegment();//return segment 
--> newSegment = new SegmentInfo(segment,……); 
--> docWriter.createCompoundFile(segment);//根據segment創建cfs文件。
  •  
    • String docStoreSegment:存儲域所要寫入的目標段。(在索引文件格式一文中已經詳細描述)
    • int docStoreOffset:存儲域在目標段中的偏移量。
    • int nextDocID:下一篇添加到此索引的文檔ID號,對於同一個索引文件夾,此變量唯一,且同步訪問。
    • DocConsumer consumer; 這是整個索引過程的核心,是IndexChain整個索引鏈的源頭。

基本索引鏈:

對於一篇文檔的索引過程,不是由一個對象來完成的,而是用對象組合的方式形成的一個處理鏈,鏈上的每個對象僅僅處理索引過程的一部分,稱爲索引鏈,由於後面還有其他的索引鏈,所以此處的索引鏈我稱爲基本索引鏈。

DocConsumer consumer 類型爲DocFieldProcessor,是整個索引鏈的源頭,包含如下部分:

  • 對索引域的處理
    • DocFieldConsumer consumer 類型爲DocInverter,包含如下部分
      • InvertedDocConsumer consumer類型爲TermsHash,包含如下部分
        • TermsHashConsumer consumer類型爲FreqProxTermsWriter,負責寫freq, prox信息
        • TermsHash nextTermsHash
          • TermsHashConsumer consumer類型爲TermVectorsTermsWriter,負責寫tvx, tvd, tvf信息
      • InvertedDocEndConsumer endConsumer 類型爲NormsWriter,負責寫nrm信息
  • 對存儲域的處理
    • FieldInfos fieldInfos = new FieldInfos();
    • StoredFieldsWriter fieldsWriter負責寫fnm, fdt, fdx信息
  • 刪除文檔
    • BufferedDeletes deletesInRAM = new BufferedDeletes();
    • BufferedDeletes deletesFlushed = new BufferedDeletes();

類BufferedDeletes包含了一下的成員變量:

  • HashMap terms = new HashMap();刪除的詞(Term)
  • HashMap queries = new HashMap();刪除的查詢(Query)
  • List docIDs = new ArrayList();刪除的文檔ID
  • long bytesUsed:用於判斷是否應該對刪除的文檔寫入索引文件。

由此可見,文檔的刪除主要有三種方式:

  • IndexWriter.deleteDocuments(Term term):所有包含此詞的文檔都會被刪除。
  • IndexWriter.deleteDocuments(Query query):所有能滿足此查詢的文檔都會被刪除。
  • IndexReader.deleteDocument(int docNum):刪除此文檔ID

刪除文檔既可以用reader進行刪除,也可以用writer進行刪除,不同的是,reader進行刪除後,此reader馬上能夠生效,而用writer刪除後,會被緩存在deletesInRAM及deletesFlushed中,只有寫入到索引文件中,當reader再次打開的時候,才能夠看到。

那deletesInRAM和deletesFlushed各有什麼用處呢?

此版本的Lucene對文檔的刪除是支持多線程的,當用IndexWriter刪除文檔的時候,都是緩存在deletesInRAM中的,直到flush,纔將刪除的文檔寫入到索引文件中去,我們知道flush是需要一段時間的,那麼在flush的過程中,另一個線程又有文檔刪除怎麼辦呢?

一般過程是這個樣子的,當flush的時候,首先在同步(synchornized)的方法pushDeletes中,將deletesInRAM全部加到deletesFlushed中,然後將deletesInRAM清空,退出同步方法,於是flush的線程程就向索引文件寫deletesFlushed中的刪除文檔的過程,而與此同時其他線程新刪除的文檔則添加到新的deletesInRAM中去,直到下次flush才寫入索引文件。

  • 緩存管理
    • 爲了提高索引的速度,Lucene對很多的數據進行了緩存,使一起寫入磁盤,然而緩存需要進行管理,何時分配,何時回收,何時寫入磁盤都需要考慮。
    • ArrayList freeCharBlocks = new ArrayList();將用於緩存詞(Term)信息的空閒塊
    • ArrayList freeByteBlocks = new ArrayList();將用於緩存文檔號(doc id)及詞頻(freq),位置(prox)信息的空閒塊。
    • ArrayList freeIntBlocks = new ArrayList();將存儲某詞的詞頻(freq)和位置(prox)分別在byteBlocks中的偏移量
    • boolean bufferIsFull;用來判斷緩存是否滿了,如果滿了,則應該寫入磁盤
    • long numBytesAlloc;分配的內存數量
    • long numBytesUsed;使用的內存數量
    • long freeTrigger;應該開始回收內存時的內存用量。
    • long freeLevel;回收內存應該回收到的內存用量。
    • long ramBufferSize;用戶設定的內存用量。
緩存用量之間的關係如下: 
DocumentsWriter.setRAMBufferSizeMB(double mb){ 

    ramBufferSize = (long) (mb*1024*1024);//用戶設定的內存用量,當使用內存大於此時,開始寫入磁盤 
    waitQueuePauseBytes = (long) (ramBufferSize*0.1); 
    waitQueueResumeBytes = (long) (ramBufferSize*0.05); 
    freeTrigger = (long) (1.05 * ramBufferSize);//當分配的內存到達105%的時候開始釋放freeBlocks中的內存 
    freeLevel = (long) (0.95 * ramBufferSize);//一直釋放到95%



DocumentsWriter.balanceRAM(){ 
    if (numBytesAlloc+deletesRAMUsed > freeTrigger) { 
    //當分配的內存加刪除文檔所佔用的內存大於105%的時候,開始釋放內存 
        while(numBytesAlloc+deletesRAMUsed > freeLevel) { 
        //一直進行釋放,直到95% 

            //釋放free blocks

            byteBlockAllocator.freeByteBlocks.remove(byteBlockAllocator.freeByteBlocks.size()-1); 
            numBytesAlloc -= BYTE_BLOCK_SIZE;

            freeCharBlocks.remove(freeCharBlocks.size()-1); 
            numBytesAlloc -= CHAR_BLOCK_SIZE * CHAR_NUM_BYTE;

            freeIntBlocks.remove(freeIntBlocks.size()-1); 
            numBytesAlloc -= INT_BLOCK_SIZE * INT_NUM_BYTE; 
        } 
    } else {

        if (numBytesUsed+deletesRAMUsed > ramBufferSize){

        //當使用的內存加刪除文檔佔有的內存大於用戶指定的內存時,可以寫入磁盤

              bufferIsFull = true;

        }

    } 
}

當判斷是否應該寫入磁盤時:

  • 如果使用的內存大於用戶指定內存時,bufferIsFull = true
  • 當使用的內存加刪除文檔所佔的內存加正在寫入的刪除文檔所佔的內存大於用戶指定內存時 deletesInRAM.bytesUsed + deletesFlushed.bytesUsed + numBytesUsed) >= ramBufferSize
  • 當刪除的文檔數目大於maxBufferedDeleteTerms時

DocumentsWriter.timeToFlushDeletes(){

    return (bufferIsFull || deletesFull()) && setFlushPending();

}

DocumentsWriter.deletesFull(){

    return (ramBufferSize != IndexWriter.DISABLE_AUTO_FLUSH && 
        (deletesInRAM.bytesUsed + deletesFlushed.bytesUsed + numBytesUsed) >= ramBufferSize) || 
        (maxBufferedDeleteTerms != IndexWriter.DISABLE_AUTO_FLUSH && 
        ((deletesInRAM.size() + deletesFlushed.size()) >= maxBufferedDeleteTerms));

}

  • 多線程併發索引
    • 爲了支持多線程併發索引,對每一個線程都有一個DocumentsWriterThreadState,其爲每一個線程根據DocConsumer consumer的索引鏈來創建每個線程的索引鏈(XXXPerThread),來進行對文檔的併發處理。
    • DocumentsWriterThreadState[] threadStates = new DocumentsWriterThreadState[0];
    • HashMap threadBindings = new HashMap();
    • 雖然對文檔的處理過程可以並行,但是將文檔寫入索引文件卻必須串行進行,串行寫入的代碼在DocumentsWriter.finishDocument中
    • WaitQueue waitQueue = new WaitQueue()
    • long waitQueuePauseBytes
    • long waitQueueResumeBytes

在Lucene中,文檔是按添加的順序編號的,DocumentsWriter中的nextDocID就是記錄下一個添加的文檔id。 當Lucene支持多線程的時候,就必須要有一個synchornized方法來付給文檔id並且將nextDocID加一,這些是在DocumentsWriter.getThreadState這個函數裏面做的。

雖然給文檔付ID沒有問題了。但是由Lucene索引文件格式我們知道,文檔是要按照ID的順序從小到大寫到索引文件中去的,然而不同的文檔處理速度不同,當一個先來的線程一處理一篇需要很長時間的大文檔時,另一個後來的線程二可能已經處理了很多小的文檔了,但是這些後來小文檔的ID號都大於第一個線程所處理的大文檔,因而不能馬上寫到索引文件中去,而是放到waitQueue中,僅僅當大文檔處理完了之後才寫入索引文件。

waitQueue中有一個變量nextWriteDocID表示下一個可以寫入文件的ID,當付給大文檔ID=4時,則nextWriteDocID也設爲4,雖然後來的小文檔5,6,7,8等都已處理結束,但是如下代碼,

WaitQueue.add(){

    if (doc.docID == nextWriteDocID){ 
       ………… 
    } else { 
        waiting[loc] = doc; 
        waitingBytes += doc.sizeInBytes(); 
   }

   doPause()

}

則把5, 6, 7, 8放入waiting隊列,並且記錄當前等待的文檔所佔用的內存大小waitingBytes。

當大文檔4處理完畢後,不但寫入文檔4,把原來等待的文檔5, 6, 7, 8也一起寫入。

WaitQueue.add(){

    if (doc.docID == nextWriteDocID) {

       writeDocument(doc);

       while(true) {

           doc = waiting[nextWriteLoc];

           writeDocument(doc);

       }

   } else {

      …………

   }

   doPause()

}

但是這存在一個問題:當大文檔很大很大,處理的很慢很慢的時候,後來的線程二可能已經處理了很多的小文檔了,這些文檔都是在waitQueue中,則佔有了越來越多的內存,長此以往,有內存不夠的危險。

因而在finishDocuments裏面,在WaitQueue.add最後調用了doPause()函數

DocumentsWriter.finishDocument(){

    doPause = waitQueue.add(docWriter);

    if (doPause) 
        waitForWaitQueue();

    notifyAll();

}

WaitQueue.doPause() { 
    return waitingBytes > waitQueuePauseBytes; 
}

當waitingBytes足夠大的時候(爲用戶指定的內存使用量的10%),doPause返回true,於是後來的線程二會進入wait狀態,不再處理另外的文檔,而是等待線程一處理大文檔結束。

當線程一處理大文檔結束的時候,調用notifyAll喚醒等待他的線程。

DocumentsWriter.waitForWaitQueue() { 
  do { 
    try { 
      wait(); 
    } catch (InterruptedException ie) { 
      throw new ThreadInterruptedException(ie); 
    } 
  } while (!waitQueue.doResume()); 
}

WaitQueue.doResume() { 
     return waitingBytes <= waitQueueResumeBytes; 
}

當waitingBytes足夠小的時候,doResume返回true, 則線程二不用再wait了,可以繼續處理另外的文檔。

  • 一些標誌位
    • int maxFieldLength:一篇文檔中,一個域內可索引的最大的詞(Term)數。
    • int maxBufferedDeleteTerms:可緩存的最大的刪除詞(Term)數。當大於這個數的時候,就要寫到文件中了。

此過程又包含如下三個子過程:

4.1、得到當前線程對應的文檔集處理對象(DocumentsWriterThreadState)

代碼爲:

DocumentsWriterThreadState state = getThreadState(doc, delTerm);

在Lucene中,對於同一個索引文件夾,只能夠有一個IndexWriter打開它,在打開後,在文件夾中,生成文件write.lock,當其他IndexWriter再試圖打開此索引文件夾的時候,則會報org.apache.lucene.store.LockObtainFailedException錯誤。

這樣就出現了這樣一個問題,在同一個進程中,對同一個索引文件夾,只能有一個IndexWriter打開它,因而如果想多線程向此索引文件夾中添加文檔,則必須共享一個IndexWriter,而且在以往的實現中,addDocument函數是同步的(synchronized),也即多線程的索引並不能起到提高性能的效果。

於是爲了支持多線程索引,不使IndexWriter成爲瓶頸,對於每一個線程都有一個相應的文檔集處理對象(DocumentsWriterThreadState),這樣對文檔的索引過程可以多線程並行進行,從而增加索引的速度。

getThreadState函數是同步的(synchronized),DocumentsWriter有一個成員變量threadBindings,它是一個HashMap,鍵爲線程對象(Thread.currentThread()),值爲此線程對應的DocumentsWriterThreadState對象。

DocumentsWriterThreadState DocumentsWriter.getThreadState(Document doc, Term delTerm)包含如下幾個過程:

  • 根據當前線程對象,從HashMap中查找相應的DocumentsWriterThreadState對象,如果沒找到,則生成一個新對象,並添加到HashMap中
DocumentsWriterThreadState state = (DocumentsWriterThreadState) threadBindings.get(Thread.currentThread()); 
if (state == null) { 
    …… 
    state = new DocumentsWriterThreadState(this); 
    …… 
    threadBindings.put(Thread.currentThread(), state); 
  • 如果此線程對象正在用於處理上一篇文檔,則等待,直到此線程的上一篇文檔處理完。
DocumentsWriter.getThreadState() { 
    waitReady(state); 
    state.isIdle = false; 


waitReady(state) { 
    while (!state.isIdle) {wait();} 
}  

顯然如果state.isIdle爲false,則此線程等待。 
在一篇文檔處理之前,state.isIdle = false會被設定,而在一篇文檔處理完畢之後,DocumentsWriter.finishDocument(DocumentsWriterThreadState perThread, DocWriter docWriter)中,會首先設定perThread.isIdle = true; 然後notifyAll()來喚醒等待此文檔完成的線程,從而處理下一篇文檔。
  • 如果IndexWriter剛剛commit過,則新添加的文檔要加入到新的段中(segment),則首先要生成新的段名。
initSegmentName(false); 
--> if (segment == null) segment = writer.newSegmentName();
  • 將此線程的文檔處理對象設爲忙碌:state.isIdle = false;

4.2、用得到的文檔集處理對象(DocumentsWriterThreadState)處理文檔

代碼爲:

DocWriter perDoc = state.consumer.processDocument();

每一個文檔集處理對象DocumentsWriterThreadState都有一個文檔及域處理對象DocFieldProcessorPerThread,它的成員函數processDocument()被調用來對文檔及域進行處理。

線程索引鏈(XXXPerThread):

由於要多線程進行索引,因而每個線程都要有自己的索引鏈,稱爲線程索引鏈。

線程索引鏈同基本索引鏈有相似的樹形結構,由基本索引鏈中每個層次的對象調用addThreads進行創建的,負責每個線程的對文檔的處理。

DocFieldProcessorPerThread是線程索引鏈的源頭,由DocFieldProcessor.addThreads(…)創建

DocFieldProcessorPerThread對象結構如下:

  • 對索引域進行處理
    • DocFieldConsumerPerThread consumer 類型爲 DocInverterPerThread,由DocInverter.addThreads創建
      • InvertedDocConsumerPerThread consumer 類型爲TermsHashPerThread,由TermsHash.addThreads創建
        • TermsHashConsumerPerThread consumer類型爲FreqProxTermsWriterPerThread,由FreqProxTermsWriter.addThreads創建,負責每個線程的freq,prox信息處理
        • TermsHashPerThread nextPerThread
          • TermsHashConsumerPerThread consumer類型TermVectorsTermsWriterPerThread,由TermVectorsTermsWriter創建,負責每個線程的tvx,tvd,tvf信息處理
      • InvertedDocEndConsumerPerThread endConsumer 類型爲NormsWriterPerThread,由NormsWriter.addThreads創建,負責nrm信息的處理
  • 對存儲域進行處理
    • StoredFieldsWriterPerThread fieldsWriter由StoredFieldsWriter.addThreads創建,負責fnm,fdx,fdt的處理。
    • FieldInfos fieldInfos;

DocumentsWriter.DocWriter DocFieldProcessorPerThread.processDocument()包含以下幾個過程:

4.2.1、開始處理當前文檔

consumer(DocInverterPerThread).startDocument(); 
fieldsWriter(StoredFieldsWriterPerThread).startDocument();

在此版的Lucene中,幾乎所有的XXXPerThread的類,都有startDocument和finishDocument兩個函數,因爲對同一個線程,這些對象都是複用的,而非對每一篇新來的文檔都創建一套,這樣也提高了效率,也牽扯到數據的清理問題。一般在startDocument函數中,清理處理上篇文檔遺留的數據,在finishDocument中,收集本次處理的結果數據,並返回,一直返回到DocumentsWriter.updateDocument(Document, Analyzer, Term) 然後根據條件判斷是否將數據刷新到硬盤上。

4.2.2、逐個處理文檔的每一個域

由於一個線程可以連續處理多個文檔,而在普通的應用中,幾乎每篇文檔的域都是大致相同的,爲每篇文檔的每個域都創建一個處理對象非常低效,因而考慮到複用域處理對象DocFieldProcessorPerField,對於每一個域都有一個此對象。

那當來到一個新的域的時候,如何更快的找到此域的處理對象呢?Lucene創建了一個DocFieldProcessorPerField[] fieldHash哈希表來方便更快查找域對應的處理對象。

當處理各個域的時候,按什麼順序呢?其實是按照域名的字典順序。因而Lucene創建了DocFieldProcessorPerField[] fields的數組來方便按順序處理域。

因而一個域的處理對象被放在了兩個地方。

對於域的處理過程如下:

4.2.2.1、首先:對於每一個域,按照域名,在fieldHash中查找域處理對象DocFieldProcessorPerField,代碼如下:

final int hashPos = fieldName.hashCode() & hashMask;//計算哈希值 
DocFieldProcessorPerField fp = fieldHash[hashPos];//找到哈希表中對應的位置 
while(fp != null && !fp.fieldInfo.name.equals(fieldName)) fp = fp.next;//鏈式哈希表

如果能夠找到,則更新DocFieldProcessorPerField中的域信息fp.fieldInfo.update(field.isIndexed()…)

如果沒有找到,則添加域到DocFieldProcessorPerThread.fieldInfos中,並創建新的DocFieldProcessorPerField,且將其加入哈希表。代碼如下:

fp = new DocFieldProcessorPerField(this, fi); 
fp.next = fieldHash[hashPos]; 
fieldHash[hashPos] = fp;

如果是一個新的field,則將其加入fields數組fields[fieldCount++] = fp;

並且如果是存儲域的話,用StoredFieldsWriterPerThread將其寫到索引中:

if (field.isStored()) { 
  fieldsWriter.addField(field, fp.fieldInfo); 
}

4.2.2.1.1、處理存儲域的過程如下:

StoredFieldsWriterPerThread.addField(Fieldable field, FieldInfo fieldInfo) 
--> localFieldsWriter.writeField(fieldInfo, field);

FieldsWriter.writeField(FieldInfo fi, Fieldable field)代碼如下:

請參照fdt文件的格式,則一目瞭然:

fieldsStream.writeVInt(fi.number);//文檔號 
byte bits = 0; 
if (field.isTokenized()) 
    bits |= FieldsWriter.FIELD_IS_TOKENIZED; 
if (field.isBinary()) 
    bits |= FieldsWriter.FIELD_IS_BINARY; 
if (field.isCompressed()) 
    bits |= FieldsWriter.FIELD_IS_COMPRESSED;

fieldsStream.writeByte(bits); //域的屬性位

if (field.isCompressed()) {//對於壓縮域 
    // compression is enabled for the current field 
    final byte[] data; 
    final int len; 
    final int offset; 
    // check if it is a binary field 
    if (field.isBinary()) { 
        data = CompressionTools.compress(field.getBinaryValue(), field.getBinaryOffset(), field.getBinaryLength()); 
    } else { 
        byte x[] = field.stringValue().getBytes("UTF-8"); 
        data = CompressionTools.compress(x, 0, x.length); 
    } 
    len = data.length; 
    offset = 0; 
    fieldsStream.writeVInt(len);//寫長度 
    fieldsStream.writeBytes(data, offset, len);//寫二進制內容 
} else {//對於非壓縮域 
    // compression is disabled for the current field 
    if (field.isBinary()) {//如果是二進制域 
        final byte[] data; 
        final int len; 
        final int offset; 
        data = field.getBinaryValue(); 
        len = field.getBinaryLength(); 
        offset = field.getBinaryOffset();

        fieldsStream.writeVInt(len);//寫長度 
        fieldsStream.writeBytes(data, offset, len);//寫二進制內容 
    } else { 
        fieldsStream.writeString(field.stringValue());//寫字符內容 
    } 
}

4.2.2.2、然後:對fields數組進行排序,是域按照名稱排序。quickSort(fields, 0, fieldCount-1);

4.2.2.3、最後:按照排序號的順序,對域逐個處理,此處處理的僅僅是索引域,代碼如下:

for(int i=0;i      fields[i].consumer.processFields(fields[i].fields, fields[i].fieldCount);

域處理對象(DocFieldProcessorPerField)結構如下:

域索引鏈:

每個域也有自己的索引鏈,稱爲域索引鏈,每個域的索引鏈也有同線程索引鏈有相似的樹形結構,由線程索引鏈中每個層次的每個層次的對象調用addField進行創建,負責對此域的處理。

和基本索引鏈及線程索引鏈不同的是,域索引鏈僅僅負責處理索引域,而不負責存儲域的處理。

DocFieldProcessorPerField是域索引鏈的源頭,對象結構如下:

  • DocFieldConsumerPerField consumer類型爲DocInverterPerField,由DocInverterPerThread.addField創建
    • InvertedDocConsumerPerField consumer 類型爲TermsHashPerField,由TermsHashPerThread.addField創建
      • TermsHashConsumerPerField consumer 類型爲FreqProxTermsWriterPerField,由FreqProxTermsWriterPerThread.addField創建,負責freq, prox信息的處理
      • TermsHashPerField nextPerField
        • TermsHashConsumerPerField consumer 類型爲TermVectorsTermsWriterPerField,由TermVectorsTermsWriterPerThread.addField創建,負責tvx, tvd, tvf信息的處理
    • InvertedDocEndConsumerPerField endConsumer 類型爲NormsWriterPerField,由NormsWriterPerThread.addField創建,負責nrm信息的處理。

4.2.2.3.1、處理索引域的過程如下:

DocInverterPerField.processFields(Fieldable[], int) 過程如下:

  • 判斷是否要形成倒排表,代碼如下:
boolean doInvert = consumer.start(fields, count); 
--> TermsHashPerField.start(Fieldable[], int)  
      --> for(int i=0;i             if (fields[i].isIndexed()) 
                 return true; 
            return false;

讀到這裏,大家可能會發生困惑,既然XXXPerField是對於每一個域有一個處理對象的,那爲什麼參數傳進來的是Fieldable[]數組, 並且還有域的數目count呢?

其實這不經常用到,但必須得提一下,由上面的fieldHash的實現我們可以看到,是根據域名進行哈希的,所以準確的講,XXXPerField並非對於每一個域有一個處理對象,而是對每一組相同名字的域有相同的處理對象。

對於同一篇文檔,相同名稱的域可以添加多個,代碼如下:

doc.add(new Field("contents", "the content of the file.", Field.Store.NO, Field.Index.NOT_ANALYZED)); 
doc.add(new Field("contents", new FileReader(f)));

則傳進來的名爲"contents"的域如下:

fields    Fieldable[2]  (id=52)    
    [0]    Field  (id=56)    
        binaryLength    0    
        binaryOffset    0    
        boost    1.0    
        fieldsData    "the content of the file."    
        isBinary    false    
        isCompressed    false    
        isIndexed    true    
        isStored    false    
        isTokenized    false    
        lazy    false    
        name    "contents"    
        omitNorms    false    
        omitTermFreqAndPositions    false    
        storeOffsetWithTermVector    false    
        storePositionWithTermVector    false    
        storeTermVector    false    
        tokenStream    null    
    [1]    Field  (id=58)    
        binaryLength    0    
        binaryOffset    0    
        boost    1.0    
        fieldsData    FileReader  (id=131)    
        isBinary    false    
        isCompressed    false    
        isIndexed    true    
        isStored    false    
        isTokenized    true    
        lazy    false    
        name    "contents"    
        omitNorms    false    
        omitTermFreqAndPositions    false    
        storeOffsetWithTermVector    false    
        storePositionWithTermVector    false    
        storeTermVector    false    
        tokenStream    null   

  • 對傳進來的同名域逐一處理,代碼如下

for(int i=0;i

    final Fieldable field = fields[i];

    if (field.isIndexed() && doInvert) {

        //僅僅對索引域進行處理

        if (!field.isTokenized()) {

            //如果此域不分詞,見(1)對不分詞的域的處理

        } else {

            //如果此域分詞,見(2)對分詞的域的處理

        }

    }

}

(1) 對不分詞的域的處理

(1-1) 得到域的內容,並構建單個Token形成的SingleTokenAttributeSource。因爲不進行分詞,因而整個域的內容算做一個Token.

String stringValue = field.stringValue(); //stringValue    "200910240957"  
final int valueLength = stringValue.length(); 
perThread.singleToken.reinit(stringValue, 0, valueLength);

對於此域唯一的一個Token有以下的屬性:

  • Term:文字信息。在處理過程中,此值將保存在TermAttribute的實現類實例化的對象TermAttributeImp裏面。
  • Offset:偏移量信息,是按字或字母的起始偏移量和終止偏移量,表明此Token在文章中的位置,多用於加亮。在處理過程中,此值將保存在OffsetAttribute的實現類實例化的對象OffsetAttributeImp裏面。

在SingleTokenAttributeSource裏面,有一個HashMap來保存可能用於保存屬性的類名(Key,準確的講是接口)以及保存屬性信息的對象(Value):

singleToken    DocInverterPerThread$SingleTokenAttributeSource  (id=150)    
    attributeImpls    LinkedHashMap  (id=945)    
    attributes    LinkedHashMap  (id=946)     
        size    2    
        table    HashMap$Entry[16]  (id=988)    
            [0]    LinkedHashMap$Entry  (id=991)     
                key    Class (org.apache.lucene.analysis.tokenattributes.TermAttribute) (id=755)     
                value    TermAttributeImpl  (id=949)    
                    termBuffer    char[19]  (id=954)    //[2, 0, 0, 9, 1, 0, 2, 4, 0, 9, 5, 7] 
                    termLength    12
     
            [7]    LinkedHashMap$Entry  (id=993)     
                key    Class (org.apache.lucene.analysis.tokenattributes.OffsetAttribute) (id=274)     
                value    OffsetAttributeImpl  (id=948)    
                    endOffset    12    
                    startOffset    0
     
    factory    AttributeSource$AttributeFactory$DefaultAttributeFactory  (id=947)    
    offsetAttribute    OffsetAttributeImpl  (id=948)    
    termAttribute    TermAttributeImpl  (id=949)   

(1-2) 得到Token的各種屬性信息,爲索引做準備。

consumer.start(field)做的主要事情就是根據各種屬性的類型來構造保存屬性的對象(HashMap中有則取出,無則構造),爲索引做準備。

consumer(TermsHashPerField).start(…)

--> termAtt = fieldState.attributeSource.addAttribute(TermAttribute.class);得到的就是上述HashMap中的TermAttributeImpl   

--> consumer(FreqProxTermsWriterPerField).start(f);

      --> if (fieldState.attributeSource.hasAttribute(PayloadAttribute.class)) {

                payloadAttribute = fieldState.attributeSource.getAttribute(PayloadAttribute.class); 
                存儲payload信息則得到payload的屬}

--> nextPerField(TermsHashPerField).start(f);

      --> termAtt = fieldState.attributeSource.addAttribute(TermAttribute.class);得到的還是上述HashMap中的TermAttributeImpl

      --> consumer(TermVectorsTermsWriterPerField).start(f);

            --> if (doVectorOffsets) {

                      offsetAttribute = fieldState.attributeSource.addAttribute(OffsetAttribute.class); 
                      如果存儲詞向量則得到的是上述HashMap中的OffsetAttributeImp }

(1-3) 將Token加入倒排表

consumer(TermsHashPerField).add();

加入倒排表的過程,無論對於分詞的域和不分詞的域,過程是一樣的,因而放到對分詞的域的解析中一起說明。

(2) 對分詞的域的處理

(2-1) 構建域的TokenStream

final TokenStream streamValue = field.tokenStreamValue();

//用戶可以在添加域的時候,應用構造函數public Field(String name, TokenStream tokenStream) 直接傳進一個TokenStream過來,這樣就不用另外構建一個TokenStream了。

if (streamValue != null) 
  stream = streamValue; 
else {

  ……

  stream = docState.analyzer.reusableTokenStream(fieldInfo.name, reader);

}

此時TokenStream的各項屬性值還都是空的,等待一個一個被分詞後得到,此時的TokenStream對象如下:

stream    StopFilter  (id=112)    
    attributeImpls    LinkedHashMap  (id=121)    
    attributes    LinkedHashMap  (id=122)     
        size    4    
        table    HashMap$Entry[16]  (id=146)     
            [2]    LinkedHashMap$Entry  (id=148)     
                key    Class (org.apache.lucene.analysis.tokenattributes.TypeAttribute) (id=154)     
                value    TypeAttributeImpl  (id=157)    
                    type    "word"     
            [8]    LinkedHashMap$Entry  (id=150)    
                after    LinkedHashMap$Entry  (id=156)     
                    key    Class (org.apache.lucene.analysis.tokenattributes.OffsetAttribute) (id=163)     
                    value    OffsetAttributeImpl  (id=164)    
                        endOffset    0    
                        startOffset    0
     
                key    Class (org.apache.lucene.analysis.tokenattributes.TermAttribute) (id=142)     
                value    TermAttributeImpl  (id=133)    
                    termBuffer    char[17]  (id=173)    
                    termLength    0
     
            [10]    LinkedHashMap$Entry  (id=151)     
                key    Class (org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute) (id=136)     
                value    PositionIncrementAttributeImpl  (id=129)    
                    positionIncrement    1     
    currentState    AttributeSource$State  (id=123)    
    enablePositionIncrements    true    
    factory    AttributeSource$AttributeFactory$DefaultAttributeFactory  (id=125)    
    input    LowerCaseFilter  (id=127)     
        input    StandardFilter  (id=213)     
            input    StandardTokenizer  (id=218)     
                input    FileReader  (id=93)    //從文件中讀出來的文本,將經過分詞器分詞,並一層層的Filter的處理,得到一個個Token 
    stopWords    CharArraySet$UnmodifiableCharArraySet  (id=131)    
    termAtt    TermAttributeImpl  (id=133)   

(2-2) 得到第一個Token,並初始化此Token的各項屬性信息,併爲索引做準備(start)。

boolean hasMoreTokens = stream.incrementToken();//得到第一個Token

OffsetAttribute offsetAttribute = fieldState.attributeSource.addAttribute(OffsetAttribute.class);//得到偏移量屬性

offsetAttribute    OffsetAttributeImpl  (id=164)    
    endOffset    8    
    startOffset    0   

PositionIncrementAttribute posIncrAttribute = fieldState.attributeSource.addAttribute(PositionIncrementAttribute.class);//得到位置屬性

posIncrAttribute    PositionIncrementAttributeImpl  (id=129)    
    positionIncrement    1   

consumer.start(field);//其中得到了TermAttribute屬性,如果存儲payload則得到PayloadAttribute屬性,如果存儲詞向量則得到OffsetAttribute屬性。

(2-3) 進行循環,不斷的取下一個Token,並添加到倒排表

for(;;) {

    if (!hasMoreTokens) break;

    …… 
    consumer.add();

    …… 
    hasMoreTokens = stream.incrementToken(); 
}

(2-4) 添加Token到倒排表的過程consumer(TermsHashPerField).add()

TermsHashPerField對象主要包括以下部分:

  • CharBlockPool charPool; 用於存儲Token的文本信息,如果不足時,從DocumentsWriter中的freeCharBlocks分配
  • ByteBlockPool bytePool;用於存儲freq, prox信息,如果不足時,從DocumentsWriter中的freeByteBlocks分配
  • IntBlockPool intPool; 用於存儲分別指向每個Token在bytePool中freq和prox信息的偏移量。如果不足時,從DocumentsWriter的freeIntBlocks分配
  • TermsHashConsumerPerField consumer類型爲FreqProxTermsWriterPerField,用於寫freq, prox信息到緩存中。
  • RawPostingList[] postingsHash = new RawPostingList[postingsHashSize];存儲倒排表,每一個Term都有一個RawPostingList (PostingList),其中包含了int textStart,也即文本在charPool中的偏移量,int byteStart,即此Term的freq和prox信息在bytePool中的起始偏移量,int intStart,即此term的在intPool中的起始偏移量。

形成倒排表的過程如下:

//得到token的文本及文本長度

final char[] tokenText = termAtt.termBuffer();//[s, t, u, d, e, n, t, s]

final int tokenTextLen = termAtt.termLength();//tokenTextLen 8

//按照token的文本計算哈希值,以便在postingsHash中找到此token對應的倒排表

int downto = tokenTextLen; 
int code = 0; 
while (downto > 0) { 
  char ch = tokenText[—downto]; 
  code = (code*31) + ch; 
}

int hashPos = code & postingsHashMask;

//在倒排表哈希表中查找此Token,如果找到相應的位置,但是不是此Token,說明此位置存在哈希衝突,採取重新哈希rehash的方法。

p = postingsHash[hashPos];

if (p != null && !postingEquals(tokenText, tokenTextLen)) {  
  final int inc = ((code>>8)+code)|1; 
  do { 
    code += inc; 
    hashPos = code & postingsHashMask; 
    p = postingsHash[hashPos]; 
  } while (p != null && !postingEquals(tokenText, tokenTextLen)); 
}

//如果此Token之前從未出現過

if (p == null) {

    if (textLen1 + charPool.charUpto > DocumentsWriter.CHAR_BLOCK_SIZE) {

        //當charPool不足的時候,在freeCharBlocks中分配新的buffer

        charPool.nextBuffer();

    }

    //從空閒的倒排表中分配新的倒排表

    p = perThread.freePostings[--perThread.freePostingsCount];

    //將文本複製到charPool中

    final char[] text = charPool.buffer; 
    final int textUpto = charPool.charUpto; 
    p.textStart = textUpto + charPool.charOffset; 
    charPool.charUpto += textLen1; 
    System.arraycopy(tokenText, 0, text, textUpto, tokenTextLen); 
    text[textUpto+tokenTextLen] = 0xffff;

    //將倒排表放入哈希表中

    postingsHash[hashPos] = p; 
    numPostings++;

    if (numPostingInt + intPool.intUpto > DocumentsWriter.INT_BLOCK_SIZE) intPool.nextBuffer();

    //當intPool不足的時候,在freeIntBlocks中分配新的buffer。

    if (DocumentsWriter.BYTE_BLOCK_SIZE - bytePool.byteUpto < numPostingInt*ByteBlockPool.FIRST_LEVEL_SIZE)

        bytePool.nextBuffer();

    //當bytePool不足的時候,在freeByteBlocks中分配新的buffer。

    //此處streamCount爲2,表明在intPool中,每兩項表示一個詞,一個是指向bytePool中freq信息偏移量的,一個是指向bytePool中prox信息偏移量的。

    intUptos = intPool.buffer; 
    intUptoStart = intPool.intUpto; 
    intPool.intUpto += streamCount;

    p.intStart = intUptoStart + intPool.intOffset;

    //在bytePool中分配兩個空間,一個放freq信息,一個放prox信息的。  
    for(int i=0;i

        final int upto = bytePool.newSlice(ByteBlockPool.FIRST_LEVEL_SIZE); 
        intUptos[intUptoStart+i] = upto + bytePool.byteOffset; 
    } 
    p.byteStart = intUptos[intUptoStart];

    //當Term原來沒有出現過的時候,調用newTerm

    consumer(FreqProxTermsWriterPerField).newTerm(p);

}

//如果此Token之前曾經出現過,則調用addTerm。

else {

    intUptos = intPool.buffers[p.intStart >> DocumentsWriter.INT_BLOCK_SHIFT]; 
    intUptoStart = p.intStart & DocumentsWriter.INT_BLOCK_MASK; 
    consumer(FreqProxTermsWriterPerField).addTerm(p);

}

(2-5) 添加新Term的過程,consumer(FreqProxTermsWriterPerField).newTerm

final void newTerm(RawPostingList p0) { 
  FreqProxTermsWriter.PostingList p = (FreqProxTermsWriter.PostingList) p0; 
  p.lastDocID = docState.docID; //當一個新的term出現的時候,包含此Term的就只有本篇文檔,記錄其ID 
  p.lastDocCode = docState.docID << 1; //docCode是文檔ID左移一位,爲什麼左移,請參照索引文件格式(1)中的或然跟隨規則。 
  p.docFreq = 1; //docFreq這裏用詞可能容易引起誤會,docFreq這裏指的是此文檔所包含的此Term的次數,並非包含此Term的文檔的個數。 
  writeProx(p, fieldState.position); //寫入prox信息到bytePool中,此時freq信息還不能寫入,因爲當前的文檔還沒有處理完,尚不知道此文檔包含此Term的總數。 
}

writeProx(FreqProxTermsWriter.PostingList p, int proxCode) {

  termsHashPerField.writeVInt(1, proxCode<<1);//第一個參數所謂1,也就是寫入此文檔在intPool中的第1項——prox信息。爲什麼左移一位呢?是因爲後面可能跟着payload信息,參照索引文件格式(1)中或然跟隨規則。 
  p.lastPosition = fieldState.position;//總是要記錄lastDocID, lastPostion,是因爲要計算差值,參照索引文件格式(1)中的差值規則。

}

(2-6) 添加已有Term的過程

final void addTerm(RawPostingList p0) {

  FreqProxTermsWriter.PostingList p = (FreqProxTermsWriter.PostingList) p0;

  if (docState.docID != p.lastDocID) {

      //當文檔ID變了的時候,說明上一篇文檔已經處理完畢,可以寫入freq信息了。

      //第一個參數所謂0,也就是寫入上一篇文檔在intPool中的第0項——freq信息。至於信息爲何這樣寫,參照索引文件格式(1)中的或然跟隨規則,及tis文件格式。

      if (1 == p.docFreq) 
        termsHashPerField.writeVInt(0, p.lastDocCode|1); 
      else { 
        termsHashPerField.writeVInt(0, p.lastDocCode); 
        termsHashPerField.writeVInt(0, p.docFreq); 
      } 
      p.docFreq = 1;//對於新的文檔,freq還是爲1. 
      p.lastDocCode = (docState.docID - p.lastDocID) << 1;//文檔號存儲差值 
      p.lastDocID = docState.docID; 
      writeProx(p, fieldState.position);  
    } else {

      //當文檔ID不變的時候,說明此文檔中這個詞又出現了一次,從而freq加一,寫入再次出現的位置信息,用差值。 
      p.docFreq++; 
      writeProx(p, fieldState.position-p.lastPosition); 
  } 
}

(2-7) 結束處理當前域

consumer(TermsHashPerField).finish();

--> FreqProxTermsWriterPerField.finish()

--> TermVectorsTermsWriterPerField.finish()

endConsumer(NormsWriterPerField).finish();

--> norms[upto] = Similarity.encodeNorm(norm);//計算標準化因子的值。

--> docIDs[upto] = docState.docID;

4.2.3、結束處理當前文檔

final DocumentsWriter.DocWriter one = fieldsWriter(StoredFieldsWriterPerThread).finishDocument();

存儲域返回結果:一個寫成了二進制的存儲域緩存。

one    StoredFieldsWriter$PerDoc  (id=322)    
    docID    0    
    fdt    RAMOutputStream  (id=325)    
        bufferLength    1024    
        bufferPosition    40    
        bufferStart    0    
        copyBuffer    null    
        currentBuffer    byte[1024]  (id=332)    
        currentBufferIndex    0    
        file    RAMFile  (id=333)    
        utf8Result    UnicodeUtil$UTF8Result  (id=335)    
    next    null    
    numStoredFields    2    
    this$0    StoredFieldsWriter  (id=327)   

final DocumentsWriter.DocWriter two = consumer(DocInverterPerThread).finishDocument();

--> NormsWriterPerThread.finishDocument()

--> TermsHashPerThread.finishDocument()

索引域的返回結果爲null

4.3、用DocumentsWriter.finishDocument結束本次文檔添加

代碼:

DocumentsWriter.updateDocument(Document, Analyzer, Term)

--> DocumentsWriter.finishDocument(DocumentsWriterThreadState, DocumentsWriter$DocWriter)

      --> doPause = waitQueue.add(docWriter);//有關waitQueue,在DocumentsWriter的緩存管理中已作解釋

            --> DocumentsWriter$WaitQueue.writeDocument(DocumentsWriter$DocWriter)

                  --> StoredFieldsWriter$PerDoc.finish()

                        --> fieldsWriter.flushDocument(perDoc.numStoredFields, perDoc.fdt);將存儲域信息真正寫入文件。

發佈了33 篇原創文章 · 獲贊 1 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章