信息检索-存储

编码与压缩:

下次可以买来看的书:

  • https://book.douban.com/subject/25789553/ 数据压缩导论(第4版)
  • https://book.douban.com/subject/35034359/ 数据压缩入门

压缩算法一般根据应用场景不同可分为文本压缩和索引压缩,后者是搜索引擎的核心技术之一。压缩的本质是对数据进行重新编码,编码依据是数据的分布特性,一般是概率分布情况,比如对于倒排索引中的数字编号。数值分布一般符合指数分布,均匀分布,Elias Gamma/Delta主要是基于这两种分布的。

压缩模型可分为全局模型,局部模型,局部模型参数可动态调整,算法复杂度高,压缩解压缩速度慢,但压缩率高。在编码方式上可分为定长和变长编码。

Elias Gamma/Elias Delta算法适合小整数使用频率较高的场景。它们都是基于数字分解的算法

  1. variable byte
  2. gamma
  3. zigzag

variable byte (在lucene中就是 VInt)

将一个数据拆分为多个编码单位存储, 读的时候读若干个连续的编码单元组成一个数据, 如用一个字节做编码单位, 这个字节的最高位为0表示需要继续读取数据, 另外7个bit表示真实的数据, 需要和之后读取的数据结合起来使用, 当读到的字节的最高位为1, 表示是最后一个编码单元了, 可以将之前读到的数据结合在一起. 在实际中, 也可能用1做延续位标识, 用0作结束标识.

编码单位越大, 操作次数越小, 即解压缩越快, 但是压缩率低. 以8位, 一个字节作为编码单位, 是压缩率与解压缩速度的权衡.
通过1-5个字节来压缩四个字节的数据类型, 如int, 对于数值较小的int具有较好的压缩率, 对于较大的int, 或者负数(最高位-1), 压缩率较低.
例子: 以 0 做延续位标识.

    static List encodeNumber(int num) {
//        byte i = (byte) 0x80;
        List bytes = new LinkedList<>();
        while (true) {
            Byte b = (byte) (num & 0x7F);  // 获取数据低位
            bytes.add(b);                 // 写入, 数据低位在索引低位
            if ((num >>>= 7) == 0) {
                break;
            }
        }
        int lastOne = bytes.size() - 1;
        Byte aByte = bytes.get(lastOne);
        Byte last = (byte) (aByte + 128);
        bytes.set(lastOne, last);
        return bytes;
    }

    static List encodeNumbers(int... nums) {
        List bytes = new LinkedList<>();
        for (int num : nums) {
            bytes.addAll(encodeNumber(num));
        }
        return bytes;
    }

    static int[] decodeBytes(List bytes) {
        List ints = new LinkedList<>();
        int v = 0;
        int count = 0;
        // 先获取的是数据低位
        for (Byte b : bytes) {
            var flag = b & 0x80; // 1000 0000
            var ans = b & 0x7F;
            ans <<= (count++)*7;
            v |= ans;
            if (flag != 0) {
                ints.add(v);
                count = 0;
                v = 0;
            } else {

            }
        }
        return ints.stream().mapToInt(Integer::intValue).toArray();
    }
lucene中的writeVInt 实现

DataOutput 是以1作为延续位标识

  public final void writeVInt(int i) throws IOException { // java 内存是小端吗? 如果把低位数据认为是低地址,那就是小端吧
    while ((i & ~0x7F) != 0) { // i &  1111...1111 1000 0000 , res != 0 表示: 如果除了低7位的其他位还有数据
      writeByte((byte) ((i & 0x7F) | 0x80)); // i & 0111 1111 , 取七位, | 1000 0000,  或1是为了设置连续位
      i >>>= 7;                              // 数据低位最先被写入
    } // 跳出循环,表示除了低七位的其他高位没有数据了,可以直接写入i了,i里面的第八位肯定是0
    writeByte((byte) i);                     // 对于负数也是支持的, 因为负数的标识-1在32位, 我们用了40位, 32的信息不会被扰乱
  }

long的写入也很类似

  private void writeSignedVLong(long i) throws IOException {
    while ((i & ~0x7FL) != 0L) {
      writeByte((byte) ((i & 0x7FL) | 0x80L));
      i >>>= 7;
    }
    writeByte((byte) i);
  }

gamma

首先了解一元编码:

主要适合于指数分布的数值分布,对于数字N,一元编码的编码长度为N,使用N-1个二进制1和末尾一个0表示,如数字3的一元编码为:110
gamma, elias gamma encoding: 参考:https://tonymazn.wordpress.com/2018/09/03/%E5%8E%8B%E7%BC%A9%E7%AE%97%E6%B3%95%E4%B9%8Belias-gamma-coding-elias-delta-coding/

  1. 对于数字x分解成 x=2N + M
  2. 对于N+1使用一元编码
  3. 对于M使用比特宽度为N的二进制编码

举例:比如对于数字13,可分解成:13 =23 + 5,即N=3,M=5,则N+1的一元编码为:1110;M的比特宽为3的二进制编码为:101,最后的Elias Gamma Coding为:1110:101.

而 elias Delta Coding是将Gamma coding的第一个因子再进行了一次Gamma coding。

zigzag

zigzag编码主要是为了应对 variable bytes的缺点: 对负数的压缩率不高的问题,因为负数采用的补码,比如-6
0000 0110 -> 补码 1111 1111 1111 1111 1111 1111 1111 1001 -> 加1 1111 1111 1111 1111 1111 1111 1111 1010
可见, 为了编码,负数,还要比int多用一个字节。 而zigzag编码则可以解决, zigzag本身很简单:

  1. a=原数向低位移动直到只剩下最后一位,高位用1补全
  2. b=原数向左移动1位,低位用0补全
  3. a和b进行异或操作: a^b
  public static int zigZagEncode(int i) {
    return (i >> 31) ^ (i << 1);
  }

  /**
   * Zig-zag encode
   * the provided long. Assuming the input is a signed long whose absolute value can be stored on
   * n bits, the returned value will be an unsigned long that can be stored on 
   * n+1 bits.
   */
  public static long zigZagEncode(long l) {
    return (l >> 63) ^ (l << 1);
  }

经过zigzag之后, 再用variable byte, 只需要一个字节就够了。

你可能感兴趣的:(信息检索-存储)