Lucene源码系列(十四):内存中倒排信息的读取

952 阅读11分钟

背景

Lucene在索引数据的时候是先把倒排数据存储在内存中,到了flush阶段读取内存中的倒排数据持久化成索引文件。

上一篇文章中,我们已经介绍了倒排数据在内存中的存储结构以及如何一步步构建的,本文就接着来看怎么读取内存中的倒排数据。其实只要掌握了怎么构建的,那读取的逻辑理解起来是比较容易的。本文先分析源码,最后再以一个实际的例子来说明如何读取相关数据。

注意:本文源码解析基于Lucene 9.1.0版本。

工具类

ByteSliceReader

倒排数据的两个stream中分别是由多个slice组成的两个链表,ByteSliceReader是用来读取slice的。slice的读取最重要的是判断当前slice的结束位置以及找到下一个slice的位置。ByteSliceReader用endIndex记录了stream的结束位置,用limit记录当前slice的结束位置,因此当limit不等于endIndex的时候,就说明还存在下一个slice,并且下一个slice的地址就是limit后面四个字节的值,而limit的确定是由ByteBlockPool.LEVEL_SIZE_ARRAY根据level计算得到的。

成员变量

// slice所在的ByteBlockPool
ByteBlockPool pool;
// 使用pool中的哪个buffer
int bufferUpto;
// 当前使用的buffer
byte[] buffer;
public int upto;
// 当前slice的结束位置,如果还有下一个slice的话,limit后面四个字节就是下个slice的起始位置
int limit;
// 当前读到的slice是哪个level
int level;
// buffer在pool中的起始位置的offset
public int bufferOffset;
// 这个数据源的slice链链表的最后一个slice的结束位置
public int endIndex;

核心方法

// startIndex是在pool中第一个slice的起始位置
public void init(ByteBlockPool pool, int startIndex, int endIndex) {
  
  this.pool = pool;
  this.endIndex = endIndex;

  // 初始level是0  
  level = 0;
    
  // 根据startIndex计算当前使用的pool中的哪个buffer 
  bufferUpto = startIndex / ByteBlockPool.BYTE_BLOCK_SIZE;
  bufferOffset = bufferUpto * ByteBlockPool.BYTE_BLOCK_SIZE;  
  buffer = pool.buffers[bufferUpto];
  // 在buffer中的offset  
  upto = startIndex & ByteBlockPool.BYTE_BLOCK_MASK;

  // 第一个slice的大小  
  final int firstSize = ByteBlockPool.LEVEL_SIZE_ARRAY[0];

  if (startIndex + firstSize >= endIndex) { // 说明只有一个slice
    limit = endIndex & ByteBlockPool.BYTE_BLOCK_MASK;
  }
   // 最后四个字节是下一个slice的起始位置,不属于正文内容。
   // 经过这一步,limit开始的四个字节就是下一个slice的位置
   else limit = upto + firstSize - 4;
}

因为slice其实是个链表,所以可以通过nextSlice来遍历:

public void nextSlice() {
  // limit开始的四个字节就是下一个slice的位置
  final int nextIndex = (int) BitUtil.VH_LE_INT.get(buffer, limit);

  // 下一个slice的level
  level = ByteBlockPool.NEXT_LEVEL_ARRAY[level];
  // 下一个slice的大小  
  final int newSize = ByteBlockPool.LEVEL_SIZE_ARRAY[level];

  bufferUpto = nextIndex / ByteBlockPool.BYTE_BLOCK_SIZE;
  bufferOffset = bufferUpto * ByteBlockPool.BYTE_BLOCK_SIZE;

  buffer = pool.buffers[bufferUpto];
  upto = nextIndex & ByteBlockPool.BYTE_BLOCK_MASK;

  if (nextIndex + newSize >= endIndex) {
    // 如果是最后一个slice
    limit = endIndex - bufferOffset;
  } else {
   // 最后四个字节是下一个slice的起始位置,不属于正文内容。
   // 经过这一步,limit开始的四个字节就是下一个slice的位置
    limit = upto + newSize - 4;
  }
}

读取入口

内存中倒排数据的读取是在flush的时候触发的,最终会调用到FreqProxTermsWriter#flush方法,至于流程怎么走到这里的我们以后介绍flush的时候再说。

