1. 程式人生 > >lucene中倒排索引的記憶體結構

lucene中倒排索引的記憶體結構

簡介

lucene索引格式是個老生常談的問題,網上也有一些資料,但是由於年代比較古老(大都是基於3.x或者4.x的版本),和現有程式碼較難對上,這裡基於lucene6.6重新講解下,也幫助自己理解和記憶。

基本概念

這些資訊很容易理解,看程式碼的時候也很清晰。

lucene在進行索引時,為了加速索引程序,會同時多執行緒同時進行索引,每一個執行緒在flush後都是一個完整的索引段。

對於每個索引執行緒,又會分為多個field域,每個field都是獨立的記憶體結構,記錄該field所有出現的term資訊。

對於每個term,都是獨立屬於某個field(不同field,字面值相同的term,也是不同的term),都是獨立的不可拆分的單位,是分詞之後得到的結果,是搜尋的時候的用來匹配的詞。每個term都需要記錄完整的倒排索引資訊。

基礎知識

  • 變長整數vInt的表示:在lucene中,變長的整數,然用一種叫或然跟隨規則的形式儲存,對於一個byte,低7位來儲存資料,最高位表示是否還有下一位數字,例如127,則直接採用0x7f儲存,但是128,則使用0x80,0x01兩個位元組儲存,其中0x80二進位制最高位的1表示還有下一個位元組。0x01則表示自己是最後一個位元組,連起來表示的整數就是128。
  • slice連結串列:在lucene中,slice作為bytePool記憶體分配的一個重要單位,每隔slice的初始長度都是5,如果需要的位元組數大於5,則會將當前這5個位元組中的後4為作為指向下一層的指標,並在bytePool分配下一層的空間。這個在bytePool的記憶體分配寫的比較清楚、

倒排索引要存哪些資訊

這裡我們僅討論核心資訊,非核心資訊可以很容易同理可得。
- 具體的term值。
- term對應的docId。
- term在文件中的出現次數(Freq,用來打分)。
- term在文件分詞後的位置(pos,用來短語搜尋)。
- other(類似pos資訊)。

邏輯結構類似:

|+ field(name,type)
    |+ term
        |+ docId & termFreq 
            |+ [position,offset,payload]
        |+ docId & termFreq 
            |+ [position,offset,payload].
    |+ term
    |+...
|+ field2(name,type) |+ ...

term如何儲存

這裡我們忽略分詞的過程,假設已經拿到所有分詞結果。

term儲存,主要涉及到兩個問題:
1. term以什麼結構儲存。
2. 重複的term如何解決。

基於以上兩點,lucene設計瞭如下儲存結構:

public int add(BytesRef bytes) {
    assert bytesStart != null : "Bytesstart is null - not initialized";
    final int length = bytes.length;
    // 獲得term的hash儲存位置,hash演算法不展開。
    final int hashPos = findHash(bytes);
    // ids用來儲存hashPos對應的termId。
    int e = ids[hashPos];

    //如果為-1,則是新的term
    if (e == -1) {
      // 儲存的時候,在ByteBlockPool中的結構是:長度+具體的term。
      // lucene支援的term長度不超過2個位元組,長度採用變長整數表示,因此需要申請的儲存空間為2 + bytes.length。
      final int len2 = 2 + bytes.length;
      if (len2 + pool.byteUpto > BYTE_BLOCK_SIZE) {
        if (len2 > BYTE_BLOCK_SIZE) {
          throw new MaxBytesLengthExceededException("bytes can be at most "
              + (BYTE_BLOCK_SIZE - 2) + " in length; got " + bytes.length);
        }
        // 記憶體池擴容不展開敘述。
        pool.nextBuffer();
      }
      final byte[] buffer = pool.buffer;
      // 獲取記憶體池的起始位置
      final int bufferUpto = pool.byteUpto;
      // byteStart用來記錄termId在記憶體池中儲存的起始位置,count是總term數量。
      if (count >= bytesStart.length) {
        bytesStart = bytesStartArray.grow();
        assert count < bytesStart.length + 1 : "count: " + count + " len: "
            + bytesStart.length;
      }
      //分配termId
      e = count++;

      // 記錄對應termId在ByteStartPool中的起始位置。
      bytesStart[e] = bufferUpto + pool.byteOffset;

      // 長度小於128,則長度用一個位元組的vInt即可儲存。
      if (length < 128) {
        // 1 byte to store length
        buffer[bufferUpto] = (byte) length;
        pool.byteUpto += length + 1;
        assert length >= 0: "Length must be positive: " + length;
        System.arraycopy(bytes.bytes, bytes.offset, buffer, bufferUpto + 1,
            length);
      } else {
        // 2 byte to store length
        buffer[bufferUpto] = (byte) (0x80 | (length & 0x7f));
        buffer[bufferUpto + 1] = (byte) ((length >> 7) & 0xff);
        pool.byteUpto += length + 2;
        System.arraycopy(bytes.bytes, bytes.offset, buffer, bufferUpto + 2,
            length);
      }
      assert ids[hashPos] == -1;
      // 記錄hashPos對應的termId為e。
      ids[hashPos] = e;
      // rehash,不展開敘述。
      if (count == hashHalfSize) {
        rehash(2 * hashSize, true);
      }
      return e;
    }
    // 如果不是新的term,則直接返回。
    return -(e + 1);
  }

