[{"data":1,"prerenderedAt":350},["ShallowReactive",2],{"note:\u002Fprojects\u002Frag-knowledge-base\u002F05-milvus混合检索与重排":3},{"id":4,"title":5,"body":6,"date":328,"description":329,"draft":330,"extension":331,"featured":330,"kind":332,"lastmod":328,"meta":333,"navigation":252,"path":337,"planned":338,"section":339,"seo":340,"source":341,"stem":342,"tags":343,"toc":252,"weight":348,"__hash__":349},"notes\u002Fprojects\u002Frag-knowledge-base\u002F05-Milvus混合检索与重排.md","Milvus 混合检索与重排",{"type":7,"value":8,"toc":318},"minimark",[9,13,16,20,23,34,38,46,102,109,113,116,122,129,133,136,142,149,152,156,159,196,203,207,220,223,226,266,273,284,287,311,314],[10,11,12],"p",{},"有一个场景几乎每个 RAG 项目都会踩：",[10,14,15],{},"用户问「报销单号 R-2024-001 走到哪一步了」，纯向量检索召不回来。\n因为「R-2024-001」这种标识符没有语义，embedding 把它压缩成一个几乎无意义的向量，\n余弦相似度给不出高分。而这类查询在内部知识库里非常高频。",[17,18,19],"h2",{"id":19},"先给结论",[10,21,22],{},"企业检索需要三种能力叠加，少一种都会在具体场景漏答案：",[24,25,31],"pre",{"className":26,"code":28,"language":29,"meta":30},[27],"language-text","语义匹配   dense 向量       —— 问法和文档用词不一致时救命\n精确匹配   sparse 向量      —— 编号、术语、缩写的兜底\n精排       CrossEncoder    —— 把正确答案从候选池里捞到最前面\n","text","",[32,33,28],"code",{"__ignoreMap":30},[17,35,37],{"id":36},"一一路模型三路表示","一、一路模型，三路表示",[10,39,40,41,45],{},"项目用的是 BGE-M3，它的价值在于",[42,43,44],"strong",{},"一个模型同时产出三种表示","：",[47,48,49,65],"table",{},[50,51,52],"thead",{},[53,54,55,59,62],"tr",{},[56,57,58],"th",{},"表示",[56,60,61],{},"作用",[56,63,64],{},"适合的场景",[66,67,68,80,91],"tbody",{},[53,69,70,74,77],{},[71,72,73],"td",{},"dense",[71,75,76],{},"语义相似度",[71,78,79],{},"「怎么请假」匹配到「请假申请流程」",[53,81,82,85,88],{},[71,83,84],{},"sparse",[71,86,87],{},"词级权重",[71,89,90],{},"编号、专有名词、缩写",[53,92,93,96,99],{},[71,94,95],{},"colbert",[71,97,98],{},"细粒度交互",[71,100,101],{},"长文档里的局部匹配",[10,103,104,105,108],{},"多表示的意义不是「更强」，而是",[42,106,107],{},"互补","：dense 漏掉的由 sparse 兜住，\nsparse 分不清的语义近似由 dense 补上。项目里做了 dense + sparse 的混合召回，\n两路各自出候选，再统一进入重排。",[17,110,112],{"id":111},"二hnsw召回速度和准确率的交换","二、HNSW：召回速度和准确率的交换",[10,114,115],{},"向量索引用的是 HNSW（分层可导航小世界图）。它的核心参数只有两个方向：",[24,117,120],{"className":118,"code":119,"language":29,"meta":30},[27],"M              每个节点的邻居数 -> 越大越准，内存越大\nefConstruction 建索引时的搜索宽度 -> 越大索引质量越高，建库越慢\nefSearch       查询时的搜索宽度 -> 越大越准，查询越慢\n",[32,121,119],{"__ignoreMap":30},[10,123,124,125,128],{},"这不是「调优」，是",[42,126,127],{},"在固定硬件上选择你愿意付出多少延迟换多少召回","。\n企业知识库的规模通常是万级到百万级 chunk，HNSW 的默认参数就够用，\n真要调，也应该先用评测集确认「漏在了召回阶段还是排序阶段」，再决定调哪个参数。",[17,130,132],{"id":131},"三重排把-top20-变成可信的-top4","三、重排：把 top20 变成可信的 top4",[10,134,135],{},"召回阶段拿回来的是候选，不是答案。当前链路的两级收敛是：",[24,137,140],{"className":138,"code":139,"language":29,"meta":30},[27],"召回：FAQ top_k = 20，文档 top_k = 20\n重排：rerank_top_n = 5\n入参：final_context_top_n = 4\n",[32,141,139],{"__ignoreMap":30},[10,143,144,145,148],{},"为什么必须重排？因为向量检索是",[42,146,147],{},"双塔结构","：query 和 document 分别编码，\n比较的是两个独立向量的距离，两者之间从未真正「见过面」。\nCrossEncoder 把 query 和 doc 拼在一起送进同一个模型打分，判断精度高得多，\n代价是无法预先建索引——只能对少量候选做，所以必须放在召回之后。",[10,150,151],{},"这一步的收益往往被低估：正确答案常常在召回结果的第 10–20 位，\n如果没有重排、直接按 top_k=4 截断，它就永远进不了 prompt。",[17,153,155],{"id":154},"四过滤检索不能跨租户跨版本","四、过滤：检索不能跨租户、跨版本",[10,157,158],{},"召回不是全局搜索，带三组约束：",[160,161,162,181,190],"ul",{},[163,164,165,45,168,171,172,171,175,171,178],"li",{},[42,166,167],{},"数据域",[32,169,170],{},"tenant_id"," \u002F ",[32,173,174],{},"dataset_id",[32,176,177],{},"visibility",[32,179,180],{},"allowed_roles",[163,182,183,45,186,189],{},[42,184,185],{},"知识分类",[32,187,188],{},"source_filter","（hr \u002F it \u002F finance）",[163,191,192,195],{},[42,193,194],{},"版本有效期","：只召回当前 active 版本对应的 chunk 范围",[10,197,198,199,202],{},"过滤条件在检索前就下推到 Milvus 查询里，而不是拿回来再筛。\n顺序反过来会带来两个问题：召回数量不足（筛完剩不下几条）、以及",[42,200,201],{},"越权召回","\n（先把别的租户的数据取回内存，再在应用层过滤，一旦日志或异常栈打印出来就是泄露）。",[17,204,206],{"id":205},"五版本兼容是真实存在的坑","五、版本兼容是真实存在的坑",[10,208,209,212,213,216,217],{},[32,210,211],{},"qa_core\u002Fretrieval\u002Fmilvus_compat.py"," 这个文件看起来不起眼，但它是被真实问题逼出来的：\nMilvus 不同版本的 API 参数、返回结构、hybrid search 的调用方式会有差异。\n把兼容逻辑收敛到一个模块，比在业务代码里到处写 ",[32,214,215],{},"if version >= ..."," 干净得多。\n",[42,218,219],{},"如果你打算长期维护一个检索链路，专门留一个兼容层是值得的。",[17,221,222],{"id":222},"动手验证",[10,224,225],{},"检索调试接口不调用 LLM，只返回中间结果，是观察检索质量最快的入口：",[24,227,231],{"className":228,"code":229,"language":230,"meta":30,"style":30},"language-powershell shiki shiki-themes github-light github-dark","# 看一次检索的完整中间态：召回、分数、重排前后顺序\ncurl -X POST http:\u002F\u002F127.0.0.1:18000\u002Fapi\u002Fretrieval\u002Fdebug -H \"Content-Type: application\u002Fjson\" -d \"{\\\"query\\\": \\\"报销单号 R-2024-001 走到哪一步了\\\", \\\"source_filter\\\": \\\"finance\\\"}\"\n\n# Milvus 基础操作（连接、建集合、写入、检索）\npython scripts\u002Fdemo\u002Fdemo_ch04_milvus_basics.py\n","powershell",[32,232,233,241,247,254,260],{"__ignoreMap":30},[234,235,238],"span",{"class":236,"line":237},"line",1,[234,239,240],{},"# 看一次检索的完整中间态：召回、分数、重排前后顺序\n",[234,242,244],{"class":236,"line":243},2,[234,245,246],{},"curl -X POST http:\u002F\u002F127.0.0.1:18000\u002Fapi\u002Fretrieval\u002Fdebug -H \"Content-Type: application\u002Fjson\" -d \"{\\\"query\\\": \\\"报销单号 R-2024-001 走到哪一步了\\\", \\\"source_filter\\\": \\\"finance\\\"}\"\n",[234,248,250],{"class":236,"line":249},3,[234,251,253],{"emptyLinePlaceholder":252},true,"\n",[234,255,257],{"class":236,"line":256},4,[234,258,259],{},"# Milvus 基础操作（连接、建集合、写入、检索）\n",[234,261,263],{"class":236,"line":262},5,[234,264,265],{},"python scripts\u002Fdemo\u002Fdemo_ch04_milvus_basics.py\n",[10,267,268,269,272],{},"对比实验建议按这个顺序做：",[42,270,271],{},"只 dense → dense + sparse → 再加 CrossEncoder","，\n每次只看同一个问题的候选顺序变化。这样你能准确知道收益来自哪一层，\n而不是笼统地觉得「加了重排好像好一点」。",[10,274,275,276,279,280,283],{},"想深入原理可以配合两篇附录：",[32,277,278],{},"site\u002Fappendix\u002Fappendix-c-hnsw-index.html"," 和\n",[32,281,282],{},"site\u002Fappendix\u002Fappendix-d-crossencoder-reranker.html","。",[17,285,286],{"id":286},"取舍与边界",[160,288,289,295,305],{},[163,290,291,294],{},[42,292,293],{},"混合检索增加了延迟和复杂度","：两路召回 + 重排，比单路 dense 慢。\n如果语料里没有编号类查询、术语也很规范，单路 dense 就够。",[163,296,297,300,301,304],{},[42,298,299],{},"重排模型吃 CPU\u002FGPU 资源","：本地 ",[32,302,303],{},"bge-reranker-large"," 推理不便宜，\n候选数量要控制在合理范围（这里是 20 进 5）。",[163,306,307,310],{},[42,308,309],{},"过滤条件必须能落到存储层","：如果你的向量库不支持元数据过滤，\n要么换库，要么在设计阶段就把租户拆成独立集合。",[10,312,313],{},"下一篇回到主链路：检索到的东西，凭什么敢拿它回答。",[315,316,317],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":30,"searchDepth":243,"depth":243,"links":319},[320,321,322,323,324,325,326,327],{"id":19,"depth":243,"text":19},{"id":36,"depth":243,"text":37},{"id":111,"depth":243,"text":112},{"id":131,"depth":243,"text":132},{"id":154,"depth":243,"text":155},{"id":205,"depth":243,"text":206},{"id":222,"depth":243,"text":222},{"id":286,"depth":243,"text":286},"2026-06-26","组合 Dense、Sparse 与 CrossEncoder，兼顾语义召回、精确匹配和候选结果精排。",false,"md","project-chapter",{"migration":334},{"generator":335,"sourceSha256":336},"nuxt-site\u002Fscripts\u002Fmigrate-content.mjs","f778c8900d69f40cd09010b586d6f354e6f946b3058a7730dd9af1d850282c85","\u002Fprojects\u002Frag-knowledge-base\u002F05-milvus混合检索与重排",null,"projects",{"title":5,"description":329},"content\u002Fprojects\u002Frag-knowledge-base\u002F05-Milvus混合检索与重排.md","projects\u002Frag-knowledge-base\u002F05-Milvus混合检索与重排",[344,345,346,347],"RAG","Milvus","混合检索","重排",35,"HJ_pB-iQYMl7Joj_FzaczKISD-ehfc27qM7ZQbnHFD8",1791279145426]