M3 · 向量召回与检索管道#
简历 Bullet Point: 搭建三阶段召回管道(BGE-M3 dense 编码 → Qdrant 向量搜索 + 多维 payload filter → BGE-Reranker cross-encoder 精排),138 万商品全量索引,中文 query 跨语言召回英文商品;从 Faiss→Qdrant hybrid→dense+filter+精排 三次选型修订;汇率/关税/运费三张表打通到手价 + 收货国四层解析
开场钩子#
场景#
第一版 Faiss 纯向量,中文”旅行收纳包”搜不到英文”travel organizer bag”。加 BM25 做 hybrid 解决了,但迁 Qdrant 后融合查询下顶层 filter 静默失效——debug 发现 filter 必须放进每条 prefetch。反思后删掉 sparse:exact-match 是 filter 问题不是打分问题,最终 dense + payload filter + 精排。
面试官切入#
“你的召回管道经历了几次选型修订——每次改的原因是什么?“
一、模块运作流程#
1.1 召回管道三阶段#
用户 query → BGE-M3 编码(1024维) → Qdrant dense + payload filter
├─ platform / price_usd(Range) / rating(Range) / brand(后置排除)
→ BGE-Reranker 精排(粗召回×3 → 截top_k, 仅真reranker时重排)
→ ItemSearchOutput(个性化走「偏好词并入检索词 + payload filter」,向量级融合已删)plaintext1.2 三次选型修订#
| 版本 | 方案 | 问题 | 改了什么 |
|---|---|---|---|
| v1 | Faiss 纯 dense | exact-match 差;进程内不扛规模 | 加 BM25 稀疏腿 |
| v2 | Qdrant hybrid (dense+sparse) | 融合查询 filter 静默失效;复杂度高 | 删 sparse |
| v3 | Qdrant dense + filter + 精排 | —(当前方案) | exact-match 用 payload filter |
核心洞察:品牌/型号/SKU 是结构化字段,exact-match 用 payload filter(Range/Match)解决,不需要在打分维度里加稀疏通道。
1.3 数据清洗与建索引#
scripts/clean_platforms.py:6 平台原始 CSV → 质量闸 → 干净表(5000→4427,eBay 整体剔除、Lazada 折叠刷量)。后续全量重建扩到 138 万商品(Amazon RAG 数据集 48860 条 + 原始 CSV 扩充),amazon 占比 99.75%。
scripts/build_item_index.py:BGE-M3 编码 → Qdrant upsert(TPM 限流 + 分批防超 32MB + --require-remote 门禁挡假向量)。
1.4 汇率/关税/运费#
- 汇率:
FX_TO_USD静态中间价表(含拉美 MXN/CLP/COP),to_base_or_none未知币种不崩 - 关税:
DUTY_RATES按品类 + 免征额表(DEFAULT_DE_MINIMIS兜底),国内单免税 - 运费:
SHIPPING_TABLE按平台+重量区间 - 收货国四层解析:用户明示 → 偏好 → 语言推断 → 默认 CN
二、踩坑实录#
坑 1:Qdrant 融合查询下顶层 filter 静默失效#
- 融合模式(多路 prefetch + fusion)下顶层 filter 不传播。把 filter 放进每条 prefetch 解决。
坑 2:Amazon 价格科学计数法(17.95 被读成 1.795)#
- CSV 解析
1.795e+01没做科学计数法处理。加回归测试覆盖。
坑 3:upsert 单请求超 32MB#
- 全量重建时批量 upsert 超 gRPC 限制。加分批(每批 ≤100 点)。
坑 4:Amazon 屠版 99.75%#
- 全量重建 138 万点中 amazon 占 99.75%,platform=“all” 搜出来全是 amazon。决定不修——不是 bug 是数据分布。
坑 5:本地 Jaccard 回退扰动真实排序#
- 无真 reranker 时 Jaccard 回退是 stub,跨语言零重叠空转。改为仅真 reranker 重排,否则直出 dense 序。
三、验收与量化#
| 指标 | 数值 |
|---|---|
| Recall@10(RAG KB) | 0.983 |
| MRR | 0.981 |
| NDCG@10 | 0.754 |
| 全量索引 | 138 万点,dense-only |
| 跨语言 | 中文 query 召回英文商品有效 |
四、面试问答#
Q1: 为什么不用 hybrid(dense + sparse)?#
exact-match 是 filter 问题不是打分问题。品牌/型号是结构化字段,Qdrant payload filter 直接解决。sparse 腿增加复杂度(融合查询 filter 坑),删掉更简单。
Q2: 跨语言怎么做到的?#
BGE-M3 原生多语言能力。中文 query 和英文商品在同一向量空间里距离近。不需要翻译或对齐训练。
Q3: 精排为什么不总是开?#
本地 Jaccard 回退是 stub,跨语言零重叠空转。只在配了真 reranker(SiliconFlow API)时重排,否则直出 dense 序——Qdrant 的 dense 序本身已经不错。
Q4: 收货国四层解析怎么做的?#
用户明示(“寄到日本”)→ 长期偏好(Store 里记过 dest_country)→ 语言推断(中文默认 CN)→ 硬编码默认 CN。国内单(发货国=收货国)免税免运费。
五、前沿概念#
5.1 向量检索 vs 全文检索#
向量:语义相似性,跨语言天然支持。全文:精确匹配,需要分词器。本项目结论:语义用向量,精确用 filter,两者正交。
5.2 Cross-encoder 精排#
Bi-encoder(BGE-M3):query 和 doc 分别编码,适合大规模粗召回。Cross-encoder(BGE-Reranker):query 和 doc 拼接后共同编码,精度高但慢。经典的粗召回→精排 funnel。
六、诚实边界#
| 维度 | 做了 | 没做 |
|---|---|---|
| Embedding | BGE-M3 API(不自训) | 无 GPU 训练 |
| 索引规模 | 138 万 | 未做分布式 Qdrant |
| 数据分布 | amazon 99.75% | 未做平衡采样 |
| 精排 | 仅真 reranker 时开 | 无本地 cross-encoder |
| 实时性 | 静态索引 | 无增量更新/实时爬虫 |