Lucene源码系列(二十二):DocValues-NumericDocValues

2,090 阅读8分钟

背景

本文介绍第一种DocValue类型:NumericDocValues。NumericDocValues是其他一些DocValue的基础,所以我们先行介绍。NumericDocValues是数值类型的DocValues,底层只用long来存储,因此如果业务中非long的数值类型需要转化成long来使用NumericDocValues。

前置知识

本文涉及到的一些工具在之前的文章中都做了详细的介绍,后续碰到不会重复介绍。

NumericDocValues的存储类型

对于NumericDocValues中值的分布的不同情况,Lucene提供了多种不同的存储方式,官方文档中为不同存储方式取名及介绍其实并没有十分准确,这里我们参考官方的命名格式做了更合适的描述:

  • table-compressed:

    如果所有的NumericDocValues中存在大量的重复value,去重后的value总数不超过256个,则把这些value排序之后存储在数组中,数组的下标就是对应value的编号,这个数组作为元信息存储在dvm文件中,数据就只存储编号,存储在dvd文件中。

  • single-block-compressed:

    如果NumericDocValues去重后的value总数超过256个,则找出最小值,所有的值都和最小值做差。比如对于数据(15,35,20,25,45),原来每个value的编码至少需要6bits,如果都与最小值15做差,得到(0,20,5,10,30),那每个value最多需要5bits。还可以继续优化,(15,35,20,25,45)的最小公倍数是5,如果再除以最小公倍数就得到了(0,4,1,2,6),这样每个value最多只需要3bits。

  • multi-blocks-compressed:

    如果数据量很大的话,则max-min会因为某个值特别大或者特别小导致max-min也很大,起不到压缩的效果,而且数据量大的情况,gcd一般都是1。所以可以分block来处理,把这种特别大或者特别小的值的影响控制在一个block中。没有全局数据信息的时候,无法判断分block是否一定是比不分block的压缩效果好,所以Lucene中会预先统计两者使用的bits数量,如果分block存储相对于single-block-compressed能有10%的存储空间优化,则使用分block的方式存储。

  • Const-compressed:

    如果所有的值都相同,这种特殊情况,只存储一个值就行了。

文件格式

dvm

整体结构

dvm-numeric.png

字段详解

  • FieldNumber:字段的编号
  • DocValueType:字段的docValue类型编号
  • DocsWithField:存在此field的docID集合,分为3种情况:
    • 所有doc都不包含
      • DocsWithFieldOffset:-2,表示所有doc都不包含情况
      • DocsWithFieldLength:0
      • JumpTableEntryCount:-1
      • DenseRankPower:-1
    • 所有doc都包含
      • DocsWithFieldOffset:-1,表示所有的doc都包含情况
      • DocsWithFieldLength:0
      • JumpTableEntryCount:-1
      • DenseRankPower:-1
    • 部分doc包含:使用IndexDISI来存储
      • DocsWithFieldOffset:IndexDISI在dvd文件中的起始位置
      • DocsWithFieldLength:IndexDISI在dvd文件中的总长度
      • JumpTableEntryCount:IndexDISI中jump的参数
      • DenseRankPower:IndexDISI中rank的参数
  • NumValues:一共有多少个value
  • Table:table-compressed存储结构的元信息
    • TableSize:
      • const-compressed或者是single-block-compressed,则TableSize=-1。后面在通过NumBitsPerValue字段来区分这两种情况,如果NumBitsPerValue=0,则是const-compressed,value读取Min字段就可以。否则就是single-block-compressed,按照single-block-compressed的存储方式来读取即可。
      • 如果使用multi-blocks-compressed存储,则TableSize=-2-NumericBlockShift,NumericBlockShift决定了block的大小。
      • 如果使用table-compressed存储,则TableSize就是去重后值的个数
    • TableValues:只有使用table-compressed存储的时候,该值才存在,并且存储的是去重后且排序的值。
  • NumBitsPerValue
    • table-compressed:uniqueValues.size() - 1所需的bit
    • multi-blocks-compressed:0xFF
    • single-block-compressed:(max - min) / gcd
  • Min:所有的value都会转成(value- min) / gcd。这个min是给single-block-compressed和table-compressed使用的。multi-blocks-compressed每个block都有一个min,存储在dvd文件中。
  • GCD:所有的value都会转成(value- min) / gcd
  • DataStartOffset:该field的docValues数据在dvd文件的起始位置
  • DataLength:该field的docValues数据在dvd文件的总长度
  • JumpTableOffset:如果是multi-blocks-compressed,则存储的是所有block的索引信息在dvd文件的起始位置。否则是-1。

