这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 本章把 Elasticsearch 的核心概念串成一条线:集群由节点组成,节点承载分片,分片是 Lucene 索引,索引用 Mapping 描述字段,字段决定了文档如何被索引和查询。
理解这条线后,后面学习 CRUD、Query DSL、分片策略和性能调优都会顺畅很多。
2.1 集群与节点
集群是一组 Elasticsearch 进程的集合,由 cluster.name 标识。每个进程是一个节点,由 node.name 标识。
cluster.name: es-learning
node.name: node-1
network.host: 0.0.0.0
节点类型:
| 节点角色 | 说明 |
|---|---|
master |
参与主节点选举,维护集群元数据 |
data |
保存分片,执行写入、搜索和聚合 |
data_content |
保存内容型数据,适合低更新、查询频繁索引 |
data_hot |
保存热数据,高 IO 与 CPU |
data_warm |
保存温数据,查询仍频繁但写入少 |
data_cold |
保存冷数据,读少、可接受较慢 |
data_frozen |
保存冻结数据,部分依赖可搜索快照 |
ingest |
执行 ingest pipeline 数据预处理 |
coordinating |
接收请求、分发分片请求、归并结果 |
ml |
执行机器学习作业 |
transform |
执行转换任务 |
任何节点都可以承担 coordinating 职责。即使没有指定角色的节点,也能接收请求并协调分片执行。
2.2 Index、Document 与 Field
2.2.1 Index
Index 是文档的逻辑集合,类似关系型数据库中的表。
示例:
products
orders
applogs-2026.08.25
user-events-2026.08
日志场景常按天或月建索引:
applogs-2026.08.25
applogs-2026.08.26
业务搜索场景常使用一个较大的主索引加别名:
products_v1
products_v2
alias: products
2.2.2 Document
Document 是一条 JSON 文档,必须属于某个 Index。
{
"product_id": 10001,
"title": "轻薄高性能笔记本电脑",
"brand": "Nova",
"price": 6999.00,
"status": "ON_SALE",
"tags": ["轻薄", "高刷新率"],
"created_at": "2026-08-25T10:00:00Z"
}
每个文档有元字段:
| 字段 | 说明 |
|---|---|
_index |
所属索引 |
_id |
文档 ID |
_version |
版本号,用于乐观并发 |
_seq_no |
主分片上的序列号 |
_primary_term |
主分片任期 |
_source |
原始 JSON |
_routing |
分片路由值 |
2.2.3 Field
Field 是文档中的字段。Mapping 决定字段的类型、是否分词、是否聚合、日期格式等。
同一字段在不同索引里可以有不同 Mapping,但在同一个索引中类型必须稳定。
2.3 Mapping
Mapping 类似表结构,但更关注“如何建索引”。
PUT /products
{
"mappings": {
"properties": {
"product_id": { "type": "long" },
"title": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 128 }
}
},
"brand": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"status": { "type": "keyword" },
"tags": { "type": "keyword" },
"created_at": { "type": "date" }
}
}
}
常见字段类型:
| 类型 | 用途 | 能否分词 | 能否聚合 |
|---|---|---|---|
text |
全文搜索 | 是 | 默认不能直接聚合 |
keyword |
精确值、排序、聚合 | 否 | 是 |
long / integer |
数值 | 否 | 是 |
double / float |
数值 | 否 | 是 |
scaled_float |
固定小数精度的数值 | 否 | 是 |
date |
时间 | 否 | 是 |
boolean |
布尔 | 否 | 是 |
object |
JSON 对象 | 按字段处理 | 视字段而定 |
nested |
对象数组,保留内部关系 | 视字段而定 | 支持 nested 聚合 |
dense_vector |
向量检索 | 否 | 专用 kNN |
text 与 keyword 是最容易被误解的类型。手机号、订单号、状态、标签、类目 ID 通常用 keyword;标题、描述、正文通常用 text。
2.4 动态映射
如果不提前定义 Mapping,Elasticsearch 会根据第一次写入的字段值推断类型。
PUT /dynamic-demo/_doc/1
{
"title": "Elasticsearch 书籍",
"price": 89.5,
"tags": ["搜索", "ES"],
"created_at": "2026-08-25T10:00:00Z"
}
查看 Mapping:
GET /dynamic-demo/_mapping
动态映射适合快速实验,但生产环境应显式定义。原因:
- 字符串可能被推断成
text+keyword,不符合业务预期; - 数字字符串可能被推断成
long; - 错误 Mapping 后续不能原地修改;
- 字段无限增长会带来 Mapping 爆炸风险;
- 分词器无法按业务需求自动选择。
可以限制动态行为:
PUT /products
{
"mappings": {
"dynamic": "strict",
"properties": {
"title": { "type": "text" }
}
}
}
dynamic 选项:
| 值 | 行为 |
|---|---|
true |
自动添加字段 |
false |
忽略新字段,文档可写入但不索引该字段 |
runtime |
新字段作为运行时字段处理 |
strict |
写入未知字段直接拒绝 |
2.5 分片与副本
每个索引由一个或多个主分片组成。
products: 3 primary shards, 1 replica each
示意:
P0 P1 P2 主分片
R0 R1 R2 副本分片
主分片数量在索引创建后不能直接修改,只能通过 Reindex 或 Split/Shrink 调整。副本数量可以随时修改。
PUT /products/_settings
{
"index": {
"number_of_replicas": 1
}
}
分片的作用:
- 水平拆分数据;
- 并行执行查询;
- 将数据分布到多个节点;
- 通过副本提升读吞吐和容灾。
分片的代价:
- 每个分片是一个 Lucene 实例,有内存和文件句柄开销;
- 查询需要扇出到更多分片,归并成本上升;
- 分片过多会加重主节点元数据压力;
- 小分片过多会造成磁盘碎文件和合并效率下降;
- 副本会增加磁盘占用和写入放大。
经验建议:
| 指标 | 建议范围 |
|---|---|
| 单分片大小 | 日志类 10-50GB,业务类 20-75GB |
| 单节点分片数 | 小集群低于 600,具体取决于版本和内存 |
| 主分片数 | 按数据规模和写入吞吐规划 |
| 副本数 | 生产至少 1 |
2.6 路由与文档归属
写入文档时,Elasticsearch 根据路由值决定文档进入哪个主分片:
shard = hash(routing) % number_of_primary_shards
默认 routing 是文档 _id。
这意味着:
- 同一文档始终路由到同一主分片;
- 主分片数变化会改变路由结果;
- 自定义路由可以让相关文档落在同一分片;
- 自定义路由后读取也要携带相同 routing;
- 不合理的路由会造成数据倾斜。
自定义路由:
PUT /orders/_doc/10001?routing=user_8888
{
"order_id": 10001,
"user_id": "user_8888",
"amount": 199.00
}
2.7 Segment 与近实时
每个分片内部由多个 segment 组成。segment 是不可变的 Lucene 索引文件,写入后的数据经过 refresh 生成 segment 后才可搜索。
Shard
+-- Segment 0
+-- Segment 1
+-- Segment 2
+-- translog
特点:
- segment 不可修改;
- 删除是标记删除;
- 更新等价于新文档写入加旧文档标记删除;
- segment merge 会合并小段,物理清理删除文档;
- 查询需要扫描多个 segment 并归并结果。
相关机制:
| 机制 | 说明 |
|---|---|
| refresh | 打开新 segment,使文档可搜索 |
| flush | 将 segment 持久化到磁盘并清空 translog |
| translog | 事务日志,用于崩溃恢复 |
| merge | 合并 segment,降低文件数量 |
2.8 Alias
Alias 是索引别名,可以指向一个或多个索引。
POST /_aliases
{
"actions": [
{ "add": { "index": "products_v2", "alias": "products" } },
{ "remove": { "index": "products_v1", "alias": "products" } }
]
}
别名价值:
- 应用不直接依赖物理索引名;
- 支持 Reindex 后零停机切换;
- 一个别名可以查询多个时间索引;
- 写入别名只能指向单个索引;
- 可以配置过滤别名,实现逻辑视图。
过滤别名示例:
POST /_aliases
{
"actions": [
{
"add": {
"index": "products_v2",
"alias": "on-sale-products",
"filter": { "term": { "status": "ON_SALE" } }
}
}
]
}
2.9 Ingest Pipeline
Ingest Pipeline 是写入前的数据预处理管道,可以解析字段、补默认值、转换格式、丰富数据。
PUT /_ingest/pipeline/product-pipeline
{
"processors": [
{
"set": {
"field": "ingested_at",
"value": "{{{_ingest.timestamp}}}"
}
},
{
"lowercase": {
"field": "brand"
}
}
]
}
使用:
PUT /products/_doc/1?pipeline=product-pipeline
{
"title": "轻薄笔记本",
"brand": "NOVA"
}
适合轻量处理。复杂清洗、关联和标准化通常放在 Logstash、Flink 或业务服务中。
2.10 数据模型设计原则
2.10.1 面向查询设计
Elasticsearch 不建议为了“像表结构”而设计,而应该围绕查询设计。
问自己:
- 哪些字段需要全文搜索;
- 哪些字段需要精确过滤;
- 哪些字段需要聚合;
- 哪些字段需要排序;
- 哪些字段只需要保存不需要索引;
示例:
{
"title": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 128 }
}
},
"description": {
"type": "text",
"index": true
},
"status": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"snapshot": { "type": "object", "enabled": false }
}
2.10.2 反范式
Elasticsearch 不适合频繁 Join。常用做法是写入时把查询需要的冗余字段直接放到文档中。
订单搜索文档示例:
{
"order_id": "O10001",
"user_id": "U8888",
"user_name": "Tom",
"shop_id": "S100",
"shop_name": "Nova 旗舰店",
"amount": 6999.00,
"status": "PAID",
"items": [
{ "sku_id": 1, "title": "轻薄笔记本", "price": 6999.00, "qty": 1 }
]
}
如果冗余字段经常变化,需要评估更新成本和数据一致性。
2.10.3 对象关系
普通 object 会把数组中的对象扁平化:
{
"items": [
{ "sku": "A", "price": 10 },
{ "sku": "B", "price": 20 }
]
}
实际索引结构近似:
items.sku = ["A", "B"]
items.price = [10, 20]
查询 sku=A AND price=20 会错误匹配。应使用 nested:
{
"items": {
"type": "nested",
"properties": {
"sku": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 }
}
}
}
nested 保留对象内部关系,但每个 nested 对象会作为隐藏子文档索引,数量过多会增加内存和查询成本。
2.11 集群状态与健康
查看健康:
GET /_cluster/health
返回示例:
{
"cluster_name": "es-learning",
"status": "green",
"timed_out": false,
"number_of_nodes": 3,
"active_primary_shards": 30,
"active_shards": 60,
"relocating_shards": 0,
"initializing_shards": 0,
"unassigned_shards": 0
}
| 状态 | 含义 |
|---|---|
green |
主分片和副本都正常 |
yellow |
主分片正常,部分副本未分配 |
red |
部分主分片不可用,存在数据风险 |
健康状态是结果,不是原因。排查时要继续查看节点、磁盘、分配解释和索引级健康。
2.12 本章小结
- 集群由节点组成,节点可以承担 master、data、ingest、coordinating 等角色;
- Index 是文档集合,Document 是 JSON,Mapping 决定字段如何索引;
- 分片是物理存储和并行执行单元,副本提供容灾和读扩展;
- 主分片数量不能直接修改,路由公式决定文档归属;
- Segment 不可变,refresh 使文档可搜索,flush 持久化,merge 合并段;
- 别名是零停机变更和逻辑视图的重要工具;
- ES 数据模型应面向查询设计,多数场景选择反范式,复杂关系使用 nested 或同步期关联。
2.13 思考题
- 为什么说 ES 的 Index 和数据库的“索引”不是同一个概念?
- 主分片数为什么不能随时修改?
text与keyword分别适合什么字段?举出你业务中的五个例子。- 普通 object 和 nested 的区别是什么?
- 如果一个索引有 1000 个 1GB 小分片,会带来哪些问题?