🪁由掘金沸点热度引发的…

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

这是怎么做的呢?

掘金是一个稿件文章为主要业务的平台,可以猜测它使用了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就不会用得分排序了,而会用指定字段排序(以前做业务的时候发生过这种问题)。

  • 加强得分不止可以下降,也可以提升,也就是将需要排前的升分,或者将需要排后的降分,只是两种不同的思路习惯。

  • 使用不同函数,代表了不同的曲线,有时候要根据选择的函数曲线来做实际验证,从而调整选择合适的函数来进行组合。

相关推荐
Victor3563 分钟前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack3 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo4 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor3565 分钟前
MongoDB(3)什么是文档(Document)?
后端
牛奔2 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌7 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
KYGALYX8 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
爬山算法9 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端