我第一次接触FAISS是在做一个图片搜索的小项目,当时手里有几十万张商品图,每张都提取成了512维的特征向量。我问同事“怎么快速找到最像的那几张”,他甩给我一个词:FAISS。后来我才明白,这个东西本质上是把“找相似”变成了一道数学题,再交给高度优化的C++代码去跑。它不负责帮你生成向量,也不帮你存原始数据,它干的事情很纯粹——在海量向量里,用尽量少的算力,帮你把最接近的那几条捞出来。理解了这一点,后面的索引类型、参数调优、工程化都会顺很多。
1.1 向量检索与相似度度量基础
我常常拿“找相似”这件事跟朋友打比方。你想在一堆照片里找到和某张最像的,人眼靠的是颜色、构图、物体轮廓这些模糊的整体印象。机器没有眼睛,我们就把每张照片压缩成一串数字,比如512个浮点数,这串数字就叫向量。向量把语义信息编码进了几何空间,两张图越像,它们的向量在高维空间里就离得越近。向量检索要做的,就是在这个高维空间里找“邻居”。
度量“近”有很多种方式,我踩过坑才搞清楚它们的区别。最常用的是内积(Inner Product),它衡量两个向量的方向一致性,数值越大越相似。欧氏距离(L2)衡量的则是空间里的直线距离,数值越小越相似。还有余弦相似度,本质上是对向量做了归一化之后的内积。这三者在实际使用中经常被混着用,但结果会不一样。我做过一个小实验:同一批文本向量,用内积检索出来的Top10和用L2检索出来的Top10,重合度不到七成。这说明选错了度量方式,检索结果会实实在在地跑偏。
后来我养成了一个习惯,做任何向量检索之前先问自己两个问题:我的向量是归一化过的吗?我真正关心的相似性是哪一种?如果Embedding模型输出时已经做了L2归一化,那内积和余弦相似度就是一回事,用哪个都行。如果没有归一化,那就要小心了。这个细节看起来很小,却是我见过最常见的检索效果不达预期的原因之一。
1.2 FAISS的定位、能力边界与典型应用场景
我挺喜欢FAISS的地方在于它“只做一件事”。它是个相似度检索库,不是数据库,不是向量生成工具,也不是完整的检索服务。你给它一批向量,给它一个查询向量,它返回最近的k个。没有花哨的SQL接口,没有分布式调度,没有自动扩缩容。正因为边界清晰,它能把检索这一件事做到极致,单机就能扛住上亿向量的检索,延迟压到毫秒级。Facebook内部最早用它做大规模图像检索,后来开源出来,成了很多向量数据库底层引擎的“心脏”。
当然,我也得说清楚它不擅长什么。它没有原生的持久化服务,不处理并发连接,不保证数据一致性,更不提供多租户隔离。如果你想做的是一个开箱即用的检索云服务,那FAISS只是整块拼图里的一块。我自己的项目里,FAISS通常扮演“计算内核”的角色,外面还要套一层API、一层任务队列、一层元数据管理。有一次我图省事直接把FAISS索引放在进程内存里对外提供查询,结果服务一重启索引就没了,被线上问题结结实实地上了一课。
它典型的用武之地我总结下来有几个。语义搜索是最常见的,把文本、图片、商品都编码成向量,用户搜一句话就能找到语义相近的内容。推荐系统里用它做召回,从百万候选里快速捞出几百个相关物品。去重场景也很好用,比如内容平台判断两篇文章是不是高度相似。还有这两年火起来的RAG,检索增强生成,FAISS经常作为知识库的检索后端,把相关文档片段找出来喂给大模型。这些场景的共同点都是:数据量大、要求快、对召回率有一定容忍度。
1.3 核心对象与流程:Index、向量、ID映射与距离计算
学FAISS我觉得最关键的是搞懂Index这个对象,它是整个库的入口和骨架。你创建什么样的Index,就意味着你选择了什么样的检索算法、什么样的内存开销、什么样的精度。IndexFlatL2是最朴素的一种,暴力比对,逐个算距离,结果百分百准确,但数据量大了之后慢得让人想砸键盘。IndexIVFFlat就属于“分桶”思路,先聚类把向量分到若干簇里,查询时只扫几个最近的簇,速度快了很多,代价是可能漏掉一些结果。不同的Index名字里藏着的字母缩写,基本就是它的算法缩写。
向量进入Index的方式很简单,调add或者add_with_ids。这里有个新手容易忽略的点:FAISS内部用的是自己维护的位置序号,默认从0开始递增。如果你的业务ID是字符串或者不连续的整数,就需要自己维护一张映射表,或者用IndexIDMap包一层。我一开始没注意这事儿,检索回来的是“第27个向量”,还得反查27对应哪个商品,多绕了一道弯。后来统一用IndexIDMap把业务ID直接塞进去,取值的时候就省心多了。
整个检索流程我梳理成一条线:准备向量 → 选择合适的Index → 训练(有些索引需要)→ 添加向量 → 执行search → 拿到距离和ID。search返回两个数组,一个是距离矩阵,一个是ID矩阵,每一行对应一个查询,每一列对应一个邻居。距离的含义取决于你的度量方式,L2距离是越小越好,内积是越大越好。我见过有人把内积检索的结果按升序看,然后抱怨“怎么最相似的排最后”,其实就是没搞清距离的语义。把这几个概念理顺,后面的安装、调参、工程化就有了稳固的地基。
pip install faiss-cpu
conda install -c conda-forge faiss-cpu
quantizer = faiss.IndexFlatL2(d) index = faiss.IndexIVFFlat(quantizer, d, nlist, faiss.METRIC_L2) index.train(xb) # IVFFlat 必须训练 index.add(xb) index.nprobe = 8 D, I = index.search(xq, k)
quantizer = faiss.IndexFlatL2(d) index = faiss.IndexIVFFlat(quantizer, d, nlist, faiss.METRIC_L2) cp = faiss.ClusteringParameters() cp.niter = 25 # k-means 迭代轮数,默认 25 够用 cp.nredo = 3 # 跑 3 次取最优,训练时间翻 3 倍 cp.verbose = True index.cp = cp index.train(xt) # xt 建议取底库的 10%~20%
index = faiss.IndexIVFPQ(quantizer, d, nlist, m, nbits) id_map = faiss.IndexIDMap2(index) id_map.train(xt) id_map.add_with_ids(xb, np.array(biz_ids, dtype=np.int64))