public void flush(
    Map<String, TermsHashPerField> fieldsToFlush,
    final SegmentWriteState state,
    Sorter.DocMap sortMap,
    NormsProducer norms)
    throws IOException {
  super.flush(fieldsToFlush, state, sortMap, norms);

  // 收集所有存在倒排数据的field
  List<FreqProxTermsWriterPerField> allFields = new ArrayList<>();

  for (TermsHashPerField f : fieldsToFlush.values()) {
    final FreqProxTermsWriterPerField perField = (FreqProxTermsWriterPerField) f;
    if (perField.getNumTerms() > 0) {
      // 对字段中的term排序,这是因为term会使用FST来存储,FST的输入需要有序  
      perField.sortTerms();
      assert perField.indexOptions != IndexOptions.NONE;
      allFields.add(perField);
    }
  }

  if (!state.fieldInfos.hasPostings()) {
    assert allFields.isEmpty();
    return;
  }

  CollectionUtil.introSort(allFields);

  // 这里把所有需要处理的  FreqProxTermsWriterPerField 封装到 FreqProxFields中,
  // 后面读取都需要借助  FreqProxFields,这就是本文要介绍的内容。
  Fields fields = new FreqProxFields(allFields);
  // 处理删除逻辑,以后介绍  
  applyDeletes(state, fields);
  if (sortMap != null) {
    final Sorter.DocMap docMap = sortMap;
    final FieldInfos infos = state.fieldInfos;
    fields =
        new FilterLeafReader.FilterFields(fields) {

          @Override
          public Terms terms(final String field) throws IOException {
            Terms terms = in.terms(field);
            if (terms == null) {
              return null;
            } else {
              return new SortingTerms(terms, infos.fieldInfo(field).getIndexOptions(), docMap);
            }
          }
        };
  }

  try (FieldsConsumer consumer =
      state.segmentInfo.getCodec().postingsFormat().fieldsConsumer(state)) {
    // 最终走到Lucene90BlockTreeTermsWriter#write,以后介绍索引文件生成再说  
    consumer.write(fields, norms);
  }
}

读取的逻辑

FreqProxFields

FreqProxFields中有好几个内部类,要读取数据的时候为每个field生成一个FreqProxTerms对象,FreqProxTerms对象可以迭代field所有的term,对每一个term用FreqProxTermsEnum封装,FreqProxTermsEnum根据是否只需要stream0的数据还是都需要分别创建FreqProxDocsEnum和FreqProxPostingsEnum最终完成倒排数据的读取。

FreqProxFields自己本身的逻辑非常简单,主要是根据字段创建FreqProxTerms。

class FreqProxFields extends Fields {
  final Map<String, FreqProxTermsWriterPerField> fields = new LinkedHashMap<>();

  public FreqProxFields(List<FreqProxTermsWriterPerField> fieldList) {
    for (FreqProxTermsWriterPerField field : fieldList) {
      fields.put(field.getFieldName(), field);
    }
  }

  // 获取所有字段的迭代器
  public Iterator<String> iterator() {
    return fields.keySet().iterator();
  }

  // 获取指定字段的所有term信息
  public Terms terms(String field) throws IOException {
    FreqProxTermsWriterPerField perField = fields.get(field);
    // 字段中所有term的倒排信息的获取逻辑都在  FreqProxTerms  中
    return perField == null ? null : new FreqProxTerms(perField);
  }
}

FreqProxTerms

FreqProxTerms可以返回字段中所有term的迭代器。

private static class FreqProxTerms extends Terms {
  // 包含了我们在内存中倒排的所有信息  
  final FreqProxTermsWriterPerField terms;

  public FreqProxTerms(FreqProxTermsWriterPerField terms) {
    this.terms = terms;
  }

  // 返回term的迭代器
  public TermsEnum iterator() {
    FreqProxTermsEnum termsEnum = new FreqProxTermsEnum(terms);
    termsEnum.reset();
    return termsEnum;
  }

  @Override
  public boolean hasFreqs() {
    return terms.indexOptions.compareTo(IndexOptions.DOCS_AND_FREQS) >= 0;
  }

  @Override
  public boolean hasOffsets() {
    return terms.indexOptions.compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS)
        >= 0;
  }

  @Override
  public boolean hasPositions() {
    return terms.indexOptions.compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS) >= 0;
  }

  @Override
  public boolean hasPayloads() {
    return terms.sawPayloads;
  }
}

FreqProxTermsEnum

FreqProxTermsEnum有两个功能:

  • 迭代获取所有的term
  • 使用posting方法获取term的倒排数据迭代器
private static class FreqProxTermsEnum extends BaseTermsEnum {
  final FreqProxTermsWriterPerField terms;
  // 按term大小排序的termID  
  final int[] sortedTermIDs;
  // 上一篇文章中遗留的问题: 最后处理的term的文档的id和频率没有写入bytePool,它存储在postingsArray中
  final FreqProxPostingsArray postingsArray;
  // 当前处理的term
  final BytesRef scratch = new BytesRef();
  final int numTerms;
  // term从小到大的排序序号,也就是field中的第几个term
  int ord;

