在电商搜索场景中,排序是个核心问题。用户输入关键词后,商品怎么排?是按价格高低、销量多少,还是其他规则?今天咱们就围绕 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);
这里用 FieldValueFactorFunctionBuilder
把 saleNum
的值拿来算分,modifier(LOG1P)
表示用对数函数(log(1 + x))处理,factor(0.1f)
是调整影响力的权重。最终得分通过 FunctionScoreQuery
的 SUM
模式汇总。
为什么要这么干?举个例子就明白了。假设有三个商品:
- 商品 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,几乎没啥变化。这种"钝化"在新品销量刚起步时还行,但对成熟商品的排序就不够精准了。
优化方向:向主流方案靠拢
那咋整才能更靠谱?主流电商的排序方案通常综合多维度因素,咱们可以从这几个方向深挖,把排序做得更聪明:
-
多因子加权:销量不是唯一主角
销量只是个开始,价格、库存、评价、上架时间都可以加进来。可以用
FunctionScoreQuery
塞多个FilterFunctionBuilder
,比如:- 销量用
log1p
处理,权重设为 0.5,突出热销商品的影响。 - 评价分数(比如 4.5 分)直接乘以权重 0.3,反映用户口碑。
- 上架时间用衰减函数(
gauss
),权重 0.2,让新品有曝光机会。 最终得分是多维度的综合:总分 = 0.5 * log(1 + saleNum) + 0.3 * rating + 0.2 * gauss(time)
。这样,新品靠时间得分能冒头,老品靠销量和评价稳住排名,整体排序更平衡。
- 销量用
-
动态调整权重:因场景而变
别把权重写死,可以根据搜索场景动态调整。比如用户搜"新款手机",时间权重调到 0.4,销量降到 0.3;搜"经典款",销量权重提到 0.6,时间降到 0.1。这种灵活性需要业务逻辑支持,但能让结果更贴近用户意图。实现上,可以通过前端传参或者后端规则引擎来控制。
-
平滑新品问题:别让零销量太惨
销量 0 的商品太吃亏,可以给个"保底分"。比如把公式改成
log(1 + saleNum + 1)
,最低得分也有 log(2) ≈ 0.693,或者直接加个常数项(比如 0.5),让新品不至于直接归零。还可以结合库存状态,如果库存充足的新品多给点分,鼓励展示。 -
个性化排序:用户画像加持
主流电商会引入用户行为数据,比如某个用户常买高价商品,就把价格因子权重调高;偏好新品的用户,时间权重就占主导。ES 本身不支持实时个性化,但可以通过外部数据源(比如 Redis 存用户偏好)预计算,再注入算分逻辑。比如:
- 用户 A:价格权重 0.4,销量 0.3,时间 0.3
- 用户 B:时间权重 0.5,销量 0.3,价格 0.2 这样每个用户看到的排序都不一样,体验更贴心。
-
A/B 测试调参:数据驱动优化
权重咋定?别靠感觉,用 A/B 测试跑数据。比如试两组参数:
- 组 1:销量 0.5 + 时间 0.3 + 评价 0.2
- 组 2:销量 0.4 + 时间 0.4 + 评价 0.2 看哪组的点击率、转化率更高。ES 的算分很灵活,但效果好不好全靠数据说话。调参过程可能有点繁琐,但结果绝对值得。
-
引入机器学习:Learning to Rank
如果资源允许,可以上 ES 的 Learning to Rank(LTR)插件。它用机器学习模型(比如 XGBoost)训练排序规则,输入特征可以包括销量、评价、点击率等,输出一个综合得分。比手动调权重更科学,但需要准备训练数据和模型部署,复杂度一下子上去了。
总结:从简单到复杂的进化之路
从最朴素的销量排序,到引入 FunctionScoreQuery
的对数算分,再到多因子加权、个性化优化,甚至机器学习,排序这事儿越来越像一门"艺术"。简单方案上手快,但不够灵活;复杂方案效果好,却需要更多数据和调参支持。起步可以用销量加对数打底,后面逐步加入业务维度,让排序更聪明、更贴合用户期待。