大家观察掘金的沸点热度排行榜,有没有发现很多热度高的沸点能连续靠前好几天,过几天后,才慢慢下降。 并且有的点赞和评论基本一样,但是热度排名却有不少差距。
这是怎么做的呢?

掘金是一个稿件文章为主要业务的平台,可以猜测它使用了ElasticSearch,Redis......等。其中,ElasticSearch和Redis都可以用来做排序,但是这里猜测它是使用了ElasticSearch做的热度排序。那么,是如何用ES(这里简称ES)做的沸点热度排序呢?
我们知道:
默认情况下,ES的搜索结果是排序的,是按 相关性 倒序排列的(相关性最高的排在最前面)。 每个文档都有相关性评分,用一个正浮点数字段 _score 来表示 。_score 的评分越高,相关性越高。 查询语句会为每个文档生成一个 _score 字段。 主要使用两种算法进行排序,TF/IDF 和 MD25,其中MD25算法是ES5以后新加的。
其实不论是TF/IDF或者BM25都是计算相关性的,只不过BM25的计算公式会将相关性的计算得分,在超过一个阈值后控制在一个稳定的数值波动上,而TF/IDF是一个长增长的曲线,BM25是为了解决词频过高,导致的得分过高而造成的排序靠前。
TF/ID百度百科:

TF-IDF(term frequency--inverse document frequency)是一种用于信息检索与数据挖掘的常用加权技术。TF是词频(Term Frequency),IDF是逆文本频率指数(Inverse Document Frequency)。
BM25是信息索引领域用来计算Query与文档相似度得分的经典算法,不同于TFIDF,BM25的公式主要由三个部分组成:
- 对Query进行语素解析,生成语素qi;
- 对于每个搜索结果D,计算每个语素qi与D的相关性得分;
- 将qi相对于D的相关性得分进行加权求和,从而得到Query与D的相关性得分。
公式如下:

