lucene增量编解码与单调性

新吸收了个单词 Monotonic(单调的),MonotonicBlockPackedWriterMonotonicBlockPackedReader类主要用于对于单调递增的数据进行编解码。

举个例子,现在有一批数据100个,是单调递增的,首元素是Interger.MAX_VALUE-88888,后面每个元素比前一个元素大1-10之间,如果不进行压缩编码的话,需要100*4=400个字节。

如果编码的话,如果按照增量编码的话,基准元素需要4字节(最小元素,也是首元素), 100个源数据都用增量来表达,假设每个增量需要1字节来表达,那么总共需要4 + 100 * 1=104个字节来表达就可以了。

那么什么是增量呢?增量是相对谁而言的增量呢?源数据一定要严格单调递增吗?

编码原理

在这里插入图片描述
这里画了一张图,假设有a,b,c,d,e,f 6个源数据,它们并非严格地单调递增,但是总体趋势是单调向上递增的,起始元素a值(我把它叫做baseValue)很大,那么如何编码才能高效压缩呢?

lucene 是这样做的, 连接a,f两点构成一条直线,认为是这些源数据分布的拟合直线l1,平均斜率avg, 拟合直线上对应x的y值我们认为是预期值expectedVale, 但是真实数据actualValue,可能分布直线的上方或者下方, 我们期待我们构造的l1在所有源数据的上方。

如何做呢?平移, 遍历源数据每个元素,每当expectedValue > actualValue时, baseValue 就要-(expectedValue-actualValue),最终就有了拟合直线l2l2l1斜率一致,起始值不一致而已, 所有源数据都在l2的上方。剩下就只需要编码 baseValue ,avgdelta(expectedValue-actualValue)就可以了。

前言中提到的问题应该就全回答了,核心代码如下

//MonotonicBlockPackedWriter.java
protected void flush() throws IOException {
    assert off > 0;
    // 第一个元素到最后一个元素的斜率, 拟合直线l1
    final float avg = off == 1 ? 0f : (float) (values[off - 1] - values[0]) / (off - 1);
    long min = values[0];
    // adjust min so that all deltas will be positive
    // 调整,生成拟合直线l2, 使所有的delta都是正值
    for (int i = 1; i < off; ++i) {
      final long actual = values[i];
      final long expected = expected(min, avg, i);
      if (expected > actual) {
        min -= (expected - actual);
      }
    }

    long maxDelta = 0;
    for (int i = 0; i < off; ++i) {
      values[i] = values[i] - expected(min, avg, i);
      maxDelta = Math.max(maxDelta, values[i]);
    }
    // min是最小值,values[i]是斜线上方的真实数据点到拟合直线l2上点的差值
    out.writeZLong(min);
    out.writeInt(Float.floatToIntBits(avg));
    if (maxDelta == 0) {
      out.writeVInt(0);
    } else {
      // 根据最大差值,来确定差值使用几个bit位表示
      final int bitsRequired = PackedInts.bitsRequired(maxDelta);
      out.writeVInt(bitsRequired);
      writeValues(bitsRequired);
    }

    off = 0;
  }

解码

