I don't have a key. 放进 Elasticsearch 以后,输入 don't have 为什么有时能查到、有时必须写成 "don't have"?这个问题不能只靠记住 match、term、fuzzy 的名字来解决。一次查询至少跨过四层:字段怎样建、文本怎样变成词项、输入采用哪种查询语法、命中的文档怎样排序。四层中任何一层不同,结果都会不同。

判断一条字符串查询前,按这个顺序检查:字段能力 → 词项 → 查询语言 → 查询类型与上下文。不要先猜“ES 的模糊匹配是不是失效了”。

本文用同一条文档贯穿所有例子:

1
2
3
4
{
"message": "I don't have a key.",
"status": "open"
}

这里的“全文检索”默认指 Elasticsearch 的词法全文检索:把文本变成词项,再在倒排索引里检索。它不会默认按语义距离找近义句子;向量检索、semantic_text 或 kNN 才属于另一条显式开启的语义检索路径。

一张图先固定四层

flowchart LR
    A[原始 JSON 字段] --> B{字段 mapping}
    B -->|text| C[analyzer\n词项和位置]
    B -->|keyword| D[一个完整值\n或 normalizer 后的一个词项]
    C --> E[倒排索引]
    D --> E
    F[Kibana / API 输入] --> G{查询语言}
    G --> H{查询类型}
    H --> I[查询词项、位置或编辑距离]
    E --> I
    I --> J{query 还是 filter context}
    J --> K[命中文档]
    J --> L[BM25 等评分与排序]

这张图回答一个常见误解:text、引号、match_phrase、fuzziness、_score 不在同一层。text 是字段建模;引号是输入语言的语法;match_phrase 是查询类型;fuzziness 是查询可选的容错;评分还取决于查询被放在 must、should 还是 filter。

text 与 keyword:不是“能查”和“不能查”的区别

字符串字段首先要回答的问题是:读者要按词搜索,还是把整个值当成一个值使用?

字段 写入 I don't have a key. 后的关键形态 典型用途 默认可排序、聚合
text 经过 analyzer,形成多个词项及其位置 标题、正文、日志消息 否
keyword 作为一个完整值形成一个词项 状态、标签、ID、邮箱、主机名 是

text 不是“更模糊的字符串”;它是可分析的文本。默认 standard analyzer 依照 Unicode 词边界切分并转小写。它不会默认删除英语停用词,也不会默认做词干化。因此这个句子的预期词项是:

1
2
3
4
I don't have a key.
↓ standard analyzer
i | don't | have | a | key
位置:0 1 2 3 4

keyword 也不是“不处理”的绝对同义词。它不使用 analyzer,但可配置 normalizer,例如统一转小写;normalizer 的结果仍是一个词项,而不是多个词项。

同一个业务值经常既要全文搜索,又要筛选、聚合和排序。此时不要在 text 上硬开 fielddata,而是用 multi-field 建两份适合不同访问路径的索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT messages
{
"mappings": {
"properties": {
"message": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"status": { "type": "keyword" }
}
}
}

message 用于 match 和 match_phrase;message.keyword 用于完整值的筛选、排序或聚合。注意两条容易混淆的默认规则:动态 mapping 遇到普通字符串通常会生成 text 加 .keyword 子字段;显式写成 { "type": "text" } 时,不会自动补 .keyword,需要手动写 fields。

字段能力速查表

“一个字段还有多少配置”没有一个脱离字段类型的固定数字。更容易记的是四种能力:能否高效查、能否保留词的位置、能否给每个文档取值、能否为相关性保留归一化信息。

能力与参数 text 默认 keyword 默认 作用与边界
index true true 建立可高效检索的索引。关掉后,text 无法搜索;保留 doc values 的 keyword、数值、日期仍可查询,但会慢。
doc_values 不支持 true 面向“按文档取值”的磁盘列式结构,供排序、聚合、脚本使用。不是全文检索索引。
index_options positions docs text 默认保留文档、词频和位置;位置是普通短语查询的基础。keyword 只有一个词项,不存在多词短语。
norms true false 为评分保存字段长度等归一化信息。关闭可省空间,也会失去这部分评分信号。
store false false 单独保存字段值以便 stored_fields 取回;不影响 _source,也不决定能否搜索。
term_vector no 不适用 额外保存词项及可选位置、偏移等,常用于特定高亮或分析需求;普通短语查询不需要它。
fielddata false 不适用 让 text 的分析后词项可用于排序、聚合;会占堆内存,优先改用 .keyword。

