4、ES数据模型:文档、映射、字段类型、动态映射
好,咱们今天聊聊ES的数据模型。说实话,很多新手刚接触ElasticSearch时,最容易犯的错就是把它当成普通数据库来用。我当年也踩过这个坑——把ES当MySQL使,结果查询性能惨不忍睹。后来才明白,ES的数据模型设计,直接决定了你的搜索体验和集群稳定性。
4.1 文档(Document):ES里的“一行数据”
文档是ES中最基本的数据单元。你可以把它理解成JSON对象,但又不完全是。
文档长什么样?
{
"_index": "users",
"_id": "1",
"_source": {
"name": "张三",
"age": 28,
"bio": "资深Java开发,热爱开源"
}
}
每个文档都有几个关键属性:
- _index:文档属于哪个索引,相当于数据库里的表名
- _id:文档的唯一标识,你可以自己指定,也可以让ES自动生成
- _source:真正的数据本体,就是那个JSON对象
文档有几个重要特性:
- 不可变性:文档一旦写入,就不能部分修改。你看到的“更新”操作,其实是先删除旧文档,再写入新文档
- 扁平化结构:ES内部会把JSON拍平。比如嵌套的
{"address": {"city": "北京"}}会变成address.city: 北京 - 元数据:除了_source,每个文档还携带_version(版本号)、_seq_no(序列号)等元数据,用于并发控制
4.2 映射(Mapping):定义文档的“骨架”
映射就是ES里的“表结构定义”。它告诉ES:你的文档里有哪些字段,每个字段是什么类型,该怎么索引和存储。
映射的核心作用:
- 定义字段名称和类型
- 控制字段是否被索引(能不能搜到)
- 控制字段是否存储(能不能返回)
- 指定分词器(中文分词还是英文分词)
举个例子,创建一个带映射的索引:
PUT /my_index
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"price": {
"type": "float"
},
"created_at": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
}
}
这里我定义了三个字段:title是文本类型,用IK分词器;price是浮点数;created_at是日期,指定了格式。
4.3 字段类型详解
ES的字段类型非常丰富,但日常开发中,最常用的就这几种。咱们一个一个说。
4.3.1 text 类型
text类型用于全文搜索。它会对内容进行分词,然后建立倒排索引。
特点:
- 支持模糊搜索、短语搜索、高亮显示
- 不能用于聚合、排序(除非开启fielddata)
- 需要指定分词器
{
"description": {
"type": "text",
"analyzer": "standard", // 标准分词器,按空格和标点切分
"search_analyzer": "standard" // 搜索时的分词器
}
}
4.3.2 keyword 类型
keyword类型用于精确匹配、聚合、排序。它不会分词,整个字符串作为一个词条存入倒排索引。
适用场景:
- ID、邮箱、标签、状态码
- 需要做terms聚合的字段
- 需要排序的字段
{
"email": {
"type": "keyword"
},
"status": {
"type": "keyword"
}
}
keyword类型有一个重要参数:ignore_above。它控制超过多长的字符串会被忽略。默认是2147483647(几乎不限),但实际使用中,我建议设置一个合理值,比如256或512,避免超大字符串撑爆内存。
4.3.3 date 类型
date类型用于存储日期和时间。ES内部会把日期转换成毫秒时间戳(long类型)存储。
支持的格式:
- 字符串:
"2024-01-15"、"2024/01/15 10:30:00" - 时间戳:
1705300000000(毫秒) - 自定义格式:通过
format参数指定
{
"publish_date": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||epoch_millis"
}
}
这里用了双竖线||,表示支持多种格式。ES会依次尝试解析。
"2024-01-15",有的用"2024/01/15",还有的用时间戳。结果做范围查询时,ES解析失败,数据丢失。后来我强制所有数据都用yyyy-MM-dd'T'HH:mm:ss'Z'(ISO 8601格式),再也没出过问题。
4.3.4 nested 类型
nested类型是ES里处理“数组中的对象”的解决方案。为什么需要它?因为ES默认会把JSON对象拍平。
举个例子:
// 原始数据
{
"user": "张三",
"orders": [
{"product": "手机", "price": 5000},
{"product": "耳机", "price": 200}
]
}
// ES内部拍平后
{
"user": "张三",
"orders.product": ["手机", "耳机"],
"orders.price": [5000, 200]
}
看到了吗?拍平后,orders.product和orders.price变成了两个独立的数组。如果你搜索“手机且价格=200”,它会匹配到——因为“手机”在product数组里,“200”在price数组里,但这两个值其实不属于同一个订单。这就是对象关系丢失问题。
nested类型怎么解决?
{
"mappings": {
"properties": {
"user": {"type": "keyword"},
"orders": {
"type": "nested",
"properties": {
"product": {"type": "keyword"},
"price": {"type": "integer"}
}
}
}
}
}
使用nested类型后,ES会把每个内部对象独立存储,保持它们之间的关联。查询时要用nested查询:
GET /my_index/_search
{
"query": {
"nested": {
"path": "orders",
"query": {
"bool": {
"must": [
{"term": {"orders.product": "手机"}},
{"term": {"orders.price": 200}}
]
}
}
}
}
}
4.4 动态映射:ES的“自动类型推断”
动态映射是ES的一大特色。你不需要事先定义映射,直接写入数据,ES会自动推断字段类型。
动态映射的规则:
| JSON类型 | ES推断类型 |
|---|---|
| 字符串(可解析为日期) | date |
| 字符串(数字格式) | float 或 long |
| 普通字符串 | text + keyword(多字段) |
| 整数 | long |
| 浮点数 | float |
| 布尔值 | boolean |
| 对象 | object |
| 数组 | 取第一个元素的类型 |
动态映射的三种模式:
- true(默认):自动检测并添加新字段
- false:忽略新字段,不索引也不存储
- strict:遇到新字段直接报错,拒绝写入
PUT /my_index
{
"mappings": {
"dynamic": "strict", // 严格模式
"properties": {
"name": {"type": "text"}
}
}
}
strict模式。为什么?因为动态映射太“聪明”了,聪明到会犯错。我遇到过:一个字段本来存的是字符串"123",后来某条数据存了数字123,ES自动把字段类型从text改成了long,导致之前的查询全部失效。这种问题排查起来非常痛苦。
动态映射的模板(Dynamic Templates):
如果你既想享受动态映射的便利,又想控制字段类型,可以用动态模板。比如:
PUT /my_index
{
"mappings": {
"dynamic_templates": [
{
"strings_as_keyword": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
},
{
"longs_as_integer": {
"match_mapping_type": "long",
"mapping": {
"type": "integer"
}
}
}
]
}
}
这个模板把所有字符串都映射成keyword,所有长整型都映射成integer。这样既保留了动态映射的灵活性,又避免了类型推断错误。
4.5 总结与最佳实践
好了,咱们把ES数据模型的核心内容过了一遍。最后给你几个我总结的“铁律”:
- 先设计映射,再写入数据。别偷懒用动态映射,除非是临时测试
- text和keyword要分清楚。需要搜索的用text,需要精确匹配和聚合的用keyword
- 日期格式要统一。推荐ISO 8601格式,兼容性好
- nested类型慎用。能用扁平结构就别嵌套,性能差距很大
- 动态映射用strict模式。宁可报错,也别让ES自作主张
记住一句话:ES的数据模型设计,决定了你未来三年的运维成本。设计时多花一小时,运维时少花一周。嗯,今天就到这里,下节课咱们聊聊ES的索引生命周期管理。