「Query DSL 与相关性打分」

讲解 ES Query DSL:叶子与复合查询、match 与 bool 组合、BM25 相关性与 function_score 打分,以及排序分页、search_after 与 explain。

Query DSL 是 Elasticsearch 的检索语言。写对了查询语法只解决能否搜到,而结果按什么顺序排列取决于相关性打分。本文覆盖叶子查询、复合查询、BM25 算法与 function_score,帮助你写出召回准确、排序合理、性能可控的检索语句。

1. Query DSL 基础

1.1 叶子查询与复合查询

Query DSL 分为两类:叶子查询直接匹配具体字段,如 term、match、range、exists;复合查询包装多个子查询,如 bool、dis_max、boosting。

查询类型名称作用打分
叶子term精确词项匹配固定分
叶子match全文分词匹配BM25
叶子range数值/日期范围固定分
叶子exists字段存在性固定分
复合bool组合 must/should/filter子查询求和
复合dis_max取最高分子查询最大值

1.2 match_all 与 range

{ "query": { "match_all": {} } }

match_all 给所有文档固定分数 1.0,常用于导出全量数据。不带 query 的 _search 默认也是 match_all,会造成全分片扫描,大数据集应避免。range 在数值与日期字段上走 BKD 树快速定位,配合 exists 实现字段非空过滤,等价于 SQL 的 IS NOT NULL。

{
  "query": {
    "bool": {
      "filter": [
        { "range":  { "price": { "gte": 100, "lte": 1000 } } },
        { "exists": { "field": "stock" } }
      ]
    }
  }
}

2. match 查询与全文检索

2.1 match 的分词与 minimum_should_match

match 默认把查询串分词后按 OR 合并,任一词命中即返回,分数为各词之和。用 operator 为 and 或 minimum_should_match 可控制最少命中词数。

查询串minimum_should_match行为
elasticsearch tutorial guide1至少命中任意 1 词
elasticsearch tutorial guide2至少命中任意 2 词
elasticsearch tutorial guide75%至少命中 3 × 75% = 2 词
elasticsearch tutorial guide-1允许最多 1 个词不命中
{
  "query": {
    "match": {
      "content": {
        "query": "分布式搜索引擎架构设计",
        "minimum_should_match": "75%"
      }
    }
  }
}

百分比按向下取整计算,是控召回精度的利器;但百分比与词数耦合,词数变化时行为不稳定,线上建议用绝对数字或压测确定的百分比。

2.2 match_phrase 与 slop

{
  "query": {
    "match_phrase": {
      "title": {
        "query": "搜索引擎 架构",
        "slop": 2
      }
    }
  }
}

match_phrase 要求词按顺序出现,slop 允许中间插入其他词的数量。短语查询依赖倒排索引中的 Position 信息,代价略高于普通 match。

2.3 multi_match 跨字段检索

{
  "query": {
    "multi_match": {
      "query": "Elasticsearch",
      "fields": ["title", "content", "tags"],
      "type": "best_fields"
    }
  }
}

multi_match 把同一查询分发到多个字段,best_fields 取最高分、most_fields 求和。常见坑是字段长度不同导致分数失衡,正文很长的文档词频被稀释,可用 field_value_factor 或提升权重字段修正。

3. bool 复合查询

3.1 must should must_not filter

bool 查询是 DSL 的核心组合器,四个子句各司其职。

{
  "query": {
    "bool": {
      "must":     [ { "match": { "title": "搜索引擎" } } ],
      "should":   [ { "match": { "tags": "分布式" } },
                    { "match": { "tags": "高可用" } } ],
      "must_not": [ { "term":  { "status": "deleted" } } ],
      "filter":   [ { "range": { "publish_date": { "gte": "2025-01-01" } } } ]
    }
  }
}
子句语义参与打分结果集缓存
must必须命中是否
should加分条件是否
must_not必须排除否否
filter必须命中否是

filter 子句不参与打分,其命中集合被缓存为位图,重复使用同一 filter 几乎零开销。纯过滤条件应放 filter 而非 must,这是查询性能优化最廉价的一步。

3.2 should 与 minimum_should_match

只有 should 没有 must/filter 时默认至少命中一个 should;当 must 存在时 should 变纯加分项,需显式设置 minimum_should_match 才强制生效。

{
  "query": {
    "bool": {
      "must": [ { "match": { "title": "数据库" } } ],
      "should": [
        { "match": { "tags": "MySQL" } },
        { "match": { "tags": "PostgreSQL" } },
        { "match": { "tags": "TiDB" } }
      ],
      "minimum_should_match": 2
    }
  }
}

该查询要求标题含数据库且标签至少命中三个中的两个,适合类目商品的属性组合筛选。

3.3 boosting 负向加权

{
  "query": {
    "boosting": {
      "positive":      { "match": { "title": "搜索引擎" } },
      "negative":      { "match": { "content": "营销" } },
      "negative_boost": 0.3
    }
  }
}

boosting 保留正查询命中文档,对命中负查询的乘以 negative_boost 压低分数,用于弱化而非完全排除某类结果。

4. BM25 相关性算法

4.1 BM25 公式

Lucene 旧版默认 TF-IDF,ES 5 之后切换到 BM25。BM25 对词频做饱和处理,词频再高对分数的贡献也趋于平稳,避免重复词无限放大分数。

score(d, q) = Σ IDF(qi) × (f(qi, d) × (k1 + 1))
              / (f(qi, d) + k1 × (1 - b + b × |d| / avgdl))

k1   控制词频饱和速度,默认 1.2
b    控制文档长度归一化强度,默认 0.75
f    词在文档中的频率
avgdl 字段平均长度

4.2 参数调节与高频词

{
  "settings": {
    "index": {
      "similarity": {
        "default": { "type": "BM25", "k1": 1.4, "b": 0.9 }
      }
    }
  }
}

k1 越大词频影响越大,适合型号类重复词有意义的场景;b 越大长度归一化越强,适合短文本优先。一般不需大改默认值,先用 explain 分析打分再微调。BM25 的 IDF 会让高频词(的、了、the)分数趋近零甚至为负,因此中文检索要过滤停用词,详见《倒排索引与分词原理》。

5. function_score 自定义打分

5.1 field_value_factor 字段加权

{
  "query": {
    "function_score": {
      "query": { "match": { "title": "手机" } },
      "field_value_factor": {
        "field": "sales",
        "factor": 1.0,
        "modifier": "log1p"
      },
      "boost_mode": "multiply"
    }
  }
}

field_value_factor 把字段值(销量、评分、点击量)变换后乘入分数,modifier 可选 log1p、sqrt、ln 等防数值悬殊导致分数爆炸。

5.2 gauss 时间衰减与 script_score

{
  "query": {
    "function_score": {
      "query": { "match": { "category": "electronics" } },
      "functions": [
        {
          "gauss": {
            "publish_date": {
              "origin": "2026-09-30",
              "scale": "30d",
              "decay": 0.5
            }
          }
        }
      ],
      "boost_mode": "multiply"
    }
  }
}

gauss 对日期字段做衰减,距离 origin 越远分数越低,适合新闻流、招聘等需要新鲜度加权的场景。script_score 用 Painless 完全接管打分,灵活但每次匹配都执行,性能敏感,应优先用 field_value_factor 或预计算字段。

场景推荐函数原因
按销量/热度加权field_value_factor简单可缓存
按时间新鲜度gauss平滑衰减
按距离排序decay(geo)地理衰减
复杂业务规则script_score完全可控但慢
类目固定加权boost常数乘子

6. 排序与分页

6.1 sort 与 search_after

{
  "query": { "match": { "title": "搜索引擎" } },
  "sort": [
    { "publish_date": "desc" },
    { "id": "asc" }
  ],
  "search_after": [1287321],
  "size": 10
}

默认按 _score 降序;指定 sort 字段后需显式加入 _score 才参与排序。sort 依赖 Doc Values,未启用的 text 字段无法排序,必须用 keyword 子字段。search_after 用上一页最后一条排序值作游标,翻页越深性能越稳,是深分页的标准方案。

6.2 分页方式对比

方式翻页性能一致性适用场景
from + size深翻页退化无保证前 100 条
search_after稳定追加式实时流式加载
scroll稳定快照全量导出
PIT稳定快照复合场景

scroll 在快照视图上滚动,适合全量导出但维护上下文内存;PIT(Point In Time)是 ES 7.10 引入的轻量级替代,与 search_after 配合做增量同步。

7. 高亮与 explain 调试

7.1 highlight 高亮

{
  "query": { "match": { "content": "分布式" } },
  "highlight": {
    "fields": { "content": { "fragment_size": 150, "number_of_fragments": 2 } },
    "pre_tags": ["<em>"],
    "post_tags": ["</em>"]
  }
}

高亮基于分词后的词项定位原文位置,_source 片段被拆成 fragments。中文高亮注意 fragment_size 按字符而非词,过小会切碎语义。

7.2 explain 与 profile

curl -s 'http://localhost:9200/article/_explain/42?pretty' \
  -H 'Content-Type: application/json' \
  -d '{"query": {"match": {"title": "搜索引擎"}}}'

_explain 返回某文档的逐项打分明细,包括 IDF、词频、长度归一化,是排查排序异常的必备工具。profile 则返回每个查询阶段(TermQuery、PhraseQuery、Collector)的耗时占比,定位查询慢在解压 Posting List、collect 还是 fetch,与 explain 搭配分别解决排序正确性与查询性能。

8. 总结

环节要点
查询分类叶子查询匹配字段,复合查询组合子句
match 语义先分词后合并,operator 与 minimum_should_match 控召回
bool 结构must/should/filter 各司其职,filter 走位图缓存
BM25k1 控词频饱和,b 控长度归一化
自定义打分field_value_factor、gauss、script_score 按场景选择
深分页search_after 游标优于 from,scroll 适合离线导出
调试工具explain 看打分明细,profile 看耗时分布
性能红线避免深 from 分页、避免 script 打全量

Query DSL 决定检索能力,相关性打分决定检索质量。写查询前先想清楚业务排序目标,把过滤条件放进 filter,用 explain 验证打分,再决定是否引入 function_score。与倒排索引底层的结合可阅读《倒排索引与分词原理》,索引结构与字段设计可阅读《数据建模与 Mapping 设计》。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 「搜索服务架构:从索引到容错」
  2. 「安全加固与访问控制:从角色到审计」
  3. 「地理空间搜索:从坐标到地图」