民宿推荐系统:Hadoop生态与协同过滤融合实践

做民宿推荐系统那年,我一开始还在用最原始的方式:SQL 排序加规则匹配,热门房源优先,新用户按城市推荐,老用户就按收藏量拉一波。数据量小的时候还能应付,等到用户行为日志一天涨到几十万条,点击、收藏、下单分散在几张表里,规则已经筛不出什么东西了。没有办法,只能老老实实把 hadoop + spark + kafka + hive 这套组合搬过来,配合协同过滤推荐算法做双推荐,把民宿推荐系统彻底重写了一遍。这篇文章就记一下整个落地的思路、踩的坑和调优过程,给同样打算做民宿、酒店或者本地生活类推荐的朋友做个参考。

这个系统适合谁来读?一种是刚入门大数据、想知道 Hadoop 生态到底怎么串起来干活的人;另一种是做推荐系统但数据链路比较粗糙、想看看离线加实时怎么配合的开发者。我不会讲太泛的理论,尽量以民宿推荐为主线,把数据链路、算法设计、可视化看板这些环节都串一遍。

1. 民宿推荐系统为什么必须上 Hadoop 这套组合

1.1 民宿业务场景与推荐需求

民宿推荐跟电商推荐有一个很大的不同:民宿是典型的低频高客单价消费品。用户不会像买日用品一样天天买,但一旦有出行计划,会在短时间内集中产生大量行为——搜城市、看图片、对比价格、收藏、咨询房东、最后下单。

这个场景带来的直接问题就是行为数据分布极不均匀。大部分用户一年只产生几次有效行为,少部分用户会在几天内密集操作。同时民宿本身也有很强的地域属性,杭州的民宿和成都的民宿几乎没有可比性,跨地域的推荐基本没有意义。

我当时的推荐需求基本可以拆成三类:

  1. 新用户冷启动:用户刚打开 App,只知道他选了一个城市,其他什么都不清楚,这时候应该推什么。
  2. 老用户个性化推荐:用户过去收藏过、浏览过一些房源,系统要基于历史行为猜他接下来可能喜欢什么。
  3. 实时兴趣捕捉:用户正在看某个房源,能否在浏览详情页的时候快速给出“相似房源”推荐,而不是等他退出之后再刷新整个列表。

这三类需求,靠单机 SQL 根本玩不转,所以才有了后面的技术选型。

1.2 Hadoop、Spark、Kafka、Hive 在系统里的分工

很多人在学大数据的时候容易被各种组件名称绕晕,觉得 Hadoop 就是一个东西。实际上在民宿推荐这个项目里,每个组件都有非常明确的分工:

组件在系统里的角色解决的具体问题
Hadoop HDFS分布式文件存储存放原始行为日志、Hive 数据仓库文件、模型训练中间结果
Hive离线数据仓库清洗原始日志、构建用户行为宽表、计算离线推荐需要的评分特征
Spark离线计算引擎训练协同过滤模型、跑大规模相似度计算、批量生成推荐候选集
Kafka实时消息队列收集用户实时行为事件,连接离线链路和实时链路
Spark Streaming实时计算引擎消费 Kafka 行为流,生成实时推荐候选并写入缓存

我见过不少团队在这个环节犯一个错误:把 Hadoop 和 Spark 当成同一个东西来用。其实 Hadoop 是存储加资源调度,Spark 是计算引擎,Hive 是构建在 Hadoop 之上的 SQL 层。项目里真正负责算的,主要还是 Spark 和 Hive 的 SQL 查询。

1.3 离线链路与实时链路怎么分流

民宿推荐系统的数据链路,我把它画成两条线:一条是离线链路,一条是实时链路。

离线链路的逻辑是:原始日志落到 HDFS → Hive 按天分区清洗 → 生成用户行为宽表 → Spark 跑协同过滤训练 → 输出离线推荐结果写到 Redis 或数据库 → 推荐服务读取。

实时链路则是:App 埋点 → Kafka → Spark Streaming 消费 → 合并用户最近行为 → 基于离线相似度结果做实时召回 → 返回相似房源列表。

两条链路在候选集层面做了一个融合:离线结果负责“稳”,覆盖大部分常规推荐场景;实时结果负责“新”,捕捉用户当前正在看的房源带来的即时兴趣。

