AI向量检索开发教程
Faiss 向量检索实战
从精确检索与余弦相似度入门,逐步理解业务 ID、IVF、PQ、索引持久化,以及召回率和性能之间的取舍。
Faiss 解决的是“在大量向量中,快速找到与查询向量最接近的若干项”。文本检索、图片去重、推荐召回和 RAG 都可能用到它,但 Faiss 只负责向量索引,不负责生成向量,也不替你保存完整业务数据。
从精确检索开始
先用最简单的 IndexFlatL2 验证数据、距离和结果顺序:
import faiss
import numpy as np
dimension = 384
vectors = np.random.random((10_000, dimension)).astype("float32")
queries = np.random.random((2, dimension)).astype("float32")
index = faiss.IndexFlatL2(dimension)
index.add(vectors)
distances, positions = index.search(queries, k=5)
positions 是向量在索引中的位置,distances 是 L2 距离。Flat 索引逐个比较,结果精确、使用简单,但数据量大时查询成本会线性增长。
余弦相似度
Faiss 没有单独的“余弦索引”。把向量归一化后使用内积,排序结果就等价于余弦相似度:
faiss.normalize_L2(vectors)
faiss.normalize_L2(queries)
index = faiss.IndexFlatIP(dimension)
index.add(vectors)
scores, positions = index.search(queries, k=5)
[!IMPORTANT] 入库向量和查询向量必须使用同一模型、同一维度和同一归一化策略。
给向量绑定业务 ID
不要把数组下标当作长期业务标识。可以用 IndexIDMap2 保存自己的整数 ID:
base = faiss.IndexFlatIP(dimension)
index = faiss.IndexIDMap2(base)
ids = np.arange(100_000, 110_000, dtype="int64")
index.add_with_ids(vectors, ids)
Faiss 仍然不保存标题、正文或权限信息。搜索得到 ID 后,需要回到数据库读取业务记录并做权限过滤。
数据量上来后再选近似索引
IVF
IVF 先把向量分到多个聚类桶,查询时只搜索其中一部分:
clusters = 256
quantizer = faiss.IndexFlatL2(dimension)
index = faiss.IndexIVFFlat(quantizer, dimension, clusters)
index.train(vectors)
index.add(vectors)
index.nprobe = 16
nlist(这里的clusters)决定桶数量;nprobe决定查询多少个桶;nprobe越大,召回率通常越高,查询也越慢;- IVF 必须先用具有代表性的数据训练。
PQ
Product Quantization 会压缩向量,显著降低内存占用,但也会损失一部分精度。常见组合是 IndexIVFPQ:先用 IVF 缩小候选范围,再用 PQ 存储压缩后的向量。
没有脱离数据分布的“最佳参数”。应在自己的验证集上同时测:
- Recall@K;
- P50 / P95 查询延迟;
- 索引构建时间;
- 内存和磁盘占用。
保存与加载
faiss.write_index(index, "vectors.faiss")
index = faiss.read_index("vectors.faiss")
索引文件与业务元数据要一起做版本管理。模型、维度或归一化方式变化后,旧向量不能直接与新向量混用,通常需要整体重建。
一条更稳的落地路线
- 固定嵌入模型和预处理;
- 用
IndexFlat*建立准确率基线; - 为每条向量保存稳定业务 ID;
- 用真实查询集评估结果;
- 性能确实成为瓶颈后,再尝试 IVF、PQ 或 GPU;
- 把索引版本、模型版本和数据库快照绑定起来。
Faiss 的难点往往不在 search(),而在“向量是否能代表业务语义”以及“索引结果如何与权限、过滤和更新机制结合”。先把基线做对,再追求更复杂的索引。