在现代推荐系统中,随着用户量和物品量的增长,传统的推荐算法可能会面临性能瓶颈。本文将介绍如何基于 ThinkPHP 实现一个高性能的推荐系统,结合显性反馈(如兴趣选择)、隐性反馈(如观看时长、评论、点赞、搜索等)、行为序列分析和关键词拆分(支持中文)等功能,并通过优化方案支持大规模用户场景。
目录
推荐系统简介
数据库设计
推荐算法类的实现
优化方案
总结与扩展
推荐系统简介
推荐系统的目标是根据用户的历史行为,预测用户可能感兴趣的物品。常见的推荐算法包括:
基于内容的推荐:根据物品的属性推荐相似物品。
协同过滤:根据用户的行为推荐相似用户喜欢的物品。
混合推荐:结合多种推荐算法,提升推荐效果。
本文将重点介绍基于协同过滤的推荐算法,并结合显性反馈和隐性反馈数据。
数据库设计
1. 用户表 (users
)
存储用户的基本信息。
sql
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID,主键,自增',
username VARCHAR(50) NOT NULL COMMENT '用户名,唯一标识用户'
) COMMENT='用户表,存储用户的基本信息';
2. 物品表 (items
)
存储物品的基本信息。
sql
CREATE TABLE items (
item_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '物品ID,主键,自增',
item_name VARCHAR(100) NOT NULL COMMENT '物品名称,唯一标识物品',
description TEXT COMMENT '物品描述,用于关键词拆分',
tags TEXT COMMENT '物品标签,用于关键词拆分'
) COMMENT='物品表,存储物品的基本信息';
3. 显性反馈表 (explicit_feedback
)
存储用户对物品的显性反馈,如兴趣选择、评分等。
sql
CREATE TABLE explicit_feedback (
feedback_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '反馈ID,主键,自增',
user_id INT COMMENT '用户ID,外键,关联用户表',
item_id INT COMMENT '物品ID,外键,关联物品表',
rating INT COMMENT '用户对物品的评分(1-5)',
interest_level ENUM('low', 'medium', 'high') COMMENT '用户兴趣选择(低、中、高)',
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, -- 用户删除时,关联的反馈也删除
FOREIGN KEY (item_id) REFERENCES items(item_id) ON DELETE CASCADE -- 物品删除时,关联的反馈也删除
) COMMENT='显性反馈表,存储用户对物品的显性反馈';
4. 隐性反馈表 (implicit_feedback
)
存储用户的隐性行为,如观看时长、评论、点赞、搜索等。
sql
CREATE TABLE implicit_feedback (
feedback_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '反馈ID,主键,自增',
user_id INT COMMENT '用户ID,外键,关联用户表',
item_id INT COMMENT '物品ID,外键,关联物品表',
action_type ENUM('view', 'comment', 'like', 'search') COMMENT '行为类型(观看、评论、点赞、搜索)',
action_value FLOAT COMMENT '行为值(如观看时长、点赞次数等)',
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, -- 用户删除时,关联的反馈也删除
FOREIGN KEY (item_id) REFERENCES items(item_id) ON DELETE CASCADE -- 物品删除时,关联的反馈也删除
) COMMENT='隐性反馈表,存储用户的隐性行为';
5. 行为序列表 (behavior_sequence
)
存储用户的行为序列,按时间排序。
sql
CREATE TABLE behavior_sequence (
sequence_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '序列ID,主键,自增',
user_id INT COMMENT '用户ID,外键,关联用户表',
item_id INT COMMENT '物品ID,外键,关联物品表',
action_type ENUM('view', 'comment', 'like', 'search') COMMENT '行为类型(观看、评论、点赞、搜索)',
action_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '行为发生时间,默认为当前时间',
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, -- 用户删除时,关联的行为也删除
FOREIGN KEY (item_id) REFERENCES items(item_id) ON DELETE CASCADE -- 物品删除时,关联的行为也删除
) COMMENT='行为序列表,存储用户的行为序列';
6. 关键词表 (keywords
)
存储从物品描述和标签中提取的关键词。
sql
CREATE TABLE keywords (
keyword_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '关键词ID,主键,自增',
keyword VARCHAR(50) NOT NULL COMMENT '关键词,用于推荐算法',
item_id INT COMMENT '物品ID,外键,关联物品表',
FOREIGN KEY (item_id) REFERENCES items(item_id) ON DELETE CASCADE -- 物品删除时,关联的关键词也删除
) COMMENT='关键词表,存储从物品描述和标签中提取的关键词';
ThinkPHP 推荐算法类
以下是基于 ThinkPHP 的推荐算法类实现,每行代码都有详细注释。
php
<?php
namespace app\common\service;
use think\Db;
use think\facade\Cache;
class Recommender
{
// 获取用户的显性反馈(兴趣选择和评分)
private function getExplicitFeedback(int $userId): array
{
// 查询用户的显性反馈数据
return Db::name('explicit_feedback')
->where('user_id', $userId) // 过滤指定用户
->field('item_id, rating, interest_level') // 选择需要的字段
->select(); // 返回查询结果
}
// 获取用户的隐性反馈(观看时长、评论、点赞、搜索等)
private function getImplicitFeedback(int $userId): array
{
// 查询用户的隐性反馈数据,按物品和行为类型分组
return Db::name('implicit_feedback')
->where('user_id', $userId) // 过滤指定用户
->field('item_id, action_type, SUM(action_value) as total_value') // 选择需要的字段,并计算行为值的总和
->group('item_id, action_type') // 按物品和行为类型分组
->select(); // 返回查询结果
}
// 获取用户的行为序列
private function getBehaviorSequence(int $userId): array
{
// 查询用户的行为序列,按时间排序
return Db::name('behavior_sequence')
->where('user_id', $userId) // 过滤指定用户
->order('action_time ASC') // 按行为时间升序排序
->field('item_id, action_type, action_time') // 选择需要的字段
->select(); // 返回查询结果
}
// 从物品描述和标签中提取关键词(支持中文)
private function extractKeywords(string $text): array
{
// 使用 jieba-php 分词库进行中文分词
$words = \Fukuball\Jieba\Jieba::cut($text); // 将文本拆分为单词
$stopWords = ['的', '了', '在', '是', '我']; // 中文停用词列表
return array_diff($words, $stopWords); // 去除停用词,返回关键词数组
}
// 获取物品的关键词
private function getItemKeywords(int $itemId): array
{
// 查询物品的描述和标签
$item = Db::name('items')
->where('item_id', $itemId) // 过滤指定物品
->field('description, tags') // 选择需要的字段
->find(); // 返回单行数据
$keywords = [];
if ($item) {
// 从描述和标签中提取关键词
$keywords = array_merge(
$this->extractKeywords($item['description']), // 提取描述中的关键词
$this->extractKeywords($item['tags']) // 提取标签中的关键词
);
}
return array_unique($keywords); // 去重后返回关键词数组
}
// 计算用户相似度(基于显性和隐性反馈)
private function calculateUserSimilarity(int $user1, int $user2): float
{
// 获取用户1的显性反馈
$feedback1 = $this->getExplicitFeedback($user1);
// 获取用户2的显性反馈
$feedback2 = $this->getExplicitFeedback($user2);
$dotProduct = 0; // 点积
$magnitude1 = 0; // 用户1的模长
$magnitude2 = 0; // 用户2的模长
// 遍历用户1的反馈数据
foreach ($feedback1 as $item) {
$itemId = $item['item_id']; // 物品ID
$rating1 = $item['rating']; // 用户1的评分
$interestLevel1 = $item['interest_level'] === 'high' ? 1 : 0; // 用户1的兴趣选择(高为1,否则为0)
// 遍历用户2的反馈数据
foreach ($feedback2 as $item2) {
if ($item2['item_id'] === $itemId) { // 如果两个用户对同一物品有反馈
$rating2 = $item2['rating']; // 用户2的评分
$interestLevel2 = $item2['interest_level'] === 'high' ? 1 : 0; // 用户2的兴趣选择(高为1,否则为0)
// 计算点积和模长
$dotProduct += ($rating1 * $rating2) + ($interestLevel1 * $interestLevel2);
$magnitude1 += ($rating1 * $rating1) + ($interestLevel1 * $interestLevel1);
$magnitude2 += ($rating2 * $rating2) + ($interestLevel2 * $interestLevel2);
}
}
}
$magnitude1 = sqrt($magnitude1); // 用户1的模长
$magnitude2 = sqrt($magnitude2); // 用户2的模长
if ($magnitude1 == 0 || $magnitude2 == 0) {
return 0; // 避免除零错误
}
return $dotProduct / ($magnitude1 * $magnitude2); // 返回余弦相似度
}
// 获取与目标用户最相似的用户
private function getSimilarUsers(int $targetUserId): array
{
// 查询所有其他用户
$users = Db::name('users')
->where('user_id', '<>', $targetUserId) // 过滤掉目标用户
->column('user_id'); // 返回用户ID列表
$similarities = [];
// 遍历所有用户,计算与目标用户的相似度
foreach ($users as $userId) {
$similarity = $this->calculateUserSimilarity($targetUserId, $userId); // 计算相似度
$similarities[$userId] = $similarity; // 存储相似度
}
arsort($similarities); // 按相似度降序排序
return $similarities; // 返回相似用户列表
}
// 为目标用户推荐物品(基于缓存)
public function recommendItems(int $targetUserId, int $numRecommendations = 5): array
{
$cacheKey = "recommendations_{$targetUserId}"; // 缓存键
$recommendations = Cache::get($cacheKey); // 从缓存中获取推荐结果
if (!$recommendations) { // 如果缓存中没有推荐结果
$similarUsers = $this->getSimilarUsers($targetUserId); // 获取相似用户
$recommendations = [];
// 遍历相似用户
foreach ($similarUsers as $userId => $similarity) {
$feedback = $this->getExplicitFeedback($userId); // 获取相似用户的显性反馈
// 遍历相似用户的反馈数据
foreach ($feedback as $item) {
$itemId = $item['item_id']; // 物品ID
$rating = $item['rating']; // 评分
$interestLevel = $item['interest_level'] === 'high' ? 1 : 0; // 兴趣选择(高为1,否则为0)
// 如果目标用户未评价过该物品,且评分或兴趣选择较高
if (!isset($this->getExplicitFeedback($targetUserId)[$itemId]) && ($rating >= 4 || $interestLevel)) {
if (!isset($recommendations[$itemId])) {
$recommendations[$itemId] = 0; // 初始化推荐分数
}
// 计算推荐分数
$recommendations[$itemId] += ($rating * $similarity) + ($interestLevel * $similarity);
}
}
}
arsort($recommendations); // 按推荐分数降序排序
$recommendations = array_slice($recommendations, 0, $numRecommendations, true); // 返回前N个推荐物品
// 缓存推荐结果,有效期1小时
Cache::set($cacheKey, $recommendations, 3600);
}
return $recommendations; // 返回推荐结果
}
}
优化方案
1.用户相似度计算的优化
- 离线计算:将用户相似度计算任务放到离线任务中,定期更新相似度矩阵。
- 分块计算:将用户分成多个块,分别计算块内用户的相似度,减少计算量。
- 近似算法:使用局部敏感哈希(LSH)或随机投影等近似算法,快速找到相似用户。
2、推荐物品的优化
- 基于物品的协同过滤:计算物品之间的相似度,推荐与用户历史行为相似的物品。
- 矩阵分解:使用矩阵分解(如SVD)将用户-物品评分矩阵分解为低维矩阵,减少计算量。
- 缓存推荐结果:将推荐结果缓存到Redis等内存数据库中,减少实时计算的压力。
3、数据库查询的优化
- 索引优化:为常用查询字段(如user_id、item_id)添加索引。
- 分库分表:将用户和物品数据分散到多个数据库或表中,减少单表数据量。
- 批量查询:减少数据库查询次数,尽量使用批量查询。
4、引入分布式计算
- 使用分布式计算框架(如Hadoop、Spark)处理大规模数据。
- 将用户相似度计算和推荐任务分布到多个节点上执行。
总结与扩展
通过上述代码和优化方案,可以实现一个高性能的推荐系统,支持大规模用户场景。以下是扩展方向:
- 引入机器学习模型:如矩阵分解(SVD)或深度学习模型,提升推荐效果。
- 实时推荐:根据用户的最新行为动态调整推荐结果。
- 多维度权重:为不同类型的隐性反馈(如观看时长、点赞)设置不同的权重。
希望本文对你理解和实现推荐系统有所帮助!如果有其他问题,欢迎留言讨论。