暂无菜单项

RAG 中的混合检索是什么?为什么要用混合检索而不是纯向量检索?

发布于 更新于
180

暂无数据

深度解析

一、为什么纯向量检索不够用?

向量检索的原理是把文本通过 Embedding 模型编码成高维向量,然后用余弦相似度或内积来衡量语义相关性。这对 “理解意思” 很在行,但它有一个致命弱点:对精确关键词的匹配能力很弱

举个实际的例子。假设你的知识库里有这样一段文档:

Spring AI 整合 Milvus 向量数据库时,需要在 pom.xml 中引入 spring-ai-milvus-store 依赖,版本号 1.0.0-M4

用户提问:”spring-ai-milvus-store 的版本号是多少?”

向量检索可能会返回一堆跟 Spring AIMilvus、依赖配置语义相关的文档,但精确包含 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 依赖:

<dependencies>
    <!-- LangChain4j 核心 -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j</artifactId>
        <version>1.0.0-beta2</version>
    </dependency>

    <!-- LangChain4j Elasticsearch 集成(支持混合检索) -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-elasticsearch</artifactId>
        <version>1.0.0-beta2</version>
    </dependency>

    <!-- LangChain4j Ollama(本地 Embedding 模型) -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-ollama</artifactId>
        <version>1.0.0-beta2</version>
    </dependency>
</dependencies>

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<Content> vectorResults = vectorRetriever.retrieve(query);
        vectorResults.forEach(c -> System.out.println(c.textSegment().text()));

        System.out.println("\n=== 混合检索结果 ===");
        List<Content> 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.5text-embedding-3-large(支持中文)或者国产的 Acme_Embedding。模型选对了,向量检索这路的基线就稳了。

七、常见误区

  • 误区一:”向量检索已经理解语义了,不需要关键词检索。”

    现实是向量检索对精确匹配天然不敏感。用户搜 ERR-4023 这种错误码,向量检索很可能给你一堆跟 “错误” 语义相关但完全不包含这个编号的文档。

  • 误区二:”混合检索就是把两路结果拼在一起。”

    不是简单拼接,需要通过 RRF 或加权融合来统一排序。直接拼接会导致两路结果各排各的,相关性混乱。

  • 误区三:”混合检索一定比单路检索好。”

    不一定。如果查询是纯语义类的(比如 “如何提高团队效率”),向量检索可能就够了。混合检索的价值在于应对复杂多样的查询场景,确保 “不管用户怎么问,都能找到相关文档”。