23-Embedding与混合检索

O泡李华 4

为什么语义相似仍然找不到?Embedding、关键词与混合检索

用户问一个产品编号,向量检索返回语义相近但编号不同的文档。语义相似适合处理不同表达,却不天然擅长所有精确条件;关键词和结构化过滤仍然有自己的位置。

1. 向量空间怎样表达相似

余弦、内积与距离不能脱离模型和索引配置讨论。

Embedding 将什么变成向量?

Embedding 模型把文本等输入映射为数值向量,通常让训练目标下相关内容具有某种接近关系。向量各维一般不是可以直接解释的“第一维是 Java,第二维是数据库”。

查询与文档要使用匹配的表示协议。有些模型要求不同查询、文档前缀,或者区分任务类型;只要接口都返回相同维数,就把两种模型混在一起,通常不具备合理比较基础。

相同模型名下的版本、归一化和维数设置也需要记录,不能只保存一个裸数组。

余弦、内积和距离有什么区别?

余弦比较方向,内积同时受方向与长度影响,欧氏距离衡量点之间距离。单位归一化向量的某些排序关系可以等价,但不是任意数据都能互换。

向量库可能返回距离或转换后的分数,不同适配器的范围与排序方向需要核对。相似度 0.8 不是“答案有 80% 概率正确”,阈值也不能跨模型直接复制。

Spring AI SimpleVectorStore 1.1.8 使用余弦计算作为演示实现;生产向量库的索引和过滤行为则要看具体实现。对应源码

一个独立余弦计算示例

使用 Java 17,保存为 CosineDemo.java。人工二维向量只用于公式说明,不是实际 Embedding 输出。

public class CosineDemo {
    static double cosine(double[] left, double[] right) {
        if (left == null || right == null || left.length == 0 || left.length != right.length) {
            throw new IllegalArgumentException("equal non-empty dimensions required");
        }
        double dot = 0, normLeft = 0, normRight = 0;
        for (int i = 0; i < left.length; i++) {
            if (!Double.isFinite(left[i]) || !Double.isFinite(right[i])) {
                throw new IllegalArgumentException("finite values required");
            }
            dot += left[i] * right[i];
            normLeft += left[i] * left[i];
            normRight += right[i] * right[i];
        }
        if (normLeft == 0 || normRight == 0 || !Double.isFinite(normLeft)
                || !Double.isFinite(normRight) || !Double.isFinite(dot)) {
            throw new IllegalArgumentException("invalid vector magnitude");
        }
        return dot / Math.sqrt(normLeft) / Math.sqrt(normRight);
    }

    public static void main(String[] args) {
        double[] query = {1, 0};
        System.out.println("old policy: " + cosine(query, new double[]{0.9, 0.1}));
        System.out.println("current policy: " + cosine(query, new double[]{0.8, -0.2}));
    }
}

由公式推导,旧制度约为 0.994,新制度约为 0.970,因此旧制度排序更靠前。这不表达真实模型质量,只说明业务有效性没有包含在这组向量距离里。

程序检查维度、有限值和零范数;极端数值的稳定处理仍不是它的用途。不能用二维示意程序估算真实索引吞吐或召回率。

哪些条件不能只靠语义表示?

租户、权限、有效版本和明确业务 ID 通常应通过结构化条件限制。用户问订单 12345,语义相关的订单说明不能替代该订单事实。

日期范围、产品型号和否定条件也可能难以仅靠相似度表达。关键词或结构检索可以补充准确约束,再用语义表示召回更广泛的相关内容。

对金额、代码、编号和特定错误码,保留原始字段很重要。清洗时把所有数字去掉,会破坏本应精确匹配的信息。

近似索引为何可能漏掉相关项?

生产向量库常采用近似最近邻索引,以降低查询成本。索引参数、候选预算、过滤方式和数据分布都会影响召回,不是只要存了正确向量就一定进入 topK。

可以用较小数据集上的精确检索作对照,区分 Embedding 表示问题与索引搜索问题。增加索引搜索预算可能改善召回,但也增加耗时与资源,需要一起比较。

metadata 过滤发生在候选搜索前、过程中还是后,也会改变结果数量与性能。授权设计不能仅从统一 Java 接口推断。

更换 Embedding 模型要做什么?

新模型通常需要重新向量化文档,并将查询指向匹配的新索引。即便维数相同,也不能随意混用旧向量和新查询。

保留 embeddingProfile,记录模型版本、维数、预处理、前缀和距离方式。可以构建新索引、完成覆盖检查与评估,再切换读取,保留回滚所需的旧配置。

2. 两条召回路径各擅长什么

精确名词与语义表达可以分路召回,再处理候选。

关键词与向量各擅长什么?

关键词检索强调词项匹配,可以较好保留型号、编号和精确术语;向量检索帮助处理同义表达和语义相关内容。中文分词、领域词典与模型训练范围都会影响实际表现。

