Notes

2023-11-07

索引文件vec/vem/vex lucene9.8.0

lucene向量搜索相关的索引文件,主要索引文件类型为.vec/.vem/.vex结尾的文件,文件中包含的内容主要包括图的分层信息、每一层中的节点编号、向量值,相连的邻居节点等信息 业界流行的向量搜索基于论文
Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs

数据结构

.vec文件中存放的数据为所有的向量值vectorData,文档号信息DocIdData,以及节点编号与文档号之间的映射信息OrdToDocData 索引文件.vec

去重编码

去重编码是对int类型数据的一种压缩方式,在FacetsConfig类中应用此方法来处理int类型数据,其优点在于,存储一个原本需要4字节空间大小的int类型数据,最好的情况下只需要一个字节,最坏情况下需要5个字节 去重编码的过程:

  • 排序
  • 去重
  • 差值存储

去重编码最重要的一点是差值存储。在存储一组有序的数值时,除第一个数值外,其他的数值如果只存储跟它前面数值的差值,那么可以使得达到最大的压缩比。这种方式在存储大数值时的有点更明显。例如我们有一组数据:{17832,17842,17844},如果我们直接对3个数值进行存储(不存储差值),那么最终需要9个字节才能存储这三个数值,而如果我们进行差值存储,那么我们需要存储的数据就变为: {17832,10,2},其中10是17842跟17832的差值,2是17844跟17842的差值,那么最终只需要5个字节存储即可。

1个字节 数值范围 0~2^7-1 0~127 2个字节 数值范围 2^7~2^14-1 128~16383 3个字节 数值范围2^14~2^21-1 16384~2097151 4个字节 数值范围2^21~2^28-1 2097152~268435455 5个字节 数值范围2^28~* 268435456~*

package lucene.compress.dedupAndEncodeTest;

import org.apache.lucene.util.BytesRef;

import java.util.Arrays;

public class ConvertIntToByteRef {

  public static BytesRef dedupAndEncode(int[] ordinals) {
    // 对 ordinal排序,为了下面存储差值
    Arrays.sort(ordinals, 0, ordinals.length);
    // 先给每一个int类型分配5个字节大小的空间, 每个字节中只有7位是有效字节(描述数值),最高位是个定界符, 所以一个int类型最多要5个字节
    byte[] bytes = new byte[5*ordinals.length];
    int lastOrd = -1;
    int upto = 0;
    // 遍历处理每一个int类型
    for(int i=0;i<ordinals.length;i++) {
      int ord = ordinals[i];
      // ord could be == lastOrd, so we must dedup:
      // 去重操作
      if (ord > lastOrd) {
        int delta;
        if (lastOrd == -1) {
          // 处理第一个值, 只能储存原始的数值
          delta = ord;
        } else {
          // 处理非第一个值,就可以储存这个值与前一个值的差值
          delta = ord - lastOrd;
        }
        // 当前数值在0~127范围内
        if ((delta & ~0x7F) == 0) {
          // 注意的是第8位是0(位数从1开始), 是个定界符, 表示下一个byte字节是另一个int的一部分
          bytes[upto] = (byte) delta;
          upto++;
        } else if ((delta & ~0x3FFF) == 0) {
          // 这个字节的最高位是1, 表示下一个byte字节和当前字节属于同一个int类型的一部分
          bytes[upto] = (byte) (0x80 | ((delta & 0x3F80) >> 7));
          // 每次存储7位
          bytes[upto + 1] = (byte) (delta & 0x7F);
          upto += 2;
        } else if ((delta & ~0x1FFFFF) == 0) {
          bytes[upto] = (byte) (0x80 | ((delta & 0x1FC000) >> 14));
          bytes[upto + 1] = (byte) (0x80 | ((delta & 0x3F80) >> 7));
          bytes[upto + 2] = (byte) (delta & 0x7F);
          upto += 3;
        } else if ((delta & ~0xFFFFFFF) == 0) {
          bytes[upto] = (byte) (0x80 | ((delta & 0xFE00000) >> 21));
          bytes[upto + 1] = (byte) (0x80 | ((delta & 0x1FC000) >> 14));
          bytes[upto + 2] = (byte) (0x80 | ((delta & 0x3F80) >> 7));
          bytes[upto + 3] = (byte) (delta & 0x7F);
          upto += 4;
        } else {
          bytes[upto] = (byte) (0x80 | ((delta & 0xF0000000) >> 28));
          bytes[upto + 1] = (byte) (0x80 | ((delta & 0xFE00000) >> 21));
          bytes[upto + 2] = (byte) (0x80 | ((delta & 0x1FC000) >> 14));
          bytes[upto + 3] = (byte) (0x80 | ((delta & 0x3F80) >> 7));
          bytes[upto + 4] = (byte) (delta & 0x7F);
          upto += 5;
        }
        // 这里将ord保存下来是为了去重
        lastOrd = ord;
      }
    }
    return new BytesRef(bytes, 0, upto);
  }

  public static void main(String[] args) {
    int[] array = {3, 2, 2, 8, 12};
    BytesRef ref = ConvertIntToByteRef.dedupAndEncode(array);
    System.out.println(ref.toString());
  }
}

解压

