haixunMaster/lib/ranking.ts

61 lines
1.8 KiB
TypeScript

import { computePlacementRecencyScore } from "@/lib/scan-recency";
export interface RawPost {
externalId?: string;
text: string;
permalink?: string;
authorName?: string;
postedAt?: Date;
likeCount?: number;
replyCount?: number;
repostCount?: number;
}
export interface RankedPost extends RawPost {
score: number;
}
function recencyBoost(postedAt?: Date): number {
if (!postedAt) return 0.5;
const hours = (Date.now() - postedAt.getTime()) / (1000 * 60 * 60);
if (hours <= 6) return 1.5;
if (hours <= 24) return 1.2;
if (hours <= 72) return 1.0;
return 0.7;
}
export function computeScore(post: RawPost): number {
const likes = post.likeCount ?? 0;
const replies = post.replyCount ?? 0;
const reposts = post.repostCount ?? 0;
const engagement = likes * 1.0 + replies * 2.0 + reposts * 1.5;
return engagement * recencyBoost(post.postedAt);
}
/** 置入海巡排序:互動為輔,時間窗口為主 */
export function computePlacementScore(post: RawPost): number {
const likes = post.likeCount ?? 0;
const replies = post.replyCount ?? 0;
const reposts = post.repostCount ?? 0;
const engagement = likes * 0.6 + replies * 1.2 + reposts * 0.8;
const recency = computePlacementRecencyScore(post.postedAt);
return recency * 2.5 + Math.log10(engagement + 1) * 12;
}
function normalizeText(text: string): string {
return text.trim().toLowerCase().replace(/\s+/g, " ");
}
export function rankAndDedupe(posts: RawPost[], limit = 20): RankedPost[] {
const seen = new Set<string>();
const ranked: RankedPost[] = [];
for (const post of posts) {
const key = post.externalId ?? `${post.authorName ?? ""}:${normalizeText(post.text).slice(0, 120)}`;
if (seen.has(key)) continue;
seen.add(key);
ranked.push({ ...post, score: computeScore(post) });
}
return ranked.sort((a, b) => b.score - a.score).slice(0, limit);
}