ElasticsearchNotes

第 02 章:核心概念与数据模型

zjc 于 2026-01-02 发布

这是《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

textkeyword 是最容易被误解的类型。手机号、订单号、状态、标签、类目 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

动态映射适合快速实验,但生产环境应显式定义。原因:

  1. 字符串可能被推断成 text + keyword,不符合业务预期;
  2. 数字字符串可能被推断成 long
  3. 错误 Mapping 后续不能原地修改;
  4. 字段无限增长会带来 Mapping 爆炸风险;
  5. 分词器无法按业务需求自动选择。

可以限制动态行为:

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
  }
}

分片的作用:

  1. 水平拆分数据;
  2. 并行执行查询;
  3. 将数据分布到多个节点;
  4. 通过副本提升读吞吐和容灾。

分片的代价:

  1. 每个分片是一个 Lucene 实例,有内存和文件句柄开销;
  2. 查询需要扇出到更多分片,归并成本上升;
  3. 分片过多会加重主节点元数据压力;
  4. 小分片过多会造成磁盘碎文件和合并效率下降;
  5. 副本会增加磁盘占用和写入放大。

经验建议:

指标 建议范围
单分片大小 日志类 10-50GB,业务类 20-75GB
单节点分片数 小集群低于 600,具体取决于版本和内存
主分片数 按数据规模和写入吞吐规划
副本数 生产至少 1

2.6 路由与文档归属

写入文档时,Elasticsearch 根据路由值决定文档进入哪个主分片:

shard = hash(routing) % number_of_primary_shards

默认 routing 是文档 _id

这意味着:

  1. 同一文档始终路由到同一主分片;
  2. 主分片数变化会改变路由结果;
  3. 自定义路由可以让相关文档落在同一分片;
  4. 自定义路由后读取也要携带相同 routing;
  5. 不合理的路由会造成数据倾斜。

自定义路由:

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

特点:

  1. segment 不可修改;
  2. 删除是标记删除;
  3. 更新等价于新文档写入加旧文档标记删除;
  4. segment merge 会合并小段,物理清理删除文档;
  5. 查询需要扫描多个 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" } }
  ]
}

别名价值:

  1. 应用不直接依赖物理索引名;
  2. 支持 Reindex 后零停机切换;
  3. 一个别名可以查询多个时间索引;
  4. 写入别名只能指向单个索引;
  5. 可以配置过滤别名,实现逻辑视图。

过滤别名示例:

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 不建议为了“像表结构”而设计,而应该围绕查询设计。

问自己:

  1. 哪些字段需要全文搜索;
  2. 哪些字段需要精确过滤;
  3. 哪些字段需要聚合;
  4. 哪些字段需要排序;
  5. 哪些字段只需要保存不需要索引;

示例:

{
  "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 本章小结

2.13 思考题

  1. 为什么说 ES 的 Index 和数据库的“索引”不是同一个概念?
  2. 主分片数为什么不能随时修改?
  3. textkeyword 分别适合什么字段?举出你业务中的五个例子。
  4. 普通 object 和 nested 的区别是什么?
  5. 如果一个索引有 1000 个 1GB 小分片,会带来哪些问题?