本文目录导读:

实现推荐重排在 PHP 项目中,通常有两种主流策略:规则/算法层面和展示/渲染层面,推荐重排的核心目的是在已有推荐结果的基础上,通过一定的策略(如打散、去重、多样性、上下文增强)来优化排序,提升用户体验。
以下是 PHP 项目可能采用的几种实现方式,从简单到复杂:
最简单的打散策略 (基于规则的硬编码)
如果推荐列表来源单一(例如从数据库直接拉取),但你需要避免连续出现同类型内容(比如全是文章,或全是视频),可以使用隔位插入或类别插花。
<?php
/**
* 基于类别的打散重排
* @param array $items 原始推荐列表 [['id'=>1, 'type'=>'article'], ...]
* @param string $field 用于打散的字段(如 type, category)
* @return array
*/
function shuffleByField(array $items, string $field = 'type'): array {
$grouped = [];
// 1. 按字段分组
foreach ($items as $item) {
$key = $item[$field] ?? 'default';
$grouped[$key][] = $item;
}
// 2. 依次从每个组中取出一个元素(轮询)
$result = [];
// 使用引用追踪每个组的当前索引
$pointers = array_fill_keys(array_keys($grouped), 0);
$total = count($items);
$maxRetries = $total * 10; // 防止死循环
$count = 0;
while (count($result) < $total && $count < $maxRetries) {
$count++;
foreach ($grouped as $key => &$group) {
if (isset($group[$pointers[$key]])) {
$result[] = $group[$pointers[$key]];
$pointers[$key]++;
}
}
unset($group); // 注意引用循环
}
// 如果还有剩余(某个组特别长),直接追加到最后
foreach ($grouped as $key => &$group) {
while (isset($group[$pointers[$key]])) {
$result[] = $group[$pointers[$key]];
$pointers[$key]++;
}
}
unset($group);
return $result;
}
// 使用示例
$items = [
['id' => 1, 'type' => 'video', 'score' => 0.9],
['id' => 2, 'type' => 'article', 'score' => 0.85],
['id' => 3, 'type' => 'video', 'score' => 0.8],
// ...
];
$reRanked = shuffleByField($items, 'type');
优点:代码极简,无额外依赖,适合列表长度和类别固定的小型站点。
结合外部排序服务(PHP 作为中间层)
对于更复杂的重排(如基于 CTR 预测、多样性模型、图神经网络),PHP 通常不适合做复杂的计算,常见的架构是:
- 后端存储/推荐引擎(Go/Python/C++):生成初步的高分候选集,包含特征向量或分数。
- PHP 应用层:接收候选集,调用一个 重排微服务(Ranker) 或 在线学习模型(如 TensorFlow Serving) 进行二次排序。
<?php
// PHP 调用重排服务(假设基于 Python 的微服务)
class RankerClient {
private string $serviceUrl = 'http://ranker-service:8080/rerank';
public function rerank(array $candidates, string $userId): array {
$payload = json_encode([
'user_id' => $userId,
'items' => $candidates, // 包含 id, features 等
'strategy' => 'diversity+mrr' // 指定重排策略
]);
$ch = curl_init($this->serviceUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT_MS => 200, // 严格超时控制
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$result = json_decode($response, true);
return $result['items'] ?? $candidates;
}
// 降级:返回原始列表
return $candidates;
}
}
// 业务使用
class RecommendController {
public function getRecommendations(Request $request) {
$userId = $request->get('user_id');
// 1. 从召回/粗排获取 200 个候选
$rawItems = $this->recommendService->fetchCandidates($userId, 200);
// 2. 调用重排微服务
$ranker = new RankerClient();
$finalItems = $ranker->rerank($rawItems, $userId);
// 3. 截取最终展示数量
return array_slice($finalItems, 0, 20);
}
}
关键考虑:
- 超时与降级:重排服务可能繁忙,必须设置超时,超时后返回原始列表。
- 特征传输:PHP 需要能传递候选集的特征(如分类、标签、embedding ID、场景上下文),而不仅仅是 ID。
纯 PHP 的热启动算法(适合小规模实时性高场景)
如果业务规模较小(单品数 < 1000),且不想引入外部服务,可以在 PHP 中使用MMR(最大边际相关性) 或 Novelty-based 重排。
MMR 简易实现(平衡相关性与多样性)
<?php
/**
* MMR 重排算法
* @param array $items [['id'=>1, 'similarities'=>[...]], ...] 需要包含 pairwise similarities
* @param array $queryFeatures 用户当前偏好向量(若没有可省略)
* @param float $lambda 多样性权重 (0~1, 1=完全相关, 0=完全多样性)
* @return array 重排后的 items
*/
function mmrRerank(array $items, float $lambda = 0.5): array {
if (empty($items)) return [];
$selected = [];
$candidates = $items;
// 假设这里需要提前计算 pairwise similarity(可使用标签余弦或自定义函数)
// 简化:items 中自带 'sim' 矩阵或 'features'
while (!empty($candidates) && count($selected) < count($items)) {
$bestItem = null;
$bestScore = -INF;
foreach ($candidates as $index => $cand) {
// 相关性分数(假设已有 'score' 字段)
$relScore = $cand['score'] ?? 0;
// 与已选集合的最大相似度(多样性惩罚)
$maxSimToSelected = 0;
foreach ($selected as $sel) {
// 假设存储了 pairwise similarity 或能计算
$sim = $cand['similarities'][$sel['id']] ?? 0;
if ($sim > $maxSimToSelected) {
$maxSimToSelected = $sim;
}
}
// MMR 公式: argmax( λ * Rel(i) - (1-λ) * maxSim(j in selected, i) )
$score = $lambda * $relScore - (1 - $lambda) * $maxSimToSelected;
if ($score > $bestScore) {
$bestScore = $score;
$bestItem = $cand;
$bestIndex = $index;
}
}
if ($bestItem) {
$selected[] = $bestItem;
unset($candidates[$bestIndex]);
} else {
break;
}
}
return $selected;
}
注意:纯 PHP 做矩阵运算效率不高,适合小规模数据(<1000 items),如果列表过大(如数万),建议使用 C 扩展(如 php-ml 或 tensorflow-php)或迁移至外部服务。
核心建议
| 策略 | 适合场景 | 性能 | 维护成本 |
|---|---|---|---|
| 简单打散 | 内容类型少,列表固定 | 极高 | 低 |
| 外部重排服务 | 中大规模,需要 ML 模型 | 高(降低 P99) | 中高(多一个微服务) |
| 纯 PHP 算法 | 小规模,快速上线 | 中等(CPU 瓶颈) | 低(无需新架构) |
推荐折衷方案:PHP 负责展示层重排,复杂计算(模型推理、embedding 相似度计算)外包给其他语言,利用 Redis 缓存重排结果,避免每次请求都重新计算。