深入 Elasticsearch(08 补充):查询为什么没命中
I don't have a key. 放进 Elasticsearch 以后,输入 don't have 为什么有时能查到、有时必须写成 "don't have"?这个问题不能只靠记住 match、term、fuzzy 的名字来解决。一次查询至少跨过四层:字段怎样建、文本怎样变成词项、输入采用哪种查询语法、命中的文档怎样排序。四层中任何一层不同,结果都会不同。
判断一条字符串查询前,按这个顺序检查:字段能力 → 词项 → 查询语言 → 查询类型与上下文。不要先猜“ES 的模糊匹配是不是失效了”。
本文用同一条文档贯穿所有例子:
1 | |
这里的“全文检索”默认指 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 | |
keyword 也不是“不处理”的绝对同义词。它不使用 analyzer,但可配置 normalizer,例如统一转小写;normalizer 的结果仍是一个词项,而不是多个词项。
同一个业务值经常既要全文搜索,又要筛选、聚合和排序。此时不要在 text 上硬开 fielddata,而是用 multi-field 建两份适合不同访问路径的索引:
1 | |
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 | |
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 | |
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 串成一条可调试的路径:
- Mapping 与字段类型 解释字段类型和 multi-fields。
- Analysis 管道 展开 analyzer 的组成与索引、搜索阶段的差异。
- 从 match 到 bool 的查询体系 解释查询类型和组合。
- BM25 与打分机制 展开相关性评分。
| 上一篇 | 下一篇 |
|---|---|
| Query DSL 深入:从 match 到 bool 的查询体系 | Aggregation 框架:搜索之上的实时分析 |
