
简介一份基于大数据与ALS协同过滤算法的房源智能推荐系统完整设计方案文档适合计算机相关专业学生、推荐系统开发者及租房平台从业者参考。文档从房源信息不透明、租户选房难等实际问题出发系统介绍Scrapy数据采集、Hbase存储、ALS算法构建用户-房源隐式关系矩阵、Embedding特征提取等核心流程并给出SpringBootVue前后端平台搭建思路。资源为1个docx文档压缩包大小2.02MB包含中英文摘要、目录、章节结构及关键算法说明便于快速理解整体实现框架。目前已有302人学习下载可用于毕业设计、课程项目或推荐系统入门实践。1. 房源推荐为什么需要 ALS 而不是简单排序一个租客在贝壳、58 同城上搜索“广州越秀区 2000 元两居室”得到的结果往往是按发布时间或平台推广位排序的列表同样的条件在不同 App 里返回的排序逻辑完全不同。原因在于这些平台没有真正利用用户行为数据做个性化匹配只是把静态条件过滤后的结果无差别展示。ALSAlternating Least Squares交替最小二乘法解决的是“用户-物品”矩阵稀疏条件下的隐含特征挖掘问题租客看房、收藏、停留时长等行为可以被分解为若干个隐因子比如价格敏感度、通勤偏好、户型权重再通过迭代优化得到用户向量和房源向量最终用余弦相似度产出 Top-K 推荐。相比“按热度排序”或“按价格排序”ALS 能区分出同样预算 2000 元下喜欢靠近地铁的年轻人与喜欢安静小区的家庭各自适合哪套房。这篇文章从数据采集、HBase 存储、ALS 训练到 SpringBoot Vue 平台搭建完整拆解一个可运行的房源推荐系统实现路径适合正在做推荐系统毕设、进入大数据开发方向或想了解协同过滤落地细节的工程师参考。2. 数据采集与存储Scrapy 抓取房源到 HBase 的完整链路2.1 选型理由为什么是 Scrapy HBase推荐系统的效果首先取决于数据量。项目中的房源信息来自多个公开租房平台需要定时抓取列表页和详情页解析出价格、面积、户型、商圈、地铁距离等结构化字段。Scrapy 是 Python 生态中最成熟的爬虫框架它用 Twisted 异步处理网络请求抓取效率远高于 requests BeautifulSoup 的串行方案。更重要的是 Scrapy 的 Pipeline 机制天然适合做数据清洗、去重和持久化能把“下载页面—解析字段—存储”三个阶段解耦。存储层选择 HBase 而不是 MySQL是因为原始点击流和用户行为数据量很容易达到千万级甚至亿级。HBase 运行在 HDFS 之上支持列族动态扩展写入吞吐量高且能通过 Rowkey 设计实现高效率的点查和范围扫描。比如我们要查“某用户最近 100 条浏览记录”Rowkey 设计成userId_reverse timestamp即可快速定位。虽然 MySQL 也能存但当单表数据超过 5000 万行时索引维护和写入性能都会明显下降。HBase 的缺点是查询语法弱所以本项目用 HBase 存原始行为数据和房源快照用 Redis 存实时热门列表用 MySQL 存业务表用户账号、帖子、评论各司其职。2.2 爬虫工程化从公开页面抓取结构化的房源数据写 Scrapy 爬虫不能只写一个 spider要考虑到目标网站的反爬策略、字段动态加载、增量抓取。以抓取链家公开房源为例仅作技术演示任何实际抓取需遵守目标站 robots 与合规要求核心代码结构如下import scrapy from scrapy.http import Request from house_spider.items import HouseItem class LianjiaSpider(scrapy.Spider): name lianjia allowed_domains [lianjia.com] # 仅示例实际需合规确认 def start_requests(self): base_url https://{city}.lianjia.com/zufang/pg{page}/ # 第一轮只抓前5页后续根据列表页总数动态扩展 for page in range(1, 6): yield Request(urlbase_url.format(citygz, pagepage), callbackself.parse_list, headers{User-Agent: Mozilla/5.0}) def parse_list(self, response): # 列表页每个房源卡片链接 links response.css(div.content__list--item--main a) for link in links: url link.css(::attr(href)).get() if url: yield response.follow(url, callbackself.parse_detail) def parse_detail(self, response): item HouseItem() item[url] response.url item[title] response.css(title::text).get().strip() # 取价格如 4500元/月 price_text response.css( span.total::text).get() item[price] self.extract_price(price_text) # 取房子基本信息面积、户型、朝向 info_list response.css( div.zf-room p::text).getall() item[area] self.extract_area(info_list) item[layout] self.extract_layout(info_list) item[toward] self.extract_toward(info_list) # 商圈和地铁 item[business_circle] response.css( a[classfl]::text).get() yield item def extract_price(self, text): if text: return int(text.replace(,, )) return 0 def extract_area(self, info_list): for info in info_list: if 平米 in info: return float(info.replace(平米, )) return 0.0 def extract_layout(self, info_list): for info in info_list: if 室 in info and 厅 in info: return info return def extract_toward(self, info_list): for info in info_list: if 朝 in info: return info return 这个过程中有个很核心的细节parse_detail里的字段提取必须做容错。真实页面经常出现某个字段缺失比如“商区”可能为空此时get()会返回None后续写入 HBase 时如果直接拼字符串会报错。所以要在 Pipeline 里做一层统一的ItemLoader或字段校验逻辑。Scrapy 的 Pipeline 中要注意启用去重和日志记录。去重可以用scrapy-core自带的 RFPDupeFilter但它在多节点部署时不共享最简单的做法是用 Redis 去重。Pipeline 代码中要控制异常行为比如某个房源解析失败不能中断整个任务要try-except后继续下一条。# pipelines.py class HBasePipeline: def process_item(self, item, spider): # 字段校验 if not item.get(price) or item[price] 0: spider.logger.warning(finvalid item: {item.get(url)}) # 这里可以选择丢弃或丢进死信队列 return item rowkey self.gen_rowkey(item[url]) # 调用HBase的put接口写入这里省略连接细节 self.client.put(house_info, rowkey, {cf:title: item[title], cf:price: str(item[price]), cf:area: str(item[area]), cf:layout: item[layout], cf:toward: item[toward], cf:biz_circle: item[business_circle]}) return item def gen_rowkey(self, url): # 用反转URL做rowkey可以让同一域名下的记录相邻 # 同时避免热点问题 import hashlib m hashlib.md5() m.update(url.encode(utf-8)) return m.hexdigest()参数说明Pipeline 中如果同一批次数据量很大建议开启批量 put 模式而不是每一条都单独 put否则会因频繁 RPC 导致写入性能下降。可以攒够 1000 条或 3 秒 flush 一次。2.3 HBase 表设计与 Rowkey 规划HBase 的表结构不是预先定义所有列而是按列族规划。本项目有三张核心表表名列族说明house_infocf基础属性存房源静态信息如价格、面积、户型、经纬度、商圈user_actioncf存用户行为日志如浏览、收藏、点击、停留时长u_interestcf存用户对房源的特征偏好由Flink实时计算后写入house_info的 Rowkey 用 URL 的 MD5 或 SHA1 哈希值。这样做的好处是随机分布写请求不会集中在某台 RegionServer。缺点是范围查询不方便。如果我们要按“商圈价格”批量扫描房源Rowkey 设计成bizCircle price更合适比如yuexiu_2000。但这样会导致同一商圈的房源连续存放产生写热点。折中方案是加盐比如reverse(bizCircle) price或者用bizCircle price uuid。实际项目中我建议house_info表用 URL 哈希因为推荐系统需要的是全表扫描或按用户行为关联后的物载查询不需要很高频的“按商圈直查”。真正要做商圈筛选时通过二级索引如 Elasticsearch实现而不是在 HBase 上做。user_action表的 Rowkey 设计要注意查询模式。我们需要查“一个用户最近的行为列表”所以 Rowkey 用userId_reverse Long.MAX_VALUE - ts。userId 取反是为了让同用户的记录连续而用最大值减时间戳是让最近的行为排在最前面。如下示例# Rowkey: userId_rev Long.MAX_VALUE - timestamp user_123456789_rev_987654321012其中userId_rev是用户 ID 的字符串反转。比如用户 ID 为10001反转后是10001本身对称那么换一个不对称的例子用户 ID 为A1002反转为2001A。反转的作用是使字典序中相邻的 Rowkey 不集中在一台服务器上缓解热点。时间戳部分用补位方式保证长度一致。2.4 数据质量校验与清洗实现爬下来的数据并不能直接用于训练。比如“价格”字段可能是“价格面议”或“4500元/月”需要统一转换为整型“面积”可能包含“建面90平”或“90平米”。在 Pipeline 中做清洗后还要做一个统计检查对每个城市的房源价格分布做分位数超过99.5%分位的价格视为噪音丢弃防止后续 ALS 训练被极端值带偏。import numpy as np def clean_price_series(prices, lower_bound0.01, upper_bound0.995): q_low, q_high np.percentile(prices, [lower_bound * 100, upper_bound * 100]) return [p for p in prices if q_low p q_high]这一步能有效去除“月租20万”的别墅或“月租200元”的异常床位这些数据在训练时即使不影响模型收敛也会让推荐结果的分数排序偏离实际。清洗后的数据入库前还要记录每个字段的来源 URL 和抓取时间方便后续溯源。3. ALS 算法原理与推荐引擎落地3.1 协同过滤家族中 ALS 的定位与适用场景推荐算法可以按信息源头分为基于内容的推荐和协同过滤推荐。基于内容的方法需要构建房源的内容特征向量比如“精装”“近地铁”这些标签再计算用户历史偏好与这些标签的相似度。这种方法的缺点是标签体系不完备且无法发现用户潜在的兴趣组合。协同过滤则直接利用用户行为矩阵不再关心具体内容语义。协同过滤里又分两类。基于内存的算法Memory-based在每个推荐请求到来时都要计算目标用户与所有其他用户的相似度或者目标物品与所有其他物品的相似度计算量随用户或物品数量线性增长在线服务时性能无法接受。而 ALS 属于基于模型的矩阵分解方法它把用户-物品评分矩阵 Rm × n分解成用户特征矩阵 Um × k和物品特征矩阵 Vn × k其中 k 是隐因子数量远小于 m 和 n。训练完成后在线预测只需要算两个向量的点积非常快。ALS 适用于评分矩阵稀疏但隐含结构明显的场景。比如房源数据中单个用户最多看过几百个房源而总房源有几十万矩阵稀疏度通常在 99% 以上。ALS 通过交替固定 U 优化 V、固定 V 优化 U每步都是最小二乘问题能利用分布式计算框架并行处理这也是它在 Spark MLlib 中成为标配算法的原因。3.2 构建用户-房源评分矩阵原始行为不是评分需要转换成评分。常见的行为映射规则为浏览记 1 分收藏记 3 分电话咨询记 5 分如果用户停留时长超过 2 分钟额外加 0.5 分。不同平台的行为权重可以叠加但需要做归一化防止收藏多的用户主导评分。以下 SQL 用于从 HBase 导出的行为日志中构建评分矩阵的输入表假设行为日志已写入 Hive 或 Spark SQL 可读的表SELECT user_id, house_id, -- 评分 0.4 * 浏览 1.0 * 收藏 2.0 * 咨询 0.01 * 停留秒数(截断到5) LEAST( COALESCE(browse_score, 0) * 0.4 COALESCE(favorite_score, 0) * 1.0 COALESCE(contact_score, 0) * 2.0 LEAST(COALESCE(stay_seconds, 0) / 60.0, 10.0), 5.0 ) AS rating FROM user_action WHERE dt 2025-01-01这里的LEAST(…, 5.0)是为了把评分限制在 0~5 区间避免停留时间过长带来的异常高值。实际训练时还要过滤掉只有一次行为的“幽灵用户”因为他们对矩阵分解的贡献只是噪音。用户在训练集中至少要有 3 次行为房源在训练集中至少要有 5 次被行为否则无法得到稳定的向量表示。3.3 ALS 迭代训练参数选择与冷启动处理在 Spark MLlib 中ALS 的训练代码非常简洁from pyspark.ml.recommendation import ALS als ALS( userColuser_id, itemColhouse_id, ratingColrating, rank20, # 隐因子数 maxIter15, # 最大迭代次数 regParam0.1, # 正则化参数 coldStartStrategydrop, implicitPrefsFalse # 显式反馈 ) model als.fit(training_df)关键参数说明rank隐因子数量。太小编不下特征比如价格区间、通勤偏好、户型偏好太大会过拟合且存储开销大。一般从 10 试到 50观察验证集 RMSE 变化。本项目用 20~30 得到不错的结果。regParam正则化系数防止参数过大。对稀疏数据来说0.01~0.1比较合适。如果训练后模型在测试集上比训练集误差大很多则增大regParam。implicitPrefs本项目行为评分虽是构造的但本质上接近隐式反馈用户没有显式打分。如果设为trueALS 会改用加权正则化矩阵分解更适用隐式数据。但隐式模型输出的是置信度而不是评分推荐结果排序稳定。实际测试中显式评分映射后效果已足够所以保持False。coldStartStrategy对于训练集中没有出现过的用户或房源模型无法预测选择drop可以直接丢弃这些条目避免生成 NaN 评分。但更好的做法是在用户向量表建立后对新用户用其注册信息如预算价格、工作区域映射到相似老用户的向量或者退回热门推荐。训练完成后生成用户向量和房源向量user_vectors model.userFactors.select(id, features) house_vectors model.itemFactors.select(id, features)这两个向量就是 embedding。用户向量维度为rank20每个维度虽然没有显式含义但经过训练后它们会自动编码出“价格敏感度”“对地铁距离的关注度”等组合特征。线上推荐时只需要拿到用户向量然后与所有房源向量做点积取 Top-K。3.4 线上推荐服务从模型输出到 Top-K 列表model.transform可以直接给一个用户列表生成预测评分但这种方式在用户量大时效率低。离线阶段我们先用模型为每个用户计算所有房源的评分把 Top 200 的userId houseId score写入 Redis线上直接读取。伪代码如下from pyspark.sql.functions import col, row_number, desc from pyspark.sql.window import Window pred model.transform(all_user_house_pairs) window Window.partitionBy(user_id).orderBy(desc(prediction)) top_k pred.withColumn(rank, row_number().over(window)).filter(col(rank) 200) # 写入redis或mysql top_k.write.mode(overwrite).jdbc(jdbc:mysql://..., rec_top200, ...)线上接口是非实时计算因为 ALS 模型不是实时更新的。它每天凌晨跑批把结果更新到 Redis。用户点击推荐页面时从 Redis 读列表并填充房源的详情信息。要保证这一点需要在 SpringBoot 中写一个推荐接口。RestController RequestMapping(/api/recommend) public class RecommendController { Autowired private StringRedisTemplate redisTemplate; Autowired private HouseService houseService; GetMapping(/{userId}) public ResultListHouseVO recommend(PathVariable Long userId) { String key rec:user: userId; ListString ids redisTemplate.opsForList().range(key, 0, 19); if (ids null || ids.isEmpty()) { // 冷启动回退热门房源 ids houseService.getHotHouseIds(20); } ListHouseVO houses houseService.selectByIds(ids); return Result.success(houses); } }这里注意redisTemplate存列表时最好预热最近 1000 个活跃用户避免所有用户同时打数据库导致缓存穿透。对于新用户可以在注册时让他选择几个偏好标签如预算、区域、户型然后映射到对应的热门列表等到行为积累后再切换到个性化推荐。4. SpringBoot Vue 搭建房源推荐平台4.1 后端模块划分与推荐接口设计平台后端采用 SpringBoot 框架按业务域拆分成模块用户模块、房源模块、行为模块、推荐模块、社区模块。结构如下src/main/java/com/house/ ├── controller/ │ ├── AuthController.java │ ├── HouseController.java │ ├── RecommendController.java │ └── SocialController.java ├── service/ │ ├── UserService.java │ ├── HouseService.java │ ├── BehaviorService.java │ └── RecommendService.java ├── mapper/ │ ├── UserMapper.java │ └── HouseMapper.java └── config/ ├── RedisConfig.java └── HBaseConfig.java推荐接口是核心。它接收userId返回ListHouseVO。在生成推荐结果前要注入一层“业务规则过滤”比如用户已经收藏过的房源不能再推荐已经被下架的房源要过滤推荐列表中同一个商圈不能超过 5 条保证多样性。行为采集接口同样关键。前端每次浏览、点击、收藏时会向/api/behavior/report发送一条日志。后端收到后异步写入 Kafka再由 Flink 任务消费并写入 HBase。为什么要异步因为行为日志量大同步写数据库会拖慢前端响应。// 前端埋点代码示例 export function reportBehavior(actionType, houseId, stayDuration) { axios.post(/api/behavior/report, { userId: getUserId(), houseId: houseId, actionType: actionType, // view | collect | contact stayDuration: stayDuration, timestamp: Date.now() }, { timeout: 2000 }).catch(() {}); }注意埋点请求需要设置短超时且不阻塞主流程。前端即使发送失败也不影响用户正常浏览因为行为数据最终可以通过日志补偿系统补全。4.2 前端页面与交互个性化筛选和推荐展示前端使用 Vue Element UI 搭建。首页有搜索筛选区用户可以选择城市、区域、价格区间、面积、户型等条件。每次条件变化前端会调用/api/houses/search接口后端通过组合查询从 Elasticsearch 返回结果。Elasticsearch 在这里做房源检索比直接在 MySQL 里WHERE price BETWEEN...效率更高也能轻松支持地理位置排序。推荐展示区在首页下方登录用户访问时调用推荐接口。为了提升点击率推荐列表的第一条会展示一个较大的卡片显示“为你推荐”的文案。卡片上除了基本信息还会展示推荐理由标签比如“比你看过的同商圈房源便宜 10%”或“通勤时间比上次浏览房源短 15 分钟”。这些理由标签可以根据用户向量和房源向量的特征差生成增加列表的感知可信度。筛选逻辑和推荐逻辑是两条独立链路。筛选是用户主动发起的条件过滤推荐是系统主动猜测用户偏好。两者存在交叉点用户完成一轮筛选后可以把当前筛选条件作为上下文特征传入推荐模型实现上下文感知的推荐。在本项目中这主要通过 Flink 计算的用户兴趣状态实现但一个轻量替代方案是当用户连续点击某个价格的房源超过 3 次后端将价格区间偏好写入用户画像表下次推荐时优先调整排序权重。4.3 用户行为日志埋点与回流行为日志回流到训练集是保持推荐新鲜度的关键。每天凌晨Spark 任务从 HBase 读取前一天的增量行为记录合并到历史评分矩阵重训 ALS 模型。为了防止模型被少量异常用户污染还需要对评分做衰减90 天前的行为权重乘以 0.5180 天前的行为权重乘以 0.2即SELECT user_id, house_id, rating * CASE WHEN days_ago 30 THEN 1.0 WHEN days_ago 90 THEN 0.7 WHEN days_ago 180 THEN 0.4 ELSE 0.2 END AS decayed_rating FROM user_action WHERE days_ago 180衰减处理的好处是让近期兴趣占主导。比如一个学生刚毕业离开学校附近系统不应该还因为一年前频繁浏览学校周边房源而继续推荐。天数计算可以用DATEDIFF(CURRENT_DATE, action_date)数据量小时直接 SQL 处理数据量大时在 Spark 中按分区处理。回流还要求推荐系统具备“反馈闭环”用户在推荐列表中点击了一个房源这个点击行为会作为正样本进入训练集如果推荐了但用户没有点击且曝光时间超过阈值则作为负样本。显式负样本对训练很有价值。前端在推荐卡片曝光时上报impression事件后端记录曝光列表第二天与点击事件做 join得到未点击的负样本。负样本的评分设为 0.5。注意不能把所有未点击的都当负样本因为可能是用户没看到只在曝光超过 30 秒且未点击的才标记。5. 推荐效果调优与排错技巧5.1 评估指标离线召回率与在线点击率离线评估用 RMSE 看评分预测误差不够直观推荐场景更关注召回率和精确率。将行为数据集按时间切分前 7 天训练后 2 天测试。对测试集中的每个用户从模型推荐的 Top N 中统计有多少命中用户真正交互过的房源。如果命中比例过低说明模型没有学到有效模式。工程实践中发现召回率对 N 的选择很敏感。Top 10 召回率一般不会超过 20%Top 100 能到 40%这很正常。更建议看“推荐置信区间”把用户按行为数量分成五档行为多的用户召回率通常更高。如果新用户的召回率远低于老用户冷启动策略就不合格需要优化用户向量初始化方法。在线评估最直接的是点击率CTR和转化率CVR。推荐位上曝光 1000 次被点击 80 次CTR 8%。对比修改算法前后的 CTR需要做 AB 测试。如果新版 CTR 没有显著提升即使离线指标变好也不能贸然全量上线。一个常见的坑是离线用的训练集与线上特征分布不一致比如离线没有加入时间衰减导致模型偏好旧行为线上用户的即时兴趣没有反映出来。5.2 常见坑数据稀疏、相似度陷阱、HBase 热读数据稀疏是 ALS 最典型的痛点。当用户行为少于 5 条时分解出的用户向量几乎没有意义。解决办法是用“填充冷启动群体向量”先按用户注册信息聚类比如“预算 3000-5000、工作区域在珠江新城”算一类用这一类用户的平均向量作为新用户的初始向量。后续每收集一条行为就做一次微调而不是重新训练整个模型。第二坑是相似度的使用方式。有的同学拿到用户向量后把所有房源的 embedding 都存到 Redis然后在 JVM 里每次计算点积来排序。当房源量为 50 万时即使每个向量只有 20 维一次推荐也要做 1000 万次浮点运算响应时间可能超过 100ms。正确做法是使用向量检索工具如 Faiss 或 Milvus把房源向量构建成索引用近似最近邻搜索返回 Top K毫秒级完成。如果不想引入额外组件就按第 3.4 节离线算好 Top 200 存 Redis换存储空间换查询时间。第三坑是 HBase 热读。如果每天凌晨更新推荐结果时大量 RegionServer 同时被访问热门房源可能导致 HBase 分区热点。缓解办法是 Rowkey 加盐或者在 Redis 中做一层缓存让大部分请求直接打 Redis 而不是 HBase。本项目在凌晨任务中先写入 Redis再异步更新 HBase保证线上用户的读流量不直接冲击 HBase。5.3 进阶用 TF-IDF 修正热门标签权重基础 ALS 矩阵分解没有用到房源标签信息。如果希望推荐结果对“精装”“近地铁”这些可解释特征更敏感可以在评分矩阵之外增加一个标签偏好矩阵。用户对标签的兴趣度可以借鉴 TF-IDF 思想用户对标签 t 的兴趣度 用户行为中标签 t 的出现频率 × log(全部标签出现次数 / 所有用户中出现过标签 t 的用户数)。这样热门标签如“南北通透”的权重会被抑制稀缺标签如“带阳台书桌”的权重会提高。在代码层面可以把它作为 ALS 评分矩阵的辅助加权。比如最终评分矩阵中rating cf_rating * (1 alpha * tfidf_score)其中alpha取 0.2~0.5防止标签特征过度放大。实际效果上这种混合方式能缓解 ALS 对罕见兴趣的“均值回归”问题。比如一个用户只收藏过一种少见户型的房源如果没有标签辅助ALS 会把用户向量推向热门方向导致推荐结果逐渐同质化。加入 TF-IDF 权重后用户对稀缺特征的表达被保留。验证这个改进是否有效可以用一个简单的在线实验将用户随机分为两组一组使用纯 ALS 推荐另一组使用 ALS TF-IDF 加权推荐对比两组在推荐位上的点击率差异。实验周期至少一周样本量不低于每/组 5000 活跃用户观察点击率提升是否超过 2 个百分点。如果提升不足说明标签体系不够完善或权重设置不当需要调整标签清洗规则。最后强烈建议在系统上线初期记录所有推荐日志包括推荐来源比如是 ALS 还是热门兜底、曝光位置、是否点击、后续是否预约看房。这些日志是持续优化推荐系统的唯一依据。而 ALS 模型本身只是一种工具真正决定推荐质量的是数据的完整度和你对用户意图的理解是否被正确编码成了矩阵中的数值。本文还有配套的精品资源点击获取