dvd

整体结构

dvd-numeric.png

字段详解

NumericDocValues的dvd文件也是按照存储格式来区分的:

const-compressed

所有的信息都在dvm文件中。

table-compressed

把所有的value都转成唯一编号,然后使用DirectWriter存储。

single-block-compressed

把所有的value都转成 (value- min) / gcd,然后使用DirectWriter存储。

multi-blocks-compressed
  • BlockDatas:block的数据

    • BlockData:有两种情况

      (1)所有的值都相等:

      • 0:标记位,表示这种情况
      • Value:都相等的那个值

      (2)使用DirectWriter存储

      • BitsPerValue:block中每个value占用的bit
      • Min:block中的最小值
      • BlockDataLength:block的存储空间大小
      • DirectWriter:使用DirectWriter存储数据
  • BlockOffsets:block的索引,也是block的起始位置

    • BlockOffset:对应的block的起始位置

源码解析

构建

数据收集

NumericDocValues临时存储的工具是PackedLongValues,每处理256个value使用的是Packed64进行压缩存储。

在持久化的时候,会把NumericDocValues的值和包含该field的docID集合会根据是否需要对doc进行排序都封装到BufferedNumericDocValues或者SortingNumericDocValues中。

这里需要介绍下doc排序相关的知识,我们在索引的时候,可以指定按某些条件对所有doc进行排序,这样是为了在检索的时候,可以根据时间或者召回个数进行截断。而排序的条件一般就是按1个字段或者多个字段的值进行排序。在这里,我们了解下即可,以后介绍索引的整个流程时会详细介绍。

NumericDocValuesWriter
class NumericDocValuesWriter extends DocValuesWriter<NumericDocValues> {
  // 用来构建PackedLongValues
  private final PackedLongValues.Builder pending;
  // 最终docValues都存储在PackedLongValues
  private PackedLongValues finalValues;
  // 全局内存统计  
  private final Counter iwBytesUsed;
  // 收集NumericDocValues使用到的内存  
  private long bytesUsed;
  // 用来记录包含此docValues字段的docId集合  
  private DocsWithFieldSet docsWithField;
  // 字段的元信息  
  private final FieldInfo fieldInfo;
  // 前一个docID,用来判断docID是否出现过
  private int lastDocID = -1;

  NumericDocValuesWriter(FieldInfo fieldInfo, Counter iwBytesUsed) {
    pending = PackedLongValues.deltaPackedBuilder(PackedInts.COMPACT);
    docsWithField = new DocsWithFieldSet();
    bytesUsed = pending.ramBytesUsed() + docsWithField.ramBytesUsed();
    this.fieldInfo = fieldInfo;
    this.iwBytesUsed = iwBytesUsed;
    iwBytesUsed.addAndGet(bytesUsed);
  }
  // 新增一个value
  public void addValue(int docID, long value) {
    // docID必须是严格递增的,也就是一个docID只能有一个NumericDocValues  
    if (docID <= lastDocID) {
      throw new IllegalArgumentException(
          "DocValuesField \""
              + fieldInfo.name
              + "\" appears more than once in this document (only one value is allowed per field)");
    }
    // 把value临时存起来
    pending.add(value);
    // 记录docId  
    docsWithField.add(docID);
    // 更新内存使用大小
    updateBytesUsed();
    lastDocID = docID;
  }

  private void updateBytesUsed() {
    final long newBytesUsed = pending.ramBytesUsed() + docsWithField.ramBytesUsed();
    iwBytesUsed.addAndGet(newBytesUsed - bytesUsed);
    bytesUsed = newBytesUsed;
  }

  @Override
  NumericDocValues getDocValues() {
    if (finalValues == null) {
      finalValues = pending.build();
    }
    // 把结果封装成  BufferedNumericDocValues,可以获取到所有的docID对应的docValues
    return new BufferedNumericDocValues(finalValues, docsWithField.iterator());
  }