为什么必须拆成两条链路?因为协同过滤模型全量训练一次,在我当时的集群规模下要跑一两个小时,不可能每次用户点击都重新训练。但用户的行为又不能忽略,所以就用 Kafka 接实时流、用在线计算补上离线模型的延迟,这是最务实的做法。

2. Hive 在民宿数据仓库里的具体打法

2.1 民宿数据有哪些,评分体系怎么定

推荐系统的基础是数据,数据的好坏直接决定算法的上限。先看一下民宿业务里有哪些数据需要接入。

民宿域的数据大概分四类:

  1. 房源主数据:房源 ID、城市、区域、经纬度、价格、房型、设施标签(比如近地铁、可做饭、有投影)、评分、房东信息。
  2. 用户主数据:用户 ID、注册城市、历史订单、常住偏好。
  3. 用户行为数据:浏览房源、点击图片、收藏、分享、咨询房东、下单。
  4. 订单数据:下单时间、入住时间、离店时间、实付金额、取消订单。

这里最核心的一步是定评分规则。协同过滤的输入是“用户-物品-评分”矩阵,民宿没有显式评分,必须从隐式行为里构造。我当时用的是行为加权法:

行为类型分值说明
点击进入详情页1表示初步感兴趣
收藏民宿3表示有较强意向
咨询房东4表示进入决策阶段
下单支付5强正反馈
下单后退款/取消1弱化原来的正反馈

这个评分体系不是拍脑袋定的。我当时的思路是:点击只是最浅层的兴趣信号,收藏和咨询对民宿来说非常关键——低频消费品里,用户愿意收藏说明出行计划基本定了,这时候推荐价值最高。降低取消订单的权重,是为了防止用户误下单拉偏模型。

2.2 ODS、DWD、DWS 分层与建表设计

Hive 数据仓库我分了标准的三层。很多初学者喜欢把原始数据直接查来查去,不对表做分层,看起来省事,实际上后面跑推荐任务的时候维护成本极高。

ODS 层就是原封不动的原始日志。这一层表按天分区,保留所有字段,方便回溯和排查问题。我建了一张 ods_user_behavior_log,每天跑一个 Flume 任务把日志写进去,分区字段是 dt。

DWD 层做清洗和规范化。我在这层做了几件事:去重、过滤异常值、统一字段格式、关联维表补全信息。比如用户行为日志里只有城市 ID,需要关联 ODS 里的城市维度表补上城市名称,方便后续按地域隔离推荐。

DWS 层是服务层,直接面向推荐任务和可视化。我建了三张宽表:用户行为聚合表、房源特征表、用户偏好特征表。这一层的表已经可以供 Spark、可视化看板直接读取了。

Hive 建表的细节比较重要,我贴一段当时 ODS 层的建表语句:

CREATE EXTERNAL TABLE ods.ods_user_behavior_log ( user_id STRING COMMENT '用户ID', house_id STRING COMMENT '房源ID', behavior_type STRING COMMENT '行为类型:click/favorite/consult/order/cancel', city_id STRING COMMENT '城市ID', scene_id STRING COMMENT '场景ID:首页信息流/搜索页/详情页相似推荐', device_type STRING COMMENT '设备类型:ios/android/h5', ts BIGINT COMMENT '事件时间戳', ext_info MAP<STRING, STRING> COMMENT '扩展字段,存放页面停留时长等' ) PARTITIONED BY (dt STRING COMMENT '事件日期,格式yyyyMMdd') STORED AS ORC LOCATION '/warehouse/ods/ods_user_behavior_log';

说两个经验。

第一,ODS 层一定要用外部表,location 指向日志落盘目录,分区目录由采集任务按 dt 创建。这样即使误删表也不会把原始数据丢掉。

第二,存储格式直接用 ORC,别用文本格式。民宿日志字段多,用文本存储既占空间又拖慢查询速度。ORC 的列式存储配合压缩,查询性能提升非常明显,尤其是后面要做全表聚合的时候。

2.3 Hive SQL 清洗与特征加工实战

分层建好后,实际的清洗和特征加工全部用 Hive SQL 跑。这里举两个最有代表性的例子。

第一个是行为数据去重。埋点日志经常出现同一用户同一时间对同一房源重复上报的情况。我的处理逻辑是:用 user_id、house_id、behavior_type 分组,ts 在 30 秒以内的记录只保留第一条。

