DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

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、阈值和排序方向。若少量分数仅因舍入略超出端点,可以在展示层处理;若仍是几十或几百,应继续查实际向量、索引文件及调用链,不能用裁剪分数掩盖配置错误。