Lucene源码系列(二十一):DocValues-综述

1,137 阅读5分钟

综述

Lucene中有个比较特殊的索引文件,叫做DocValues。从字面意思看,就是文档的值,在lucene中,文档是由字段组成的,所以文档的值其实就是字段的值。说起字段的值,前面我们已经介绍过了正排索引,就是用来存储文档中字段的值,那它和我们接下来要介绍的DocValues有什么区别呢?

在正排索引中,同一个文档中所有的字段的值都存储在一起,以存储的专业术语就是行式存储。而DocValues是同一字段所有doc的值都存储在一起,也就是列式存储。

行式存储我们可以通过docId很容易拿到docId所有字段的值,更注重的是根据docID来获取所有字段的值,因此lucene中正排数据是用来在返回召回结果时,根据docID填充字段信息返回给用户。而列式存储我们可以容易拿到某个一个字段在所有文档中的值,用列式存储来做排序聚合类操作性能更高。

Lucene中针对不同使用场景,一共设计了5种DocValues。

  • NumericDocValues

    数值类型的docValues,最终都是会转成long来处理,并且一个doc中的字段只能有一个NumericDocValues。

  • SortedNumericDocValues

    同NumericDocValues一样,也是数值类型的docValues,只是一个doc中的字段可以有多个SortedNumericDocValues,对于同一个字段来说,这些数值是有序的。

  • BinaryDocValues

    字节数组类型的docValues,并且一个doc中的字段只能有一个BinaryDocValues。

  • SortedDocValues

    字节数组类型的docValues,并且一个doc中的字段只能有一个SortedDocValues,但是在存储的时候是全局有序的。

  • SortedSetDocValues

    多值且有序的字节数组

例子

下面的例子中,包含了所有DocValues类型的使用示例:

public class DocValueDemo {
    public static void main(String[] args) throws IOException {
        Directory directory = FSDirectory.open(new File("D:\\code\\lucene-9.1.0-learning\\data").toPath());
        WhitespaceAnalyzer analyzer = new WhitespaceAnalyzer();
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
        indexWriterConfig.setUseCompoundFile(false);
        IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
        
        Document document = new Document();
        // NumericDocValuesField最多只能一个,存储数值类型
        document.add(new NumericDocValuesField("age", 20));
        
        // SortedNumericDocValuesField可以有多个,存储数值类型
        document.add(new SortedNumericDocValuesField("level", 4));
        document.add(new SortedNumericDocValuesField("level", 3));
        document.add(new SortedNumericDocValuesField("level", 0));
        
        // BinaryDocValuesField最多只能一个,存储二进制
        document.add(new BinaryDocValuesField("name", new BytesRef("zjc".getBytes(StandardCharsets.UTF_8))));
        
        // SortedSetDocValuesField可以有多个,存储二进制
        document.add(new SortedSetDocValuesField("address", new BytesRef("hangzhou".getBytes(StandardCharsets.UTF_8))));
        document.add(new SortedSetDocValuesField("address", new BytesRef("beijing".getBytes(StandardCharsets.UTF_8))));
        document.add(new SortedSetDocValuesField("address", new BytesRef("shanghai".getBytes(StandardCharsets.UTF_8))));
        
        // SortedDocValuesField只能有一个,存储二进制
        document.add(new SortedDocValuesField("nickname", new BytesRef("alpha".getBytes(StandardCharsets.UTF_8))));

        indexWriter.addDocument(document);
        indexWriter.flush();
        indexWriter.commit();
        indexWriter.close();
    }
}

文件格式

dvm

整体结构

dvm存储的是每个DocValues字段的元信息,是读取的时候使用。它是分字段存储的,如下图所示:

dvm.png

字段详解

  • FieldMeta:DocValue字段的元信息
    • FieldNumber:字段编号
    • DocValueType:字段的DocValue类型编号
    • DocValueTypeRelated:跟DocValue类型相关的其他元信息,后面的文章会一一介绍。

dvd

整体结构

dvd存储的是每个DocValues字段的数据,它也是分字段存储的,如下图所示,每个字段的数据根据docValues的类型不同,有不同的存储结构,后面我们会一一介绍。

dvd.png

源码解析

本文中的源码解析主要是对DocValues在整体上的构建和读取的代码逻辑做解析,没有具体到各个不同的DocValues类型,具体的DocValues的相关源码我们后面会详细介绍。

构建

数据收集

在文档索引的过程中,如果文档中有DocValues相关的字段,会先使用DocValues类型对应的的DocValuesWriter# addValue实现临时存储起来,在flush的时候执行真正的持久化逻辑。

