运营的怒火:爆款为什么排后面
搜索上线后,老王收到运营的连环质问:搜「燕麦」,月销一万的「燕麦拿铁」排第二,月销三百的「燕麦酥」排第一——这排序是认真的吗?打开结果一看,每条文档都带着一个 _score。这个分数不是魔法,是算法:ES 默认用 BM25 给每条命中文档打分。想干预排序,先得看懂它在算什么。
BM25 的三个因素,说人话版
BM25 公式长得吓人,但拆开就三个直觉:
第一,词频 TF,但会饱和。「燕麦」在文档里出现 3 次比出现 1 次分高,但第 3 次的增益远不如第 2 次——出现得越多越不稀奇,分数增长越来越钝。控制饱和度的旋钮叫 k1。这个设计天然反作弊:把关键词在页脚堆一百遍,也涨不了多少分。
第二,逆文档频率 IDF,越稀有越值钱。「燕麦」在十万件商品里只有三百件含它,信息量大,分高;「的」「大杯」哪儿都有,信息量为零,分低。搜「燕麦 大杯」,命中的文档主要靠「燕麦」拉开分差。
第三,字段长度归一,短字段命中更值钱。同样命中「拿铁」,在 8 个字的商品名里命中,比在 800 字的商品详情里命中更说明问题。控制强度的是 b 参数。这就是「燕麦酥」能反超的原因:它的名字短,「燕麦」两个字在标题里占比高,长度归一把它抬了上去。
用 explain 亲眼看打分
与其背公式,不如让 ES 把计算过程摊开。加一个 explain 参数,或者用 explain 接口:
GET /product/_explain/1024
{
"query": { "match": { "name": "燕麦" } }
}
返回的 description 里能看到:
score = tf(频率饱和) x idf(稀有度) x 字段长度归一
每一步都有数值,谁抬的分谁拖的分一目了然排查「为什么它排前面」的问题,explain 是第一工具。老王看完就懂了:燕麦酥赢在字段短,不是算法针对爆款。
调 k1 和 b?不如交给 function_score
第一反应是调 BM25 参数让爆款上来,先劝退:k1、b 是全局旋钮,牵一发动全身,调完所有查询的排序都变,属于玄学调参。业务诉求(销量高的靠前、新品加权)不该塞进相关性算法,该用 function_score,把相关性分和业务分明明白白合在一起:
GET /product/_search
{
"query": {
"function_score": {
"query": { "match": { "name": "燕麦" } },
"functions": [
{
"field_value_factor": {
"field": "sales", "modifier": "log1p", "factor": 1
}
},
{
"filter": { "term": { "tags.keyword": "新品" } },
"weight": 2
}
],
"score_mode": "sum",
"boost_mode": "multiply"
}
}
}逐块解释:functions 里每个函数算出一个业务分。field_value_factor 拿销量字段开方——用 log1p 修正是关键,直接拿原始销量当分,月销一万的商品分数会爆炸,一立方把 10000 变成约 4.6,曲线立刻温和。score_mode 说的是多个业务分怎么合(sum 相加、multiply 相乘、max 取大),boost_mode 说业务分和相关性分怎么合(multiply 相乘是最常用组合:相关性为前提,业务分做放大器)。
还有两招常用招式:random_score 加随机扰动,让搜索页每次刷新略有变化,用户感觉商品丰富;衰减函数(gauss)能实现「离我越近越靠前」「越新越靠前」这类平滑衰减。运营想插广告位、想置顶单品,也都是在 function_score 里加 filter 加 weight,规则清晰可回滚。
小结
评分三句话:BM25 算的是词频饱和、稀有度加成、短字段红利;看不懂排序先 explain,让 ES 自己交代;业务加权别动 BM25 参数,function_score 的 filter 加 weight 干净利落。排序不满意,八成不是算法错,是业务规则还没写进去。
排序理顺了,搜索页还差最后一块拼图:结果里关键词不高亮、用户打错字搜不到东西。下一篇讲进阶查询:高亮、拼写纠错与多字段搜索,把搜索页的用户体验一次配齐。
评论 (0)