深入浅出聊聊 ElasticSearch 相关性算分在电商搜索场景应用:从简单销量排序到复杂优化


在电商搜索场景中,排序是个核心问题。用户输入关键词后,商品怎么排?是按价格高低、销量多少,还是其他规则?今天咱们就围绕 ElasticSearch(简称 ES)的相关性算分展开聊聊,尤其是在 Java 电商项目中常见的基于销量的 FunctionScoreQuery 排序。从最朴素的思路起步,一步步推到更复杂、更靠谱的方案,顺便看看有哪些坑和优化空间。


起点:最朴素的销量排序

假设刚开始设计搜索排序,最直观的点子是什么?当然是"销量高的排前面嘛!"。在 ES 里实现起来很简单,直接按字段排序:

java 复制代码
searchSourceBuilder.sort("saleNum", SortOrder.DESC);

这样,销量(saleNum)高的商品排前面,低的靠后。逻辑简单直接,用户一看也觉得有道理,毕竟"卖得多说明受欢迎"嘛。

但用着用着,问题就冒出来了。比如新上架的商品,销量是 0,按这规则直接沉底,连露脸的机会都没,这对新品太不公平,用户也看不到新鲜货,体验自然打折扣。再比如,销量 1000 和 990 的商品,差了 10 件,但排序上差别挺大,这公平吗?销量高的商品会不会天然垄断曝光?显然,这种朴素策略有点"用力过猛"。


进阶:引入相关性算分

ES 默认排序不是按字段,而是基于相关性算分(relevance score),用的是 BM25 算法。这算法是为文本搜索设计的,衡量查询关键词和文档内容的匹配度。比如搜索"手机",标题里"手机"出现多次的商品得分就高。但电商场景里,光靠文本匹配不够,用户更在乎销量、价格这些实际指标。

这时候,FunctionScoreQuery 就派上用场了。它能在基础查询(比如关键词匹配)上叠加自定义算分规则。看看下面这段代码:

java 复制代码
ScoreFunctionBuilder<FieldValueFactorFunctionBuilder> saleNumScoreFunction = 
    new FieldValueFactorFunctionBuilder("saleNum")
        .modifier(FieldValueFactorFunction.Modifier.LOG1P)
        .factor(0.1f);

这里用 FieldValueFactorFunctionBuildersaleNum 的值拿来算分,modifier(LOG1P) 表示用对数函数(log(1 + x))处理,factor(0.1f) 是调整影响力的权重。最终得分通过 FunctionScoreQuerySUM 模式汇总。

为什么要这么干?举个例子就明白了。假设有三个商品:

  • 商品 A:销量 1000
  • 商品 B:销量 100
  • 商品 C:销量 10

直接按销量排,A > B > C,没啥悬念。但用 log1p 处理后:

  • A 的得分:log(1 + 1000) ≈ 6.91
  • B 的得分:log(1 + 100) ≈ 4.62
  • C 的得分:log(1 + 10) ≈ 2.40

再乘以 factor(0.1),得分变成 0.691、0.462、0.240。差距明显缩小了!这避免了销量差别太大时高销量商品完全碾压低销量商品的情况,新品也有机会冒头。

不过,问题还没完全解决。销量 0 的商品得分是 log(1 + 0) = 0,还是没戏。而且,factor(0.1) 这值咋定的?拍脑袋吗?如果销量得分和关键词匹配度混在一起,比例怎么调?这就需要更细致的打磨了。


发现问题:朴素算分的局限

再挖挖这个方案的坑。假设基础查询是关键词"手机"的匹配得分,算出来分别是:

  • 商品 A:匹配度 2.0,销量 1000,总分 2.0 + 0.691 = 2.691
  • 商品 B:匹配度 1.5,销量 100,总分 1.5 + 0.462 = 1.962
  • 商品 C:匹配度 3.0,销量 10,总分 3.0 + 0.240 = 3.240

结果呢?C 排第一,A 第二,B 第三。咦?销量低的反而跑前面了?这说明直接把匹配度和销量得分相加,可能会让相关性高的低销量商品占便宜,高销量商品吃亏。用户想要的可能是"既相关又热销"的结果,而不是偏向某一边。

还有个麻烦,log1p 虽然压平了销量差距,但对小数值敏感度不够。销量从 0 到 10,得分涨了 2.40,但从 1000 到 1010,几乎没啥变化。这种"钝化"在新品销量刚起步时还行,但对成熟商品的排序就不够精准了。


优化方向:向主流方案靠拢

那咋整才能更靠谱?主流电商的排序方案通常综合多维度因素,咱们可以从这几个方向深挖,把排序做得更聪明:

  1. 多因子加权:销量不是唯一主角

    销量只是个开始,价格、库存、评价、上架时间都可以加进来。可以用 FunctionScoreQuery 塞多个 FilterFunctionBuilder,比如:

    • 销量用 log1p 处理,权重设为 0.5,突出热销商品的影响。
    • 评价分数(比如 4.5 分)直接乘以权重 0.3,反映用户口碑。
    • 上架时间用衰减函数(gauss),权重 0.2,让新品有曝光机会。 最终得分是多维度的综合:总分 = 0.5 * log(1 + saleNum) + 0.3 * rating + 0.2 * gauss(time)。这样,新品靠时间得分能冒头,老品靠销量和评价稳住排名,整体排序更平衡。
  2. 动态调整权重:因场景而变

    别把权重写死,可以根据搜索场景动态调整。比如用户搜"新款手机",时间权重调到 0.4,销量降到 0.3;搜"经典款",销量权重提到 0.6,时间降到 0.1。这种灵活性需要业务逻辑支持,但能让结果更贴近用户意图。实现上,可以通过前端传参或者后端规则引擎来控制。

  3. 平滑新品问题:别让零销量太惨

    销量 0 的商品太吃亏,可以给个"保底分"。比如把公式改成 log(1 + saleNum + 1),最低得分也有 log(2) ≈ 0.693,或者直接加个常数项(比如 0.5),让新品不至于直接归零。还可以结合库存状态,如果库存充足的新品多给点分,鼓励展示。

  4. 个性化排序:用户画像加持

    主流电商会引入用户行为数据,比如某个用户常买高价商品,就把价格因子权重调高;偏好新品的用户,时间权重就占主导。ES 本身不支持实时个性化,但可以通过外部数据源(比如 Redis 存用户偏好)预计算,再注入算分逻辑。比如:

    • 用户 A:价格权重 0.4,销量 0.3,时间 0.3
    • 用户 B:时间权重 0.5,销量 0.3,价格 0.2 这样每个用户看到的排序都不一样,体验更贴心。
  5. A/B 测试调参:数据驱动优化

    权重咋定?别靠感觉,用 A/B 测试跑数据。比如试两组参数:

    • 组 1:销量 0.5 + 时间 0.3 + 评价 0.2
    • 组 2:销量 0.4 + 时间 0.4 + 评价 0.2 看哪组的点击率、转化率更高。ES 的算分很灵活,但效果好不好全靠数据说话。调参过程可能有点繁琐,但结果绝对值得。
  6. 引入机器学习:Learning to Rank

    如果资源允许,可以上 ES 的 Learning to Rank(LTR)插件。它用机器学习模型(比如 XGBoost)训练排序规则,输入特征可以包括销量、评价、点击率等,输出一个综合得分。比手动调权重更科学,但需要准备训练数据和模型部署,复杂度一下子上去了。


总结:从简单到复杂的进化之路

从最朴素的销量排序,到引入 FunctionScoreQuery 的对数算分,再到多因子加权、个性化优化,甚至机器学习,排序这事儿越来越像一门"艺术"。简单方案上手快,但不够灵活;复杂方案效果好,却需要更多数据和调参支持。起步可以用销量加对数打底,后面逐步加入业务维度,让排序更聪明、更贴合用户期待。

相关推荐
梦醒沉醉12 分钟前
Scala的初步使用
开发语言·后端·scala
重庆穿山甲16 分钟前
建造者模式实战指南:场景案例+实战代码,新手也能快速上手
后端
小安同学iter38 分钟前
Spring(七)AOP-代理模式
java·后端·spring
Goboy1 小时前
老婆问我:“大模型的 Token 究竟是个啥?”
后端·程序员·架构
子洋2 小时前
Chroma+LangChain:让AI联网回答更精准
前端·人工智能·后端
追逐时光者2 小时前
基于 .NET Blazor 开源、低代码、易扩展的插件开发框架
后端·.net
MZWeiei5 小时前
Scala:解构声明(用例子通俗易懂)
开发语言·后端·scala
woniu_maggie8 小时前
SAP DOI EXCEL&宏的使用
后端·excel
二两小咸鱼儿9 小时前
Java Demo - JUnit :Unit Test(Assert Methods)
java·后端·junit
字节源流9 小时前
【spring】配置类和整合Junit
java·后端·spring