ElasticSearch学习篇8_Lucene数据落盘之数据压缩编码(vint、zigzag、writeTLong等)

前言

Lucene全文检索主要分为索引、搜索两个过程,对于索引过程就是将文档磁盘存储然后按照指定格式构建索引文件,其中涉及数据存储一些压缩编码、数据结构设计还是很巧妙的。

下面主要记录学习过程中的StoredField、DocValue、Lucene数据存储类型以及压缩编码方式。

参考:

目录

Lucene数据存储基础知识

  • Lucene数据分类
  • Lucene存储文件
  • Lucene数据存储
    • Stored Field
    • DocValue

一、Lucene数据存储基础知识

1、数据分类

在Lucene中索引数据存储的逻辑层次有多个层次,从大到小依次是

  • index:索引代表了一类数据的完整存储
  • segment: 一个索引可能有一个或者多个段构成
  • doc: segment中存储的是一篇一篇的文档doc,每个segment是一个doc的集合
  • field: 每个doc都有多个field构成,filed才包含了具体的文本,类似于一个json对象的一个属性
  • term: 每个field的值可以进行分词,进而得到多个term,term是最基本的单元,每个field可以保存自己的词向量,用来计算搜索相似度

按照数据的维度整个Lucene把需要处理的数据分为这么几类

  1. PostingList, 倒排表,也就是term->[doc1, doc3, doc5]这种倒排索引数据
  2. BlockTree, 从term和PostingList的映射关系,这种映射一般都用FST这种数据结构来表示,这种数据结构其实是一种树形结构,类似于Tire树,所以Lucene这里就叫BlockTree, 其实我更习惯叫它TermDict。
  3. StoredField一般类型的field原始数据存储
  4. DocValue 键值数据,这种数据主要用于数值、日期类型的field,是用来加速对字段的排序、筛选的,列式存储。
  5. **TermVector **词向量信息,主要记一个不同term的全局出现频率等信息,用于score,如搜索的str会被分为一个个term,然后会被转为指定维度的向量,存储文档维护索引会根据当前文档、所有文档中term出现的频率以得到一个当前term的权重创建一个对应的指定维度的向量,然后就计算查询相关性score。
  6. **Norms **用来存储Normalisation信息, 比如给某些field加权之类的。
  7. PointValue 用来加速 range Query的信息。

一个段索引维护的数据,Lucene9_9_0版本https://lucene.apache.org/core/9_9_0/core/org/apache/lucene/codecs/lucene99/package-summary.html#package.description

  • Segment info. This contains metadata about a segment, such as the number of documents, what files it uses, and information about how the segment is sorted。其中包含有关片段的元数据,例如文档数量、它使用的文件以及有关片段排序方式的信息
  • Field names. This contains metadata about the set of named fields used in the index.包含文档fields的元数据以及名称。
  • Stored Field values. This contains, for each document, a list of attribute-value pairs, where the attributes are field names. These are used to store auxiliary information about the document, such as its title, url, or an identifier to access a database. The set of stored fields are what is returned for each hit when searching. This is keyed by document number.以文档ID作为key,存储当前文档的fields键值对。
  • Term dictionary. A dictionary containing all of the terms used in all of the indexed fields of all of the documents. The dictionary also contains the number of documents which contain the term, and pointers to the term’s frequency and proximity data.包含所有文档的所有索引字段中使用的所有term的字典。该词典还包含包含该term的文档数量,以及指向该术语的频率和邻近数据的指针。
  • Term Frequency data. For each term in the dictionary, the numbers of all the documents that contain that term, and the frequency of the term in that document, unless frequencies are omitted (IndexOptions.DOCS)。term在当前文档出现的频率以及在全部文档出现的频率,主要用于score得分,比如term在当前文档出现的频率最高,在所有文档出现的频率最低,那么搜索该term在该文档中搜索得分高。
  • Term Proximity data. For each term in the dictionary, the positions that the term occurs in each document. Note that this will not exist if all fields in all documents omit position data。term出现在所有文档的位置,可省略。
  • Normalization factors. For each field in each document, a value is stored that is multiplied into the score for hits on that field.计算相关性score的时候可为某些field字段乘以一个系数。
  • Term Vectors. For each field in each document, the term vector (sometimes called document vector) may be stored. A term vector consists of term text and term frequency. To add Term Vectors to your index see the Field constructors。每一个文档的每一个field会有一个term向量,主要根据term出现的频率计算出来,用于搜索的score分值计算。
  • Per-document values. Like stored values, these are also keyed by document number, but are generally intended to be loaded into main memory for fast access. Whereas stored values are generally intended for summary results from searches, per-document values are useful for things like scoring factors.类似StoreField,可以更快加载到内存访问,用于搜索的摘要结果,但是每个文档的值对于评分因素有很大的影响。
  • Live documents. An optional file indicating which documents are live.一个可选文件,指定哪些文档是实时的。主要用于段数据删除时候,在段外部维护一个状态记录段的最新状态。
  • Point values. Optional pair of files, recording dimensionally indexed fields, to enable fast numeric range filtering and large numeric values like BigInteger and BigDecimal (1D) and geographic shape intersection (2D, 3D).可选的一对文件,记录维度索引字段,以启用快速数值范围过滤和大数值,例如 BigInteger 和 BigDecimal (1D) 以及地理形状交集(2D、3D)。
  • Vector values. The vector format stores numeric vectors in a format optimized for random access and computation, supporting high-dimensional nearest-neighbor search.

