Elasticsearch CRUD 全维度解析

2025博客之星年度评选已开启 10w+人浏览 3.2k人参与

一、Create(创建):从单文档写入到批量写入

1. 基础用法:核心创建操作

Create 包含「索引创建」和「文档创建」两层核心动作,是所有操作的前提。

(1)创建索引(Index)

索引是 ES 存储文档的逻辑容器,需先定义索引的「设置(settings)」和「映射(mapping)」(基础场景可省略映射,ES 自动推断)。

# 基础索引创建(默认配置)
PUT /product_index  # 索引名必须小写,不能含特殊字符
{
  "settings": {
    "number_of_shards": 3,    # 主分片数(一旦创建不可修改)
    "number_of_replicas": 1   # 副本分片数(可动态修改)
  },
  "mappings": {
    "properties": {           # 字段映射:定义字段类型、分词规则等
      "product_name": { "type": "text", "analyzer": "ik_smart" },  # 中文分词
      "price": { "type": "float" },
      "stock": { "type": "integer" },
      "category": { "type": "keyword" },  # 精确匹配,用于聚合/筛选
      "create_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" }
    }
  }
}
(2)创建文档(Document)

文档是 ES 的最小数据单元(JSON 格式),创建文档分「指定 ID」和「自动生成 ID」两种方式:

# 方式1:指定文档ID(PUT),ID存在则覆盖,不存在则创建
PUT /product_index/_doc/1001  # _doc是文档类型(7.x后统一为_doc)
{
  "product_name": "华为Mate60 Pro",
  "price": 6999.0,
  "stock": 500,
  "category": "智能手机",
  "create_time": "2023-08-29 10:00:00"
}

# 方式2:自动生成ID(POST),每次请求生成唯一随机ID
POST /product_index/_doc
{
  "product_name": "苹果iPhone 15",
  "price": 7999.0,
  "stock": 800,
  "category": "智能手机",
  "create_time": "2023-09-15 09:30:00"
}
(3)批量创建文档(_bulk)

单条写入效率低,批量写入用 _bulk API,格式为「操作行+数据行」成对出现:

POST /_bulk
{"index":{"_index":"product_index","_id":"1003"}}  # 操作行:指定索引和ID
{"product_name":"小米14","price":4299.0,"stock":1000,"category":"智能手机","create_time":"2023-11-01 08:00:00"}  # 数据行
{"index":{"_index":"product_index","_id":"1004"}}
{"product_name":"华为FreeBuds Pro 2","price":1299.0,"stock":2000,"category":"蓝牙耳机","create_time":"2023-10-05 14:00:00"}

注意:_bulk 每行必须是独立 JSON,不能换行;即使单条失败,其他条仍会执行,需检查返回结果中的 errors 字段。

2. 进阶用法:精细化写入控制

(1)带路由的写入(Routing)

ES 默认按「文档 ID 的哈希值 % 主分片数」路由到指定分片,自定义路由可将相关文档路由到同一分片(提升查询效率):

# 按category路由,确保同一分类的商品在同一分片
PUT /product_index/_doc/1005?routing=平板电脑  # routing值指定为category字段值
{
  "product_name": "iPad Pro 2023",
  "price": 9999.0,
  "stock": 300,
  "category": "平板电脑",
  "create_time": "2023-10-20 11:00:00"
}
(2)写入参数控制
参数作用示例
refresh控制写入后是否立即刷新索引(默认1s后刷新,true 实时刷新但性能差)PUT /xxx/_doc/1?refresh=true
timeout写入超时时间(默认30s,分片不可用时等待)PUT /xxx/_doc/1?timeout=10s
version版本控制,避免并发写入冲突PUT /xxx/_doc/1?version=2

3. 底层原理:文档写入流程(附流程图)

ES 写入文档并非直接写入磁盘,而是经过「内存→缓冲区→分段文件」的过程,核心流程如下:

客户端发送写入请求

协调节点(Coordinating Node)

计算分片路由

