ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于协同过滤的鲜花店推荐系统:Spring Boot实战与冷启动方案

基于协同过滤的鲜花店推荐系统:Spring Boot实战与冷启动方案

在毕业设计和中小型电商项目里,推荐系统一直是个既能体现技术含量、又贴近真实业务需求的选题。我之前正好帮某位开发者朋友梳理过一个“基于协同过滤的鲜花店推荐系统”,技术栈锁定在 Spring Boot 上,算法选的是协同过滤。这个项目表面看是个常规的电商系统,但真正把它做扎实、让推荐结果“看起来合理”,其实里面有不少弯弯绕绕。这篇就围绕这个标题,把我实际梳理和落地过程中的关键设计、算法选型、数据建模、踩坑经验完整拆开讲一遍,给正在做类似项目或者想给自家小商城加推荐功能的朋友一个可直接参考的蓝本。

1. 项目整体设计与协同过滤选型思路

1.1 为什么是“协同过滤”,而不是别的主流算法

拿到“鲜花店推荐系统”这个需求时,第一反应不是急着写代码,而是先想清楚推荐算法选型。主流的推荐方案大概有三类:基于内容的推荐、基于协同过滤的推荐、基于深度学习的推荐。

深度学习方案(比如 DeepFM、DIN 这类)效果上限高,但它们对数据量的要求很苛刻,需要海量用户行为日志、特征工程和训练资源。对于鲜花店这种垂直品类、用户规模和商品数量都有限的中小型电商场景来说,投入产出比太低,而且部署和维护成本也不适合单人或小团队开发。

基于内容推荐理解起来最直白:把每束花的品类、花语、价格、风格打上标签,再根据用户的历史购买或浏览行为,找标签相似的鲜花推荐。这个方案实现简单,但也有一个老毛病——严重依赖标签质量。鲜花这种商品,标签写得再细,也很难覆盖“用户为什么买这束花”这种深层动机。同样是买向日葵,有人是毕业典礼送人,有人是给自己办公桌添点生气,基于内容的推荐很难区分这两种场景。

协同过滤的核心思路是“物以类聚、人以群分”。它不关心商品本身长什么样,只看用户和物品之间的交互行为。UserCF(基于用户的协同过滤)先找和你喜好相似的一群人,再把那群人喜欢的鲜花推荐给你;ItemCF(基于物品的协同过滤)则直接分析物品之间的共现规律,比如“买了红玫瑰的人通常也会买满天星”,然后给你推荐和已购鲜花关联度高的其他花材。

在这个项目里,我们最终选择的是ItemCF 为主、UserCF 为辅的混合策略。核心原因有三点:

  • 鲜花是典型的长尾品类,但用户购买行为具备很强的场景性(节日送花、生日送花、日常装饰),同一场景下的物品共现规律比用户之间的相似度更稳定。
  • 物品数量远小于用户数量。在稀疏矩阵下,物品相似度矩阵的计算和存储压力更小。
  • 用户的兴趣会随时间漂移,但物品之间的关联关系变化较慢。ItemCF 的离线计算结果可缓存复用,实时推荐响应更快。

所以,从工程落地角度看,ItemCF 是这个场景下最稳的起点。

1.2 系统模块边界划分

明确了算法路线后,就要把整个系统拆成可落地的模块。一个完整的鲜花店推荐系统,绝不能只有推荐算法,它本质上还是一个电商系统,推荐只是其中一环。我按这套边界来拆分:

基础数据模块

涵盖用户管理(注册、登录、收货地址)、鲜花管理(上架、分类、库存、价格)、订单管理(购物车、下单、支付回调、订单状态流转)。这些模块提供推荐算法最基础的数据来源——用户画像和行为日志。

行为采集模块

推荐系统离不开“用户行为数据”。用户浏览了什么花、搜索了什么关键词、把哪束花加进了购物车、最终购买了哪束花、是否对订单做过评价,这些行为需要以日志或结构化记录的方式沉淀下来。这个模块常被低估,但没了它,协同过滤就是无源之水。

推荐引擎模块

核心算法层。包括离线计算任务(周期性计算物品相似度矩阵)、在线推荐接口(接收用户标识,拼装候选集并排序)、策略融合层(冷启动兜底、热门榜补充、去重打散)。

