前言
在使用 Elasticsearch (ES) 开发时,很多同学(包括我自己)都曾遇到过这样一个"灵异事件":
数据库里明明有一条数据,字段值是 "CodingAddress",我用 term 精确查询去搜,结果却是 0 hits(查不到) 。我把查询条件改成小写的 "codingaddress" 就能查到了?或者有时候字段里带个横杠 - 或斜杠 /,也查不到了?
这背后其实是 ES 中最基础也最重要的概念差异:Text(文本) 与 Keyword(关键字)。
今天我们就从一个真实的查询失败案例入手,彻底搞懂这两个类型的区别以及 mapping 的正确配置姿势。
1. 案发现场
假设我们需要查询代码库地址,Mapping 定义如下:
json
"codingAddress": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
我们的查询语句是这样的:
json
{
"query": {
"term": {
"codingAddress": "CodingAddress"
}
}
}
结果:查不到数据。
甚至,如果数据是 "uniops-platform/uniops-server",我们用 term 查这个完整字符串,也查不到。
2. 核心原因:分词(Analysis)
问题的根源在于:你以为你存的是 "CodingAddress",但在 ES 的倒排索引里,它早就"变身"了。
因为你的字段类型是 "type": "text"。
Text 类型:为了"搜得到"而生
text 类型是 ES 专门用来做全文检索 的。ES 默认会使用 Standard Analyzer(标准分词器) 对存入的内容进行处理:
- 分词(Tokenization) :遇到空格、特殊符号(如
-,/,.) 就会把字符串切断。 - 转小写(Lowercase):把所有大写字母变成小写。
实际存储过程演示:
- 输入数据 :
"CodingAddress"- 索引存储 :
["codingaddress"](变小写了)
- 索引存储 :
- 输入数据 :
"uniops-platform/uniops-server"- 索引存储 :
["uniops", "platform", "server"](被符号拆散了,且 uniops 存了一次)
- 索引存储 :
Term 查询:为了"精准匹配"而生
term 查询是不分词的。它代表"精确等于"。
- 你的查询 :它拿着
"CodingAddress"这个字符串去索引里找。 - 匹配结果 :索引里只有
"codingaddress",两者不相等(大小写不同),所以 匹配失败。 - 你的查询 :它拿着
"uniops-platform/uniops-server"去找。 - 匹配结果 :索引里全是碎的单词(tokens),没有这个长字符串,所以 匹配失败。
3. 解决方案:三种姿势
方案一:查 .keyword 子字段(推荐 ✅)
在很多自动生成的 Mapping 中,都会有一个 fields 属性,里面定义了 keyword。
json
"codingAddress": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" } // <--- 就是它!
}
}
keyword 类型不分词,直接存储原始字符串。
- 存入
"CodingAddress"-> 索引里就是"CodingAddress"。
修正后的查询:
json
{
"term": {
"codingAddress.keyword": "CodingAddress" // 注意字段名加了 .keyword
}
}
适用场景:ID、枚举状态、URL、文件路径的精确过滤。
方案二:使用 Match 查询(模糊匹配)
如果你就是想搜 codingAddress 这个主字段,请用 match。
json
{
"match": {
"codingAddress": "CodingAddress"
}
}
match 查询知道字段是 text 类型,所以它会先把你的搜索词 "CodingAddress" 也变成小写 "codingaddress",然后再去匹配,这样就能对上了。
适用场景:文章标题、内容搜索。
方案三:使用 Match Phrase(短语匹配)
如果你的数据包含符号(如 uniops-platform/uniops-server),用 match 可能会搜出包含 platform 的其他杂乱数据。此时可以用 match_phrase,它要求单词必须相邻且顺序一致。
4. 深度解析:Keyword 类型的坑 (ignore_above)
细心的同学可能发现了,Mapping 里还有个配置:
json
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
这个 ignore_above: 256 是什么鬼?
这是一个保护机制。它的意思是:如果某个字段的字符串长度超过 256 个字节,那么这个字段值将不会被存入 keyword 索引中。
- 后果 :
- 如果你存了一个超长的 URL(比如 300 字符)。
text字段依然可以分词搜索(能搜到里面的单词)。- 但是!你用
term查codingAddress.keyword时,会发现查不到这条数据(就像它不存在一样)。 - 你也无法对这条数据进行聚合(Aggregation)或排序(Sorting)。
如何解决? 如果你的业务字段(如 URL 或 长文本ID)确实可能超过 256,记得在定义 Mapping 时把这个值调大(例如 2048),或者直接干掉这个参数。
注意 :修改 Mapping 后,必须 Reindex(重建索引) 才能对旧数据生效。
5. 总结:Text vs Keyword 对照表
为了方便记忆,我整理了这张对照表,建议收藏:
| 特性 | Text (文本) | Keyword (关键字) |
|---|---|---|
| 是否分词 | ✅ 是 (Analyzed) | ❌ 否 (Not Analyzed) |
| 大小写 | 默认转小写 | 区分大小写 (原样存储) |
| 处理特殊符号 | 会拆分 (如 - /) |
保留符号 |
| 适用查询 | match, match_phrase |
term, terms, range |
| 排序/聚合 | ❌ 不支持 (除非开 fielddata,极其耗内存) | ✅ 支持 (性能高) |
| 典型场景 | 文章正文、商品描述、日志内容 | ID、状态码、邮箱、URL、标签 |
最佳实践建议
在生产环境中,通常建议采用 "Multi-fields" (多字段) 策略(就像文章开头的 Mapping 那样):
codingAddress(text):用于支持全文检索(搜关键词)。codingAddress.keyword(keyword):用于支持精确匹配、排序和聚合报表。
搞清楚了这一点,以后再遇到"查不到数据"的情况,先问自己一句:我是不是用 term 查了 text 字段?