在 Elasticsearch (ES) 的世界里,keyword
字段类型是用于存储那些不需要分词、需要精确匹配的文本,比如标签、状态码、用户名、邮箱地址等等。它就像一个严谨的守门员,只有一模一样的值才能通过。
但有时候,这种『严谨』反而会带来麻烦。比如,你想根据用户输入的邮箱 USER@EXAMPLE.COM
查找对应的用户文档,而数据库里存的是 user@example.com
。标准的 keyword
字段会告诉你:“抱歉,找不到!” 因为大小写不同,在它眼里就是两个完全不同的字符串。
类似的情况还有很多:
- 大小写不敏感搜索:
Apple
vsapple
- 忽略口音/变音符号:
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.com
user2.café@example.net
->lowercase
->user2.café@example.net
->asciifolding
->user2.cafe@example.net
user3@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
字段的查询和聚合性能往往有正面影响,因为它减少了需要精确匹配的唯一词项数量。 - 无法通过
_analyze
API 测试:_analyze
API 是用来测试analyzer
的,不能直接用来测试normalizer
的完整效果。你需要通过索引实际数据和执行查询来验证其行为。
结语
Elasticsearch 的 normalizer
是一个强大而精妙的工具,它填补了标准 keyword
的严格精确匹配和 text
字段分词分析之间的空白。当你需要在 keyword
字段上实现不区分大小写、忽略变音符号或其他形式的规范化精确匹配、聚合或排序时,normalizer
就是你的不二之选。
记住它和 analyzer
的核心区别(无分词器),以及它对 term
和 match
查询的不同影响,你就能有效地利用 normalizer
让你的 Elasticsearch 查询更加灵活和智能!