主分片所在节点

主分片写入:1.写入内存缓冲区 2.记录translog日志

内存缓冲区满/刷新触发

生成分段文件(Segment)写入磁盘

同步写入副本分片(所有副本写入成功才算完成)

副本分片返回确认给主分片

主分片返回确认给协调节点

协调节点返回成功给客户端

定期合并分段文件(Merge)

清理删除/更新的旧文档

核心原理要点

  1. 协调节点不存储数据,仅负责路由和转发;
  2. 先写 translog 日志(防止宕机数据丢失),再写内存缓冲区;
  3. 必须等待「所有副本分片写入成功」才返回客户端,保证数据高可用;
  4. 分段文件一旦生成不可修改,更新/删除都是「标记操作」,合并时才清理。

4. 实战案例:批量导入电商商品数据

业务场景:导入1000条商品数据,要求按分类路由,写入后立即可见(测试环境)。

# 1. 先创建优化后的索引(指定路由字段)
PUT /product_index
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "routing": {
      "required": true  # 强制写入时指定routing
    },
    "refresh_interval": "1s"  # 生产环境建议设为30s提升性能
  },
  "mappings": {
    "properties": {
      "product_name": { "type": "text", "analyzer": "ik_max_word" },
      "price": { "type": "float" },
      "stock": { "type": "integer" },
      "category": { "type": "keyword" },
      "create_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" }
    }
  }
}

# 2. 批量写入(示例5条,实际可拼接1000条)
POST /_bulk?refresh=true  # 测试环境强制刷新
{"index":{"_index":"product_index","_id":"1006","routing":"笔记本电脑"}}
{"product_name":"联想拯救者Y9000P","price":8999.0,"stock":200,"category":"笔记本电脑","create_time":"2023-09-01 10:00:00"}
{"index":{"_index":"product_index","_id":"1007","routing":"笔记本电脑"}}
{"product_name":"华硕ROG枪神7","price":12999.0,"stock":100,"category":"笔记本电脑","create_time":"2023-09-05 10:00:00"}
{"index":{"_index":"product_index","_id":"1008","routing":"耳机"}}
{"product_name":"索尼WF-1000XM5","price":1999.0,"stock":500,"category":"耳机","create_time":"2023-08-15 10:00:00"}
{"index":{"_index":"product_index","_id":"1009","routing":"耳机"}}
{"product_name":"森海塞尔Momentum True Wireless 4","price":2499.0,"stock":300,"category":"耳机","create_time":"2023-08-20 10:00:00"}
{"index":{"_index":"product_index","_id":"1010","routing":"平板电脑"}}
{"product_name":"华为MatePad Pro 11","price":3999.0,"stock":800,"category":"平板电脑","create_time":"2023-10-01 10:00:00"}

二、Read(查询):从简单ID查询到复杂聚合查询

查询是 ES 最核心的能力,从「简单精确查询」到「复杂多条件聚合」,需理解倒排索引和查询执行流程。

1. 基础用法:简单查询

(1)按ID精确查询

最快速的查询方式(直接定位分片和文档),适合单文档获取:

GET /product_index/_doc/1001  # 指定ID查询
GET /product_index/_doc/1005?routing=平板电脑  # 自定义路由的文档,查询需指定routing
(2)简单匹配查询
  • match:分词匹配(适合文本字段,如商品名、描述);
  • term:精确匹配(适合keyword、数值、日期字段)。
# 1. match分词查询(查商品名含「华为」的商品)
GET /product_index/_search
{
  "query": {
    "match": {
      "product_name": "华为"  # ik分词器会拆分为「华为」,匹配所有含该词的文档
    }
  }
}

# 2. term精确查询(查分类为「智能手机」的商品)
GET /product_index/_search
{
  "query": {
    "term": {
      "category": { "value": "智能手机" }
    }
  }
}

