概述
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" } } } }
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" } } } }
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 进行进一步处理。
核心特点:
| 特性 | Analyzer | Normalizer |
|---|
| 作用对象 | 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"] } } } }, "mappings": { "properties": { "city": { "type": "keyword", "normalizer": "my_normalizer" } } } }
|
配置说明:
| 参数 | 说明 | 示例 |
|---|
| type | normalizer类型 | custom |
| char_filter | 字符过滤器列表 | [] |
| filter | Token过滤器列表 | ["lowercase"] |
可用过滤器
常用 Token 过滤器:
| 过滤器 | 功能 | 示例 |
|---|
| lowercase | 转换为小写 | Apple → apple |
| uppercase | 转换为大写 | Apple → APPLE |
| asciifolding | 转换非ASCII字符 | café → cafe |
| trim | 去除首尾空格 | test → test |
组合使用示例:
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" } } }
|
测试不同大小写:
1 2 3 4 5
| "city": "new york" "city": "New York" "city": "NEW YORK" "city": "NeW yOrK"
|
步骤 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" } } }
GET /index/_search { "query": { "term": { "name.keyword": "IPHONE" } } }
GET /index/_search { "query": { "term": { "name.raw": "iPhone" } } }
|
自定义 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
| PUT /my_index { "settings": { "analysis": { "normalizer": { "my_normalizer": { "type": "custom", "filter": ["lowercase"] } } } }, "mappings": { "properties": { "field_name": { "type": "keyword", "normalizer": "my_normalizer" } } } }
GET /my_index/_search { "query": { "term": { "field_name": "any case" } } }
|