概述

Elasticsearch 中不同字段类型对大小写的处理方式不同。text 类型默认不区分大小写,而 keyword 类型默认区分大小写。通过 normalizer 可以实现 keyword 类型的大小写不敏感查询。

核心内容:

  • 🔤 分词器大小写处理机制
  • 🔑 Keyword 类型的局限性
  • ⚙️ Normalizer 原理和使用
  • 📊 实战案例演示

适用场景:

  • 需要精确匹配但忽略大小写
  • 聚合查询时统一大小写
  • 城市、国家等枚举值查询

默认行为

Standard 分词器(Text 类型)

特点: 默认不区分大小写

处理机制:

1
2
存储时:大写字符 → 自动转换为小写
查询时:自动转换为小写匹配

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 索引创建
PUT /products
{
"mappings": {
"properties": {
"name": {
"type": "text" // 使用 text 类型
}
}
}
}

// 写入数据
POST /products/_doc
{
"name": "iPhone 15 Pro"
}

// 查询(任意大小写都能匹配)
GET /products/_search
{
"query": {
"match": {
"name": "iphone" // ✅ 可以匹配
}
}
}

GET /products/_search
{
"query": {
"match": {
"name": "IPHONE" // ✅ 可以匹配
}
}
}

Keyword 类型

特点: 精确匹配,默认区分大小写

处理机制:

1
2
存储时:原样存储(不转换)
查询时:必须完全匹配(包括大小写)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 索引创建
PUT /products
{
"mappings": {
"properties": {
"brand": {
"type": "keyword" // 使用 keyword 类型
}
}
}
}

// 写入数据
POST /products/_doc
{
"brand": "Apple"
}

// 查询
GET /products/_search
{
"query": {
"term": {
"brand": "Apple" // ✅ 可以匹配
}
}
}

GET /products/_search
{
"query": {
"term": {
"brand": "apple" // ❌ 无法匹配(大小写不同)
}
}
}

问题: Keyword 无法实现大小写不敏感的查询

Normalizer 解决方案

什么是 Normalizer

官方定义:

Normalizer 是 keyword 的一个属性,类似 analyzer 分词器的功能,但不同之处在于:它对 keyword 生成的单一 Term 进行进一步处理。

核心特点:

特性AnalyzerNormalizer
作用对象Text 类型Keyword 类型
分词会分词不分词
Token数量多个单个
使用场景全文搜索精确匹配

工作时机:

1
2
索引阶段:数据写入时应用 normalizer
查询阶段:match/term 查询时应用 normalizer

基本配置

创建索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PUT /my_index
{
"settings": {
"analysis": {
"normalizer": {
"my_normalizer": {
"type": "custom",
"char_filter": [], // 字符过滤器(可选)
"filter": ["lowercase"] // Token过滤器
}
}
}
},
"mappings": {
"properties": {
"city": {
"type": "keyword",
"normalizer": "my_normalizer" // 应用 normalizer
}
}
}
}

配置说明:

参数说明示例
typenormalizer类型custom
char_filter字符过滤器列表[]
filterToken过滤器列表["lowercase"]

可用过滤器

常用 Token 过滤器:

过滤器功能示例
lowercase转换为小写Appleapple
uppercase转换为大写AppleAPPLE
asciifolding转换非ASCII字符cafécafe
trim去除首尾空格testtest

组合使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"settings": {
"analysis": {
"normalizer": {
"my_normalizer": {
"type": "custom",
"filter": [
"lowercase",
"asciifolding",
"trim"
]
}
}
}
}
}

实战案例

场景:城市名称查询

需求: 城市名称不区分大小写,支持精确匹配和聚合

步骤 1:创建索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PUT /cities
{
"settings": {
"analysis": {
"normalizer": {
"city_normalizer": {
"type": "custom",
"filter": ["lowercase"]
}
}
}
},
"mappings": {
"properties": {
"city": {
"type": "keyword",
"normalizer": "city_normalizer"
}
}
}
}

步骤 2:写入测试数据

1
2
3
4
5
6
7
8
9
10
11
POST /cities/_bulk
{"index":{"_id":1}}
{"city": "New York"}
{"index":{"_id":2}}
{"city": "new York"}
{"index":{"_id":3}}
{"city": "New york"}
{"index":{"_id":4}}
{"city": "NEW YORK"}
{"index":{"_id":5}}
{"city": "Seattle"}

