BM25 + Embedding 混合检索 实现

我们做一个标准的:

🔎 BM25(关键词召回) + Embedding(语义召回) + 融合排序


一、整体架构

text 复制代码
                用户 Query
                     ↓
        ┌─────────────────────────┐
        │                         │
   BM25 关键词检索           Embedding 语义检索
        │                         │
        └──────────┬──────────────┘
                   ↓
              候选结果合并
                   ↓
              融合打分 / RRF
                   ↓
                 TopK

二、实现步骤(完整示例)

下面给你一个最小可用版本

假设:

  • 文档是 List[str]
  • 使用 rank_bm25
  • 使用 sentence-transformers 做 embedding
  • 使用 FAISS 做向量搜索(推荐)

1️⃣ 安装依赖

bash 复制代码
pip install rank-bm25
pip install sentence-transformers
pip install faiss-cpu

2️⃣ 构建混合检索类

python 复制代码
import jieba
import numpy as np
import faiss
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
from typing import List


class HybridRetriever:
    def __init__(self, model_name="all-MiniLM-L6-v2"):
        # Embedding 模型
        self.model = SentenceTransformer(model_name)

        self.bm25 = None
        self.documents = None
        self.embeddings = None
        self.index = None  # FAISS 索引

    # ======================
    # 1. 构建索引
    # ======================
    def build(self, documents: List[str]):
        self.documents = documents

        # ---------- BM25 ----------
        tokenized_docs = [list(jieba.cut(doc)) for doc in documents]
        self.bm25 = BM25Okapi(tokenized_docs)

        # ---------- Embedding ----------
        self.embeddings = self.model.encode(documents)

        # 归一化(用于 cosine)
        self.embeddings = np.array(self.embeddings).astype("float32")
        faiss.normalize_L2(self.embeddings)

        dim = self.embeddings.shape[1]
        self.index = faiss.IndexFlatIP(dim)  # 内积 = cosine
        self.index.add(self.embeddings)

    # ======================
    # 2. 混合检索
    # ======================
    def search(self, query: str, top_k=5, bm25_weight=0.5):

        # ---------- BM25 ----------
        tokenized_query = list(jieba.cut(query))
        bm25_scores = self.bm25.get_scores(tokenized_query)

        # 取 BM25 TopN
        bm25_top = np.argsort(bm25_scores)[::-1][:top_k*5]

        # ---------- Embedding ----------
        query_vec = self.model.encode([query]).astype("float32")
        faiss.normalize_L2(query_vec)

        D, I = self.index.search(query_vec, top_k*5)

        embedding_top = I[0]

        # ---------- 候选集合 ----------
        candidate_ids = set(bm25_top).union(set(embedding_top))

        # ---------- 融合打分(加权) ----------
        results = []

        for idx in candidate_ids:
            bm25_score = bm25_scores[idx]

            # embedding 相似度
            emb_score = 0
            if idx in embedding_top:
                emb_score = 1  # 简化处理

            final_score = bm25_weight * bm25_score + \
                          (1 - bm25_weight) * emb_score

            results.append((idx, final_score))

        # 排序
        results.sort(key=lambda x: x[1], reverse=True)

        return [self.documents[i] for i, _ in results[:top_k]]

三、这里发生了什么?

🔹 BM25 负责:

  • 精确关键词匹配
  • 函数名
  • 错误码
  • 专业术语

🔹 Embedding 负责:

  • 同义表达
  • 自然语言问法
  • 语义理解
  • 模糊问题

🔹 融合方式

这里用了:

python 复制代码
加权融合

更高级一点可以用:

  • RRF(推荐)
  • Cross-Encoder rerank(更强)

四、更工业级版本(推荐)

真正生产系统一般是:

text 复制代码
1️⃣ BM25 召回 100 条
2️⃣ Embedding 召回 100 条
3️⃣ 合并
4️⃣ 用 Cross-Encoder 重新排序

Cross-Encoder 是:

把 query 和 document 拼在一起

让模型判断相关性

效果最好。


五、什么时候必须用混合检索?

如果你的系统:

  • 是代码问答
  • 是 Bug 定位
  • 是知识库问答
  • 是 RAG 系统
  • 用户输入是自然语言

👉 强烈建议混合。


六、为什么混合效果好?

因为:

BM25 = 高精度召回

Embedding = 高语义召回

两者覆盖不同类型问题。

单独用任何一个都会有盲区。


七、如果你想接入你现有代码

你现在有:

python 复制代码
KeywordIndex (TF-IDF)

升级路径:

  1. 把 TF-IDF 换成 BM25
  2. 新增 EmbeddingIndex
  3. 写一个 HybridRetriever
  4. 对外只暴露一个 search()

八、我给你一个实战建议

如果你是做:

  • ChatBI
  • 代码分析系统
  • Bug 定位系统

推荐结构:

text 复制代码
Hybrid Retrieval
      ↓
LLM Rerank
      ↓
最终答案生成

这是当前主流 RAG 架构。


相关推荐
火山引擎开发者社区2 小时前
技术速递|使用 GitHub Copilot CLI 构建 Emoji 列表生成器
人工智能
codefan※3 小时前
干掉“幻觉“实战:如何构建企业级知识图谱增强 RAG
人工智能·知识图谱
wukangjupingbb3 小时前
传统基于药物 SMILES 序列和蛋白质氨基酸序列的 DTI(Drug-Target Interaction)预测方法的缺陷
人工智能
沪漂阿龙3 小时前
Codex 额度重置周期变化:AI 编程免费试玩时代正在结束
人工智能
TickDB3 小时前
美股行情 API 接入避坑:REST 快照、WebSocket 推送、盘前盘后数据的边界
人工智能·python·websocket·行情数据 api
装不满的克莱因瓶3 小时前
深入理解卷积神经网络(CNN)——从原理到代码实践
人工智能·神经网络·cnn
完成大叔3 小时前
模块二,Agent知识图谱的工具链思考
人工智能
lauo3 小时前
ibbot手机发布:搭载poplang技术 + token节点经济,革新AI手机体验
人工智能·智能手机
咖啡星人k4 小时前
云端开发环境技术架构深度解析:从容器隔离到AI Agent集成
人工智能·架构
袋鼠云数栈4 小时前
从前端到基础设施,ACOS 如何打通企业全链路可观测
运维·前端·人工智能·数据治理·数据智能