前端展示模块

花店商城页面 + 推荐位。推荐位一般放在首页“猜你喜欢”、商品详情页“买了这束花的人也买了”、购物车页“搭配推荐”三个位置。前端不复杂,但推荐位的埋点数据要传回行为采集模块,形成闭环。

这套模块划分的好处在于,每一步都可以独立开发、并行推进。就算推荐引擎最后效果不理想,基础电商功能不受影响,可以直接砍掉推荐位降级为普通商城,风险可控。

1.3 推荐系统核心流程闭环

整个推荐流程我习惯用一条链路来理解:

用户行为上报 → 数据清洗与归一化 → 离线相似度计算 → 候选集召回 → 规则过滤(库存、上下架、价格区间) → 排序打分 → 推荐位展示 → 新行为数据回流

这条链路里,最容易翻车的是第一步和最后一步——工程师往往把精力都放在中间的算法上,却忽略了埋点和数据回流。实际上,算法效果的上限由数据质量决定。后面我会专门讲数据模型该怎么设计。

2. 核心细节解析:从数据表设计到相似度计算原理

2.1 用户行为数据建模:一张表撑起整个推荐算法

如果你只是做一个演示性质的项目,那行为数据可以从订单表里反推:用户买了什么花,就算一次“隐式正反馈”。但这远远不够。真实的推荐系统需要更细粒度的行为数据,我通常会设计一张独立的用户行为表,结构参考如下:

字段类型说明
idbigint主键
user_idbigint用户ID,必填索引
item_idbigint鲜花ID,必填索引
behavior_typetinyint行为类型:1-浏览 2-搜索点击 3-加购 4-下单 5-收藏
scenevarchar行为场景:home-首页推荐位 detail-详情页相关推荐 cart-购物车推荐
stay_secondsint浏览时长(秒),仅浏览行为有值
create_timedatetime行为发生时间

这里有几个容易被忽略的细节:

s场景字段必须留。没有场景信息,你就无法评估同一个推荐位自身的转化效果,也无法做分场景的推荐策略调优。

stay_seconds 时间戳可以过滤“误点击”。用户点进详情页 2 秒就退出,大概率只是随手点了一下,这种行为如果直接当成正反馈,会污染推荐结果。我一般会把浏览时长小于 3 秒的记录降权,或者直接把权重置为 0.1。

加购和下单的权重要区分开。协同过滤在计算相似度时,可以对不同行为赋予不同权重。比如:浏览 = 1,搜索点击 = 2,加购 = 4,下单 = 8。这样算物品相似度时,一次真实的购买行为对相似度的贡献远大于一次无意间的浏览。

2.2 协同过滤核心公式:余弦相似度与共现矩阵

现在进入算法本身。ItemCF 的核心是计算任意两束鲜花之间的相似度。计算公式有很多种,项目里最常用的是余弦相似度:

对物品 i 和物品 j,它们之间的相似度可以定义为:

sim(i, j) = |N(i) ∩ N(j)| / sqrt(|N(i)| × |N(j)|)

其中 N(i) 是“喜欢或购买过物品 i 的用户集合”,N(j) 是“喜欢或购买过物品 j 的用户集合”。两者交集大小说明有多少用户同时喜欢这两束花,交集越大,相似度越高。除以各自喜欢人数的几何平均数,是为了惩罚热门商品——如果一个商品被所有人买过(比如红玫瑰),它对任何物品的相似度贡献都应该被压下来,否则推荐结果永远被头部商品霸占。

实际计算时,我不用笛卡尔积暴力求解。一个常见的工程优化是:先构建“用户 → 物品列表”的倒排表,再两两统计物品之间的共现次数。伪代码如下:

