上一篇解决了分数怎么算出来的——BM25 的三个因子决定了文档的相关性排序。这一篇进入 Query DSL 的查询类型体系和组合逻辑。

ES 的查询体系不是一个扁平的 API 列表。全文查询、精确查询、模糊查询、地理查询、复合查询分别对应不同的底层数据结构和匹配逻辑。理解这个分类,才能在具体场景中选对查询类型。

本文抓两个问题:ES 的查询类型怎样分类,以及 bool 查询怎样把不同类型的查询组合起来。

查询体系全景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ES 查询体系
├── 全文查询(走 analyzer,参与打分)
│ ├── match 分词后多词项匹配
│ ├── match_phrase 分词后要求位置相邻
│ ├── multi_match 跨多个字段的 match
│ └── match_phrase_prefix 短语前缀(输入补全)

├── 精确查询(不走 analyzer,通常用于 filter context)
│ ├── term / terms 精确词项匹配
│ ├── range 数值/日期范围
│ ├── exists 字段是否存在
│ ├── ids 按 _id 匹配
│ ├── prefix 前缀匹配
│ ├── wildcard 通配符匹配
│ └── regexp 正则匹配

├── 模糊查询(容错匹配)
│ ├── fuzzy 编辑距离容错
│ └── match + fuzziness match 查询内置容错

├── 地理查询(空间匹配)
│ ├── geo_distance 圆形范围
│ ├── geo_bounding_box 矩形范围
│ └── geo_shape 复杂形状

├── 复合查询(组合多个查询)
│ ├── bool must/filter/should/must_not
│ ├── boosting 正面匹配 + 降权匹配
│ └── dis_max 取最高分的子查询

└── 特殊查询
├── nested / has_child / has_parent
└── more_like_this

Query Context vs Filter Context

所有查询都在两种上下文之一中执行:

上下文 是否打分 是否缓存 典型位置
Query context 是(BM25) bool.must, bool.should
Filter context 否(只返回 0 或 1) 是(bitset 缓存) bool.filter, bool.must_not

同一个查询放在不同的上下文中,行为不同。一个 range 查询放在 must 里会参与打分,放在 filter 里只做过滤不打分但会被缓存。通常精确查询和范围查询放在 filter context 中性能更好——不需要计算分数,且结果可以被缓存复用。

flowchart LR
    Q["一条查询子句"]
    Q -->|放在 must / should| QC["Query Context<br/>BM25 打分<br/>不缓存"]
    Q -->|放在 filter / must_not| FC["Filter Context<br/>只返回 yes/no<br/>bitset 缓存"]
    QC --> R1["影响 _score 排序"]
    FC --> R2["只决定文档是否进入结果集"]

同一条 rangeterm 子句,放进 must 就会参与 BM25 计算,放进 filter 就变成纯布尔过滤并享受 bitset 缓存。选择上下文的判据是:这个条件需不需要影响结果排序。

全文查询

全文查询的共同特征:对查询文本先走 analyzer,产生词项,然后在倒排索引中匹配。

match 查询

match 是最常用的全文查询。它的内部流程:

1
2
3
4
5
查询文本 "distributed search"
→ analyzer 分词 → ["distributed", "search"]
→ 在倒排索引中查找两个词项
→ 默认 OR:文档包含任一词项就匹配
→ BM25 打分
1
2
3
4
5
6
7
8
GET /my-index/_search
{
"query": {
"match": {
"title": "distributed search"
}
}
}

默认行为是 OR——包含 “distributed” 或 “search” 的文档都会被返回。通过 operator 参数改为 AND:

1
{ "match": { "title": { "query": "distributed search", "operator": "and" } } }

这就是 match 被称为"模糊匹配"的原因——它匹配的是分析后的词项,不是原始字符串。“Distributed Search” 和 “search distributed” 和 “a distributed full-text search engine” 都能被 match: "distributed search" 匹配到。

match_phrase 查询

match_phrase 不仅要求词项匹配,还要求词项在文档中的位置相邻且顺序一致。这利用了倒排索引 Posting List 中存储的 positions 信息。

1
{ "match_phrase": { "title": "search engine" } }

“a search engine for documents” 能匹配(“search” 和 “engine” 相邻且顺序正确)。
“an engine for search” 不能匹配(顺序不对)。
“search and engine” 不能匹配(中间有其他词)。

slop 参数允许词项之间有间隔:

1
{ "match_phrase": { "title": { "query": "search engine", "slop": 1 } } }

slop: 1 允许词项之间最多间隔 1 个位置。“search and engine” 现在可以匹配了(“search” 在位置 0,“engine” 在位置 2,间隔 1)。

multi_match 查询

multi_match 在多个字段上执行 match 查询:

1
2
3
4
5
6
7
{
"multi_match": {
"query": "elasticsearch guide",
"fields": ["title^2", "content"],
"type": "best_fields"
}
}

title^2 表示 title 字段的分数乘以 2 的权重。type 控制多字段分数的组合方式:

type 行为
best_fields(默认) 取分数最高的字段
most_fields 所有字段分数加和
cross_fields 把多个字段当成一个字段处理

精确查询

精确查询不走 analyzer,直接用原始值在倒排索引或 BKD-tree 中匹配。

term 查询

term 查询把输入值当作精确的词项,直接在倒排索引中查找:

1
{ "term": { "status": "published" } }

term 查询不对输入做分析。这意味着对 text 字段使用 term 查询几乎总是错误的——text 字段在索引时被 analyzer 转成了小写词项,而 term 查询不转换输入。

1
2
3
字段 title (text) 存储的倒排索引词项:["elasticsearch", "guide"]
term 查询 "Elasticsearch Guide":在倒排索引中查找 "Elasticsearch Guide" → 不存在 → 无结果
match 查询 "Elasticsearch Guide":analyzer → ["elasticsearch", "guide"] → 匹配成功

这是 ES 中最常见的使用错误之一。term 查询应该用在 keyword 字段上。

terms 查询

termsterm 的多值版本,等价于 SQL 的 IN

1
{ "terms": { "status": ["published", "draft", "review"] } }

range 查询

range 对数值、日期和字符串字段做范围匹配。数值和日期字段底层使用 BKD-tree 索引,范围查询效率高:

1
2
{ "range": { "price": { "gte": 10, "lt": 100 } } }
{ "range": { "created_at": { "gte": "2026-01-01", "lt": "2026-07-01" } } }
参数 含义
gt 大于
gte 大于等于
lt 小于
lte 小于等于

日期 range 支持日期数学表达式:"gte": "now-7d" 表示最近 7 天。

exists 查询

判断字段是否存在(有值):

1
{ "exists": { "field": "description" } }

精确查询族总结

查询类型 匹配方式 适用字段类型 底层结构
term 单值精确匹配 keyword, numeric, date 倒排索引 / BKD-tree
terms 多值精确匹配(IN) keyword, numeric, date 倒排索引 / BKD-tree
range 范围匹配 numeric, date, keyword BKD-tree / 倒排索引
exists 字段是否存在 所有类型 doc values / norms
ids _id 匹配 _id UID 索引
prefix 前缀匹配 keyword, text 倒排索引 Term Dictionary 前缀扫描
wildcard 通配符(*, ? keyword 倒排索引扫描(可能很慢)
regexp 正则表达式 keyword 倒排索引扫描(可能很慢)

wildcardregexp 需要扫描 Term Dictionary 中的词项逐一匹配,代价可能很高。尤其是 wildcard* 开头时(如 *engine),无法利用 FST 的前缀定位能力,需要扫描所有词项。

模糊查询:编辑距离容错

模糊匹配解决用户拼写错误的问题。核心概念是编辑距离(Levenshtein distance)——把一个字符串变成另一个字符串所需的最少单字符操作次数(插入、删除、替换、相邻字符转置)。

fuzzy 查询

1
{ "fuzzy": { "title": { "value": "elastisearch", "fuzziness": 1 } } }

“elastisearch” 和 “elasticsearch” 的编辑距离是 1(缺少一个 ‘c’),所以 fuzziness=1 可以匹配。

match + fuzziness

更常用的方式是在 match 查询中启用 fuzziness:

1
{ "match": { "title": { "query": "elastisearch guids", "fuzziness": "AUTO" } } }

这会对每个分词后的词项分别做模糊匹配:“elastisearch” 模糊匹配到 “elasticsearch”,“guids” 模糊匹配到 “guide”。

fuzziness: "AUTO" 的规则:

词项长度 允许的编辑距离
0-2 字符 0(必须精确匹配)
3-5 字符 1
6+ 字符 2

从精确到模糊的查询谱系

flowchart LR
    TERM["term<br/>不分析, 不容错<br/>性能: 最快"]
    MATCH["match<br/>分析后多词项<br/>OR / AND"]
    PHRASE["match_phrase<br/>分析 + 位置相邻<br/>slop 容错"]
    FUZZY["fuzzy / match+fuzziness<br/>编辑距离容错"]
    WILD["wildcard<br/>通配符 * ?"]
    REGEX["regexp<br/>正则模式"]

    TERM -->|"加 analyzer"| MATCH
    MATCH -->|"加位置约束"| PHRASE
    PHRASE -->|"放宽到字符容错"| FUZZY
    FUZZY -->|"放宽到模式匹配"| WILD
    WILD -->|"放宽到正则"| REGEX

    style TERM fill:#e8f5e9
    style REGEX fill:#ffebee

从左到右,匹配条件逐步放宽,查询开销也随之上升。选择查询类型时沿这条谱系找到精度刚好满足需求的那个点——越靠左性能越好。

查询 是否分析 位置要求 容错机制 性能
term 最快
match(单词项)
match(多词项) OR/AND
match_phrase 相邻 slop 中等
fuzzy / match+fuzziness 编辑距离 中等
prefix 前缀匹配 中等
wildcard 通配符 慢(尤其前缀通配)
regexp 正则

地理查询

ES 支持地理空间查询,用于"附近 N 公里"之类的场景。

geo_point 字段类型

地理查询依赖 geo_point 字段类型。先定义 mapping:

1
2
3
4
5
6
7
8
9
PUT /geo-demo
{
"mappings": {
"properties": {
"name": { "type": "text" },
"location": { "type": "geo_point" }
}
}
}

写入带经纬度的文档:

1
2
3
4
5
6
7
8
9
POST /geo-demo/_bulk
{"index": {"_id": "1"}}
{"name": "天安门", "location": {"lat": 39.9087, "lon": 116.3975}}
{"index": {"_id": "2"}}
{"name": "故宫", "location": {"lat": 39.9163, "lon": 116.3972}}
{"index": {"_id": "3"}}
{"name": "颐和园", "location": {"lat": 39.9999, "lon": 116.2755}}
{"index": {"_id": "4"}}
{"name": "上海外滩", "location": {"lat": 31.2400, "lon": 121.4900}}

geo_distance 查询

查找某个中心点指定半径内的文档——最常见的地理查询场景:

1
2
3
4
5
6
7
8
9
10
11
12
GET /geo-demo/_search
{
"query": {
"geo_distance": {
"distance": "10km",
"location": {
"lat": 39.9087,
"lon": 116.3975
}
}
}
}

以天安门为圆心、10 公里为半径,天安门、故宫、颐和园都在范围内(北京城区直径约 30 公里),上海外滩不在范围内。

geo_bounding_box 查询

矩形范围查询——指定左上角和右下角经纬度:

1
2
3
4
5
6
7
8
9
10
11
GET /geo-demo/_search
{
"query": {
"geo_bounding_box": {
"location": {
"top_left": { "lat": 40.0, "lon": 116.2 },
"bottom_right": { "lat": 39.8, "lon": 116.5 }
}
}
}
}

geo_shape 查询

复杂几何形状查询——支持多边形、圆、矩形的包含/相交/不相交判断。需要使用 geo_shape 字段类型(比 geo_point 更重量级)。

地理查询的底层数据结构

geo_point 使用 BKD-tree 索引——和数值 range 查询使用同一套 Lucene points 索引结构。经纬度被编码为二维数值,BKD-tree 支持高效的矩形范围查询,geo_distance 先用外接矩形做粗筛,再用精确距离计算做细筛。

geo_shape 使用 tessellated triangles 索引——把复杂几何形状分解为三角形网格,用 BKD-tree 索引这些三角形。

地理查询通常放在 filter context 中(不需要打分),配合 _geo_distance 排序使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET /geo-demo/_search
{
"query": {
"bool": {
"filter": {
"geo_distance": {
"distance": "50km",
"location": { "lat": 39.9087, "lon": 116.3975 }
}
}
}
},
"sort": [
{
"_geo_distance": {
"location": { "lat": 39.9087, "lon": 116.3975 },
"order": "asc",
"unit": "km"
}
}
]
}

bool 查询:组合的核心

bool 查询是 Query DSL 的组合引擎。它用四个子句把多个查询条件组合在一起:

子句 语义 是否打分 类比 SQL
must AND,必须匹配 WHERE … AND …
filter AND,必须匹配 否(可缓存) WHERE … AND …(无排序权重)
should OR,可选匹配 无直接对应(加分项)
must_not NOT,必须不匹配 否(可缓存) WHERE NOT …

mustfilter 的区别只在于是否打分。功能上两者都要求条件必须满足。把不需要影响排序的条件放在 filter 里——跳过打分计算,结果还能被缓存。

flowchart TD
    BOOL["bool query"]
    MUST["must<br/>必须匹配 + 打分"]
    FILTER["filter<br/>必须匹配, 不打分, 可缓存"]
    SHOULD["should<br/>可选匹配 + 加分"]
    MUSTNOT["must_not<br/>必须不匹配, 不打分, 可缓存"]

    BOOL --> MUST
    BOOL --> FILTER
    BOOL --> SHOULD
    BOOL --> MUSTNOT

    MUST -->|贡献 BM25| SCORE(("_score"))
    SHOULD -->|贡献 BM25| SCORE
    FILTER -->|yes/no| RESULT["结果集"]
    MUSTNOT -->|排除| RESULT

四个子句分成两条通路:mustshould 走打分通路影响 _scorefiltermust_not 走布尔通路只决定文档进不进结果集。

should 的行为取决于上下文:如果 bool 查询中没有 mustfilter 子句,should 中至少一个条件必须匹配;如果有 mustfiltershould 变为可选的加分项。minimum_should_match 参数可以控制最少需要匹配几个 should 子句。

综合实验

构建一个完整的 bool 查询,同时使用四个子句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET /my-index/_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "elasticsearch" } }
],
"filter": [
{ "term": { "status": "published" } },
{ "range": { "date": { "gte": "2025-01-01" } } }
],
"should": [
{ "match": { "title": "guide" } },
{ "match": { "title": "tutorial" } }
],
"must_not": [
{ "term": { "language": "french" } }
],
"minimum_should_match": 1
}
}
}

这个查询的语义:

  • title 必须包含 “elasticsearch”(must,参与打分)
  • status 必须是 “published” 且 date >= 2025-01-01(filter,不打分,可缓存)
  • title 中包含 “guide” 或 “tutorial” 至少一个(should + minimum_should_match=1,加分)
  • language 不能是 “french”(must_not,排除)

explain: true 可以看到最终分数只包含 mustshould 中匹配项的 BM25 贡献,filtermust_not 不贡献分数。

1
2
3
4
5
6
7
8
bool query 执行流程:
query → bool
├── must: match "elasticsearch" → BM25 score = 1.2
├── filter: term "published" → yes/no (cached bitset)
├── filter: range date >= 2025 → yes/no (cached bitset)
├── should: match "guide" → BM25 score = 0.8
├── must_not: term "french" → exclude
└── final score = 1.2 + 0.8 = 2.0

模式提炼:声明式查询组合

1
2
3
bool(must, filter, should, must_not)
→ 声明"什么条件要满足"和"什么条件影响排序"
→ 执行引擎决定怎样高效执行
系统 组合方式 打分控制 缓存
ES bool query must/filter/should/must_not query vs filter context filter 子句自动缓存
SQL WHERE AND / OR / NOT 无内置打分 查询计划缓存
Solr fq + q q 打分 + fq 过滤 q 参与打分,fq 不参与 fq 缓存
MongoDB $and / $or / $not 无内置打分

ES 的 bool 查询比 SQL WHERE 更有表达力——它同时控制匹配逻辑和排序权重。SQL 的 WHERE 只负责过滤,排序需要单独的 ORDER BY。

工程迁移表

概念 Elasticsearch SQL (MySQL/PG) Solr MongoDB
全文搜索 match / match_phrase LIKE '%..%' / FULLTEXT q=... $text
精确匹配 term / terms = / IN fq=field:value {field: value}
范围查询 range BETWEEN / > < fq=field:[a TO b] $gte / $lt
模糊匹配 fuzzy / match+fuzziness SOUNDEX / 扩展 q=term~2 无原生支持
地理查询 geo_distance / geo_bounding_box PostGIS ST_DWithin Solr spatial $near / $geoWithin
组合逻辑 bool (must/filter/should/must_not) WHERE AND/OR/NOT q + fq $and / $or / $not
打分控制 query context vs filter context q 打分 / fq 不打分

常见误解

误解一:对 text 字段用 term 查询。 这是最常见的错误。text 字段在索引时经过 analyzer 转成小写词项,term 查询不对输入做分析。用 term: "Elasticsearch" 查 text 字段不会命中,因为倒排索引中存的是 “elasticsearch”(小写)。对 text 字段应该用 match

误解二:filter 和 must 的结果一样所以无所谓用哪个。 结果集是一样的,但 filter 不计算分数、结果可以被缓存。对于不需要影响排序的条件(如状态过滤、时间范围过滤),用 filter 比 must 性能更好。

误解三:geo_distance 查询可以用在 text 字段上。 地理查询只能用在 geo_pointgeo_shape 类型的字段上。这些字段在 mapping 中必须预先定义。

误解四:wildcard 查询和 match 一样好用。 wildcard 查询(尤其是 *engine 这种前缀通配)需要扫描整个 Term Dictionary,性能可能很差。能用 matchprefix 解决的需求不要用 wildcard

练习

  1. 创建一个 index,定义 title(text + keyword multi-field)、status(keyword)、price(double)字段。写入数据后,对比 match 查 title 和 term 查 title.keyword 的行为差异。

  2. 构建一个 bool 查询,用 must 做全文搜索、filter 做状态和价格过滤、should 做加分。用 explain: true 验证 filter 子句不贡献分数。

  3. 创建一个带 geo_point 字段的 index,写入几个不同城市的坐标,用 geo_distance 查询"某坐标 100km 内的文档",并用 _geo_distance 排序按距离升序返回。

  4. match + fuzziness: "AUTO" 搜索一个故意拼写错误的词,验证模糊匹配能否找到正确文档。

系列导航

上一篇 下一篇
相关性评分:BM25 与打分机制 Aggregation 框架:搜索之上的实时分析

参考资料