  FreqProxTermsEnum(FreqProxTermsWriterPerField terms) {
    this.terms = terms;
    this.numTerms = terms.getNumTerms();
    sortedTermIDs = terms.getSortedTermIDs();
    assert sortedTermIDs != null;
    postingsArray = (FreqProxPostingsArray) terms.postingsArray;
  }

  public void reset() {
    ord = -1;
  }

  @Override
  public SeekStatus seekCeil(BytesRef text) {
    // sortedTermIDs已经是有序的了,所以可以通过二分查找指定的term
    int lo = 0;
    int hi = numTerms - 1;
    while (hi >= lo) {
      int mid = (lo + hi) >>> 1;
      int textStart = postingsArray.textStarts[sortedTermIDs[mid]];
      terms.bytePool.setBytesRef(scratch, textStart);
      int cmp = scratch.compareTo(text);
      if (cmp < 0) {
        lo = mid + 1;
      } else if (cmp > 0) {
        hi = mid - 1;
      } else { // 找到指定的term
        ord = mid;
        return SeekStatus.FOUND;
      }
    }

    ord = lo;
    if (ord >= numTerms) { // 什么也没找到
      return SeekStatus.END;
    } else { // 找到的是第一个大于指定term的term
      int textStart = postingsArray.textStarts[sortedTermIDs[ord]];
      terms.bytePool.setBytesRef(scratch, textStart);
      assert term().compareTo(text) > 0;
      return SeekStatus.NOT_FOUND;
    }
  }

  // 获取第ord个term
  public void seekExact(long ord) {
    this.ord = (int) ord;
    int textStart = postingsArray.textStarts[sortedTermIDs[this.ord]];
    terms.bytePool.setBytesRef(scratch, textStart);
  }

  @Override
  public BytesRef next() {
    ord++;
    if (ord >= numTerms) {
      return null;
    } else {
      int textStart = postingsArray.textStarts[sortedTermIDs[ord]];
      terms.bytePool.setBytesRef(scratch, textStart);
      return scratch;
    }
  }

  @Override
  public BytesRef term() {
    return scratch;
  }

  // 获取倒排信息的迭代器。  
  // 如果需要需要stream0中的倒排信息则使用FreqProxDocsEnum,否则使用FreqProxPostingsEnum。
  public PostingsEnum postings(PostingsEnum reuse, int flags) {
    // 需要位置信息  
    if (PostingsEnum.featureRequested(flags, PostingsEnum.POSITIONS)) { 
      FreqProxPostingsEnum posEnum;

      if (!terms.hasProx) { 
        // 没有构建位置信息抛出异常
        throw new IllegalArgumentException("did not index positions");
      }

      if (!terms.hasOffsets && PostingsEnum.featureRequested(flags, PostingsEnum.OFFSETS)) {
        // 没有构建offset信息抛出异常  
        throw new IllegalArgumentException("did not index offsets");
      }

      if (reuse instanceof FreqProxPostingsEnum) { 
        // 如果reuse也是FreqProxPostingsEnum,则进一步判断postingsArray来确定是有复用reuse
        posEnum = (FreqProxPostingsEnum) reuse;
        if (posEnum.postingsArray != postingsArray) { 
          // 如果reuse的postingsArray不是我们需要的,则重新创建FreqProxPostingsEnum
          posEnum = new FreqProxPostingsEnum(terms, postingsArray);
        }
      } else {
        // 创建新的FreqProxPostingsEnum
        posEnum = new FreqProxPostingsEnum(terms, postingsArray);
      }
      // 重置posEnum  
      posEnum.reset(sortedTermIDs[ord]);
      return posEnum;
    }

    FreqProxDocsEnum docsEnum;

    if (!terms.hasFreq && PostingsEnum.featureRequested(flags, PostingsEnum.FREQS)) {
      throw new IllegalArgumentException("did not index freq");
    }

    // 逻辑同上创建  FreqProxPostingsEnum 情况
    if (reuse instanceof FreqProxDocsEnum) {
      docsEnum = (FreqProxDocsEnum) reuse;
      if (docsEnum.postingsArray != postingsArray) {
        docsEnum = new FreqProxDocsEnum(terms, postingsArray);
      }
    } else {
      docsEnum = new FreqProxDocsEnum(terms, postingsArray);
    }
    docsEnum.reset(sortedTermIDs[ord]);
    return docsEnum;
  }
}

FreqProxDocsEnum

只需要term所在文档id及其在文档中的出现频率使用FreqProxDocsEnum。

// 只读取stream0:doc和freq
private static class FreqProxDocsEnum extends PostingsEnum {

  final FreqProxTermsWriterPerField terms;
  final FreqProxPostingsArray postingsArray;
  final ByteSliceReader reader = new ByteSliceReader();
  final boolean readTermFreq;
  int docID = -1;
  int freq;
  // 用来标记是否处理结束
  boolean ended;
  int termID;

  public FreqProxDocsEnum(
      FreqProxTermsWriterPerField terms, FreqProxPostingsArray postingsArray) {
    this.terms = terms;
    this.postingsArray = postingsArray;
    this.readTermFreq = terms.hasFreq;
  }

  public void reset(int termID) {
    this.termID = termID;
    terms.initReader(reader, termID, 0);
    ended = false;
    docID = -1;
  }

  @Override
  public int docID() {
    return docID;
  }

  @Override
  public int freq() {
    if (!readTermFreq) {
      throw new IllegalStateException("freq was not indexed");
    } else {
      return freq;
    }
  }

  @Override
  public int nextDoc() throws IOException {
    // 因为docID是差值存储,并且第一个docID是和0做差值,所以docID初始化为0
    if (docID == -1) {
      docID = 0;
    }
    // stream 0是否结束了  
    if (reader.eof()) {
      // 如果已经全部处理结束,则返回  NO_MORE_DOCS
      if (ended) {
        return NO_MORE_DOCS;
      } else {
        // 标记全部处理结束  
        ended = true;
        // 我们在构建中的遗留问题答案:
        // term出现的最后一个docID和freq没有写入bytePool的stream0中,
        // 所以在bytePool的stream0读取结束之后,
        // 我们需要从postingsArray.lastDocIDs和postingsArray.termFreqs获取term出现的最后一个docID和对应的freq
        docID = postingsArray.lastDocIDs[termID];
        if (readTermFreq) {
          freq = postingsArray.termFreqs[termID];
        }
      }
    } else {
      int code = reader.readVInt();
      if (!readTermFreq) { // 如果没有存频率,则读取的就是docID的差值
        docID += code;
      } else { // 如果存在频率,则读取的是docID差值的左移一位,因为还原需要右移一位
        docID += code >>> 1;
        if ((code & 1) != 0) { // code的最后一位是1,表示频率就是1
          freq = 1;
        } else { // code的最后一位是0,表示频率需要读取下一个数据
          freq = reader.readVInt();
        }
      }
    }

    return docID;
  }
}

FreqProxPostingsEnum

完整倒排信息的迭代器。

// 完整的倒排信息读取
private static class FreqProxPostingsEnum extends PostingsEnum {

  final FreqProxTermsWriterPerField terms;
  final FreqProxPostingsArray postingsArray;
  // 读取stream0  
  final ByteSliceReader reader = new ByteSliceReader();
  // 读取stream1  
  final ByteSliceReader posReader = new ByteSliceReader();
  final boolean readOffsets;
  int docID = -1;
  int freq;
  int pos;
  int startOffset;
  int endOffset;
  // 剩余的position个数,从freq开始递减  
  int posLeft;
  int termID;
  boolean ended;
  boolean hasPayload;
  BytesRefBuilder payload = new BytesRefBuilder();

  public FreqProxPostingsEnum(
      FreqProxTermsWriterPerField terms, FreqProxPostingsArray postingsArray) {
    this.terms = terms;
    this.postingsArray = postingsArray;
    this.readOffsets = terms.hasOffsets;
    assert terms.hasProx;
    assert terms.hasFreq;
  }

  public void reset(int termID) {
    this.termID = termID;
    terms.initReader(reader, termID, 0);
    terms.initReader(posReader, termID, 1);
    ended = false;
    docID = -1;
    posLeft = 0;
  }

  @Override
  public int docID() {
    return docID;
  }

  @Override
  public int freq() {
    return freq;
  }

  // 同FreqProxDocsEnum#nextDoc类似
  public int nextDoc() throws IOException { 
    if (docID == -1) {
      docID = 0;
    }
    // 如果当前docID的stream1的信息还没处理完,则通过  nextPosition 都忽略掉
    while (posLeft != 0) {
      nextPosition();
    }

    if (reader.eof()) {
      if (ended) {
        return NO_MORE_DOCS;
      } else {
        ended = true;
        docID = postingsArray.lastDocIDs[termID];
        freq = postingsArray.termFreqs[termID];
      }
    } else {
      int code = reader.readVInt();
      docID += code >>> 1;
      if ((code & 1) != 0) {
        freq = 1;
      } else {
        freq = reader.readVInt();
      }
    }

    posLeft = freq;
    pos = 0;
    startOffset = 0;
    return docID;
  }

