text vs keyword:90% 的困惑都在这

22 May 2026 – wusfe · 4 min read

text vs keyword:90% 的困惑都在这

面试过 30+ 个号称"用过 ES"的候选人,问到 textkeyword 的区别,只有不到 5 个能讲清楚:什么场景用哪个、两者在底层存储和查询行为上到底差在哪里。

如果你对这个概念也模模糊糊的,这篇就是写给你的。我们用一张表、三个例子、两个真实事故把它彻底讲穿。

一句话区别

text keyword
核心职责 全文搜索 精确匹配 + 聚合 + 排序
是否分词 会,按 analyzer 拆成 term 不会,整个字符串就是一个 term
底层结构 倒排索引(term → doc list) doc values(列式存储)+ 可选倒排
查询方式 matchmatch_phrasequery_string termtermsrange
能否聚合 ❌ 不允许,需要 .keyword 子字段
能否排序

一句话:需要搜索就用 text,需要精确匹配或聚合就用 keyword

但现实远比这句话复杂。

实例 1:一个日志字段

假设你有一条日志:

{
  "message": "User admin logged in from 192.168.1.1",
  "level": "ERROR"
}

message 应该用 text——你要能搜 "admin AND login" 找到它。level 应该用 keyword——你需要按 ERROR/WARN/INFO 精确筛选和统计。

{
  "message": { "type": "text" },
  "level":   { "type": "keyword" }
}

分别查询:

// 用 match 搜文本——走 text
GET /logs/_search
{ "query": { "match": { "message": "admin login" } } }

// 用 term 筛等级——走 keyword
GET /logs/_search
{ "query": { "term": { "level": "ERROR" } } }

如果你对 level(text 类型)用了 term 查询,ES 不会报错,但搜不到任何东西。因为存进去的 "ERROR" 经过默认分词器(standard analyzer)后变成了小写 "error",而你的 term 查的是原始 "ERROR",term 字典里根本没有这个值。

实例 2:keyword 的 ignore_above 陷阱

线上常见事故:某个 keyword 字段写入成功,但搜索出来内容被截断了。

PUT /articles
{
  "mappings": {
    "properties": {
      "title": { "type": "text" },
      "content": { "type": "text" },
      "url": { "type": "keyword", "ignore_above": 256 }
    }
  }
}

你插入一条长 URL(450 字符),写入成功,没有报错。但搜这个 URL 时死活搜不到。原因是 ignore_above: 256 会让超过 256 字符的值不被索引——它仍在 _source 里,JSON 原样返回,但倒排索引里没有它,term 查询自然找不到。

教训:keyword 的 ignore_above 默认值是 2147483647,建议按实际需要显式设置。如果数据可能超过 32766 字节(Lucene 的单 term 硬上限),考虑用 text 或其 hash 值做匹配。

实例 3:多字段映射——两个都要

一个字段既要做全文搜索又要做聚合,怎么办?ES 的默认行为已经给出了答案——多字段映射

{
  "title": {
    "type": "text",
    "fields": {
      "keyword": { "type": "keyword", "ignore_above": 256 }
    }
  }
}

写入 "iPhone 15 Pro Max" 后:

  • title 被分词为 ["iphone", "15", "pro", "max"],用于全文搜索
  • title.keyword 完整保留 "iPhone 15 Pro Max",用于精确匹配和聚合
// 全文搜索:"iPhone 15 Pro Max" 拆开匹配
GET /products/_search
{ "query": { "match": { "title": "iPhone 15 Pro Max" } } }

// 精确匹配:整个字符串相等才算
GET /products/_search
{ "query": { "term": { "title.keyword": "iPhone 15 Pro Max" } } }

// 聚合统计:有多少种不同的商品名
GET /products/_search
{
  "size": 0,
  "aggs": {
    "top_titles": { "terms": { "field": "title.keyword", "size": 20 } }
  }
}

事故 1:text 字段做聚合,CPU 打满

同事在 Kibana 上做可视化,按 user_id 分组统计订单数。user_id 是 text 类型,ES 抛出 fielddata 异常。同事搜到一篇文章说开启 fielddata: true 就能解决,照做后——

ES 集群 CPU 飙到 95%+,因为 text 字段开启 fielddata 会把所有 term 加载到堆内存,10 万用户 ID 就是 10 万个 term,加上海量文档,直接打爆 JVM。

解法:永远不要对 text 字段开 fielddata。需要聚合时,用 .keyword 子字段或重建索引改映射。

事故 2:用 term 查 text 字段,空结果

写了半天查询,返回 0 条。日志显示 ES 确实收到了请求,数据也确实在索引里。

// 这条查不出数据
GET /products/_search
{ "query": { "term": { "title": "iPhone" } } }

title 是 text 类型,"iPhone" 分词后存的是小写 "iphone"。而 term 不做分词,直接拿 "iPhone" 去查倒排索引——找不到,因为索引里的 term 是 "iphone"

解决:text 字段用 match 查,keyword 字段用 term 查。记不住就记一句——match 会分词,term 不会。

如何快速检查字段类型

GET /your_index/_mapping

看到 "type": "text" 的字段,如果有 "fields": {"keyword": ...},说明 ES 自动生了 keyword 子字段,聚合时用 字段名.keyword

如果只有 "type": "text" 没有子字段,而且你需要聚合——对不起,要么改 Mapping 重建索引,要么换条路。

总结

你想要的 用这个
用户输入关键词模糊搜索文章标题 title: text + match 查询
按订单状态筛选(已支付/已取消) status: keyword + term 查询
按商品分类数量做统计图表 category: keyword + terms 聚合
既要搜索又要聚合 多字段映射,搜索用字段本身,聚合用 .keyword

记住一句话:text 会分词,keyword 不会。你想让 ES 拆开就看,你想原样匹配就 keyword。