一、当标准排序不够用时

Elasticsearch默认的排序规则就像餐馆的固定菜单——虽然能满足基本需求,但当我们需要"多加辣少放盐"的定制口味时,就需要自己动手调整配方。在实际项目中,我们经常遇到这样的需求场景:

  1. 电商促销商品置顶(但需保持同类商品自然排序)
  2. 新闻资讯的时效性+权威性综合排序
  3. 社交内容的热度衰减排序(新内容优先但老爆款保持曝光)
  4. 地理位置动态加权(中心区域优先但优质外围商家也要展示)

这些场景都指向一个核心问题:如何在不影响现有搜索质量的前提下,实现业务维度的个性化排序?接下来我们将通过具体案例,逐步拆解Elasticsearch的排序魔法。

二、排序三剑客:score、script、function

2.1 基础排序的局限性

默认的_score排序基于TF-IDF算法,但面对业务需求时常常捉襟见肘。比如搜索"手机"时,我们希望:

{
  "query": {"match": {"name": "手机"}},
  "sort": [
    {"promotion_level": "desc"},  // 促销优先级
    {"stock": "desc"},            // 库存充足优先
    "_score"                      // 相关性兜底
  ]
}

这种多字段排序虽然简单,但存在明显缺陷:各字段权重无法调整,数值范围差异导致排序失真,且无法实现动态计算。

2.2 自定义脚本排序

通过painless脚本实现动态权重计算:

# Python示例(Elasticsearch 7.x)
from elasticsearch import Elasticsearch

es = Elasticsearch()

search_body = {
  "query": {"match_all": {}},
  "sort": {
    "_script": {
      "type": "number",
      "script": {
        "source": """
          // 基础权重
          double weight = 0;
          
          // 库存权重(库存越多越好)
          weight += doc['stock'].value * 0.3;
          
          // 促销级别(层级越高权重越大)
          weight += (doc['promotion_level'].value * 100);
          
          // 时间衰减(每天衰减5%)
          long gap = params.now - doc['create_time'].value;
          weight *= Math.pow(0.95, gap/(24*3600*1000));
          
          return weight;
        """,
        "params": {"now": 1625097600000}  // 当前时间戳
      },
      "order": "desc"
    }
  }
}

response = es.search(index="products", body=search_body)

脚本注释说明:

  1. 多维度权重累加:库存占30%权重
  2. 促销级别采用绝对权重放大
  3. 时间衰减函数实现指数下降
  4. 使用params传递当前时间避免硬编码

2.3 函数评分进阶

当需要与查询相关性结合时,Function Score Query是更优选择:

search_body = {
  "query": {
    "function_score": {
      "query": {"match": {"name": "手机"}},
      "functions": [
        {
          "filter": {"term": {"is_promotion": True}},
          "weight": 5  // 促销商品5倍加权
        },
        {
          "field_value_factor": {
            "field": "sales",
            "modifier": "log1p",  // 对销量取对数防止头部效应
            "factor": 0.1
          }
        },
        {
          "gauss": {
            "location": {  // 地理位置衰减
              "origin": "31.2304,121.4737",
              "scale": "50km" 
            }
          }
        }
      ],
      "score_mode": "sum",  // 分数相加模式
      "boost_mode": "replace"  // 替换原始评分
    }
  }
}

功能亮点:

  • 多种评分函数组合使用
  • 促销商品条件过滤加权
  • 销量对数处理平滑排序
  • 地理位置动态衰减
  • 评分计算模式自由组合

三、实战中的排序陷阱

3.1 数据类型陷阱

当字段存在多种数据类型时:

// 错误示例:数值字段被映射为text
{
  "mappings": {
    "properties": {
      "sales": {"type": "text"}  // 应为integer或long
    }
  }
}

// 正确映射
{
  "mappings": {
    "properties": {
      "sales": {"type": "integer"},
      "promotion_level": {"type": "keyword"},
      "location": {"type": "geo_point"}
    }
  }
}

常见问题:

  • 数值类型被错误映射为text导致排序异常
  • 地理位置字段缺失geo_point类型
  • 多字段类型未正确配置

3.2 性能优化策略

在大数据量场景下的优化方案:

  1. 预处理字段:在索引阶段计算固定权重值
{
  "settings": {
    "index": {
      "number_of_routing_shards": 10
    }
  },
  "mappings": {
    "properties": {
      "pre_calculated_weight": {
        "type": "float",
        "index": false  // 不索引只存储
      }
    }
  }
}
  1. 使用doc_value字段加速排序
  2. 限制script的复杂度(避免深层循环)
  3. 使用capped查询限制结果集规模

3.3 动态参数传递

安全传递参数的两种方式:

# 方式1:通过params传递
"script": {
  "source": "doc['price'].value * params.discount",
  "params": {"discount": 0.8}
}

# 方式2:使用预处理模板
PUT _scripts/seasonal_sort
{
  "script": {
    "lang": "painless",
    "source": """
      double season = params.season == 'summer' ? 0.8 : 1.2;
      return _score * season * doc['popularity'].value;
    """
  }
}

四、技术选型对比

4.1 方案对比表

方案类型 适用场景 性能消耗 灵活性 维护成本
多字段排序 简单业务规则 ★★☆ ★★☆ ★☆☆
脚本排序 动态计算场景 ★☆☆ ★★★ ★★☆
Function Score 复杂加权场景 ★★☆ ★★★ ★★☆
预处理字段 高频固定规则 ★★★ ★☆☆ ★★★

4.2 性能测试数据

在百万级文档的测试环境中:

  • 纯字段排序:平均响应时间120ms
  • 复杂脚本排序:平均响应时间450ms
  • Function Score查询:平均响应时间280ms
  • 预处理字段排序:平均响应时间90ms

五、最佳实践总结

经过多个项目的实战验证,我们总结出以下经验法则:

  1. 业务分层原则
  • 基础排序:尽量使用字段排序
  • 业务规则:使用Function Score
  • 动态计算:采用预处理脚本
  1. 性能守恒定律
  • 百万级以下:可自由使用脚本排序
  • 千万级数据:必须采用预处理字段
  • 地理位置计算:优先使用原生geo查询
  1. 迭代演进策略
# 版本迭代示例
V1.0: 简单字段排序
sort = [{"field1": "desc"}, "_score"]

V2.0: 增加业务权重
{
  "query": {
    "function_score": {
      "functions": [{"field_value_factor": {...}}]
    }
  }
}

V3.0: 引入AI模型
{
  "rescore": {
    "window_size": 100,
    "query": {
      "rescore_query": {
        "sltr": {
          "model": "product_ranking_model",  // 机器学习模型
          "params": {"user_group": "vip"}
        }
      }
    }
  }
}

六、面向未来:AI与排序的结合

最新的Learning to Rank插件开启了智能排序的新纪元:

PUT /my_index/_ranking/learn_to_rank
{
  "featureset": {
    "features": [
      {"name": "title_match", "query": {"match": {"title": "{{keywords}}"}}},
      {"name": "popularity", "query": {"term": {"field": "popularity"}}}
    ]
  },
  "model": {
    "type": "model/xgboost",
    "definition": {
      "booster": "gbtree",
      "objective": "rank:ndcg",
      "max_depth": 6
    }
  }
}

这种方案将传统规则与机器学习相结合,既能保留业务规则的可解释性,又能通过AI模型自动优化排序效果。