text vs keyword:90% 的困惑都在这
text vs keyword:90% 的困惑都在这
面试过 30+ 个号称"用过 ES"的候选人,问到 text 和 keyword 的区别,只有不到 5 个能讲清楚:什么场景用哪个、两者在底层存储和查询行为上到底差在哪里。
如果你对这个概念也模模糊糊的,这篇就是写给你的。我们用一张表、三个例子、两个真实事故把它彻底讲穿。
一句话区别
text |
keyword |
|
|---|---|---|
| 核心职责 | 全文搜索 | 精确匹配 + 聚合 + 排序 |
| 是否分词 | 会,按 analyzer 拆成 term | 不会,整个字符串就是一个 term |
| 底层结构 | 倒排索引(term → doc list) | doc values(列式存储)+ 可选倒排 |
| 查询方式 | match、match_phrase、query_string |
term、terms、range |
| 能否聚合 | ❌ 不允许,需要 .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。