  // 根据  sortMap 中的排序信息排序,sortMap中的oldToNew方法,输入是docID,输出是根据排序得到的新的docID
  static NumericDVs sortDocValues(int maxDoc, Sorter.DocMap sortMap, NumericDocValues oldDocValues)
      throws IOException {
    FixedBitSet docsWithField = new FixedBitSet(maxDoc);
    long[] values = new long[maxDoc];
    while (true) {
      int docID = oldDocValues.nextDoc();
      if (docID == NO_MORE_DOCS) {
        break;
      }
      int newDocID = sortMap.oldToNew(docID);
      docsWithField.set(newDocID);
      values[newDocID] = oldDocValues.longValue();
    }
    //  NumericDVs中封装的是排序后的docID和对应的docValues 
    return new NumericDVs(values, docsWithField);
  }

  @Override
  public void flush(SegmentWriteState state, Sorter.DocMap sortMap, DocValuesConsumer dvConsumer)
      throws IOException {
    if (finalValues == null) { // 固定values
      finalValues = pending.build();
    }
    final NumericDVs sorted;
    if (sortMap != null) {
      NumericDocValues oldValues =
          new BufferedNumericDocValues(finalValues, docsWithField.iterator());
      sorted = sortDocValues(state.segmentInfo.maxDoc(), sortMap, oldValues);
    } else {
      sorted = null;
    }
    // dvConsumer就是Lucene90DocValuesConsumer
    dvConsumer.addNumericField(
        fieldInfo,
        new EmptyDocValuesProducer() {
          @Override
          public NumericDocValues getNumeric(FieldInfo fieldInfo) {
            if (fieldInfo != NumericDocValuesWriter.this.fieldInfo) {
              throw new IllegalArgumentException("wrong fieldInfo");
            }
            if (sorted == null) { // 不需要对doc排序,则按docID的处理顺序存储
              return new BufferedNumericDocValues(finalValues, docsWithField.iterator());
            } else { // 按照指定的doc排序存储
              return new SortingNumericDocValues(sorted);
            }
          }
        });
  }
}
BufferedNumericDocValues
  static class BufferedNumericDocValues extends NumericDocValues {
    // value  
    final PackedLongValues.Iterator iter;
    // docID  
    final DocIdSetIterator docsWithField;
    // 当前docID对应的value  
    private long value;

    BufferedNumericDocValues(PackedLongValues values, DocIdSetIterator docsWithFields) {
      this.iter = values.iterator();
      this.docsWithField = docsWithFields;
    }

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

    @Override
    public int nextDoc() throws IOException {
      int docID = docsWithField.nextDoc();
      if (docID != NO_MORE_DOCS) {
        value = iter.next();
      }
      return docID;
    }

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

    @Override
    public boolean advanceExact(int target) throws IOException {
      throw new UnsupportedOperationException();
    }

    @Override
    public long cost() {
      return docsWithField.cost();
    }

    @Override
    public long longValue() {
      return value;
    }
  }
SortingNumericDocValues
  static class SortingNumericDocValues extends NumericDocValues {
    // 存储的是按指定顺序排好序的docID和对应的docValues
    private final NumericDVs dvs;
    private int docID = -1;
    private long cost = -1;

    SortingNumericDocValues(NumericDVs dvs) {
      this.dvs = dvs;
    }

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

    @Override
    public int nextDoc() {
      if (docID + 1 == dvs.docsWithField.length()) {
        docID = NO_MORE_DOCS;
      } else {
        docID = dvs.docsWithField.nextSetBit(docID + 1);
      }
      return docID;
    }

    @Override
    public int advance(int target) {
      throw new UnsupportedOperationException("use nextDoc() instead");
    }

    @Override
    public boolean advanceExact(int target) throws IOException {
      // needed in IndexSorter#{Long|Int|Double|Float}Sorter
      docID = target;
      return dvs.docsWithField.get(target);
    }

    @Override
    public long longValue() {
      return dvs.values[docID];
    }

    @Override
    public long cost() {
      if (cost == -1) {
        cost = dvs.docsWithField.cardinality();
      }
      return cost;
    }
  }

持久化