  @Override
  public int advance(int target) {
    throw new UnsupportedOperationException();
  }

  @Override
  public long cost() {
    throw new UnsupportedOperationException();
  }

  @Override
  public int nextPosition() throws IOException {
    // 剩余的位置个数减一
    posLeft--;
    int code = posReader.readVInt();
    pos += code >>> 1;
    if ((code & 1) != 0) { // 如果存在payload数据则解析payload数据
      hasPayload = true;
      payload.setLength(posReader.readVInt());
      payload.grow(payload.length());
      posReader.readBytes(payload.bytes(), 0, payload.length());
    } else {
      hasPayload = false;
    }

    // 获取startOffset和endOffset  
    if (readOffsets) {
      startOffset += posReader.readVInt();
      endOffset = startOffset + posReader.readVInt();
    }

    return pos;
  }

  @Override
  public int startOffset() {
    if (!readOffsets) {
      throw new IllegalStateException("offsets were not indexed");
    }
    return startOffset;
  }

  @Override
  public int endOffset() {
    if (!readOffsets) {
      throw new IllegalStateException("offsets were not indexed");
    }
    return endOffset;
  }

  @Override
  public BytesRef getPayload() {
    if (hasPayload) {
      return payload.get();
    } else {
      return null;
    }
  }
}

读取demo

倒排内存结构9.png

上图是我们在讲构建时的例子,构建最后完成时的结构如上图所示。我们以上图为例,来看看内存中的倒排结构是怎么读取的,下面例子假设stream0和stream1 都需要读取。

读取所有的term

如何获取所有的term?通过textStarts中的信息,我们可以获取term在bytePool中的起始位置,然后我们就可以读取term的长度,再读取term的内容。

读取term的stream

读取stream其实就是读取stream中所有的slice,我们从intPool中可以得到term的stream的结束位置,因为intPool中记录的就是stream下一个可以写入的位置。从byteStarts[termID] + stream * ByteBlockPool.FIRST_LEVEL_SIZE可以获取term的stream的起始位置,用level变量来记录当前stream用到的slice的level,从level我们可以知道当前的slice的大小。如果intPool中的记录的结束位置大于当前的slice的结束位置,则说明还存在下一个slice,当前slice的最后四个字节就是下一个slice的起始位置。

读取term的倒排信息

接下来,我们以“lucene”为例,获取“lucene”所有的倒排信息。

首先,从byteStarts中,根据byteStarts[termID]和byteStarts[termID] + ByteBlockPool.FIRST_LEVEL_SIZE获取“lucene”的stream0和stream1的第一个slice的起始位置,分别是7和12。

通过FreqProxPostingsEnum#nextDoc定位到第一个doc的存储位置,读取stream0当前位置,得到1,1是文档编号和频率的混合编码,1的最低位是1说明freq是1,1 >> 1 = 0,因为stream0中记录的docId是偏移量,初始docId=0,所以docId是0 + 0 = 0。也就是说,“lucene”在doc0中出现了一次,接着通过FreqProxPostingsEnum#nextPosition可以从stream1当前位置读取到0,0的最低位是0表示没有payload信息,0 >> 1 = 0,表示“lucene”在doc0出现的position是0。这时候stream1已经到了limit,说明当前slice读取完毕,往后读四个字节,得到下一个slice的起始位置是63,接着读stream1,0表示startOffset,下一个值是6,表示term的长度,startOffset+term长度=6=endOffset。到此doc0的数据已经读取完了。

接着调用FreqProxPostingsEnum#nextDoc读取下一个文档号和频率,在stream0中读取,这时候发现slice已经读完了,说明只剩下最后一个doc了,最后一个doc的文档号和频率从postingsArray.lastDocIDs和postingsArray.termFreqs中获取,得到分别是1和2。说明“lucene”在doc1中出现了两次,可以调用两次FreqProxPostingsEnum#nextPosition获取位置相关信息:

  • 读取第一个位置

    从stream1当前位置读取到0,0的最低位是0表示没有payload信息,0 >> 1 = 0,表示“lucene”在doc1第一次出现的position是0。接着读stream1,0表示startOffset,下一个值是6,表示term的长度,startOffset+term长度=6=endOffset。

  • 读取第二个位置

    从stream1当前位置读取到6,0的最低位是0表示没有payload信息,6 >> 1 = 3,表示“lucene”在doc1第二次出现的position是3。接着读stream1,20表示startOffset,下一个值是6,表示term的长度,startOffset+term长度=26=endOffset。

到此,就读取到了“lucene”的所有倒排信息。