Elasticsearch 系列 · 第三篇
同一个字符串字段,为什么有时候搜不到,有时候聚合直接报错?
Elasticsearch 里存字符串,其实有两种完全不同的存法:text(会被拆词、用来做全文检索)和 keyword(原样存一整份、只能精确匹配)。选错了类型,轻则搜索结果莫名其妙,重则一执行聚合就报错。
这篇不是背概念——是在本机真实跑起来的 Elasticsearch 8.15.0 上,用真实的分词结果、真实的查询命中/不命中、真实的报错信息,把这个区别讲透。
5 个 token
"Apple MacBook Pro 16-inch" 被 text 字段拆成的真实分词结果
1 个 token
同样的字符串放进 keyword 字段,原样不动
先看真实的分词结果
同一个索引里建两个字段,一个 text、一个 keyword,分别丢进同一句话,看 Elasticsearch 真实存下来的是什么。
真实实测
PUT /products/_analyze { "field": "name", "text": "Apple MacBook Pro 16-inch" }
→ apple · macbook · pro · 16 · inch (5 个小写 token)
PUT /products/_analyze { "field": "brand", "text": "Apple Inc." }
→ Apple Inc. (原样 1 个 token,大小写、标点全部保留)
applemacbookpro16inch
"Apple Inc."(整体一个)
text 字段会经过一个"分析器"(analyzer):先分词、再小写化(默认的 standard 分析器还会去掉大部分标点)。keyword 字段完全跳过这一步,存的就是原始字符串本身——这也是名字的来源:它更像一个"标签",不是一段"文章"。
text
为搜索而生
拆成词,建倒排索引(上一篇的话题)。适合"这句话里有没有出现某个词"这种模糊匹配。
keyword
为精确匹配 / 排序 / 聚合而生
整体存一份,不拆词。适合"这个字段的值恰好等于 X"、按这个字段排序、按这个字段分组统计。
multi-field
都要?那就两个都建
同一份数据,同时建一个 text 版本(搜索用)和一个 keyword 版本(精确匹配/聚合用)——这是 Elasticsearch 的默认行为,下面会展示。
真实测出来的四种查询结果
用 term 查询(要求精确匹配某个具体的 token)分别打在这两个字段上。
| 查询字段 | 查询值 | 结果 | 为什么 |
| brand (keyword) | "Apple Inc." | 命中 | 和存储的原始值完全一致 |
| brand (keyword) | "apple inc." | 不命中 | keyword 精确匹配,大小写都算数 |
| name (text) | "macbook" | 命中 | 是分词后 5 个 token 之一 |
| name (text) | "Apple MacBook Pro 16-inch" | 不命中 | 这个完整字符串从来没有作为单独一个 token 存在过 |
最后一行最容易让人困惑:明明这句话就是原文,用 term 查询却找不到——因为 text 字段压根没存过"整句话"这个 token,只存了拆开的 5 个词。想用完整原文去匹配,得用 match 查询(帮你把查询词也过一遍分词器)或者去查 keyword 字段。
聚合:keyword 直接成功,text 直接报错
对同一份数据做 terms 聚合(按字段值分组计数)。
真实实测
POST /products/_search { "aggs": { "by_brand": { "terms": { "field": "brand" }}}}
→ 成功: { "Apple Inc.": 1 }
POST /products/_search { "aggs": { "by_name": { "terms": { "field": "name" }}}}
→ illegal_argument_exception: Fielddata is disabled on [name] in [products].
Text fields are not optimised for operations that require per-document
field data like aggregations and sorting, so these operations are
disabled by default. Please use a keyword field instead.
这不是一个模糊的"不建议",是真实会抛出来的错误,而且原文直接告诉你怎么修——换用 keyword 字段。为什么 text 字段做聚合要专门被禁用?接着往下看,把它强行打开就知道了。
真实实测
-- 手动给 text 字段打开 fielddata,再跑一次同样的聚合
PUT /products/_mapping { "properties": { "name": { "type": "text", "fielddata": true }}}
→ 这次成功了,但结果是:
{ "16": 1, "apple": 1, "inch": 1, "macbook": 1, "pro": 1 }
打开之后聚合确实能跑,但按的是每一个词分组,不是按整个字段值分组——"Apple MacBook Pro 16-inch"这一件商品,在聚合结果里被拆成了 5 条记录,分别落在 5 个桶里。这几乎从来不是你想要的统计口径,这也是为什么 Elasticsearch 默认直接禁用而不是悄悄给你一个"看起来能用但语义错误"的结果。
默认行为:其实两个都会帮你建好
如果不手动指定映射,直接写一条数据进去,Elasticsearch 的动态映射(dynamic mapping)默认会怎么处理字符串字段?
真实实测
POST /autodemo/_doc { "title": "Hello World" }
GET /autodemo/_mapping
→ {
"title": {
"type": "text",
"fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }
}
}
默认行为其实是"两个都要"——自动建一个 text 主字段(搜索用)加一个 title.keyword 子字段(精确匹配/排序/聚合用)。ignore_above: 256 也是真实默认值:超过 256 字符的字符串不会存进 keyword 子字段(避免一个巨长的值把词典撑爆),但完全不影响它继续存进 text 字段参与搜索。
演示:同一句话,两种存法
改一改下面的文本,看 text vs keyword 分别会存成什么
这里的分词规则(小写化 + 按空格/标点切分)是本文实测用的 standard 分析器规则的简化还原,用于演示;真实 Elasticsearch 的 standard 分析器基于 Unicode 文本分段算法,对中文等语言的处理比这里复杂得多(中文默认会被逐字拆开,不是按词语切分——这也是为什么中文全文检索通常需要额外装 IK 等分词插件,不在本文范围内)。
参考与说明
- 本文所有查询结果、分词输出、报错信息均为本机真实运行的 Elasticsearch 8.15.0(单节点,security 关闭)真实执行得到,通过 curl 直接调用 REST API,未做任何删改。
- 演示区的分词规则是简化版本(仅按空白/标点切分 + 小写化),用来直观展示"拆词 vs 不拆词"这个核心区别,不是对 Elasticsearch standard 分析器(基于 Lucene 的 StandardTokenizer,遵循 Unicode UAX#29 文本分段规则)的精确复刻。
- 没有涉及:自定义分析器(自己组合 tokenizer + filter)、中文分词插件、keyword 字段的 normalizer(给 keyword 也做一点轻量处理,比如统一小写但不拆词)、text 字段的 fields 多字段除了 keyword 之外还能加别的分析器变体。
→