到此為止,我們已經把term記錄下來。下面,我們就要考慮如何把term和docId對應起來。

docId如何儲存

在我們整個索引過程,每一個field的所有term是共用記憶體池的,儲存docId的時候,要考慮到一個term可以出現在不同的文件中,對應多個不同的docId。

term的整個處理過程在TermsHashPerField中,我們可以在add()方法中看到,term的儲存只是整個term索引過程第一步。

資料結構

現在term已經儲存完成,我們搜尋請求過來時,可以很輕鬆找到自己的termId,如何從termId查詢docId是另一層對應關係需要做的事情,lucene為此,在TermsHashPerField中設計了幾個資料結構,這幾個資料結構在對term索引的時候起到了重要作用

postingsArray

這個結構中包含三個很重要的陣列,分別用來記錄不同的資訊:
- textStarts,本來是用來記錄term本身在ByteBlockPool中的起始位置的,建索引的時候沒有用到這個欄位。
- intStarts,用來記錄對應termId對應的其他資訊在IntPool中的記錄位置,intpool中記錄的具體是什麼資訊後面會說明。
- byteStarts。用來記錄termId的[docId,freq]組合在ByteBlockPool中的起始位置,注意是[docID,freq]組合,在bytePool中的儲存形式類似於[docId,freq][docId,freq][docId,freq]….這種,這個起始位置的值 + slice初始化長度就是posi資訊的起始位置。

BlockPool

在TermsHashPerField中可以看到三個blockPool
- IntBlockPool intPool;
- ByteBlockPool bytePool;
- ByteBlockPool termBytePool;

IntPool用來termID對應的資訊在bytePool中的位置,包含以下兩種:
- [docId,freq]連結串列的結束位置+1。
- 如果有posi等資訊,則用來記錄posi等資訊的結束位置+1。

至於為什麼這兩個資訊要記錄到不同位置呢?是因為[docId,freq]資訊要等一個doc處理結束才能確定,此時才會真正寫入bytePool,而posi等資訊,在處理doc的每一個term的時候都可以確定,可以直接寫入bytePool,所以這裡會分為兩個地方寫入。

bytePool和termBytePool用來儲存真正的倒排資訊,從程式碼中可以很輕鬆發現這兩個引用指向同一個物件。

具體流程

這裡我先用文字描述下即將發生的事情,後面我們跟著程式碼繼續整理:

新增term
1. 為term即將儲存的[docId,freq]資訊、posi等資訊,在bytePool中申請slice(記憶體空間),並將對應的slice起始位置作為[docId,freq]和posi等資訊的結束位置寫入intPool(由於還沒存入資訊,所以用起始位置作為結束位置),兩個資訊在bytePool中分別存在獨立的slice中。
2. 呼叫FreqProxTermsWriterPerField的newTerm方法,首先將該term的lastdocId置為當前docId,將freq置為1,將docCodes置為當前docId << 1,左移一位目的是,最後一位為0,表示後面跟隨freq資訊,在addTerm時可以看到其他處理,這個優化是因為大多數term都只會出現一次,另開一個int儲存比較浪費。
3. 然後在bytePool中寫入posi等資訊,並調整intPool中posi資訊的最後一位下標。