虽然我们介绍了TF/IDF和BM25,但是貌似并不能满足掘金沸点的产品设计需求,那这时候怎么办呢?
对了,ES中还提供了二次计算得分的方式 function score,我们用它来解决。
先简单介绍下 function score:
function score 是 elasticsearch 提供的一种通过函数来对相关性评分进行二次计算的方法。这里的函数可以大致分为两种。
第一种:script_score 我们开发人员自己通过 plain painless 进行编写的。
json
GET /_search
{
"query": {
"script_score": {
"query": {
"match": { "message": "elasticsearch" }
},
"script": {
"source": "doc['my-gg'].value / 8 " //这个函数是我们自定义的,可以解决的场景问题很多
}
}
}
}
第二种:elasticsearch 提供的,有:
weight : 加权。
random_score : 随机打分。
field_value_factor : 使用字段的数值参与计算分数。
decay_function : 衰减函数 gauss, linear, exp 等
加权函数weight function:
css
{
"query": {
"function_score": {
"query": { "match": { "message": "elasticsearch" } },
"functions": [
{
"filter": { "match": { "title": "elasticsearch" } },
"weight": 8
}
]
}
}
}
通过对文档进行加权。比如在文章搜索中,我们文章title命中关键词得到的分数,应该比文章content命中关键词得到的分数更高。 weight 是 8 ,即自定义函数得分 func_score = 8 ,最终结果的 score 等于 query_score * 8。
衰减函数 decay function:
decay function 可以是以下任意一种函数:
-
linear : 线性函数
-
exp : 指数函数
-
gauss : 高斯函数
-
origin : 中心点,只能是数值、日期、geo-point
-
scale : 定义到中心点的距离
-
offset : 偏移量,默认 0
-
decay : 衰减指数,默认是 0.5
bash
GET /_search
{
"query": {
"function_score": {
"gauss": {
"@timestamp": {
"origin": "2024-01-16",
"scale": "10d",
"offset": "5d",
"decay": 0.5
}
}
}
}
}
上述例子中gauss曲线的原点 (origin )的值是 2024-01-16 , offset 是 5d ,也就是在范围 2024-01-16 - 5d <= value <= 2024-01-16 + 5d 内的所有值都会被当作原点 origin 处理------所有这些点的评分都是满分 权重1.0 。
在此范围之外,评分开始衰减,衰减率由 scale 值(此例中的值为 5 )和 衰减值 decay (此例中为默认值 0.5 )共同决定。
linear 、 exp 和 gauss (线性、指数和高斯)函数三者之间的区别在于范围( origin +/- (offset + scale) )之外的曲线形状:
可以参考(注意其中一些内容已过时): Elasticsearch: 权威指南 | Elastic
linear 线性函数是条直线,一旦直线与横轴 0 相交,所有其他值的评分都是 0.0 。
exp 指数函数是先剧烈衰减然后变缓。
gauss 高斯函数是钟形的------它的衰减速率是先缓慢,然后变快,最后又放缓。
选择曲线的依据完全由期望评分 _score 的衰减速率来决定,即距原点 origin 的值。
这里考虑掘金主要是使用 field value factor做的热度排序,那么我们再来介绍下field value factor:
把一条沸点的评论数和赞作为相关性分数计算中的一部分,点赞和评论更多的应该排在前面。
json
{
"query": {
"function_score": {
"query": { "match": { "message": "elasticsearch" } },
"field_value_factor": {
"field": "likes",
"factor": 1.2,
"missing": 1,
"modifier": "log1p"
}
}
}
}
- field : 参与计算的字段。
- factor : 乘积因子,默认为 1 ,将会与 field 的字段值相乘。
- missing : 如果 field 字段不存在则使用 missing 指定的缺省值。
- modifier : 计算函数,为了避免分数相差过大,用于平滑分数,可以是以下之一:
- none : 不处理,默认
- ln : ln(factor * field_value)
- ln1p : ln(1 + factor * field_value)
- ln2p : ln(2 + factor * field_value)
- log : log(factor * field_value)
- log1p : log(1 + factor * field_value)
- log2p : log(2 + factor * field_value)
- square : 平方,(factor * field_value)^2
- sqrt : 开方,sqrt(factor * field_value)
- reciprocal : 求倒数,1/(factor * field_value)
假设某个沸点的点赞数是 800 ,那么例子中其打分函数生成的分数就是 log(1 + 1.2 * 800),最终的分数是原来的 query 分数与此打分函数分数相差的结果。
到这里,我们忽然想到,掘金它会按照日期衰减啊,这个貌似也不是太匹配啊,别急,其实我们是可以支持多个function的,接着往下走:
有多个加强函数(或是filter+加强函数),每一个加强函数会产生一个加强score,这样的话functions会有多个加强score。
GET
{
"query": {
"function_score": {
"query": {.....},
"functions": [
{ "field_value_factor": ... },
{ "gauss": ... },
{ "filter": {...}, "weight": ... }
],
"score_mode": "sum", //决定加强score 怎么合并,
"boost_mode": "multiply" //決定总加强score怎么和old_score合并
}
}
}
这时候我们可以考虑field value factor组合衰减函数 decay function做排序,使用field value factor对点赞数和评论数进行计算得分,使用 decay function对时间进行计算得分,然后对所有加强score进行合并。
由此就可以通过ES实现掘金沸点的热度排序了。
需要注意的是(经验):
-
在使用得分排序的时候,不要指定字段排序,如果指定了字段,这时候ES就不会用得分排序了,而会用指定字段排序(以前做业务的时候发生过这种问题)。
-
加强得分不止可以下降,也可以提升,也就是将需要排前的升分,或者将需要排后的降分,只是两种不同的思路习惯。
-
使用不同函数,代表了不同的曲线,有时候要根据选择的函数曲线来做实际验证,从而调整选择合适的函数来进行组合。