跳到正文
Elaine Blog
返回

文本为什么能被搜索:Embedding、相似度与向量索引

更新于:
RAG 从入门到 Agentic RAG

用户问“邮费谁出”,文档写“退货运输费用由商家承担”。关键词不完全相同,人却能理解它们在谈同一件事。文本 Embedding 试图让这种关系体现在向量空间里:问题与适合回答它的文本,能够通过数值计算被匹配。

但向量不是把一句话变成“意义编号”。一个表示要服务于训练目标,同义改写、问题与答案、产品编号匹配,也不完全是同一种关系。本篇先看表示怎样形成,再看怎样在大量表示中寻找候选。

从 Token 向量到一个片段向量

Token Embedding 是模型内部对 Token ID 的初始表示。Transformer 处理上下文后,每个位置的向量还会受到其他位置影响。检索通常需要一个固定长度表示,所以文本编码器还要把这些位置汇总。

平均池化可写为 v = Σ(mᵢhᵢ) / Σmᵢ。hᵢ 是第 i 个位置的上下文向量,mᵢ 对有效位置取 1,对填充位置取 0。两个有效位置为 [1,3] 和 [3,1] 时,平均是 [2,2];若把补齐用的 [0,0] 也算进去,会变成 [1.33,1.33]。这说明 mask 的作用,不意味着真实模型的填充表示一定是零。

其他模型可能用特定位置、最后有效位置或不同汇总方式。不能任取一个语言模型,把 Token 向量平均后就宣称检索质量有保证。汇总只是结构,训练才让表示适合比较。

第一篇采用的多语言 MiniLM 输出 384 维表示,并有自己的输入长度限制。长条款超过编码器窗口时,尾部条件可能没有进入向量;生成模型能读长文本并不能补救编码阶段的截断。应按实际 Tokenizer 检查输入,模型结构以模型卡为准。

训练怎样告诉模型什么叫相关

给问题 q 配上正例 d⁺ 和若干负例 d⁻,可以鼓励正例得分高于负例。一个简化目标是 L = -log(exp(s(q,d⁺)/τ) / Σⱼ exp(s(q,dⱼ)/τ)),其中 s 是相似度,τ 是温度,分母包含正例和负例。正例在候选中分配到的概率越低,损失越大。

假设除以温度后的三个分数为 [2,1,0],正例是第一个。softmax 后其概率约 0.665,损失约 0.408。如果正例和负例都差不多高,目标就鼓励训练继续区分它们。这是教学损失计算,不是完整训练方案。Sentence Transformers 的损失说明列出了不同数据形式对应的目标。

对“P-204 电池退货”来说,发票说明是容易的负例,P-205 电池退货则是难负例。后者文本几乎相同,正好逼模型关注产品差异。但难负例必须真的不相关:通用政策可能同时适用于两个产品,误标会教模型排斥本来有用的资料。

同一批其他问题的正例也可作为当前问题的候选负例,这称为批内负样本。它能提高数据利用率,却仍可能遇到多个问题共享答案的情况。模型训练和领域适配需要正确标签,不能把所有没点开的文档都当错误答案。

问题通常短,文档通常长,两边语用也不同。双编码器可分别处理查询和文档,有些共享参数,有些采用不同配置或任务前缀。使用时必须遵循模型要求的 query、passage 前缀或专用编码接口,不能凭习惯统一加上自己喜欢的标签。

相似度到底算的是什么

点积是 q·d = Σ qᵢdᵢ,同时受方向和模长影响。余弦是 q·d / (||q|| ||d||),消除了模长,只比较方向。欧氏距离是两点间直线距离,通常越小越近。

q=[1,0],A=[0.8,0.6],B=[8,6]。A 与 B 同方向,余弦都为 0.8;点积却分别为 0.8 和 8。若训练使用点积并让模长承载信息,事后归一化可能改变排序,所以应按模型和索引约定选择度量。

当两边都归一化为单位向量时,||q-d||² = 2 - 2(q·d)。这时最大余弦、最大点积和最小欧氏距离得到等价排序。条件是双方都规范化,不能只规范化问题就套结论。

"""用二维教学向量观察点积、归一化与距离;这些数值不是文本模型输出。"""
import math

def dot(a, b):
    # zip 会忽略较长一侧的尾部,所以比较前显式检查维度。
    if len(a) != len(b):
        raise ValueError("向量维度不一致")
    return sum(x * y for x, y in zip(a, b))

def normalize(vector):
    length = math.sqrt(dot(vector, vector))
    if length == 0:
        raise ValueError("零向量没有可定义的余弦方向")
    return [value / length for value in vector]

query = [1.0, 0.0]
candidates = {"A": [0.8, 0.6], "B": [8.0, 6.0], "C": [0.0, 1.0]}
for name, vector in candidates.items():
    cosine = dot(normalize(query), normalize(vector))
    distance = math.sqrt(sum((x-y)**2 for x,y in zip(query, vector)))
    # A 与 B 的方向相同,余弦相同;原始点积与欧氏距离并不相同。
    print(name, {"dot": dot(query, vector), "cosine": cosine, "distance": distance})