按照数据存储的方向维度可以分为

  • 一般存储形式:按层次保存了从索引,一直到词的包含关系:索引(Index) –> 段(segment) –> 文档 (Document) –> 域(Field) –> 词(Term) ,层次结构,则每个层次都保存了本层次的信息以及下一层次的元信息。如StoredFileld、DocValue存储形式。
  • 反向存储形式:如倒排索引(PostingList + BlockTree)数据存储形式。

2、数据存储方式

In Lucene, fields may be stored, in which case their text is stored in the index literally, in a non-inverted manner. Fields that are inverted are called indexed. A field may be both stored and indexed.
也就是说Lucene需要存储数据以及为存储数据建立索引,这两种之间是独立的,下面主要分析存储数据的相关设计,主要关注各数据类型是如何存储的? 最终写入索引是如何压缩的?
ps:学习分析Lucene版本为9_9_0

2.1、StoredField

写入Lucene的数据最终都会被转成字节流,然后写入内存,最终flush磁盘。
在CompressingStoredField中,含有一个对象 bufferedDocs,这个实际上就是最终落盘的字节流,它是一个GrowableByteArrayDataOutput, 可以理解为是一个C++里的vector, 支持自动扩容等,这个类继承自抽象类DataOutput ,同时在方法定义了写入各种底层基本类型的数据的细节。记住,这个阶段它不落盘, 只是在内存里面,需要到flush阶段才会落盘。
写入的数据是有格式的,Lucene的field数据类型有下面几大类

  • int:会进行 zigzag encode,就是输入一个int 类型的数值,先将其zigzag编码, 而后进行vint编码
  • long:会进行writeTLong编码,用于存储超大整数和时间dateTime,这里的T指的就是timestamp
  • Float:会进行writeZFloat编码,尝试精度压缩,能省空间就省,压缩不包含小数部分的float数据当作整数,但是对于负小数可能需要多损失一个区分bytes
  • Double:类似float,都是采用尝试精度压缩,区别float就是当发现可以精度可以压缩成float的时候就压缩float,可以压缩为整数的时候就压缩整数。
  • String:lucene使用utf-8编码,采用一个基本足够的bytes避免每次开辟空间,减少计算写入bytes的长度。
  • bytes:当数据是二进制,可以使用bytes,该类型是没有任何多余的压缩操作的,直接将内容追加到bytes数组后面即可,扩容的话策略是每次增长1/8,希望更多的CPU运算避免不必要的内存浪费。
vint编码

