在 Elasticsearch (ES) 的世界里,keyword 字段类型是用于存储那些不需要分词、需要精确匹配的文本,比如标签、状态码、用户名、邮箱地址等等。它就像一个严谨的守门员,只有一模一样的值才能通过。
但有时候,这种『严谨』反而会带来麻烦。比如,你想根据用户输入的邮箱 USER@EXAMPLE.COM 查找对应的用户文档,而数据库里存的是 user@example.com。标准的 keyword 字段会告诉你:“抱歉,找不到!” 因为大小写不同,在它眼里就是两个完全不同的字符串。
类似的情况还有很多:
- 大小写不敏感搜索:
Applevsapple - 忽略口音/变音符号:
cafévscafe - 处理其他非关键字符变化
这时候,你可能会想:“难道我必须用 text 字段配合分析器(analyzer)吗?可我明明只需要精确匹配,不需要分词啊!”
别急,Elasticsearch 提供了一个专门为 keyword 字段量身定做的工具——Normalizer(规范器)。
什么是 Normalizer?
normalizer 可以看作是 analyzer 的一个简化版,它专门应用于 keyword 字段,在索引时对字段的值进行一系列的规范化处理,但不会进行分词。
它的核心作用是:在保留字段原子性的前提下(即不拆分成多个词元),对整个字符串进行统一的格式化处理。
你可以把它想象成一个『预处理器』,在数据存入索引之前,先把它『打磨』成一个标准格式。
Normalizer 如何工作?
一个 normalizer 主要由以下部分组成(和 analyzer 很像,但有关键区别):
- Character Filters (字符过滤器):可选,0个或多个。在处理的最开始阶段作用于原始字符串,可以进行添加、删除、替换字符等操作。比如,移除 HTML 标签的
html_strip过滤器。 - Token Filters (词元过滤器):可选,0个或多个。注意!虽然叫 Token Filter,但在
normalizer的上下文中,它作用于整个字符串,而不是分词后的词元(因为normalizer不分词)。常见的有lowercase(转小写)、asciifolding(移除变音符号,如é->e)等。
关键区别:normalizer 没有 Tokenizer(分词器)! 这是它和 analyzer 最本质的不同。analyzer 的核心是分词,将文本拆分成多个词元;而 normalizer 始终将输入视为一个单一的词元进行处理。
处理流程如下:
原始 keyword 值 -> [字符过滤器(们)] -> [词元过滤器(们)] -> 最终规范化的 keyword 值 (存入倒排索引)
定义和使用 Normalizer
normalizer 需要在索引的设置(settings)中定义,然后在映射(mapping)中指定给 keyword 字段。
假设我们想实现一个忽略大小写、忽略常见英文变音符号的 keyword 字段,比如用于存储和查询邮箱地址。
第一步:在索引设置中定义 Custom Normalizer
我们需要在创建索引时,通过 analysis 设置来定义一个自定义的 normalizer。我们给它起个名字,比如 my_email_normalizer,并指定它使用的过滤器:lowercase 和 asciifolding。
PUT /my_users
{
"settings": {
"analysis": {
"normalizer": {
"my_email_normalizer": {
"type": "custom",
"char_filter": [],
"filter": ["lowercase", "asciifolding"]
}
}
}
},
"mappings": {
"properties": {
"email": {
"type": "keyword",
"normalizer": "my_email_normalizer"
},
"username": {
"type": "keyword"
}
}
}
}
settings.analysis.normalizer: 定义normalizer的地方。my_email_normalizer: 我们自定义的normalizer的名字。type: "custom": 表明是自定义的。char_filter: []: 这里我们不需要字符过滤器。filter: ["lowercase", "asciifolding"]: 指定使用的词元过滤器,顺序很重要。先转小写,再移除变音符号。mappings.properties.email.normalizer: 在email字段的映射中,通过normalizer属性指定使用我们刚刚定义的my_email_normalizer。username字段则是一个普通的keyword字段,没有应用normalizer,作为对比。
第二步:索引文档
现在我们索引一些文档:
POST /my_users/_doc/1
{
"email": "User1@Example.COM",
"username": "User1"
}
POST /my_users/_doc/2
{
"email": "user2.café@example.net",
"username": "User2.Café"
}
POST /my_users/_doc/3
{
"email": "user3@example.org",
"username": "user3"
}
发生了什么?
当这些文档被索引时,email 字段的值会经过 my_email_normalizer 的处理:
User1@Example.COM->lowercase->user1@example.com->asciifolding->user1@example.comuser2.café@example.net->lowercase->user2.café@example.net->asciifolding->user2.cafe@example.netuser3@example.org->lowercase->user3@example.org->asciifolding->user3@example.org
最终,Elasticsearch 的倒排索引中存储的是这些规范化之后的值:user1@example.com, user2.cafe@example.net, user3@example.org。
而 username 字段因为没有 normalizer,所以存储的是原始值:User1, User2.Café, user3。
Normalizer 对查询的影响:Term Query vs Match Query
理解 normalizer 如何影响查询行为至关重要,特别是 term 查询和 match 查询。
1. term 查询
term 查询用于查找未经分析的精确词元。这意味着,当你在一个应用了 normalizer 的 keyword 字段上使用 term 查询时,你提供的查询词 必须 与索引中存储的 规范化后 的值完全匹配! normalizer 不会 自动应用于 term 查询的查询文本本身。
能匹配的情况:
GET /my_users/_search { "query": { "term": { "email": "user1@example.com" } } }这个查询会成功找到文档 1,因为查询词
user1@example.com和索引中存储的规范化值完全一致。GET /my_users/_search { "query": { "term": { "email": "user2.cafe@example.net" } } }这个查询也会成功找到文档 2,因为查询词
user2.cafe@example.net和索引中存储的规范化值完全一致。不能匹配的情况:
GET /my_users/_search { "query": { "term": { "email": "User1@Example.COM" } } }失败! 因为查询词
User1@Example.COM没有经过规范化,与索引中的user1@example.com不匹配。GET /my_users/_search { "query": { "term": { "email": "user2.café@example.net" } } }失败! 因为查询词
user2.café@example.net包含变音符号,没有经过规范化,与索引中的user2.cafe@example.net不匹配。
思考一下为什么会这样? term 查询的设计目标就是查找底层倒排索引中的精确词元。normalizer 只在索引时修改了存入倒排索引的值,它并不改变 term 查询本身的行为(即直接使用你给的查询词去索引里查找)。所以,你需要自己确保 term 查询的词是规范化后的形式。
这看起来有点反直觉,对吧?我用了 normalizer 不就是为了忽略大小写和变音符号吗,怎么查询的时候还得自己处理?别急,看 match 查询。
2. match 查询
match 查询通常用于全文搜索 (text 字段),它会对查询文本应用相同的分析器(analyzer),然后再去匹配索引中的词元。
那么,当 match 查询作用于一个带有 normalizer 的 keyword 字段时会发生什么呢?
一个有趣的现象发生了:Elasticsearch 会将该字段的 normalizer 应用于查询文本! 这意味着 match 查询可以自动处理大小写和变音符号等问题,实现我们期望的『不拘小节』的精确匹配。
都能匹配的情况:
GET /my_users/_search { "query": { "match": { "email": "User1@Example.COM" } } }成功! ES 会将查询文本
User1@Example.COM通过my_email_normalizer规范化为user1@example.com,然后去匹配索引,找到文档 1。GET /my_users/_search { "query": { "match": { "email": "user2.café@example.net" } } }成功! ES 会将查询文本
user2.café@example.net通过my_email_normalizer规范化为user2.cafe@example.net,然后去匹配索引,找到文档 2。GET /my_users/_search { "query": { "match": { "email": "user3@example.org" } } }成功! 查询文本
user3@example.org规范化后仍然是user3@example.org,找到文档 3。
对比一下没有 normalizer 的 username 字段:
GET /my_users/_search
{
"query": {
"match": {
"username": "user1"
}
}
}
失败! 因为 username 是普通 keyword,没有 normalizer,match 查询也不会对其进行任何处理,直接用 user1 去匹配索引里的 User1,自然找不到。
GET /my_users/_search
{
"query": {
"term": {
"username": "User1"
}
}
}
成功! term 查询精确匹配索引中的 User1。
总结一下查询行为:
| 字段类型 | 查询类型 | 查询文本处理方式 | 匹配逻辑 |
|---|---|---|---|
keyword (无 Normalizer) |
term |
不处理 | 查询文本 == 索引值 |
keyword (无 Normalizer) |
match |
不处理 | 查询文本 == 索引值 |
keyword (有 Normalizer) |
term |
不处理 | 查询文本 == 规范化后的索引值 |
keyword (有 Normalizer) |
match |
应用 Normalizer 规范化查询文本 | 规范化后的查询文本 == 规范化后的索引值 |
text (有 Analyzer) |
term |
不处理 | 查询文本 == 某个分词后的词元 |
text (有 Analyzer) |
match |
应用 Analyzer 分析查询文本 | 分析后的查询词元之一 == 某个分词后的索引词元 |
所以,如果你想在应用了 normalizer 的 keyword 字段上进行忽略大小写/变音符号等的精确匹配,使用 match 查询通常是更方便、更符合直觉的选择。 如果你坚持使用 term 查询,那么你需要确保你的应用程序在查询前,手动将查询词进行与 normalizer 相同的规范化处理。
normalizer vs analyzer:再次强调
虽然它们都用到 char_filter 和 token_filter,但核心区别在于:
normalizer:- 用于
keyword字段。 - 没有分词器 (Tokenizer)。
- 处理结果是一个规范化后的词元 (token)。
- 目的是在不分词的前提下,对整个值进行格式统一,以支持更灵活的精确匹配、聚合和排序。
- 用于
analyzer:- 用于
text字段。 - 必须有分词器 (Tokenizer)。
- 处理结果是零个或多个词元 (token)。
- 目的是将文本拆分成词元,并进行处理(如转小写、去停用词、词干提取等),以支持全文检索。
- 用于
简单说:normalizer 是为了让 keyword 更『宽容』一点,而 analyzer 是为了让 text 能够被『理解』和搜索。
常见用例
- 大小写不敏感的标签/分类/状态码匹配与聚合:比如标签
Java,java,JAVA能被视为同一个。 - 邮箱地址、用户名查询:忽略大小写。
- URL 规范化:比如统一协议为小写,移除末尾斜杠等(可能需要自定义
char_filter或token_filter)。 - 需要忽略口音/变音符号的精确匹配场景:比如人名、地名。
注意事项
- 只在索引时生效:
normalizer的处理发生在文档写入索引时。一旦定义并应用到字段上,它只对之后新索引或更新的文档生效。如果想对已有数据生效,需要进行 reindex 操作。 - 选择合适的过滤器:根据你的具体需求选择
char_filter和token_filter。不是越多越好,过度规范化可能会导致意想不到的冲突。 - 对聚合的影响:
terms聚合也会作用于规范化后的值。这意味着User1@Example.COM和user1@example.com会被聚合到同一个桶user1@example.com中,这通常是我们想要的结果。 - 性能考虑:
normalizer的处理会增加索引时的一点点开销,但通常非常小,对于keyword字段的查询和聚合性能往往有正面影响,因为它减少了需要精确匹配的唯一词项数量。 - 无法通过
_analyzeAPI 测试:_analyzeAPI 是用来测试analyzer的,不能直接用来测试normalizer的完整效果。你需要通过索引实际数据和执行查询来验证其行为。
结语
Elasticsearch 的 normalizer 是一个强大而精妙的工具,它填补了标准 keyword 的严格精确匹配和 text 字段分词分析之间的空白。当你需要在 keyword 字段上实现不区分大小写、忽略变音符号或其他形式的规范化精确匹配、聚合或排序时,normalizer 就是你的不二之选。
记住它和 analyzer 的核心区别(无分词器),以及它对 term 和 match 查询的不同影响,你就能有效地利用 normalizer 让你的 Elasticsearch 查询更加灵活和智能!