本质上都是最近邻检索 Aproxingmate Nearest Neighbor.

ES检索(关键字检索)

通过paddle的实体抽取模型,从Query中抽取实体信息的关键字段,在ES检索的时候进行不同的加权,ES返回匹配的source文档集合,source可以认为是doc向量索引的parent_id。

向量检索

向量查询本质上是将Query向量和collection中的所有向量进行相似度匹配,所以需要确定一个相似度指标,比如欧氏距离、Cosine相似度等。
当向量都在单位球面上(即长度为1)时,直线距离(欧氏距离)越短,夹角(余弦相似度)就越小,两者是严格单调转换关系。两个方法严格成反比。

L2 归一化 (L2 Normalization): 它的作用就是把所有长长短短的向量,全部“缩放”到这个单位球的表面上。无论原来的向量多长,归一化后,它们的模长(Length/Norm)都变成了 1。
把向量投影到单位球面上(L2 归一化),实际上是会损失信息的。我们丢失了向量的“模长,但在现代深度学习(特别是 NLP 和 Embedding 模型)的语境下,“模长”通常代表噪音或无关特征,而“方向”才代表语义。 不能因量忽略质。
归一化后维度不变,数值变了。

一些索引构建方式,需要先对所有节点进行相似度计算,才能决定 谁是邻居,所以相似度算法会影响索引
很多底层向量索引库(如 FAISS 或 Milvus 早期版本)对 L2 距离(欧氏距离) 的优化做得最好,支持的索引类型最全。 余弦相似度本质上是“内积(Inner Product)”。虽然不用归一化,但是其实将 Embedding 向量统一进行 L2 Normalize 是一个标准操作。一旦归一化了,使用内积(Inner Product)就等于余弦相似度,使用 L2 距离也等价于余弦相似度。这给了后端数据库更大的灵活性。

L2欧式距离向量相似度计算

  • Cosine 相似度: 衡量的是两个向量的夹角。夹角越小,越相似(Cosine 值越大,接近 1)。
  • 欧氏距离: 衡量的是两点之间直线连线(弦)的长度

余弦向量相似度计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
 * 计算两个向量的余弦相似度
 */
public double cosineSimilarity(float[] vec1, float[] vec2) {
    double dotProduct = 0.0;
    double norm1 = 0.0;
    double norm2 = 0.0;
    for (int i = 0; i < vec1.length; i++) {
        dotProduct += vec1[i] * vec2[i];
        norm1 += vec1[i] * vec1[i];
        norm2 += vec2[i] * vec2[i];
    }
    return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
// 示例:
// 问题向量: [0.1, 0.2, 0.3, ...]
// 文档向量: [0.15, 0.18, 0.32, ...]
// 相似度 = cos(θ) = 0.87  (越接近1越相似)

稀疏检索TF-IDF BM25

![[Pasted image 20260311190635.png]]
然而,TF-IDF 存在两个明显的问题:它没有考虑文档的长度(长文档天然地具有更高的词频),并且词频的增长是线性的(一个词出现10次的重要性真的是出现5次的2倍吗?)。

为了解决这些问题,BM25 (BestMatching 25)算法应运而生。BM25 引入了两个关键的调节参数:k1和 b。
k1 参数用于控制词频的”饱和度”,即随着词频的增加,其对总分数的贡献会逐渐趋于平缓,解决了词频线性增长的问题。b 参数则用于控制文档长度的归一化程度,使得模型能够更公平地处理不同长度的文档。

稠密检索

但是还是没法处理一词多义
所以上下文感知嵌入模型产生:BERT BGE-M3

复杂架构

“漏斗式(Funnel)”混合检索架构

先ES前置检索初筛出子切片,再用3种方向的模型进行Milvus精筛,大大减少了计算量,也几乎杜绝了“跨文档误召回”(比如搜 A 合同的条款,结果搜出了 B 合同的内容)。
对source集合内的每个子doc进行更精确的向量检索匹配,我们选取了3个异质化的Embedding模型,构建了三个collection集合,在这三个collection上并行检索topk,最后再通过RRF算法进行融合截断。

RRF (Reciprocal Rank Fusion): 倒数排名融合算法

不看具体的相似度分数(因为不同模型的分数分布不同,0.8 和 0.6 没法直接比),它只看排名。如果一个文档在三个模型里都排前三,那它一定是最终的 No.1。

混合检索策略

所谓的混合检索是靠多种Embedding+索引方式实现的。
混合检索 = 路 A (稠密) + 路 B (稀疏) -> 结果融合。 它是在物理层面上维护了两套完全不同的索引结构,然后在查询层面上进行了合并。加权合并、RRF融合。
RAGFlow 支持向量检索 + 关键词检索的混合模式:

1
2
3
4
5
6
7
requestBody.put("vector_similarity_weight", 0.7);  // 向量检索权重70%
requestBody.put("keyword", false);                 // 禁用纯关键词搜索
最终得分计算:
FinalScore = VectorScore * 0.7 + KeywordScore * 0.3
其中:
- VectorScore: 余弦相似度 (0-1)
- KeywordScore: BM25得分归一化 (0-1)

重排序 (Reranker)

跨编码器。
这与我们在检索阶段使用的”双编码器(Bi-Encoder)”形成了鲜明对比。双编码器为査询和文档独立生成向量然后通过简单的向量运算(如点积)来计算相似度,速度极快,适合从海量数据中进行初步筛选。

而跨编码器则将査询和每个候选文档拼接在一起,作为一个整体输入给一个更强大的 Transformer 模型(如项目 retrieval-pipeline 中使用的 BAAl/bge-reranker-v2-m3),让模型在深层次上对两者之间的每一个词进行交互式、细粒度的相关性判断。这种“共同关注”的机制,使得跨编码器能够捕捉到双编码器无法感知的.更细微的语义关联,从而输出一个远比任何单一检索方法都更准确、更相关的最终排序结果。

使用 bge-reranker-v2-m3 对初步检索结果进行重排序:
Reranker 工作原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

输入: (问题, 文档) 配对
模型: Cross-Encoder (交叉编码器)
处理过程:
┌────────────────────────────────────────┐
│ 1. 拼接问题和文档                      │
│   "[CLS] 问题文本 [SEP] 文档文本 [SEP]"│
└────────────────┬───────────────────────┘
                 │
                 ▼
┌────────────────────────────────────────┐
│ 2. Transformer 编码                    │
│   深度交互理解问题和文档的关联         │
└────────────────┬───────────────────────┘
                 │
                 ▼
┌────────────────────────────────────────┐
│ 3. 分类层输出相关性分数                │
│   Score ∈ [0, 1]                       │
└────────────────────────────────────────┘

re-ranking:检索回来10条结果,怎么用BGE-Reranker模型把最相关的排到前面?(这点非常加分) 向量库为了速度,用的是ANN(近似最近邻),召回的东西里经常混着垃圾。直接把这些垃圾丢给大模型,大模型就会产生幻觉。用一个专门的 Cross-Encoder 模型(如 BGE-Reranker)Cross-Encoder架构,就是把query和每个候选doc组成一个pair对,对每个pair进行相似性打分,取一个小的topk进行截断

一个形象的比喻:招聘流程

假设你要招一个“懂 Rust 的后端工程师”。

  1. 混合检索(HR 筛简历):
    • HR 看简历(倒排索引):包含 “Rust”、”后端” 关键词的留下。
    • HR 看眼缘(向量索引):经历看起来像是个高手的留下。
    • 结果: 选出了 50 份简历。这里面可能混进去了“做 Rust 游戏开发的前端”(关键词中了,但方向不对)。
  2. 重排序(技术总监面试):
    • 总监(Cross-Encoder)把这 50 个人叫来,面对面聊(深度交互)。
    • 总监发现:“这个人虽然简历写了 Rust,但其实只会 Hello World,淘汰。”,“那个人虽然简历没写 Rust,但他 C++ 极强,学 Rust 只要两天,排第一。”
    • 结果: 选出最终 3 个 Offer 人选。

召回重点是在topk中能把真实相关的doc给捞出来,而重排则需要把真实相关的doc排在前面,同时减少topk的数量。