INSERT OVERWRITE TABLE dwd.dwd_user_behavior PARTITION (dt = '20241201') SELECT user_id, house_id, behavior_type, city_id, scene_id, ts FROM ( SELECT user_id, house_id, behavior_type, city_id, scene_id, ts, ROW_NUMBER() OVER ( PARTITION BY user_id, house_id, behavior_type ORDER BY ts ) AS rn, LAG(ts) OVER ( PARTITION BY user_id, house_id, behavior_type ORDER BY ts ) AS prev_ts FROM ods.ods_user_behavior_log WHERE dt = '20241201' ) t WHERE rn = 1 OR ts - prev_ts > 30000;

第二个是房源特征加工。民宿的特征很多是标签型的,比如“近地铁”、“可做饭”、“有投影”,这些标签在原始数据里是字符串拼接的,我需要拆成多行再聚合。

SELECT house_id, tag, COUNT(*) AS tag_cnt FROM dwd.dwd_house_info LATERAL VIEW EXPLODE(SPLIT(tags, ',')) t AS tag WHERE dt = '20241201' GROUP BY house_id, tag;

这样处理后,每个房源就变成了一组标签向量,可以直接用于计算房源之间的相似度。

这一层最容易忽略的是数据质量监控。我当时加了一个简单但很有效的机制:每天清洗完 DWD 层后,跑几个条件检查——行为总数比昨天下降超过 30% 就报警、user_id 为空的比例超过 1% 就报警、单用户行为量超过 100 条就标记为异常用户。宁可多跑几个 MR 任务,也不要让脏数据悄悄流进训练集。

3. 双推荐算法:UserCF 与 ItemCF 的融合策略

3.1 为什么民宿场景适合双推荐

标题里写的“双推荐算法”,核心就是同时跑基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF),然后把两个结果按权重融合输出。

先看这两种算法各自的性格。

UserCF 的思路是“和你相似的人喜欢什么,我就给你推荐什么”。它先计算用户之间的相似度,再从相似用户的行为里找候选房源。UserCF 有个特点:推荐结果偏热门。因为相似用户群体大,行为交集多,候选房源容易集中在高曝光、高浏览的民宿上。

ItemCF 的思路是“你喜欢的房源相似,推荐其他类似房源”。它先计算房源之间的相似度,再从用户历史上喜欢过的房源出发,找相似房源作为候选。ItemCF 的推荐结果偏向个性化,能捕捉民宿的具体风格,比如用户看过一个带院子的乡村民宿,ItemCF 会推更多带院子的房源。

那为什么民宿场景要用双推荐而不是只选一个?

因为民宿用户的行为模式差异很大。有一部分用户是“探索型”——自己也不知道要什么民宿,看到别人收藏的网红房源就会去点,这类用户吃 UserCF 这一套。另一部分是“明确型”——心里已经想好了要带宠物、住 loft、离地铁站近,这类用户吃 ItemCF 这一套。单一算法只能覆盖一半人群,双推荐融合可以两头都照顾。

另外从工程角度说,双推荐也能兜底。UserCF 在用户行为稀疏时效果差;ItemCF 在新用户初期没有历史行为时完全失效。两个模型并行,各自的短板可以用对方的长板补上。

3.2 离线训练:Spark 实现两个模型的完整过程

离线训练这块我用的是 Spark + Scala。数据从 Hive 宽表读取,跑完训练后把结果写回 Hive 表,同时导出到 Redis 供线上推荐服务查询。

先说 UserCF。标准实现分三步:

第一步,构建行为评分表。直接从 Hive 把用户-房源-评分读成 RDD。

第二步,计算用户相似度。我当时用的是改进后的余弦相似度,只计算“共同评分过至少 2 个房源”的用户对。公共分组的 RDD 操作要注意数据倾斜,后面专门说。

第三步,产生推荐。对目标用户 u,找到最相似的 K 个用户,把这些用户评过分的房源做加权汇总,得到候选房源评分。

核心代码结构如下:

// 读取用户-房源-评分 val ratings = spark.sql( """ SELECT user_id, house_id, score FROM dws.dws_user_house_score WHERE dt = '20241201' """ ).rdd.map(row => (row.getString(0), row.getString(1), row.getDouble(2))) // 计算用户评分向量 val userRatings = ratings.map { case (u, h, s) => (u, (h, s)) }.groupByKey() // 构建倒排表并计算用户相似度 val invertedIndex = ratings.map { case (u, h, s) => (h, (u, s)) }.groupByKey() val userSim = invertedIndex .flatMapValues(users => users.toList.combinations(2)) .map { case (house, List((u1, s1), (u2, s2))) => if (u1 < u2) ((u1, u2), (s1, s2)) else ((u2, u1), (s2, s1)) } .reduceByKey { case ((s1_s1, s1_s2), (s2_s1, s2_s2)) => ((s1_s1 + s2_s1), (s1_s2 + s2_s2)) } .filter { case ((_, _), (a, b)) => a > 1 && b > 1 } // 至少共同评分2次 .map { case ((u1, u2), (dotProduct, _)) => val sim = dotProduct / (norm(u1) * norm(u2)) ((u1, u2), sim) }