// 假设 data 是用户行为列表,格式为 { // userId: [itemId1, itemId2, ...] // } function buildItemSimilarity(data) { const itemUsers = {}; // 每个物品对应的用户集合,用 Set 去重 const itemCount = {}; for (const [userId, items] of Object.entries(data)) { // 同一用户的行为列表里,物品去重 const uniqueItems = Array.from(new Set(items)); uniqueItems.forEach(item => { if (!itemUsers[item]) itemUsers[item] = new Set(); itemUsers[item].add(userId); itemCount[item] = (itemCount[item] || 0) + 1; }); // 对每个用户内部的物品两两配对计数 for (let i = 0; i < uniqueItems.length; i++) { for (let j = i + 1; j < uniqueItems.length; j++) { const a = uniqueItems[i], b = uniqueItems[j]; const key = a < b ? a + '_' + b : b + '_' + a; if (!coCount[key]) coCount[key] = 0; coCount[key]++; } } } // 最后套余弦公式,结果存 Map }

注意这里的关键细节:同一用户行为列表里必须先去重。如果某个用户把红玫瑰买了 5 次,不去重的话,“红玫瑰与其他鲜花”的共现次数会被放大 5 倍,这会严重扭曲相似度矩阵。

2.3 UserCF 与 ItemCF 在鲜花场景下的对比选择

既然我们最终是混合策略,那就得明确两种算法各管哪一段。

UserCF 的逻辑是“看和你品味相似的人买什么”。在鲜花店这个场景,它的有效场景是:新客冷启动和用户生命周期较短的个人化场景。比如毕业季集中出现一批新用户,这些用户可能都是第一次在某平台买花,行为稀疏。这时我们很难用 ItemCF 找到关联(因为他们还没买过任何东西),但可以借助注册时选择的“购买用途”(送恋人/送朋友/家居装饰)快速聚类,找到同一类用途下的热门商品做初步推荐。

ItemCF 的逻辑是“看你买过的东西,推关联的东西”。它的有效场景是:复购率较高的日常鲜花消费。比如用户 A 上周买了向日葵,系统发现“向日葵”和“洋甘菊”在历史行为中经常被同时加购,于是推荐洋甘菊。这类推荐解释起来也非常自然——“和向日葵搭配的洋甘菊”,用户容易接受。

在实现上,我建议把两种算法结果按比例融合。权重分配可以参考:

  • 新注册用户(行为数 < 5):UserCF 结果 70%,热门榜兜底 30%
  • 老用户(行为数 >= 5):ItemCF 结果 70%,UserCF 结果 20%,热门榜 10%
  • 已购商品且处于特殊节日(情人节/母亲节):在原有结果基础上,把节日品类鲜花权重上调 30%~50%

这个比例不是拍脑袋定的,而是在 A/B 测试里反复调出来的。初始可以按上面这个值跑,后面再根据点击率和转化率微调。

3. 实操过程与核心环节实现

3.1 技术选型与项目初始化

先确认技术栈:Spring Boot 2.7 + MyBatis-Plus + Redis + MySQL(或 PostgreSQL)。算法层不需要单独的服务,直接在 Spring Boot 里跑定时任务即可。

为什么选 Spring Boot 而不是其他轻量框架?核心原因:Spring Boot 在 Java 技术栈里生态最完善,毕业设计、简历项目、中小公司内部系统落地,大家最认的也是这一套。而且 Spring Boot 自带的定时任务(@Scheduled)可以方便调度离线计算,Spring Data Redis 可以轻松缓存相似度矩阵,集成成本很低。

项目初始化时,我习惯按下面的目录分层:

src/main/java/com/example/flower/ ├── controller/ # REST 接口层 │ ├── FlowerController.java │ ├── OrderController.java │ └── RecommendController.java ├── service/ # 业务逻辑层 │ ├── RecommendService.java │ ├── ItemSimilarityService.java │ └── UserBehaviorService.java ├── mapper/ # MyBatis 数据访问层 ├── model/ # 实体类 └── config/ # Redis、线程池等配置

3.2 离线相似度计算的实现策略

这是整个推荐系统的“发动机”。初期别一上来就上复杂的流式计算框架,直接用 Spring Boot 的 @Scheduled 定时任务就能跑通。

我这里给出一版可落地的实现流程:

第一步:拉取近 30 天的有效行为数据。注意“有效”二字。行为类型取浏览、加购、下单、收藏四类;去重规则按用户 + 物品维度;时间窗口取 30 天是为了兼顾推荐的时效性和数据的稀疏度。鲜花这类强节日属性的商品,如果只看近 7 天,数据会稀疏得惨不忍睹;如果看一年,又会把季节性的流行元素错误地固化下来。