vint 中的v叫variant, 变长的,可变的, 也就是这种编码可以根据数值的大小来采用合适的字节数量来进行编码。
在Java中一个int占用4个字节,也就是32bit,假如需要存储20,转为二进制为 “10100”,也就是5位都能表示,前面的21位bit 白白浪费。
在Lucene API中,经常需要在文档中保存int整数,比如

Document doc = new Document();
doc.add(new StoredField("age",30));
...
indexWriter.addDocument(doc)

为了节省空间,可以采用vint编码进行空间压缩,规则就是:

  • 如果要编码的整数数值小于等于127(2的7次方),则使用单个字节来表示,并将最高位设置为0,剩余7位表示整数的二进制形式。
  • 如果要编码的整数数值大于127,则使用多个字节来表示。每个字节都将最高位(字节的最左bit)设置为1,除了最后一个字节外,其他所有字节的最低有效位都设为1。剩余的比特位用来表示整数的二进制形式。

举个例子

  • 编码整数20:20小于等于127,因此以单个字节表示,最高位为0,剩下的7位即为20的二进制形式,所以编码为0001 0100。
  • 编码整数150:150大于127,需要使用多个字节表示。首先将150转换为二进制形式1001 0110,然后从低位bit开始以7位为单位分组,第一次取得7位是001 0110,然后接着在前面添加1 表示还没完,还剩下一个1,接着再取7位数,000 0001,此时全部完毕,在前面添加0即可, 所最后结果为 1001 0110 0000 0001

Vint编码可以根据整数的大小动态地调整编码占用的字节数,从而实现对不同范围内的整数进行高效的压缩存储。

代码demo

public static void main(String[] args) {
    int number = 150;
    byte[] encodedBytes = encodeVInt(number);
    System.out.println("VInt encoded bytes: ");
    for (byte b : encodedBytes) {
        System.out.print(String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0') + " ");
    }
}

public static byte[] encodeVInt(int number) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 首先判断(number & ~0x7F)是否不等于0,即判断number的高位是否还有值需要编码。
// number & ~0x7F 是用来检查一个整数 number 的最高有效位是否为 1
// 0x7F 对应的是 01111111,所以取反后的结果为 10000000。
// 0x80 是一个十六进制数,对应的二进制表示为 10000000。
while ((number & ~0x7F) != 0) {
    // 在循环中,先将number的低7位与0x7F进行按位与操作,再与0x80进行按位或操作,然后将结果写入bos中。
    // 将number右移7位,继续下一轮循环,直到number的高位全部被编码完毕。
    bos.write((byte) ((number & 0x7F) | 0x80));
    number >>>= 7;
}
// 最后将number的最后一部分(低7位)直接写入bos中。
bos.write((byte) number);
return bos.toByteArray();
}
zigzag编码

vint的一个不足就是对负数仍会耗费大量字节,因为负数采用的是补码编码,比如-10的原码是 10000000 00000000 00000000 00001010,补码就是 11111111 11111111 11111111 11110101

经过vint编码11110101 11111111 11111111 11111111 011111111
符号位也需要vint压缩,也就是需要4个首字节1有效位和1个首字节0有效位,一共需要5个字节,不太合适。

因此需要zigzag编码,规则

  • 根据符号位生成一个Mask掩码,如果是正数,就全是0,如果是负数就全是1,位数和原位数相等。
  • 把数左移动一位,去掉最高位的符号位。
  • 将Mask和刚才移动的负数做异或操作。