ItemCF 的实现类似,区别在于把用户-物品矩阵转置成物品-用户矩阵,计算的是房源之间的相似度。房源相似度的计算比用户相似度更有业务意义:民宿具有强地域性,相似度计算时需要把地域信息作为重要维度加权进去。我当时给城市相同的房源相似度加了一个 0.5 的权重系数,实际效果比纯靠行为相似要好。

3.3 加权融合、冷启动兜底与 Redis 缓存

两个模型跑完之后,就是融合。我的融合公式非常朴素:

最终评分 = α × UserCF评分 + (1 - α) × ItemCF评分

α 的取值不是固定的,我在离线评估阶段尝试了 0.3、0.4、0.5、0.6、0.7 五个档位,对比各档位的精确率和召回率,最终选择了 0.45。意思就是 ItemCF 的权重稍微大一点,因为民宿推荐里房源本身的属性对用户决定的影响确实更大。

但光靠用户历史行为不够,因为冷启动问题很严重。新用户没有任何行为,新民宿也没有任何用户行为。这时候我加了一个兜底策略:

  • 新用户:根据用户选的城市直接推荐该城市热销榜 PLUS 一定比例的“小红书风格”民宿(高颜值、带拍照标签),用规则和热度解决冷启动。
  • 新民宿:给新民宿打上高质量标签(房东评分高、真实拍摄图多),在 ItemCF 的相似计算中加入“时间衰减因子”,新民宿即使行为少,只要与热门房源有相同的设施标签,也能出现在推荐列表里。

融合后的候选集最终写到 Hive 离线结果表,同步导入 Redis。线上推荐服务读 Redis 的时候,先试 UserCF 结果,再试 ItemCF 结果,最后试规则兜底结果,一层层降级。

这里要特别提醒一点:Redis 里存的不是全部候选,而是每个用户的前 100 个推荐房源 ID。一开始我存了 500 个,发现线上读取耗时明显变长,QPS 也上不去。后来压缩到 100 个,效果没有变差——因为用户根本看不完 100 个推荐,曝光位置就那么多。

4. Kafka 接入实时行为流与 Spark Streaming 实时推荐

4.1 民宿推荐里“实时”到底解决什么问题

离线推荐跑得再准,也存在一个天然缺陷:用户此刻的行为无法立刻影响推荐结果。比如用户刚才搜索了“杭州西湖区”,退回到首页,首页信息流还是继续推滨江区的民宿,这就很出戏。

民宿是典型的即时决策场景,用户可能在出行前一天才开始看,决策窗口只有几个小时。在这几个小时里,抓住每一秒的行为信号比三个月前的历史画像重要得多。这就是 Kafka + Spark Streaming 实时链路存在的意义。

我当时的实时链路只解决三件事:

  1. 用户最近 30 分钟浏览过的房源,避免推荐列表里重复出现。
  2. 用户正在浏览的房源,立刻算出相似房源并要求“两小时内刚看过的不再重复展示”。
  3. 用户收藏一个房源后,立即基于收藏房源扩展推荐。

不需要实时重训模型,只需要实时更新候选集。

4.2 埋点日志设计与 Kafka Topic 规划

埋点日志是实时链路的源头。我在 App 端定义了一个标准 JSON 格式的行为事件,所有行为统一走这个格式:

{ "user_id": "user_20241201_abc123", "house_id": "house_330106_xyz789", "behavior_type": "click", "page": "house_detail", "city_id": "330106", "scene": "similar_recommend", "duration_ms": 8500, "ts": 1733040000000 }

Kafka 我建了三个 Topic:

  • user_behavior_topic:接收所有用户行为事件,默认 3 个分区、副本数 2。
  • recommend_request_topic:接收推荐服务的实时请求事件,主要用于日志分析和效果追踪。
  • model_update_topic:离线模型训练完成后通知实时组件刷新缓存。

分区数的设计也踩过坑。刚开始只建了 1 个分区,因为测试环境数据量小没感觉。上了生产之后,Kafka 消费速度直接成了瓶颈。改成 6 个分区之后,消费并行度上来了,整个实时链路才算跑顺。

4.3 Spark Streaming 消费与候选集实时生成

实时端我用的是 Spark Streaming,消费 Kafka,处理逻辑并不复杂。

核心逻辑是:收到用户行为事件 → 更新 Redis 中该用户的最近行为窗口 → 根据行为类型触发对应的召回操作。

比如用户收藏了一套民宿,我要做两件事:

// 伪代码,说明实时处理逻辑 stream.foreachRDD { rdd => rdd.foreach { event => val userId = event.userId val houseId = event.houseId // 1. 记录用户最近行为,窗口存储为30分钟 redis.lpush(s"user:${userId}:recent_houses", houseId) redis.expire(s"user:${userId}:recent_houses", 1800) // 2. 从Redis读取该房源的相似房源(离线ItemCF结果已预计算) val similarHouses = redis.smembers(s"house:${houseId}:similar") // 3. 过滤掉用户近期看过的,生成候选列表 val recentHouses = redis.lrange(s"user:${userId}:recent_houses", 0, -1) val candidates = similarHouses.filterNot(recentHouses.contains) // 4. 写回推荐结果缓存,线上接口优先读取 redis.set(s"user:${userId}:realtime_recommend", candidates.mkString(",")) } }

这段代码是整个实时链路里最核心的部分。我故意把处理逻辑做得很轻,不在这里做复杂计算。原因很简单:实时链路的延迟要求极高,任何超过 200 毫秒的处理用户都能感知到。重计算全部交给离线模型预计算,实时端只做读取、过滤、写入,这样才能保证性能。

不过这套方案有一个前提:房源相似度表必须是预计算好的。也就是离线模型跑完后,把每个房源 top 30 的相似房源写进 Redis,实时端直接查就行。当时 Redis 里存了约 20 万套房源的相似结果,内存完全够用。

实时链路的性能调优我印象比较深的一点是批量写入。最初每次用户行为都单独调 Redis 写入,小流量下没问题,一旦流量涨到每秒几百个行为事件,Redis 连接数直接被打满。后来改成 Spark Streaming 的 foreachPartition 内用连接池批量写,单条延迟反而更低了。

还有一个注意事项:Kafka 消费的 offset 要妥善管理。我遇到过 Spark Streaming 重启后重复消费导致 Redis 里最近行为列表脏数据变多的问题。后来我改成手动提交 offset,并把 Redis 中每个用户的最近行为限制为最长 100 条,超过就裁剪,避免内存无限膨胀。

5. 民宿可视化看板:把 Hive 结果变成能用的运营看板

5.1 民宿运营方要看什么数据

推荐系统跑起来之后,业务方的第一个反应是:我怎么知道这套东西有没有用。所以可视化看板绝对不能是只给自己看的模型指标,而必须是业务方真正关心的一线数据。

我当时访谈了运营团队的几个核心角色,整理了看板需求:

看板模块关键指标数据来源更新频率
热门区域各城市民宿订单量、搜索量、热度指数Hive DWS 层每天
价格分布各城市价格带分布、均价变化趋势Hive DWS 层每天
用户画像新老用户占比、年龄段、出行场景偏好Hive ODS 层每周
推荐效果推荐位点击率、曝光量、转化率、推荐占比埋点日志 + 推荐服务日志实时(大屏)/ 每天(报表)
房东运营各区域房东活跃度、房源新上线统计Hive DWD 层每天

这个需求清单看起来简单,但实现过程有一个很关键的坑:Hive 里的数据不能直接给前端用。我走过一段弯路,刚开始让运营直接在 Hive 上跑 SQL,他们根本不会用;后来做了一个定时脚本把 Hive 结果导出成 CSV 再发到群里,其实效率也很低。

最终方案是:Hive 结果通过调度任务导出到 MySQL,后端服务读取 MySQL 接口,前端用图表库渲染。

5.2 Hive 结果导出与数据同步方案

Hive 到 MySQL 的同步,我用过两种方式,各有利弊。

第一种是用 Sqoop。适合大批量全量同步。比如把 DWS 层的用户画像表全量拉到 MySQL,Sqoop 一条命令就搞定。