NumericDocValues持久化的核心逻辑在writeValues方法中,主要做了以下几件事:

  • 遍历所有的value,做一些评估,确定使用哪种存储方式
    • 计算所有value的gcd
    • 记录去重的value个数,如果超过256就不使用table-compressed
    • 记录multi-blocks-compressed和single-block-compressed分别使用的bits总数(借助MinMaxTracker工具类,比较简单,自行查看)
  • 记录本字段的docValue元信息
  • 记录存在本字段的docID集合
  • 根据存储方式存储docValue数据
  public void addNumericField(FieldInfo field, DocValuesProducer valuesProducer)
      throws IOException {
    // 字段的编号  
    meta.writeInt(field.number);
    // docValue的类型  
    meta.writeByte(Lucene90DocValuesFormat.NUMERIC);
    // 决定使用哪种存储方式的逻辑都在这里面
    writeValues(
        field,
        new EmptyDocValuesProducer() {
          @Override
          public SortedNumericDocValues getSortedNumeric(FieldInfo field) throws IOException {
            return DocValues.singleton(valuesProducer.getNumeric(field));
          }
        },
        false);
  }  

  private long[] writeValues(FieldInfo field, DocValuesProducer valuesProducer, boolean ords)
      throws IOException {
    SortedNumericDocValues values = valuesProducer.getSortedNumeric(field);
    // 获取第一个value,所有的value都会减去这个值再算gcd
    // MR的commit信息说这样merge的时候能加速10%左右,不懂什么原理,先留个坑,以后明白了再来补充
    final long firstValue;
    if (values.nextDoc() != DocIdSetIterator.NO_MORE_DOCS) {
      firstValue = values.nextValue();
    } else {
      firstValue = 0L;
    }
    // values是迭代器,需要重新获取   
    values = valuesProducer.getSortedNumeric(field);
    // 多少个doc存在该field的docValue  
    int numDocsWithValue = 0;
    // 全局的最大最小值跟踪器  
    MinMaxTracker minMax = new MinMaxTracker();
    // 每个block的最大最小值跟踪器  
    MinMaxTracker blockMinMax = new MinMaxTracker();
    // 所有value的gcd  
    long gcd = 0;
    // 用来统计唯一值有多少个,临界是256,不超过的话有特殊的存储方式  
    Set<Long> uniqueValues = ords ? null : new HashSet<>();
    // 遍历所有的doc  
    for (int doc = values.nextDoc(); doc != DocIdSetIterator.NO_MORE_DOCS; doc = values.nextDoc()) {
      // 遍历doc中所有的value  
      for (int i = 0, count = values.docValueCount(); i < count; ++i) {
        long v = values.nextValue();
        // 更新gcd
        if (gcd != 1) {
          if (v < Long.MIN_VALUE / 2 || v > Long.MAX_VALUE / 2) {
            gcd = 1;
          } else {
            gcd = MathUtil.gcd(gcd, v - firstValue);
          }
        }
        // 更新当前block的最大最小值
        blockMinMax.update(v);
        if (blockMinMax.numValues == NUMERIC_BLOCK_SIZE) { // 如果block满了
          // 更新全局的最大最小值  
          minMax.update(blockMinMax);
          blockMinMax.nextBlock();
        }
        // 如果唯一值超过了256,则不需要再继续统计了
        if (uniqueValues != null && uniqueValues.add(v) && uniqueValues.size() > 256) {
          uniqueValues = null;
        }
      }

      numDocsWithValue++;
    }

    minMax.update(blockMinMax);
    minMax.finish();
    blockMinMax.finish();

    final long numValues = minMax.numValues;
    long min = minMax.min;
    final long max = minMax.max;
    assert blockMinMax.spaceInBits <= minMax.spaceInBits;
    
    // 存储存在该field的docID,有三种方式
    // 在dvm文件中会有四个字段,其实是使用IndexDISI的时候需要:
    // 1.docsWithFieldOffset
    // 2.docsWithFieldLength  
    // 3.jumpTableEntryCount
    // 4.denseRankPower  
    if (numDocsWithValue == 0) { // 如果所有的文档都不存在该字段
      meta.writeLong(-2); 
      meta.writeLong(0L); 
      meta.writeShort((short) -1); 
      meta.writeByte((byte) -1); 
    } else if (numDocsWithValue == maxDoc) { // 所有的文档都存在该字段
      meta.writeLong(-1); 
      meta.writeLong(0L); 
      meta.writeShort((short) -1); 
      meta.writeByte((byte) -1); 
    } else { // 部分文档存在该字段,使用IndexedDISI存储所有的docID
      // 在dvd文件中的起始位置  
      long offset = data.getFilePointer();
      meta.writeLong(offset); 
      values = valuesProducer.getSortedNumeric(field);
      final short jumpTableEntryCount =
          IndexedDISI.writeBitSet(values, data, IndexedDISI.DEFAULT_DENSE_RANK_POWER);
      meta.writeLong(data.getFilePointer() - offset); 
      meta.writeShort(jumpTableEntryCount);
      meta.writeByte(IndexedDISI.DEFAULT_DENSE_RANK_POWER);
    }
    // 元信息记录一共有多少个value
    meta.writeLong(numValues);
    final int numBitsPerValue;
    // 判断是否使用multi-blocks-compressed存储方式  
    boolean doBlocks = false;
    // table-compressed存储方式使用
    Map<Long, Integer> encode = null;
    if (min >= max) { // 所有的value都是0
      numBitsPerValue = 0;
      meta.writeInt(-1); // tablesize
    } else {
      // single-block-compressed使用的空间:DirectWriter.unsignedBitsRequired((max - min) / gcd) 
      // table-compressed使用的空间:DirectWriter.unsignedBitsRequired(uniqueValues.size() - 1)
      if (uniqueValues != null
          && uniqueValues.size() > 1
          && DirectWriter.unsignedBitsRequired(uniqueValues.size() - 1)
              < DirectWriter.unsignedBitsRequired((max - min) / gcd)) {
        numBitsPerValue = DirectWriter.unsignedBitsRequired(uniqueValues.size() - 1);
        final Long[] sortedUniqueValues = uniqueValues.toArray(new Long[0]);
        // 对所有的唯一值进行排序。源码看到现在,没发现排序有什么用,判断某个值是否存在?以后有结论了来修改  
        Arrays.sort(sortedUniqueValues);
        // 使用table-compressed的时候存储的是tableSize  
        meta.writeInt(sortedUniqueValues.length); 
        for (Long v : sortedUniqueValues) {
          meta.writeLong(v); 
        }
        // key是value,值是编码,其实就是数组的下标  
        encode = new HashMap<>();
        for (int i = 0; i < sortedUniqueValues.length; ++i) {
          encode.put(sortedUniqueValues[i], i);
        }
        min = 0;
        gcd = 1;
      } else {
        uniqueValues = null;
        // 如果multi-blocks-compressed比single-block-compressed的存储空间优化10%以上,
        // 则使用multi-blocks-compressed
        doBlocks =
            minMax.spaceInBits > 0 && (double) blockMinMax.spaceInBits / minMax.spaceInBits <= 0.9;
        if (doBlocks) {
          numBitsPerValue = 0xFF;
          // multi-blocks-compressed存储方式,tableSize= -2 - NUMERIC_BLOCK_SHIFT
          meta.writeInt(-2 - NUMERIC_BLOCK_SHIFT); 
        } else {
          numBitsPerValue = DirectWriter.unsignedBitsRequired((max - min) / gcd);
          if (gcd == 1
              && min > 0
              && DirectWriter.unsignedBitsRequired(max)
                  == DirectWriter.unsignedBitsRequired(max - min)) { 
            // 把min设为0,这样读取的时候不需要额外的计算
            min = 0;
          }
          // single-block-compressed存储方式,tableSize= -1
          meta.writeInt(-1);
        }
      }
    }

    meta.writeByte((byte) numBitsPerValue);
    meta.writeLong(min);
    meta.writeLong(gcd);
    // 当前字段的docvalues在dvd文件中的起始位置  
    long startOffset = data.getFilePointer();  
    meta.writeLong(startOffset); 
    // 使用multi-blocks-compressed存储方式时,block的索引在dvd文件中的起始位置  
    long jumpTableOffset = -1;
    if (doBlocks) {  
      jumpTableOffset = writeValuesMultipleBlocks(valuesProducer.getSortedNumeric(field), gcd);
    } else if (numBitsPerValue != 0) {
      // 使用DirectWriter存储  
      writeValuesSingleBlock(
          valuesProducer.getSortedNumeric(field), numValues, numBitsPerValue, min, gcd, encode);
    }
    // 当前field的docvalues在dvd文件中的data length  
    meta.writeLong(data.getFilePointer() - startOffset); 
    meta.writeLong(jumpTableOffset);
    return new long[] {numDocsWithValue, numValues};
  }

  private void writeValuesSingleBlock(
      SortedNumericDocValues values,
      long numValues,
      int numBitsPerValue,
      long min,
      long gcd,
      Map<Long, Integer> encode)
      throws IOException {
    DirectWriter writer = DirectWriter.getInstance(data, numValues, numBitsPerValue);
    for (int doc = values.nextDoc(); doc != DocIdSetIterator.NO_MORE_DOCS; doc = values.nextDoc()) {
      for (int i = 0, count = values.docValueCount(); i < count; ++i) {
        long v = values.nextValue();
        if (encode == null) {
          writer.add((v - min) / gcd);
        } else { // 如果使用的是table-compressed,则存储的value的编码
          writer.add(encode.get(v));
        }
      }
    }
    writer.finish();
  }

  // Returns the offset to the jump-table for vBPV
  private long writeValuesMultipleBlocks(SortedNumericDocValues values, long gcd)
      throws IOException {
    long[] offsets = new long[ArrayUtil.oversize(1, Long.BYTES)];
    int offsetsIndex = 0;
    final long[] buffer = new long[NUMERIC_BLOCK_SIZE];
    final ByteBuffersDataOutput encodeBuffer = ByteBuffersDataOutput.newResettableInstance();
    int upTo = 0;
    for (int doc = values.nextDoc(); doc != DocIdSetIterator.NO_MORE_DOCS; doc = values.nextDoc()) {
      for (int i = 0, count = values.docValueCount(); i < count; ++i) {
        buffer[upTo++] = values.nextValue();
        if (upTo == NUMERIC_BLOCK_SIZE) { // 生成一个block
          offsets = ArrayUtil.grow(offsets, offsetsIndex + 1);
          // block在dvd文件中的起始位置  
          offsets[offsetsIndex++] = data.getFilePointer();
          writeBlock(buffer, NUMERIC_BLOCK_SIZE, gcd, encodeBuffer);
          upTo = 0;
        }
      }
    }
    if (upTo > 0) { // 最后一个block
      offsets = ArrayUtil.grow(offsets, offsetsIndex + 1);
      offsets[offsetsIndex++] = data.getFilePointer();
      writeBlock(buffer, upTo, gcd, encodeBuffer);
    }

    // 记录block的索引,也就是每个block在dvd文件中的起始位置
    final long offsetsOrigo = data.getFilePointer();
    for (int i = 0; i < offsetsIndex; i++) {
      data.writeLong(offsets[i]);
    }
    data.writeLong(offsetsOrigo);
    return offsetsOrigo;
  }

  private void writeBlock(long[] values, int length, long gcd, ByteBuffersDataOutput buffer)
      throws IOException {
    assert length > 0;
    long min = values[0];
    long max = values[0];
    for (int i = 1; i < length; ++i) {
      final long v = values[i];
      assert Math.floorMod(values[i] - min, gcd) == 0;
      min = Math.min(min, v);
      max = Math.max(max, v);
    }
    if (min == max) { // 所有值都相等
      data.writeByte((byte) 0);
      data.writeLong(min);
    } else { // 使用DirectWriter存储
      final int bitsPerValue = DirectWriter.unsignedBitsRequired((max - min) / gcd);
      buffer.reset();
      assert buffer.size() == 0;
      final DirectWriter w = DirectWriter.getInstance(buffer, length, bitsPerValue);
      for (int i = 0; i < length; ++i) {
        w.add((values[i] - min) / gcd);
      }
      w.finish();
      data.writeByte((byte) bitsPerValue);
      data.writeLong(min);
      data.writeInt(Math.toIntExact(buffer.size()));
      buffer.copyTo(data);
    }
  }