第二步:构建倒排表和共现矩阵。按 2.2 节伪代码逻辑,基于全量用户行为,算出任意两两物品之间的共现次数。

第三步:计算余弦相似度矩阵。共现次数归一化后,得到的相似度矩阵可能非常大。假设鲜花 SKU 有 1000 个,矩阵规模就是 100 万条,还不算特别大。但为了性能,我会做两个优化:

  • 只保留相似度排名前 50 的物品作为邻居。对每件商品,只需知道“最像它的 50 件商品”就够了,推荐时从这 50 件里取候选集,效果和取全量几乎一样,但计算量少了一个数量级。
  • 结果缓存到 Redis。用 hash 结构存储,key 是物品 ID,field 是邻居物品 ID,value 是相似度值。在线接口直接读 Redis,避免每次请求都查数据库。

第四步:生成候选集并过滤。根据用户的历史行为,从相似度矩阵里取 TopN 候选商品,然后做过滤:

  • 商品状态必须为“上架”
  • 库存必须大于 0
  • 排除用户已经购买过的商品(避免重复推荐)

这一步千万别省。协同过滤的原始输出是“数学上最相似”,但“数学上相似”和“用户能买”是两回事。

3.3 在线推荐接口的两种输出模式

在线推荐接口是给前端调用的。我把它设计成两个模式,方便不同页面使用:

模式一:猜你喜欢(首页)

输入参数:userId。后台逻辑:读取用户最近 10 条有效行为 → 取行为物品的相似邻居 → 按相似度加权排序 → 过滤 → 输出 20 条。

这里的核心是“按相似度加权排序”。简单说,用户最近对多束花有行为,每束花都有自己的相似邻居列表,把这些邻居的相似度得分按“加权累加”的方式求和,然后按累加得分排序。同一件商品如果同时是用户看过和买过的两束花的共同邻居,它的得分就会叠加,排名更靠前。

模式二:看了又看 / 买了又买(详情页)

输入参数:userId + 当前商品 ID。后台逻辑:直接取当前商品的 Top50 相似邻居 → 根据用户行为降权(已购商品排除) → 输出 10 条。

模式二相比模式一简单很多,但对用户体验的影响非常大。用户在详情页停留时,推荐位就在手边,推得准,加购率会显著提升;推得不准,用户体验会直线下降。

3.4 冷启动问题的兜底方案

冷启动是这个项目无论如何绕不开的大坑。新用户没有任何行为数据,协同过滤算法直接“哑火”。我采用的是四级冷启动降级策略:

第一级:基于画像的粗推荐。注册时让用户勾选“购买意图”,比如:送恋人、送朋友、家居装饰、商务活动、节日问候。后台把意图映射到鲜花品类,直接推荐对应品类下的热销 TOP 20,按销量排序。

第二级:实时行为探索。用户刚点了某一束花的详情页,立刻根据详情页所属品类,扩大推荐范围,推荐同品类下相似价位段的其他花材。这个策略不需要历史数据,只看当前会话行为,见效非常快。

第三级:热门榜兜底。按最近 7 天销量 + 加购率综合排序,生成热门榜单。这榜可不只是“红玫瑰满天星”那种大热搭配,我加了用户评分维度,把热度和品类的多样性结合起来,避免用户以为花店全是玫瑰。

第四级:节日主题推荐。鲜花是强节日属性的品类。平时推“红玫瑰”可能只有情侣会点,但情人节前一周,所有人都在搜它。我在配置表里维护一个节日词库和关联 SKU 池,在节日窗口期内,把推荐池的权重从“纯个性化”切换为“个性化 × 节日时期权重”。

这套冷启动方案跑下来,新用户推荐位的点击率能明显改善,不会出现“第一次打开,推荐位全空白”的尴尬局面。

3.5 推荐结果的后处理:去重、打散与解释

算法算完只完成了一半,后处理才是决定推荐结果“看起来聪明不聪明”的关键。

去重是必须盯住的细节。用户买了一束红玫瑰,系统第二行推荐位又是红玫瑰,第三行还是红玫瑰——这是最典型的翻车现场。原因在于相似度矩阵里,某件商品常常和自己高度相似(和自身相似度恒等于 1),如果代码里过滤不干净,自己就会被当成自己的推荐结果。我处理时,在过滤阶段无条件剔除用户已经产生过行为的商品 ID,同时在相似度矩阵构建时,把物品和自己的相似度强制设为 0。

