❝做 RAG 系统,十个团队九个栽在检索上。本文把语义检索、关键词检索、混合检索、Rerank 重排序一次讲清楚。 ❞
❝「生产级 RAG 必须用混合检索。单一检索方式,无论是语义还是关键词,都有致命盲区。」 ❞
下面展开讲为什么。
RAG(Retrieval-Augmented Generation)的核心流程是:「先搜,再答」。
检索层的质量,直接决定了 RAG 系统的上限。
❝「Garbage In, Garbage Out.」 ❞
目前主流有三种检索方式:
假设用户问了一句话:「"Transformer 模型的注意力机制是什么"」
用户 Query
│
├── ① 语义检索(理解你想问啥)
│ Query → Embedding 向量 → 向量数据库搜索
│ 能找到:"自注意力机制通过 Q/K/V 实现序列内部关联"
│ 可能漏掉:包含 "multi-head attention" 但语义向量偏远的文档
│
├── ② 关键词检索(一字不差地匹配)
│ Query → 分词 → 关键词匹配
│ 能找到:包含 "Transformer"、"注意力" 等关键词的文档
│ 可能漏掉:"自注意力机制让模型学会了序列中各位置的关联"
│
└── ③ 混合检索 = ① + ② + 融合排序
同时跑两路,合并结果
兼顾语义理解和精确匹配 ← 这才是正解
看个更直观的对比:
场景 | 纯语义 | 纯关键词 | 混合检索 |
|---|---|---|---|
「同义词理解」"提升代码质量" → "提高程序可维护性" | 支持 | 不支持 | 支持 |
「精确术语」"BGE-M3 模型" | 不支持 | 支持 | 支持 |
「产品型号/错误码」"Error 0x80070005" | 不支持 | 支持 | 支持 |
「长尾知识/罕见词」 | 不支持 | 支持 | 支持 |
「多语言混合」"Golang 内存泄漏排查" | 支持 | 部分支持 | 支持 |
只有混合检索在所有场景下都能覆盖。
文本 → Embedding 模型 → 稠密向量(1024 维浮点数组) → 向量数据库 ANN 搜索
简单说就是:把文字变成一串数字,然后用"数字之间的距离"来衡量语义相不相近。
"机器学习是人工智能的一个子领域"
→ [0.23, -0.45, 0.67, 0.12, ..., -0.01] (1024 维,每维都有值)
特点:
• 维度固定(768 / 1024 / 1536),取决于模型
• 几乎每一维都非零 → 所以叫"稠密"
• 捕捉的是语义:同义词、上下位关系都能 handle
关键词检索不是只有一种做法,它有「两条完全不同的路线」。很多人搞混了,所以我重点展开讲。
关键词检索
│
┌────────────┴────────────┐
▼ ▼
路线 A:稀疏向量 路线 B:全文索引 + 分词
(BM25 in Milvus) (jieba + Qdrant/ES)
│ │
把关键词检索 用经典的
"向量化",统一到 倒排索引
向量检索框架中 直接做关键词匹配
两条路线「都能实现关键词检索」,但实现机制、能力边界、适用场景完全不同。
把文本用 BM25 或学习型模型(SPLADE、BGE-M3 Sparse)编码成一个「超高维但绝大部分维度为 0」 的向量,然后存进向量数据库,用内积搜索来匹配。
文本 → 分词 → BM25 算法 → 稀疏向量(大部分维度为 0)
↓
Milvus SparseFloatVector 字段
↓
查询时也转成稀疏向量,用内积(IP)匹配
词汇表 = {猫:0, 狗:1, 吃:2, 鱼:3, 睡觉:4, ...} (假设 30000 个词)
"猫吃鱼" → {0: 1.2, 2: 0.8, 3: 1.5} // 30000 维,只有 3 个维度非零
每个非零维度的值 = 该词的 BM25 权重(综合了词频、逆文档频率、文档长度等因素)。
方法 | 说明 | 特点 |
|---|---|---|
「BM25 统计」 | 基于语料统计算词权重 | 简单、可解释 |
「SPLADE」 | 学习型稀疏编码模型 | 效果更好,需要推理服务 |
「BGE-M3 Sparse」 | BGE-M3 的稀疏输出 | 一个模型同时出稠密 + 稀疏 |
// 定义 Collection Schema
schema := &entity.Schema{
CollectionName: "chunks",
Fields: []*entity.Field{
{Name: "id", DataType: entity.FieldTypeVarChar, PrimaryKey: true},
{Name: "dense_vector", DataType: entity.FieldTypeFloatVector, Dim: 1024},
{Name: "sparse_vector", DataType: entity.FieldTypeSparseVector}, // 稀疏向量
{Name: "content", DataType: entity.FieldTypeVarChar},
},
}
一个 Collection 里同时放稠密和稀疏两种向量,混合检索一次 API 搞定。
用经典的「倒排索引(Inverted Index)」:先把文本拿分词器拆成一个个词,然后建"词 → 文档列表"的反向映射。查询时也拆词,直接查映射表。
文本 → jieba 分词 → 建倒排索引
↓
词项 → [文档ID + 位置 + 词频]
↓
查询时分词 → 查倒排索引 → BM25 打分
文档1: "猫吃鱼" → ["猫", "吃", "鱼"]
文档3: "狗吃骨头" → ["狗", "吃", "骨头"]
文档5: "猫睡觉" → ["猫", "睡觉"]
倒排索引:
"猫" → [{doc1, pos=[0]}, {doc5, pos=[0]}]
"吃" → [{doc1, pos=[1]}, {doc3, pos=[1]}]
"鱼" → [{doc1, pos=[2]}]
"狗" → [{doc3, pos=[0]}]
查询 "猫吃鱼" → 分词 ["猫","吃","鱼"] → doc1 命中 3 个词 → 最相关
「jieba 分词」在中文 RAG 场景中是最关键的环节之一,它直接决定了索引质量:
# 默认分词——可能出问题
jieba.cut("深度学习是人工智能的核心技术")
→ ["深度", "学习", "是", "人工智能", "的", "核心", "技术"]
# ↑ "深度学习" 被拆开了!
# 加自定义词典——效果立竿见影
jieba.add_word("深度学习", freq=10000)
jieba.add_word("ChatGPT", freq=10000)
jieba.add_word("BGE-M3", freq=10000)
jieba.cut("深度学习是人工智能的核心技术")
→ ["深度学习", "是", "人工智能", "的", "核心", "技术"]
# ↑ 完美保持整词
{
"synonym_filter": {
"type": "synonym",
"synonyms": [
"LLM, 大语言模型, 大模型",
"RAG, 检索增强生成",
"Embedding, 嵌入, 向量化"
]
}
}
搜 "大模型" 时自动匹配 "LLM" 和 "大语言模型",召回率显著提升。
引擎 | 全文检索实现 | 分词支持 |
|---|---|---|
「Elasticsearch」 | 原生全文搜索,最成熟 | 内置 IK 分词、jieba 插件 |
「Qdrant」 | Multilingual 全文索引 | 内置多语言分词 |
「PostgreSQL」 | ParadeDB @@@ 全文匹配 | pg_jieba 插件 |
这是大家最关心的问题,做了一张详细对比表:
维度 | 稀疏向量(Milvus) | 全文索引(jieba + ES/Qdrant) |
|---|---|---|
「存储格式」 | 高维稀疏浮点向量 | 倒排索引 |
「和语义检索的关系」 | 和稠密向量在「同一系统」 | 可能在「不同系统」 |
「打分算法」 | 向量内积,近似 BM25 | 原生 BM25 / TF-IDF |
「混合检索」 | 一次调用搞定 | 两次调用 + 结果合并 |
「分词控制」 | 黑盒,不可自定义 | jieba 自定义词典 + 同义词 |
「精确匹配」 | 较弱 | 很强 |
「查询能力」 | 只有相似度搜索 | 布尔、短语、通配符、正则... |
「运维复杂度」 | 低(一个系统) | 中~高 |
这是两条路线「最本质的差异」。
「稀疏向量」——黑盒分词:
无法控制 "深度学习" 是拆成两词还是保持整词
无法添加业务专有术语
无法配置同义词
→ 适合通用场景
「全文索引」——白盒分词:
自定义词典: jieba.add_word("深度学习")
专有名词: jieba.add_word("ChatGPT")
同义词扩展: "LLM" ↔ "大语言模型"
停用词控制: 过滤 "的"、"是"、"了"
→ 适合中文和专业领域
❝「划重点:如果你做的是中文 RAG,自定义词典和同义词扩展几乎是刚需,这时候全文索引方案有明显优势。」 ❞
「稀疏向量」只能做相似度搜索,给你一个排好序的结果列表,没了。
「全文索引」支持丰富的查询语法:
# 精确短语
"机器学习" → 必须连续出现
# 布尔组合
(Transformer OR BERT) AND 预训练 NOT GPT-2
# 通配符
deep* → deeplearning, deepfake, ...
# 模糊匹配
machne~1 → machine(允许 1 个编辑距离)
# 高亮
搜索 "注意力机制" → 返回 <em>注意力机制</em>
「选稀疏向量」:
「选全文索引」:
一次 API 调用,数据库内部同时搜两种向量,自动 RRF 融合。
searchRequests := []*milvus.ANNSearchRequest{
// 稠密向量搜索(语义)
milvus.NewANNSearchRequest("dense_vector", "COSINE", denseQuery, topK),
// 稀疏向量搜索(关键词)
milvus.NewANNSearchRequest("sparse_vector", "IP", sparseQuery, topK),
}
// Milvus 内部完成 RRF 融合
results, _ := client.HybridSearch(ctx, collectionName, searchRequests,
milvus.NewRRFRanker(60), topK)
「一句话评价」:简单省事,但分词不可控。
两次独立调用 + 应用层融合。
// 第一步:向量检索
vectorResults := qdrantClient.Search(ctx, &qdrant.SearchPoints{
CollectionName: collection,
Vector: queryEmbedding,
Limit: uint64(topK),
})
// 第二步:全文检索
textResults := qdrantClient.Query(ctx, &qdrant.QueryPoints{
CollectionName: collection,
Query: qdrant.NewQueryText("搜索关键词"),
})
// 第三步:合并去重 + Rerank
mergedResults := mergeAndDedup(vectorResults, textResults)
finalResults := reranker.Rerank(query, mergedResults)
「一句话评价」:灵活强大,但需要多走一步。
最常用的是 「RRF(倒数排名融合)」,简单又有效:
公式: RRF_score(d) = Σ 1/(k + rank_i(d)) k = 60
举个例子:
文档 X: 语义排第 1, 关键词排第 5
→ RRF = 1/61 + 1/65 = 0.03177
文档 Y: 语义排第 3, 关键词排第 2
→ RRF = 1/63 + 1/62 = 0.03200
→ Y 排前面(两边都靠前 > 一边极前一边靠后)
RRF 的妙处在于:「只看排名,不看分数」。所以不用操心两路检索分数量纲不同的问题。
混合检索的第一阶段(召回)追求的是「快」和「全」,精度是有限的。Rerank 用更精确的模型做"精排":
Embedding 模型(Bi-Encoder):
分别编码 Query 和 Chunk → 独立向量 → 快,但精度有限
Reranker(Cross-Encoder):
同时编码 Query + Chunk → 联合理解 → 慢,但精度高得多
打个比方:「召回是海选,Rerank 是终面。」
模型 | 特点 |
|---|---|
「BGE-Reranker-v2-m3」 | 开源,多语言,中文友好 |
「Cohere Rerank」 | 商业 API,效果好,易集成 |
「bce-reranker-base_v1」 | 中英双语,轻量级 |
混合检索取 Top 20~50 → Rerank 精排 → 输出 Top 5
关键参数:
• 召回数量: 最终要 N 条,先召回 4N 条
• 分数阈值: 过滤 Rerank 分数太低的结果
• 降级策略: Rerank 挂了就退回原始排序,保证可用性
完整代码示例:
func (s *SearchService) HybridSearchWithRerank(
ctx context.Context,
knowledgeBaseID string,
query string,
topK int,
) ([]*SearchResult, error) {
denseVec, err := s.embedder.EmbedDense(ctx, query)
if err != nil {
returnnil, fmt.Errorf("embed dense: %w", err)
}
sparseVec, err := s.embedder.EmbedSparse(ctx, query)
if err != nil {
returnnil, fmt.Errorf("embed sparse: %w", err)
}
// 4 倍候选量,留给 Rerank 筛选
candidates, err := s.vectorRepo.HybridSearch(
ctx, knowledgeBaseID, denseVec, sparseVec, topK*4,
)
if err != nil {
returnnil, fmt.Errorf("hybrid search: %w", err)
}
reranked, err := s.reranker.Rerank(ctx, query, candidates, topK)
if err != nil {
return candidates[:topK], nil// 降级:Rerank 挂了就用原始结果
}
return reranked, nil
}
┌─────────────────────────────────┐
│ Milvus │
│ ┌──────────┐ ┌──────────┐ │
│ │ Dense Vec│ │Sparse Vec│ │
│ │ (语义) │ │ (BM25) │ │
│ └──────────┘ └──────────┘ │
│ HybridSearch + RRF │
└─────────────────────────────────┘
优点:架构最简,一个库搞定;混合检索一次调用 不足:分词不可控;无复杂查询
┌───────────────┐ ┌───────────────┐
│ Milvus │ │Elasticsearch │
│ (语义检索) │ │ (关键词) │
└───────┬───────┘ └───────┬───────┘
└────────┬───────────┘
▼
应用层 RRF 融合 → Rerank
优点:各取所长;分词可控;复杂查询 不足:两套系统;需要自己写融合
┌─────────────────────────────────┐
│ Qdrant / ES v8 / PostgreSQL │
│ ┌──────────┐ ┌──────────┐ │
│ │ Vector │ │ 全文索引 │ │
│ │ (语义) │ │ (关键词) │ │
│ └──────────┘ └──────────┘ │
│ 单引擎覆盖两种检索模式 │
└─────────────────────────────────┘
优点:单引擎双模;运维简单;分词可控 不足:超大规模下不如专业向量库
你的 RAG 项目需要什么?
│
├── 快速上线 + 数据 < 1000万 + 通用领域
│ → 方案 A:Milvus 单引擎
│
├── 中文场景 + 需要自定义词典 + 精确匹配
│ │
│ ├── 数据 > 5000万,性能要求高
│ │ → 方案 B:Milvus 语义 + ES 关键词
│ │
│ └── 数据量适中,运维简单优先
│ → 方案 C:Qdrant / ES v8 单引擎
│
├── 多租户 SaaS + 不同客户不同需求
│ → 方案 C:全能引擎 + 按需组合
│
└── 已有 PostgreSQL + 不想引入新组件
→ 方案 C:pgvector + ParadeDB
方案 AMilvus | 方案 B双引擎 | 方案 C全能引擎 | |
|---|---|---|---|
「哲学」 | 一切皆向量 | 术业有专攻 | 每个引擎都全能 |
「运维」 | 简单 | 复杂 | 中等 |
「分词」 | 不可控 | 可控 | 可控 |
「精确匹配」 | 弱 | 强 | 强 |
「超大规模」 | 最佳 | 最佳 | 中等 |
「代表」 | Dify (Milvus) | 自研大系统 | Qdrant / ES v8 |
术语 | 解释 |
|---|---|
「RAG」 | 检索增强生成,先搜再答 |
「ANN」 | 近似最近邻搜索,用少量精度换速度 |
「BM25」 | 经典关键词检索算法 |
「RRF」 | 倒数排名融合,多路结果合并算法 |
「SPLADE」 | 学习型稀疏编码模型 |
「Bi-Encoder」 | 分别编码 Query 和 Doc(Embedding 模型) |
「Cross-Encoder」 | 同时编码 Query + Doc(Reranker) |
「倒排索引」 | 从词到文档列表的映射 |
「jieba」 | 中文分词库,支持自定义词典 |
「Dense Vector」 | 稠密向量,编码语义 |
「Sparse Vector」 | 稀疏向量,编码关键词权重 |
「Rerank」 | 重排序,对召回结果精排 |
「pgvector」 | PostgreSQL 向量搜索扩展 |
「ParadeDB」 | PostgreSQL 全文搜索扩展 |
「HNSW」 | 多层级近邻图索引 |
RAG 的检索层看似简单,但真正做好需要理解: