在搜索业务场景中,经常需要根据特定字段调整排序评分,从而实现目标结果前置(置顶)或非目标结果后置(沉底)的需求。以商机管理的"扫街拓客"场景为例,当寻找目标商户时,核心筛选维度包括:距离远近、GMV 潜力大小、被跟进次数多少。从策略设计来看,距离远近的权重应高于 GMV 潜力------优先选择就近可拜访的商户;在距离相近的前提下,再优先筛选 GMV 潜力更大的商户。
进一步细化排序规则:若商户属于"激励商户"且转化意愿高,需将其排序前置(置顶),提升被优先开发的概率;若商户已被多次重复开发,开发成本会显著增加,被跟进次数越多则应越靠后排序,即实现沉底效果。本文将系统介绍 Elasticsearch(以下简称 ES)支持的各类排序能力,并结合具体场景说明其适用范围与实践方式。
一、基础权重调整:Boost 参数提升查询优先级
在 ES 查询中,通过 Boost 参数可直接调整不同查询语句的权重,让核心查询条件比其他条件更"重要"。这种方式适用于简单排序需求,具有改动成本低、调整速度快的优势。需注意的是:所有类型的查询均支持 Boost 参数,其作用是作为 _score(相关性评分)的影响因子,而非简单的倍数放大------例如将 Boost 设为 2,不代表最终 _score 会变为原来的 2 倍。当未显式设置 Boost 时,默认值为 1。
GET /_search
{
"query": {
"bool": {
"should": [
{
"match": {
"title": {
"query": "elastic search",
"boost": 2 // 提升title字段匹配的权重
}
}
},
{
"match": {
"content": "elastic search" // 默认boost=1
}
}
]
}
}
}
二、相关性精准控制:组合查询优化多条件权重占比
当查询需求包含多组并列条件时,需通过组合查询调整各条件的权重占比,避免单一条件权重被稀释。以下是典型场景与优化方案:
1. 场景定义
查询包含 "Elasticsearch" 或("Golang" 或 "Go")或 "function_score" 的文档。
2. 初始方案(存在缺陷)
GET /_search
{
"query": {
"bool": {
"should": [
{"term": {"content": "elasticsearch"}},
{"term": {"content": "Golang"}},
{"term": {"content": "Go"}},
{"term": {"content": "function_score"}}
]
}
}
}
该方案虽能查询出符合条件的所有文档,但存在核心问题:4 个条件权重完全均等(各占 25%),而实际需求中 "Golang 或 Go" 应视为一组组合条件,与 "Elasticsearch""function_score" 处于同等并列层级。
3. 优化方案(嵌套 Bool 查询)
通过嵌套 Bool 查询将 "Golang 或 Go" 封装为一个子查询,使其与其他两个条件形成顶层并列关系。优化后权重占比调整为:"Elasticsearch""function_score""Golang 或 Go" 各占 33.3%,更符合实际业务逻辑。
GET /_search
{
"query": {
"bool": {
"should": [
{"term": {"content": "elasticsearch"}},
{"term": {"content": "function_score"}},
{
"bool": { // 嵌套Bool查询封装组合条件
"should": [
{"term": {"content": "Golang"}},
{"term": {"content": "Go"}}
]
}
}
]
}
}
}
三、实现排序沉底:Boosting 查询弱化非目标结果
当需要保留符合核心条件但包含非目标元素的文档,同时降低其排序优先级(实现沉底)时,可使用 Boosting 查询。以下结合具体场景对比两种方案:
1. 场景定义
查询包含 "Elasticsearch" 的文档,但不希望包含 "MySQL" 的文档排序过前(需沉底,而非直接排除)。
2. 方案一:使用 must_not(过于严格)
GET /_search
{
"query": {
"bool": {
"must": {
"match": {"content": "elasticsearch"} // 核心匹配条件
},
"must_not": {
"match": {"content": "mysql"} // 直接排除包含MySQL的文档
}
}
}
}
该方案的问题在于:若文档大量包含 "Elasticsearch" 但仅少量提及 "MySQL",会被直接排除,错失有价值的文档。
3. 方案二:Boosting 查询(柔性弱化)
Boosting 查询通过"正向查询(positive)"定义核心匹配条件,"负向查询(negative)"定义需要弱化的条件,通过 negative_boost 参数(0~1 之间的数值)控制弱化权重。最终评分公式为:new_score = 正向查询评分 × negative_boost。通过该方式,包含 "MySQL" 的文档仍会被保留,但评分被降低,实现排序沉底。
GET /_search
{
"query": {
"boosting": {
"positive": {
"match": {"content": "elasticsearch"} // 核心匹配条件
},
"negative": {
"match": {"content": "mysql"} // 需要弱化的条件
},
"negative_boost": 0.8 // 弱化权重,评分变为原来的80%
}
}
}
四、统一评分标准:Constant_score 查询忽略 TF/IDF 影响
部分场景下,用户不关心文档中关键词的出现频率(即 TF/IDF 权重),仅关注是否包含目标条件,且希望包含的条件越多评分越高。例如:出差选择酒店时,优先考虑包含 "WIFI""健身房(Gym)""早餐(Breakfast)" 等设施的酒店,不关心这些词在描述中出现的次数。
1. 方案一:普通 Match 查询(不适用)
GET /_search
{
"query": {
"match": {
"hotel_desc": "WIFI Gym Breakfast" // 依赖TF/IDF,受出现频率影响
}
}
}
普通 Match 查询仍会基于 TF/IDF 计算评分,关键词出现次数越多评分越高,不符合"仅关注是否存在"的需求。
2. 方案二:Constant_score 查询(统一评分)
Constant_score 查询为每个匹配的条件赋予固定评分(默认 1 分),匹配的条件越多,总评分越高,且不受关键词出现频率影响。若不同条件的重要性不同,可结合 Boost 参数调整权重。
GET /_search
{
"query": {
"bool": {
"should": [
{
"constant_score": {
"query": {"match": {"hotel_desc": "WIFI"}} // 匹配得1分
}
},
{
"constant_score": {
"query": {"match": {"hotel_desc": "Gym"}},
"boost": 2 // 提升权重,匹配得2分
}
},
{
"constant_score": {
"query": {"match": {"hotel_desc": "Breakfast"}} // 匹配得1分
}
}
]
}
}
}
3. Constant_score 的扩展应用:结合 Filter 查询
在 Boosting 查询中,若正向查询使用 Filter(过滤)查询,由于 Filter 仅筛选结果不计算评分,无法直接通过 negative_boost 弱化评分。此时可通过 Constant_score 封装 Filter 查询,既保留 Filter 的缓存优势(提升查询速度),又能生成基础评分供后续计算。
GET /_search
{
"query": {
"boosting": {
"positive": {
"constant_score": {
"filter": {"term": {"price": 50}} // 过滤价格为50的文档,生成固定评分
}
},
"negative": {
"match": {"content": "mysql"} // 弱化包含MySQL的文档
},
"negative_boost": 0.8
}
}
}
五、复杂场景排序:Function_score 自定义评分逻辑
Function_score 查询是 ES 中最灵活的排序工具,适用于多维度因子(含正向、负向)综合排序的场景。例如:结合"投票数(正向因子)"和"发布时间(负向因子)"排序------投票数越高越靠前,发布时间越早越靠后。其核心能力通过多种函数实现,具体如下:
1. 核心函数说明
-
Weight:为文档赋予固定权重,不进行规范化处理,最终评分 = 原始评分 × Weight 值(如 Weight=2 则评分翻倍)。
-
Field_value_factor:基于文档字段值修改评分,适用于将"热度""销量"等量化指标纳入排序维度。
-
Random_score:为不同用户生成个性化随机排序,但对同一用户而言,排序结果始终一致。
-
衰减函数(Linear/Exp/Gauss):将数值、时间、经纬度等连续型数据纳入评分,实现"越接近目标值评分越高"的效果。
-
Script_score:自定义脚本实现复杂评分逻辑,适用于超出内置函数能力的场景。
2. 实践案例:结合热度调整 POI 排序
场景:POI 搜索(如"烧烤店"),需将抖音搜索热度(douyin_hot 字段)高的结果前置,同时以 POI 名称匹配度作为核心排序依据。
(1)初始方案:直接使用 Field_value_factor
GET /_search
{
"query": {
"function_score": {
"query": {
"match": {
"query": "烧烤",
"fields": "poi_name" // 核心匹配条件:POI名称
}
},
"field_value_factor": {
"field": "douyin_hot", // 纳入热度因子
"missing": 0 // 字段缺失时默认值为0
}
}
}
}
问题:douyin_hot 字段值通常较大(如数万),会完全覆盖 POI 名称匹配度的评分,导致核心排序依据失效。
(2)优化方案一:使用 Modifier 平滑热度值
通过 Modifier 函数对热度值进行平滑处理,降低极端值的影响。例如使用 "log1p" 函数(公式:new_score = 原始评分 × log(1 + douyin_hot)),使热度值的差异更合理------热度 0 与 1 的差异远大于热度 100 与 101 的差异。
GET /_search
{
"query": {
"function_score": {
"query": {
"match": {
"query": "烧烤",
"fields": "poi_name"
}
},
"field_value_factor": {
"field": "douyin_hot",
"missing": 0,
"modifier": "log1p" // 平滑热度值
}
}
}
}
Modifier 可选值:none(默认)、log、log1p、log2p、ln、ln1p、ln2p、square、sqrt、reciprocal。
(3)优化方案二:Factor 调节平滑力度
若平滑后的热度值对评分影响仍不足,可通过 Factor 参数增强调节力度。公式变为:new_score = 原始评分 × log(1 + Factor × douyin_hot),Factor 为正数,可放大热度值的影响。
GET /_search
{
"query": {
"function_score": {
"query": {
"match": {
"query": "烧烤",
"fields": "poi_name"
}
},
"field_value_factor": {
"field": "douyin_hot",
"missing": 0,
"modifier": "log1p",
"factor": 2 // 增强热度值的影响力度
}
}
}
}
(4)优化方案三:Boost_mode 控制评分结合方式
默认情况下,Function_score 采用"乘积(multiply)"方式结合原始评分与函数值,易放大极端值。可通过 Boost_mode 调整结合方式,常用"求和(sum)"实现更均衡的评分。
Boost_mode 可选值:
-
multiply(默认):new_score = 原始评分 × 函数值
-
sum:new_score = 原始评分 + 函数值(推荐,评分更平滑)
-
min:取两者较小值
-
max:取两者较大值
-
replace:用函数值替代原始评分
GET /_search
{
"query": {
"function_score": {
"query": {
"match": {
"query": "烧烤",
"fields": "poi_name"
}
},
"field_value_factor": {
"field": "douyin_hot",
"missing": 0,
"modifier": "log1p",
"factor": 2
},
"boost_mode": "sum" // 求和方式结合评分
}
}
}
5)优化方案四:Max_boost 限制最大权重
通过 Max_boost 参数设置函数值的上限,避免辅助因子(如热度)的评分过高,完全压制核心评分(如名称匹配度)。
GET /_search
{
"query": {
"function_score": {
"query": {
"match": {
"query": "烧烤",
"fields": "poi_name"
}
},
"field_value_factor": {
"field": "douyin_hot",
"missing": 0,
"modifier": "log1p",
"factor": 2
},
"boost_mode": "sum",
"max_boost": 1.5 // 函数值最大不超过1.5
}
}
}
3. 衰减函数:实现"越接近目标越优"的排序
场景:酒店选择时,需综合考虑"距离景区远近"和"价格高低",优先选择距离近、价格接近心理预期(如 200 元)的酒店。此时可使用衰减函数(以 Gauss 为例)将连续型数据纳入评分,实现多维度均衡排序。
衰减函数核心参数:
-
origin:最佳值(中心点),落在该点的文档评分=1.0。
-
offset:偏移量,以 origin 为中心的数值范围,此范围内评分均为 1.0。
-
scale:衰减率,文档值超出 [origin-offset, origin+offset] 范围后,评分下降的速度。
-
decay:衰减到 scale 位置时的评分(默认 0.5)。
GET /_search
{
"query": {
"function_score": {
"functions": [
{
"gauss": { // 距离维度:越接近目标经纬度评分越高
"location": {
"origin": {"lat": 10.1, "lon": 0.11}, // 景区经纬度
"offset": "1km", // 1km范围内评分=1.0
"scale": "2km" // 超出1km后,每远离2km评分衰减到0.5
}
}
},
{
"gauss": { // 价格维度:越接近200元评分越高
"price": {
"origin": "200", // 心理预期价格
"offset": "50", // 150~250元范围内评分=1.0
"scale": "30" // 超出范围后,每偏离30元评分衰减到0.5
}
},
"weight": 2 // 提升价格维度的权重
}
]
}
}
}
六、总结与实践建议
ES 提供了从基础(Boost)到复杂(Function_score)的全链路排序能力,可根据业务场景的复杂度选择合适的方案:简单权重调整用 Boost,多条件权重均衡用组合查询,排序沉底用 Boosting 查询,忽略词频的均等评分用 Constant_score,复杂多维度排序用 Function_score。
需要注意的是,实现置顶/沉底效果往往需要组合多种排序能力,而非单一方案。尽管 Function_score 灵活性最高,但查询复杂度越高,对 ES 性能的消耗越大。在数据量较大的生产环境中,需重点权衡排序效果与查询性能:建议上线前在仿真环境中进行大数据量压测,验证性能表现;必要时可通过预处理数据(如缓存热点结果、预计算评分)降低查询压力。