谈谈推荐排序

  • Post author:
  • Post category:IT
  • Post comments:0评论

本文说的排序并不是指「冒泡」之类的技术概念,而是一个业务相关的问题。

举例来说:某个网站,每天都能产生很多数据,需要一个推荐列表页面来展示数据。最初是完全按照时间倒序来排序的,但是这样就产生了一个问题:新鲜的数据不一定是有价值的数据!假设某个时段灌水的数据比较多,那么用户当时在列表页看到的就都是灌水的内容。既然如此,不妨换个思路:给每个数据投票,投票规则可以按业务逻辑自定义,比如:每次评论加一票,每次转发加两票等等。然后按照投票数来倒序是不是就可以了?可惜还有问题:有价值的数据不一定是新鲜的数据!假设历史上曾经产生了一个高票的数据,那么不管过多久,它都会一直占据前排座位。由此可见最好的结果是让用户能看到既新鲜又有价值的数据。

实际上此问题早就有人讨论过了,建议大家阅读相关文章:

一种方法是让分数随着时间的流逝而降低,比如说一条数据原本的分数是 100 分,第二天衰减到 50 分,第三天衰减到 25 分,以此类推。此方法的优点是简单易懂,缺点是数据的分数随着时间的变化而变化,实际应用中,可能需要用 cron 之类的方法定期更新数据的分数,一旦数据比较多就悲剧了。

另一种方法是让分数本身就包含时间因子:「t / 43200 + log10(x)」:

  • 「t」表示的是数据生成时的时间戳。
  • 「43200」表示的是半天(12 个小时)的秒数。
  • 「x」表示的是数据得到的票数。

于是乎可知「t / 43200」得到的就是有多少个半天:

<?php

$result = [];

$dates = [
    '2017-01-01',
    '2017-01-02',
    '2017-01-03',
    '2017-01-04',
    '2017-01-05',
];

foreach ($dates as $date) {
    $result[$date] = strtotime($date) / 43200;
}

var_export($result);

/*

结果:

array (
  '2017-01-01' => 34334,
  '2017-01-02' => 34336,
  '2017-01-03' => 34338,
  '2017-01-04' => 34340,
  '2017-01-05' => 34342,
)

*/

?>

实际上「t / 43200」相当于是「时间分」,随着时间的增加而增加,其基数比较大,保证了新鲜的数据会排在前面,同时每天「时间分」的差值为 2,后面会用到这个数字。

那「log10(x)」又是什么意思呢,我们可以把它理解成「价值分」,其基数比较小,之所以做 log10 的对数运算就是为了快速衰减,确保前面投票的权重大于后面投票,如此有利于让有价值的数据尽早被发现:

<?php

$result = [];

$values = [
    10000,
    1000,
    100,
];

foreach ($values as $value) {
    $result[$value] = sprintf('%.5f', log10($value));
}

var_export($result);

/*

结果:

array (
  10000 => '4.00000',
  1000 => '3.00000',
  100 => '2.00000',
)

*/

?>

如上所示:如果一条数据的得票数是 100,那么它的价值分将是 2,最多可以在推荐列表上停留一天;如果一条数据的得票数是 1000,那么它的价值分将是 3,最多可以在推荐列表上停留一天半;如果一条数据的得票数是 10000,那么它的价值分将是 4,最多可以在推荐列表上停留两天,如此保证价值分高的数据不会霸占前排座位太久。

最后要说明的是,与其死记硬背公式,倒不如理解它的意思,并根据客观情况调整,比如说,有的人觉得时间分本身太大了,那么我们可以减去一个参照时间。有的人觉得为什么一定要除以 12 个小时,除以 12.5 个小时可不可以?答案当然是可以的!有的人觉得为什么一定要用以 10 为底的对数,可不可以使用科学对数?答案当然也是可以的!只要你理解了分数中「时间分」和「价值分」的用意,那么怎么实现它们都可以。

发表回复