我先构建了仅向量搜索。以下是我必须重写它的原因。
Source: Dev.to
我花了三周时间为电商产品目录构建了纯向量搜索,使用 intfloat/multilingual-e5-large 对所有内容进行嵌入,将向量存储在 Qdrant 中,并运行了一些测试查询。
- “Gift for someone who likes cooking” → kitchen knives, spice sets – ✅
- “Nike Air Max 90 black” → Adidas running shoes – ❌
- “XJ‑4520” (a real SKU) → random kitchen appliance – ❌
引擎能够理解意图,但在最简单的精确匹配查询上失败了。
为什么仅使用向量搜索会失败
描述性查询有效
嵌入将文本映射到高维空间,在该空间中语义相似的含义会聚集在一起。像 “gift for someone who likes cooking”(送给喜欢烹饪的人的礼物)这样的查询会落在刀具、食谱书和香料套装附近,即使产品标题中根本没有出现 gift 这个词。
SKU 和型号
像 XJ‑4520 这样的 SKU 对嵌入模型来说只是一个毫无意义的字符串。它会被投射到向量空间的某个位置,最近的邻居就是恰好在附近的其他毫无意义的字符串。实际上,SKU 查询几乎从不返回正确的产品。
品牌 + 属性组合
“Nike Air Max 90 black size 42” 本应返回单一商品。向量搜索却返回了 Nike、Adidas 和 Puma 的鞋子,因为它们在语义上都是 “运动鞋”。精确匹配往往只能在第 2 页才出现。
数值过滤
像 “under $50” 或 “500 ml bottle” 这样的查询并不会被理解为数值约束。模型知道 500 ml 与 bottle、liquid 有关联,但它不会根据实际数字进行过滤。
简短、特定的查询
单个词条如 “Bosch” 在向量搜索中会得到随机的电动工具,而使用 BM25 索引则会返回 所有 Bosch 产品并按相关性排序。
Source: …
结合 BM25 与向量搜索
并行执行
对同一目录同时运行 BM25 索引和 向量 索引,然后合并结果。BM25 在精确匹配(SKU、品牌名称、特定属性)方面表现出色,而向量则处理描述性、意图驱动以及跨语言的查询。
分数归一化
BM25 的分数大致在 0–25+ 之间,而向量相似度的取值范围在 0 到 1 之间。将两者归一化到统一的 0‑1 区间是合并前的必要步骤。
def normalize_scores(results: dict[str, float]) -> dict[str, float]:
"""Scale scores to the 0‑1 interval."""
if not results:
return {}
min_score = min(results.values())
max_score = max(results.values())
if max_score == min_score:
return {k: 1.0 for k in results}
return {
k: (v - min_score) / (max_score - min_score)
for k, v in results.items()
}
可配置加权
归一化后,使用店铺特定的权重将两条分数流进行混合。对部件编号依赖度高的零件供应商会给 BM25 更大的权重,而描述期望风格的时尚零售商则会倾向于 向量 侧。
Cross‑encoder 重新排序
对合并后的前 k 个候选项应用 cross‑encoder(例如 cross-encoder/ms-marco-MiniLM-L-6-v2)。该模型直接比较每个候选项与查询,重新排序结果,以纠正朴素合并导致的误排情况。
实际考虑
搜索前的查询分析
- 类似 SKU 的查询(字母数字,无空格)→ 完全跳过向量搜索。
- 长描述性句子 → 增加向量权重。
- 混合查询(例如 “red Nike something for running”)→ 平衡两种检索引擎。
单词查询
向量搜索在单个词汇上表现不佳;更多依赖 BM25 或回退启发式方法。
处理混合结果集
如果 BM25 返回 50 条结果,而向量仅返回 3 条,合并后可能偏向 BM25。通过归一化和权重调优可以缓解此问题。
拼写错误和错别字
BM25 和向量模型在出现大量拼写错误时都会性能下降。考虑加入拼写纠正层或模糊匹配。
个性化
所述流水线对每个查询独立处理。若加入用户历史信号,则需要额外的排序层。
缓存嵌入
嵌入计算成本高。可以缓存产品文本的向量表示,但在目录数据变更时需实现可靠的失效机制。
技术栈
- 向量存储 & BM25: Qdrant(内置 BM25 支持)
- 嵌入模型:
intfloat/multilingual-e5-large(1024 维,100+ 语言) - 重排序模型:
cross-encoder/ms-marco-MiniLM-L-6-v2 - 语言: Python(异步搜索执行)
- 编排: LangGraph(集成到更大的聊天助手工作流中)
结论
不要从仅使用向量的方案开始。虽然嵌入在理解意图和处理多语言、描述性查询方面表现出色,但在精确匹配查找、数值约束和短 token 上表现不足。混合方法——并行运行 BM25 和 vector 搜索,归一化分数,应用可配置权重,并可选地使用交叉编码器进行重排序——能够提供稳健、可投入生产的搜索体验。
如果您在电商搜索中遇到类似挑战,欢迎分享您的解决方案。