FAISS 的 IndexFlatIP 为什么会返回大于 1 的分数
IndexFlatIP 返回内积。需要余弦相似度时,库向量与查询向量都要归一化,检查 float32、零向量及原地修改行为,区分 L2 与近似索引。
向 FAISS 的 IndexFlatIP 加入文本向量后,search 返回了几十甚至几百的分数。若应用期望得到余弦相似度,先检查库中向量和查询向量是否都经过 L2 归一化。
IndexFlatIP 计算内积,本身不会把输入转换成单位向量。未归一化时,向量的长度也影响分数;较长的向量可能得到更大的内积。这里讨论的是希望按余弦相似度检索的场景,业务原本需要最大内积检索时,不应直接改变归一化规则。
库和查询都归一化,再使用内积索引
非零向量的余弦相似度等于内积除以两边的模长。两边都变成单位向量后,内积才等于余弦相似度。只归一化查询,库中向量的长度仍会影响排序;只归一化库,单条查询的排序可以对应余弦排序,但分数还会随查询长度缩放,不能直接拿去与固定余弦阈值比较。
下面是使用 NumPy 和 FAISS Python CPU API 的小型示例。它新建一个内存索引,不修改已有索引文件;数组里的数字只用于说明计算,不代表真实文本 embedding:
import faiss
import numpy as np
xb = np.array([[3, 4], [1, 0], [-3, -4]], dtype="float32")
xq = np.array([[6, 8]], dtype="float32")
def unit_vectors(values):
values = np.array(values, dtype="float32", order="C", copy=True)
if values.ndim != 2 or values.shape[0] == 0:
raise ValueError("expected a nonempty two-dimensional array")
if not np.isfinite(values).all():
raise ValueError("embedding contains NaN or infinity")
norms = np.linalg.norm(values, axis=1)
if not np.isfinite(norms).all() or np.any(norms == 0):
raise ValueError("embedding has an invalid L2 norm")
faiss.normalize_L2(values)
return values
xb_unit = unit_vectors(xb)
xq_unit = unit_vectors(xq)
if xb_unit.shape[1] != xq_unit.shape[1]:
raise ValueError("database and query dimensions differ")
index = faiss.IndexFlatIP(xb_unit.shape[1])
index.add(xb_unit)
scores, ids = index.search(xq_unit, 3)
print(scores)
print(ids)
按这些示例数字,预期分数约为 1、0.6、-1,顺序对应三条库向量。浮点计算会有舍入误差,不能要求所有端点都逐位等于数学值。
这里拒绝零向量,因为它的余弦相似度没有定义。遇到零值、NaN 或无穷大,应先检查 embedding 生成过程,而不是把它们作为普通结果加入索引。非常小或非常大的数值还可能在 float32 中下溢或溢出;示例检查模长,但不能替代上游的数值质量约束。
normalize_L2 会修改输入,返回值不要赋回数组
faiss.normalize_L2 在原数组上修改数值,Python 包装函数没有返回归一化后的数组。下面的写法会让变量变成 None:
vectors = faiss.normalize_L2(vectors)
应单独调用这个函数,需要保留原始向量时先复制。这个返回约定和 Python 列表 sort 原地修改并返回 None 类似,但函数接受的数组类型与内存布局仍按 FAISS 要求处理,不能把列表排序的调用方式直接套过来。
normalize_L2 的输入采用二维、连续的 float32 数组。一条查询也保留形状 (1, d),而不是长度为 d 的一维数组。若出现 float * 类型转换错误,检查 dtype 和布局,不要仅把归一化调用删掉来消除异常。
已加入索引的旧向量需要重新处理
索引已经保存了未归一化的向量时,后来只修改查询代码不够。应从保留的原始 embedding 重新生成归一化向量,并构建新的索引,连同 ID 到文档的映射一起验收。仅修改当初传给 add 的 NumPy 数组,不会追溯修改索引里已保存的数据。
保留原索引和映射文件,在确认结果后切换到新版本;撤回时两者要一起恢复。不要让新索引的第十行对应旧映射中的另一篇文档。
IndexFlatIP 不需要 train。若实际使用 IVF 或 PQ,训练向量、入库向量和查询向量也要采用一致的处理规则,但近似检索会引入召回或分数近似问题。不要把这个精确 Flat 示例的预期分数,直接作为所有压缩索引必须逐位一致的标准。
负数可以是正常的余弦分数
余弦相似度的数学范围是 -1 到 1,不是 0 到 1。业务若需要显示百分比或其他范围,应另行定义映射,不能为了隐藏负数就把检索分数截断,再继续沿用旧阈值。
也要确认实际索引的度量类型。IndexFlatL2 返回平方欧氏距离,越小越近;单位向量之间可用 cosine = 1 - squared_l2 / 2 换算。这个关系要求两边都已归一化,不适用于任意原始向量。
验收时选几条已知库向量与查询,用归一化后的点积核对对应分数,再检查返回 ID、阈值和排序方向。若少量分数仅因舍入略超出端点,可以在展示层处理;若仍是几十或几百,应继续查实际向量、索引文件及调用链,不能用裁剪分数掩盖配置错误。