问题特征 需要重点保留
错误码、商品型号 原始编号与准确匹配
同义表达 语义相关性
时间、版本、租户 结构化过滤
综合解释 多种召回与完整证据

混合检索不保证一定更好。某一路召回大量无关片段,融合后可能引入噪声;需要在固定问题集上与单路基线比较。

为什么不能直接相加两个原始分数?

BM25 类分数和向量相似度通常不在同一尺度。直接相加,可能让数值较大的一路主导结果,而不是相关性更好的一路。

一种选择是校准和归一化后加权,但需要明确分布和稳定性。另一种是 Reciprocal Rank Fusion,简称 RRF,按每一路的排名贡献分数:某文档的贡献为 1 / (c + rank),缺席该路则贡献零。

c 是平滑参数,rank 从一开始。RRF 不要求原始分数同尺度,但会丢掉原始分数差距,不能当作答案置信度。Elasticsearch RRF 文档

3. RRF 怎样融合排名

用完整程序说明排名融合,避免直接相加不同尺度的原始分数。

一个完整的融合程序

下面 Python 示例使用人工排序,不连接搜索引擎。保存为 rrf_demo.py。

def fuse(rankings, constant=60, limit=10):
    if constant < 0 or limit < 1:
        raise ValueError("invalid parameters")
    scores = {}
    for ranking in rankings:
        seen = set()
        rank = 0
        for document_id in ranking:
            if document_id in seen:
                continue
            seen.add(document_id)
            rank += 1
            scores[document_id] = scores.get(document_id, 0.0) + 1 / (constant + rank)
    return sorted(scores.items(), key=lambda item: (-item[1], item[0]))[:limit]

if __name__ == "__main__":
    dense = ["A", "B", "C"]
    lexical = ["B", "D", "A"]
    for document_id, score in fuse([dense, lexical]):
        print(document_id, round(score, 6))

由公式计算,预期顺序为 B、A、D、C。B 在两路排名都靠前,因此融合分数高于只出现一路的 D。相同分数按 ID 排序,这是示例的确定性约定,不是业务相关性的证明。

去重应使用同一个片段或文档的稳定 ID,不能用正文字符串随意合并。不同版本或不同租户的相同文字,也不应变成同一授权对象。

候选窗口为什么影响结果?

每路只取前五条,排在第六的正确证据就完全没有参与融合。增大候选窗口可以带来更多机会,但也增加网络、排序和后续重排成本。

融合后的 topK 与每路候选数是不同参数。需要保存每路候选及最终排序,才能解释某片段是没有召回,还是融合后被挤出。

两路都必须使用当前身份、有效版本和可访问文档条件。不能向量路受限、关键词路查全库,再希望融合自动消除泄漏。

编号与查询改写要怎样处理?

把 DB-042 改写成“数据库故障”可能丢失精确锚点。可以保留原始查询进行关键词检索,再增加独立的语义改写召回,而不是用改写完全替代原文。

分词器是否保留连字符、大小写与数字,需通过实际索引规则确认。产品名称和错误码可以建立独立字段,使用适合其语义的匹配方式。

查询条件缺少版本或产品线时,可以先澄清,避免召回多个相似对象后由模型自行猜测。

怎样比较混合方案?

固定问题集,分别比较关键词、向量、两路融合,以及融合加重排。按编号类、同义表达类和跨段问题分别查看证据覆盖、引用准确性、耗时与成本。

并行发出两路查询的总延迟不能简单相加,但协调、超时与失败处理仍有成本。某一路失败时,是返回部分结果还是拒绝,需要有明确规则并记录实际使用了哪几路。

本文的排名只来自人工列表和公式,没有宣称混合检索在真实语料上提升了多少。

4. 设计判断与适用边界

把精确过滤留给可证明的条件,把语义表示用于相关性。租户、日期和有效状态不能只写在自然语言查询里,期待向量自然排除。编号多的资料可以加入词法召回,但分词、字段和候选窗口仍要配置与验证。

相似度不是答案置信度。混合检索改善的是候选集合,回答仍需要完整证据。更换 Embedding 时保留版本与索引重建计划,不能把两个空间的向量随意混在同一检索过程。

Contextual Retrieval 讨论为片段补充文档上下文。是否适用仍要通过本项目样本比较,本文没有移用其效果指标。 Anthropic:Introducing Contextual Retrieval。

5. 问答

问:相似度高就是事实可信度高吗?

答:不是,它主要反映向量表示关系,版本、权限和证据支持需另外判断。

问:两个模型维数一样就能混用吗?

答:不能保证处于同一表示空间,需要匹配模型与输入协议。

问:混合检索就是 BM25 分数加余弦分数吗?

答:两种尺度不同,需要校准或采用排名融合等明确方法。

问:RRF 分数能表示回答正确概率吗?

答:不能,它是融合排序信号。

参考资料