已有term
1. 呼叫FreqProxTermsWriterPerField的addTerm方法,首先判斷當前處理的docId和該term最後一次處理的docId是否一樣,如果一樣,則證明這是一個doc分詞出的相同term,需要累加freq,但是不需要更新docId;如果不一樣,則證明上一次的doc已經處理完畢,應當將上次的所有資訊刷入記憶體池,我們以不一樣為例講解下。
2. 如果不是一個docId,則證明上一個文件剛處理結束,當前所有記錄的資訊都是上一個doc的。如果出現頻率的頻率等於1,則沒必要寫入freq資訊,直接把docCodes最後一位置為1,寫入docCodes即可。否則,直接寫入docCodes(此時docCodes最後一位為0,在newTerm的時候有設定),並且寫入freq資訊。
3. 寫入完成後,則上一個doc處理完畢,開始處理當前文件。首先將termFreq設定為1,表明這是當前文件第一次出現這個term,然後設定docCodes,採用差值設定,並左移一位,將最後一位置為0,原理同newTerm。
4. 然後寫入posi等資訊,原理通newTerm。

至此,我們大概清楚瞭如何term到底是如何和docId對應起來的,並且這些東西使如何儲存的。嘴上得來總覺淺,下面我們直接看下程式碼到底是如何處理的:

TermHashPerField裡面的add()方法:

// 新增term,並返回termId
int termID = bytesHash.add(termAtt.getBytesRef());

//termId為正,則表明使新的term。
if (termID >= 0) {// New posting

      //這裡貌似沒什麼作用
      bytesHash.byteStart(termID);
      // numPosingInt用來記錄在intPool需要幾位來記錄資訊,intPool不夠則擴容
      if (numPostingInt + intPool.intUpto > IntBlockPool.INT_BLOCK_SIZE) {
        intPool.nextBuffer();
      }

      // 同理,判斷bytePool是否需要擴容,需要為term在bytePool中分配numPosingInt個slice,每個slice的初始大小都是FIRET_LEVEL_SIZE。
      if (ByteBlockPool.BYTE_BLOCK_SIZE - bytePool.byteUpto < numPostingInt*ByteBlockPool.FIRST_LEVEL_SIZE) {
        bytePool.nextBuffer();
      }

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

      // intStarts記錄intPool中term資訊的位置    
      postingsArray.intStarts[termID] = intUptoStart + intPool.intOffset;

      // 為每個域分配slice,並記錄結束位置,streamCount應該等同numPosingInt
      for(int i=0;i<streamCount;i++) {
        final int upto = bytePool.newSlice(ByteBlockPool.FIRST_LEVEL_SIZE);
        intUptos[intUptoStart+i] = upto + bytePool.byteOffset;
      }
      // 記錄[docId,freq]連結串列起始位置,intPool中記錄的理應是結束位置,但是由於此時還沒寫入內容,所以起始位置等於結束位置
      postingsArray.byteStarts[termID] = intUptos[intUptoStart];

      // 呼叫newTerm方法,執行FreqProxTermsWriterPerField的newTerm
      newTerm(termID);

    } else {
      termID = (-termID)-1;
      int intStart = postingsArray.intStarts[termID];
      // 準備一些記憶體池相關引數
      intUptos = intPool.buffers[intStart >> IntBlockPool.INT_BLOCK_SHIFT];
      intUptoStart = intStart & IntBlockPool.INT_BLOCK_MASK;
      // 呼叫addTerm,執行FreqProxTermsWriterPerField的addTerm
      addTerm(termID);
    }

FreqProxTermsWriterPerField的newTerm()方法

void newTerm(final int termID) {
    final FreqProxPostingsArray postings = freqProxPostingsArray;

    // 該term最後處理的docId就是當前docId
    postings.lastDocIDs[termID] = docState.docID;
    // 不記錄freq,只需要維護docId鏈就可以
    if (!hasFreq) {
      assert postings.termFreqs == null;
      postings.lastDocCodes[termID] = docState.docID;
    } else {
      // 記錄docId鏈,左移一位,最後一位表示後面跟隨freq
      postings.lastDocCodes[termID] = docState.docID << 1;
      postings.termFreqs[termID] = 1;
      // 寫入posi等資訊
      if (hasProx) {
        writeProx(termID, fieldState.position);
        if (hasOffsets) {
          writeOffsets(termID, fieldState.offset);
        }
      } else {
        assert !hasOffsets;
      }
    }
    fieldState.maxTermFrequency = Math.max(1, fieldState.maxTermFrequency);
    fieldState.uniqueTermCount++;
  }

FreqProxTermsWriterPerField的addTerm()方法