读取

其实如果清楚了文件格式中各个字段含义,读取的逻辑是比较简单:

先判断使用的是哪种存储方式,按照对应的存储方式格式来解析数据,构造成可读取的对象。

  private NumericEntry readNumeric(IndexInput meta) throws IOException {
    NumericEntry entry = new NumericEntry();
    // 把NumericDocValues的元信息解析存储在entry中  
    readNumeric(meta, entry);
    return entry;
  }
  
  private void readNumeric(IndexInput meta, NumericEntry entry) throws IOException {
    entry.docsWithFieldOffset = meta.readLong();
    entry.docsWithFieldLength = meta.readLong();
    entry.jumpTableEntryCount = meta.readShort();
    entry.denseRankPower = meta.readByte();
    entry.numValues = meta.readLong();
    // 用来判断是哪种存储方式的  
    int tableSize = meta.readInt();
    if (tableSize > 256) { // 非法值
      throw new CorruptIndexException("invalid table size: " + tableSize, meta);
    }
    if (tableSize >= 0) { // table-compressed存储方式
      entry.table = new long[tableSize];
      for (int i = 0; i < tableSize; ++i) { // 读取所有类别的value,数据table的下标就是编号
        entry.table[i] = meta.readLong();
      }
    }
    if (tableSize < -1) { // multi-blocks-compressed存储方式
      entry.blockShift = -2 - tableSize;
    } else { // single-blocks-compressed存储方式
      entry.blockShift = -1;
    }
    entry.bitsPerValue = meta.readByte();
    entry.minValue = meta.readLong();
    entry.gcd = meta.readLong();
    entry.valuesOffset = meta.readLong();
    entry.valuesLength = meta.readLong();
    entry.valueJumpTableOffset = meta.readLong();
  }
  
  private NumericDocValues getNumeric(NumericEntry entry) throws IOException {
    if (entry.docsWithFieldOffset == -2) { // 所有doc都不包含该field
      return DocValues.emptyNumeric();
    } else if (entry.docsWithFieldOffset == -1) { // 所有doc都包含该field
      if (entry.bitsPerValue == 0) { // 所有的value都相等
        return new DenseNumericDocValues(maxDoc) {
          @Override
          public long longValue() throws IOException {
            return entry.minValue;
          }
        };
      } else {
        final RandomAccessInput slice =
            data.randomAccessSlice(entry.valuesOffset, entry.valuesLength);
        if (entry.blockShift >= 0) { // multi-blocks-compressed
          return new DenseNumericDocValues(maxDoc) {
            final VaryingBPVReader vBPVReader = new VaryingBPVReader(entry, slice);

            @Override
            public long longValue() throws IOException {
              return vBPVReader.getLongValue(doc);
            }
          };
        } else {
          final LongValues values =
              getDirectReaderInstance(slice, entry.bitsPerValue, 0L, entry.numValues);
          if (entry.table != null) { // table-compressed
            final long[] table = entry.table;
            return new DenseNumericDocValues(maxDoc) {
              @Override
              public long longValue() throws IOException {
                return table[(int) values.get(doc)];
              }
            };
          } else if (entry.gcd == 1 && entry.minValue == 0) {
            // gcd是1,min是0的情况,可以直接读取,不需要做 value * gcd + min 的还原计算
            return new DenseNumericDocValues(maxDoc) {
              @Override
              public long longValue() throws IOException {
                return values.get(doc);
              }
            };
          } else {
            final long mul = entry.gcd;
            final long delta = entry.minValue;
            return new DenseNumericDocValues(maxDoc) {
              @Override
              public long longValue() throws IOException {
                return mul * values.get(doc) + delta;
              }
            };
          }
        }
      }
    } else {// 部分doc包含field
      final IndexedDISI disi =
          new IndexedDISI(
              data,
              entry.docsWithFieldOffset,
              entry.docsWithFieldLength,
              entry.jumpTableEntryCount,
              entry.denseRankPower,
              entry.numValues);
      if (entry.bitsPerValue == 0) { // 所有值都一样
        return new SparseNumericDocValues(disi) {
          @Override
          public long longValue() throws IOException {
            return entry.minValue;
          }
        };
      } else {
        final RandomAccessInput slice =
            data.randomAccessSlice(entry.valuesOffset, entry.valuesLength);
        if (entry.blockShift >= 0) { // multi-blocks-compressed
          return new SparseNumericDocValues(disi) {
            final VaryingBPVReader vBPVReader = new VaryingBPVReader(entry, slice);

            @Override
            public long longValue() throws IOException {
              final int index = disi.index();
              return vBPVReader.getLongValue(index);
            }
          };
        } else { // single-block-compressed
          final LongValues values =
              getDirectReaderInstance(slice, entry.bitsPerValue, 0L, entry.numValues);
          if (entry.table != null) { // table-compressed
            final long[] table = entry.table;
            return new SparseNumericDocValues(disi) {
              @Override
              public long longValue() throws IOException {
                return table[(int) values.get(disi.index())];
              }
            };
          } else if (entry.gcd == 1 && entry.minValue == 0) {
            return new SparseNumericDocValues(disi) {
              @Override
              public long longValue() throws IOException {
                return values.get(disi.index());
              }
            };
          } else {
            final long mul = entry.gcd;
            final long delta = entry.minValue;
            return new SparseNumericDocValues(disi) {
              @Override
              public long longValue() throws IOException {
                return mul * values.get(disi.index()) + delta;
              }
            };
          }
        }
      }
    }
  }