ES_映射
一、 映射(Mapping)是什么?
简单来说,映射就像是关系型数据库中的表结构定义(Schema)。它定义了索引(Index)中的文档(Document)可以包含哪些字段(Field),以及每个字段的数据类型、格式、分词方式、是否索引、是否存储等属性。
核心思想: Elasticsearch是一个** schema-on-write **(写时定义模式)的系统。这意味着在数据被写入之前,最好就明确其结构,而不是像MongoDB这类 NoSQL 数据库是 schema-on-read
(读时解析模式)。预先定义好映射,可以让ES更高效地处理、索引和存储数据。
映射的三大组成部分:
- 字段类型(Field datatypes): 定义字段的数据类型,如
text
,keyword
,long
,integer
,date
,boolean
,nested
,object
,geo_point
等。 - 元字段(Metadata fields): 用于处理文档的元信息,如
_index
,_id
,_source
。 - 映射参数(Mapping parameters): 精细化控制字段的索引和存储行为,如
index
,analyzer
,copy_to
,fields
等。
二、 为什么映射如此重要?(架构师视角)
-
性能优化(Performance):
- 正确的数据类型(如使用
keyword
而非text
进行精确匹配和聚合)可以大幅提升查询和聚合速度。 - 避免不必要的字段索引(
"index": false
)可以节省磁盘空间和内存,减少索引大小,提升写入和检索效率。
- 正确的数据类型(如使用
-
存储效率(Storage Efficiency):
- 选择合适的类型(如
integer
比long
更省空间)和开启索引压缩(如doc_values
)可以减少磁盘占用。
- 选择合适的类型(如
-
功能正确性(Functionality Correctness):
- 错误的映射会导致查询结果不符合预期。例如,一个本应做全文搜索的字段被错误地设置为
keyword
类型,将无法被分词搜索;一个日期字段被存为text
类型,将无法进行时间范围查询。
- 错误的映射会导致查询结果不符合预期。例如,一个本应做全文搜索的字段被错误地设置为
-
避免后期重构(Avoiding Reindexing):
- 映射一旦确定,虽然可以添加新字段,但不允许修改现有字段的类型。如果后期需要修改,必须进行
_reindex
操作(创建一个新索引并重建数据),这对于大数据量的集群来说成本极高。预先精心设计映射可以避免这种痛苦的重构过程。
- 映射一旦确定,虽然可以添加新字段,但不允许修改现有字段的类型。如果后期需要修改,必须进行
三、 映射的核心概念详解
1. 字段类型(Field Data Types)
-
核心类型:
text
: 用于全文搜索的字符串类型,会被分词器(Analyzer)切分成倒排索引。适用于内容描述、正文等需要被搜索的文本。keyword
: 用于精确值过滤、排序和聚合的字符串类型,不会被分词。适用于状态码、标签、姓名、邮箱等。date
: 日期类型,可以指定格式。long
,integer
,short
,byte
,double
,float
: 数值类型。boolean
: 布尔类型。binary
: 二进制类型。range
(如integer_range
,date_range
): 范围类型。
-
复杂类型:
object
: 用于处理单个JSON对象。nested
: 非常重要的类型。用于处理对象数组,且需要数组中的对象被独立索引和查询。普通object
数组中的对象在Lucene底层会被扁平化,导致跨对象的查询出现逻辑错误。nested
类型通过为数组中的每个对象创建独立的隐藏文档来解决这个问题。flattened
: 将整个子对象或数组映射为一个字段,其值被存储为keyword
类型。适用于不确定子结构且不需要深层查询的场景,是nested
的一种轻量级替代方案。
-
专用类型:
geo_point
: 存储经纬度坐标,用于地理位置搜索和距离计算。ip
: 存储IPv4/IPv6地址,支持IP范围查询。completion
: 用于实现自动补全(Suggesters)功能。
2. 关键映射参数(Mapping Parameters)
index
: 控制字段是否被索引。默认为true
。如果设置为false
,则该字段不可被搜索,但仍会出现在_source
中。analyzer
: 指定在索引和搜索时用于text
字段的分词器。如standard
(默认),ik_smart
,ik_max_word
(中文常用),english
。search_analyzer
: 指定在搜索时使用的分词器,默认与analyzer
一致。fields
: 多字段(Multi-fields)特性。允许对同一个字符串值以不同的方式索引多次,实现不同的目的。这是映射设计的精髓之一。- 例如:一个
product_name
字段可以同时被定义为text
类型(用于全文搜索)和keyword
类型(用于精确聚合和排序)。
查询时,使用"product_name": {"type": "text","analyzer": "ik_max_word","fields": {"raw": { // 定义一个名为 raw 的子字段,类型为 keyword"type": "keyword"}} }
product_name
进行全文搜索,使用product_name.raw
进行精确匹配或聚合。- 例如:一个
copy_to
: 将多个字段的值复制到一个组字段中,实现类似_all
的跨字段搜索(ES7已移除_all
)。doc_values
: 默认开启。为keyword
,numeric
,date
,geo
等类型生成一个列式存储结构,用于排序、聚合和脚本访问。消耗磁盘空间,但极大提升聚合性能。dynamic
: 控制是否动态添加新字段。策略包括:true
(默认): 自动添加新字段。false
: 忽略新字段(不会被索引,但会出现在_source
中)。strict
: 遇到新字段时抛出异常,拒绝文档写入。生产环境推荐使用strict
或false
以避免“映射爆炸”(mapping explosion)。
四、 结合实际案例:电商平台商品搜索
让我们以一个典型的电商平台商品数据模型为例,来设计其ES映射。
1. 需求分析
我们需要存储和搜索商品信息,核心功能包括:
- 按商品名称、描述、分类进行全文搜索。
- 按品牌、分类、店铺进行精确筛选和聚合。
- 按价格、销量、上架时间进行排序和范围过滤。
- 按商品属性(如颜色、尺寸、CPU型号)进行动态筛选和聚合。
- 根据用户地理位置推荐附近的商品(基于店铺地址)。
2. 映射设计
我们将创建一个名为 products
的索引,并预先定义其映射。
PUT /products
{"settings": {"number_of_shards": 3,"number_of_replicas": 1,"analysis": {"analyzer": {"my_ik_analyzer": {"type": "custom","tokenizer": "ik_max_word"}}}},"mappings": {"dynamic": "strict", // 严格模式,防止未知字段污染映射"properties": {"id": {"type": "keyword"},"name": {"type": "text","analyzer": "my_ik_analyzer", // 使用IK中文分词器"fields": {"raw": {"type": "keyword" // 用于精确匹配,如作为聚合键}},"copy_to": "full_text" // 复制到全文搜索字段},"description": {"type": "text","analyzer": "my_ik_analyzer","copy_to": "full_text"},"brand": {"type": "keyword" // 品牌是典型的精确值字段},"category_id": {"type": "integer" // 分类ID用于关联查询,整数类型更高效},"category_path": {"type": "keyword" // 存储分类路径,如 "家电/空调/壁挂式"},"price": {"type": "scaled_float", // 缩放浮点型,避免浮点数精度问题"scaling_factor": 100},"sales_volume": {"type": "integer"},"is_on_shelf": {"type": "boolean"},"listing_time": {"type": "date","format": "epoch_millis" // 使用时间戳格式存储},"tags": {"type": "keyword" // 标签,用于精确过滤和聚合},"specs": { // 商品规格(动态属性),如 {"color": "红色", "size": "XL"}"type": "flattened" // 使用flattened类型,避免映射爆炸,且支持基本的键值查询},"store_info": { // 店铺信息,是一个对象"type": "object","properties": {"id": {"type": "keyword"},"name": {"type": "text","analyzer": "my_ik_analyzer","fields": {"raw": {"type": "keyword"}}},"location": { // 店铺地理位置"type": "geo_point"}}},"full_text": { // 用于跨字段全文搜索的组字段"type": "text","analyzer": "my_ik_analyzer"}}}
}
3. 使用方法与查询示例
a. 插入数据
POST /products/_doc/1001
{"id": "1001","name": "Apple iPhone 15 Pro Max 256GB 原色钛金属","description": "全新A17 Pro芯片,钛金属设计,强悍性能与卓越影像能力。","brand": "Apple","category_id": 1,"category_path": "手机通讯/手机/苹果手机","price": 8999.00,"sales_volume": 1500,"is_on_shelf": true,"listing_time": 1696118400000,"tags": ["新品", "优惠", "免息"],"specs": {"color": "原色钛金属","memory": "256GB","network": "5G"},"store_info": {"id": "s100","name": "Apple官方旗舰店","location": {"lat": 39.9042,"lon": 116.4074}}
}
b. 复杂查询示例
-
全文搜索 + 过滤 + 聚合:搜索包含“苹果手机”且价格在5000-10000元之间的商品,并按品牌聚合。
GET /products/_search {"query": {"bool": {"must": [{"match": {"full_text": "苹果手机"}}],"filter": [{"range": {"price": {"gte": 5000,"lte": 10000}}},{"term": {"is_on_shelf": true}}]}},"aggs": {"brands": {"terms": {"field": "brand"}}},"sort": [{"sales_volume": {"order": "desc"}}] }
-
基于地理位置的查询:查找距离我(经纬度[116.40, 39.90])10公里以内的店铺的商品。
GET /products/_search {"query": {"bool": {"filter": [{"geo_distance": {"distance": "10km","store_info.location": {"lat": 39.90,"lon": 116.40}}}]}} }
-
查询特定规格:查询内存为“256GB”的商品。
GET /products/_search {"query": {"match": {"specs": "256GB" // Flattened字段支持简单的匹配查询}} }
五、 最佳实践与总结
- 预先定义映射:切忌依赖动态映射。对于核心业务索引,务必在写入数据前通过
PUT /my_index { "mappings": { ... } }
创建好映射。 - 善用
keyword
和text
:明确字段是用来全文搜索还是精确匹配。绝大多数字符串字段都应使用fields
配置为text
+keyword
多字段。 - 谨慎使用
nested
:nested
类型查询性能开销较大,仅在确实需要独立查询数组内对象时使用。如果关系简单,考虑使用flattened
。 - 控制映射大小:使用
dynamic: strict
或false
,避免无效字段无限增长。一个索引中字段过多(例如超过1000)会影响集群性能。 - 规划索引策略:结合ILM(Index Lifecycle Management),根据时间或数据量进行滚动索引(Rollover),这也能在一定程度上缓解映射不可修改的问题(因为新索引可以用新映射)。
- 进行映射评审:像评审数据库表结构一样,将重要的ES索引映射设计纳入技术评审流程。