PHP如何实现类似知乎的文章点赞收藏与推荐算法功能

各位同学,大家下午好!

请把手机静音,把电脑打开。今天我们不聊怎么用Laravel写个CRUD,也不聊怎么用PHP接个微信支付回调。今天我们要聊点硬核的,聊聊如何在PHP里构建一个类似于知乎的推荐系统。

我知道,你们中有些新来的实习生可能觉得PHP就是个“写网页的后端脚本”,甚至还有人觉得它跑不了大计算。嘿,年轻真好。我当年写PHP的时候,连服务器还在用拨号上网呢。PHP这门语言,就像咱们中国的老干妈,看着不起眼,但在配菜这块儿,它是真的能镇得住场子。只要你能把它的性能榨干,它就是宇宙第一。

今天,我们要用PHP模拟一个微型知乎。核心功能有三个:点赞热度计算收藏构建用户画像、以及基于标签的协同过滤推荐算法

废话不多说,咱们直接开整。


第一模块:点赞与热度算法 —— 不仅仅是数字的堆叠

大家点开知乎,看到首页那个红心,点一下,文章热度就变了。很多初级开发者想当然地想:“热度就是点赞数 + 收藏数 + 评论数嘛,存个计数器,每来一个请求 UPDATE likes SET count = count + 1。”

错了!大错特错!如果这么做,你的服务器会在双十一当天因为数据库死锁而当场去世。而且,这根本不是知乎的算法。

知乎的热度算法核心在于时间衰减。就像一碗刚出锅的馄饨很烫,但放久了就凉了。热门内容也是,新的内容权重高,老的内容如果不持续产出新的互动,热度就会慢慢掉下去。

我们需要一个数学模型。常用的类似“微博热度”或“知乎热度”的公式大致是这样的:

$$Score = (A times Likes + B times Comments + C times Favorites) times Decay$$

其中 Decay 是一个基于时间的衰减因子,通常是对数函数或者指数衰减。

PHP实现思路

咱们用PHP来写一个热度计算器。为了演示,我们假设每篇博文有一个 create_time

<?php

class ArticleHeatCalculator
{
    /**
     * 计算文章热度分数
     * @param int $likes 点赞数
     * @param int $comments 评论数
     * @param int $favorites 收藏数
     * @param int $createTime 文章发布时间戳
     * @return float
     */
    public static function calculateHeat($likes, $comments, $favorites, $createTime)
    {
        // 权重系数:根据经验调整
        // 点赞权重较高,评论次之,收藏最低(因为收藏可能只是顺手点,不代表当下共鸣)
        $weights = [
            'like'    => 1.0,
            'comment' => 0.5,
            'fav'     => 0.3
        ];

        // 计算原始热度分
        $baseScore = ($likes * $weights['like']) + 
                     ($comments * $weights['comment']) + 
                     ($favorites * $weights['fav']);

        // 计算时间衰减因子
        // 这里用了一个简单的逻辑:距离现在越久,分数除以的数越大
        // 这是一个对数衰减模拟
        $hoursPassed = (time() - $createTime) / 3600;
        $decayFactor = max(0.2, 1 / (log($hoursPassed + 2, 10)));

        return $baseScore * $decayFactor;
    }

    // 测试一下
    public static function demo()
    {
        $now = time();

        // 场景1:一篇刚刚发布的老文章
        $oldArticle = [
            'likes' => 1000,
            'comments' => 50,
            'favorites' => 200,
            'createTime' => $now - 86400 * 30 // 30天前
        ];

        // 场景2:一篇刚刚发布的爆款
        $newArticle = [
            'likes' => 1000,
            'comments' => 50,
            'favorites' => 200,
            'createTime' => $now
        ];

        echo "30天前的老爆款热度: " . self::calculateHeat(...array_values($oldArticle)) . "n";
        echo "刚刚发布的爆款热度: " . self::calculateHeat(...array_values($newArticle)) . "n";
    }
}

ArticleHeatCalculator::demo();

你看,运行这个脚本,你会发现“刚刚发布”的文章虽然互动数据一样,但热度分数可能比那个30天前的还要高,因为它的“时效性”还没过。这就是算法的魅力。

但是,各位,别高兴得太早。上述代码只是前端展示用的分数。如果每一页都要执行这个复杂的数学公式,数据库得哭死。

专家级优化:
在实际生产环境中,我们不会在每次查询文章详情时都算一遍。我们会维护一个Redis的ZSet(有序集合)。

  • Key是文章ID。
  • Value是这个计算好的热度分数。
  • 当用户点赞时,不是去数据库改数字,而是去Redis执行 ZADD hot_article_list $score $article_id
  • 推荐列表直接从Redis里 ZRANGE 出来就行了。这才是PHP高并发的正确打开方式。我们代码里就不写Redis了,免得大家觉得太复杂,但脑子里得有这根弦。

第二模块:收藏与用户画像 —— 给用户贴标签

知乎最核心的价值是什么?是“收藏夹”。大家收藏文章,往往是因为“以后有用”或者“观点认同”。收藏,是一个强意向的行为。

当用户收藏了一篇关于“PHP高性能编程”的文章,系统就要意识到:这个用户对“PHP”感兴趣。

这就是用户画像的雏形。我们需要把非结构化的文本(文章标题+正文)转化为结构化的标签(vector)。

PHP实现思路

假设我们有一个简单的文章库,用户做了一个收藏动作。我们需要把文章的标签加到用户的标签库里。

  1. 文章有 tags 字段(用逗号分隔的字符串,例如 “PHP,编程,架构”)。
  2. 用户有 tag_vector 字段(存储用户感兴趣的标签列表)。
  3. 收藏时,提取文章标签,更新用户画像。
<?php

class UserProfile
{
    /**
     * 更新用户画像(收藏文章后触发)
     * @param int $userId 用户ID
     * @param array $articleTags 文章标签数组 ['PHP', '架构', 'Swoole']
     */
    public static function updateOnCollect($userId, $articleTags)
    {
        // 1. 获取当前用户的标签偏好(这里简化处理,实际应从Redis或数据库读取)
        // 假设当前用户标签是 ['Web开发', '后端']
        $currentTags = ['Web开发', '后端']; 

        // 2. 合并新标签,并计算权重(收藏次数越多,权重越高)
        $newTags = [];
        foreach ($articleTags as $tag) {
            if (in_array($tag, $currentTags)) {
                $newTags[$tag] = isset($newTags[$tag]) ? $newTags[$tag] + 1 : 1;
            } else {
                $newTags[$tag] = 1;
            }
        }

        // 3. 执行更新(实际操作中,这会是一个异步任务,避免阻塞用户)
        // echo "用户 {$userId} 的标签库更新了,新增热度: " . json_encode($newTags);
        return $newTags;
    }

    /**
     * 生成向量(为了后面做相似度计算)
     * 我们把标签映射为数字,方便做数学运算
     */
    public static function tagsToVector($tags, $allTags)
    {
        $vector = [];
        foreach ($allTags as $index => $tag) {
            $vector[$index] = in_array($tag, $tags) ? 1 : 0;
        }
        return $vector;
    }
}

// 模拟场景
$myTags = ['Web开发', '后端'];
$newArticleTags = ['PHP', '架构', 'Swoole', 'Web开发']; // 用户收藏了一篇关于PHP架构的文章

$updated = UserProfile::updateOnCollect(1001, $newArticleTags);
print_r($updated);

这里有个关键点:所有用户的全量标签。我们需要一个全局的标签列表 ['PHP', '架构', 'Swoole', 'Web开发', '后端']

当用户收藏文章时,系统实际上是在做一件事:向量更新。如果用户收藏了,向量中对应的位置就置为1(或者加1),代表这个维度被激活了。

专家级思考:
如果文章有500个标签,用户收藏了,这500个标签都要存吗?那用户画像得有几百万个维度的数组,内存爆炸!
所以,TF-IDF(词频-逆文档频率)在这里就派上用场了。系统应该自动识别文章中哪些是核心关键词(比如“PHP”权重高,“的”、“了”权重低),只提取核心词作为用户画像的标签。这才是正经算法。


第三模块:推荐算法 —— 基于内容的协同过滤

好了,画像做好了,热度也有了。现在用户打开APP,首页应该推什么?

如果你直接推“PHP高性能编程”,万一用户刚才收藏了“前端Vue”,你推PHP他可能就卸载了。

所以,我们需要一个推荐引擎。在知乎这种模式下,最实用的算法是基于内容的过滤

原理很简单:“人以群分,物以类聚”
如果你收藏了A文章,说明你喜欢的属性和B文章很像,那么B文章很可能也喜欢你。

PHP实现思路

我们需要计算两个向量之间的相似度。最常用的指标是余弦相似度

$$ Cosine(Similarity) = frac{A cdot B}{||A|| times ||B||} $$

其中 $A cdot B$ 是点积,$||A||$ 是向量的模(长度)。

咱们用PHP来实现这个核心逻辑。别看公式高大上,代码其实很简洁。

<?php

class RecommendationEngine
{
    /**
     * 计算两个向量的余弦相似度
     * @param array $vectorA 用户A的标签向量
     * @param array $vectorB 文章B的标签向量
     * @return float 0到1之间的数,1表示完全一样
     */
    public static function cosineSimilarity($vectorA, $vectorB)
    {
        $dotProduct = 0.0;
        $magnitudeA = 0.0;
        $magnitudeB = 0.0;

        $n = count($vectorA);
        if ($n != count($vectorB)) {
            return 0.0; // 维度不一致,无法计算
        }

        for ($i = 0; $i < $n; $i++) {
            $dotProduct += $vectorA[$i] * $vectorB[$i];
            $magnitudeA += pow($vectorA[$i], 2);
            $magnitudeB += pow($vectorB[$i], 2);
        }

        if ($magnitudeA == 0 || $magnitudeB == 0) {
            return 0.0;
        }

        return $dotProduct / ($magnitudeA * $magnitudeB);
    }

    /**
     * 为用户生成推荐列表
     * @param int $userId 用户ID
     * @param array $allArticles 所有文章数据(包含文章标签向量)
     */
    public static function recommendForUser($userId, $allArticles)
    {
        // 1. 获取用户画像向量
        // 假设我们从数据库或者缓存里读出来,这里为了演示,手动构造一个
        // 比如这个用户喜欢 PHP, Web开发, 后端
        $userVector = [1, 1, 0, 0, 0]; // 对应标签: PHP, Web, Java, Python, Go

        $recommendations = [];

        // 2. 遍历所有文章,计算与用户的相似度
        foreach ($allArticles as $article) {
            // 文章向量
            $articleVector = $article['tags_vector'];

            // 计算相似度
            $score = self::cosineSimilarity($userVector, $articleVector);

            // 如果分数大于0.5,就算推荐
            if ($score > 0.5) {
                $recommendations[] = [
                    'article_id' => $article['id'],
                    'title' => $article['title'],
                    'score' => $score
                ];
            }
        }

        // 3. 按分数排序
        usort($recommendations, function($a, $b) {
            return $b['score'] <=> $a['score'];
        });

        return array_slice($recommendations, 0, 5); // 只返回前5条
    }
}

// 模拟数据
$articles = [
    [
        'id' => 101,
        'title' => 'PHP从入门到放弃',
        'tags_vector' => [1, 0, 0, 0, 0] // 只有PHP
    ],
    [
        'id' => 102,
        'title' => 'Java高并发实战',
        'tags_vector' => [0, 0, 1, 0, 0] // 只有Java
    ],
    [
        'id' => 103,
        'title' => 'Web全栈开发指南',
        'tags_vector' => [1, 1, 0, 0, 0] // PHP + Web
    ]
];