第二种是用 Spark 程序做增量同步。适合列表类数据,比如每日推荐效果统计,只同步当天分区。增量同步的代码不复杂,用 JDBC 逐条写入太慢,可以用批量方式:

# 伪代码,batch insert 至 MySQL def write_to_mysql(df, table_name): df.write \ .mode("overwrite") \ .format("jdbc") \ .option("url", "jdbc:mysql://your-mysql:3306/recommend_dashboard") \ .option("dbtable", table_name) \ .option("user", "xxx") \ .option("password", "xxx") \ .save()

实际开发中建议用 DataX 这类工具,性能更好。我最初图省事直接用 Spark JDBC,数据量到百万级之后写入明显变慢,后来换成 DataX 同步,单表千万级的全量同步也只要几分钟。

5.3 看板可视化选型与刷新策略

前端可视化我们用了 ECharts,配置简单、图表种类多,热力图、散点图、折线图都能直接支持。民宿热力地图用 ECharts 的地理坐标系 + 散点图,把 Hive 里按城市聚合的房源数量、订单热度渲染到地图上,看起来最直观。

看板的数据刷新策略也要设计好。我当时的做法是:

  • 大屏页:每 5 分钟轮询后端接口,展示实时订单趋势和推荐转化率。
  • 日报页:每天早上 8 点由调度任务触发 Hive 计算,生成昨日全量数据后同步 MySQL,前端固定时间刷新。

这里有一个教训:不要让前端直接查 Hive。有人为了图省事,用后端服务直连 Hive 的 Thrift 接口,结果慢查询直接把 HiveServer2 拖挂了,影响了整个离线任务的调度。可视化数据一定要走 MySQL 这类关系型数据库做缓存层。

可视化本身不复杂,真正的复杂度在于数据口径的一致性。我前后跟运营核对了好几轮,才把“订单量”“推荐转化率”这些指标的定义统一掉。比如“推荐点击率”到底是推荐位曝光后点击算,还是从推荐列表点进详情页才算?这个问题不提前定清楚,后面每个看板数字都会引发争议。

6. 上线后的调参与踩坑记录

6.1 数据倾斜:Spark 跑相似度计算时的性能杀手

推荐系统上线后第一周,离线任务经常跑不完,最长一次跑了 3 个小时。排查后定位到数据倾斜。

现象很典型:所有 reduce task 里,有一个 task 长时间卡住,其他 task 早就跑完了。原因是民宿的热门房源上下线波动大,个别头部房源的行为数据量是普通房源的百倍以上。

比如西湖边的某家网红民宿,一天的行为量可能超过其他所有房源的平均水平。它在倒排表里对应的用户列表特别长,导致计算相似度时,那个 key 所在的分区严重过载。

我当时的解决办法是加盐打散:把这个热点房源的 key 加上随机后缀拆成多份,计算完再合并。伪代码如下:

// 伪代码,加盐打散处理热点key val salted = invertedIndex.map { case (houseId, userList) => if (isHotHouse(houseId)) { // 给热点房源的用户列表拆成多个分区并行处理 userList.grouped(100).zipWithIndex.map { case (group, idx) => ((s"$houseId#$idx", group), null) } } else { ((houseId, userList), null) } }

除了加盐,还有一个思路是过滤长尾。民宿行为数据长尾特别明显,很多房源只有一两条行为记录,这些记录对相似度计算没有贡献,反而拖慢任务。我在预处理阶段就做了过滤:行为数低于 3 的房源不参与相似度计算。跑完之后,离线训练时间从接近 3 小时降到了 50 分钟以内。

6.2 冷启动兜底与推荐质量的两难

冷启动问题在民宿场景比电商更尖锐,因为民宿具有很强的时效性。

一个城市每天晚上上新的房源数量有限,但用户往往是搜索特定区域、特定房型。如果纯粹依赖协同过滤,新上架的房源根本进不了推荐池,用户看到的永远是那几个老面孔。

我的处理策略有两条:

第一条是巧用相似度。新民宿虽然没有行为数据,但有设施标签、价格、房型。我事先对房源标签做了一次 cosine 相似度预计算,新民宿只要发布,立刻就能通过标签找到同城相似的老房源,从而获得初始的推荐权重。

第二条是控制推荐新鲜度。在最终排序阶段,我强制加入一个“新鲜度加分”:房源上架时间越新,分数加成越高。这个加成的权重不能太大,否则会牺牲点击率。我压到总分的 5% 左右,既保证了新民宿有曝光机会,又不至于影响整体个性化质量。