举栗,如-10使用zint编码,首先-10的补码为 11111111 11111111 11111111 11110110`,生成的Mask为11111111 11111111 11111111 11111111,原码左移动一位去掉符号位 11111111 11111111 11111111 11101100,最后和Mask异或(位相异为1)结果为00000000 00000000 00000000 00010011,即十进制的19;后续可以再使用vint编码,结果为00010011。

实际业务里,有大量的数值都是用一个字节即可表示的小数值, 用zint 编码即可以保证减少字节数量,也可以适应偶发的大数字的情况, 代价只是多出一个flag标志位。google protobuf 也才用了这种压缩方式来压缩传输的数据。

lucene writeTLong编码

对于时间戳的long存储类型,lucene整个压缩编码将原数值分为两个部分header、body,
首先看header,按照时间精度header分别对应不同的值,其他情况不压缩body。

  • 精度为天,数值能被 10006060*24 整除
  • 精度为小时,数值能被 10006060 整除
  • 精度为妙,数值能被 1000 整除

这三种情况body为vlong编码,道理和vint类似,这三种的header部分总共8位

  • 高位的两位用作精度标识位,如11为天,10为小时,01为秒,00位其他。
  • 低位的6位正常zigzag、vlong使用。

举栗,如时间戳1616198400000使用writeTLong方法编码步骤:

  1. 确定精度,能被10006060*24整除,结果为18706,可以压缩编码,那么header的高两位为11,将18706转为二进制为 01001001 00010010
  2. 进行zigzag编码,mask为00000000 00000000,正数直接左移1位,10010010 00100100
  3. 将最后5位00100编码之后10100补充到header的低5位,加上之前高两位的11,因此header为 1101 0100
  4. 其他位进行vlong编码填充到body,1001 0001 00001001
  5. 最后的编码结果 为 11010100 10010001 00001001
lucene writeZFloat编码

对于浮点型的float存储类型,lucene压缩的思路是尝试降低精度,如果降低精度和原来精度一样,那么就压缩,比如假如输入的是一个小数值,小数位为0,那么就可以当作是个整数int来编码,如果是真的小数,那么还得多浪费一个byte。

因此lucene做为通用的搜索引擎,只是一般的场景,如果想要设计一个更符合业务方需求的搜索引擎,需要考虑数据进行选择合适的压缩方案。

首先看IEEE标准的的float编码规则,在java中float共4字节32位,第一位是符号位,后面8位是整数位,最后23位表哦是尾数部分,举个例子12.25f
image.png

  • 首先将其转为二进制,整数部分:1100,小数部分:01(也就是2的-2次方),所以转为二进制为1100.01
  • 然后转为科学计数法,也就是底数部分是1.10001,指数部分是3(小数点向左移动了3位),转为科学计数法之后的底数恒为1可以不参与编码。
  • 考虑符号位:现在是正数,符号位为0,指数为3,但是像0.000101的指数为为-4,对于指数位为了避免-0,+0的出现,当指数为负数的时候,需要+127归一成正数[0,255]。因此指数3+127=130,指数部分二进制为10000010,尾数部分由于底数1不参与编码,所以只编码小数点后面的部分

因此最终的编码结果0 1000 0010 1000 1000 0000 0000 0000 000

对于lucene的writeZFloat规则,比如12.25f将小数转为整数12,判断原float类型的值是否和这个int类型的值相同

  • 相同并且并且该数值在[-1,125]之间,可以压缩采用一个字节编码:规则就是将高位设置为1,然后其他位将该int值转为二进制然后加1,比如12的二进制为 0000 1100,压缩编码后为 1000 1101
  • 不相同,但是是一个正数,直接按照IEEE编码规则。
  • 不相同,但是是一个负数,为了区分第一种情况编码,需要补充一整个额外的byte来表示这种情况。例如-12.15f这种情况,11111111 1 10000010 1000100 00000000 00000000,因为如果不加的话,可能和第一种情况无法区分,加上之后,即使第一种情况最大为11111110(125的二进制为01111101)。
lucene writeString编码

在C++内,一个char占用一个字节,采用的是utf-8编码,一个字母、数字只占一个字节,一个中文汉字占三个字节。
在Java内,一个char占用两个字节,因为采用的是utf-16编码,每个字母、数字、汉字等字符采用两个字节表示。(少数四个字节)
image.png

对于存储英文,还是选择utf-8更省空间,因此lucene采用utf-8,但是java中默认使用的utf-16,因此需要转化为utf-8在写入。因此开辟的bytes数组大小就要考虑一下,过大浪费,过小需要扩容copy,开销挺大的,如果每次都准确计算,又停耗费CPU的。

lucene采用的策略是

  1. 首先开辟一个足够大的三个字节的数组scratchBytes,作为临时存放utf-8,开辟一次重复使用,后续从这个bytes直接放到目标bytes中,最后落盘的就是一串bytes。
  2. 判断是否够用,如果这个字符比较小转为utf-8不超过2^16=65535,那就放在scratchBytes,可以避免计算正式的长度开辟空间,然后写入目标bytes,同时使用vint编码长度一并写入,防止找不到边界;如果这个字节比较大转为utf-8超过了65535,那么就需要计算实际需要多少bytes了,进而完成目标数组的扩容保证足够能写下。

image.png

public void writeString(String string) throws IOException {
    // 计算最大需要的长度,其实就是string.length()*3
    int maxLen = UnicodeUtil.maxUTF8Length(string.length());
    if (maxLen <= MIN_UTF8_SIZE_TO_ENABLE_DOUBLE_PASS_ENCODING)  {
        // 这个字符串足够小,因此我们为了避免两次运算而不需要内存
        // string is small enough that we don't need to save memory by falling back to double-pass approach
        // this is just an optimized writeString() that re-uses scratchBytes.
        // 建一个scratchBytes,避免每次都新开内存。
        if (scratchBytes == null) {
            scratchBytes = new byte[ArrayUtil.oversize(maxLen, Character.BYTES)];
        } else {
            scratchBytes = ArrayUtil.grow(scratchBytes, maxLen);
        }
        int len = UnicodeUtil.UTF16toUTF8(string, 0, string.length(), scratchBytes);
        writeVInt(len);
        writeBytes(scratchBytes, len);
    } else  {
        // use a double pass approach to avoid allocating a large intermediate buffer for string encoding
        int numBytes = UnicodeUtil.calcUTF16toUTF8Length(string, 0, string.length());
        writeVInt(numBytes);
        bytes = ArrayUtil.grow(bytes, length + numBytes);
        // 注意一下,这边这个length是一个私有成员变量,是指的当前bytes数组已使用的所有数量, bytes.length是开辟的bytes数组的长度,其中有部分还没使用。
        length = UnicodeUtil.UTF16toUTF8(string, 0, string.length(), bytes, length);
    }
}

元数据

最后落盘的是一个bytes数组,查数据的时候如何把数据取出来就需要靠元数据了,需要告诉哪些部分是field1,哪些部分是field2,每个field的类型是什么。
元数据总共8bytes,格式为:3位的field类型、61位的field序号,然后在使用vlong编码,每个meta info对应一个filed的元信息,后面跟了一个value表示field的value。

image.png
1、field类型的意思就是该字段的数据类型
image.png
2、field序号的意思是这是第几个field,如总共三个,那么编号为0、1、2,那么正常的元数据只用最后一个bytes即够了

2.2、DocValue

行式存储是按照一个个doc来落盘,列式存储则是按照一个个field来落盘,主要是考虑性能和存储成本,

  • 性能:行式存储的优势是根据docId可以查询全部的field数据,仅需要访问一次磁盘,如果需要针对field排序、筛选、求和等,如果是行式存储则需要读全部的磁盘,列式的化只需要找到field聚集的区域,大幅减少读取磁盘的时间。
  • 存储成本:比如某个字段fieldc是score,只用于内部打分、统计、筛选、排序等,不会用于呈现,比如34,36,24,26,可以采用差值压缩的方式编码。

在lucene中有5类DocValue,分别是SortedDocValues, SortedSetDocValues, NumericDocValues, SortedNumericDocValues, BinaryDocValues。

  • NumericDocValues是针对数值类型进行的存储。
  • SortedDocValue是针对字节类型来进行的存储。
  • SortedSetDocValue 和SortedNumericDocValues 相比SortedDocValue 和NumericDocValues 而言可以让一个field具有多个值。例如,一个文档可能有一个字段表示发布日期,而这个字段可能包含多个日期值,这些值需要按时间顺序排列。

对于sorted的理解,"sorted"则是指字段值在存储前会被按照字典顺序排序。这种排序不是对文档进行排序,而是对字段值进行排序,并且每个唯一的字段值会被赋予一个唯一的数字标识符(Term ID)。这样,当存储文档时,字段值就会被转换成对应的数字标识符。这种方法的好处是可以有效地压缩存储空间,因为对于重复的字段值,只需要存储一次值和对应的标识符,而在文档中只需要存储标识符即可。

比如对SortedDocValue来说,doc1的fieldC=apple,doc2的fieldC=orange,doc3的fieldC=add,那么在排序并赋予标识符后,"add"可能会得到标识符0,"apple"得到标识符1,"banana"得到标识符2。因此,这些文档的该字段值会分别存储为1、2、0。lucene有没有提供value->docid 类似于mysql索引的机制

这种排序和编码机制有几个好处:

  • 压缩存储:通过仅存储唯一值及其标识符,可以减少存储空间的需求。
  • 快速查找:对值进行排序和编码可以加快搜索和过滤操作,因为可以直接通过标识符进行比较,而不是字符串比较。
  • 范围查询优化:对于范围查询(如查找所有以"a"开头的文档),通过排序的标识符可以更快地定位到相关的文档。
NumericDocValues

当fieldValue类型为数值型的时候。

落盘方式:存储任何类型的数据,都至少会包含两种文件,一种是meta文件,一种是data文件,对于docValue来说,meta文件后缀是.dvm,value后缀是.dvd,一般是通过.dvm获取元信息再去.dvd还原出原始信息。

压缩算法

// TODO

2.3、倒排存储

3、数据落盘

落盘时机

在落盘时,分为两种落盘文件,.fdt记录数据文件,.fdx是.fdt的索引
几个概念, doc是field的集合, chunk是doc的集合, block是chunk的集合。 doc到chunk的阈值有两个,一个是达到128个doc数量,第二个是bufferedDocs达到16384长度;chunk到block的阈值是1024。

落盘也就是所说的flush,对于StoredField有两种落盘的时机

  • 处理完document执行finishDocument的时候,检查doucument文档数量是否超过了28篇,或者内存中的bufferedDocs长度超过16384=16k的时候,会发生落盘。
  • 当关闭indexWriter的时候,必须要commit,然后落盘。

落盘文件格式与压缩算法

对于Lucene7版本的StoredField来说,落盘文件有.fdt和.fdx,另外还有其他的一些落盘文件的格式和概念。
一个索引相关的存储文件对应一个文件夹,一个段的所有文件都具有相同的名称和不同的扩展名。扩展名对应于下面描述的不同文件格式。当使用复合文件格式时(小段的默认格式),这些文件(段信息文件、锁定文件和文件夹文档文件除外)将折叠为单个.cfs文件。

  • Segments info:多个段文件名永远不会重复使用。也就是说,当任何文件保存到目录时, 以前从未使用过的文件名。这是使用简单的生成方法实现的。比如说, 第一个段文件是segments_1,然后是segments_2,依此类推。生成是连续的长 以字母数字(以36为基数)形式表示的整数。主要保存段的元信息,segments_N 保存了此索引包含多少个段,每个段包含多少篇文档,实际的数据信息保存在field和词中的。
  • Write.lock:写锁默认存储在索引目录中,名为“write.lock”。如果锁目录与索引目录不同,则写锁将被命名为“XXXX-write.lock”,其中“”是从索引目录的完整路径导出的唯一前缀。如果存在此文件,则表示编写者正在修改索引(添加或删除文档)。这个锁文件确保一次只有一个writer修改索引。
  • Fields、Field Index 、Field Data:This is keyed by document number.也就是上面说的一般存储形式,保存了此段包含了多少个field,每个field的名称及索引方式以及数据
  • Term Vector Index、Term Vector Data:当你将字段设置为存储Term Vector时,Lucene会提取出该字段中每个词项的相关信息,并将其存储到倒排索引中。这样可以在搜索时不仅找到包含关键词的文档,还能得知每个关键词在文档中的频率和位置。因为不仅要根据倒排索引找到文档ID,还需要计算文档的相关性得分,会存储当前文档全部term的频率、位置信息,为了下一步也就是根据文档内全部的term的频率信息计算下面的vector value。
  • Vector values:根据每个文档的所有term vector data数据,为每个文档计算出一个指定的相关性vector values,然后在跟query vevtor计算相关性score。

  • chunk :是指一堆文档的集合, 每128个doc会形成一个chunk, 或者存储的实体数据超过16k也会形成一个chunk。
  • block :又是1024个chunk所组成一个集合。
  • slice :也是指一批文档的集合
fdt数据文件

分为四个部分

  • header数据:魔数,标识等
  • chunk元数据:chunks的数量和PackedIntVersion压缩版本
  • chunk数据,系列doc元数据以及field-value数据
  • footer数据:魔数,CRC校验标识

header数据image.png
chunk元数据
image.png
chunk数据,也就是具体的doc数据
image.png

footer数据
image.png

数组压缩PackInt算法

chunk存储的DocFieldCount和DocLength都是int数组,分别表示当前chunk包含每个doc的field数量和doc长度,lucence也是采用压缩算法,压缩策略

  • 如果数组元素值都是一样的,首先标志位写为0,再写入这个数组的第一个数值即可,比如写DocFieldCount的时候,因为schema是固定的,一般doc的field数量也是固定的不变的。
  • 如果数组的数值不一样,会使哟哦那个PackInt压缩,该算法的思路是在存储int元素的时候,首先计算最大值需要的bytes,然后为其他元素都开辟同样的空间,最后pack起来。该算法一般需要lucene分块一块使用,比如一个chunk基本上doc length为400bytes,只有一个doc length为10000bytes,这个时候就要分块,否则使用PackInt很浪费空间。
数据无损压缩LZ4算法

对编码之后的doc数据进行二次压缩
参考:https://lz4.org/

fdx索引文件

类似fdt文件,本质上是记录指向fdt文件chunks(docs)的指针,结构如下
image.png

数组差值压缩编码

前面fdt文件的DocFieldCount、DocCount两个字段采用PackedInt算法,对于docBaseDeltas和startPointerDeltasl是两个bytes,lucene采用另一种差值压缩算法。
算法描述,比如有数组[128,76,75,102,100,120]

  • 首先计算平均值,四舍五入计算出来是100
  • 计算一个delta差值数组,将原数组转为delta数组,规则如下
base = 0
delta_array = []
for i in range(len(array)):
    delta = base - avg * i
    delta_array.append(delta)
    base += array[i]


// 思考为什么不用 (array[i] - avg) for i in range(len(array))
  • 算下来delta数组如下[0, 28, 4, -21, -19, -19]
  • 对于这个delta数组,采用PackInt压缩,对于负数需要在压缩之前使用zigzag编一下码

思考:为什么在对.fdt文件里的DocFieldCount以及DocLength进行编码时没有采用类似的delta编码,而是直接使用packedInt编码的原因,可能包括:

  1. 数据特性:DocFieldCount和DocLength的值可能不适合使用delta编码。例如,如果这些值的变化非常大,使用delta编码可能不会节省空间,反而可能增加存储成本。
  2. 编解码效率:packedInt编码是一种高效的编码方式,特别是对于小整数值。它可以在保持较高压缩率的同时,提供较快的编解码速度。如果DocFieldCount和DocLength的值通常较小,使用packedInt编码可能更有效率。
  3. 实现复杂度:使用packedInt编码可能比实现一个高效的delta编码算法要简单。在实际应用中,开发者需要权衡编码效率、压缩率和实现复杂度,选择最适合当前需求的编码方案。

总之,选择特定的编码方案需要考虑数据的特性、处理性能、内存使用以及实现的复杂度等多个因素。不同的场景和需求可能导致不同的选择。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

scl、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值