【ES避坑指南】明明存的是 "CodingAddress",为什么 term 查询死活查不到?彻底搞懂 text 和 keyword

17 阅读4分钟

前言

在使用 Elasticsearch (ES) 开发时,很多同学(包括我自己)都曾遇到过这样一个“灵异事件”:

数据库里明明有一条数据,字段值是 "CodingAddress",我用 term 精确查询去搜,结果却是 0 hits(查不到)。我把查询条件改成小写的 "codingaddress" 就能查到了?或者有时候字段里带个横杠 - 或斜杠 /,也查不到了?

这背后其实是 ES 中最基础也最重要的概念差异:Text(文本)Keyword(关键字)

今天我们就从一个真实的查询失败案例入手,彻底搞懂这两个类型的区别以及 mapping 的正确配置姿势。


1. 案发现场

假设我们需要查询代码库地址,Mapping 定义如下:

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

我们的查询语句是这样的:

{
    "query": {
        "term": {
            "codingAddress": "CodingAddress"
        }
    }
}

结果:查不到数据。

甚至,如果数据是 "uniops-platform/uniops-server",我们用 term 查这个完整字符串,也查不到

2. 核心原因:分词(Analysis)

问题的根源在于:你以为你存的是 "CodingAddress",但在 ES 的倒排索引里,它早就“变身”了。

因为你的字段类型是 "type": "text"

Text 类型:为了“搜得到”而生

text 类型是 ES 专门用来做全文检索的。ES 默认会使用 Standard Analyzer(标准分词器) 对存入的内容进行处理:

  1. 分词(Tokenization):遇到空格、特殊符号(如 -, /, .) 就会把字符串切断。
  2. 转小写(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

"codingAddress": {
    "type": "text",
    "fields": {
        "keyword": { "type": "keyword" } // <--- 就是它!
    }
}

keyword 类型不分词,直接存储原始字符串。

  • 存入 "CodingAddress" -> 索引里就是 "CodingAddress"

修正后的查询:

{
    "term": {
        "codingAddress.keyword": "CodingAddress" // 注意字段名加了 .keyword
    }
}

适用场景:ID、枚举状态、URL、文件路径的精确过滤。

方案二:使用 Match 查询(模糊匹配)

如果你就是想搜 codingAddress 这个主字段,请用 match

{
    "match": {
        "codingAddress": "CodingAddress"
    }
}

match 查询知道字段是 text 类型,所以它会先把你的搜索词 "CodingAddress" 也变成小写 "codingaddress",然后再去匹配,这样就能对上了。

适用场景:文章标题、内容搜索。

方案三:使用 Match Phrase(短语匹配)

如果你的数据包含符号(如 uniops-platform/uniops-server),用 match 可能会搜出包含 platform 的其他杂乱数据。此时可以用 match_phrase,它要求单词必须相邻且顺序一致。


4. 深度解析:Keyword 类型的坑 (ignore_above)

细心的同学可能发现了,Mapping 里还有个配置:

"keyword": {
    "ignore_above": 256,
    "type": "keyword"
}

这个 ignore_above: 256 是什么鬼?

这是一个保护机制。它的意思是:如果某个字段的字符串长度超过 256 个字节,那么这个字段值将不会被存入 keyword 索引中。

  • 后果
    • 如果你存了一个超长的 URL(比如 300 字符)。
    • text 字段依然可以分词搜索(能搜到里面的单词)。
    • 但是!你用 termcodingAddress.keyword 时,会发现查不到这条数据(就像它不存在一样)。
    • 你也无法对这条数据进行聚合(Aggregation)或排序(Sorting)。

如何解决? 如果你的业务字段(如 URL 或 长文本ID)确实可能超过 256,记得在定义 Mapping 时把这个值调大(例如 2048),或者直接干掉这个参数。

注意:修改 Mapping 后,必须 Reindex(重建索引) 才能对旧数据生效。


5. 总结:Text vs Keyword 对照表

为了方便记忆,我整理了这张对照表,建议收藏:

特性Text (文本)Keyword (关键字)
是否分词✅ 是 (Analyzed)❌ 否 (Not Analyzed)
大小写默认转小写区分大小写 (原样存储)
处理特殊符号会拆分 (如 - /)保留符号
适用查询match, match_phraseterm, terms, range
排序/聚合❌ 不支持 (除非开 fielddata,极其耗内存)✅ 支持 (性能高)
典型场景文章正文、商品描述、日志内容ID、状态码、邮箱、URL、标签

最佳实践建议

在生产环境中,通常建议采用 "Multi-fields" (多字段) 策略(就像文章开头的 Mapping 那样):

  1. codingAddress (text):用于支持全文检索(搜关键词)。
  2. codingAddress.keyword (keyword):用于支持精确匹配、排序和聚合报表。

搞清楚了这一点,以后再遇到“查不到数据”的情况,先问自己一句:我是不是用 term 查了 text 字段?