打散重排。协同过滤天然倾向于推“相似的一类花”,如果用户最近一直买玫瑰,那推荐结果可能整页都是玫瑰系列。这类推荐虽然“精准”,但会视觉疲劳,也会压缩品类覆盖率。我一般按品类做打散:同一品类的商品在相邻位置最多出现 2 个,下一位置强制切换品类。

推荐理由生成。推荐系统的“解释性”往往被忽略,但对转化率影响巨大。同一束洋甘菊,裸推和带一句“因为你买过向日葵,猜你也喜欢这种小清新花材”的带解释版本,加购率可以差出一大截。我实现时,是根据行为类型来决定推荐理由模板:

  • 基于购买行为的推荐:模棱两可不用,直接写“常买花人群喜欢搭配:”
  • 基于加购行为的推荐:写“加购了向日葵的人,也收藏了:”
  • 基于热门榜兜底的推荐:写“最近大家都在买:”

推荐理由的本质是把协同过滤的“数学关联”翻译成人话,这一步虽然不涉及复杂算法,但绝对值得花时间打磨。

4. 常见问题与排查技巧实录

4.1 相似度矩阵“全零”或推荐结果为空:多半是数据管道断了

这个现象在项目联调阶段经常出现,尤其是我刚把订单表和用户行为表关联起来的时候。排查路径如下:

  1. 先查行为表有没有数据。“用户行为”不是凭空产生的,如果前端没有正确埋点或者后端接收接口报错,行为表就是空的。协同过滤算空气,结果当然也是空的。
  2. 确认行为表的用户ID有没有正确关联。如果订单表里用的是 user_id,而行为表里的 userId 是前端自己生成的匿名 ID,两者对不上,就会导致系统把新用户当成全冷启动状态。
  3. 查看定时任务日志。相似度计算任务如果因为数据库连接超时或内存溢出中断,矩阵就不会生成。此时 Redis 里没有缓存,在线接口会直接返回空集。

排查思路:先在行为表里执行一个最简单的统计 SQL,查一下最近一周有多少条行为记录、涉及多少用户、多少商品。如果这个数字为 0,问题一定在数据源头,别急着优化算法。

4.2 相似度计算正确性校验:用可解释的商品对做回归测试

协同过滤算出来的相似度矩阵对不对,很难用肉眼直接证明。我的做法是:抽几对“人人都知道应该相似”的商品,验证计算结果是否符合直觉。比如:

  • “红玫瑰”和“粉玫瑰”的相似度应该显著高于“红玫瑰”和“君子兰”
  • “满天星”作为最常见的搭配花材,应当和多类花束都有较高的相似度
  • “向日葵”和“向日葵花束”这种同源商品,相似度应该接近 1

如果这些指标不符合预期,优先怀疑三点:数据去重有没有做好(同一用户对同一商品的多次购买是否污染了统计);时间窗口是不是抓错了;是否有“幽灵用户”或“刷单用户”在干扰数据。

针对第三点,我还会做一个前置的数据质量清洗:同一个用户若在短时间内对同一件商品的行为次数超过 10 次且全是浏览/加购,那大概率是爬虫或刷子,这批用户的行为应该整体剔除,避免污染离线相似度矩阵。

4.3 推荐结果全是头部热门商品:罚热门逻辑没生效

如果你做出来的推荐结果永远都是店里那几款销量最高的花,那问题基本出在余弦相似度公式或者后处理环节。

前面说过,余弦相似度用“除以几何平均数”来惩罚热门商品。如果你嫌这个惩罚力度不够,可以用更激进的方式,即在相似度公式里再乘以一个“热门惩罚因子”:

sim(i, j) = 共现次数 / (sqrt(商品i的热度) × sqrt(商品j的热度))

其中商品热度 = 购买人数 + 加购人数 × 加权系数。热度越高的商品在分母里越大,相似度被压得越低。真正常用的降热门技巧还有“对热门商品设置相似度上限”,比如最热的 5% 商品,相似度得分超过 0.8 时统一截断为 0.8。这个技巧能有效防止头部商品霸屏。

