十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
近期有一个需求,需要对优惠券可用商品列表加个排序,只针对面值类的券不包括折扣券。
成都创新互联公司服务项目包括民勤网站建设、民勤网站制作、民勤网页制作以及民勤网络营销策划等。多年来,我们专注于互联网行业,利用自身积累的技术优势、行业经验、深度合作伙伴关系等,向广大中小型企业、政府机构等提供互联网行业的解决方案,民勤网站推广取得了明显的社会效益与经济效益。目前,我们服务的客户以成都为中心已经辐射到民勤省份的部分城市,未来相信会继续扩大服务区域并继续获得客户的支持与信任!需求是这样的,假设有一张面值券 50 块钱,可用商品列表 A 100、B 40、C 10,当用户查询当前券可用商品列表的时候优先将卡券可以直接抵扣且不需要用户在额外支付的商品排在前面。
C 10
B 40
A 100
其实排序有很多侧重,比如:
1.根据用户利益大化原则,排序列表应该是 B、C、A
2.根据用户购买习惯,有可能是 A、B、C
3.根据运营策略、第三方利益等有可能是C、B、A
这里暂且先不扩展如何对商品列表进行智能排序,如果需要完整的个性化商品推荐,涉及很多东西,后面有经验在拿来分享。
我们就这个简单的 case,一开始最直接的想法就是加个排序列,建索引的时候将排序值计算好直接写入。后来分析了下原来索引(index) 结构不是这种笛卡尔积的排列,所以在短时间内很难立马上线,需要新建index结构。
后来通过讨论用影响评分的方法来解决,可以节省时间快速上线。
通过脚本改变评分ES query DSL支持很多种类型的查询,结果的排序如果没有特殊声明sort field则是根据es打分(score)来排序的,score分值越高排序越靠前。
ES score计算比较复杂,涉及到TF(词频)/IDF(逆向文档频率)、罕见词、匹配文档长度、权重 boost向量空间模型等,不过ES提供了几种封装好的评分插件供使用。
function_score查询来让我们根据业务场景改变文档评分方法,根据业务场景我们需要完全控制score生成的逻辑,所以我们选择script_score方式。
script_score
如果需求超出以上范围时,用自定义脚本可以完全控制评分计算,实现所需逻辑。
(参考:https://www.elastic.co/guide/cn/elasticsearch/guide/current/function-score-query.html)
脚本默认是groovy,当然也可以根据需要使用其他脚本语言,我们来看下实现。
script.inline: on
script.enfine.groovy.inline.aggs: on
script.indexed: on
script.file: on
首先在 es.yml 配置中打开脚本支持相关选项。
{
"query": {
"function_score": {
"query": {
"bool": {
"should": [
{
"match": {
"productName": "英语"
}
}
]
}
},
"score_mode": "first",
"script_score": {
"lang": "groovy",
"params": {
"couponPrice": 100
},
"script": "def deduct = couponPrice - doc['unitCost'].value.toFloat(); if (deduct > 0) {return 10000 + deduct;}else if(deduct==0 || (deduct<1 && deduct>0)){return 20000;}else{return doc['unitCost'].value.toFloat()-couponPrice;}"
},
"boost_mode": "replace"
}
},
"from": 0,
"size": 100
}
查询条件可以任意,关键是 __script_score对象,script是需要ES__ 脚本引擎执行的脚本代码。
一个比较重要的选项boost_mode,boost_mode是控制整个document的评分方式,这里我们选择替代(replace)默认计算好的评分。
这里面的排序有一个小技巧,如何将负数排序在前面,正数排序在后面,还有抵扣后是0的处理。
def deduct = couponPrice - doc['unitCost'].value.toFloat();
if (deduct > 0) {
return 10000 + deduct;
}else if(deduct==0 || (deduct<1 && deduct>0)){
return 20000;
}else{
return doc['unitCost'].value.toFloat()-couponPrice;
}
通过 couponPrice 变量表示优惠券面值金额,如果当前商品抵扣完是负数说明需要排序在前面,那么如何和抵扣完正数分开尼,这里可以取一个稍微大点的值加上抵扣后的负值,这样把负值转换成正数自然就排序在前面。
抵扣后等于0的或者小于1大于0的值也是可以优先安排在前面,当然这里还是不够灵活的,最好的方式是根据当前面值、商品价格动态计算才准确。
最后就是抵扣完需要用户在额外支付的排在最后面,直接取需要额外支付的金额数值作为排序。
通过 ES 评分我们能做很多事情,这个case只是一个简单的场景。
作者:王清培 (沪江集团资深架构师)