# 简化的三候选对比目标:索引 0 是正例,另两个是负例。
# 固定 logits 只是展示损失,不执行梯度更新,不代表训练效果。
logits = [2.0, 1.0, 0.0]
weights = [math.exp(x - max(logits)) for x in logits]
p_positive = weights[0] / sum(weights)
print("正例概率与损失:", p_positive, -math.log(p_positive))

完整程序见 vector_geometry.py下载。手工向量只让公式可见,真实语义实验仍用第一篇的编码脚本。相似度不是事实正确率,也没有跨模型通用的“0.8 就可靠”。三十天政策与四十天问题可以高度相关,因为它帮助判断期限不满足。

文档多了以后,逐条比较为什么会慢

N 个 D 维文档向量,与一个问题精确比较约需 N×D 次分量计算。十万条、384 维就是约 3840 万次乘加量级;实际延迟还受批处理、内存带宽和硬件影响,不能只用这个数推算毫秒。

仅保存一百万条 768 维 float32 向量,就需要 1,000,000×768×4 字节,约 3.07 GB 十进制空间;原文、元数据、索引结构和运行缓冲另外占用。精确搜索提供很好的对照基线,但规模和并发增长后,可能需要减少比较范围。

近似最近邻搜索允许少量候选遗漏,换取更低延迟或存储。应把近似索引结果与同一向量空间的精确结果比较,才能区分是表示没学好,还是索引搜索没找到。

HNSW 怎样沿着图靠近目标

可以把每个向量看作一个节点,边连接邻近节点。HNSW 在多个层次组织这张图:高层稀疏,帮助快速跨过大范围;低层更密,进一步搜索附近候选。查询不是访问每个向量,而是在图上比较并移动。HNSW 原论文介绍了分层可导航图的构造与搜索。

想象从城市远处寻找一家店,先走主干道接近街区,再进入小路寻找门牌。这个类比只解释粗到细的导航,不代表向量必须具有二维地理布局。实际高维空间的邻接关系由距离计算建立。

搜索保留的候选越少,越容易错过需要绕一步才能到达的更近节点。通常需要结合连接数量、建图搜索范围与查询搜索范围调整;增加范围会消耗内存或时间。索引参数不是越大越好,应通过召回与延迟曲线选择。

常见实现把连接数量相关参数称为 M,把建图候选范围称为 efConstruction,把查询候选范围称为 efSearch。它们分别影响图的连通性、建图质量和查询时愿意探索的范围,具体含义与限制仍以所用库为准。查询参数不会重建已经形成的图,建图参数也不会自动让每次查询访问所有节点。

假设某次查询从 A 出发,邻居 B 看起来更近,但真正最近的 D 需要经过另一个候选 C 才能到达。只保留单一路径可能过早停在 B;保留更多候选才有机会继续探索 C。这个小例子说明近似搜索为什么会漏结果,不是说每次提高 efSearch 都一定改善同一条问题。

IVF 和量化怎样减少比较与存储

IVF 先用聚类中心把空间分成若干倒排桶,查询只搜索选中的几个桶。粗略说,若十万条向量均匀分到一百桶,搜索五桶约比较五千条;现实桶大小不均,实际工作量会不同。靠近桶边界的相关向量可能落在没搜索的桶中,增加探测桶数能缓解这个问题。

可以用一维数值看桶边界问题:两个中心是 0 和 10,文档向量 A=0、B=5.1 分别进入两桶,查询 q=4.9 更靠近中心 0。如果只查这一桶,返回 A 的距离为 4.9,却错过另一桶里距离只有 0.2 的 B。查两个桶就能恢复这条结果,但访问量也增加。真实 IVF 在高维空间用训练得到的中心,原理同样是先选桶再比较。

量化把高精度表示压成更紧凑的编码。标量量化可把每维映射到较少离散值;乘积量化则把向量拆成子空间,用各子空间的码本编号表示。它减少存储,却引入近似误差;把两个原本很接近的候选顺序颠倒并不奇怪。

例如一维数值 0.37 按 0.1 步长近似为 0.4,误差为 0.03。高维量化不是把每个数简单保留一位小数,但同样要评价距离估计误差。也可以先用压缩表示找候选,再用原向量重算,代价是需要保留或取回原表示。

Faiss 提供精确、倒排、图与量化等索引类别,具体实现和组合见索引说明。它们是不同取舍,不是一条从低级到高级的升级路线。

向量数据库比一个索引多做什么

索引主要解决搜索,数据库还涉及元数据过滤、持久化、并发、更新、删除与运维。选择时先明确知识量、更新频率、授权过滤和延迟目标,再判断内存精确搜索、嵌入式索引或外部数据库哪种合适。

模型更换、切分变更和向量归一化规则变更都可能要求重建。不能将新查询向量与旧模型文档向量混用,也不能把能执行矩阵乘法当成语义空间一致。第九篇会说明如何进行版本切换。

现在我们能解释文本怎样进入搜索空间,也知道搜索可能在哪里丢候选。下一篇把它与关键词、混合检索和重排结合,处理语义接近但业务标识不同的问题。


分享这篇文章:

上一篇
文档怎样变成知识库:解析、切分与来源保留
下一篇
为什么找不准:关键词、混合检索与重排