Skip to content

fix(C9): 启用 hybrid_search 中真实的 BM25(jieba) 检索 + RRF 替代 round-robin#106

Merged
FutureUnreal merged 2 commits into
datawhalechina:mainfrom
yzh3434:pr/bm25-jieba-rrf
May 2, 2026
Merged

fix(C9): 启用 hybrid_search 中真实的 BM25(jieba) 检索 + RRF 替代 round-robin#106
FutureUnreal merged 2 commits into
datawhalechina:mainfrom
yzh3434:pr/bm25-jieba-rrf

Conversation

@yzh3434

@yzh3434 yzh3434 commented May 1, 2026

Copy link
Copy Markdown
Contributor

问题

Chapter 9 hybrid_retrieval.py 当前 hybrid_search 实现存在两处问题:

1. BM25 是占位实现,实际从未参与检索

  • __init__ / initialize 中创建了 self.bm25_retriever = BM25Retriever.from_documents(chunks)(第 63-65 行)
  • hybrid_search(query, top_k) 只调用 dual_level_retrieval + vector_search_enhanced从未查询过 bm25_retriever
  • 即使调用,langchain.BM25Retriever 默认分词是 text.split(),对中文等于"整句作为一个 token",无检索意义

2. Round-robin 合并完全不利用分数信息

  • 当前 hybrid_search 末尾对每个 doc 设了 metadata["final_score"](vector 路还做了 1.0 - vector_score 的 cosine→similarity 转换),但函数最终用 final_docs = merged_docs[:top_k] 直接按 round-robin 位置切片返回,从未按 final_score 重排序
  • 这意味着 final_score metadata并未使用,相关 doc 的最终排名完全取决于在原列表中的位置,与真实相关度无关
  • 实测中相关 doc 经常出现在 rank 2 而非 rank 1(即"召回到了但没排在第一位"),这正是 round-robin 丢失分数信息的典型症状

改动概述

启用真正的中文 BM25

  • 引入 jieba 精确分词 + 中文停用词表(按烹饪场景手挑约 60 词,覆盖助词/疑问词/语气词等)
  • 改用 rank_bm25.BM25Okapi 直接获取 score(langchain BM25Retriever 不暴露 score)
  • BM25 索引与 Milvus 向量索引建立在同一批 chunks 上,两路候选集合一致,避免 RRF 融合时出现"某 doc 只在一路索引里"的覆盖偏差

RRF 融合替代 Round-robin

  • 标准公式:score(d) = Σ_i 1 / (k + rank_i(d)),k=60
  • 三路输入:dual_level / vector / bm25,每路取 max(top_k * 2, 10) 候选给 RRF 重排
  • node_id 去重,hash 兜底(不同检索路径同 recipe 的 page_content 可能拼接了"相关信息",按 hash 会漏融合)

评测对比

在自建 100 题评测集(7 类问题 × 3 难度)上对比 round-robin vs RRF:

指标 round-robin(改前) RRF + 真 BM25(改后) Δ
Hit@5 0.990 0.990 持平
Recall@5 0.915 0.915 持平
MRR@10 0.727 0.939 +0.21
Latency P50 5647 ms 5214 ms ≈持平

注:本对比的两侧使用相同的查询路由策略(独立改动),以隔离 BM25+RRF 本身的贡献。原 datawhale baseline(无路由优化)的 MRR@10 为 0.628 — 但路由优化是另一组改动,与本 PR 无关。

按问题类型分组看 MRR@10 涨幅:

  • 走 hybrid_search 的查询(事实查询 / 属性查询 / 步骤型 / 因果推理 / 实体关系,约 5 类):MRR@10 涨幅 +0.20 ~ +0.32 不等
  • 走图查询的查询(多跳推理、菜品对比等,需要 Cypher 路径检索):MRR@10 不受影响

由于改动只发生在 hybrid_search 函数内部,不影响其他检索路径——分组数据也确认了这一点。

评测集与对比脚本可另作附件提供,本 PR 默认仅含代码改动。

兼容性

  • requirements.txt 新增 jieba>=0.42.1rank-bm25 已有)
  • HybridRetrievalModule.hybrid_search(query, top_k) 外部签名不变
  • 移除未被使用的 from langchain_community.retrievers import BM25Retriever

- 接入 rank_bm25.BM25Okapi + jieba 精确分词 + 中文停用词过滤
- 新增 _rrf_merge:标准 RRF 公式 score=Σ1/(k+rank),k=60,按 node_id 去重
- hybrid_search 重写为三路(dual_level + vector + bm25)→ RRF 融合
- 移除占位的 langchain BM25Retriever(原代码初始化但从未被查询过)

在 100 题自建评测集上,控制其他变量对比 round-robin vs RRF:
- MRR@10 +0.17(排序质量提升)
- Hit@5 / Recall@5 已触顶不变(召回路径未变)
- Latency P50 ≈ 持平(jieba 首次加载一次性开销)
@FutureUnreal

Copy link
Copy Markdown
Member

感谢 pr!jieba 这个问题本来是打算作为一个错误案例,在第十章项目中提及。目前 pr 的这个 RRF 的问题,看起来是考虑了跨来源按菜谱去重,但是有考虑到同一道菜多个 chunk 共享 node_id 后在同一 source 内重复加分的问题吗,不知道是否方便优化一下

…chunk

  - 原实现 score 累加未按 source 去重,同一 recipe 的多个 chunk 在同一路里
    会被反复加分,违反 RRF "每个 ranker 对每个 doc 贡献一次" 的语义
  - canonical doc 原按"输入顺序首次见到"选取,受 ranked_lists 顺序支配;
    改为按全局最小 rank 选取,rank 相同时按 ranked_lists 顺序优先
  - 同时把每个 source 的 chunk 命中次数另存到 metadata.rrf_chunk_hits,
    便于后续分析
  - _rrf_merge 不再 mutate 输入 Document.metadata,返回新 Document 对象

  更新之后在100 题评测集上 MRR@10 0.898 → 0.939,Faithfulness 0.680 → 0.734。

  Addresses review comment in datawhalechina#106
@yzh3434

yzh3434 commented May 2, 2026

Copy link
Copy Markdown
Contributor Author

感谢指出!的确是 bug:同 source 内同 node_id 多 chunk 会被重复加分。已在 commit 9e4526a 修复,主要改动:

  1. 算分按 (doc_id, source) 去重,每路只用该 doc 在该路内的最佳 rank 算一次贡献
  2. 顺手修了 canonical doc 选择逻辑——原来按"输入顺序首次见到(双层检索 → milvus → BM25)",改为"全局最小 rank 优先",避免 BM25 在 rank=1 召回的精确 chunk 被 dual_level 在 rank=8 的 chunk 替代
  3. chunk 命中次数另存到 metadata.rrf_chunk_hits 供分析
  4. _rrf_merge 不再改变输入 Document.metadata,而是返回新 Document

评测集上 MRR@10 / faithfulness 双双提升,详细数字见 commit 9e4526a 的 message。如还有别的考虑请指出,谢谢!

@FutureUnreal FutureUnreal merged commit b3d0d7d into datawhalechina:main May 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants