回到笔记
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")

索引文件与业务元数据要一起做版本管理。模型、维度或归一化方式变化后,旧向量不能直接与新向量混用,通常需要整体重建。

一条更稳的落地路线

  1. 固定嵌入模型和预处理;
  2. IndexFlat* 建立准确率基线;
  3. 为每条向量保存稳定业务 ID;
  4. 用真实查询集评估结果;
  5. 性能确实成为瓶颈后,再尝试 IVF、PQ 或 GPU;
  6. 把索引版本、模型版本和数据库快照绑定起来。

Faiss 的难点往往不在 search(),而在“向量是否能代表业务语义”以及“索引结果如何与权限、过滤和更新机制结合”。先把基线做对,再追求更复杂的索引。