面试知识库

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」,向量级融合已删)
plaintext

1.2 三次选型修订#

版本方案问题改了什么
v1Faiss 纯 denseexact-match 差;进程内不扛规模加 BM25 稀疏腿
v2Qdrant hybrid (dense+sparse)融合查询 filter 静默失效;复杂度高删 sparse
v3Qdrant 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
MRR0.981
NDCG@100.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。


六、诚实边界#

维度做了没做
EmbeddingBGE-M3 API(不自训)无 GPU 训练
索引规模138 万未做分布式 Qdrant
数据分布amazon 99.75%未做平衡采样
精排仅真 reranker 时开无本地 cross-encoder
实时性静态索引无增量更新/实时爬虫