单机向量检索最重要的工具之一,理解它几乎等于理解许多向量数据库的底层能力
为什么先学 FAISS
很多向量数据库的底层索引思想,并不是从零开始发明的。它们要么直接集成成熟检索库,要么在其基础上封装服务能力。在单机场景里,FAISSFacebook AI Similarity SearchFAISS is a high-performance similarity search library developed by Meta. It provides many vector indexes, from exact flat search to IVF, PQ, and HNSW, and is widely used as a building block underneath vector search systems. 几乎是最重要的基础设施之一。
学习 FAISS 的价值在于,它把向量检索的核心概念暴露得非常直接。你需要自己处理数据矩阵、索引训练、添加向量、查询与参数调优,因此比起直接调用一个数据库 API,更容易看清“索引到底在做什么”。很多生产系统最终未必直接使用 FAISS,但理解它之后,再看 Milvus、Qdrant 或其他产品,很多能力边界都会清楚得多。
本文只覆盖三个最常见的入口:精确搜索 IndexFlatL2、基于聚类的 IndexIVFFlat,以及图索引 IndexHNSWFlat。这三种结构正好对应前面几篇讨论过的三类思路。
先准备环境与样例数据
本篇示例默认使用 CPU 版本,便于本地直接运行。先安装依赖:
pip install faiss-cpu numpy
下面的代码会生成一批随机向量,并做一次简单查询。注意 FAISS 默认要求输入是 float32 的二维数组,形状通常为 (n, d),其中 n 是向量数量,d 是维度。
import numpy as np
import faiss
np.random.seed(42)
d = 128
nb = 10000
nq = 3
xb = np.random.random((nb, d)).astype("float32")
xq = np.random.random((nq, d)).astype("float32")
print(xb.shape, xq.shape)
在真实项目里,这些向量通常不是随机数,而是 embedding 模型输出。无论来源是什么,只要维度一致,FAISS 的使用方式都相同。
IndexFlatL2:最直接、最精确的基线
IndexFlatL2 是最容易理解的索引。它不训练、不压缩、不做近似,只是在查询时把目标向量与库中全部向量做L2 距离L2 distanceL2 distance is the Euclidean distance between two vectors. In FAISS, IndexFlatL2 performs exact nearest neighbor search under this metric by scanning all stored vectors.
计算,因此结果是精确的。
import numpy as np
import faiss
np.random.seed(42)
d = 128
nb = 10000
nq = 3
xb = np.random.random((nb, d)).astype("float32")
xq = np.random.random((nq, d)).astype("float32")
index = faiss.IndexFlatL2(d)
print("is_trained:", index.is_trained)
index.add(xb)
print("ntotal:", index.ntotal)
k = 5
distances, indices = index.search(xq, k)
print("distances:\n", distances)
print("indices:\n", indices)
search() 返回两部分内容:距离矩阵和索引矩阵。前者记录每个查询命中的前 k 个距离,后者记录对应向量在库中的下标。对于小规模数据集,这种结构完全够用,也是评估其他近似索引召回率时最常用的基线。
IndexIVFFlat:先聚类,再扫描部分簇
IndexIVFFlat 在结构上分两层。最外层是一个粗量化器,负责把向量分配到若干簇中;簇内部仍保存完整向量,不做压缩,所以名字里有 Flat。相比 IndexFlatL2,它需要多一个训练步骤。
import numpy as np
import faiss
np.random.seed(42)
d = 128
nb = 10000
nq = 3
xb = np.random.random((nb, d)).astype("float32")
xq = np.random.random((nq, d)).astype("float32")
nlist = 100
quantizer = faiss.IndexFlatL2(d)
index = faiss.IndexIVFFlat(quantizer, d, nlist, faiss.METRIC_L2)
print("before training:", index.is_trained)
index.train(xb)
print("after training:", index.is_trained)
index.add(xb)
index.nprobe = 8
k = 5
distances, indices = index.search(xq, k)
print("distances:\n", distances)
print("indices:\n", indices)
这里有两个关键参数。nlist 表示把向量库分成多少个粗簇,nprobe 表示查询时要访问多少个簇。前者更像建索引时的结构参数,后者更像查询时的质量开关。nprobe 调高,通常召回率更好,但延迟也会上升。
训练索引训练索引在 IVF 索引里,训练并不是训练一个深度学习模型,而是用样本向量学习聚类中心等辅助结构。训练完成后,索引才知道如何把新向量分桶以及查询时该先访问哪些桶。
也是很多初学者容易忽视的环节。你不能在未训练状态下直接 add(),因为簇中心还不存在。
IndexHNSWFlat:图结构索引的本地实践
如果你想体验图索引的用法,IndexHNSWFlat 是很直接的入口。它内部仍保存全精度向量,但检索时通过 HNSW 图来做近似搜索。
import numpy as np
import faiss
np.random.seed(42)
d = 128
nb = 10000
nq = 3
xb = np.random.random((nb, d)).astype("float32")
xq = np.random.random((nq, d)).astype("float32")
M = 32
index = faiss.IndexHNSWFlat(d, M)
index.hnsw.efConstruction = 200
index.add(xb)
index.hnsw.efSearch = 64
k = 5
distances, indices = index.search(xq, k)
print("distances:\n", distances)
print("indices:\n", indices)
其中 M 大致决定每个节点保留多少邻接关系,efConstruction 影响建图质量与构建成本,efSearch 影响查询时愿意探索多少候选。和 IVF 的 nprobe 类似,efSearch 越高,通常越接近精确结果,但延迟也会相应增加。
如何做一个最小可用的索引对比
如果你只是把三段代码跑通,还不算真正理解 FAISS。更有价值的做法,是拿同一批数据、同一批查询,比较不同索引在速度与结果上的差异。一个最小实验可以这样写:
import time
import numpy as np
import faiss
np.random.seed(42)
d = 128
nb = 20000
nq = 100
k = 10
xb = np.random.random((nb, d)).astype("float32")
xq = np.random.random((nq, d)).astype("float32")
flat = faiss.IndexFlatL2(d)
flat.add(xb)
ivf = faiss.IndexIVFFlat(faiss.IndexFlatL2(d), d, 100, faiss.METRIC_L2)
ivf.train(xb)
ivf.add(xb)
ivf.nprobe = 8
hnsw = faiss.IndexHNSWFlat(d, 32)
hnsw.hnsw.efConstruction = 200
hnsw.add(xb)
hnsw.hnsw.efSearch = 64
def run(name, index):
start = time.perf_counter()
distances, indices = index.search(xq, k)
elapsed = time.perf_counter() - start
print(f"{name}: {elapsed:.4f}s, sample top1={indices[0, 0]}")
return indices
flat_result = run("flat", flat)
ivf_result = run("ivf", ivf)
hnsw_result = run("hnsw", hnsw)
flat_top10 = set(flat_result[0])
ivf_overlap = len(flat_top10 & set(ivf_result[0])) / len(flat_top10)
hnsw_overlap = len(flat_top10 & set(hnsw_result[0])) / len(flat_top10)
print("ivf overlap:", ivf_overlap)
print("hnsw overlap:", hnsw_overlap)
这里没有做严格学术评测,只是用 IndexFlatL2 作为近似真值,粗看近似索引命中前十的重合度。真正线上评估通常会更系统,包含不同查询分布、不同参数设置下的延迟分位数、召回率曲线与内存占用。
FAISS 实战中的三个常见坑
FAISS 更像“检索引擎内核”,不是完整数据库
到这里可以看出,FAISS 对“向量怎么查”给出了强大的原语,但它本身不负责完整数据库语义。它不替你管理文档元数据,不处理租户隔离,不提供 HTTP API、权限、复制和备份,也不会自动帮你把搜索结果与原始文本绑定起来。你需要自己在业务层补这些部分,或者使用上层产品来封装。
这也是为什么单机实验往往从 FAISS 起步,而生产服务往往转向向量数据库。前者让你精准控制索引与性能,后者让你获得更完整的系统能力。两者不是竞争关系,更像是层次关系。
本篇要点
- FAISS 是单机向量检索领域最重要的基础库之一,理解它有助于理解许多向量数据库的底层能力。
IndexFlatL2提供精确检索,是评估近似索引效果时常用的基线。IndexIVFFlat需要先训练再写入,通过nlist和nprobe在召回率与速度之间调节。IndexHNSWFlat用图索引做近似搜索,M、efConstruction和efSearch是常见调优入口。- FAISS 负责高性能检索内核,但不等于完整数据库,元数据、服务化和运维能力通常需要上层系统补齐。
下一篇
如果说 FAISS 是偏底层、偏索引工程的工具,那么下一篇要讲的 Chroma 则更接近本地原型开发的“开箱即用”方案。它很适合在个人电脑上快速搭知识库、接 LangChain、验证检索链路是否成立。
参考资料
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:FAISS实战
本文链接:https://www.sshipanoo.com/blog/ai/vector-db/04-FAISS实战/
本文最后一次更新为 天前,文章中的某些内容可能已过时!