ElasticsearchNotes

第 27 章:相关性与排序优化

zjc 于 2026-01-27 发布

这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 相关性优化是搜索系统从“能搜到”走向“搜得好”的关键。它不只是调大某个字段权重,而是包含查询理解、召回、特征、排序、评估和持续迭代的完整过程。

27.1 什么是相关性

从用户视角看,相关结果应满足:

  1. 语义匹配:商品确实符合关键词;
  2. 意图匹配:理解品牌、型号、规格、用途;
  3. 可购买:有货、可售、价格合理;
  4. 可信:销量、评价、售后、商家服务;
  5. 可解释:排序符合用户直觉,异常结果能解释。

从系统视角看,排序由三部分组成:

相关性得分:BM25 / 向量相似度 / phrase / boost
业务得分:销量、转化、库存、信誉、时效
策略得分:置顶、活动、多样性、个性化

只优化 BM25 往往解决不了业务排序问题;只按销量排序又会牺牲文本相关性。

27.2 BM25 原理

BM25 是 Elasticsearch 默认文本评分算法。直观形式:

score =
  IDF(term)
  x TF(term in doc) / (TF + k1 * (1 - b + b * dl / avgdl))
  x boost

三个核心因素:

因素 含义 影响
IDF 词在集合中越罕见,区分度越高 罕见词贡献更高
TF 词在文档中出现次数越多,越相关 受饱和效应限制
文档长度 norm 文档越短,同样匹配越集中 长文档被适当惩罚

参数:

查看字段 similarity:

GET products-v3/_mapping/field/title

自定义 similarity:

PUT articles-v1
{
  "settings": {
    "index": {
      "similarity": {
        "default": {
          "type": "BM25",
          "k1": 1.2,
          "b": 0.75
        }
      }
    }
  }
}

除非有评测集证明有效,不建议凭感觉改 BM25 参数。

27.3 字段权重

不同字段的重要性不同。标题、品牌、类目、运营词、详情应有不同权重:

GET products-v3/_search
{
  "query": {
    "multi_match": {
      "query": "苹果手机",
      "type": "best_fields",
      "fields": [
        "title^4",
        "brand_name^3",
        "keywords^2",
        "description^1"
      ]
    }
  }
}

best_fields:取匹配效果最好的字段得分,适合“多个字段表达同一含义”。

most_fields:多个字段得分相加,适合字段互补,但容易让多字段弱匹配超过单字段强匹配。

cross_fields:把多个字段视作一个大字段,适合名、姓、地址拼接,但对中文复杂分词要谨慎。

类型 适合 风险
best_fields 标题优先的搜索 忽略多字段弱匹配
most_fields 多信号补充 重复加权
cross_fields 结构化姓名地址 依赖 analyzer 一致
phrase 精确顺序要求 召回变少,成本更高

27.4 Match 与 Phrase

match 不保证词序:

{ "match": { "title": "红色无线鼠标" } }

match_phrase 要求词项顺序和临近:

{ "match_phrase": { "title": "无线鼠标" } }

组合使用:

GET products-v3/_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "title": { "query": "无线鼠标", "boost": 1 } } },
        { "match_phrase": { "title": { "query": "无线鼠标", "boost": 3, "slop": 1 } } }
      ],
      "minimum_should_match": 1
    }
  }
}

这样能先保证召回,再让更精确的短语排序更靠前。

27.5 Function Score

Function Score 用于把业务因子加入相关性排序。

GET products-v3/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query": "笔记本",
          "fields": ["title^4", "keywords^2"]
        }
      },
      "functions": [
        {
          "filter": { "term": { "tags": "hot" } },
          "weight": 1.2
        },
        {
          "field_value_factor": {
            "field": "sales_30d",
            "modifier": "log1p",
            "factor": 0.5,
            "missing": 0
          }
        },
        {
          "gauss": {
            "on_shelf_at": {
              "origin": "now",
              "scale": "30d",
              "decay": 0.5
            }
          }
        },
        {
          "filter": { "term": { "shop_grade": "a" } },
          "weight": 1.1
        }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply",
      "max_boost": 10
    }
  }
}

常用函数:

函数 用途
weight 固定加权
field_value_factor 根据数值字段加分
decay 时间、距离、价格接近度衰减
random_score 实验分流或打散
script_score 自定义公式或模型打分

score_mode 决定多个函数之间怎么合并;boost_mode 决定函数结果和原查询得分怎么合并。上线前必须固定排序版本并记录特征值。

27.6 Script Score

当公式复杂时,可以使用 script score:

GET products-v3/_search
{
  "query": {
    "script_score": {
      "query": {
        "multi_match": {
          "query": "笔记本",
          "fields": ["title^4", "keywords^2"]
        }
      },
      "script": {
        "source": """
          double score = _score;
          double sales = doc['sales_30d'].size() == 0 ? 0 : doc['sales_30d'].value;
          double stock = doc['stock'].size() == 0 ? 0 : doc['stock'].value;
          score += Math.log1p(sales) * params.salesWeight;
          if (stock > 0) {
            score *= params.stockBoost;
          }
          return score;
        """,
        "params": {
          "salesWeight": 0.8,
          "stockBoost": 1.05
        }
      }
    }
  }
}

注意:

27.7 多路召回与 Rerank

复杂搜索常用多路召回:

query
  -> 文本召回 BM25
  -> 短语召回 phrase boost
  -> 向量召回 semantic kNN
  -> 运营/个性化召回
  -> merge by id
  -> first-stage rank
  -> rerank model

混合检索示例:

GET products-v3/_search
{
  "size": 50,
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "轻薄高续航笔记本" } }
      ],
      "should": [
        { "match_phrase": { "title": { "query": "轻薄高续航笔记本", "boost": 2 } } }
      ],
      "filter": [
        { "term": { "status": "on_sale" } }
      ]
    }
  },
  "knn": {
    "field": "embedding",
    "query_vector": [0.01, 0.02, 0.03],
    "k": 50,
    "num_candidates": 200
  }
}

不同版本对 query 与 kNN 的组合方式有差异,需要按当前版本验证。工程上更常见的做法是:

  1. 分别取 BM25 top N 和向量 top N;
  2. 应用层合并去重;
  3. 使用 RRF(Reciprocal Rank Fusion)或模型 rerank;
  4. 再叠加业务规则;
  5. 记录每个来源的分数和排名。

RRF 示例:

score(doc) = sum(1 / (k + rank_i(doc)))

其中 k 常取 60。RRF 不要求不同来源的分数可比,适合合并 BM25 和向量结果。

27.8 搜索评估

没有评估体系,相关性优化只能靠感觉。至少建立三层数据。

27.8.1 离线评测集

query 文档 相关等级 标注人
无线鼠标 doc_1 3 搜索组
无线鼠标 doc_2 2 业务组
无线鼠标 doc_3 0 搜索组

常用指标:

NDCG 关注高质量文档是否排在前面,适合搜索排序评估。

27.8.2 在线指标

27.8.3 分层指标

不同 query 应分开看:

类型 示例 重点
品牌词 iPhone 品牌、型号、正品
类目词 手机 类目、销量、多样性
属性词 轻薄 16G 属性召回
长尾词 送女生的礼物 意图、推荐
错别字 显视器 纠错、同义词

全站平均指标可能掩盖局部劣化,必须按 query 分层评估。

27.9 点击日志与反馈闭环

搜索日志建议记录:

PUT search_logs-v1/_doc/search_log_001
{
  "search_id": "search_log_001",
  "user_id": "u_1001",
  "query": "无线鼠标",
  "rewritten_query": "无线 鼠标",
  "result_ids": ["sku_1", "sku_2", "sku_3"],
  "result_scores": [12.3, 11.8, 10.1],
  "clicked_positions": [1],
  "search_version": "rank-v3.4",
  "experiment_bucket": "control",
  "latency_ms": 48,
  "result_count": 320,
  "created_at": "2026-08-25T10:00:00Z"
}

这些数据可以用于:

注意不要把点击当成绝对真理:首屏位置本身会带来点击偏差,需要用位置偏差修正或 A/B 实验验证。

27.10 排名稳定性与多样性

搜索结果不稳定会损害用户信任。常见原因:

建议:

多样性策略:

27.11 排序治理流程

一次相关性优化应经过:

发现 bad case
  -> 归因:召回 / 过滤 / 分词 / 排序
  -> 构造离线评测集
  -> 修改配置或模型
  -> 离线回归
  -> 小流量实验
  -> 在线指标验证
  -> 全量上线
  -> 记录版本与回滚方案

禁止只看一个 query 的截图就上线权重改动。搜索是全局系统,一个 case 的提升可能带来一批 query 劣化。

27.12 相关性优化清单

本章小结

相关性优化是“查询理解 + 召回 + 排序 + 评估”的闭环。BM25 和字段权重只是起点,业务因子、点击反馈、评测集和版本治理才决定搜索质量能否持续提升。所有排序改动都要可解释、可回放、可回滚。

思考题

  1. BM25 中 IDF、TF 和文档长度分别如何影响得分?
  2. best_fieldsmost_fields 分别适合什么场景?
  3. 为什么排序字段需要唯一 tie-breaker?
  4. BM25 分数和向量相似度为什么不能直接相加?
  5. 如果一个 query 效果变差,你会如何定位是分词、召回还是排序问题?