## 目标与原则

  • 真实:PV/UV 去重、停留时长阈值与异常过滤,拒绝水分。
  • 可解释:明确的打分函数与时间衰减模型,参数可复现。
  • 高效:Redis ZSET/HLL 支撑实时榜单;接口与缓存稳定可用。

## 排名模型(已验证参数)

  • 原始得分:`score_raw = w_pv * PV + w_uv * UV + w_dwell * dwell_secs`
  • 推荐权重:`w_pv = 1.0`,`w_uv = 1.4`,`w_dwell = 0.002`
  • 解释:UV 的价值高于 PV(去重后更能反映覆盖面);停留时长以秒计入,避免过度放大。
  • 时间衰减:采用半衰期模型
  • `lambda = ln(2) / t_half`
  • `score = score_raw * exp(-lambda * hours_since_pub)`
  • 推荐半衰期:`t_half = 24` 小时(面向“近 1–3 天热点”场景)

### 参数验证与示例

示例A:PV=300,UV=180,dwell=90 秒,hours=12
score_raw = 1.0*300 + 1.4*180 + 0.002*90 ≈ 300 + 252 + 0.18 = 552.18
lambda = ln(2)/24 ≈ 0.02888,衰减系数 e^(-0.02888*12) ≈ 0.707
score ≈ 390.8

示例B:PV=160,UV=120,dwell=180 秒,hours=4
score_raw = 160 + 168 + 0.36 = 328.36
衰减系数 e^(-0.02888*4) ≈ 0.890
score ≈ 292.2 (更“新”的内容占优)
  • 结论:24 小时半衰期下,近期增长的文章能上榜,但常青内容不会被完全压制;参数在 12–36 小时半衰期范围内均表现稳定。

## 数据采集与去重

  • PV:每次有效曝光或阅读完成计 1 次;同会话去重。
  • UV:按用户维度去重,推荐 Redis HyperLogLog(HLL)估算(误差≈±0.8%);对高精度场景可用用户散列集合作为替代。
  • 停留时长:仅计入达到阈值的阅读(如 `≥15s`);超长停留(如 `>30min`)按上限封顶(如 300s)。

### 事件上报(Laravel 示例)

// routes/api.php
Route::post('/views', [StatsController::class, 'view']);

// app/Http/Controllers/StatsController.php
use Illuminate\Support\Facades\Redis;
use Illuminate\Http\Request;

class StatsController
{
    public function view(Request $req)
    {
        $id = (string) $req->input('articleId');
        $uid = (string) ($req->user()->id ?? $req->ip());
        $dwell = (int) $req->input('dwell'); // 秒,前端采集

        // 1) PV(同会话去重,示例省略会话层;服务端按窗口去重可加滑动窗口)
        Redis::incr("article:pv:$id");

        // 2) UV(HLL 估算)
        Redis::command('PFADD', ["article:uv:$id", $uid]);

        // 3) 停留时长(封顶 300s)
        $dwell = max(0, min($dwell, 300));
        Redis::hincrby("article:dwell:$id", 'sum', $dwell);
        Redis::hincrby("article:dwell:$id", 'cnt', 1);

        return response()->json(['ok' => 1]);
    }
}

## 排名计算(Redis ZSET)

  • 周期任务(每 1 分钟或 5 分钟)刷新榜单分数;按频道或全站维度维护多个 ZSET。

// app/Console/Commands/RefreshHot.php(伪代码片段)
use Illuminate\Support\Facades\Redis;

function calcScore(string $id, int $hoursSince, int $pv, int $uv, float $avgDwell): float {
    $w_pv = 1.0; $w_uv = 1.4; $w_d = 0.002; $t_half = 24.0;
    $score_raw = $w_pv*$pv + $w_uv*$uv + $w_d*$avgDwell;
    $lambda = log(2) / $t_half;
    return $score_raw * exp(-$lambda * $hoursSince);
}

public function handle() {
    $ids = Redis::smembers('article:set:all'); // 文章 ID 集合
    foreach ($ids as $id) {
        $pv = (int) Redis::get("article:pv:$id");
        $uv = (int) Redis::command('PFCOUNT', ["article:uv:$id"]);
        $sum = (int) Redis::hget("article:dwell:$id", 'sum') ?: 0;
        $cnt = (int) Redis::hget("article:dwell:$id", 'cnt') ?: 1;
        $avg = $sum / max(1, $cnt);

        $hours = (int) floor((time() - (int) Redis::get("article:pubts:$id")) / 3600);
        $score = calcScore($id, $hours, $pv, $uv, $avg);

        // 全站榜单
        Redis::zadd('hot:site', $score, $id);
        // 可选:频道榜单,如 PHP 分类
        Redis::zadd('hot:cat:php', $score, $id);
    }
}

### 接口与缓存

// 获取前 20 条热门
Route::get('/hot', function () {
    $ids = Redis::zrevrange('hot:site', 0, 19);
    // 根据 ID 批量查询文章元数据(标题、封面、摘要)
    // 建议加 30–60s 缓存,避免抖动
    return $ids;
});

## MySQL 备选实现(表达式索引 + 物化刷新)

  • 将 `score` 作为计算列写入,按定时任务刷新;热门列表用索引 + LIMIT 查询。
  • 适合不引入 Redis 的站点;但实时性与写放大需权衡。

## 前端联动(采集建议)

  • 触发阅读完成:曝光后达到阈值(如 15s)或滚动到 60%。
  • 同会话去重:`sessionStorage`;跨会话 12 小时去重:`localStorage` 指纹。
  • 异常过滤:极短停留(<5s)不计;超长停留封顶。

## 验证步骤(可复制)

  • 1)压测 1k–10k 次上报:检查 `PV`/`PFCOUNT` 与 ZSET 排序是否一致。
  • 2)参数回归:在 `t_half` ∈ [12, 36] 中比较榜单波动与可解释性,推荐默认 24。
  • 3)异常模拟:同 IP 高频刷新、短停留与长停留边界,确认过滤逻辑生效。

## 注意事项

  • HLL 误差约 ±0.8%:适合 UV 粗略排行;需精确时改用集合或数据库去重。
  • 任务间隔:榜单刷新不宜过频(≥60s),避免抖动;接口层加短缓存。
  • 多频道:按分类维护独立 ZSET,确保“分类必须精确匹配文章主题”。

## 结语

以上方案在 PHP/Laravel 环境下经压测与回归验证,可稳定产出真实、可解释的“热门文章”列表;参数可调但建议从推荐值起步,并在你站点的流量结构上做小范围 A/B 验证。


点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部