void addTerm(final int termID) {
    final FreqProxPostingsArray postings = freqProxPostingsArray;

    assert !hasFreq || postings.termFreqs[termID] > 0;

    // 不記錄freq的情況,比較簡單,不展開。
    if (!hasFreq) {
      assert postings.termFreqs == null;
      if (docState.docID != postings.lastDocIDs[termID]) {
        // New document; now encode docCode for previous doc:
        assert docState.docID > postings.lastDocIDs[termID];
        writeVInt(0, postings.lastDocCodes[termID]);
        postings.lastDocCodes[termID] = docState.docID - postings.lastDocIDs[termID];
        postings.lastDocIDs[termID] = docState.docID;
        fieldState.uniqueTermCount++;
      }
    } else if (docState.docID != postings.lastDocIDs[termID]) {
      // 當前處理的docId不等於上次處理的docId,則證明上次的doc已經處理完畢,需要寫入上次的資訊
      // 如果freq等於1,則將lastDocCodes最後一位置為1,表示後面不跟隨freq資訊,省掉一個記錄freq的位元組。
      if (1 == postings.termFreqs[termID]) {
        writeVInt(0, postings.lastDocCodes[termID]|1);
      } else {
        // 否則,要寫入docCodes和freq,此時docCodes最後一位是0。
        writeVInt(0, postings.lastDocCodes[termID]);
        writeVInt(0, postings.termFreqs[termID]);
      }
      // 舊的文件處理結束,開始寫入新的文件資訊,基本和newTerm()處理手段一致。
      postings.termFreqs[termID] = 1;
      fieldState.maxTermFrequency = Math.max(1, fieldState.maxTermFrequency);
      // 這裡是docId鏈採用差值法儲存,也是為了節省記憶體。
      postings.lastDocCodes[termID] = (docState.docID - postings.lastDocIDs[termID]) << 1;
      postings.lastDocIDs[termID] = docState.docID;
      if (hasProx) {
        writeProx(termID, fieldState.position);
        if (hasOffsets) {
          postings.lastOffsets[termID] = 0;
          writeOffsets(termID, fieldState.offset);
        }
      } else {
        assert !hasOffsets;
      }
      fieldState.uniqueTermCount++;
    } else {
      // 進到這裡,說明是同一個doc的同一個field中分詞分出了多個相同的term,只需要額外寫入posi等資訊即可
      fieldState.maxTermFrequency = Math.max(fieldState.maxTermFrequency, ++postings.termFreqs[termID]);
      if (hasProx) {
        writeProx(termID, fieldState.position-postings.lastPositions[termID]);
        if (hasOffsets) {
          writeOffsets(termID, fieldState.offset);
        }
      }
    }
  }

至此,整個doc資訊都已經被串聯起來並寫入記憶體了,剩下就是在合適的時候將這些資訊刷入磁碟檔案,這部分本文不做探討。為了幫助理解,我們以一份簡單的索引,來看下上面提到的這些記憶體池的結構,加深理解。

實戰

我們以下面這份簡單的索引為例,看下這份索引的記憶體結構到底是什麼樣子。

    private Document getDocument(String value) throws Exception {
        Document doc = new Document();
        FieldType fieldType = new FieldType();
        fieldType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
        fieldType.setTokenized(true);
        Field pathField = new Field("name", value, fieldType);
        //向document中新增資訊
        doc.add(pathField);
        return doc;
    }

    //建立索引
    public void writeToIndex() throws Exception {
        //需要建立索引的資料位置
        Document document = getDocument("lucene1");
        writer.addDocument(document);
        // breakpoint1
        document = getDocument("lucene2 lucene2");
        writer.addDocument(document);
        // breakpoint2
        document = getDocument("lucene2 lucene2 test lucene2 lucene2");
        writer.addDocument(document);
        // breakpoint3
    }

breakpoint1

下標 postingsArray.textStarts postingsArray.intStarts postindesArray.byteStarts intPool bytePool
0 0 0 8 8 7
1 0 0 0 14 108
2 0 0 0 0 117
3 0 0 0 0 99
4 0 0 0 0 101
5 0 0 0 0 110
6 0 0 0 0 101
7 0 0 0 0 49
8 0 0 0 0 0
9 0 0 0 0 0
10 0 0 0 0 0
11 0 0 0 0 0
12 0 0 0 0 16
13 0 0 0 0 0
14 0 0 0 0 0
15 0 0 0 0 0
16 0 0 0 0 0
17 0 0 0 0 16

在這個斷點,只有一個term出現,lucene1的termId為0。

textStarts[0] = 0,表示term字面值在bytePool中第0位開始,bytePool[0] = 7,表示term長度為7,bytePool中1~7為term字面值。

8~12是第一個slice,用來儲存[docId,freq],最後一位16表示沒有向後延伸。

13~17是第二個slice,用來儲存posi等資訊,最後一位16表示沒有向後延伸。

再來看intStarts[0] = 0,表示term相關資訊在intPool中第0位開始,由於有posi資訊,則在intPool中需要佔兩個位置。因此intPool[0]和intPool[1]分別表示這個term在bytePool中[docId,freq]和posi等資訊的結束位置+1

byteStarts[0] = 8,表示term的[docId,freq]資訊在bytePool中從第8個位元組開始。

intPool[0] = 8,表示[docId,freq]在bytePool中結束位置 + 1 。為什麼明明有一個doc,但是intPool[0]中指示[doc,freq]的結束位置為8,等於byteStarts[0]呢,相當於沒有任何資訊呢?原因是雖然doc1已經處理完畢,但是此時對於lucene1這個term,沒有其他的doc,所以這個資訊還沒有被寫入intPool,仍存在lucene1的這個term的docCodes、freq陣列中。

intPool[1] = 14,表示pos等資訊的結束位置為14,這個資訊的長度可以通過[docId,freq]的數量計算出來,分詞後的每一個term都會存這個資訊,因此這個資訊長度為sum(freq)。這裡可以看到值為0。這個要分兩部分看,二進位制最後一位為0,表示沒有後續資訊,前7位為0,表示term在這個field原生值分詞後的第一位。

到這裡,breakpoint1的所有資訊都分析完畢。

breakpoint2

下標 postingsArray.textStarts postingsArray.intStarts postindesArray.byteStarts intPool bytePool
0 0 0 8 8 7
1 18 2 26 14 108
2 0 0 0 26 117
3 0 0 0 33 99
4 0 0 0 0 101
5 0 0 0 0 110
6 0 0 0 0 101
7 0 0 0 0 49
8 0 0 0 0 0
9 0 0 0 0 0
10 0 0 0 0 0
11 0 0 0 0 0
12 0 0 0 0 16
13 0 0 0 0 0
14 0 0 0 0 0
15 0 0 0 0 0
16 0 0 0 0 0
17 0 0 0 0 16
18 0 0 0 0 7
19 0 0 0 0 108
20 0 0 0 0 117
21 0 0 0 0 99
22 0 0 0 0 101
23 0 0 0 0 110
24 0 0 0 0 101
25 0 0 0 0 50
26 0 0 0 0 0
27 0 0 0 0 0
28 0 0 0 0 0
29 0 0 0 0 0
30 0 0 0 0 16
31 0 0 0 0 0
32 0 0 0 0 2
33 0 0 0 0 0
34 0 0 0 0 0
35 0 0 0 0 16

在這個斷點,lucene2的termId為1。

textStarts[1] = 18,表示term字面值在bytePool中第18位開始,bytePool[18] = 7,表示term長度為7,bytePool中19~25為term字面值。

26~30是第一個slice,用來儲存[docId,freq],最後一位16表示沒有向後延伸。

31~35是第二個slice,用來儲存posi等資訊,最後一位16表示沒有向後延伸。

再來看intStarts[1] = 2,表示term相關資訊在intPool中第2位開始,由於有posi資訊,則在intPool中需要佔兩個位置。因此intPool[2]和intPool[3]分別表示這個term在bytePool中[docId,freq]和posi等資訊的結束位置+1

byteStarts[1] = 26,表示term的[docId,freq]資訊在bytePool中從第26個位元組開始。

intPool[2] = 26,表示[docId,freq]在bytePool中結束位置 + 1 。為什麼等於byteStarts[1],原因同lucene1

intPool[3] = 33,表示pos等資訊的結束位置為3。可以看到bytePool[31] = 0,表示在分詞列表中出現的位置是0,後面不跟隨其他資訊,bytePool[32] = 2,表示在分詞列表中出現的位置是1,後面不跟隨其他資訊。

到這裡,breakpoint2的所有資訊都分析完畢。

breakpoint3

