石家庄市网站建设_网站建设公司_动画效果_seo优化
2025/12/23 13:56:06 网站建设 项目流程

从关键词到语义:用 Elasticsearch 打造真正“懂你”的推荐系统

最近在做推荐系统的重构,团队里有个很现实的拷问:“为什么用户刷了半天,看到的还是那些热门商品?”——这背后,其实是传统推荐架构的瓶颈。

我们过去依赖基于规则的筛选和协同过滤模型,虽然稳定,但面对冷启动、长尾内容曝光不足、语义理解缺失等问题时显得力不从心。更麻烦的是,为了实现“个性化”,不得不额外引入 Faiss 或 Milvus 这类向量数据库,运维复杂度陡增。

直到我们将目光转向Elasticsearch 的向量检索能力—— 没想到,这个原本只用来做日志搜索和商品全文匹配的“老伙计”,如今已经悄然进化成了一个支持语义级推荐的利器。

今天就想和大家分享:如何利用现有的 Elasticsearch 集群,构建一套高效、灵活、可落地的个性化推荐系统。全程无需新增向量库,也不必推倒重来,关键路径我们都已跑通。


当推荐遇上向量:一次范式的转变

先说清楚一个问题:为什么非得上“向量”?

想象一下,用户刚看完一篇《iPhone 15 Pro Max 深度评测》,紧接着你想推荐相关产品。如果靠关键词匹配,“MacBook Pro”可能根本不会出现结果中——它既不含“iPhone”,也没有“手机”字样。但你知道,这两个产品在语义上高度相关。

这就是传统方法的盲区:只能匹配字面,无法理解意图

而通过 embedding 技术,我们可以把“iPhone 15 Pro Max”和“MacBook Pro”都映射成 128 维的稠密向量。它们虽然文字不同,但在向量空间中的距离却很近。只要计算余弦相似度,就能自动发现这种潜在关联。

向量化不是噱头,它是让机器学会“类比思维”的第一步 —— 就像人类会说:“喜欢 A 的人往往也喜欢 B”。

Elasticsearch 自7.10 版本起支持dense_vector字段类型,并在后续版本中集成了 HNSW 图索引和原生 KNN 查询(8.0+),正式具备了处理这类语义检索的能力。

这意味着什么?意味着你现在可以用一套系统,同时搞定:

  • 商品标题的关键词搜索
  • 用户兴趣的语义匹配
  • 多条件组合过滤(价格、类目、库存)

不再需要拼接多个服务,也不用担心数据一致性问题。ES 成了真正的“统一检索入口”


核心武器:Elasticsearch 是怎么做到快速找“相似”的?

dense_vector + HNSW:让亿级向量也能毫秒响应

当你往 ES 写入一条商品记录时,除了 title、price 这些常规字段,还可以加一个embedding字段:

{ "title": "无线蓝牙降噪耳机", "category": "electronics", "price": 599, "embedding": [0.12, -0.34, 0.78, ..., 0.56] }

这个embedding就是经过 Sentence-BERT 或 Item2Vec 模型生成的语义向量。它的长度固定(比如 128 或 384 维),类型为dense_vector

关键来了:你可以在该字段上建立 HNSW 索引,大幅提升向量相似性搜索的速度。

什么是 HNSW?

HNSW(Hierarchical Navigable Small World)是一种图结构索引算法。简单来说,它通过构建多层导航图,实现“从粗到细”的跳跃式查找。

你可以把它想象成城市地铁系统:
- 最高层是“快线”,覆盖范围广但站点少,快速定位大致区域;
- 越往下站台越密集,最终精准抵达目标站点。

相比暴力遍历(O(n)),HNSW 可以将查询时间降到 O(log n),即使面对千万甚至上亿条向量,也能做到亚秒级返回 Top-K 结果。

如何配置 HNSW 索引?

创建索引时指定index_options.type = "hnsw"即可:

PUT /products { "mappings": { "properties": { "title": { "type": "text" }, "category": { "type": "keyword" }, "price": { "type": "integer" }, "embedding": { "type": "dense_vector", "dims": 384, "index": true, "similarity": "cosine", "index_options": { "type": "hnsw", "m": 16, "ef_construction": 100 } } } } }

几个核心参数解释一下:

参数作用建议值
dims向量维度根据模型定,常见 128/384
similarity相似度函数推荐"cosine"
m每个节点连接数16~32,越大越准越占内存
ef_construction构建时候选集大小100 左右,影响建索引速度
ef_search查询时候选集大小可运行时调整,越大越准

实践建议:初期可用默认值试跑,再根据 P99 延迟和召回率调优。


