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

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

这是怎么做的呢?

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

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

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

相关推荐
草捏子8 分钟前
状态机设计:比if-else优雅100倍的设计
后端
考虑考虑2 小时前
Springboot3.5.x结构化日志新属性
spring boot·后端·spring
涡能增压发动积2 小时前
一起来学 Langgraph [第三节]
后端
sky_ph2 小时前
JAVA-GC浅析(二)G1(Garbage First)回收器
java·后端
涡能增压发动积2 小时前
一起来学 Langgraph [第二节]
后端
hello早上好2 小时前
Spring不同类型的ApplicationContext的创建方式
java·后端·架构
roman_日积跬步-终至千里2 小时前
【Go语言基础【20】】Go的包与工程
开发语言·后端·golang
00后程序员4 小时前
提升移动端网页调试效率:WebDebugX 与常见工具组合实践
后端
HyggeBest4 小时前
Mysql的数据存储结构
后端·架构
TobyMint4 小时前
golang 实现雪花算法
后端