普通搜索 VS 向量搜索
向量搜索已经在成长了有些年头了,但是随着近几年机器学习和深度学习的蓬勃发展,“特别是万物皆可 embedding“的观点越来越流行之后,向量搜索才逐渐从小众的技术走入人们的视野之中。相较于普通搜索(基于词元和倒排索引),向量搜索会成为一个革命者代替它(们)的位置,还是会与它互补,并有机的整合在一起呢?
overview
首先,我们先来了解一下这两种搜索方案的特点以及各自的优缺点
普通搜索
以广泛被使用的 Lucene
、Elasticsearch
、Solr
,以及最近出来的一些类似 MeiliSearch
、Redisearch
等为代表,基于词元和倒排索引所构建的普通搜索,是建立在准确的搜索内容和检索语句上的,他们往往通过各种方式对文档进行分词(analyze
),通过诸如BKD tree
等数据结构,将拆解出来的词元(token
)进行倒排索引,在检索时也会对检索语句进行同样的分词处理,通过相同词元的匹配进行召回,再通过文本相关性的算法(如TF/IDF
、BM25
等)对结果进行打分排序,最终返回结果。
因此,他们大多具有以下的特点:
- 具有较高的索引速度
- 中等的索引大小
- 较高的查询速度(在大数据量的场景)
- 良好的缩放比例
- (对于精确匹配)具有完美的精度
- 精确且无损的词元和词组搜索
- 只能通过词元的精确匹配做召回
- 无法捕获语义与相似性
-
ES
的synonym
是类似在同一个位置把所有预先定义的同义词同时索引来实现的
向量搜索
如果你在搜索时不知道确切的query 词元
,或者你希望能对更广泛的同/近义词所指向的内容进行召回,可以考虑通过向量搜索来完成。目前市面上比较流行的向量搜索解决方案,无论是业界流行的 Faiss
、ScANN
库,还是工业级的开源解决方案Milvus
、Jina
,或者Elasticsearch
及其衍生品Elastiknn
、OpenDistro Elasticsearch KNN
,多少会通过 KNN
(K nearest neighbors
)对向量进行预聚类的方式进行存取加速。(参考的benchmark)
所以,他们大多会具有以下一些特点:
- 较慢的索引速度
- 较大的索引大小
- 较慢的查询速度(在大数据量的场景)
- 有限的缩放比例
- (对于精确匹配)具有较低的精度
- 较差的词元和词组的搜索能力
- 通过向量(某些解决方案中可以包含一部分标量字段)进行召回
- 对近似语义的捕获程度较高,可以很好的处理同/近义词
小结
通过普通搜索或者向量搜索构建个简单的搜索引擎系统并不难,但是随着数据量的增长、并发请求的增加、数据使用场景的变化,搜索引擎系统需要更多的组件一同完成其功能,如搜索前的数据预处理,到搜索过程中的query理解、改写、自动补全,缓存,分数计算,地理位置信息计算,到返回结果前的结果排序和过滤,结果分页等。
数据结构与搜索算法
之所以普通搜索和向量搜索会存在上面那些特点和差异,是因为他们构建数据的索引的数据结构以及召回算分的算法有很大差异,我们分别来看他们。
普通搜索
倒排索引
倒排索引是一个类似 hashmap
的数据结构,它的 key
是每个词元,而 value
是一个包含这个词元的所有文档的 id
列表(也可能是 hashset
、链表等结构),这样的数据结构的好处在于对于一个词元,可以用接近 O(1)
的代价来找到包含它的文章。有时倒排索引中也会包含词元在文档中的位置信息,这是为了能在搜索时,在考虑了 query
中的词元信息之外,也把词元的顺序也一并考虑进去。
LSM树
LSM 树(Log-Structured Merge-Tree),或称为日志结构合并树,被广泛运用于以 hbase
为代表的类数据库存储中,它的特点在于牺牲部分读的性能换取强大的写入性能,因为它作为一种基于硬盘的数据结构,可以明显的减少硬盘磁盘臂的开销,并能在较长的时间内提供文件的高速插入和删除。一般的倒排索引会构建在内存中,但随着数据量增加,我们可能需要通过磁盘来帮忙保存一部分数据,这就用到了 LSM树
,因为硬盘(无论 SSD
还是 HDD
都比 RAM
慢的好几个数量级),而 LSM树
可以在写数据的时候先把数据缓存在内存中,等积累一定量之后再通过归并排序的方式将数据追加到磁盘队尾,以提高写入速度。
带版本的数据提交
LSM树
只解决了数据插入的问题,搜索引擎中还会存在大量的更新操作,这就涉及到了随机读写了,我们知道随机读写会比顺序读写慢得多,特别是在 HDD
硬盘上的读写,这时就需要使用带版本的数据提交操作了。由上一节可知,数据写入时会先写内存中的缓冲区(ES
的translog
等)再通过定时提交的方式追加到磁盘中,在更新操作时也是一样的,不同的是搜索引擎往往会在内存中保留数据的指针,每次的更新(删除)操作作用在硬盘上也是追加操作,不同的是会将数据版本 +1,同时将指针指向新的地址,在召回时访问数据只访问最新版本的数据,同时定时对磁盘中的数据进行merge
操作,清理掉过期的旧版本的数据,从而释放磁盘空间。
升级和调优
存储:
- Size-tiered compaction
- Leveled compaction
- Sharded compaction
索引:
- zstd(Zstandard)压缩
- Elias-Fano 编码
- 停止词
- 词干
- ngram 索引
向量搜索
向量搜索就完全不一样了,由于他们索引的不是【词元 - 文档】的信息而是向量,所以他们会在索引构建的时候会尝试通过聚类而非倒排索引的方式构建。
市面上大部分的向量搜索引擎是靠 KNN
配合距离计算来进行存储的,差别可能会是距离计算公式以及存储结构的优化。常见的距离计算如:
- 欧式距离((euclidian distance)[https:///wiki/Euclidean_distance])
- 点积((dot product)[https:///wiki/Dot_product])
- cosine 相似度((cosine similarity)[https:///wiki/Dot_product])
索引数据
向量搜索的数据索引不同于普通搜索的分词,他们会需要先通过各种 machine learning
、deep learning
技术将文档、句子、词组等转化成向量存进搜索引擎,搜索引擎会根据配置使用距离计算模块对向量进行聚类保存。
常见的向量化(嵌入)的算法:
- Word2Vec
- GloVe
- fastText
- BERT
- Word level embeddings from BERT
- Sentence level embeddings from BERT
召回数据
向量搜索的召回和索引一样是基于向量距离的,从简单到复杂可以大致分为线性搜索、分级导航(Hierarchical Navigable Small Worlds (HNSW))、索引分块及聚类等
- 线性搜索
- 顾名思义,线性搜索就是将
query
向量和索引中所有的向量依次比较,再按距离排序 -
ES
对Dense Vector
字段的处理就是线性搜索 - 这是最简单也是最慢的方式,而且随着索引数据量的上升,召回时间也会随之大幅上升
- 分级导航
- 介绍 CN
- 介绍 EN
- 通过近似图遍历的方式找到更接近的向量进行距离计算
- 索引分块及聚类
- 是向量搜索中较为常用怼方式,通过预先配置的参数对向量进行
KNN
聚类的方式进行索引 - 召回时会通过寻找较近的核向量的方式来找到
topK
的向量进行 - 主要包含的一些方式:
- K中心聚类(k-medoids clustering)
- 围绕中心分区(Partitioning Around Medoids (PAM))
- Correlated-Sequential-Halving
- Voronoi iteration with Voronoi cells (Dirichlet tessellation)
- K中值聚类(k-medians clustering)
- Kmeans聚类(k-means/k-centroids clustering)
- Locality Sensitive Hashing
- Support Vector machines
升级和调优
其他一些可用的开源库
- NGT
- ANNOY
- RNSG
- ScaNN
- 更多
- ann-benchmarks
- awesome-vector-search
索引优化:
- 用zstd对文档进行压缩
- 向量优化(vector quantization (VQ))
- 主成分分析(Principal component analysis (PCA))
- 主要用于降维,把向量维度减少,通过损失部分精度来获取更小储存体积
- 乘积量化(Product Quantization (PQ))
- 用于压缩和储存大维向量
- Optimized Product Quantization (OPQ)
- CPU 和/或 GPU 的硬件加速
针对性能和准确性的权衡:
- 在相同的搜索场景中,准确性往往意味着更高维更高精度的向量,但是这些向量的计算(无论是线性还是聚类)中,单个向量间的计算成本会随之上升,使得整个召回过程性能下降
- 同时可以通过
nlist
、nprobe
以及其他聚类、距离计算公式的调整来调整精度和性能