cloud.tencent.com/developer/a…
相似性测量 (Similarity Measurement)
欧几里得距离(Euclidean Distance)
反映向量的绝对距离,适用于需要考虑向量长度的相似性计算
余弦相似度(Cosine Similarity)
对向量的长度不敏感,只关注向量的方向
点积相似度 (Dot product Similarity)
相似性搜索 (Similarity Search)
高效的搜索算法有很多,其主要思想是通过两种方式提高搜索效率:
- 减少向量大小——通过降维或减少表示向量值的长度。
- 缩小搜索范围——可以通过聚类或将向量组织成基于树形、图形结构来实现,并限制搜索范围仅在最接近的簇中进行,或者通过最相似的分支进行过滤。
首先来介绍一下大部分算法共有的核心概念,也就是聚类。
K-Means 和 Faiss
我们可以在保存向量数据后,先对向量数据先进行聚类。例如下图在二维坐标系中,划定了 4 个聚类中心,然后将每个向量分配到最近的聚类中心,经过聚类算法不断调整聚类中心位置,这样就可以将向量数据分成 4 个簇。每次搜索时,只需要先判断搜索向量属于哪个簇,然后再在这一个簇中进行搜索,这样就从 4 个簇的搜索范围减少到了 1 个簇,大大减少了搜索的范围。
常见的聚类算法有 K-Means,它可以将数据分成 k 个类别,其中 k 是预先指定的。以下是 k-means 算法的基本步骤:
- 选择 k 个初始聚类中心。
- 将每个数据点分配到最近的聚类中心。
- 计算每个聚类的新中心。
- 重复步骤 2 和 3,直到聚类中心不再改变或达到最大迭代次数。
但是这种搜索方式也有一些缺点,例如在搜索的时候,如果搜索的内容正好处于两个分类区域的中间,就很有可能遗漏掉最相似的向量。
现实情况中,向量的分布也不会像图中一样区分的那么明显,往往区域的边界是相邻的,就像下图 [Faiss 算法] 一样。
我们可以将向量想象为包含在 Voronoi 单元格中 - 当引入一个新的查询向量时,首先测量其与质心 (centroids) 之间的距离,然后将搜索范围限制在该质心所在的单元格内。
那么为了解决搜索时可能存在的遗漏问题,可以将搜索范围动态调整,例如当 nprobe = 1 时,只搜索最近的一个聚类中心,当 nprobe = 2 时,搜索最近的两个聚类中心,根据实际业务的需求调整 nprobe 的值。
实际上,除了暴力搜索能完美的搜索出最相邻,所有的搜索算法只能在速度和质量还有内存上做一个权衡,这些算法也被称为近似最相邻(Approximate Nearest Neighbor)。
Product Quantization (PQ) 乘积量化
但是在高维坐标系中,还会遇到维度灾难问题,具体来说,随着维度的增加,数据点之间的距离会呈指数级增长,这也就意味着,在高维坐标系中,需要更多的聚类中心点将数据点分成更小的簇,才能提高分类的质量。否者,向量和自己的聚类中心距离很远,会极大的降低搜索的速度和质量。
但如果想要维持分类和搜索质量,就需要维护数量庞大的聚类中心。随之而来会带来另一个问题,那就是聚类中心点的数量会随着维度的增加而指数级增长,这样会导致我们存储码本的数量极速增加,从而极大的增加了内存的消耗。例如一个 128 维的向量,需要维护 2^64 个聚类中心才能维持不错的量化结果,但这样的码本存储大小已经超过维护原始向量的内存大小了。
解决这个问题的方法是将向量分解为多个子向量,然后对每个子向量独立进行量化,比如将 128 维的向量分为 8 个 16 维的向量,然后在 8 个 16 维的子向量上分别进行聚类,因为 16 维的子向量大概只需要 256 个聚类中心就能得到还不错的量化结果,所以就可以将码本的大小从 2^64 降低到 8 * 256 = 2048 个聚类中心,从而降低内存开销。
而将向量进行编码后,也将得到 8 个编码值,将它们拼起来就是该向量的最终编码值。等到使用的时候,只需要将这 8 个编码值,然后分别在 8 个子码本中搜索出对应的 16 维的向量,就能将它们使用笛卡尔积的方式组合成一个 128 维的向量,从而得到最终的搜索结果。这也就是乘积量化(Product Quantization)的原理。
使用 PQ 算法,可以显著的减少内存的开销,同时加快搜索的速度,它唯一的问题是搜索的质量会有所下降,但就像我们刚才所讲,所有算法都是在内存、速度和质量上做一个权衡。
Hierarchical Navigable Small Worlds (HNSW)
除了聚类以外,也可以通过构建树或者构建图的方式来实现近似最近邻搜索。这种方法的基本思想是每次将向量加到数据库中的时候,就先找到与它最相邻的向量,然后将它们连接起来,这样就构成了一个图。当需要搜索的时候,就可以从图中的某个节点开始,不断的进行最相邻搜索和最短路径计算,直到找到最相似的向量。
这种算法能保证搜索的质量,但是如果图中所有的节点都以最短的路径相连,如图中最下面的一层,那么在搜索的时候,就同样需要遍历所有的节点。
解决这个问题的思路与常见的跳表算法相似,如下图要搜索跳表,从最高层开始,沿着具有最长“跳过”的边向右移动。如果发现当前节点的值大于要搜索的值-我们知道已经超过了目标,因此我们会在下一级中向前一个节点。
HNSW 继承了相同的分层格式,最高层具有更长的边缘(用于快速搜索),而较低层具有较短的边缘(用于准确搜索)。
Locality Sensitive Hashing (LSH) 局部敏感哈希
局部敏感哈希(Locality Sensitive Hashing)也是一种使用近似最近邻搜索的索引技术。它的特点是快速,同时仍然提供一个近似、非穷举的结果。LSH 使用一组哈希函数将相似向量映射到“桶”中,从而使相似向量具有相同的哈希值。这样,就可以通过比较哈希值来判断向量之间的相似度。
但是在向量搜索中,我们的目的是为了找到相似的向量,所以可以专门设计一种哈希函数,使得哈希碰撞的概率尽可能高,并且位置越近或者越相似的向量越容易碰撞,这样相似的向量就会被映射到同一个桶中。
当搜索一个向量时,将这个向量再次进行哈希函数计算,得到相同桶中的向量,然后再通过暴力搜索的方式,找到最接近的向量。
Random Projection for LSH 随机投影
如果在二维坐标系可以通过随机生成的直线区分相似性,那么同理,在三维坐标系中,就可以通过随机生成一个平面,将三维坐标系划分为两个区域。在多维坐标系中,同样可以通过随机生成一个超平面,将多维坐标系划分为两个区域,从而区分相似性。
但是在高维空间中,数据点之间的距离往往非常稀疏,数据点之间的距离会随着维度的增加呈指数级增长。导致计算出来的桶非常多,最极端的情况是每个桶中就一个向量,并且计算速度非常慢。所以实际上在实现 LSH 算法的时候,会考虑使用随机投影的方式,将高维空间的数据点投影到低维空间,从而减少计算的时间和提高查询的质量。
其基本步骤是:
- 从高维空间中随机选择一个超平面,将数据点投影到该超平面上。
- 重复步骤 1,选择多个超平面,将数据点投影到多个超平面上。
- 将多个超平面的投影结果组合成一个向量,作为低维空间中的表示。
- 使用哈希函数将低维空间中的向量映射到哈希桶中。
选型
- ChromaDB is best suited for applications where fast vector similarity search is critical, data fits on a single machine, and simplicity is preferred over a feature-rich API.
传统数据库的扩展
Redis 除了传统的 Key Value 数据库用途外,Redis 还提供了 Redis Modules,这是一种通过新功能、命令和数据类型扩展 Redis 的方式。例如使用 RediSearch 模块来扩展向量搜索的功能。
过滤 (Filtering)
在实际的业务场景中,往往不需要在整个向量数据库中进行相似性搜索,而是通过部分的业务字段进行过滤再进行查询。所以存储在数据库的向量往往还需要包含元数据,例如用户 ID、文档 ID 等信息。这样就可以在搜索的时候,根据元数据来过滤搜索结果,从而得到最终的结果。
为此,向量数据库通常维护两个索引:一个是向量索引,另一个是元数据索引。
过滤过程可以在向量搜索本身之前或之后执行,但每种方法都有自己的挑战,可能会影响查询性能:
- Pre-filtering:在向量搜索之前进行元数据过滤。虽然这可以帮助减少搜索空间,但也可能导致系统忽略与元数据筛选标准不匹配的相关结果。
- Post-filtering:在向量搜索完成后进行元数据过滤。这可以确保考虑所有相关结果,在搜索完成后将不相关的结果进行筛选。