解码的核心代码在MonotonicBlockPackedReader类的构造函数上,比较简单,注释如下


  private MonotonicBlockPackedReader(IndexInput in, int packedIntsVersion, int blockSize, long valueCount, boolean direct) throws IOException {
    this.valueCount = valueCount;
    blockShift = checkBlockSize(blockSize, MIN_BLOCK_SIZE, MAX_BLOCK_SIZE);
    blockMask = blockSize - 1;
    // 写入时每blockSize个源数据就要flush一次, 每flush一次就形成一条拟合之前
    // 所以解码时,会有numBlocks条拟合曲线,斜率,基准值是不同的,所以下面要解码numBlocks次
    final int numBlocks = numBlocks(valueCount, blockSize);
    minValues = new long[numBlocks];
    averages = new float[numBlocks];
    subReaders = new PackedInts.Reader[numBlocks];
    long sumBPV = 0;
    for (int i = 0; i < numBlocks; ++i) {
      minValues[i] = in.readZLong();
      averages[i] = Float.intBitsToFloat(in.readInt());
      final int bitsPerValue = in.readVInt();
      sumBPV += bitsPerValue;
      if (bitsPerValue > 64) {
        throw new IOException("Corrupted");
      }
      if (bitsPerValue == 0) {
        subReaders[i] = new PackedInts.NullReader(blockSize);
      } else {
        final int size = (int) Math.min(blockSize, valueCount - (long) i * blockSize);
        if (direct) {
          final long pointer = in.getFilePointer();
          // subReader里记录着预期值与拟合值的差值,getDirectReaderNoHeader读取delta
          // 可以随机访问,是需要磁盘寻址的
          subReaders[i] = PackedInts.getDirectReaderNoHeader(in, PackedInts.Format.PACKED, packedIntsVersion, size, bitsPerValue);
          in.seek(pointer + PackedInts.Format.PACKED.byteCount(packedIntsVersion, size, bitsPerValue));
        } else {
          // getReaderNoHeader是一把就将所有delta读取到内存里了
          subReaders[i] = PackedInts.getReaderNoHeader(in, PackedInts.Format.PACKED, packedIntsVersion, size, bitsPerValue);
        }
      }
    }
    this.sumBPV = sumBPV;
  }

  // 获取源数据值
  @Override
  public long get(long index) {
    assert index >= 0 && index < valueCount;
    final int block = (int) (index >>> blockShift);
    final int idx = (int) (index & blockMask);
    // 拟合直线上的拟合值 + delta 
    return expected(minValues[block], averages[block], idx) + subReaders[block].get(idx);
  }

其他编解码(大值)

如果有一批源数据,不是单调递增,但是数据都很大,怎么编解码呢?
在这里插入图片描述
入上图所示,使用BlockPackedWriter,它以这批数据中最小的值作为baseValue,编码其他数据与baseValue 之间的差量delta,关键代码如下:

  protected void flush() throws IOException {
    assert off > 0;
    long min = Long.MAX_VALUE, max = Long.MIN_VALUE;
    for (int i = 0; i < off; ++i) {
      min = Math.min(values[i], min);
      max = Math.max(values[i], max);
    }

    final long delta = max - min;
    int bitsRequired = delta == 0 ? 0 : PackedInts.unsignedBitsRequired(delta);
    if (bitsRequired == 64) {
      // no need to delta-encode
      min = 0L;
    } else if (min > 0L) {
      // make min as small as possible so that writeVLong requires fewer bytes
      min = Math.max(0L, max - PackedInts.maxValue(bitsRequired));
    }
    final int token = (bitsRequired << BPV_SHIFT) | (min == 0 ? MIN_VALUE_EQUALS_0 : 0);
    out.writeByte((byte) token);
    if (min != 0) {
      writeVLong(out, zigZagEncode(min) - 1);
    }

    if (bitsRequired > 0) {
      if (min != 0) {
        for (int i = 0; i < off; ++i) {
          values[i] -= min;
        }
      }
      writeValues(bitsRequired);
    }
    off = 0;
  }

其实最开始的单调递增编码可以看作是大值编码的一种特例,认为是其特殊实现,其实lucene中也确实是这样处理的,继续看下面的章节

PackedLongValues

PackedLongValues将源数据(一批long型数据)进行压缩编码,提供了读写的方法,其压缩方式主要有两种,一种是大值编码,没有严格单调递增性质;一种是大值编码,且具有严格单调递增性质。编解码的原理上文已经提到,这里不再解释,附上一张类图。
在这里插入图片描述
1⃣️使用了Builder设计模式,LongValues的生成是依赖builder.build()模式产出。
2⃣️MonotonicLongValuesDeltaPackedLongValues的子类,其实很好理解,其本身就是父类的一个特殊实现。

相关

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值