### [RAG 中的混合检索是什么?为什么要用混合检索而不是纯向量检索?](https://www.isearch.life/article/678)
**Published:** 2026-06-18T07:10:37
**Author:** tearell
**Excerpt:** 深度解析 一、为什么纯向量检索不够用? 向量检索的原理是把文本通过 Embedding 模型编码成高维向量,然后用余弦相似度或内积来衡量语义相关性。这对 “理解意思” 很在行,但它有一个致命弱点:对精确关键词的匹配能力很弱。 举个实际的例子
## 深度解析
### 一、为什么纯向量检索不够用?
向量检索的原理是把文本通过 Embedding 模型编码成高维向量,然后用余弦相似度或内积来衡量语义相关性。这对 “理解意思” 很在行,但它有一个致命弱点:**对精确关键词的匹配能力很弱**。
举个实际的例子。假设你的知识库里有这样一段文档:
> “
>
> `Spring AI` 整合 `Milvus` 向量数据库时,需要在 `pom.xml` 中引入 `spring-ai-milvus-store` 依赖,版本号 `1.0.0-M4`。
用户提问:”`spring-ai-milvus-store` 的版本号是多少?”
向量检索可能会返回一堆跟 `Spring AI`、`Milvus`、依赖配置语义相关的文档,但精确包含 `spring-ai-milvus-store` 这个 artifactId 的那段文档,排名可能反而靠后——因为 Embedding 模型把这段文本编码后,”精确匹配 artifactId” 这个信号被稀释到整个语义空间里了。
而 BM25 关键词检索呢?它看到 `spring-ai-milvus-store` 这个精确 token,直接就能命中目标文档,简单粗暴但有效。
反过来,用户问:”`Spring AI` 怎么连接向量数据库?”BM25 可能搜不到好结果,因为文档里写的是 “整合 `Milvus`“,没有 “连接” 这个词。但向量检索能理解 “连接” 和 “整合” 在这个语境下是一个意思。
**这就是为什么需要混合检索——两种检索方式是互补的,不是替代关系。**
### 二、混合检索的整体架构

