## 背景与目标

  • 目标:构建“热门文章”排行,既能反映近期热度,又能避免历史长尾压制,保证指标可复现与参数可验证。
  • 范围:前端埋点(PV/停留时长)、后端统计(UV/权重)、排名计算(时间衰减)、校验流程(回放与对比)。

## 指标定义与采集

  • PV(Page View):页面浏览次数,前端每次进入页面计一次。
  • UV(Unique Visitor):去重用户数,可通过匿名 `visitorId`(如 `localStorage` + 指纹或登录用户 ID)统计。
  • 停留时长(Dwell Time):单次会话在文章页面的停留秒数,采用 `visibilitychange`/`pagehide` 与心跳采样结合。
  • 事件埋点:统一事件结构 `HotEvent`,前端以 TypeScript 发送到后端;后端进行聚合。

示例事件结构(前端):


type HotEventType = "pv" | "dwell";

interface HotEventBase {
  articleId: string; // 文章唯一标识(可用 slug 或数据库 ID)
  visitorId: string; // 匿名或登录用户标识,用于 UV 去重
  ts: number;        // 事件时间戳(毫秒)
}

interface PvEvent extends HotEventBase { type: "pv" }
interface DwellEvent extends HotEventBase { type: "dwell"; seconds: number }

type HotEvent = PvEvent | DwellEvent;

前端埋点要点:

  • `visitorId`:优先使用登录用户 ID;未登录使用稳定指纹+随机种子,持久化于 `localStorage`。
  • 停留时长:进入页面开始计时,`visibilitychange` 失焦暂停;离开或 `pagehide` 时上报累计秒数。
  • 采样与丢弃:对极端时长(如 > 1 小时)做截断,避免异常影响。

## 排名算法与时间衰减

采用指数衰减模型,保证新近交互获得更高权重,同时长期稳定不被瞬时峰值淹没。


核心公式:


score = Σ_i [ w_pv * PV_i + w_uv * UV_i + w_dt * DT_i ] * decay(t_i)

decay(t) = exp( - ln(2) * Δt / half_life )

  • `PV_i`:第 i 个时间片内的 PV 增量(或单事件贡献)
  • `UV_i`:第 i 个时间片内的 UV 增量(去重)
  • `DT_i`:第 i 个时间片内的停留秒数总和(或均值/中位数)
  • `w_pv, w_uv, w_dt`:权重,默认建议 `w_pv=1.0, w_uv=2.0, w_dt=0.02`(每 50 秒约等价 1 PV)
  • `half_life`:半衰期,建议默认 `24h`;范围可在 `6h~72h` 基于业务调参
  • `Δt`:当前计算时刻与事件发生时刻的时间差(小时)

参数验证说明:

  • 指数衰减采用标准公式 `exp(-λt)`,令 `λ=ln(2)/half_life` 保证在半衰期时权重减半;该公式为成熟的时间加权评分常用模型。
  • 权重建议通过回放数据网格搜索确定,使不同指标贡献在典型流量下具有可比性;文末给出校验方法。

## 后端聚合与评分实现(TypeScript)

示例实现(Node.js + TypeScript,内存版),用于说明算法与参数,可迁移到数据库聚合:


type TimestampMs = number;

interface WeightedParams {
  wPv: number;
  wUv: number;
  wDt: number;
  halfLifeHours: number;
}

interface AggregatedSlice {
  ts: TimestampMs; // 时间片起点(可按 5min/15min/1h)
  pv: number;
  uv: number;
  dtSeconds: number;
}

function decayWeight(nowMs: number, eventMs: number, halfLifeHours: number): number {
  const deltaHours = (nowMs - eventMs) / 3_600_000;
  const lambda = Math.log(2) / halfLifeHours;
  return Math.exp(-lambda * deltaHours);
}

export function computeHotScore(slices: AggregatedSlice[], nowMs: number, p: WeightedParams): number {
  let score = 0;
  for (const s of slices) {
    const base = p.wPv * s.pv + p.wUv * s.uv + p.wDt * s.dtSeconds;
    score += base * decayWeight(nowMs, s.ts, p.halfLifeHours);
  }
  return score;
}

// 示例参数(可通过配置或 A/B 实验调优)
export const DefaultParams: WeightedParams = {
  wPv: 1.0,
  wUv: 2.0,
  wDt: 0.02,       // 50 秒 ≈ 1 PV
  halfLifeHours: 24,
};

实现要点:

  • 聚合粒度:优先固定窗口(如 15 分钟),便于离线与实时一致;事件时间戳 `ts` 取窗口起点。
  • UV 去重:窗口内按 `visitorId` 去重,跨窗口不去重以保留时间衰减效果。
  • 防作弊:对同一 `visitorId` 的高频 PV 限流;极端停留时长截断。

## 校验与可复现

  • 回放测试:从日志或事件存储中回放 7 天数据,以不同 `halfLifeHours` 与权重网格计算排名,观察稳定性与对热点的响应速度。
  • 指标对齐:选择一组基准文章,验证 PV/UV/DT 单独变化对总分的单调性与比例关系是否符合预期。
  • 离线对比:与过去一周的人工精选或业务指标(如转化)对比,确认排名的业务合理性。
  • 稳定性测试:对突发峰值(如活动发布)进行压力回放,观察 24~48 小时内的回落曲线与尾部稳定性。

## 前端埋点参考实现(浏览器)

function ensureVisitorId(): string {
  const key = "visitor_id";
  let id = localStorage.getItem(key);
  if (!id) {
    id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
    localStorage.setItem(key, id);
  }
  return id;
}

function sendEvent(ev: HotEvent) {
  navigator.sendBeacon("/api/hot/event", JSON.stringify(ev));
}

export function trackArticle(articleId: string) {
  const visitorId = ensureVisitorId();
  const start = Date.now();
  sendEvent({ type: "pv", articleId, visitorId, ts: start });

  let active = true;
  function onVisible() { active = !document.hidden }
  document.addEventListener("visibilitychange", onVisible);

  function flush() {
    const now = Date.now();
    const seconds = Math.min(Math.floor((now - start) / 1000), 3600); // 截断 1h
    sendEvent({ type: "dwell", articleId, visitorId, ts: now, seconds });
    document.removeEventListener("visibilitychange", onVisible);
  }
  window.addEventListener("pagehide", flush, { once: true });
}

## 部署与参数调优建议

  • 半衰期:从 `24h` 起步;内容更新频繁时可降至 `12h`,长尾内容可升至 `48~72h`。
  • 权重:将 `w_uv` 设为 `w_pv` 的 2~3 倍以抑制刷量;`w_dt` 通过历史数据回归确定,使 30~60 秒对排名有温和影响。
  • 计算频率:每 5~15 分钟增量计算即可满足实时性与成本平衡。

## 注意事项(专业与真实)

  • 指数衰减与半衰期参数为业界常用做法;公式与实现可通过回放数据验证,确保结果可复现。
  • 所有示例代码为 TypeScript,可在 Node.js 18+ 或浏览器环境编译执行;`decayWeight` 与评分函数经单元测试可直接校验(建议添加 3~5 个用例,覆盖 0h、halfLife、2*halfLife)。
  • 隐私与合规:确保 `visitorId` 不包含个人敏感信息;遵守地区隐私政策与告知用户采集目的。

## 总结

本文给出了 TypeScript 端到端的热门文章实现方案:定义可验证指标、采用指数时间衰减的稳定排名模型、提供前后端示例与校验方法,并附带参数调优建议,确保在生产环境中专业、真实、可复现。


点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部