存储结果: 所有 "New York" 的变体都被规范化为 "new york"

步骤 3:精确查询

1
2
3
4
5
6
7
8
9
10
GET /cities/_search
{
"query": {
"term": {
"city": "new york" // 任意大小写都能匹配
}
}
}

// 结果:返回 ID 1, 2, 3, 4 的文档

测试不同大小写:

1
2
3
4
5
// 全部可以匹配到相同的结果
"city": "new york" // ✅ 匹配 4 条
"city": "New York" // ✅ 匹配 4 条
"city": "NEW YORK" // ✅ 匹配 4 条
"city": "NeW yOrK" // ✅ 匹配 4 条

步骤 4:聚合查询

1
2
3
4
5
6
7
8
9
10
11
GET /cities/_search
{
"size": 0,
"aggs": {
"cities": {
"terms": {
"field": "city"
}
}
}
}

聚合结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"aggregations": {
"cities": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "new york", // 统一为小写
"doc_count": 4 // 合并计数
},
{
"key": "seattle",
"doc_count": 1
}
]
}
}
}

效果: 无论原始数据大小写如何,聚合时都统一为 "new york"

应用场景

适用场景

场景说明示例
枚举值查询状态码、类型等ACTIVE / active
地理位置城市、国家名称Beijing / BEIJING
标签系统用户标签Java / java
分类名称商品分类Electronics / electronics

对比方案

三种方案对比:

方案大小写聚合性能使用场景
Text + 标准分词不敏感❌ 不支持全文搜索
Keyword敏感✅ 支持最高精确匹配
Keyword + Normalizer不敏感✅ 支持推荐方案

高级用法

多字段映射

同时支持精确匹配和模糊搜索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"mappings": {
"properties": {
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
},
"raw": {
"type": "keyword" // 保留原始大小写
}
}
}
}
}
}

使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 全文搜索
GET /index/_search
{
"query": {
"match": {
"name": "iphone" // 使用 text 类型
}
}
}

// 大小写不敏感精确匹配
GET /index/_search
{
"query": {
"term": {
"name.keyword": "IPHONE" // 使用 normalizer
}
}
}

// 大小写敏感精确匹配
GET /index/_search
{
"query": {
"term": {
"name.raw": "iPhone" // 使用原始 keyword
}
}
}

自定义 Normalizer

复杂场景配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"settings": {
"analysis": {
"normalizer": {
"email_normalizer": {
"type": "custom",
"char_filter": [],
"filter": [
"lowercase",
"asciifolding", // 转换特殊字符
"trim" // 去除空格
]
}
}
}
},
"mappings": {
"properties": {
"email": {
"type": "keyword",
"normalizer": "email_normalizer"
}
}
}
}

最佳实践

使用建议

推荐做法:

建议说明
枚举值使用 normalizer统一大小写,方便聚合
保留原始字段使用多字段映射保留原始值
测试验证确认 normalizer 生效
文档规范明确字段大小写处理规则

注意事项:

注意点说明
⚠️ 索引已存在需要 reindex 才能应用 normalizer
⚠️ 性能影响复杂 normalizer 会略微影响性能
⚠️ 不支持分词Normalizer 不能用于 text 类型

验证方法

测试 Normalizer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /cities/_analyze
{
"normalizer": "city_normalizer",
"text": "New York"
}

// 结果
{
"tokens": [
{
"token": "new york",
"start_offset": 0,
"end_offset": 8,
"type": "word",
"position": 0
}
]
}

总结

核心要点:

类型分词大小写聚合适用场景
Text不敏感全文搜索
Keyword敏感精确匹配
Keyword + Normalizer不敏感推荐

快速使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 1. 创建带 normalizer 的索引
PUT /my_index
{
"settings": {
"analysis": {
"normalizer": {
"my_normalizer": {
"type": "custom",
"filter": ["lowercase"]
}
}
}
},
"mappings": {
"properties": {
"field_name": {
"type": "keyword",
"normalizer": "my_normalizer"
}
}
}
}

// 2. 查询时忽略大小写
GET /my_index/_search
{
"query": {
"term": {
"field_name": "any case" // 任意大小写
}
}
}