Lucene源码分析 - CompressingStoredFieldsWriter 文档列写入

本文主要分析了Lucene 6.3.0中CompressingStoredFieldsWriter的实现,该类负责将Document的Field数据写入.fdt和.fdx文件。.fdt文件中的数据使用LZ4压缩,而.fdx文件用于快速定位Document。内容包括Field的优化存储方式、内存缓冲区管理和磁盘刷写策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文的代码以lucene-core 6.3.0为准,包含CompressingStoredFieldsWriter具体实现。转载请注明出处。

0 - 基本信息

   CompressingStoredFieldsWriter类是Lucene将每个Document的所有Field写入文件的具体实现类,在DefaultIndexingChain类中被调用,调用顺序是startDocument -> writeField -> finishDocument,过程比较简单。
   通过构造函数中可以了解到,Document需要存两部分数据,.fdt中存的是Document的每个Field的数据,.fdx存的是Document的索引数据。这里需要了解到的一个背景是,.fdx文件中,将16K(1 << 14)大小的或者数量为128的Document组成一个chunk,每个chunk是用LZ4函数压缩存储的。在整个.fdx中快速定位一个文档,需要先定位到文档的chunk位置,然后解压缩这个chunk的数据,最后找到文档在chunk中的位置才能找到文档的所有数据。
   在构造函数中fieldsStreamindexWriter分别可以写入.fdt和.fdx文件,了解背景,再看源码相对比较简单。

1 - CompressingStoredFieldsWriter 源码分析

   writeField函数实现了将一个Field的内容写入到内存中。

  public void writeField(FieldInfo info, IndexableField field)
      throws IOException {

    ++numStoredFieldsInDoc;

    int bits = 0;
    final BytesRef bytes;
    final String string;
	
	// 判断Field的类型,数字,字符串或者二进制,并计算长度
    Number number = field.numericValue();
    if (number != null) {
      if (number instanceof Byte || number instanceof Short || number instanceof Integer) {
        bits = NUMERIC_INT;
      } else if (number instanceof Long) {
        bits = NUMERIC_LONG;
      } else if (number instanceof Float) {
        bits = NUMERIC_FLOAT;
      } else if (number instanceof Double) {
        bits = NUMERIC_DOUBLE;
      } else {
        throw new IllegalArgumentException("cannot store numeric type " + number.getClass());
      }
      string = null;
      bytes = null;
    } else {
      bytes = field.binaryValue();
      if (bytes != null) {
        bits = BYTE_ARR;
        string = null;
      } else {
        bits = STRING;
        string = field.stringValue();
        if (string == null) {
          throw new IllegalArgumentException("field " + field.name() + " is stored but does not have binaryValue, stringValue nor numericValue");
        }
      }
    }

    final long infoAndBits = (((long) info.number) << TYPE_BITS) | bits;
    bufferedDocs.writeVLong(infoAndBits);
    
	// 不同的数据类型,都要写入到内存buffer中
    if (bytes != null) {
      bufferedDocs.writeVInt(bytes.length);
      bufferedDocs.writeBytes(bytes.bytes, bytes.offset, bytes.length);
    } else if (string != null) {
      bufferedDocs.writeString(string);
    } else {
      if (number instanceof Byte || number instanceof Short || number instanceof Integer) {
        bufferedDocs.writeZInt(number.intValue());
      } else if (number instanceof Long) {
        writeTLong(bufferedDocs, number.longValue());
      } else if (number instanceof Float) {
        writeZFloat(bufferedDocs, number.floatValue());
      } else if (number instanceof Double) {
        writeZDouble(bufferedDocs, number.doubleValue());
      } else {
        throw new AssertionError("Cannot get here");
      }
    }
  }

   数字类型的Field写入内存Buffer的时候进行了优化,都存的是可变长度,对于整形数字还用了ZigZag编码,目的就是要让值比较小的数字存储的时候占用的空间尽可能少。
   当一个Document的多个Field都写入内存之后,调用finishDocument记录Field的元数据,如果内存的Buff写满了需要刷到磁盘。

  public void finishDocument() throws IOException {
  	// numBufferedDocs数组用于记录Document的Field的数量
  	// 数组长度不够的话扩容
    if (numBufferedDocs == this.numStoredFields.length) {
      final int newLength = ArrayUtil.oversize(numBufferedDocs + 1, 4);
      this.numStoredFields = Arrays.copyOf(this.numStoredFields, newLength);
      endOffsets = Arrays.copyOf(endOffsets, newLength);
    }
    // 记录Document的Field的数量
    this.numStoredFields[numBufferedDocs] = numStoredFieldsInDoc;
    // 当前Document已经结束,所以重置Field计数器
    numStoredFieldsInDoc = 0;
    // 记录当前Document在文件中的偏移量
    endOffsets[numBufferedDocs] = bufferedDocs.length;
    ++numBufferedDocs;
    // 判断是否需要刷新到磁盘,内存满了就需要刷
    if (triggerFlush()) {
      flush();
    }
  }

   如果内存Buff不够,或者所有的文档写入完毕,调用finish将所有Field的数据写入到文件中,.fdt文件中存的数据要用LZ4压缩算法进行压缩,.fdx中只存了。

  private void flush() throws IOException {
  	// 记录当前chunk在 .fdt 文件中的起始位置
    indexWriter.writeIndex(numBufferedDocs, fieldsStream.getFilePointer());

    // 将每个文档的 position 值转换一下,让除了第一个值以外的每个值变得更小
    // 相当于是优化存储,细节可以不管
    final int[] lengths = endOffsets;
    for (int i = numBufferedDocs - 1; i > 0; --i) {
      lengths[i] = endOffsets[i] - endOffsets[i - 1];
      assert lengths[i] >= 0;
    }
    // 如果出现文档比较小,导致 16k(chunkSize) 内存中有超过 2*chunkSize 数量的文档
    // 那么把这个 chunk 的文档切成多个部分,分别存储
    // 实际上这种情况是不存在的,因为内存中最多存在 maxDocsPerChunk=128 个文档
    final boolean sliced = bufferedDocs.length >= 2 * chunkSize;
    // 将 docBase 写入到 chunk 的header中
    writeHeader(docBase, numBufferedDocs, numStoredFields, lengths, sliced);

    // 压缩存储
    if (sliced) {
      // 如果内存中的文档数量太多,那么分成多个部分存储
      for (int compressed = 0; compressed < bufferedDocs.length; compressed += chunkSize) {
        compressor.compress(bufferedDocs.bytes, compressed, Math.min(chunkSize, bufferedDocs.length - compressed), fieldsStream);
      }
    } else {
      // 正常的压缩存储
      compressor.compress(bufferedDocs.bytes, 0, bufferedDocs.length, fieldsStream);
    }

    // 重置所有变量
    docBase += numBufferedDocs;
    numBufferedDocs = 0;
    bufferedDocs.length = 0;
    numChunks++;
  }

2 - CompressingStoredFieldsIndexWriter 说明

   以上可以了解到.fdt文件中的数据是怎么存的,这里分析下.fdx文件中的数据如何存。上述已知.fdt文件中,所有文档被划分成多个chunk存到文件中,那么.fdx中的索引数据就是要能快速定位chunk,所以需要存docBase和文件的startPosition,这部分内容相对简单,就不详细说明了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值