Vespa 详解及对比 Elasticsearch
Table of Contents
在这家公司做了两年 Vespa 相关开发后,当被问到 Vespa 相比 Elasticsearch 的优势时,我居然很茫然。于是又重新好好地梳理了下 Vespa 的逻辑,参照之前了解的 ES(Elasticsearch)的一些概念,记录下这篇文章来总结这两者的区别。
真正的实时更新
在 lucene 中,一个 index 由多个 segments 组成。1 新的文档会先写入内存中的 buffer,一定时间后,写入 filesystem cache 后再 flush 至磁盘。新添加的文档是不可被搜的,直至写入 filesystem cache。有检索请求时,内存中文档 flush 至文件的频率默认为 1s。 2
Vespa 有两种索引模式,一种为 index,适合文本检索,支持 tokenizer / stemming / normalization,与 ES keyword 类型类似的会构建倒排索引;另一种为 attribute,是全内存的列存索引结构,以 local docId 为序存于 RCU Vector 中,打开某项配置后会在内存中构建倒排。
除了 index 和 attribute 两种列存索引,Vespa 还有类似于 ES 中 stored fields 的行存正排:document store。文档被顺序写入磁盘中,随着文件大小的增长分割为多份(默认不大于 1G)。
Vespa 的 index 由一份 memory index 和一份以上 immutable disk index 组成,文档更新会立刻作用于 memory index。Memory index 会不定期 flush 至磁盘,随后和 primary disk index 合并(称为 fusion)形成新的 disk index。在搜索时,memory index 和 disk index 都会被检索。3
由此引出一个问题,Lucene 存储的 segments 的数量可能达到上百,而 Vespa 却往往只有一两份 disk index,是否 Vespa 的 fusion 开销会过大以至于影响性能?答案是否定的4:
- Vespa 可以控制 feeding 使用到的线程比例,减少对 searching 的影响;
- Lucene segments 为行存,而 Vespa 是列存的,每个 field 都有单独的 memory/disk index;
- Lucene 合并 segments 重写了大量的数据,而 Vespa 中由于倒排和词典都是已排序的,几乎都是顺序读写,开销相对低很多(TODO:这里以后再具体展开);
- 文档更新总是先写入内存,减少了垃圾回收和文档合并的频率;
- 过多的 Segments 使得同一个文档要经历更多的合并次数,增加 IO 读写。
反过来讲,较少的 index 数量带来比 Lucene 更高的查询效率。
动态扩缩容
ES 不能非常方便地扩缩容,需要人为参与到数据迁移的过程中,才能增加 shard。56
Vespa 使用与 Key-Value 存储类似的机制,文档被 hash 至 buckets 进行自动的管理,不需要手动控制分片。当增加 node/shard 时,会重新计算 bucket 的分布,存储节点间自动迁移文档至新的 node;而当 node 挂掉或在配置文件中被删除时,也会重新分配 bucket,文档会自动地从其它节点的备份中恢复,再复制出满足最少 replica 的备份。7
相应的为了实现这样的功能,Vespa 的 bucket 操作的代码逻辑复杂度是相对比较高的,需要为此付出一些维护代价。
Partial Update
Lucene 不存在真正的 partial update,segments 均是 immutable。8 更新文档时,将已有 segment 中该文档处标记为已删除,之后的流程即是增加一个新的文档。9 注意到当更新文档量频率较高时,该实现会在一定程度上影响检索性能。
Vespa 的三种存储形态中,index,attribute 为内存中更新,定期 flush 至内存。document store 不涉及倒排检索,需要顺序写入更新后文档的全部字段。
稀疏向量性能
Lucene 的稀疏向量(dense vector)和其余类型一样,分布存在多个 segments 中,自然地,为此每次合并 segments 都需要重新计算 HNSW 图,而每次搜索也需要在多个 segments 上计算 HNSW 后再合并,带来了额外的开销。10
稀疏向量(tensor)在 Vespa 中是第一公民,在内存中生成 HNSW 图,存储全部文档的向量,11 在性能上显然比 ES 更快。12 但由于 ES 的向量存储在磁盘上,而 Vespa 只能全量放在内存中,不能支持大于内存的向量空间的查询。
多线程查询
Vespa 在设计上屏蔽了 matching 对下层多种索引结构的感知,抽象为了对本分片上 local docId 的“遍历”查询,于是可以天然地将文档 id 切割为多个段,用于多线程查询。
当然在搜索引擎的场景中并不是线程越多延迟和吞吐就会越好,不过对于特定的业务场景和规模,设置数个线程并发查询能带来明显的改善。
大模型 Ranking
Vespa 支持众多大模型,如 Tensorflow,ONNX 等;还支持 Huggingface,Bert 等进行 Embedding。13 不过我们线上环境中没有使用。
TBC