package lucene.compress.dedupAndEncodeTest;

import org.apache.lucene.util.BytesRef;
public class ConvertByteRefToInt {

    public static void decode(BytesRef bytesRef){
        byte[] bytes = bytesRef.bytes;
        int end = bytesRef.offset + bytesRef.length;
        int ord = 0;
        int offset = bytesRef.offset;
        int prev = 0;
        while (offset < end) {
            byte b = bytes[offset++];
            // if语句为真:decode结束,用ord表示
            if (b >= 0) {
                // ord的值为差值,所以(真实值 = 差值 + 前面一个值)
                prev = ord = ((ord << 7) | b) + prev;
                System.out.println(ord);
                ord = 0;
                // decode没有结束,需要继续拼接
            } else {
                ord = (ord << 7) | (b & 0x7F);
            }
        }
    }

    public static void main(String[] args) {
        int[] array = {3, 2, 2, 8, 12};
        BytesRef ref = ConvertIntToByteRef.dedupAndEncode(array);
        ConvertByteRefToInt.decode(ref);
    }
}

SortedDocValues

SortedDocValues同NumericDocValues、SortedNumericDocValues一样,在实际应用中最多的场景用于提供给搜索结果一个排序规则,在使用SortedDocValues之后,在.dvd .dvm文件中的索引结构, facet join group等功能,==>根据.dvd dvm中的索引数据进行查询的过程

前置

TermID

在创建索引阶段,会根据IndexWriter中添加document的顺序,有序地处理每一个document中的SortedDocValuesField,并且对每一个SortedDocValuesField的域值赋予一个从0开始递增的itemID,相同的域值具有相同的itemID

        String groupField = "superStart";
        // 0
        Document doc = new Document();
        doc.add(new SortedDocValuesField(groupField, new BytesRef("mop")));
        indexWriter.addDocument(doc);

        // 1
        doc = new Document();
        doc.add(new SortedDocValuesField(groupField, new BytesRef("moth")));
        indexWriter.addDocument(doc);

        // 2
        doc = new Document();
        doc.add(new SortedDocValuesField(groupField, new BytesRef("of")));
        indexWriter.addDocument(doc);

        // 3
        doc = new Document();
        doc.add(new SortedDocValuesField(groupField, new BytesRef("star")));
        indexWriter.addDocument(doc);
        indexWriter.commit();

会生成如下的new BytesRef与itemID之间的对应关系 |域值|aa|cc|bb|ff| |—-|—-|—-|—-|—-| |itemID|0|3|2|1|

currentValues[]数组

currentValues[]数组中,下标值为文档号docId,数组元素为ItemId,在索引阶段,由于处理的数据是按照IndexWriter中添加document的顺序进行的,即第一篇文档的document,文档号docId为0,文档号递增的顺序,所以在这个过程中,就可以通过数组方式记录,文档号docId跟ItemId之间的映射关系

currentvalues[] |ItemId|0|1|2|3|3| |–|–|–|–|–|–| |docId|0|1|2|3|4|

sortedValues[]数组 ord

sortedValues[]数组中的数组元素是ItemID,数组下标是ord值,数组元素是有序的,但是排序规则不是根据ItemId的值,而是根据termId对应的域值的字典序

ordMap[]数组

sortedValues[]数组中实现了 数组下标ord 到 数组元素termId的映射,而ordMap[]数组则是实现了 数组下标termId 到 数组元素 ord的映射。

数据结构

DocValues 在搜索引擎中,通常都是对域名field构建倒排索引inverted index 实现域值values到文档的映射,而DocValues则是构建一个正向索引,实现文档到域值的映射, DocValues目前主要有五种类型: Sorted_set Sorted_Numeric Numeric Sorted Binary

.dvd

BytesRefHash

BytesRefHash类是专门为BytesRef对象做优化的一种类似hashMap数据结构,该类的主要作用就是将所有的BytesRef存储到一个连续的存储空间,并且使得能在查询阶段达到O(1)的时间复杂度 BytesRefHash属性 byte[][]buffers: 二维数组buffers[][]用来存储bytesRef对象,所有的bytesRef都连续存储在byte[][] buffers中

int itemID:是从0开始的递增的值,每个bytesRef根据他存储到buffers的位置获得一个唯一的itemId

int[] ids: ids数组下标 是bytesRef按照murmurhash计算出的hash值,ids[]数组元素则是itemId值

int[] bytesStart:bytesStart数组下标是itemID,数组元素是itemID对应的BytesRef值在buffers[][]中的起始位置

** 代码块{} 优先 main 局部变量初始化**

BytesRef数组

压缩BulkOperationPacked

BulkOperation类的子类BulkOperationPacked 提供了很多对整数integers的压缩存储方法,其压缩过程就是对数据进行编码,将每一个整数long或者int编码为固定大小进行存储,大小取决于最大的那个值所需要的bit位数,优点是减少存储空间,并且对编码之后的数据能够提供随机访问的功能

十进制{1,1,1,0,2,2,0,0} 二进制表示为{01,01,01,00,10,10,0,0}存储需要4*8=32byte,数据中最大值为2,需要2bit即可表示,所以其他数据统一2bit表示,编码后需要的空间 2x8/8=2byte