Faiss 向量搜索库
bash conda install -c pytorch -c conda-forge faiss-cpu
或使用 pip:
```bash
pip install faiss-cpu
GPU 版本(需已安装 CUDA 驱动与 PyTorch 环境)
conda install -c pytorch -c conda-forge faiss-gpu
安装完成后,在 Python 中验证:
import faiss
print(faiss.__version__) # 应输出版本号,如 1.7.4
向量相似性搜索基础
在深入 Faiss 之前,先理解向量搜索的工作流:
- 向量化:将原始数据(文本、图片、用户行为)通过模型转换为固定长度的浮点数向量。
- 构建索引:将所有向量组织成一种数据结构(即索引)。
- 查询:给定一个查询向量,索引快速返回距离最近的一组向量及其 ID。
- 距离度量:常用 L2 距离(欧氏距离) 和 内积(IP,用于余弦相似度当向量归一化时)。
Faiss 将距离度量抽象为 MetricType,在创建索引时指定。
快速上手:一个极简搜索流程
从零开始,用 128 维随机向量模拟一个“10 万向量库,查询 10 条”的完整演示。
import numpy as np
import faiss
# ------------------- 1. 准备数据 -------------------
d = 128 # 向量维度
nb = 100000 # 数据库向量数量
nq = 10 # 查询向量数量
np.random.seed(42) # 复现结果
xb = np.random.random((nb, d)).astype('float32') # 数据库向量,必须为 float32
xq = np.random.random((nq, d)).astype('float32') # 查询向量
# ------------------- 2. 构建平面索引 -------------------
# IndexFlatL2 是暴力搜索的 L2 索引,精度最高但速度慢
index = faiss.IndexFlatL2(d) # d 是向量维度
print("是否已训练:", index.is_trained) # 平面索引无需训练,直接为 True
index.add(xb) # 将数据库向量加入索引
print("索引中向量数量:", index.ntotal)
# ------------------- 3. 执行搜索 -------------------
k = 4 # 返回最近邻的个数
D, I = index.search(xq, k) # D: 距离矩阵, I: 索引 ID 矩阵
print("第一个查询的最近邻 ID:", I[0])
print("第一个查询对应的距离:", D[0])
输出解读:
I[0]是离第一个查询向量最近的 4 个数据库向量的 ID(从 0 开始,按照距离升序)。D[0]是相应的平方欧氏距离。
注意事项:
- Faiss 中大部分索引要求数据为 float32 类型的连续数组,务必提前转换。
- 添加数据后,可通过
index.ntotal查看索引中的向量总数。
索引的选择与构建
Faiss 的核心概念是索引。不同索引在搜索速度、内存占用和精度间做出权衡。以下是常用索引的分类及使用场景。
精确搜索:基准索引
IndexFlatL2 / IndexFlatIP
- 原理:暴力计算查询向量与所有数据库向量的距离。
- 精度:100% 准确,返回真正的最近邻。
- 适用场景:作为其他近似索引的精度基准;适用于百万级以下的小数据集。
- 使用:
faiss.IndexFlatL2(d)或faiss.IndexFlatIP(d)(内积)。
IndexHNSWFlat
- 原理:基于分层可导航小世界图(HNSW),通过快速图遍历找到最近邻。
- 精度:通常能获得 >99% 的召回率,速度比 Flat 快数十倍。
- 适用场景:追求较高精度且数据量适中的场景(百万到千万级)。
- 内存占用高:因为它需要存储原始向量,不如压缩索引省内存。
# 构建 HNSW 索引,M 控制连接数,越大精度越高但内存越大
index = faiss.IndexHNSWFlat(d, 32) # d 为维度,32 是连接数,典型值 16-64
index.add(xb)
压缩索引:牺牲精度换取内存与速度
当数据集有数千万甚至数十亿条时,存储完整向量(d * 4 字节)压力巨大。Faiss 通过**乘积量化(PQ)**等技术大幅压缩向量。
IndexIVFPQ(倒排文件 + 乘积量化)
- 架构:先用倒排索引粗分,再用 PQ 编码残差向量。
- 步骤:
- 使用 K-Means 对向量进行粗聚类,划分到
nlist个倒排链表。 - 用乘积量化对每个向量的残差编码为短代码。
- 使用 K-Means 对向量进行粗聚类,划分到
- 优点:内存仅需完整向量的 10%-20%,搜索极快。
- 缺点:需训练,精度有折损。
nlist = 100 # 倒排列表数量,建议为 sqrt(n) 到 4*sqrt(n)
m = 8 # PQ 子向量个数,必须能整除维度 d
bits = 8 # 每个子向量的量化比特,通常为 8,即 1 字节
# 构建 IndexFlatL2 作为粗量化器(选择 nlist 个聚类中心)
quantizer = faiss.IndexFlatL2(d)
index = faiss.IndexIVFPQ(quantizer, d, nlist, m, bits)
# 训练:用数据学习聚类中心和 PQ 码本
index.train(xb) # xb 为训练向量,通常用全集或子集
index.add(xb) # 添加数据
参数建议:
nlist:聚类中心数,常见范围 100 ~ 65536。越大精度越高,但训练和搜索变慢。m:PQ 子空间数。维度 d 必须能被 m 整除。值越大精度越高,但编码越大。bits:每子空间量化位数,通常为 8。
IndexHNSWPQ
融合 HNSW 图结构 + PQ 压缩,内存占用极低且能保持较高精度。适合数亿甚至十亿级别数据。
index = faiss.IndexHNSWPQ(d, m, 32) # m 为 PQ 子空间,32 为 HNSW 的 M
内存映射索引:突破 RAM 限制
当向量数量过大无法全载入内存时,Faiss 支持内存映射 (MMAP),直接从磁盘读取索引。
index = faiss.read_index("large_index.faiss", faiss.IO_FLAG_MMAP)
该模式下,操作系统可按需将部分索引调入内存,非常适合超大规模数据集部署。
索引工厂:一行字符串创建索引
faiss.index_factory 提供了一种极简的声明式构建方式,根据描述字符串自动生成对应索引。
dim = 128
index = faiss.index_factory(dim, "IVF100,PQ8")
# 含义:IVF 聚类 100 个中心,PQ 编码 8 个子向量
常用工厂字符串:
"Flat"→ IndexFlatL2 (或 IP)"HNSW32"→ IndexHNSWFlat, M=32"IVF4096,PQ64"→ IndexIVFPQ, nlist=4096, m=64"PCA80,Flat"→ 先 PCA 降维至 80,再 Flat 索引"OPQ16_64,IVF1024,PQ16"→ OPQ 预处理旋转 + IVF+PQ
工厂模式是快速原型和实验的最佳方式。
搜索技巧与参数调优
构建索引后,可通过调整搜索参数在不重建索引的前提下平衡速度与精度。
IVF 索引的搜索参数 nprobe
当使用 IVF 系列索引(如 IndexIVFFlat、IndexIVFPQ)时,查询只访问部分倒排列表,nprobe 控制访问的聚类数。
- 默认
nprobe=1:只搜索最近的一个聚类,速度快但容易遗漏。 - 调大
nprobe可提高召回率,但带来更多计算。
index.nprobe = 10 # 在搜索前设置
D, I = index.search(xq, k)
经验规律:nprobe 从 1 增加到 nlist,精度持续提升,一般设到 nlist/10 即可获得较高召回。
HNSW 的搜索参数 efSearch
对于 HNSW 系列索引,efSearch 决定了动态搜索时的队列宽度。
index.hnsw.efSearch = 128 # 默认 16,增大可提高精度,但搜索变慢
批次化搜索
Faiss 本身高度优化了向量化操作,将多条查询打包成 batch 能显著提升吞吐量,尤其在 GPU 上。
batch_size = 256
for i in range(0, nq, batch_size):
batch_queries = xq[i:i+batch_size]
D_batch, I_batch = index.search(batch_queries, k)
# 处理结果...
GPU 加速入门
将计算迁移到 GPU 通常仅需几行代码,却能使搜索吞吐量提升 10~100 倍。
转移索引到 GPU
res = faiss.StandardGpuResources() # 申请 GPU 资源
gpu_index = faiss.index_cpu_to_gpu(res, 0, index) # 将 CPU 索引克隆到 GPU 0
# 搜索与 CPU 版本完全相同
D, I = gpu_index.search(xq, k)
直接在 GPU 上构建索引
gpu_index = faiss.index_factory(d, "IVF1024,PQ16", faiss.METRIC_L2)
# 若 faiss-gpu 安装正确,它会自动使用 GPU 加速训练和搜索
gpu_index.train(xb)
gpu_index.add(xb)
注意:GPU 版本需保证 CUDA 和 faiss-gpu 环境正确配置,且数据需提前放入 GPU 显存;超大索引可能需要多 GPU 分配。
典型实战场景
语义搜索系统
基于 BERT / Sentence-Transformers 将文本编码为 768 维或更高向量,构建 Faiss 索引实现毫秒级检索。
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('all-MiniLM-L6-v2')
sentences = ["机器学习很有趣", "Faiss 是向量搜索利器", ...]
embeddings = model.encode(sentences) # shape: [N, 384]
# 构建索引并添加
index = faiss.IndexFlatIP(embeddings.shape[1])
index.add(embeddings.astype('float32'))
# 查询
query_vec = model.encode(["如何快速检索文本?"])
D, I = index.search(query_vec.astype('float32'), k=5)
图像去重与相似推荐
使用预训练的 CNN(如 ResNet50)提取图像特征向量,通过 Faiss 查询近邻,若距离小于阈值则可判定为相似或重复。
# 伪代码:features 是 [N, 2048] 的图像特征
index = faiss.IndexHNSWFlat(2048, 64)
index.add(features)
# 对每一张图像,找到与它最相似的大量图像
D, I = index.search(features, k=10)
# 结合距离阈值用于去重
常见问题与调试建议
-
“Index not trained” 错误
某些索引(如 IVF、PQ 系列)需要先调用index.train(data)再add()。通过index.is_trained检查。 -
内存不足
尝试:- 使用压缩索引(IVFPQ、HNSWPQ)。
- 采用 MMAP 模式。
- 降低精度(如减少 m 或 bits)。
-
搜索结果精度骤降
- 检查
nprobe或efSearch是否设置过低。 - 如果数据分布变化大(如在线更新),原有训练码本可能失效,需定期重新训练。
- 检查
-
数据不是 float32
Faiss 严格要求输入为 contiguous float32 数组。使用np.ascontiguousarray(data.astype('float32'))进行转换。 -
距离度量与归一化
当使用内积(IP)而不是 L2 时,通常需要将向量 L2 归一化,使内积等价于余弦相似度。归一化后,IndexFlatIP即成为余弦相似度索引。
from sklearn.preprocessing import normalize
xb = normalize(np.random.random((nb, d)).astype('float32'))
index = faiss.IndexFlatIP(d) # 此时搜索等同于余弦相似度