// IndexingChain#indexDocValue
private void indexDocValue(int docID, PerField fp, DocValuesType dvType, IndexableField field) {
  switch (dvType) {
    case NUMERIC:
      if (field.numericValue() == null) {
        throw new IllegalArgumentException(
            "field=\"" + fp.fieldInfo.name + "\": null value not allowed");
      }
      ((NumericDocValuesWriter) fp.docValuesWriter)
          .addValue(docID, field.numericValue().longValue());
      break;

    case BINARY:
      ((BinaryDocValuesWriter) fp.docValuesWriter).addValue(docID, field.binaryValue());
      break;

    case SORTED:
      ((SortedDocValuesWriter) fp.docValuesWriter).addValue(docID, field.binaryValue());
      break;

    case SORTED_NUMERIC:
      ((SortedNumericDocValuesWriter) fp.docValuesWriter)
          .addValue(docID, field.numericValue().longValue());
      break;

    case SORTED_SET:
      ((SortedSetDocValuesWriter) fp.docValuesWriter).addValue(docID, field.binaryValue());
      break;

    case NONE:
    default:
      throw new AssertionError("unrecognized DocValues.Type: " + dvType);
  }
}

持久化

持久化的入口是使用DocValue对应的DocValuesWriter#flush来开始的:

private void writeDocValues(SegmentWriteState state, Sorter.DocMap sortMap) throws IOException {
  DocValuesConsumer dvConsumer = null;
  boolean success = false;
  try {
    for (int i = 0; i < fieldHash.length; i++) {
      PerField perField = fieldHash[i];
      while (perField != null) {
        if (perField.docValuesWriter != null) {
          // 。。。去掉一些判断逻辑
          // 使用的是对应的DocValues类型的DocValuesWriter中的flush方法来持久化
          perField.docValuesWriter.flush(state, sortMap, dvConsumer);
          perField.docValuesWriter = null;
        } else if (perField.fieldInfo != null
            && perField.fieldInfo.getDocValuesType() != DocValuesType.NONE) {
          throw new AssertionError(
              "segment="
                  + state.segmentInfo
                  + ": field=\""
                  + perField.fieldInfo.name
                  + "\" has docValues but did not write them");
        }
        perField = perField.next;
      }
    }
    success = true;
  } finally {
    if (success) {
      IOUtils.close(dvConsumer);
    } else {
      IOUtils.closeWhileHandlingException(dvConsumer);
    }
  }
}

而真正持久化的核心逻辑在DocValuesConsumer中,其中对应了5中DocValues的处理逻辑,在lucene-9.1.0版本中,实现类是Lucene90DocValuesConsumer,我们后面会一一介绍不同DocValues的持久化逻辑。

public abstract class DocValuesConsumer implements Closeable {

  // 持久化NumericDocValues
  public abstract void addNumericField(FieldInfo field, DocValuesProducer valuesProducer)
      throws IOException;

  // 持久化BinaryDocValues
  public abstract void addBinaryField(FieldInfo field, DocValuesProducer valuesProducer)
      throws IOException;

  // 持久化SortedDocValues
  public abstract void addSortedField(FieldInfo field, DocValuesProducer valuesProducer)
      throws IOException;

  // 持久化SortedNumericDocValues
  public abstract void addSortedNumericField(FieldInfo field, DocValuesProducer valuesProducer)
      throws IOException;

  // 持久化SortedSetDocValues
  public abstract void addSortedSetField(FieldInfo field, DocValuesProducer valuesProducer)
      throws IOException;
    
}

读取

读取的接口是DocValuesProducer:

public abstract class DocValuesProducer implements Closeable {

  public abstract NumericDocValues getNumeric(FieldInfo field) throws IOException;

  public abstract BinaryDocValues getBinary(FieldInfo field) throws IOException;

  public abstract SortedDocValues getSorted(FieldInfo field) throws IOException;

  public abstract SortedNumericDocValues getSortedNumeric(FieldInfo field) throws IOException;

  public abstract SortedSetDocValues getSortedSet(FieldInfo field) throws IOException;
}

DocValuesProducer在lucene-9.1.0版本中的实现类是Lucene90DocValuesProducer,在读取的开始,最重要的是解析元信息,

  // 按DocValues类型解析所有字段的元信息,具体逻辑我们后面一一介绍。
  private void readFields(IndexInput meta, FieldInfos infos) throws IOException {
    for (int fieldNumber = meta.readInt(); fieldNumber != -1; fieldNumber = meta.readInt()) {
      FieldInfo info = infos.fieldInfo(fieldNumber);
      if (info == null) {
        throw new CorruptIndexException("Invalid field number: " + fieldNumber, meta);
      }
      byte type = meta.readByte();
      if (type == Lucene90DocValuesFormat.NUMERIC) {
        numerics.put(info.name, readNumeric(meta));
      } else if (type == Lucene90DocValuesFormat.BINARY) {
        binaries.put(info.name, readBinary(meta));
      } else if (type == Lucene90DocValuesFormat.SORTED) {
        sorted.put(info.name, readSorted(meta));
      } else if (type == Lucene90DocValuesFormat.SORTED_SET) {
        sortedSets.put(info.name, readSortedSet(meta));
      } else if (type == Lucene90DocValuesFormat.SORTED_NUMERIC) {
        sortedNumerics.put(info.name, readSortedNumeric(meta));
      } else {
        throw new CorruptIndexException("invalid type: " + type, meta);
      }
    }
  }

总结

本文主要是对DocValues整体的一个介绍,并没有深入到某一种具体的DocValues,因为每种DocValues的文件格式都是不一样的,后面的文章我们来一一介绍。