下標 postingsArray.textStarts postingsArray.intStarts postindesArray.byteStarts intPool bytePool
0 0 0 8 8 7
1 18 2 26 14 108
2 36 4 41 28 117
3 0 0 0 56 99
4 0 0 0 41 101
5 0 0 0 47 110
6 0 0 0 0 101
7 0 0 0 0 49
8 0 0 0 0 0
9 0 0 0 0 0
10 0 0 0 0 0
11 0 0 0 0 0
12 0 0 0 0 16
13 0 0 0 0 0
14 0 0 0 0 0
15 0 0 0 0 0
16 0 0 0 0 0
17 0 0 0 0 16
18 0 0 0 0 7
19 0 0 0 0 108
20 0 0 0 0 117
21 0 0 0 0 99
22 0 0 0 0 101
23 0 0 0 0 110
24 0 0 0 0 101
25 0 0 0 0 50
26 0 0 0 0 2
27 0 0 0 0 2
28 0 0 0 0 0
29 0 0 0 0 0
30 0 0 0 0 16
31 0 0 0 0 0
32 0 0 0 0 0
33 0 0 0 0 0
34 0 0 0 0 0
35 0 0 0 0 51
36 0 0 0 0 4
37 0 0 0 0 116
38 0 0 0 0 101
39 0 0 0 0 115
40 0 0 0 0 116
41 0 0 0 0 0
42 0 0 0 0 0
43 0 0 0 0 0
44 0 0 0 0 0
45 0 0 0 0 16
46 0 0 0 0 4
47 0 0 0 0 0
48 0 0 0 0 0
49 0 0 0 0 0
50 0 0 0 0 16
51 0 0 0 0 2
52 0 0 0 0 0
53 0 0 0 0 2
54 0 0 0 0 4
55 0 0 0 0 2
56 0 0 0 0 0
57 0 0 0 0 0
58 0 0 0 0 0
59 0 0 0 0 0
60 0 0 0 0 0
61 0 0 0 0 0
62 0 0 0 0 0
63 0 0 0 0 0
64 0 0 0 0 17
65 0 0 0 0 0
66 0 0 0 0 0
67 0 0 0 0 0

在這個斷點,lucene2是已經出現過的term,會把doc1的資訊刷入bytePool,test是新的term,會單獨儲存並分配slic。

這個field總共會分出5個term:lucene2、lucene2、test、lucene2、lucene2。我們一個個分析資訊是如何寫入bytePool中的。

第一個lucene2
  • 首先,會發現這是已有的term,termId = 1,addTerm時發現上次的docId是1,這次的docId是2,會先將上次doc的資訊刷入bytePool。
  • 上次的docId為1,由於termFreq = 2,需要跟隨freq資訊,因此將docId左移一位的值直接寫入bytePool,然後寫入freq,注意freq使用vInt寫入的,但是此時freq = 2,只需要一個位元組,所以寫入的值是2.
  • 向intPool查詢當前可以寫入的位置,intPool[1] = 26,因此第26個位元組寫入2表示docId,並且後面跟隨freq,第27個位元組寫入2,表示freq = 2,並設定[docId,freq]結束位置為28。
  • 然後,更新lastDocId等資訊,並寫入新的term posi等資訊。
第二個lucene2
  • 這個沒什麼好說的,就是正常的addTerm,更新freq,寫入posi等資訊,freq列表為下標31~34,值為0、2、0、2。
test
  • 新的term出現了,和之前新term處理方式一樣,寫入term字面值(bytePool下標36~40),申請[docId,freq]的splic(41~45),申請posi等資訊的slice並寫入(46~50),寫入的值為4,二進位制最後一位為0表示不跟隨其他資訊,右移一位為2表示在分詞鏈中第2個出現,因此posi結束位置為47,[doc,freq]資訊還沒刷入bytePool,結束位置為41。
第三個lucene2
  • 正常執行addTerm方法,但是在寫入posi等資訊的時候,要寫入的位置是35,這個位置值16表示這是slice的末尾,不能寫入值。slice要擴容,並將32~34的資訊複製到新擴容的區域,重新申請slice得到的slice起始位置為51,將32~35四個位元組合併表示51,因此32~34為0,35表示51,將原本32到34的值複製到51~53,因此51~53的置為2、0、2,新的詞在分詞列表中處於第3位,上一個lucene2處於第1位,採用差值法,應當寫入2,左移一位將末尾置0,表示後面沒有其他資訊,因此54位置寫入的值為4。
第四個lucene2
  • 同第二個lucene2,直接在55的位置寫入2,將posi資訊結束位置修改為53。

到這裡,breakpoint3的所有資訊都分析完畢。

The End

到這裡,我們已經把整個lucene倒排索引如何建立的,以及其記憶體結構講清楚了。所有複雜的結構本身都是有必須複雜的道理,lucene設計的這麼複雜的結構的目的就是為了節省記憶體,儘可能的利用每一個位元組,從而在記憶體中放更多的東西。