上接《 索引創建(3):DocumentWriter 處理流程二 》
?
1.3.3 第三車間 —— TermsHashPerField & FreqProxTermsWriterPerField
?
TermsHashPerField和FreqProxTermsWriterPerField負責將token信息(字符串內容termTest,所在文檔編號docID,所在文檔中的位置position,所在文檔中的詞頻frequence)添加到索引的Hash表結構(postingsHash)中。事實上,這些信息并不是直接存放在Hash表中的,而是存放在三個很重要的數據緩存池中 (CharBlockPool、IntBlockPool、ByteBlockPool) 。而postingsHash中存放的只是數據在三個數據池中的地址偏移。
?
★ TermsHashPerField
?
這里首先簡單介紹一下這三個數據池, 想要深刻了解這三個數據池, 可見《 索引數據池及內存數據細節 》。
1) CharBlockPool: 存儲token的字面信息
2) ByteBlockPool: 存儲token所在文檔編號,位置和詞頻信息
3) IntBlockPool: 存儲對應的ByteBlockPool中slice的位置
TermsHashPerField類的主要職責就是建立和維護一張postingsHash的倒排索引表。這張表是一張以token字符串作為關鍵字的Hash表。其結構如下:
final class TermsHashPerField extends InvertedDocConsumerPerField { private int postingsHashSize = 4; //倒排索引表的Hash數組結構,初始大小為4 private RawPostingList[] postingsHash = new RawPostingList[postingsHashSize]; }
其中postingHash[]的每個元素都是RawPostingList類型的。該類在內存中表示一個由token唯一標示的posting list(倒排索引結構:token-> posting list)。
//該類在內存中表示一個由token唯一標示的posting list(倒排索引結構: token -> posting list)。 abstract class RawPostingList { int textStart; //存儲token信息在CharBlockPool中的初始位置 int intStart; //存儲token信息在IntBlockPool中的初始位置 ? int byteStart; //存儲token信息在ByteBlockPool中的初始位置 }
實際上postingHash[]的每個元素的實際類型是PostingList。這個類型包含的數據域如下:
static final class PostingList extends RawPostingList { int docFreq; //此詞在當前文檔中出現的次數 int lastDocID; // 上次處理完的包含此詞的文檔號。 int lastDocCode; // 文檔號和詞頻按照或然跟隨原則形成的編碼 int lastPosition; // 上次處理完的此詞的位置 }?
?
TermsHashPerField.add()方法是創建待排索引的核心方法,我們分功能段來解析它的源代碼:
// 將token加入到倒排索引的Hash鏈表結構中:token -> posting list @Override void add() throws IOException {....}
?
1、首先,計算token的hashcode值。
很顯然,下面的代碼表明處理token中的字符采用的是UTF-16編碼方式(Java對字符串都是Unicode編碼的)。想要了解UTF-16編碼方式,可參見《
解析Unicode編碼和Java char
》。另外,token的hashcode計算公式是code=(code*31)+ch,其中ch是token從末尾開始向前遍歷的字符。
//得到當前token的內容 final char[] tokenText = termAtt.termBuffer(); //得到當前token的長度 final int tokenTextLen = termAtt.termLength(); int downto = tokenTextLen; int code = 0; //計算token的hashcode(其中每個字符按照的UTF-16編碼方式進行編碼) while (downto > 0) { //從后往前取出token中的每一個字符 char ch = tokenText[--downto]; //判斷ch是不是Unicode編碼中的替代區域(surrogate area),這個區域不表示任何字符 if (ch >= UnicodeUtil.UNI_SUR_LOW_START && ch <= UnicodeUtil.UNI_SUR_LOW_END) { if (0 == downto) { ch = tokenText[downto] = UnicodeUtil.UNI_REPLACEMENT_CHAR; } else { //取出連續兩個字符進行hashcode計算(UTF-16編碼方式) final char ch2 = tokenText[downto-1]; if (ch2 >= UnicodeUtil.UNI_SUR_HIGH_START && ch2 <= UnicodeUtil.UNI_SUR_HIGH_END) { code = ((code*31) + ch)*31+ch2; downto--; continue; } else { ch = tokenText[downto] = UnicodeUtil.UNI_REPLACEMENT_CHAR; } } } else if (ch >= UnicodeUtil.UNI_SUR_HIGH_START && (ch <= UnicodeUtil.UNI_SUR_HIGH_END || ch == 0xffff)) { ch = tokenText[downto] = UnicodeUtil.UNI_REPLACEMENT_CHAR; } //如果ch是正常字符,則開始計算token的hashcode code = (code*31) + ch; }//end while
?
2、 其次,根據token的hashcode定位到postingHash[]上的位置。 我們通過code & postingHashMask來找到當前token在postingHash[]上的位置(其中postingsHashMask = postingsHashSize-1)。當產生Hash沖突的時候,其解決辦法就是不斷計算新的位置直到不產生沖突為止。
?
這里順便提一下:code & postingHashMask 和code % postingHashMask是完全等價的,而且位運算效率更高,Lucene經常使用位運算。
//計算當前token將要插入在Hash表中的位置,code & postingsHashMask等價于code % postingHashMask //postingsHashMask=postingsHashSize-1 int hashPos = code & postingsHashMask; //取出原hash表中hashPos位置上的數據 p = postingsHash[hashPos]; //如果原Hash表中的該位置上有數據并且兩個token的字符串內容不等,則產生Hash沖突 if (p != null && !postingEquals(tokenText, tokenTextLen)) { // 沖突解決方法:不斷計算新的hashcode,直到避開沖突位置 final int inc = ((code>>8)+code)|1; do { code += inc; hashPos = code & postingsHashMask; p = postingsHash[hashPos]; } while (p != null && !postingEquals(tokenText, tokenTextLen)); }?
3、最后,將token的各種信息寫入數據池,并在PostingList中存儲數據池的地址偏移。 整個 過程有兩種可能:
?
(1) 如果當前token在前面從未遇到過,也就是已經加入索引結構的所有詞都沒有這個詞語。那么首先需要在postingHash中開辟一個新的PostingList準備存放當前token所對應信息(code line :18,33)。接著在CharBlackPool中創建一個新的區域來存儲當前token的字符串信息(line: 27),并且將地址偏移記錄在新創建的PostingList.textStart中(line: 25)。然后就是在ByteBlockPool中開辟兩個slice (line:57,58)。并且在IntBlockPool中開辟兩個空間存儲ByteBlockPool中的新開辟的slice地址偏移(line:57,60)。最后調用 FreqProxTermsWriterPerField.newTerm() 將token的docID+freq和position信息存儲進去(line: 67)。
?
(2) 如果當前token在前面已經遇到過了,此時就不需要在三大數據池中分配新的空間存放了。直接調用 FreqProxTermsWriterPerField.addTerm() 將信息存儲進去(line:72)。
//說明當前token以前的文本中從未出現過 if (p == null) { final int textLen1 = 1+tokenTextLen; //當CharBlockPool當前的buffer空間不足時,則重新分配一個新的buffer if (textLen1 + charPool.charUpto > DocumentsWriter.CHAR_BLOCK_SIZE) { if (textLen1 > DocumentsWriter.CHAR_BLOCK_SIZE) { if (docState.maxTermPrefix == null) docState.maxTermPrefix = new String(tokenText, 0, 30); consumer.skippingLongTerm(); return; } charPool.nextBuffer(); } if (0 == perThread.freePostingsCount) perThread.morePostings(); //從空閑的posting中分配新的posting p = perThread.freePostings[--perThread.freePostingsCount]; assert p != null; //將當前token的內容寫入CharBlockPool中,此時text和CharBlockPool中的當前buffer都指向同一塊內存區域 final char[] text = charPool.buffer; final int textUpto = charPool.charUpto; //PostingList類中的textStart保存的是當前token首字母在CharBlockPool中的位置 p.textStart = textUpto + charPool.charOffset; charPool.charUpto += textLen1; System.arraycopy(tokenText, 0, text, textUpto, tokenTextLen); //每個詞當中存放一個分隔符'#66535'(66535號字符,我們用“#66535”表示) text[textUpto+tokenTextLen] = 0xffff; assert postingsHash[hashPos] == null; //將postingHash[hashPos]指向剛開辟的空閑RawPostList鏈表 postingsHash[hashPos] = p; //記錄鏈表數量 numPostings++; //如果postingsHash的加載因子達到了50%,則擴大2倍的postingsHash容量 if (numPostings == postingsHashHalfSize) rehashPostings(2*postingsHashSize); //當IntBlockPool中buffer容量不足時,分配一個新buffer if (numPostingInt + intPool.intUpto > DocumentsWriter.INT_BLOCK_SIZE) intPool.nextBuffer(); //當ByteBlockPool中buffer容量不足時,分配一個新buffer if (DocumentsWriter.BYTE_BLOCK_SIZE - bytePool.byteUpto < numPostingInt*ByteBlockPool.FIRST_LEVEL_SIZE) bytePool.nextBuffer(); intUptos = intPool.buffer; intUptoStart = intPool.intUpto; //streamCount=2在ByteBlockPool對一個token同時開辟兩個大小一樣的slice,一個存放docID+frequence,另一個存放positive. //并且在IntBlockPool中也同時開辟兩個空間,用于分別存放一個token對應在ByteBlockPool中兩個slice的地址偏移 intPool.intUpto += streamCount; //PostingList類中的intStart保存的是當前token在IntBlockPool中的兩個存儲空間的第一個地址 p.intStart = intUptoStart + intPool.intOffset; //每一次記錄一個token,都需要在ByteBlckPool中開辟兩個塊來記錄: docID+freq(文檔ID+詞頻) 和 prox(位置) for(int i=0;i<streamCount;i++) { final int upto = bytePool.newSlice(ByteBlockPool.FIRST_LEVEL_SIZE); //IntBlockPool用來存儲ByteBlockPool每次開辟的塊的初始位置 intUptos[intUptoStart+i] = upto + bytePool.byteOffset; } //PostingList類中的byteStart用于存儲當前token使用ByteBlckPool開辟的空間的初始位置 //也就是剛開辟的兩個塊中第一個塊的初始位置 p.byteStart = intUptos[intUptoStart]; //token原來沒有出現過的時候,FreqProxTermsWriterPerField調用newTerm()記錄docID,freq和position consumer.newTerm(p); } else { //如果此Token之前曾經出現過,FreqProxTermsWriterPerField調用addTerm()記錄docID,freq和position intUptos = intPool.buffers[p.intStart >> DocumentsWriter.INT_BLOCK_SHIFT]; intUptoStart = p.intStart & DocumentsWriter.INT_BLOCK_MASK; consumer.addTerm(p); }
????
我們在《
全文檢索基本理論
》中講到的倒排索引結構是 token -> posting list的形式,而文檔結合的所有token組成了一個Dictionary。
TermsHashPerField類的作用就是建立這樣一個倒排索引結構——postingHash。其中
postingHash[](哈希數組)是以token的字面值作為關鍵字的,相當于Dictionary。而
postingHash的每一個元素都指向了PostingList對象,這個對象就是用來存儲指定token所對應的posting list信息(包括docID,freq,position)。實際上,真正的信息是存儲在三大數據池中的,但
PostingList對象只存儲三大數據池中的地址偏移。我們通過上面的代碼可以發現:
TermsHashPerField已經把token的字面值存儲在CharBlockPool中了,并且在ByteBlockPool中分配好的存儲空間,并將地址偏移記錄到了IntBlockPool中了。接下來要做的就是把
token所對應的docID,freq,position的信息通過
FreqProxTermsWriterPerField
的方法寫入ByteBlockPool。
?
?
★ FreqProxTermsWriterPerField ?
?
(1)當出現一個索引結構中沒有的token時,我們需要調用 newTerm()方法將token所對應的 docID,freq,position信息存儲到ByteBlockPool中。
final void newTerm(RawPostingList p0) { assert docState.testPoint("FreqProxTermsWriterPerField.newTerm start"); FreqProxTermsWriter.PostingList p = (FreqProxTermsWriter.PostingList) p0; //當一個新的term出現的時候,包含此Term的就只有本篇文檔,記錄其ID p.lastDocID = docState.docID; if (omitTermFreqAndPositions) { p.lastDocCode = docState.docID; } else { //docCode是文檔ID左移一位,為什么左移,這就需要Lucene的索引文件結構來解釋了。 p.lastDocCode = docState.docID << 1; p.docFreq = 1; //寫入position信息到bytePool中,此時freq信息還不能寫入,因為當前的文檔還沒有處理完,尚不知道此文檔包含此token的總數。 writeProx(p, fieldState.position); } }
?
(2) 當出現的token在索引結構中已經存在,我們需要調用 addTerm()方法將token所對應的 docID,freq,position信息存儲到 ByteBlockPool中。
final void addTerm(RawPostingList p0) { assert docState.testPoint("FreqProxTermsWriterPerField.addTerm start"); //取出postingHash已經存在的PostingList FreqProxTermsWriter.PostingList p = (FreqProxTermsWriter.PostingList) p0; assert omitTermFreqAndPositions || p.docFreq > 0; if (omitTermFreqAndPositions) { //p.lastDocID記錄了上一次token寫入索引結構的docID //docState.docID記錄的是token將要寫入索引結構的當前docID if (docState.docID != p.lastDocID) { assert docState.docID > p.lastDocID; termsHashPerField.writeVInt(0, p.lastDocCode); p.lastDocCode = docState.docID - p.lastDocID; p.lastDocID = docState.docID; } } else { if (docState.docID != p.lastDocID) { assert docState.docID > p.lastDocID; //如果當前的docID與上一次相同的token寫入的docID不同 //則表明上一篇文本中該token已經處理完畢,則將freq信息ByteBlockPool中 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加1,寫入再次出現的位置信息,用差值存儲。這里不寫入freq,因為該文檔還沒有結束 p.docFreq++; //將position信息寫入ByteBlockPool中 writeProx(p, fieldState.position-p.lastPosition); } } }
?
上面newTerm()和addTerm()方法都需要調用writeProx()方法將position信息寫入ByteBlockPool中
//將position信息寫入ByteBlockPool中 final void writeProx(FreqProxTermsWriter.PostingList p, int proxCode) { final Payload payload; //payloadAttribute是token的元數據信息 if (payloadAttribute == null) { payload = null; } else { payload = payloadAttribute.getPayload(); } //將position信息寫入ByteBlockPool中 //其中writeVInt()第一個參數1表示將position寫入開辟在ByteBlockPool中兩個slice的第2個中 //第一個slice存放docID+freq,第二個slice存放position ?if (payload != null && payload.length > 0) { termsHashPerField.writeVInt(1, (proxCode<<1)|1); termsHashPerField.writeVInt(1, payload.length); termsHashPerField.writeBytes(1, payload.data, payload.offset, payload.length); hasPayloads = true; } else termsHashPerField.writeVInt(1, proxCode<<1); p.lastPosition = fieldState.position; }
?
實際上,寫入ByteBlockPool中的數據到底是什么樣子的呢?還有在CharBlockPool中是如何存儲token的字面值的呢?IntBlockPool又是怎么樣記錄ByteBlockPool中的地址偏移的呢?這些問題我們將在《 索引數據池及其存儲細節 》詳細闡明.
?
?
★ 總結:
?
???? 我們以前面所舉的例子為例,將《 索引數據池及內存數據細節 》中的content field的第一個token="lucene"(docID=0,position=1)創建索引結構,其圖示如下:
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