# 3. 基础分页(from/size)
GET /product_index/_search
{
  "query": { "match_all": {} },  # 匹配所有文档
  "from": 0,  # 起始位置
  "size": 10, # 返回条数
  "sort": [ { "price": { "order": "desc" } } ]  # 按价格降序
}

2. 进阶用法:复杂查询与聚合

(1)布尔查询(Bool Query)

组合多条件查询,核心子句:

  • must:必须满足(影响评分);
  • should:可选满足(满足越多评分越高);
  • must_not:必须不满足;
  • filter:过滤条件(不影响评分,结果缓存,性能更高)。
# 业务场景:查「智能手机」分类、价格5000-8000元、库存>100的华为/苹果商品
GET /product_index/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "product_name": "华为 苹果" } }  # 商品名含华为或苹果
      ],
      "filter": [  # 过滤条件(性能优先)
        { "term": { "category": "智能手机" } },
        { "range": { "price": { "gte": 5000, "lte": 8000 } } },
        { "range": { "stock": { "gt": 100 } } }
      ]
    }
  },
  "sort": [ { "create_time": { "order": "desc" } } ],
  "_source": [ "product_name", "price", "stock" ]  # 只返回指定字段(减少数据传输)
}
(2)聚合查询(Aggregation)

对查询结果做统计分析(类似SQL的GROUP BY、SUM、AVG),核心分「桶聚合」和「指标聚合」:

# 业务场景:按分类统计商品数量、平均价格、总库存
GET /product_index/_search
{
  "size": 0,  # 不返回具体文档,只返回聚合结果
  "aggs": {
    "category_stats": {  # 聚合名称(自定义)
      "terms": { "field": "category", "size": 10 },  # 桶聚合:按分类分组
      "aggs": {  # 嵌套指标聚合
        "avg_price": { "avg": { "field": "price" } },  # 平均价格
        "total_stock": { "sum": { "field": "stock" } }  # 总库存
      }
    }
  }
}
(3)深分页解决方案

基础分页(from/size)在 from>10000 时性能极差(ES需遍历所有分片的前N条数据),推荐两种方案:

方案1:Scroll(滚动查询,适合全量导出)
# 1. 初始化scroll,保留上下文1分钟
GET /product_index/_search?scroll=1m
{
  "size": 1000,
  "query": { "match_all": {} }
}

# 2. 遍历scroll(用返回的_scroll_id)
GET /_search/scroll
{
  "scroll": "1m",
  "scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAC..."
}

# 3. 清理scroll上下文(避免内存泄漏)
DELETE /_search/scroll
{
  "scroll_id": ["DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAC..."]
}
方案2:Search After(实时分页,适合用户翻页)
# 1. 第一页(按create_time+id排序,避免重复)
GET /product_index/_search
{
  "size": 10,
  "query": { "match_all": {} },
  "sort": [
    { "create_time": "desc" },
    { "_id": "asc" }
  ]
}

# 2. 第二页(用第一页最后一条的sort值作为search_after)
GET /product_index/_search
{
  "size": 10,
  "query": { "match_all": {} },
  "sort": [
    { "create_time": "desc" },
    { "_id": "asc" }
  ],
  "search_after": ["2023-09-15T01:30:00.000Z", "1002"]  # 第一页最后一条的排序值
}

3. 底层原理:查询执行流程与倒排索引

(1)倒排索引(核心)

ES 快速查询的核心是「倒排索引」,与传统数据库的「正排索引」相反:

  • 正排索引:文档ID → 字段值(如1001→华为Mate60 Pro);
  • 倒排索引:字段值 → 文档ID列表(如「华为」→[1001,1004])。

倒排索引示意图

词条(Term)文档ID列表(Posting List)频率(TF)位置
华为[1001, 1004]1,10,0
Mate60[1001]11
Pro[1001]12
苹果[1002]10
(2)查询执行流程(图解)

客户端发送查询请求

协调节点

解析查询语句

向所有相关分片发送查询请求(Query Phase)

各分片执行查询,返回「文档ID+评分」的轻量结果

协调节点汇总结果,排序后取Top N

