在之前的文章中,我们系统性的介绍了 嵌入和语义检索 以及 向量相似度。
本文将在语义检索领域探讨更加接近实战的话题:如何如何选择FAISS的索引类型。
Faiss 是一个用于高效相似性搜索和密集向量聚类的库。它使用 C++ 编写,并提供了完整的 Python 封装。 可以在 CPU 上运行,只是一些最有用的算法是在 GPU 上实现的。它的主要功能是:
- k-NN 检索: 不仅返回最近的邻居,还返回第二近、第三近、……、第 k 近的邻居;
- 批量处理: 一次搜索多个向量,而不是一次搜索一个。 对于许多索引类型,这比一个接一个地搜索向量更快;
- 以精度换取速度: 以 10% 的不正确结果为代价,换取 10 倍的速度或使用更少的内存;
- 范围搜索:返回查询点给定半径内的所有元素;
- 本地存储: 将索引存储在磁盘上而不是在 RAM 中;
- …
FAISS索引概览
Faiss 支持多种相似度度量方式,包括欧氏距离(L2距离)、内积(余弦相似度)、汉明距离等,以适应不同应用场景的需求。
Faiss 提供了众多类型的索引,大体上可以分为两大家族:二进制索引/Binary 索引/Binary Indexes(不常用) 和 实值索引(常用)。
Faiss 的核心在于其高效的索引结构和搜索算法,常用的有以下几类:
从索引结构上看:
- 平面索引(Flat,方法名:
IndexFlatL2/IP):最简单的索引结构,它不修改输入的向量,将所有向量存储在一起。搜索时需遍历整个数据集,计算查询向量与每个数据向量的相似度。 - 倒排文件索引(Inverted File Index,缩写:
IVF,方法名:IndexIVF*):基于聚类的思想,先将数据集划分为多个子集。搜索时先找到最相关的几个子集(近似搜索),再在子集中精确搜索。为什么要倒排?
正排 :应该是 “向量→所属聚类”,即每个向量记录自己属于哪个分区(以向量为中心); 倒排(IVF 的实际做法):是 “聚类→向量列表”,即每个聚类中心记录属于该分区的所有向量,形成 “倒排表(Inverted List)”。
在向量检索中,若用 正排 逻辑(每个向量记录自己的分区),查询时需要遍历所有向量,检查其所属分区是否匹配,效率极低(O(N))。而 倒排 逻辑通过 聚类→向量列表 的映射,能直接定位到候选分区的向量集合,只需在小范围内搜索(O(M),M 远小于 N),这正是 “倒排” 带来的核心效率提升。 - 基于图的索引(Hierarchical Navigable Small World,缩写:
HNSW,方法名:IndexHNSW*):基于图的近似最近邻搜索算法,构建多层图结构,每一层节点代表一个向量,节点间边代表相似度。搜索时通过层次跳跃快速缩小搜索范围,最终找到近似最近邻。HNSW(Hierarchical Navigable Small World)的本质是给向量构建一张多层级的 “导航图”,上层图是 “高速路”(少节点、跳得远),下层图是 “街道”(多节点、走得细)。即:上层快速缩小范围,下层精准定位目标。
- 乘积量化索引(Product Quantization):将高维向量拆分成多个低维子向量,分别量化(压缩)后存储,大幅降低内存占用并加速距离计算。
单独使用较少,多与 IVF(倒排文件)结合为IndexIVFPQ(先分区再量化),平衡速度、内存与精度。
从搜索算法角度看:
- 精确索引 (Exact Index) : 遍历所有向量,保证返回最精确的近邻结果。
IndexFlatIP和IndexFlatL2就属于精确索引。虽然结果精确,但当数据集非常大时,搜索速度会比较慢。 - 近似索引 (Approximate Index) : 通过牺牲一定的搜索精度来换取更快的搜索速度和更低的内存占用。例如
IndexIVFFlat和IndexHNSWFlat。它们适用于需要处理大规模数据集,且对精度要求相对宽松的场景。
下图展示了 Faiss 索引家族的概况:
二进制索引
二进制索引(Binary Indexes) 是 FAISS 的一个独立分支体系,它们与实值索引(Real-valued Index)共享一套 相同的上层接口,但底层存储和相似度计算方式完全不同。如下表:
| 家族类型 | 存储形式 | 距离度量 | 典型用途 |
|---|---|---|---|
| 实值索引(Real-valued Index) | 向量为 float32(连续数值) |
欧氏距离、内积、余弦相似度 | 语义检索、图像检索、文本嵌入 |
| 二进制索引(Binary Index) | 向量为二进制码(0/1 比特) | 汉明距离(Hamming Distance) | 二值特征、指纹特征、压缩特征匹配 |
汉明距离(Hamming Distance):计算两个等长二进制字符串(或二进制向量)中,对应位置上 “0” 和 “1” 不同的位数,以此衡量两者的差异程度。
例如:
- 字符串 A:101101
- 字符串 B:111001
逐位对比:第 2 位(0 vs 1)、第 4 位(1 vs 0)不同,共 2 个不同位 → 汉明距离 = 2。
二进制索引的核心思想
在 二进制索引 中,每个向量不是浮点数,而是一串二进制比特:
浮点向量(x): [0.1, 0.7, -0.3, -0.2, ...]
二值向量(b): 1100...
这些比特通常来自于浮点向量经过二值化(binarization)处理,例如使用下面的 Sign函数 即可将上面的 浮点向量(x) 转换为 二值向量(b):
$$ b_i = \begin{cases} 1 & \text{if } x_i \ge 0 \newline 0 & \text{otherwise} \end{cases} $$
然后通过汉明距离(Hamming distance)来比较相似度。
二进制索引的常见类型
| 索引类型 | 含义 | 特点 |
|---|---|---|
IndexBinaryFlat |
精确汉明距离计算 | 与 IndexFlatL2 类似(精确搜索) |
IndexBinaryIVF |
倒排文件 + 汉明距离 | 与 IndexIVFFlat 类似(分区加速) |
IndexBinaryHNSW |
图索引 + 汉明距离 | 与 IndexHNSWFlat 类似(高效近似) |
二进制索引与实值索引的关系
| 对比项 | 实值索引(Real-valued) | 二进制索引(Binary) |
|---|---|---|
| 向量类型 | float32 |
uint8(bit-packed) |
| 相似度度量 | L2 / Inner Product / Cosine | Hamming Distance |
| 搜索精度 | 高(连续空间) | 较低(离散空间) |
| 存储占用 | 较大 | 极小(压缩比高) |
| 搜索速度 | 较慢 | 极快(按位异或+位计数) |
| 常见用途 | 语义检索、嵌入(Embedding) | 二值哈希检索、图像指纹、局部特征 |
示例:两类索引的使用区别
import faiss
import numpy as np
# 浮点向量
xb = np.random.random((1000, 128)).astype('float32')
index_f = faiss.IndexFlatL2(128)
index_f.add(xb)
# 二值向量(假设已二值化)
xb_bin = np.random.randint(0, 2, (1000, 128)).astype('uint8')
index_b = faiss.IndexBinaryFlat(128)
index_b.add(xb_bin)
两者都可以 .add() / .search(),
但计算方式完全不同:
IndexFlatL2使用欧氏距离IndexBinaryFlat使用汉明距离
二进制索引的优势与局限
| 优势 | 局限 |
|---|---|
| 极小存储(128维仅16字节) | 相似度精度较低 |
| 查询极快(基于位运算) | 无法直接处理浮点嵌入(Embedding) |
| 适合硬件加速(GPU/FPGA) | 不适合复杂语义向量空间 |
小结
二进制索引是一条并行的、针对二进制特征优化的索引体系,它的结构设计借鉴了实值索引,但使用汉明距离代替了浮点距离。
它不是“Flat/IVF/PQ”的子类,而是另一类以汉明距离为核心的索引体系,用于二值化特征的超高效相似度搜索。
实值索引
下面我们看看常用的 Faiss 实值索引。
1. IndexFlatIP (Flat Inner Product Index)
类型: 精确索引 (Exact Index)
度量: 内积 (Inner Product, IP)
工作原理: IndexFlatIP 是最基础的 Faiss 索引类型之一。它将所有向量 不做任何压缩或编码 直接存储。在搜索时,它会 暴力遍历 索引中的所有向量,计算每个向量与查询向量的内积,并返回内积值最高的 K 个向量作为最近邻。
优点:
- 精度高: 由于是针对全部向量的暴力搜索,具有完美的召回率。
- 实现简单: 理解和实现都非常直接。
缺点:
- 速度慢: 当数据集非常大时,搜索速度会非常慢,不适合大规模数据集的实时应用。
- 内存占用高: 需要存储所有原始向量,内存消耗大。
适用场景:
- 小规模数据集: 当数据集规模较小时,例如几百万甚至更小,可以接受暴力搜索的耗时。
- 对精度要求极高: 在一些对搜索结果精度要求非常高的场景下,例如需要作为评估其他近似索引方法精度的基准。
- 原型验证和实验: 在算法原型开发阶段,可以使用 IndexFlatIP 来快速验证算法的正确性,然后再考虑更高效的索引方法。
index = faiss.IndexFlatIP(dimension)
2. IndexFlatL2 (Flat L2 Distance Index)
类型: 精确索引 (Exact Index)
度量: L2 距离 (欧氏距离, Euclidean Distance)
工作原理: 与 IndexFlatIP 类似,IndexFlatL2 也将所有向量原始地存储,并进行暴力搜索。不同之处在于,IndexFlatL2 计算的是 L2 距离 (欧氏距离),并返回 L2 距离最小的 K 个向量作为最近邻。
优点和缺点: 与 IndexFlatIP 类似,精度高,实现简单,但速度慢,内存占用高。
适用场景:
- 小规模数据集,且需要基于 L2 距离 进行相似性搜索的场景。
- 同样适用于精度要求高或作为基准测试的场景。
index = faiss.IndexFlatL2(dimension)
3. IndexIVFFlat (Inverted File Flat Index)
类型: 近似索引 (Approximate Index)
度量: 通常与 L2 距离或内积结合使用
工作原理: IndexIVFFlat 引入了 倒排文件 (Inverted File) 的思想来加速搜索。它首先将向量空间划分为多个单元 ,每个单元有一个 聚类中心。在索引构建阶段,每个向量会被分配到离它最近的聚类中心所代表的单元中。在搜索阶段,首先找到与查询向量最近的若干个单元,然后在这些单元内部进行暴力搜索 (Flat 搜索)。
优点:
- 搜索速度快: 通过只在部分单元内搜索,大大减少了搜索范围,提高了搜索速度。
- 可控的精度和速度平衡: 可以通过调整单元的数量 (
nlist) 和搜索时访问的单元数量 (nprobe) 来平衡搜索精度和速度。
在FAISS的IVF(倒排文件)索引中,nlist和nprobe是两个核心参数,直接决定了索引的空间分区粒度和搜索范围,进而平衡搜索的速度与精度:
nlist:聚类中心的数量(空间分区数)
- 作用:定义IVF索引将整个向量空间划分的聚类分区数量。
训练时,IVF会对向量进行聚类,生成nlist个聚类中心,每个中心对应一个“ Voronoi 胞腔”(分区),所有向量被分配到距离最近的分区中,形成nlist个倒排表。 - 影响:
nlist越大,每个分区包含的向量越少,理论上局部搜索速度越快,但聚类中心的存储和计算成本越高(训练时间更长)。nlist需与数据量匹配(通常设为1024、2048等,数据量越大,nlist可适当增大)。
nprobe:查询时访问的分区数
- 作用:指定搜索时需要检查的候选分区数量。
查询时,先计算查询向量与nlist个聚类中心的距离,然后选取距离最近的nprobe个分区,仅在这些分区的倒排表中搜索向量。 - 影响:
nprobe越小,搜索范围越窄,速度越快,但可能漏掉真正的近邻(精度下降)。nprobe越大,覆盖的候选分区越多,精度越高,但需要计算更多向量的距离(速度变慢)。- 是IVF索引中最关键的调优参数,通常通过实验在速度与精度间权衡(例如
nlist=1024时,nprobe=16为常见折中值)。
缺点:
- 近似搜索: 由于只搜索了部分单元,可能错过真正的最近邻,因此是近似搜索。
- 需要预训练: 需要先对数据集进行聚类,得到聚类中心。
适用场景:
- 中等规模数据集: 适用于数据集规模较大,但仍然希望在一定精度下获得较快搜索速度的场景。
- 需要平衡精度和速度: 可以通过调整参数来适应不同的精度和速度需求。
quantizer = faiss.IndexFlatL2(dimension) # 使用 L2 距离的量化器
index = faiss.IndexIVFFlat(quantizer, dimension, nlist=100) # nlist 是聚类中心的数量
index.train(embeddings_array) # 必须先训练索引
index.add(embeddings_array) # 然后将数据添加到索引中
4. IndexHNSWFlat (Hierarchical Navigable Small World graph Index)
类型: 近似索引 (Approximate Index)
度量: 通常与 L2 距离或内积结合使用
工作原理: IndexHNSWFlat 基于 分层可导航小世界图 (HNSW,Hierarchical Navigable Small World graph) 算法。它构建一个多层图结构,图中每个节点代表一个向量。在高层图中,节点之间的连接较少,主要用于快速定位到可能包含最近邻的区域;在低层图中,节点连接更密集,用于在局部区域进行更精细的搜索。搜索过程从顶层图开始,逐步向下层图深入,最终快速定位到最近邻。
优点:
- 搜索速度非常快: HNSW 算法在搜索速度上通常比 IndexIVFFlat 更快,尤其是在高精度要求下。
- 精度较高: 在相同的搜索速度下,HNSW 通常能提供更高的精度。
- 无需预训练: HNSW 索引的构建过程不需要预先进行聚类。
缺点:
- 索引构建时间较长: HNSW 索引的构建时间通常比 IndexIVFFlat 长。
- 内存占用相对较高: 为了维护图结构,HNSW 的内存占用通常比 IndexIVFFlat 高。
适用场景:
- 大规模数据集: 适用于需要处理非常大的数据集,并对搜索速度和精度都有较高要求的场景。
- 对实时性要求高: 例如在线推荐系统、实时检索等。
index = faiss.IndexHNSWFlat(dimension, M=16)
5. IndexIVFPQ
类型: 近似索引 (Approximate Index)
度量: 通过 “IVF + PQ (Product Quantization)” 实现高效检索
工作原理: 倒排文件与乘积量化(Product Quantization)结合(将高维向量压缩为低维编码)。使用乘积量化来进一步压缩数据存储,优化存储空间和查询速度。
优点:对于非常大的数据集和需要压缩存储的场景,IndexIVFPQ 是一种高效的索引方式。 缺点:训练和构建索引的过程相对复杂,适合大数据集。
适用场景:
- 大规模高维向量检索:如亿级图像特征、文本嵌入向量;
- 内存有限但需要平衡速度与精度的场景:相比精确索引 IndexFlatL2,内存可压缩 10-100 倍。
import faiss
# 1. 定义向量维度、参数
d = 128 # 向量维度
nlist = 1024 # 聚类中心数
m = 16 # PQ 子向量数
nbits = 8 # 每个子向量的量化位数
# 2. 构建量化器(用于 IVF 聚类,通常用 IndexFlatL2)
quantizer = faiss.IndexFlatL2(d)
# 3. 初始化 IndexIVFPQ
index = faiss.IndexIVFPQ(quantizer, d, nlist, m, nbits)
# 4. 训练索引(用样本向量生成聚类中心和 PQ 码本)
index.train(train_vectors) # train_vectors 是形状为 (N, d) 的样本向量
# 5. 添加向量到索引
index.add(database_vectors) # database_vectors 是待索引的向量库
# 6. 设置查询参数 nprobe,搜索
index.nprobe = 32 # 访问 32 个候选分区
D, I = index.search(query_vectors, k=10) # 返回每个查询的 top-10 近似近邻(距离 D,索引 I)
使用时需重点调优 nlist(分区数)和 nprobe(查询分区数),在速度与精度间找到平衡(通常 nprobe 越大,精度越接近精确搜索,但速度越慢)。
总结
由以上的分析可见,在选择 Faiss 索引时,需要综合考虑以下因素:
数据集规模: 数据集越大,越需要考虑近似索引,例如 IndexIVFFlat 或 IndexHNSWFlat。
精度要求: 如果对精度要求非常高,可以考虑精确索引 (IndexFlatIP, IndexFlatL2),但需要注意速度限制。如果可以接受一定的精度损失,则近似索引是更好的选择。
速度要求: 对搜索速度要求越高,越需要选择更快的近似索引,例如 IndexHNSWFlat。
内存限制: 内存资源有限时,需要考虑内存占用较低的索引方法。比如 PQ 或者 二进制索引。
在实际应用中,通常需要根据具体的数据集和需求进行实验和调优,选择最合适的索引方法和参数配置。
建议先从简单的 IndexFlatIP 或 IndexFlatL2 开始,如果速度无法满足需求,再尝试 IndexIVFFlat 或 IndexHNSWFlat 等近似索引方法。
参考:
🪐祝您好运🪐