混合查询:不只是“最像”,还要“最合适”

很多人误以为向量检索就是“找个最像的”。其实真正有用的推荐,是在满足业务约束的前提下找最相关的

举个例子:你想给一位预算 1000 元以下、偏好电子产品的用户推荐耳机。这时候不能只看向量相似度,还得考虑:

  • 是否属于 electronics 类目?
  • 价格是否 ≤ 1000?
  • 是否有货?

幸运的是,Elasticsearch 完美支持这种“混合打分”模式。

方式一:script_score(兼容旧版)

适用于 7.x ~ 8.7 版本:

{ "size": 10, "query": { "script_score": { "query": { "bool": { "must": [ { "term": { "category": "electronics" } } ], "filter": [ { "range": { "price": { "lte": 1000 } } }, { "term": { "in_stock": true } } ] } }, "script": { "source": "cosineSimilarity(params.vec, 'embedding') + 1.0", "params": { "vec": [0.1, 0.5, ..., 0.9] } } } } }

这里做了三件事:
1.bool.query先做硬性条件筛选;
2.script计算余弦相似度作为打分依据;
3.+1.0是因为cosineSimilarity返回 [-1,1),而评分必须 ≥0。

方式二:原生 KNN 查询(推荐!)

Elasticsearch 8.8 开始,官方推出了简洁高效的knn查询语法:

{ "knn": { "field": "embedding", "query_vector": [0.1, 0.5, ..., 0.9], "k": 10, "num_candidates": 100, "filter": [ { "term": { "category": "electronics" } }, { "range": { "price": { "lte": 1000 } } } ] }, "query": { "term": { "in_stock": true } }, "size": 10 }

优势非常明显:
- 更清晰的语义划分:knn负责语义匹配,query/filter控制业务逻辑;
- 性能更好:底层优化了预筛选与向量搜索的执行顺序;
- 支持num_candidates控制精度与性能平衡。

新项目强烈建议直接使用原生 KNN 查询


推荐质量的命门:嵌入向量到底怎么来?

很多人以为上了向量检索就万事大吉,其实不然。检索只是“腿”,走得快不快看算法;但方向对不对,全靠嵌入质量

换句话说:垃圾向量进,垃圾推荐出。

所以我们花了很多精力打磨两个核心模块:物品嵌入生成用户兴趣建模

物品向量:内容 + 行为双驱动

我们采用“两条腿走路”的策略生成 item embedding:

1. 内容嵌入(Content-based)—— 解决冷启动

对于新上架的商品,还没有用户行为怎么办?靠内容!

我们用Sentence-BERT对商品标题、描述、标签进行编码:

from sentence_transformers import SentenceTransformer model = SentenceTransformer('paraphrase-MiniLM-L6-v2') def get_content_embedding(title: str) -> list: return model.encode(title).tolist() # 示例 vec = get_content_embedding("苹果 MacBook Pro 笔记本电脑")

这类轻量级模型推理快(单条 <10ms),可以直接部署在边缘服务器或 API 网关旁,实现实时向量化。

2. 协同嵌入(Collaborative)—— 捕捉群体偏好

当商品积累了一定交互数据后,我们会用Item2VecGraphSAGE在用户-物品二分图上训练,得到更具行为感知能力的向量。

例如,若大量用户同时点击“降噪耳机”和“AirPods Pro”,即便两者名字无关,模型也会让它们在向量空间靠近。

我们的做法是:新商品先用 content embedding 上线曝光 → 积累行为后切换为 collaborative embedding → 实现平滑过渡。


用户向量:动态捕捉兴趣漂移

用户的兴趣不是静态的。昨天爱看数码,今天可能就在搜母婴用品。因此用户向量必须近实时更新

我们的方案如下:

简单有效:加权平均池化

将用户近期(如最近 30 条)点击/购买的商品向量做加权平均,时间越近权重越高:

import numpy as np def build_user_vector(click_history): vectors = [] weights = [] for item_id, timestamp in click_history: vec = item_embeddings[item_id] # 预加载的商品向量 age_in_hours = (now - timestamp).total_seconds() / 3600 weight = np.exp(-age_in_hours / 24) # 按天衰减 vectors.append(vec * weight) weights.append(weight) return np.sum(vectors, axis=0) / sum(weights)

优点是实现简单、延迟低,适合中小规模系统。

高阶玩法:序列模型(BERT4Rec)

如果你有足够的训练数据,可以尝试用Transformer 结构建模用户行为序列,输出综合兴趣向量。

这类模型能识别“会话内偏好”、“跨品类跳转”等复杂模式,表达能力更强,但需要离线训练 + 在线 Serving。

目前我们在主站用平均池化,在信息流场景试点 BERT4Rec,CTR 提升约 7%。


我们的系统架构:轻量接入,渐进演进

不想一次性重构整个推荐链路?没关系,我们也是从小处切入的。

这是目前线上运行的简化架构图:

[前端埋点] ↓ (Kafka) [Fluentd/Flink 流处理] ↓ [Embedding Service] ├── 批量生成商品向量 → 写入 ES └── 实时聚合用户行为 → 更新用户向量(Redis) ↓ [Elasticsearch Cluster] ├── 存储商品向量 + HNSW 索引 └── 提供 knn 查询接口 ↓ [Recommendation API] ├── 获取 user vector from Redis └── 发起 knn 查询 → 返回 Top-N ↓ [App/Web 展示]

关键设计思路

  1. 复用现有 ES 集群
    初期直接在原有商品索引上增加embedding字段,避免新建集群带来的资源浪费和运维负担。

  2. 读写分离,避免干扰
    向量检索较耗 CPU 和内存,我们在协调节点层面做了分流:
    - 普通搜索走 A 组协调节点;
    - KNN 请求定向到 B 组专用节点,防止相互拖慢。

  3. 冷启动友好策略
    - 新用户:使用“城市平均向量”或“热门商品聚类中心”作为初始兴趣;
    - 新商品:强制启用 content embedding,并设置 higher boost 分数提升曝光机会。

  4. 监控不可少
    我们重点关注几个指标:
    - P99 查询延迟:<100ms(否则影响用户体验)
    - recall@10:定期抽样验证 top-10 是否合理
    - 向量更新延迟:用户行为→向量生效控制在 5 分钟内


落地效果与避坑指南

上线两个月后,我们对比了 AB 实验数据:

指标提升幅度
页面点击率(CTR)+12.3%
平均停留时长+18.7%
GMV(成交额)+9.5%

尤其是长尾商品的曝光占比提升了近 3 倍,说明系统真的开始“挖掘潜力股”了。

但也踩过不少坑,总结几点血泪经验:

❌ 坑点一:没归一化的向量用了 cosine 相似度

一开始我们直接把原始 embedding 写进去,结果发现某些向量特别“长”,导致点积远大于其他,排序严重失衡。

解决方案:写入前务必做 L2 归一化!

import numpy as np vec = np.array(raw_vec) vec = vec / np.linalg.norm(vec) # L2 normalize

这样所有向量长度均为 1,cosine 相似度才真正反映“夹角”差异。


❌ 坑点二:HNSW 参数设得太激进

曾把ef_construction=500,想着“越高越准”。结果建索引慢得像蜗牛,每天凌晨任务拖到早上还没完。

建议:生产环境ef_construction不超过 100,m控制在 16~32。追求精度不如多做几轮召回+精排。


❌ 坑点三:忽略了向量维度与存储成本的关系

384 维 vs 128 维,看着只差 3 倍,但乘以亿级文档量,磁盘和内存压力完全不同。

建议:优先测试低维模型(如 MiniLM),在精度损失可控前提下尽量压缩维度。我们最终选择了 128 维,节省了 60% 存储。


写在最后:这不是终点,而是起点

现在回头看,当初决定用 Elasticsearch 做向量推荐,最大的收益不是技术先进性,而是落地效率

没有引入新组件,没有重建 pipeline,两周内就在现有架构上完成了原型验证。而且由于 ES 本身高可用、易监控、支持热扩容,运维同学也没抱怨。

更重要的是,我们终于实现了“一人一策”的语义推荐。用户不再被框死在“看了又看”里,而是能自然地从“手机”跳到“支架”,从“咖啡机”延伸到“滤纸”——这才是真实世界中的兴趣迁移。

未来我们计划进一步探索:

  • 使用 ES 内置的稀疏向量支持(sparse_vector)结合 BM25,打造 hybrid ranking;
  • 试验ELSER(Elastic Learned Sparse Encoder)做纯语义检索,摆脱对第三方 embedding 模型的依赖;
  • 探索多向量交叉检索,比如分别编码标题、图片、评论,再融合打分。

Elasticsearch 正在从“搜索引擎”蜕变为“智能语义中枢”。而对于大多数团队而言,这或许是一条最具性价比的智能化升级之路。

如果你也在纠结要不要上向量推荐,我的建议是:别等完美方案,先在一个小场景试起来。用你手头的 ES 集群,跑通一次 knn 查询,看到第一条“意料之外却又情理之中”的推荐结果时,你会明白:这场进化,值得投入。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询