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,是整個索引鏈的源頭,包含如下部分:
|
- 刪除文檔
- BufferedDeletes deletesInRAM = new BufferedDeletes();
- BufferedDeletes deletesFlushed = new BufferedDeletes();
類BufferedDeletes包含了一下的成員變量:
由此可見,文檔的刪除主要有三種方式:
刪除文檔既可以用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);//用戶設定的內存用量,當使用內存大於此時,開始寫入磁盤 DocumentsWriter.balanceRAM(){ if (numBytesAlloc+deletesRAMUsed > freeTrigger) { //當分配的內存加刪除文檔所佔用的內存大於105%的時候,開始釋放內存 while(numBytesAlloc+deletesRAMUsed > freeLevel) { //一直進行釋放,直到95% //釋放free blocks byteBlockAllocator.freeByteBlocks.remove(byteBlockAllocator.freeByteBlocks.size()-1); freeCharBlocks.remove(freeCharBlocks.size()-1); freeIntBlocks.remove(freeIntBlocks.size()-1); if (numBytesUsed+deletesRAMUsed > ramBufferSize){ //當使用的內存加刪除文檔佔有的內存大於用戶指定的內存時,可以寫入磁盤 bufferIsFull = true; } } 當判斷是否應該寫入磁盤時:
DocumentsWriter.timeToFlushDeletes(){ return (bufferIsFull || deletesFull()) && setFlushPending(); } DocumentsWriter.deletesFull(){ return (ramBufferSize != IndexWriter.DISABLE_AUTO_FLUSH && } |
- 多線程併發索引
- 爲了支持多線程併發索引,對每一個線程都有一個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){ 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) notifyAll(); } WaitQueue.doPause() { 當waitingBytes足夠大的時候(爲用戶指定的內存使用量的10%),doPause返回true,於是後來的線程二會進入wait狀態,不再處理另外的文檔,而是等待線程一處理大文檔結束。 當線程一處理大文檔結束的時候,調用notifyAll喚醒等待他的線程。 DocumentsWriter.waitForWaitQueue() { WaitQueue.doResume() { 當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對象結構如下:
|
DocumentsWriter.DocWriter DocFieldProcessorPerThread.processDocument()包含以下幾個過程:
4.2.1、開始處理當前文檔
consumer(DocInverterPerThread).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.fieldInfo.update(field.isIndexed()…)
如果沒有找到,則添加域到DocFieldProcessorPerThread.fieldInfos中,並創建新的DocFieldProcessorPerField,且將其加入哈希表。代碼如下:
fp = new DocFieldProcessorPerField(this, fi); |
如果是一個新的field,則將其加入fields數組fields[fieldCount++] = fp;
並且如果是存儲域的話,用StoredFieldsWriterPerThread將其寫到索引中:
if (field.isStored()) { |
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);//文檔號 fieldsStream.writeByte(bits); //域的屬性位 if (field.isCompressed()) {//對於壓縮域 fieldsStream.writeVInt(len);//寫長度 |
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是域索引鏈的源頭,對象結構如下:
|
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)); |
則傳進來的名爲"contents"的域如下:
fields Fieldable[2] (id=52) |
- 對傳進來的同名域逐一處理,代碼如下
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) |
(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); --> nextPerField(TermsHashPerField).start(f); --> termAtt = fieldState.attributeSource.addAttribute(TermAttribute.class);得到的還是上述HashMap中的TermAttributeImpl --> consumer(TermVectorsTermsWriterPerField).start(f); --> if (doVectorOffsets) { offsetAttribute = fieldState.attributeSource.addAttribute(OffsetAttribute.class); |
(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 = docState.analyzer.reusableTokenStream(fieldInfo.name, reader); } |
此時TokenStream的各項屬性值還都是空的,等待一個一個被分詞後得到,此時的TokenStream對象如下:
stream StopFilter (id=112) |
(2-2) 得到第一個Token,並初始化此Token的各項屬性信息,併爲索引做準備(start)。
boolean hasMoreTokens = stream.incrementToken();//得到第一個Token
OffsetAttribute offsetAttribute = fieldState.attributeSource.addAttribute(OffsetAttribute.class);//得到偏移量屬性
offsetAttribute OffsetAttributeImpl (id=164) |
PositionIncrementAttribute posIncrAttribute = fieldState.attributeSource.addAttribute(PositionIncrementAttribute.class);//得到位置屬性
posIncrAttribute PositionIncrementAttributeImpl (id=129) |
consumer.start(field);//其中得到了TermAttribute屬性,如果存儲payload則得到PayloadAttribute屬性,如果存儲詞向量則得到OffsetAttribute屬性。
(2-3) 進行循環,不斷的取下一個Token,並添加到倒排表
for(;;) { if (!hasMoreTokens) break; …… …… |
(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 hashPos = code & postingsHashMask; //在倒排表哈希表中查找此Token,如果找到相應的位置,但是不是此Token,說明此位置存在哈希衝突,採取重新哈希rehash的方法。 p = postingsHash[hashPos]; if (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; //將倒排表放入哈希表中 postingsHash[hashPos] = p; 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; p.intStart = intUptoStart + intPool.intOffset; //在bytePool中分配兩個空間,一個放freq信息,一個放prox信息的。 final int upto = bytePool.newSlice(ByteBlockPool.FIRST_LEVEL_SIZE); //當Term原來沒有出現過的時候,調用newTerm consumer(FreqProxTermsWriterPerField).newTerm(p); } //如果此Token之前曾經出現過,則調用addTerm。 else { intUptos = intPool.buffers[p.intStart >> DocumentsWriter.INT_BLOCK_SHIFT]; } |
(2-5) 添加新Term的過程,consumer(FreqProxTermsWriterPerField).newTerm
final void newTerm(RawPostingList p0) { writeProx(FreqProxTermsWriter.PostingList p, int proxCode) { termsHashPerField.writeVInt(1, proxCode<<1);//第一個參數所謂1,也就是寫入此文檔在intPool中的第1項——prox信息。爲什麼左移一位呢?是因爲後面可能跟着payload信息,參照索引文件格式(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) //當文檔ID不變的時候,說明此文檔中這個詞又出現了一次,從而freq加一,寫入再次出現的位置信息,用差值。 |
(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) |
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);將存儲域信息真正寫入文件。 |