短语匹配特别容易被误配。text 的 index_options: positions 默认已经打开,match_phrase 因而可以工作;index_phrases: true 是为常见的两词短语额外建索引、提高性能,不是“开启短语匹配”的开关。若把 index_options 降到不保存位置的级别,短语查询就失去所需信息。

评分也不是某个字段的单独开关。字段上的 norms、similarity、index_options 决定可用的评分信息;查询是否真的计算 _score,由它所处的 query context 或 filter context 决定。bool.filter 只筛选,bool.must 与 bool.should 默认会参与相关性评分。

分词的本质:把字符串变成带位置的词项流

“分词”常被说成“按 delimiter 切开”,这只描述了 tokenizer 的一部分工作。更准确地说,analysis 是一条把字符流变成带位置和偏移信息的词项流的管道:

flowchart LR
    A[原始文本\nI don't have a key.] --> B[Character filters\n可选:改字符、去 HTML]
    B --> C[Tokenizer\n按 Unicode 词边界切分]
    C --> D[Token filters\n小写、停用词、词干、同义词]
    D --> E[词项流\ni@0, don't@1, have@2, a@3, key@4]
    E --> F[倒排索引\n词项 -> 文档、频率、位置]

一个 analyzer 有零到多个 character filter、恰好一个 tokenizer、零到多个 token filter。Tokenizer 决定“怎样切”;token filter 决定“切出的 token 怎样改、删或补”。例如 english analyzer 可能删除停用词并做词干化,standard 默认不会。中文文本通常需要面向中文的 analyzer;仅靠空格切分无法得到合适的中文词项。

词法检索比较的是两侧分析后的词项:查询词项与文档词项。默认不是把原始句子做等值比较,也不是计算句意距离。大小写归一、同义词和词干化会改变词项,所以能够扩大匹配;它们来自配置规则,不是模型理解出来的语义相近。

用目标索引和目标字段运行 _analyze,不要只猜:

1
2
3
4
5
POST messages/_analyze
{
"field": "message",
"text": "I don't have a key."
}

field 很重要:它会使用该字段实际配置的 analyzer。若索引时与搜索时使用了不同的 analyzer、search_analyzer 或 search_quote_analyzer,同一个字符串在两个阶段可能得到不同的词项流。

为什么 don't have 有时命中,有时要加双引号

先固定前提:message 是上面的 text 字段,索引与搜索都用 standard analyzer,查询目标确实是 message。此时三种 Query DSL 的语义不同:

Query DSL 对 don't have 做什么 对这条文档的含义
match 分析为 don't、have,默认任一词项即可匹配 会命中;词序无要求。operator: "and" 时要求两个词都存在。
match_phrase 分析为同样的词项,并检查位置和顺序 会命中,因为 don't@1 后面紧跟 have@2。
term 不分析,直接找一个名为 don't have 的词项 不会命中;字段里只有 don't 和 have 两个词项。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET messages/_search
{
"query": {
"bool": {
"must": [
{
"match_phrase": {
"message": "don't have"
}
}
],
"filter": [
{ "term": { "status": "open" } }
]
}
}
}

match_phrase 不是“整字段精确相等”。它只要求查询词项以指定顺序、默认不留间隔地出现在字段中;I don't have a key. 可以命中 don't have。slop 可以允许位置间隔,仍然不是原始字符串比较。

实际的 Kibana 查询栏还多了一层:输入的语言。Discover 默认使用 KQL,也可以切换到 Lucene 或 ES|QL。它们的引号规则不同,不能把任意一处双引号理解为“ES 要做短语查询”。

所在位置 don't have "don't have" 单引号
Kibana KQL,message: 是 text 无引号的多个词按 text 的分析结果匹配,词序不固定 双引号要求词项按原顺序出现,相当于短语条件 不是 KQL 的字符串引号语法;使用双引号并按需反斜杠转义。
Lucene query_string 一个或多个 term,默认操作符及字段设置会影响结果 双引号表示 phrase 不承担 phrase 语义。
Dev Tools 的 JSON "query": "don't have" 只是 JSON 字符串值;若它在 match 里仍是 match JSON 的双引号是语法;只有显式 match_phrase 或 query-string 内嵌双引号才表达短语 JSON 不接受单引号作为字符串定界。
ES QL 由 ES QL 的表达式语法决定

因此,“不加引号查不到,加双引号能查到”不是一条 Elasticsearch 通则。排查应先确认四件事:查询栏当前是 KQL、Lucene 还是 ES|QL;是否写了字段名;这个字段是 text、keyword 还是 multi-field;_analyze 的两个词项是否真的存在。未指定字段的裸词会受 index.query.default_field 控制,可能根本没有搜索 message。

全文、精确、短语、模糊、评分:五个独立轴

“精确匹配 vs 模糊匹配”只覆盖了查询世界的一小部分。更可靠的分类方式是先问查询在约束什么:词项值、词项位置、字符编辑距离,还是排序分数。

想约束的东西 常用查询 会分析输入 是否默认要求顺序 是否天然算分
已经存在的单个词项或完整值 term、terms 否 不适用 放在 query context 时可算分;放在 filter 时不算。
多个分析后的词项 match、multi_match 是 否 query context 会算分。
词项的位置与相邻关系 match_phrase 是 是 query context 会算分。
拼写相近的词项 fuzzy、match 加 fuzziness fuzzy 直接作用于给定 term;match 先分析再对词项容错 否 query context 会算分。
字符模式 prefix、wildcard、regexp 通常不走全文 analysis 取决于模式 与是否评分无必然绑定。

fuzziness 衡量的是编辑距离,例如少一个字符、替换一个字符或相邻字符换位,不是“语义距离”。全文查询也不等于会模糊拼写:match 默认做的是分析后的词项查询,拼错 elastisearch 并不会自动命中 elasticsearch,除非显式配置 fuzziness、同义词等机制。

评分是另一个维度。match_phrase 可以只作过滤,也可以在 query context 中按 BM25 等相似度参与排序;term 也一样。查询类型回答“什么算命中”,上下文回答“命中后是否影响排序”。

一条可复用的排查路径

查询没有命中时,按数据流从左到右检查,比改引号或改查询类型更快:

flowchart TD
    A[确认目标 index 与时间范围] --> B[GET index/_mapping\n字段类型和 multi-field]
    B --> C{查询字段是否可搜索?}
    C -->|否| D[检查 index:false\n或改用合适字段]
    C -->|是| E[POST index/_analyze\n用 field 观察实际词项]
    E --> F[确认查询栏语言\nKQL / Lucene / ES|QL / JSON]
    F --> G[选择 term / match / match_phrase / fuzziness]
    G --> H[需要排序吗?\nquery context 或 filter context]
    H --> I[用 _explain 核对命中与分数]

排查的核心不是背 API,而是让写入路径和查询路径可见。_mapping 回答“字段具备什么能力”,_analyze 回答“这条文本最后变成什么”,_explain 回答“为何命中或为何得分”。三者合起来能把大多数“明明包含这句话却查不到”的问题从猜测变成可验证的事实。

记忆卡

看到的需求 先选什么 不要误用成
搜一段自然语言正文 text + match term 查整句
句子中必须按顺序出现几个词 text + match_phrase keyword 的完整值相等
状态、标签、ID 分组或排序 keyword / 数值 / 日期的 doc values 在 text 上盲开 fielddata
同一字符串既全文搜又聚合排序 text + .keyword multi-field 指望显式 text 自动生成子字段
容忍拼写错误 match + fuzziness 或 fuzzy 以为全文检索天然理解语义
Kibana 里双引号改变结果 先确认 KQL / Lucene / ES QL

系列导航

这一篇放在第 08 篇之后,作为查询段的实战补充。它不替代既有章节,而是把字段、分析、评分和 Query DSL 串成一条可调试的路径:

上一篇 下一篇
Query DSL 深入:从 match 到 bool 的查询体系 Aggregation 框架:搜索之上的实时分析

参考资料