// 假设用户收藏了103号文章,所以向量变成了 [1, 1, 0, 0, 0]
$result = RecommendationEngine::recommendForUser(1001, $articles);

echo "推荐结果:n";
print_r($result);

运行这段代码,你会看到:

  1. 103号文章(Web全栈)相似度最高,排在第一。
  2. 101号文章(PHP入门)因为只包含一个共同标签,相似度较低,排在后面。
  3. 102号文章(Java高并发)完全不相关,被过滤掉了。

这就是“基于内容”的推荐。它很精准,不需要像“基于协同过滤”那样去分析“喜欢这篇文章的人还喜欢那篇文章”(这会导致推荐过于同质化,比如大家都看美食,系统就一直推美食,你容易吃到吐)。

进阶:混合推荐
知乎实际上是把这两种结合起来用的。

  • 热榜(热度算法):解决“发现热点”的需求。
  • 推荐(协同过滤):解决“发现感兴趣内容”的需求。
  • 个性化信息流(Feed流):把两者混合,既有热点又有兴趣。

第四模块:架构与性能 —— 别让PHP变成面条代码

聊完了算法,咱们得聊聊架构。因为算法再好,如果架构烂,那就是一坨屎。

很多人质疑PHP性能差,是因为他们把PHP当成了同步阻塞的脚本。在现代架构中,PHP早就不是那个样子了。

1. 异步IO与高并发

知乎这种体量的系统,绝对不能用同步阻塞的代码去处理点赞和推荐。

我们需要用到PHP的协程异步IO

<?php
// 这是一个基于 Swoole 协程的伪代码示例

use SwooleCoroutine as Co;

$articles = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 启动10个协程,并发处理推荐逻辑
Cocreate(function() use ($articles) {
    foreach ($articles as $id) {
        Cogo(function() use ($id) {
            // 这里模拟去数据库查推荐列表
            $recommendations = Database::getRecommendations($id);

            // 这里模拟推送到客户端
            Client::push($id, json_encode($recommendations));

            echo "用户 {$id} 的推荐列表已推送n";
        });
    }
});

// 即使处理10万用户,这行代码也会瞬间完成,等待后台异步执行

就像刚才那段代码,虽然只有几行,但它利用Swoole,可以轻松撑起每秒几万甚至几十万的并发连接。这就是PHP的杀手锏。

2. 缓存为王

再牛逼的算法,没有缓存也是白搭。

  • 用户画像缓存:用户收藏了文章,我们更新用户画像。但不要马上把全量标签存数据库。把最新的标签存Redis,过期时间设为1小时。读的时候,直接读Redis。
  • 热点文章缓存:首页的热榜文章,永远不要去查数据库。Redis的ZSet存了,直接拿。
  • 推荐列表缓存:每个用户的推荐列表,缓存5分钟。5分钟一更新,这样既保证了新鲜度,又减轻了数据库压力。

3. 队列削峰填谷

当某个爆款文章火了,几万人同时点赞,你的数据库瞬间就堵死了。这时候你需要一个消息队列。

用户点赞 -> 发送消息到RabbitMQ/Kafka -> PHP Worker异步消费 -> 更新Redis计数 -> 后台异步计算热度 -> 写入数据库(低频)。

这种“解耦”思想,是所有高并发架构的核心。PHP配合RabbitMQ,简直是天作之合。


第五模块:实战整合 —— 一个简易的PHP知乎后端原型

最后,咱们把上面的东西串起来。别光看不练,哪怕是个简易模型,也要跑起来看看。

我们要实现一个 ZhihuLite 类,包含热度计算、收藏处理和推荐生成。

<?php

// 模拟数据库类
class MockDB {
    private static $articles = [
        1 => ['title' => 'PHP是世界上最好的语言', 'tags' => ['PHP', '编程', '争论'], 'likes' => 100, 'comments' => 10, 'favorites' => 5, 'time' => time() - 3600],
        2 => ['title' => '前端框架的衰落', 'tags' => ['前端', '技术趋势', '就业'], 'likes' => 50, 'comments' => 2, 'favorites' => 1, 'time' => time() - 86400],
        3 => ['title' => '如何写出优雅的代码', 'tags' => ['编程', '架构', '最佳实践'], 'likes' => 200, 'comments' => 50, 'favorites' => 20, 'time' => time()],
    ];

    // 模拟用户收藏记录
    private static $userFavorites = [
        'u1' => [1, 3] // 用户u1收藏了1和3
    ];

    // 全局标签库
    private static $allTags = ['PHP', '编程', '争论', '前端', '技术趋势', '就业', '架构', '最佳实践'];

    public static function getArticle($id) {
        return self::$articles[$id] ?? null;
    }

    public static function getUserFavorites($uid) {
        return self::$userFavorites[$uid] ?? [];
    }
}

class ZhihuLite {

    // 核心算法入口
    public function process($uid) {
        echo "=== 正在处理用户 {$uid} 的请求 ===n";

        // 1. 获取用户收藏
        $favoriteIds = MockDB::getUserFavorites($uid);

        // 2. 收集用户感兴趣的标签(构建画像)
        $userTagSet = [];
        foreach ($favoriteIds as $id) {
            $article = MockDB::getArticle($id);
            if ($article) {
                $userTagSet = array_merge($userTagSet, $article['tags']);
            }
        }

        // 去重
        $userTags = array_unique($userTagSet);

        // 3. 遍历所有文章,计算热度与相似度
        $results = [];

        foreach (MockDB::$articles as $id => $article) {
            // A. 计算热度
            $heat = ArticleHeatCalculator::calculateHeat(
                $article['likes'], 
                $article['comments'], 
                $article['favorites'], 
                $article['time']
            );

            // B. 计算推荐分数(基于内容的相似度)
            // 这里我们简单把用户标签和文章标签的交集长度作为相似度
            $intersection = array_intersect($userTags, $article['tags']);
            $similarity = count($intersection) / count(array_unique(array_merge($userTags, $article['tags'])));

            // C. 综合评分 = 热度 * 0.7 + 相似度 * 0.3
            $finalScore = $heat * 0.7 + ($similarity * 100); // 热度可能是浮点,相似度是0-1,做个比例调整

            $results[] = [
                'id' => $id,
                'title' => $article['title'],
                'heat' => round($heat, 2),
                'similarity' => round($similarity, 2),
                'score' => round($finalScore, 2)
            ];
        }

        // 4. 排序并返回
        usort($results, function($a, $b) {
            return $b['score'] <=> $a['score'];
        });

        echo "推荐结果(Top 3):n";
        foreach (array_slice($results, 0, 3) as $r) {
            echo "- [{$r['id']}] {$r['title']} (综合分: {$r['score']})n";
        }
    }
}

// 运行演示
$zhihu = new ZhihuLite();
$zhihu->process('u1');

结语

你看,其实所谓的“知乎算法”,在代码层面并没有那么神秘。

  1. 热度:是一碗热粥,得有衰减系数,别让陈年老文占着茅坑。
  2. 收藏:是给大脑做CT,通过标签提取用户兴趣点。
  3. 推荐:是相亲媒人,拿着你的画像去挑最般配的文章。

PHP之所以强大,不仅在于它语法简单,更在于它能在这些逻辑层面上提供足够的灵活性。你不用去维护几百G的Hadoop集群,用PHP配合Redis和MQ,你完全可以搭起一个基于现代架构的高性能推荐系统。

当然,真正的知乎系统还要处理黑产刷赞、图片CDN加速、实时弹幕推送等无数细节。但万变不离其宗,核心逻辑依然是这几个模型。

好了,今天的讲座就到这里。下课!记得把代码跑一下,如果报错了,那一定是你数据库没装好,跟我没关系!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注