一、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 写入文档并非直接写入磁盘,而是经过「内存→缓冲区→分段文件」的过程,核心流程如下:
核心原理要点:
- 协调节点不存储数据,仅负责路由和转发;
- 先写
translog日志(防止宕机数据丢失),再写内存缓冲区; - 必须等待「所有副本分片写入成功」才返回客户端,保证数据高可用;
- 分段文件一旦生成不可修改,更新/删除都是「标记操作」,合并时才清理。
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,1 | 0,0 |
| Mate60 | [1001] | 1 | 1 |
| Pro | [1001] | 1 | 2 |
| 苹果 | [1002] | 1 | 0 |
(2)查询执行流程(图解)
核心要点:
- 查询分「Query Phase(查ID和评分)」和「Fetch Phase(查完整文档)」,减少数据传输;
- 评分基于 BM25 算法(替代传统 TF-IDF),综合词条频率、文档长度等;
- 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. 底层原理:文档更新的本质
核心要点:
- 每个文档有版本号(version),更新后版本号自增,可通过版本控制避免并发冲突;
- 局部更新(_update)比全量更新更高效,因为只需传输修改的字段;
- 脚本更新需注意性能,复杂脚本会增加分片负载。
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. 底层原理:删除流程与段合并
核心要点:
- 删除文档并非立即释放磁盘空间,而是标记为「已删除」,段合并时才真正清理;
_delete_by_query会触发全量扫描,大索引操作时需加scroll_size控制批次(默认1000);- 避免直接删除索引,可通过「索引别名」切换,先创建新索引,再删除旧索引(减少业务中断)。
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 最佳实践总结
- 写入:批量写入(_bulk)代替单条写入,生产环境关闭实时刷新(refresh_interval=30s);
- 查询:优先用 filter 过滤条件,深分页用 search_after/scroll,避免通配符开头查询(如*华为);
- 更新:局部更新(_update)代替全量更新,并发场景用版本控制;
- 删除:避免大规模 _delete_by_query,可通过索引生命周期(ILM)自动删除过期索引;
- 性能:合理设置分片数(单分片10-50GB),热点数据路由到同一分片提升查询效率。
815

被折叠的 条评论
为什么被折叠?



