尧图精选

旅游推荐系统实战:从数据采集到排序模型的大数据全链路解析

🕒 发布时间:2026/10/1 22:59:04 📁 来源:尧图网络
1. 从“千人一面”到“千人千面”旅游推荐系统遇到的真实问题打开任何一家旅游App首页推荐无非是“热门景点TOP10”“周边游推荐”“XX网红打卡地”刷三天内容基本一样。这不是旅行App偷懒而是绝大多数推荐系统只解决了一个问题——“大家都在看什么”却完全没回答“你这个人现在到底想干嘛”。我接手这个项目时手里握着某个中型旅游平台的三类数据用户注册信息、点击/收藏/下单行为、景点与行程的基本属性但当时线上用的却是一套按城市热门度的静态榜单转化率一路掉到不足0.8%用户停留时长也在逐月走低。一个典型的用户场景让我印象特别深一位带着两个孩子的妈妈周末想出去走走但她打开App看到的却是“蹦极挑战一日游”和“夜爬XX山看日出”。她需要的其实是一个车程在1小时内、有儿童活动区、最好能推婴儿车的公园。这个需求靠榜单永远猜不到。这个项目要做的就是把“猜大家喜欢什么”升级为“猜你此刻喜欢什么”。借助大数据技术我们重新梳理了从数据采集、清洗、特征工程到推荐算法、在线服务的完整链路。这篇文章我会把整个系统的设计和实现掰开揉碎地讲一遍重点不在于告诉你“我们用了Hadoop、Spark、Flink”而在于每一个环节为什么要这样设计哪些地方是纯靠踩坑才学到的。无论你是大数据入门者还是已经接触过推荐系统的开发这里面的细节应该都有参考价值。2. 数据基建不能糊弄采集、清洗、特征工程的实战细节2.1 我们需要哪些数据从哪来旅游推荐比电商推荐更依赖“上下文”。电商看商品标签就能猜大概旅游却要结合时间周末/暑假、地点出发城市、同行人亲子/情侣/老人、天气、预算、历史偏好等一堆信息。我们最终把数据源拆成四类用户基础数据注册时填写的城市、年龄、性别以及通过授权获取的常驻地。注意年龄和性别只能当弱特征不能当主要依据。行为日志数据点击、搜索、收藏、分享、下单、评价每条记录带时间戳、会话ID、设备类型、页面位置。这是整个系统最核心的数据资产。景点与POI数据景点名称、所在城市、经纬度、星级、类型自然风光/主题乐园/博物馆等、门票价格、开放时间、评分、评论标签。外部补充数据天气接口、节假日日历、交通路况、甚至酒店价格波动这些会让推荐结果在“当下”更合理。当时我们遇到的最大问题不是“数据不够”而是“日志数据脏得离谱”。最典型的是同一个用户在App和H5上采集到的用户ID完全不同导致会话断裂还有爬虫伪装成正常用户一天刷几万次点击。如果不在清洗阶段处理掉后面所有统计都会失真。2.2 清洗规则宁可少一点不要错一点对于行为日志我定了几条清洗硬规矩去除同一会话内间隔小于1秒、连续点击超过50次的异常序列这类基本是机器行为。用户ID统一用设备指纹登录账号做交叉映射建立user_mapping表保证离线统计和在线服务看到的是同一个人。过滤掉“无效POI”下架超过90天的景点、未通过审核的商家、坐标明显偏移的脏数据。缺失值处理城市缺失用IP反查或手机号归属地补齐年龄缺失不做平均填充而是单独设置“unknown”标签避免引入虚假模式。这条规则执行后行为日志的有效率从58%提升到了91%。很多团队会在这里偷懒想着靠算法去容忍脏数据但我可以很负责地说数据基建不靠谱推荐系统的天花板也就锁死在那里了。2.3 特征工程才是真正的“个性化”来源算法模型决定了推荐质量的上限但特征决定了它能不能逼近这个上限。我们把特征拆成三大类用户特征基础属性年龄、性别、城市、长期偏好过去90天内点击最多的景点类型、平均消费水平、短期行为最近一次搜索关键词、当前会话浏览序列。景点特征景点本身的固有属性类型、价格带、评分、景区热度近7天UV、下单转化率、实时上下文当前天气、是否节假日、距用户出发地的车程。交互特征用户最近是否收藏过同类景点、是否已经去过该景点、同一行程中是否已包含某景点。这中间最容易被忽略的是“车程”。旅游推荐和电商有一个本质区别用户是有地理边界的。一个北京用户你给他推荐上海迪士尼就算他再喜欢也不可能今天下单明天去。所以我们把景点的“推荐半径”设成三档3小时车程内算强相关3~6小时算中等相关更远只靠长短期偏好驱动。这个半径值不是拍脑袋定的而是分析了历史订单中“从搜索到下单的时间差”和“出发城市到目的地距离”两个指标发现80%的即时型订单都发生在3小时车程内。特征全部存成稀疏向量格式用Parquet列式存储放在HDFS上。一开始我们用JSON存特征后来发现每次训练都要全量读一遍JSON的解析开销比计算本身还大更别说压缩率了后来全部迁到Parquet训练数据的加载时间直接减少了60%。3. 推荐算法不只协同过滤多路召回与排序模型的选择3.1 为什么不能只靠“猜你喜欢”很多人一提到推荐就想到协同过滤但协同过滤在旅游场景下有天然的坑它的前提是“历史行为能预测未来”可旅游行为大多数是低频、长周期、强意图的。一个用户可能一年只旅行两次他的历史点击大多贡献给“看风景”但你无法从他上次去杭州推断他这次想去三亚度假更别说他这次是想“遛娃”还是“团建”。另外纯协同过滤对冷启动几乎无能为力。新景点没有任何用户行为数据老用户如果换了城市也等于半个新用户。所以我们的方案是“多路召回 排序模型”的经典架构这也是当前推荐系统工程上最实用、最可控的做法。3.2 四路召回路每一路都有明确理由召回阶段的目标是从几十万个POI里快速筛出几百个候选不需要太精确但一定要“全”。我们设计了四路召回基于用户行为的协同过滤Item-CF计算“喜欢A的用户也喜欢B”这条规则。对于老用户、热门景点效果好是召回主力之一。基于用户画像的召回把用户三分之的长期偏好标签如“亲子”“户外”“自然风光”“历史人文”和景点的标签做匹配保证推荐结果契合用户基本盘。基于地理位置的召回以用户常驻地为中心按车程半径圈出候选池再叠加热门度降序。这路召回主要解决出游可行性问题。基于会话的实时召回用户当前正在浏览什么、搜索了什么就用相似景点做Top N。这样用户上一秒搜“杭州亲子”下一秒推荐的就会是杭州的动物园、科技馆。每路召回的数量级控制协同过滤1万条画像5000条地理3000条会话2000条四路合并后按权重去重最终保留800个候选交给排序模型。3.3 排序模型从LR到GBDT再到重排序的取舍排序阶段我们最开始用的是一套逻辑回归LR模型线上效果比原来的静态榜单提高了12%但提升很快就遇到了瓶颈。原因是LR对非线性关系几乎无能为力而旅游推荐里恰好充满了交叉特征用户是“亲子”标签 景点是“主题乐园” 今天是“周六”这三者的交互信息LR很难自动学到。后来换成了GBDT具体用的是LightGBM线上A/B测试显示点击率相对LR又提升了8.7%。LightGBM对稀疏高维特征支持得不错训练速度也比XGBoost快很多而且不需要做特征归一化非常适合我们这种“先上路再迭代”的团队。但GBDT也有自己的毛病它学不到用户和景点向量的内部相似度所以我们在GBDT之上又加了FM做二阶交叉的残差学习整体效果再提升了1个百分点左右。重排序阶段我们虽然尝试过用深度学习模型一开始试了DIN依赖用户行为序列建模但最终没有全量上线原因是数据规模还不够训练出来的收益在AB测试里不稳定。这个决策很重要不要因为“别人都在用深度学习”就盲目上推荐系统的提升必须是可量化的不能靠感觉。最终线上排序服务的延迟控制在50ms以内用的是“截图最近一次离线训练好的模型 实时特征拼接”的方式没有上真正的在线学习。对于旅游这种低频业务在线学习的收益还不明显先把离线做扎实更重要。4. 大数据管线搭建从Hadoop到Spark再到实时兜底4.1 项目的总体架构分层如果画一张架构图这里就不画了文字描述可以按大数据处理的经典四层来拆数据接入层、数据存储层、计算层、服务层。接入层前端埋点日志通过Kafka实时写入业务数据库的变更通过Canal同步到Kafka。存储层原始日志落HDFS明细数据用Hive分区表特征和维表用HBase供在线服务快速Get。计算层离线批处理用Spark主要是Spark SQL实时特征用Flink做轻量计算模型训练直接跑在Spark on YARN上。服务层召回和排序服务用Java编写通过Redis缓存热数据加载HBase里的特征和模型文件做推理。这套架构看起来不算“高大全”但胜在每一层都有明确的取舍逻辑。比如没有用Hudi或Iceberg做实时数仓是因为我们短期内没有流式写入Hive的硬需求KafkaFlink维度表的方式已经够用了。4.2 离线批量计算Spark任务如何设计才算“收集”我们每天夜里12点跑一次全量特征更新主要包含三块计算特征聚合任务计算用户近1天/7天/30天的行为统计景点近1天/7天的热度、转化率。这里用Spark的groupByKey会有性能坑我改用reduceByKey或RDD的aggregateByKey减少shuffle数据量。用户画像任务基于用户行为序列用TF-IDF的思想提取用户标签权重。例如用户点击“动物园”5次、“博物馆”2次则“动物园”标签权重高于“博物馆”但要注意按天衰减否则10年前的爱好会把现在的影响盖掉。相似度矩阵任务计算Item-CF的共现矩阵。这里有一个优化经验只统计“同一个会话内同时出现”的POI对而不是“同一个用户所有历史中同时出现”因为会话内共现更能代表用户当下的决策意图而且计算量直接降了一个数量级。这些Spark任务我们统一用Spark SQL DataFrame API写而不是纯粹的RDD算子。原因很简单DataFrame的Catalyst优化器会自动做谓词下推和列裁剪对于95%的分析场景已经足够高效代码还得短一半。刚开始团队里有人习惯写RDD后来我们把规范定成“能用SQL就用SQLSQL搞不定的再用算子”整个任务的平均运行时间降了四成。4.3 实时特征与在线服务衔接的那些细节虽然我们没上完整的实时训练但实时特征这块是必须的。举个例子用户当前会话里搜索了“厦门 亲子”过了两分钟又点了“鼓浪屿”可是还没下单。如果我们只在离线特征里看到历史点击就错过了“这一刻他明显对亲子游有强烈兴趣”。我们的Flink任务从Kafka消费行为日志在内存里维护每个会话最近20条行为序列按规则引擎输出一份“会话实时标签”写入Rediskey是userIdsessionId过期时间2小时。在线推荐接口读取Redis里的实时标签替代离线画像中的对应部分就能做到“用户刚搜完什么推荐列表里立刻出现相关景点”。这里要特别提醒一个坑实时特征和离线特征会发生冲突。举例说用户离线画像显示“偏好自然风光”但实时会话里他正在搜索“城市乐园”如果直接用实时标签全覆盖式替换离线标签会让长期偏好完全失效。我们的做法是设了个优先级实时标签权重为3离线标签权重为1最后排序特征里两个都用让模型自己决定谁更重要。4.4 集群规模与部署注意事项这套系统当时跑在一个6节点的Hadoop集群上3个DataNode 1个NameNode 2个TaskNode内存128G磁盘4T。很多人一听“大数据”就觉得得几百台机器其实对于日活几十万的旅游平台6台机器绰绰有余。当然前提是设计任务时不能乱拉全量数据尽量做分区裁剪和采样优化。部署踩过的最大坑是内存溢出的问题。最初我们给Spark Executor分配的是8G内存但运行特征聚合时频繁出现OOM后来发现是数据倾斜造成的——某些热门城市比如北京、上海的POI数据量是普通城市的几十倍按城市聚合时这几个key的task直接被压爆。解决方案有两层第一层是给Spark任务加上spark.sql.adaptive.skewJoin.enabledtrue自动拆分倾斜的join第二层是业务上把热门城市数据单独分桶冷门城市走另一个小任务跑完后合并。这个方法把任务成功率从76%拉到了99%。5. 评测与上线别让推荐指标骗了你5.1 线下评测我们为什么要看“脑平台”而不敢只看点击率做推荐系统最怕的就是被单一指标绑架。一开始我们也紧盯点击率Re召回率甚至AUC但后来发现一个残酷的事实点击率涨了订单转化却没涨多少。为什么因为用户可能会点进一个吸引眼球的“坑爹景点”发现评价很差根本不打算下单。这意味着我们的模型学会了“诱导点击”而不是“促成决策”。所以我们把评测指标拆成两层行为指标点击率CTR、人均点击次数、停留时长。业务指标下单转化率CVR、人均下单金额、下单后取消率。线下评测的时候除了AUC我们额外设计了一个“成单倾向指标”用历史下单用户的行为序列作为正样本训练一个单独的CVR预估模型同样用LightGBM然后用这个模型给推荐列表打分看线上推荐Top10被这个CVR模型打出的平均分。这个舞台指标和最终业务指标高度相关比单纯看点击率靠谱得多。这里也给个小建议不要直接用“AUC越高越好”来判断推荐算法好坏。AUC是整体排序质量但影响用户体验的是头部推荐质量比如NDCG10、NDCG20更适合观察Top结果的精度。我们当时迭代排序模型时AUC只升了0.3个点但NDCG10提升了4.8个点后者更让我有信心。5.2 AB测试的黄金准则样本人群不洗脸测试时间长一倍我们上线新模型从来不做全量切换强制要求做AB实验至少跑2周。很多人觉得用户量大跑个3天就能出结果但我们发现旅游App的“决策周期”很长用户可能周一看到推荐周末才下单如果你只跑3天明天下单的那批人压根没被算进去。我们自己的统计显示从首次点击到下单平均要4.7天所以两周都是保守的。AB测试的分组必须做到用户维度隔离不能同一个用户今天在实验组、明天在对照组否则会串数据。分组算法我们用userId取模保证稳定同时每个实验都要设“空跑期”也就是模型上线前先推全流量但不改变线上逻辑看实验组和对照组的基础指标差是否在0.5%以内如果差太多说明分流有问题重新洗牌。5.3 上线的最后一公里Redis缓存与模型热升级在线推荐服务的数据链路由三步组成请求参数 - 拼装特征向量 - 加载模型打分。其中特征拼装是从HBase读取模型文件每次请求都加载必然不行我们做了两层缓存特征缓存用户特征和景点特征的实时查询量很大把最近30分钟的热点特征存到Redis过期时间根据业务调整。模型缓存LightGBM模型文件不是很大200MB左右加载到进程内存里每天凌晨自动更新。同时保留上一版模型如果发现当天新模型的特征分布异常可以一键回滚。这里有个隐藏问题模型文件更新后特征名字典也必须同步更新否则会出现“特征名找不到”的静默错误。我们把模型和特征字典打包成一个版本号上传到HDFS服务启动时拉取指定版本版本不一致就告警算是给上线加了最后一道保险。6. 冷启动、扩展与那些没做好的事6.1 三个冷启动场景的解法推荐系统的冷启动一般分三种新用户、新景点、新城市。这个项目里我们分别用了不同的办法新用户没有行为记录就用“注册时填写的城市年龄性别”作为初始标签匹配同城市同年龄层用户的热门选择同时给一个“低门槛探索池”如免费景点、公园来提高试错概率。新景点没有用户行为就靠景点基础属性和地理信息把它推荐给对这个类型有强偏好的用户做小流量测试。等积累到100次曝光后再逐步纳入正常召回流。新城市数据不足这是最难搞的。我们采用“位置感知迁移”从相似城市GDP水平相近、纬度相近、城市面积相近继承一部分景点类型偏好权重。比如某地用户偏好“古镇”那么候选新城市里的古镇景点权重就稍微抬高。冷启动不可能靠算法单独解决产品上也要配合。比如我们给新用户强制做一个“兴趣标签选择页”虽然很多人会随便勾选但这个信号比注册信息可靠得多后续做画像初始化时能减少很多噪音。6.2 那段时间踩过的两个“隐形”坑第一个是时间特征泄露。我们特征里有“近7天热度”但在离线训练时如果把当天热度直接作为训练样本的特征其实就泄露了“未来信息”。正确做法是训练样本的特征必须使用“样本时间之前”的数据也就是特征和标签必须保持时间一致性。这个错误特别隐蔽一开始没注意结果线下AUC很高0.86一上线就变成0.72。排查了一星期最后发现是特征表里包含了当天的统计把时间窗口切干净之后线上和线下差值立刻缩小。第二个是长期不活跃用户的服务超时。我们最初给所有用户都走全量特征拼装结果发现有一部分“三个月没打开App”的老用户HBase里的特征记录还在但Redis缓存已经过期了导致每次请求都要穿透到HBase接口耗时从45ms涨到200ms。后来加了逻辑超过30天未活跃的用户直接用一张“默认推荐列表”走小时级缓存不实时拼特征。虽然简化了他们的推荐但至少保障了服务可用性而且这类低活跃用户对推荐敏感度也低影响不大。6.3 为什么不急着上“每天训练”的在线学习我看到很多行业交流里都在讲“在线学习”“秒级更新模型”但我们这个项目停在了“离线日更实时特征”这个阶段没有盲目追在线学习。原因是旅游推荐是一个“低频长周期决策”的业务用户行为在一天内的变化不足以让模型参数产生有意义的大幅更新反而是“节假日周期”比如春节、国庆带来的分布漂移离线模型用近30天数据训练已经能捕捉到部分周期规律。真要用在线学习还要解决样本延迟回传、模型稳定性和监控成本收益未必抵得上投入。把这个项目做完我最深的体会是大数据技术本身没有神奇的魔力它只是让“个性化”成为可能的底座。真正决定推荐质量的是你对业务的理解深度——你要清楚用户的决策周期清楚地理距离的分量清楚特征泄漏和AB实验长短这些细节。如果你的目标也是做一个能真正改善用户体验的推荐系统我建议先别急着堆技术把数据资产盘清把评测口径定准再谈用哪个框架、哪种模型。这套“从数据到上线”的完整链路走通一次你学到的会比看十本大数据教程都管用。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →