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

相关推荐
明月与玄武1 小时前
Spring Boot中的拦截器!
java·spring boot·后端
菲兹园长1 小时前
SpringBoot统一功能处理
java·spring boot·后端
muxue1781 小时前
go语言封装、继承与多态:
开发语言·后端·golang
开心码农1号2 小时前
Go语言中 源文件开头的 // +build 注释的用法
开发语言·后端·golang
北极象2 小时前
Go主要里程碑版本及其新增特性
开发语言·后端·golang
lyrhhhhhhhh2 小时前
Spring框架(1)
java·后端·spring
喝养乐多长不高4 小时前
Spring Web MVC基础理论和使用
java·前端·后端·spring·mvc·springmvc
莫轻言舞4 小时前
SpringBoot整合PDF导出功能
spring boot·后端·pdf
玄武后端技术栈5 小时前
什么是死信队列?死信队列是如何导致的?
后端·rabbitmq·死信队列
老兵发新帖6 小时前
NestJS 框架深度解析
后端·node.js