RAG 混合检索 Hybrid Search 架构图
RAG 混合检索 Hybrid Search 架构图
上图是混合检索的完整流程。核心就三步:
- **两路并行检索**:用户查询同时进入稠密检索通道和稀疏检索通道,各自返回 Top-K 个结果(通常 K=10~20)
- **结果融合**:用 RRF(Reciprocal Rank Fusion)或加权分数融合,把两路结果合并成统一的排序
- **重排序(可选但推荐)**:用 Cross-Encoder 模型对融合后的结果做精排,进一步提升相关性
### 三、RRF 融合算法——混合检索的核心魔法
混合检索最关键的步骤是**怎么把两路结果合并**。你不能简单地把向量检索的分数和 BM25 的分数相加——因为它们的分数量纲完全不同(向量相似度是 0~1 的余弦值,BM25 是基于词频的评分,可能到几十甚至上百)。
RRF(Reciprocal Rank Fusion)巧妙地避开了分数归一化的问题,它只看**排名位置**,不看原始分数:
```
RRF_score(d) = Σ 1 / (k + rank_i(d))
```
- `rank_i(d)`:文档 d 在第 i 路检索结果中的排名
- `k`:平滑常数,通常取 60
举个直观的例子:假设文档 A 在向量检索中排第 1 名,在 BM25 中排第 5 名:
```
RRF_score(A) = 1/(60+1) + 1/(60+5) = 1/61 + 1/65 ≈ 0.031
```
文档 B 在向量检索中排第 3 名,在 BM25 中排第 1 名:
```
RRF_score(B) = 1/(60+3) + 1/(60+1) = 1/63 + 1/61 ≈ 0.031
```
RRF 的设计哲学是:**两种检索方式都给高排名的文档,最终得分就高**。即使某一路的分数偏低,只要另一路排名靠前,文档仍然能获得不错的最终分数。这个算法最早由 Cormack, Clarke & Büttcher 在 2009 年提出,到今天已经成为 Elasticsearch、Azure AI Search、OpenSearch 等主流搜索引擎的默认融合策略。
### 四、代码示例:用 `LangChain4j` + Elasticsearch 实现混合检索
目前 `LangChain4j` 框架中,通过 Elasticsearch 集成实现混合检索是最成熟的方案。以下是基于 Elastic 官方博客(2026 年 3 月发布)的完整示例:
**Maven 依赖:**
```
dev.langchain4j
langchain4j
1.0.0-beta2
dev.langchain4j
langchain4j-elasticsearch
1.0.0-beta2
dev.langchain4j
langchain4j-ollama
1.0.0-beta2
```
**Java 代码:**
```
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
import dev.langchain4j.rag.content.Content;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.elasticsearch.ElasticsearchEmbeddingStore;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import java.util.List;
publicclass HybridSearchDemo {
public static void main(String[] args) {
// 1. 配置 Elasticsearch 客户端
RestClientBuilder restClientBuilder = RestClient.builder(
new HttpHost("localhost", 9200, "http")
);
// 2. 配置 Embedding 模型(这里用 Ollama 本地模型)
EmbeddingModel embeddingModel = OllamaEmbeddingModel.builder()
.baseUrl("http://localhost:11434")
.modelName("nomic-embed-text")
.build();
// 3. 创建 Elasticsearch 向量存储
ElasticsearchEmbeddingStore embeddingStore = ElasticsearchEmbeddingStore.builder()
.restClientBuilder(restClientBuilder)
.embeddingModel(embeddingModel)
.indexName("knowledge_base")
.build();
// 4. 纯向量检索(作为对比)
ContentRetriever vectorRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(5)
// 默认使用 kNN 向量检索
.build();
// 5. 混合检索(BM25 + kNN + RRF 融合)
ContentRetriever hybridRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(5)
// 开启混合检索:自动使用 BM25 + kNN,通过 RRF 融合结果
.searchType(ElasticsearchEmbeddingStore.SearchType.hybrid())
.build();
// 6. 执行检索对比
String query = "怎么在 Spring AI 中整合 Milvus 向量数据库?";
System.out.println("=== 纯向量检索结果 ===");
List vectorResults = vectorRetriever.retrieve(query);
vectorResults.forEach(c -> System.out.println(c.textSegment().text()));
System.out.println("\n=== 混合检索结果 ===");
List hybridResults = hybridRetriever.retrieve(query);
hybridResults.forEach(c -> System.out.println(c.textSegment().text()));
}
}
```
这段代码的核心区别就在第 5 步——设置 `.searchType(ElasticsearchEmbeddingStore.SearchType.hybrid())` 就能开启混合检索模式。Elasticsearch 底层会自动执行 BM25 + kNN 双路检索,然后用 RRF 融合排序。
### 五、主流向量数据库的混合检索支持情况
| 数据库 | 混合检索支持 | 稀疏检索方式 | 融合策略 |
| --- | --- | --- | --- |
| **Elasticsearch** | 原生支持 | BM25 | RRF(内置) |
| **Milvus 2.5+** | 原生支持 | Sparse-BM25 / SPLADE | 加权融合 |
| **Weaviate** | 原生支持 | BM25 | 加权融合 |
| **Qdrant** | 原生支持 | 稀疏向量 | RRF / 加权 |
| **OpenSearch** | 原生支持 | BM25 | RRF(内置) |
| **Chroma** | 有限支持 | 需外部配合 | 手动融合 |
如果你用的是 `Milvus 2.5`,它原生支持 Sparse-BM25 全文检索,可以直接在一个 Collection 中同时存储稠密向量和稀疏向量,然后用 `HybridSearchRequest` 执行混合查询。`Spring AI` 框架中通过 `MilvusVectorStore` 可以对接 `Milvus`,不过目前 `Spring AI` 对混合检索的原生抽象还在社区讨论阶段。
### 六、生产环境的最佳实践
说几个我实际踩过的坑和经验:
**1\. 混合检索 + Rerank 是黄金组合**
Azure AI Search 做过一组对比实验,结论很明确:
| 方案 | 召回准确率 |
| --- | --- |
| 纯关键词检索(BM25) | 基准 |
| 纯向量检索 | +8% |
| 混合检索(BM25 + 向量) | +15% |
| **混合检索 + Rerank** | **+25%** |
加了 Rerank 之后提升非常明显。推荐用 `bge-reranker-v2-m3` 或 `Cohere Rerank` 这类 Cross-Encoder 模型,在融合结果的基础上做精排。
**2\. 权重需要根据场景调整**
两路检索的权重不是固定的。如果你的知识库中专业术语、编号、代码片段比较多,BM25 的权重应该适当提高;如果是概念性、描述性的内容为主,向量检索的权重可以大一些。一般建议从 0.5:0.5 开始,然后根据实际效果调优。
**3\. Embedding 模型的选择很关键**
中文场景别用英文 Embedding 模型,效果会差很多。推荐 `bge-large-zh-v1.5`、`text-embedding-3-large`(支持中文)或者国产的 `Acme_Embedding`。模型选对了,向量检索这路的基线就稳了。
### 七、常见误区
- **误区一**:”向量检索已经理解语义了,不需要关键词检索。”
现实是向量检索对精确匹配天然不敏感。用户搜 `ERR-4023` 这种错误码,向量检索很可能给你一堆跟 “错误” 语义相关但完全不包含这个编号的文档。
- **误区二**:”混合检索就是把两路结果拼在一起。”
不是简单拼接,需要通过 RRF 或加权融合来统一排序。直接拼接会导致两路结果各排各的,相关性混乱。
- **误区三**:”混合检索一定比单路检索好。”
不一定。如果查询是纯语义类的(比如 “如何提高团队效率”),向量检索可能就够了。混合检索的价值在于应对**复杂多样的查询场景**,确保 “不管用户怎么问,都能找到相关文档”。
**Categories:** 热门资讯
---