运营一开始反馈“推荐来来回回都是那几个”,我通过调高新鲜度权重解决了。但随之而来的问题是从推荐页点击进详情页的转化率下降了一点。后来用 AB 测试看数据,才把新鲜度权重调到一个良性区间。冷启动的兜底和推荐质量的平衡,本质是一个调参活。

6.3 模型评估:离线指标与在线指标的取舍

推荐系统的效果,我建议不要只看一个指标。离线评估我用的是准确率和召回率,在线评估主要看点击率。

离线评估的步骤很简单:把行为数据按时间切分,前 7 天作为训练集,第 8 天作为测试集,跑一遍推荐流程,看推荐的房源有多少比例出现在用户的真实行为里。

上线前我用这个方法分别测了纯 UserCF、纯 ItemCF、融合后的双推荐算法:

方案准确率召回率覆盖度
纯 UserCF6.2%9.8%23%
纯 ItemCF8.1%12.4%31%
双推荐融合9.3%14.7%44%

这个结果说明融合确实有效,尤其是覆盖度的提升非常明显——原因是双推荐用两个不同的视角找候选集,覆盖的房源范围远大于单模型。

不过离线的准确率再高,也不能完全代表线上效果。真实用户的行为受曝光位置、图片质量、价格排序等很多因素影响。我上线后做了 AB 测试,双推荐算法对比原来的热门规则推荐,推荐位点击率提升了大约 12%,详情页转化率也略有上升。这个结果验证了整套方案的可行性。

有一点值得强调:离线阶段的测试集时间窗口不要跨节假日。民宿是强季节性业务,节假日和非节假日的用户行为差异巨大,跨节假日评估会让模型的效果看起来特别差,误导调参方向。

6.4 线上服务降级策略与缓存管理

最后一个比较容易被忽略的环节是线上服务的稳定性和降级策略。

推荐服务是用户浏览首页时的第一接口,一旦挂掉,整个首页信息流都会出问题。所以我在设计线上推荐服务时,特意做了四级降级:

第一级,读取 Redis 中的双推荐融合结果。 第二级,读取离线 Hive 同步过来的全量推荐结果表。 第三级,读取 MySQL 中的热门民宿榜。 第四级,返回当前城市随机房源列表。

每一级数据源挂了,自动跳到下一级,保证用户始终能拿到可用的推荐内容。这个降级逻辑代码很简单,但价值巨大。上线后有过两次 Redis 抖动,推荐服务依然能扛住,靠的就是这个设计。

缓存管理上也走过弯路。最开始离线任务每天全量更新 Redis,但 Redis 里除了推荐结果,还存了大量实时行为数据,全量覆盖会把这些实时数据冲掉。后来我干脆换了一套方案:推荐结果和实时行为分开两个 Redis 实例,离线任务只更新推荐结果实例,实时链路只写行为实例,两者互不干扰。虽然多用了点内存,但逻辑清晰多了,排查问题也方便。

这套系统从设计到上线,花了大概一个半月。后端逻辑、离线任务、实时链路、可视化看板全部跑通的时候,我自己最大的感受是:大数据推荐系统的难点其实不在算法有多高深,而在数据链路能不能稳、组件之间能不能无缝衔接。hadoop + spark + kafka + hive 这套组合,每一环单独看都不算稀有,关键是怎么在民宿这个具体的业务场景里把它们串成一条完整的链路。

最后分享两个个人经验。第一,民宿推荐的权重调优,别迷信复杂的模型,先把 UserCF 和 ItemCF 的融合系数、相似度阈值、冷启动兜底策略这些基础参数调好,效果提升比换模型来得快。第二,可视化的数据口径一定要提前和业务方对齐,一套推荐系统上了线,如果没有业务方能看懂的看板,技术再先进也没有说服力。

相关新闻

python学习---归来仍是新人

python学习---归来仍是新人

目录 表达式 基本输入与输出 函数参数传递 交换两个变量的值 创建列表 数值类型&#xff1a; int float 复数 空值&#xff08;None&#xff09; 布尔型&#xff08;bool&#xff09; 字节型&#xff08;bytes&#xff09; 字符串 字符编码&#xff1a; Python语…

2026/10/20 2:11:22 阅读更多 →
如何快速修改汽车总线Graphic信号仿真值

如何快速修改汽车总线Graphic信号仿真值