向对应分片发送获取请求(Fetch Phase)

分片返回完整文档数据

协调节点组装结果,返回给客户端

核心要点

  1. 查询分「Query Phase(查ID和评分)」和「Fetch Phase(查完整文档)」,减少数据传输;
  2. 评分基于 BM25 算法(替代传统 TF-IDF),综合词条频率、文档长度等;
  3. Filter 条件的结果会缓存到内存,重复查询时直接命中,性能远高于 must。

4. 实战案例:电商商品筛选与销量统计

业务场景

  • 筛选条件:价格>3000、库存>50、分类为「智能手机/笔记本电脑」;
  • 排序:按价格升序,价格相同按库存降序;
  • 聚合:按分类统计商品数量、最高/最低价格。
GET /product_index/_search
{
  "size": 20,  # 返回前20条商品
  "query": {
    "bool": {
      "filter": [
        { "range": { "price": { "gt": 3000 } } },
        { "range": { "stock": { "gt": 50 } } },
        { "terms": { "category": ["智能手机", "笔记本电脑"] } }  # 多值精确匹配
      ]
    }
  },
  "sort": [
    { "price": { "order": "asc" } },
    { "stock": { "order": "desc" } }
  ],
  "_source": [ "product_name", "price", "stock", "category" ],
  "aggs": {
    "category_analysis": {
      "terms": { "field": "category", "size": 2 },
      "aggs": {
        "max_price": { "max": { "field": "price" } },
        "min_price": { "min": { "field": "price" } },
        "count": { "value_count": { "field": "_id" } }
      }
    }
  }
}

三、Update(更新):从全量更新到脚本更新

ES 文档是「不可变的」,更新并非修改原文档,而是标记原文档为删除,再创建新文档。

1. 基础用法

(1)全量更新(PUT)

覆盖整个文档,需传入所有字段(缺失字段会丢失):

# 更新商品1001的价格和库存(需传入所有字段)
PUT /product_index/_doc/1001
{
  "product_name": "华为Mate60 Pro",
  "price": 6899.0,  # 价格下调100
  "stock": 450,     # 库存减少50
  "category": "智能手机",
  "create_time": "2023-08-29 10:00:00"
}
(2)局部更新(_update)

仅更新指定字段,无需传入所有字段,效率更高:

# 局部更新:仅修改价格和库存
POST /product_index/_update/1001
{
  "doc": {
    "price": 6899.0,
    "stock": 450
  }
}

2. 进阶用法

(1)脚本更新(Painless 脚本)

适合「基于原字段值更新」的场景(如库存减1、价格涨5%),Painless 是 ES 内置的脚本语言:

# 业务场景:商品1001库存减10,价格涨2%
POST /product_index/_update/1001
{
  "script": {
    "source": "ctx._source.stock -= 10; ctx._source.price = ctx._source.price * 1.02",
    "lang": "painless"  # 指定脚本语言
  }
}
(2)条件更新

仅满足条件时更新(避免并发冲突):

# 业务场景:仅当库存>100时,才将库存减50
POST /product_index/_update/1001
{
  "script": {
    "source": "if (ctx._source.stock > 100) { ctx._source.stock -= 50; } else { ctx.op = 'none'; }",
    "lang": "painless"
  }
}
(3)批量更新(_bulk 或 _update_by_query)
# 方式1:_bulk批量更新
POST /_bulk
{"update":{"_index":"product_index","_id":"1001"}}
{"doc":{"stock":400}}
{"update":{"_index":"product_index","_id":"1002"}}
{"doc":{"price":7899.0}}

# 方式2:按条件批量更新(所有智能手机库存加100)
POST /product_index/_update_by_query
{
  "query": {
    "term": { "category": "智能手机" }
  },
  "script": {
    "source": "ctx._source.stock += 100",
    "lang": "painless"
  }
}

3. 底层原理:文档更新的本质

发起更新请求

协调节点路由到主分片

主分片查找原文档,标记为删除(tombstone)

创建新文档(含更新后字段),版本号+1

同步新文档到副本分片

副本分片返回确认

主分片返回更新成功

段合并时清理标记为删除的旧文档

核心要点

  1. 每个文档有版本号(version),更新后版本号自增,可通过版本控制避免并发冲突;
  2. 局部更新(_update)比全量更新更高效,因为只需传输修改的字段;
  3. 脚本更新需注意性能,复杂脚本会增加分片负载。

4. 实战案例:订单支付后更新商品库存

业务场景:用户下单购买商品1001(数量2),支付成功后更新库存(原库存450→448),并记录更新时间。

POST /product_index/_update/1001
{
  "script": {
    "source": """
      ctx._source.stock -= params.buy_count;
      ctx._source.update_time = params.update_time;
    """,
    "lang": "painless",
    "params": {  # 传入参数,避免硬编码
      "buy_count": 2,
      "update_time": "2023-11-05 16:30:00"
    }
  },
  "version": 3  # 仅当版本号为3时更新(防止并发修改)
}

四、Delete(删除):从单文档删除到条件删除

删除操作同样基于「不可变文档」特性,分为「文档删除」和「索引删除」,核心是「标记删除+段合并清理」。

1. 基础用法

(1)按ID删除文档
DELETE /product_index/_doc/1001  # 删除指定ID的文档
DELETE /product_index/_doc/1005?routing=平板电脑  # 自定义路由的文档需指定routing
(2)删除索引
DELETE /product_index  # 删除整个索引(不可逆,需谨慎)

2. 进阶用法

(1)条件删除(_delete_by_query)

按查询条件批量删除文档:

# 业务场景:删除库存为0、创建时间早于2023-01-01的商品
POST /product_index/_delete_by_query
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "stock": 0 } },
        { "range": { "create_time": { "lt": "2023-01-01 00:00:00" } } }
      ]
    }
  }
}
(2)批量删除(_bulk)
POST /_bulk
{"delete":{"_index":"product_index","_id":"1003"}}
{"delete":{"_index":"product_index","_id":"1004"}}

3. 底层原理:删除流程与段合并

发起删除请求

协调节点路由到主分片

主分片标记文档为删除(tombstone)

同步删除标记到副本分片

副本返回确认,主分片返回删除成功

定期段合并(Merge)

清理标记为删除的文档,释放磁盘空间

核心要点

  1. 删除文档并非立即释放磁盘空间,而是标记为「已删除」,段合并时才真正清理;
  2. _delete_by_query 会触发全量扫描,大索引操作时需加 scroll_size 控制批次(默认1000);
  3. 避免直接删除索引,可通过「索引别名」切换,先创建新索引,再删除旧索引(减少业务中断)。

4. 实战案例:清理过期日志数据

业务场景:删除日志索引中3个月前的日志数据(日志索引名:log_index)。

# 1. 先查询确认要删除的数据(避免误删)
GET /log_index/_search
{
  "query": {
    "range": { "log_time": { "lt": "now-3M/d" } }  # now-3M/d:3个月前的当天
  }
}

# 2. 条件删除(加批次控制,避免性能问题)
POST /log_index/_delete_by_query?scroll_size=5000
{
  "query": {
    "range": { "log_time": { "lt": "now-3M/d" } }
  }
}

五、CRUD 最佳实践总结

  1. 写入:批量写入(_bulk)代替单条写入,生产环境关闭实时刷新(refresh_interval=30s);
  2. 查询:优先用 filter 过滤条件,深分页用 search_after/scroll,避免通配符开头查询(如*华为);
  3. 更新:局部更新(_update)代替全量更新,并发场景用版本控制;
  4. 删除:避免大规模 _delete_by_query,可通过索引生命周期(ILM)自动删除过期索引;
  5. 性能:合理设置分片数(单分片10-50GB),热点数据路由到同一分片提升查询效率。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Flying_Fish_Xuan

你的鼓励将是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值