4.4 推荐结果“不够新”:加入时间衰减因子

鲜花是一个时效性很强的品类。用户三个月前买的圣诞主题花束,和现在的关联度已经很低。如果不做处理,过期的节日花材会一直出现在推荐列表里。解决方法是给行为一个时间衰减权重。核心思想是“越近的行为对当前推荐影响越大”:

权重 = 行为类型基础权重 × exp(-流逝天数 / 衰减周期)

衰减周期我取 30 天。也就是说,30 天前的行为对当前推荐的贡献只有当时的 36.8%(e 的负一次方),60 天前的贡献只有 13.5%。这样既保留了历史长期偏好(比如用户真的很喜欢玫瑰),又不会让“一条来自三个月前的搜索记录”把推荐结果带偏。

4.5 推荐位点击率评测:首次上线这样看数据

我给这个项目跑评测时,没有一上来就搞复杂的 A/B 测试平台,而是用最朴素的方法:在推荐位上做埋点,统计曝光量和点击量,算点击率(CTR)。

上线第一周,我会关注三个指标:

指标计算方式正常范围参考
推荐位点击率 CTR推荐位点击量 / 推荐位曝光量3% - 8%(鲜花类目)
加购转化率推荐位加购次数 / 推荐位点击量15% - 25%
推荐位成交占比推荐位产生的订单 / 全站订单10% - 20%

如果 CTR 低于 2%,通常说明推荐位位置不显眼或者推荐结果和用户意图不匹配,先检查推荐结果是否过冷或过热,再调整推荐位 UI 样式。如果 CTR 正常但加购转化率低,大概率是推荐的商品本身没打中用户需求(比如推荐了相似但价格过高/过低的商品),这时候要检查价格段过滤条件是否合理。

5. 应用场景扩展与实战经验总结

5.1 这套推荐系统还能迁移到哪些场景

做项目的过程中,我发现这套基于协同过滤的推荐骨架非常通用,只是换一个数据集就能平移到别的垂直零售类场景。比如:

  • 宠物用品店:用户给猫买了猫粮,系统推“猫罐头”“猫条”,协同过滤的关联逻辑一模一样。
  • 茶叶店:买了龙井的用户,大概率也会看“茶具”“茶点”,这种饮茶场景下的共现规律,ItemCF 能很轻松地抓到。
  • 手作文创店:用户买了羊毛毡工具包,系统可以推“配件补充包”“新手教程书籍”,这类消费频次不高的品类,恰好适合用冷启动策略中的“品类探索”。

迁移时只需要重新设计“用户行为权重”和“商品分类体系”,协同过滤的核心代码一行都不用改,这算是这套方案最大的长期价值。

5.2 推荐系统团队协作中的坑

这项目看起来不大,但实际开发时很容易陷入一个协作陷阱:算法工程师沉迷优化模型,却忘了和后端开发对齐“推荐位曝光埋点”的接口协议。结果就是模型指标一路向好(离线 AUC 很高),但前端口径不对,线上效果完全评测不了。

我在这类项目里的体会是:从第一天开始,就要把“推荐结果字典”的字段定义清楚。前端要展示什么,后端必须返回什么,形成一份专门的接口约定文档。比如我给前端返回的字段包括但不限于:

返回字段说明
itemId商品 ID
itemName商品标题
categoryId分类 ID
price展示价格
recommendReason推荐理由文案
coverImage商品主图
score推荐得分(前端不需要,但方便排查)

5.3 这是我实操后最想留给你的一句话

老实说,市面上很多推荐系统的教学项目,都默认把数据准备得很干净、数据集很规整,但真实电商场景不是那样。鲜花店的用户行为数据稀疏、嘈杂、充满异常点,百分之七十的精力其实都花在把数据洗干净、把兜底策略做扎实、把推荐结果调得“像人想的”上。

如果你照着这个思路把行为表建好、把 ItemCF 落地、把冷启动和兜底策略补齐,这套系统已经足以应对绝大多数中小型推荐场景的面试答辩和业务上线需求。至于什么深度模型、实时特征、图神经网络,那都是后话——先把这块地基打牢,比什么花架子都管用。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进