定义 Graphic信号仿真值快速修改是总线工具INTEWORK-VBA Graphic窗口中的"信号快捷配置与观察"功能&#xff0c;它允许工程师在波形图页面直接选中目标信号并通过右键菜单唤起信号面板&#xff0c;无需切换至其他模块即可完成仿真信号参数值的修改。该功能解决了传统…

2026/10/20 2:11:22 阅读更多 →
OptiSystem仿真案例实战:从参数设置到10Gbps链路建模与避坑指南

OptiSystem仿真案例实战:从参数设置到10Gbps链路建模与避坑指南

简介&#xff1a;这份《OptiSystem仿真模型案例[汇编].pdf》面向光通信系统设计与仿真的初学者及工程技术人员&#xff0c;围绕OptiSystem这一专业光通信仿真软件&#xff0c;系统整理了从器件到系统的建模案例。内容覆盖光发送机、光接收机、光纤系统、光放大器、光波分复用系…

2026/10/20 2:10:18 阅读更多 →

最新新闻

大模型Skills应用实战:在Trae中把提示词变成可复用工作流

大模型Skills应用实战:在Trae中把提示词变成可复用工作流

这个系列写到第14篇&#xff0c;我聊点真正能提效的东西——大模型Skills应用。之前几篇要么在讲模型能力边界&#xff0c;要么在讲上下文工程&#xff0c;这次把视角落到一个具体场景&#xff1a;在Trae这款AI原生IDE里&#xff0c;怎么把Skill从"听说过"变成"…

2026/10/20 2:53:45 阅读更多 →
PC端缩放布局不崩:clamp()、视口字号、防撑破与Grid弹性收缩

PC端缩放布局不崩:clamp()、视口字号、防撑破与Grid弹性收缩

做 Web 开发的人&#xff0c;十有八九都被一个场景折腾过&#xff1a;你在 PC 上写了个布局&#xff0c;看着还挺满意。结果用户那边把浏览器窗口拉窄一点&#xff0c;或者按了Ctrl 滚轮缩放一下&#xff0c;整个页面瞬间就没法看了——侧边栏把内容挤成一团、文字溢出到屏幕外…

2026/10/20 2:53:45 阅读更多 →
OpenCode技能机制详解:从SKILL.md设计到团队共享实战

OpenCode技能机制详解:从SKILL.md设计到团队共享实战

1. 这份折腾的前提&#xff1a;OpenCode是什么&#xff0c;技能解决的是什么问题先说清楚我为什么会去研究这个&#xff0c;因为市面上叫"技能"或"Skills"的AI功能太多了。几个月前我还在终端里同时开着几个不同的AI编程工具&#xff0c;来回粘贴代码上下文…

2026/10/20 2:53:45 阅读更多 →
基于YOLOv5的动态增强遥感小目标检测方案

基于YOLOv5的动态增强遥感小目标检测方案

做遥感图像目标检测的时候&#xff0c;有一类问题特别让人头疼&#xff1a;高分辨率遥感图里目标很小、分布密集&#xff0c;通用检测模型跑起来一看&#xff0c;漏检的全是那些只有几十像素甚至十几个像素的小目标。我最初直接用现成的一套检测框架去处理&#xff0c;效果始终…

2026/10/20 2:53:45 阅读更多 →
BEV感知数据工程:数据集选择、标定时序与真值对齐

BEV感知数据工程:数据集选择、标定时序与真值对齐

做BEV感知这块时间久了&#xff0c;会有个特别深的感受&#xff1a;模型结构只是面子&#xff0c;数据工程才是里子。我接触过的几个环视检测项目里&#xff0c;模型选型、backbone替换、多尺度融合&#xff0c;加起来可能只花一周&#xff1b;真正耗进去一个多月的&#xff0c…

2026/10/20 2:53:45 阅读更多 →
Spring Boot公益服务平台开发实战:从数据库建模到答辩全流程指南

Spring Boot公益服务平台开发实战:从数据库建模到答辩全流程指南

最近帮几个学弟学妹把基于Spring Boot的公益服务平台从零到一完整搭了一遍&#xff0c;整个过程走了不少弯路&#xff0c;也沉淀了不少可以直接复用的经验。这类系统在课程设计和毕业设计里出现频率很高&#xff0c;一是业务场景贴近生活&#xff0c;二是功能边界清晰——用户注…

2026/10/20 2:52:44 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →