深入 Elasticsearch(08):从 match 到 bool 的查询体系
上一篇解决了分数怎么算出来的——BM25 的三个因子决定了文档的相关性排序。这一篇进入 Query DSL 的查询类型体系和组合逻辑。
ES 的查询体系不是一个扁平的 API 列表。全文查询、精确查询、模糊查询、地理查询、复合查询分别对应不同的底层数据结构和匹配逻辑。理解这个分类,才能在具体场景中选对查询类型。
本文抓两个问题:ES 的查询类型怎样分类,以及 bool 查询怎样把不同类型的查询组合起来。
查询体系全景
1 | |
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["只决定文档是否进入结果集"]
同一条 range 或 term 子句,放进 must 就会参与 BM25 计算,放进 filter 就变成纯布尔过滤并享受 bitset 缓存。选择上下文的判据是:这个条件需不需要影响结果排序。
全文查询
全文查询的共同特征:对查询文本先走 analyzer,产生词项,然后在倒排索引中匹配。
match 查询
match 是最常用的全文查询。它的内部流程:
1 | |
1 | |
默认行为是 OR——包含 “distributed” 或 “search” 的文档都会被返回。通过 operator 参数改为 AND:
1 | |
这就是 match 被称为"模糊匹配"的原因——它匹配的是分析后的词项,不是原始字符串。“Distributed Search” 和 “search distributed” 和 “a distributed full-text search engine” 都能被 match: "distributed search" 匹配到。
match_phrase 查询
match_phrase 不仅要求词项匹配,还要求词项在文档中的位置相邻且顺序一致。这利用了倒排索引 Posting List 中存储的 positions 信息。
1 | |
“a search engine for documents” 能匹配(“search” 和 “engine” 相邻且顺序正确)。
“an engine for search” 不能匹配(顺序不对)。
“search and engine” 不能匹配(中间有其他词)。
slop 参数允许词项之间有间隔:
1 | |
slop: 1 允许词项之间最多间隔 1 个位置。“search and engine” 现在可以匹配了(“search” 在位置 0,“engine” 在位置 2,间隔 1)。
multi_match 查询
multi_match 在多个字段上执行 match 查询:
1 | |
title^2 表示 title 字段的分数乘以 2 的权重。type 控制多字段分数的组合方式:
| type | 行为 |
|---|---|
best_fields(默认) |
取分数最高的字段 |
most_fields |
所有字段分数加和 |
cross_fields |
把多个字段当成一个字段处理 |
精确查询
精确查询不走 analyzer,直接用原始值在倒排索引或 BKD-tree 中匹配。
term 查询
term 查询把输入值当作精确的词项,直接在倒排索引中查找:
1 | |
term 查询不对输入做分析。这意味着对 text 字段使用 term 查询几乎总是错误的——text 字段在索引时被 analyzer 转成了小写词项,而 term 查询不转换输入。
1 | |
这是 ES 中最常见的使用错误之一。term 查询应该用在 keyword 字段上。
terms 查询
terms 是 term 的多值版本,等价于 SQL 的 IN:
1 | |
range 查询
range 对数值、日期和字符串字段做范围匹配。数值和日期字段底层使用 BKD-tree 索引,范围查询效率高:
1 | |
| 参数 | 含义 |
|---|---|
gt |
大于 |
gte |
大于等于 |
lt |
小于 |
lte |
小于等于 |
日期 range 支持日期数学表达式:"gte": "now-7d" 表示最近 7 天。
exists 查询
判断字段是否存在(有值):
1 | |
精确查询族总结
| 查询类型 | 匹配方式 | 适用字段类型 | 底层结构 |
|---|---|---|---|
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 | 倒排索引扫描(可能很慢) |
wildcard 和 regexp 需要扫描 Term Dictionary 中的词项逐一匹配,代价可能很高。尤其是 wildcard 以 * 开头时(如 *engine),无法利用 FST 的前缀定位能力,需要扫描所有词项。
模糊查询:编辑距离容错
模糊匹配解决用户拼写错误的问题。核心概念是编辑距离(Levenshtein distance)——把一个字符串变成另一个字符串所需的最少单字符操作次数(插入、删除、替换、相邻字符转置)。
fuzzy 查询
1 | |
“elastisearch” 和 “elasticsearch” 的编辑距离是 1(缺少一个 ‘c’),所以 fuzziness=1 可以匹配。
match + fuzziness
更常用的方式是在 match 查询中启用 fuzziness:
1 | |
这会对每个分词后的词项分别做模糊匹配:“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 | |
写入带经纬度的文档:
1 | |
geo_distance 查询
查找某个中心点指定半径内的文档——最常见的地理查询场景:
1 | |
以天安门为圆心、10 公里为半径,天安门、故宫、颐和园都在范围内(北京城区直径约 30 公里),上海外滩不在范围内。
geo_bounding_box 查询
矩形范围查询——指定左上角和右下角经纬度:
1 | |
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 | |
bool 查询:组合的核心
bool 查询是 Query DSL 的组合引擎。它用四个子句把多个查询条件组合在一起:
| 子句 | 语义 | 是否打分 | 类比 SQL |
|---|---|---|---|
must |
AND,必须匹配 | 是 | WHERE … AND … |
filter |
AND,必须匹配 | 否(可缓存) | WHERE … AND …(无排序权重) |
should |
OR,可选匹配 | 是 | 无直接对应(加分项) |
must_not |
NOT,必须不匹配 | 否(可缓存) | WHERE NOT … |
must 和 filter 的区别只在于是否打分。功能上两者都要求条件必须满足。把不需要影响排序的条件放在 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
四个子句分成两条通路:must 和 should 走打分通路影响 _score,filter 和 must_not 走布尔通路只决定文档进不进结果集。
should 的行为取决于上下文:如果 bool 查询中没有 must 或 filter 子句,should 中至少一个条件必须匹配;如果有 must 或 filter,should 变为可选的加分项。minimum_should_match 参数可以控制最少需要匹配几个 should 子句。
综合实验
构建一个完整的 bool 查询,同时使用四个子句:
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 可以看到最终分数只包含 must 和 should 中匹配项的 BM25 贡献,filter 和 must_not 不贡献分数。
1 | |
模式提炼:声明式查询组合
1 | |
| 系统 | 组合方式 | 打分控制 | 缓存 |
|---|---|---|---|
| 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_point 或 geo_shape 类型的字段上。这些字段在 mapping 中必须预先定义。
误解四:wildcard 查询和 match 一样好用。 wildcard 查询(尤其是 *engine 这种前缀通配)需要扫描整个 Term Dictionary,性能可能很差。能用 match 或 prefix 解决的需求不要用 wildcard。
练习
-
创建一个 index,定义 title(text + keyword multi-field)、status(keyword)、price(double)字段。写入数据后,对比
match查 title 和term查 title.keyword 的行为差异。 -
构建一个 bool 查询,用 must 做全文搜索、filter 做状态和价格过滤、should 做加分。用
explain: true验证 filter 子句不贡献分数。 -
创建一个带
geo_point字段的 index,写入几个不同城市的坐标,用geo_distance查询"某坐标 100km 内的文档",并用_geo_distance排序按距离升序返回。 -
用
match+fuzziness: "AUTO"搜索一个故意拼写错误的词,验证模糊匹配能否找到正确文档。
系列导航
| 上一篇 | 下一篇 |
|---|---|
| 相关性评分:BM25 与打分机制 | Aggregation 框架:搜索之上的实时分析 |
