Elasticsearch核心概念详解
学习目标
- 深入理解索引、文档、映射的概念和底层原理
- 掌握所有数据类型和字段属性的使用场景
- 理解分片和副本的工作机制和最佳实践
- 能够设计高性能、可扩展的索引结构
- 掌握索引生命周期管理和优化策略
- 理解倒排索引的实现原理
知识结构思维导图
#mermaid-svg-kPtIGTqdXuYWJbhm{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kPtIGTqdXuYWJbhm .error-icon{fill:#552222;}#mermaid-svg-kPtIGTqdXuYWJbhm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kPtIGTqdXuYWJbhm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kPtIGTqdXuYWJbhm .marker.cross{stroke:#333333;}#mermaid-svg-kPtIGTqdXuYWJbhm svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kPtIGTqdXuYWJbhm p{margin:0;}#mermaid-svg-kPtIGTqdXuYWJbhm .edge{stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .section–1 rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section–1 path,#mermaid-svg-kPtIGTqdXuYWJbhm .section–1 circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section–1 polygon,#mermaid-svg-kPtIGTqdXuYWJbhm .section–1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section–1 text{fill:#ffffff;}#mermaid-svg-kPtIGTqdXuYWJbhm .node-icon–1{font-size:40px;color:#ffffff;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-edge–1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-depth–1{stroke-width:17;}#mermaid-svg-kPtIGTqdXuYWJbhm .section–1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled circle,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:lightgray;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:#efefef;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-0 rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section-0 path,#mermaid-svg-kPtIGTqdXuYWJbhm .section-0 circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section-0 polygon,#mermaid-svg-kPtIGTqdXuYWJbhm .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section-0 text{fill:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .node-icon-0{font-size:40px;color:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-depth-0{stroke-width:14;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled circle,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:lightgray;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:#efefef;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-1 rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section-1 path,#mermaid-svg-kPtIGTqdXuYWJbhm .section-1 circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section-1 polygon,#mermaid-svg-kPtIGTqdXuYWJbhm .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section-1 text{fill:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .node-icon-1{font-size:40px;color:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-depth-1{stroke-width:11;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled circle,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:lightgray;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:#efefef;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-2 rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section-2 path,#mermaid-svg-kPtIGTqdXuYWJbhm .section-2 circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section-2 polygon,#mermaid-svg-kPtIGTqdXuYWJbhm .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section-2 text{fill:#ffffff;}#mermaid-svg-kPtIGTqdXuYWJbhm .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-depth-2{stroke-width:8;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled circle,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:lightgray;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:#efefef;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-3 rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section-3 path,#mermaid-svg-kPtIGTqdXuYWJbhm .section-3 circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section-3 polygon,#mermaid-svg-kPtIGTqdXuYWJbhm .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section-3 text{fill:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .node-icon-3{font-size:40px;color:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-depth-3{stroke-width:5;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled circle,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:lightgray;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:#efefef;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-4 rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section-4 path,#mermaid-svg-kPtIGTqdXuYWJbhm .section-4 circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section-4 polygon,#mermaid-svg-kPtIGTqdXuYWJbhm .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section-4 text{fill:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .node-icon-4{font-size:40px;color:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-depth-4{stroke-width:2;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled circle,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:lightgray;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:#efefef;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-5 rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section-5 path,#mermaid-svg-kPtIGTqdXuYWJbhm .section-5 circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section-5 polygon,#mermaid-svg-kPtIGTqdXuYWJbhm .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section-5 text{fill:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .node-icon-5{font-size:40px;color:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-depth-5{stroke-width:-1;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled circle,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:lightgray;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:#efefef;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-6 rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section-6 path,#mermaid-svg-kPtIGTqdXuYWJbhm .section-6 circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section-6 polygon,#mermaid-svg-kPtIGTqdXuYWJbhm .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section-6 text{fill:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .node-icon-6{font-size:40px;color:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-depth-6{stroke-width:-4;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled circle,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:lightgray;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:#efefef;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-7 rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section-7 path,#mermaid-svg-kPtIGTqdXuYWJbhm .section-7 circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section-7 polygon,#mermaid-svg-kPtIGTqdXuYWJbhm .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section-7 text{fill:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .node-icon-7{font-size:40px;color:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-depth-7{stroke-width:-7;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled circle,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:lightgray;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:#efefef;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-8 rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section-8 path,#mermaid-svg-kPtIGTqdXuYWJbhm .section-8 circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section-8 polygon,#mermaid-svg-kPtIGTqdXuYWJbhm .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section-8 text{fill:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .node-icon-8{font-size:40px;color:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-depth-8{stroke-width:-10;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled circle,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:lightgray;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:#efefef;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-9 rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section-9 path,#mermaid-svg-kPtIGTqdXuYWJbhm .section-9 circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section-9 polygon,#mermaid-svg-kPtIGTqdXuYWJbhm .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section-9 text{fill:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .node-icon-9{font-size:40px;color:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-depth-9{stroke-width:-13;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled circle,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:lightgray;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:#efefef;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-10 rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section-10 path,#mermaid-svg-kPtIGTqdXuYWJbhm .section-10 circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section-10 polygon,#mermaid-svg-kPtIGTqdXuYWJbhm .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section-10 text{fill:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .node-icon-10{font-size:40px;color:black;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .edge-depth-10{stroke-width:-16;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled circle,#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:lightgray;}#mermaid-svg-kPtIGTqdXuYWJbhm .disabled text{fill:#efefef;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-root rect,#mermaid-svg-kPtIGTqdXuYWJbhm .section-root path,#mermaid-svg-kPtIGTqdXuYWJbhm .section-root circle,#mermaid-svg-kPtIGTqdXuYWJbhm .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-kPtIGTqdXuYWJbhm .section-root text{fill:#ffffff;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-root span{color:#ffffff;}#mermaid-svg-kPtIGTqdXuYWJbhm .section-2 span{color:#ffffff;}#mermaid-svg-kPtIGTqdXuYWJbhm .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-kPtIGTqdXuYWJbhm .edge{fill:none;}#mermaid-svg-kPtIGTqdXuYWJbhm .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-kPtIGTqdXuYWJbhm :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Elasticsearch核心概念
索引Index
索引设置
分片配置
副本配置
刷新间隔
索引别名
索引模板
索引生命周期
文档Document
文档结构
文档ID
文档版本
文档路由
映射Mapping
动态映射
静态映射
字段类型
字段属性
数据类型
文本类型
text
keyword
数值类型
long/integer
double/float
日期类型
布尔类型
对象类型
object
nested
特殊类型
geo_point
ip
completion
分片与副本
主分片
副本分片
分片分配
分片路由
倒排索引
Term Dictionary
Posting List
Term Index
一、索引(Index)深入理解
1.1 什么是索引?
索引是文档的容器,类似于 MySQL 中的数据库。但与传统数据库不同,Elasticsearch 的索引是基于倒排索引实现的。
概念对比(ES 8.x):
MySQL 5.x/6.x:
database.table.row
shop.products.row1
Elasticsearch 6.x(旧版本):
index.type.document
shop.products.doc1
❌ 已废弃
Elasticsearch 7.x(过渡版本):
index._doc.document
shop._doc.doc1
⚠️ Type 标记为废弃
Elasticsearch 8.x(当前版本):
index.document
shop.doc1
✓ Type 完全移除
为什么 ES 8.x 完全移除了 Type?
历史背景:
ES 早期版本模仿 MySQL 的设计
Index = Database
Type = Table
Document = Row
实际问题:
1. 映射冲突
同一索引中,不同 Type 的相同字段名必须有相同的映射
products/phone: { "name": "text" }
products/tablet: { "name": "keyword" }
❌ 冲突!无法共存
2. 稀疏存储
不同 Type 有不同的字段
products/phone: { "name", "price", "network" }
products/tablet: { "name", "price", "screenSize" }
❌ 每个文档都要存储所有字段的映射,浪费空间
3. 性能问题
Lucene 没有 Type 的概念
查询 products/phone 时,仍需要扫描所有文档
❌ 无法真正隔离数据
4. 复杂性
增加了 API 的复杂性
增加了用户的学习成本
❌ 收益不大
ES 8.x 解决方案:
✓ 完全移除 Type 概念
✓ 一个索引只存储一种类型的文档
✓ 如果需要多种类型,创建多个索引
✓ 或使用字段区分:{ "type": "phone" }
ES 8.x 的正确用法:
# ✓ 正确:为不同类型创建不同索引
PUT /products_phone/_doc/1
{
"name": "华为手机",
"price": 3999
}
PUT /products_tablet/_doc/2
{
"name": "iPad",
"price": 6799
}
# ✓ 正确:使用字段区分类型
PUT /products/_doc/1
{
"type": "phone",
"name": "华为手机",
"price": 3999
}
PUT /products/_doc/2
{
"type": "tablet",
"name": "iPad",
"price": 6799
}
# ❌ 错误:ES 8.x 不再支持自定义 Type
PUT /products/phone/1
{
"name": "华为手机"
}
# 返回错误:Rejecting mapping update to [products] as the final mapping would have more than 1 type
迁移指南(从旧版本升级到 ES 8.x):
# 旧版本(ES 6.x)
PUT /shop/products/1
{
"name": "华为手机"
}
PUT /shop/orders/1
{
"orderId": "001"
}
# 新版本(ES 8.x)- 方案 1:分离索引(推荐)
PUT /products/_doc/1
{
"name": "华为手机"
}
PUT /orders/_doc/1
{
"orderId": "001"
}
# 新版本(ES 8.x)- 方案 2:使用字段区分
PUT /shop/_doc/product_1
{
"docType": "product",
"name": "华为手机"
}
PUT /shop/_doc/order_1
{
"docType": "order",
"orderId": "001"
}
1.2 索引的创建
1.2.1 自动创建(不推荐生产环境)
POST /products/_doc/1
{
"name": "华为手机",
"price": 3999
}
// 如果 products 索引不存在,会自动创建
自动创建的问题:
❌ 无法控制分片数量
❌ 无法控制字段类型
❌ 可能导致类型推断错误
❌ 生产环境可能导致问题
示例:
"123" → 推断为 text
123 → 推断为 long
导致后续插入失败!
禁用自动创建:
PUT /_cluster/settings
{
"persistent": {
"action.auto_create_index": "false"
}
}
// 或者只允许特定模式
PUT /_cluster/settings
{
"persistent": {
"action.auto_create_index": "+logs-*,-*"
}
}
1.2.2 手动创建(推荐)
基本创建:
PUT /products
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
}
}
完整创建(包含映射):
PUT /products
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "1s",
"max_result_window": 10000
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word"
},
"price": {
"type": "double"
}
}
}
}
1.3 索引设置详解
1.3.1 静态设置(创建后不可修改)
PUT /products
{
"settings": {
// ==================== 分片配置 ====================
"number_of_shards": 3, // 主分片数(不可修改)
// ==================== 编解码器 ====================
"codec": "best_compression", // 压缩算法(default/best_compression)
// ==================== 路由分片数 ====================
"routing_partition_size": 1 // 路由分区大小
}
}
为什么主分片数不可修改?
文档路由公式:
shard = hash(routing) % number_of_primary_shards
↑
修改后路由会错乱,文档找不到!
示例:
原来 3 个分片:
doc1 → hash(doc1) % 3 = 1 → 存储在分片 1
修改为 5 个分片:
doc1 → hash(doc1) % 5 = 3 → 查询分片 3(找不到!)
如何修改分片数?
// 方法 1:Reindex(推荐)
POST /_reindex
{
"source": {
"index": "products"
},
"dest": {
"index": "products_new"
}
}
// 方法 2:Split API(分裂分片)
POST /products/_split/products_new
{
"settings": {
"index.number_of_shards": 6 // 必须是原来的倍数
}
}
// 方法 3:Shrink API(合并分片)
POST /products/_shrink/products_new
{
"settings": {
"index.number_of_shards": 1 // 必须是原来的因数
}
}
1.3.2 动态设置(可随时修改)
PUT /products/_settings
{
// ==================== 副本配置 ====================
"number_of_replicas": 2, // 副本数(可动态修改)
// ==================== 刷新配置 ====================
"refresh_interval": "30s", // 刷新间隔(默认 1s)
// "refresh_interval": "-1", // 禁用自动刷新(批量导入时)
// ==================== 查询配置 ====================
"max_result_window": 10000, // 最大返回结果数(from + size)
"max_inner_result_window": 100, // 内部聚合最大结果数
"max_rescore_window": 10000, // 重新评分窗口大小
// ==================== 分析配置 ====================
"max_ngram_diff": 1, // N-gram 最大差值
"max_shingle_diff": 3, // Shingle 最大差值
// ==================== 索引配置 ====================
"max_docvalue_fields_search": 100, // 最大 doc_values 字段数
"max_script_fields": 32, // 最大脚本字段数
// ==================== 路由配置 ====================
"routing.allocation.enable": "all", // 分片分配策略
// "all": 允许所有分片分配
// "primaries": 只允许主分片分配
// "new_primaries": 只允许新索引的主分片分配
// "none": 不允许分片分配
// ==================== 慢查询日志 ====================
"search.slowlog.threshold.query.warn": "10s",
"search.slowlog.threshold.query.info": "5s",
"search.slowlog.threshold.query.debug": "2s",
"search.slowlog.threshold.query.trace": "500ms",
"search.slowlog.threshold.fetch.warn": "1s",
"search.slowlog.threshold.fetch.info": "800ms",
"search.slowlog.threshold.fetch.debug": "500ms",
"search.slowlog.threshold.fetch.trace": "200ms",
"indexing.slowlog.threshold.index.warn": "10s",
"indexing.slowlog.threshold.index.info": "5s",
"indexing.slowlog.threshold.index.debug": "2s",
"indexing.slowlog.threshold.index.trace": "500ms",
// ==================== 合并配置 ====================
"merge.scheduler.max_thread_count": 1 // 合并线程数
}
refresh_interval 详解:
什么是 refresh?
将内存中的文档写入磁盘段(segment),使其可被搜索
默认 1s:
✓ 近实时搜索(1秒内可见)
✗ 频繁刷新消耗资源
增加到 30s:
✓ 减少资源消耗
✗ 搜索延迟增加
禁用(-1):
✓ 批量导入时性能最佳
✗ 数据不可搜索
⚠️ 导入完成后必须手动刷新
实战场景:批量导入优化
// 1. 导入前:禁用刷新和副本
PUT /products/_settings
{
"refresh_interval": "-1",
"number_of_replicas": 0
}
// 2. 批量导入数据
POST /_bulk
{ "index": { "_index": "products", "_id": "1" }}
{ "name": "产品1", "price": 100 }
{ "index": { "_index": "products", "_id": "2" }}
{ "name": "产品2", "price": 200 }
// … 大量数据
// 3. 导入后:手动刷新
POST /products/_refresh
// 4. 恢复设置
PUT /products/_settings
{
"refresh_interval": "1s",
"number_of_replicas": 1
}
1.4 索引别名(Alias)
1.4.1 为什么需要别名?
场景:索引需要重建
products_v1 → 应用使用
↓
需要修改映射
↓
创建 products_v2
↓
问题:如何无缝切换?
↓
解决:使用别名!
1.4.2 创建别名
基本别名:
// 创建别名
POST /_aliases
{
"actions": [
{
"add": {
"index": "products_v1",
"alias": "products"
}
}
]
}
// 应用使用别名
GET /products/_search
// 实际查询 products_v1
切换别名(零停机):
POST /_aliases
{
"actions": [
{
"remove": {
"index": "products_v1",
"alias": "products"
}
},
{
"add": {
"index": "products_v2",
"alias": "products"
}
}
]
}
// 原子操作,瞬间切换
1.4.3 过滤别名
// 创建过滤别名(只显示上架商品)
POST /_aliases
{
"actions": [
{
"add": {
"index": "products",
"alias": "products_on_sale",
"filter": {
"term": {
"onSale": true
}
}
}
}
]
}
// 查询别名(自动应用过滤)
GET /products_on_sale/_search
// 只返回 onSale=true 的商品
1.4.4 路由别名
// 创建路由别名
POST /_aliases
{
"actions": [
{
"add": {
"index": "products",
"alias": "products_category_1",
"routing": "1"
}
}
]
}
// 写入时自动路由到指定分片
POST /products_category_1/_doc/1
{
"name": "商品1",
"category": "1"
}
1.4.5 写入别名
// 创建写入别名
POST /_aliases
{
"actions": [
{
"add": {
"index": "products_v2",
"alias": "products",
"is_write_index": true // 标记为写入索引
}
},
{
"add": {
"index": "products_v1",
"alias": "products"
}
}
]
}
// 写入别名(写入 products_v2)
POST /products/_doc/1
{
"name": "新商品"
}
// 查询别名(查询两个索引)
GET /products/_search
1.5 索引状态管理
1.5.1 查看索引信息
// 查看所有索引
GET /_cat/indices?v
// 查看索引设置
GET /products/_settings
// 查看索引映射
GET /products/_mapping
// 查看索引统计
GET /products/_stats
// 查看索引分片
GET /_cat/shards/products?v
1.5.2 打开/关闭索引
// 关闭索引(释放内存,但数据保留)
POST /products/_close
// 打开索引
POST /products/_open
// 关闭索引的好处:
// ✓ 释放内存
// ✓ 数据保留在磁盘
// ✓ 可以快速重新打开
// ✗ 无法读写
1.5.3 删除索引
// 删除单个索引
DELETE /products
// 删除多个索引
DELETE /products,orders
// 使用通配符删除
DELETE /logs–*
// 删除所有索引(危险!)
DELETE /_all
// 建议禁用:action.destructive_requires_name: true
1.6 索引生命周期管理(ILM)
1.6.1 什么是 ILM?
场景:日志索引
logs-2024-01-01 → 热数据(频繁查询)
logs-2024-01-02 → 热数据
logs-2024-01-03 → 热数据
logs-2023-12-01 → 温数据(偶尔查询)
logs-2023-11-01 → 冷数据(很少查询)
logs-2023-10-01 → 可以删除
问题:如何自动管理这些索引?
解决:使用 ILM!
1.6.2 ILM 阶段
Hot(热)→ Warm(温)→ Cold(冷)→ Delete(删除)
↓ ↓ ↓ ↓
频繁读写 偶尔查询 很少查询 自动删除
高性能 中性能 低性能 释放空间
SSD SSD/HDD HDD –
1.6.3 创建 ILM 策略
PUT /_ilm/policy/logs_policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "50GB", // 索引大小超过 50GB
"max_age": "1d", // 或索引创建超过 1 天
"max_docs": 10000000 // 或文档数超过 1000 万
},
"set_priority": {
"priority": 100 // 恢复优先级
}
}
},
"warm": {
"min_age": "7d", // 7 天后进入温阶段
"actions": {
"shrink": {
"number_of_shards": 1 // 合并为 1 个分片
},
"forcemerge": {
"max_num_segments": 1 // 强制合并段
},
"set_priority": {
"priority": 50
}
}
},
"cold": {
"min_age": "30d", // 30 天后进入冷阶段
"actions": {
"freeze": {}, // 冻结索引
"set_priority": {
"priority": 0
}
}
},
"delete": {
"min_age": "90d", // 90 天后删除
"actions": {
"delete": {}
}
}
}
}
}
1.6.4 应用 ILM 策略
// 创建索引模板并应用 ILM
PUT /_index_template/logs_template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"index.lifecycle.name": "logs_policy", // 应用 ILM 策略
"index.lifecycle.rollover_alias": "logs" // 滚动别名
}
}
}
// 创建初始索引
PUT /logs–000001
{
"aliases": {
"logs": {
"is_write_index": true
}
}
}
// 写入数据(使用别名)
POST /logs/_doc
{
"message": "日志消息",
"timestamp": "2024-01-17T10:00:00"
}
// ILM 会自动:
// 1. 当索引满足条件时,创建 logs-000002
// 2. 将写入别名切换到新索引
// 3. 按照策略管理旧索引
1.7 索引模板详解
1.7.1 组件模板(Component Template)
// 创建通用设置模板
PUT /_component_template/common_settings
{
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
}
}
}
// 创建通用映射模板
PUT /_component_template/common_mappings
{
"template": {
"mappings": {
"properties": {
"timestamp": {
"type": "date"
},
"message": {
"type": "text"
}
}
}
}
}
1.7.2 索引模板(Index Template)
// 创建索引模板(组合组件模板)
PUT /_index_template/logs_template
{
"index_patterns": ["logs-*"],
"composed_of": [
"common_settings",
"common_mappings"
],
"priority": 100,
"template": {
"settings": {
"index.lifecycle.name": "logs_policy"
},
"mappings": {
"properties": {
"level": {
"type": "keyword"
}
}
}
}
}
1.7.3 模板优先级
// 优先级高的模板会覆盖优先级低的模板
PUT /_index_template/logs_template_general
{
"index_patterns": ["logs-*"],
"priority": 100,
"template": {
"settings": {
"number_of_shards": 1
}
}
}
PUT /_index_template/logs_template_specific
{
"index_patterns": ["logs-app-*"],
"priority": 200, // 优先级更高
"template": {
"settings": {
"number_of_shards": 3 // 会覆盖上面的设置
}
}
}
// 创建 logs-app-001
// 使用 logs_template_specific(优先级 200)
// number_of_shards = 3
二、文档(Document)深入理解
2.1 文档结构详解
{
// ==================== 元数据字段 ====================
"_index": "products", // 所属索引
"_id": "1", // 文档ID(唯一标识)
"_version": 3, // 版本号(每次更新+1)
"_seq_no": 5, // 序列号(全局唯一,递增)
"_primary_term": 1, // 主分片任期号
"_score": 1.2345, // 相关性评分(搜索时)
"_routing": "user123", // 路由值(可选)
// ==================== 原始数据 ====================
"_source": {
"name": "华为手机",
"price": 3999,
"category": "电子产品",
"tags": ["5G", "拍照"],
"createTime": "2024-01-17T10:00:00"
}
}
元数据字段说明:
┌──────────────────────────────────────────────────────────────┐
│ 字段 │ 说明 │
├──────────────────────────────────────────────────────────────┤
│ _index │ 文档所属的索引名称 │
│ _id │ 文档的唯一标识符 │
│ _version │ 文档版本号,用于乐观锁 │
│ _seq_no │ 序列号,用于并发控制 │
│ _primary_term │ 主分片任期号,用于并发控制 │
│ _score │ 相关性评分,搜索时计算 │
│ _routing │ 路由值,决定文档存储在哪个分片 │
│ _source │ 原始 JSON 文档 │
└──────────────────────────────────────────────────────────────┘
2.2 文档 ID 策略
2.2.1 自动生成 ID
// 自动生成 ID(使用 POST)
POST /products/_doc
{
"name": "小米手机",
"price": 2999
}
// 返回
{
"_index": "products",
"_id": "AWxyz123ABC", // 自动生成的 UUID
"_version": 1,
"result": "created"
}
自动生成 ID 的特点:
✓ 保证全局唯一
✓ 无需维护 ID 生成器
✓ 性能好(无需检查 ID 冲突)
✗ ID 无业务含义
✗ 无法通过 ID 直接查询
2.2.2 手动指定 ID
// 手动指定 ID(使用 PUT)
PUT /products/_doc/1
{
"name": "华为手机",
"price": 3999
}
// 返回
{
"_index": "products",
"_id": "1",
"_version": 1,
"result": "created"
}
手动指定 ID 的特点:
✓ ID 有业务含义
✓ 可以通过 ID 直接查询
✓ 方便与数据库主键对应
✗ 需要保证 ID 唯一性
✗ 可能存在 ID 冲突
2.2.3 使用业务 ID 作为文档 ID(推荐)
// Java 代码示例
@Service
public class ProductService {
@Autowired
private RestHighLevelClient esClient;
/**
* 同步商品到 ES
* 使用数据库主键作为 ES 文档 ID
*/
public void syncProductToES(Product product) throws IOException {
IndexRequest request = new IndexRequest("products")
.id(String.valueOf(product.getId())) // 使用数据库主键
.source(
"productId", product.getId(),
"name", product.getName(),
"price", product.getPrice(),
"category", product.getCategory()
);
esClient.index(request, RequestOptions.DEFAULT);
}
/**
* 根据数据库主键查询 ES 文档
*/
public Product getProductFromES(Long productId) throws IOException {
GetRequest request = new GetRequest("products", String.valueOf(productId));
GetResponse response = esClient.get(request, RequestOptions.DEFAULT);
if (response.isExists()) {
Map<String, Object> source = response.getSourceAsMap();
// 转换为 Product 对象
return convertToProduct(source);
}
return null;
}
}
最佳实践:
推荐:使用数据库主键作为 ES 文档 ID
✓ 方便数据同步
✓ 方便根据 ID 查询
✓ 方便更新和删除
✓ 保持数据一致性
示例:
MySQL: id=10001, name="华为手机"
ES: _id="10001", name="华为手机"
2.3 文档版本控制
2.3.1 内部版本控制
// 第一次创建文档
PUT /products/_doc/1
{
"name": "华为手机",
"price": 3999
}
// 返回:_version: 1
// 第一次更新
PUT /products/_doc/1
{
"name": "华为手机",
"price": 3899
}
// 返回:_version: 2
// 第二次更新
PUT /products/_doc/1
{
"name": "华为手机",
"price": 3799
}
// 返回:_version: 3
版本号的作用:
1. 乐观锁:防止并发更新冲突
2. 变更追踪:记录文档修改次数
3. 调试:帮助定位问题
2.3.2 乐观锁(基于版本号)
// 场景:两个用户同时修改商品价格
// 用户 A 读取文档(version=1)
GET /products/_doc/1
// 返回:price=3999, _version=1
// 用户 B 读取文档(version=1)
GET /products/_doc/1
// 返回:price=3999, _version=1
// 用户 A 更新(指定版本号)
PUT /products/_doc/1?if_seq_no=0&if_primary_term=1
{
"name": "华为手机",
"price": 3899
}
// 成功,version=2
// 用户 B 更新(使用旧版本号)
PUT /products/_doc/1?if_seq_no=0&if_primary_term=1
{
"name": "华为手机",
"price": 3799
}
// 失败!返回 409 冲突错误
Java 代码示例:
@Service
public class ProductService {
@Autowired
private RestHighLevelClient esClient;
/**
* 使用乐观锁更新商品价格
*/
public boolean updateProductPrice(Long productId, Double newPrice) {
try {
// 1. 先查询获取当前版本
GetRequest getRequest = new GetRequest("products", String.valueOf(productId));
GetResponse getResponse = esClient.get(getRequest, RequestOptions.DEFAULT);
if (!getResponse.isExists()) {
return false;
}
long seqNo = getResponse.getSeqNo();
long primaryTerm = getResponse.getPrimaryTerm();
// 2. 使用版本号更新
UpdateRequest updateRequest = new UpdateRequest("products", String.valueOf(productId))
.setIfSeqNo(seqNo)
.setIfPrimaryTerm(primaryTerm)
.doc("price", newPrice);
UpdateResponse updateResponse = esClient.update(updateRequest, RequestOptions.DEFAULT);
return updateResponse.getResult() == DocWriteResponse.Result.UPDATED;
} catch (ElasticsearchException e) {
if (e.status() == RestStatus.CONFLICT) {
// 版本冲突,可以重试
log.warn("版本冲突,商品ID: {}", productId);
return false;
}
throw new RuntimeException("更新失败", e);
} catch (IOException e) {
throw new RuntimeException("更新失败", e);
}
}
}
2.3.3 外部版本控制
// 使用外部版本号(如数据库的 version 字段)
PUT /products/_doc/1?version=5&version_type=external
{
"name": "华为手机",
"price": 3999
}
// 规则:
// 外部版本号必须 > 当前版本号
// 否则返回 409 冲突错误
使用场景:
场景:MySQL 数据同步到 ES
MySQL:
id=1, name="华为手机", price=3999, version=5
ES:
_id=1, name="华为手机", price=3999, _version=5
同步时使用外部版本号:
✓ 保证数据一致性
✓ 防止旧数据覆盖新数据
2.4 文档路由
2.4.1 默认路由
文档路由公式:
shard = hash(_id) % number_of_primary_shards
示例(3 个主分片):
doc1 → hash("1") % 3 = 1 → 存储在分片 1
doc2 → hash("2") % 3 = 2 → 存储在分片 2
doc3 → hash("3") % 3 = 0 → 存储在分片 0
路由流程:
客户端
↓
协调节点(任意节点)
↓
计算路由:shard = hash(_id) % number_of_primary_shards
↓
转发到目标分片所在节点
↓
主分片写入
↓
副本分片同步
↓
返回结果
2.4.2 自定义路由
// 使用自定义路由值
PUT /products/_doc/1?routing=user123
{
"userId": "user123",
"name": "华为手机",
"price": 3999
}
// 路由公式变为:
// shard = hash("user123") % number_of_primary_shards
// 查询时也要指定相同的路由值
GET /products/_doc/1?routing=user123
自定义路由的好处:
场景:用户订单系统
不使用自定义路由:
user123 的订单分散在所有分片
查询时需要查询所有分片
性能差
使用自定义路由(routing=userId):
user123 的所有订单在同一个分片
查询时只需查询一个分片
性能好
Java 代码示例:
@Service
public class OrderService {
@Autowired
private RestHighLevelClient esClient;
/**
* 创建订单(使用用户ID作为路由)
*/
public void createOrder(Order order) throws IOException {
IndexRequest request = new IndexRequest("orders")
.id(String.valueOf(order.getId()))
.routing(order.getUserId()) // 使用用户ID作为路由
.source(
"orderId", order.getId(),
"userId", order.getUserId(),
"productName", order.getProductName(),
"amount", order.getAmount()
);
esClient.index(request, RequestOptions.DEFAULT);
}
/**
* 查询用户的所有订单(只查询一个分片)
*/
public List<Order> getUserOrders(String userId) throws IOException {
SearchRequest request = new SearchRequest("orders")
.routing(userId) // 指定路由,只查询一个分片
.source(new SearchSourceBuilder()
.query(QueryBuilders.termQuery("userId", userId))
);
SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
// 转换结果
return Arrays.stream(response.getHits().getHits())
.map(hit -> convertToOrder(hit.getSourceAsMap()))
.collect(Collectors.toList());
}
}
2.4.3 路由的注意事项
⚠️ 注意事项:
1. 写入和查询必须使用相同的路由值
PUT /orders/_doc/1?routing=user123
GET /orders/_doc/1?routing=user123 // 必须指定
2. 路由值分布要均匀
❌ 不好:routing=category(类别少,分布不均)
✓ 好:routing=userId(用户多,分布均匀)
3. 路由会影响分片分布
如果路由值分布不均,会导致某些分片数据过多
4. 删除文档也要指定路由
DELETE /orders/_doc/1?routing=user123
2.5 文档操作
2.5.1 创建文档
方法 1:PUT(指定 ID)
PUT /products/_doc/1
{
"name": "华为手机",
"price": 3999
}
方法 2:POST(自动生成 ID)
POST /products/_doc
{
"name": "小米手机",
"price": 2999
}
方法 3:PUT + _create(仅创建,ID 存在则失败)
PUT /products/_create/1
{
"name": "华为手机",
"price": 3999
}
// 如果 ID=1 已存在,返回 409 冲突错误
2.5.2 查询文档
根据 ID 查询:
GET /products/_doc/1
批量查询:
GET /products/_mget
{
"ids": ["1", "2", "3"]
}
条件查询:
GET /products/_search
{
"query": {
"match": {
"name": "华为"
}
}
}
2.5.3 更新文档
方法 1:全量更新(覆盖)
PUT /products/_doc/1
{
"name": "华为手机",
"price": 3899
}
// 整个文档被替换
方法 2:部分更新
POST /products/_update/1
{
"doc": {
"price": 3899
}
}
// 只更新 price 字段,其他字段保持不变
方法 3:脚本更新
POST /products/_update/1
{
"script": {
"source": "ctx._source.price -= params.discount",
"params": {
"discount": 100
}
}
}
// 价格减少 100
方法 4:upsert(不存在则创建)
POST /products/_update/1
{
"doc": {
"price": 3899
},
"doc_as_upsert": true
}
// 如果文档不存在,则创建
2.5.4 删除文档
根据 ID 删除:
DELETE /products/_doc/1
根据条件删除:
POST /products/_delete_by_query
{
"query": {
"range": {
"price": {
"lt": 1000
}
}
}
}
// 删除价格小于 1000 的所有商品
2.5.5 批量操作(Bulk API)
POST /_bulk
{"index": {"_index": "products", "_id": "1"}}
{"name": "华为手机", "price": 3999}
{"index": {"_index": "products", "_id": "2"}}
{"name": "小米手机", "price": 2999}
{"update": {"_index": "products", "_id": "1"}}
{"doc": {"price": 3899}}
{"delete": {"_index": "products", "_id": "3"}}
Bulk API 的特点:
✓ 批量操作,性能高
✓ 减少网络开销
✓ 单个操作失败不影响其他操作
⚠️ 每行必须以换行符结尾
⚠️ 最后一行也要换行符
Java 代码示例:
@Service
public class ProductService {
@Autowired
private RestHighLevelClient esClient;
/**
* 批量同步商品到 ES
*/
public void batchSyncProducts(List<Product> products) throws IOException {
BulkRequest bulkRequest = new BulkRequest();
for (Product product : products) {
IndexRequest indexRequest = new IndexRequest("products")
.id(String.valueOf(product.getId()))
.source(
"productId", product.getId(),
"name", product.getName(),
"price", product.getPrice(),
"category", product.getCategory()
);
bulkRequest.add(indexRequest);
}
BulkResponse bulkResponse = esClient.bulk(bulkRequest, RequestOptions.DEFAULT);
// 检查是否有失败的操作
if (bulkResponse.hasFailures()) {
for (BulkItemResponse item : bulkResponse.getItems()) {
if (item.isFailed()) {
log.error("批量操作失败: {}, 原因: {}",
item.getId(), item.getFailureMessage());
}
}
}
}
}
2.6 文档的 _source 字段
2.6.1 什么是 _source?
{
"_index": "products",
"_id": "1",
"_source": { // 原始 JSON 文档
"name": "华为手机",
"price": 3999,
"category": "电子产品"
}
}
_source 的作用:
1. 返回原始文档:查询时返回完整数据
2. 部分更新:更新时需要读取原始数据
3. Reindex:重建索引时需要原始数据
4. 高亮显示:搜索结果高亮需要原始文本
2.6.2 禁用 _source
PUT /products
{
"mappings": {
"_source": {
"enabled": false // 禁用 _source
},
"properties": {
"name": { "type": "text" },
"price": { "type": "double" }
}
}
}
禁用 _source 的影响:
✓ 节省存储空间(约 30-50%)
✗ 无法返回原始文档
✗ 无法部分更新
✗ 无法 Reindex
✗ 无法高亮显示
适用场景:
– 只需要搜索,不需要返回原始数据
– 数据量特别大,存储成本高
– 数据可以从其他地方获取
2.6.3 过滤 _source
PUT /products
{
"mappings": {
"_source": {
"includes": ["name", "price"], // 只存储这些字段
"excludes": ["description"] // 排除这些字段
}
}
}
查询时过滤 _source:
// 只返回指定字段
GET /products/_search
{
"_source": ["name", "price"],
"query": {
"match_all": {}
}
}
// 排除指定字段
GET /products/_search
{
"_source": {
"excludes": ["description"]
},
"query": {
"match_all": {}
}
}
三、映射(Mapping)
3.1 什么是映射?
映射定义了文档的结构和字段类型,类似于 MySQL 的表结构。
MySQL: CREATE TABLE products (
id INT,
name VARCHAR(100),
price DECIMAL(10,2)
);
ES: PUT /products
{
"mappings": {
"properties": {
"id": { "type": "long" },
"name": { "type": "text" },
"price": { "type": "double" }
}
}
}
3.2 动态映射 vs 静态映射
动态映射(自动推断):
POST /products/_doc/1
{
"name": "华为手机",
"price": 3999,
"onSale": true
}
// ES 自动推断类型:
// name → text
// price → long
// onSale → boolean
静态映射(手动定义):
PUT /products
{
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word"
},
"price": {
"type": "double"
},
"onSale": {
"type": "boolean"
}
}
}
}
⚠️ 映射一旦创建,字段类型不可修改!
四、数据类型
4.1 常用数据类型
| text | 全文检索字段,会分词 | 商品名称、描述 |
| keyword | 精确匹配字段,不分词 | 状态、标签、ID |
| long | 长整型 | 商品ID、数量 |
| integer | 整型 | 年龄、评分 |
| double | 双精度浮点 | 价格、评分 |
| boolean | 布尔值 | 是否上架 |
| date | 日期 | 创建时间 |
| object | 对象 | 嵌套数据 |
| nested | 嵌套对象 | 数组对象 |
4.2 text vs keyword
{
"mappings": {
"properties": {
"title": {
"type": "text", // 用于全文搜索
"fields": {
"keyword": {
"type": "keyword" // 用于精确匹配、排序、聚合
}
}
}
}
}
}
使用场景:
// 全文搜索(使用 text)
GET /products/_search
{
"query": {
"match": { "title": "华为手机" }
}
}
// 精确匹配(使用 keyword)
GET /products/_search
{
"query": {
"term": { "title.keyword": "华为 Mate 50" }
}
}
// 排序(使用 keyword)
GET /products/_search
{
"sort": [
{ "title.keyword": "asc" }
]
}
4.3 日期类型
{
"mappings": {
"properties": {
"createTime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
}
支持的格式:
// 字符串
{ "createTime": "2024-01-17 10:30:00" }
// 时间戳(毫秒)
{ "createTime": 1705460400000 }
4.4 对象类型
{
"mappings": {
"properties": {
"user": {
"properties": {
"name": { "type": "text" },
"age": { "type": "integer" }
}
}
}
}
}
// 文档示例
{
"user": {
"name": "张三",
"age": 25
}
}
4.5 nested 类型
为什么需要 nested?
// 使用 object(错误)
{
"comments": [
{ "user": "张三", "content": "很好" },
{ "user": "李四", "content": "不错" }
]
}
// ES 内部存储为:
{
"comments.user": ["张三", "李四"],
"comments.content": ["很好", "不错"]
}
// 问题:无法保持对象关联!
使用 nested(正确):
{
"mappings": {
"properties": {
"comments": {
"type": "nested",
"properties": {
"user": { "type": "keyword" },
"content": { "type": "text" }
}
}
}
}
}
五、分片与副本
5.1 分片的作用
单机存储限制:1TB
↓
分片到3个节点
↓
每个节点:333GB
↓
支持水平扩展
5.2 分片策略
PUT /products
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
}
}
分片数量建议:
数据量 < 10GB:1个分片
数据量 10-100GB:3-5个分片
数据量 > 100GB:根据节点数和数据量计算
计算公式:
分片数 = 数据总量 / 单分片目标大小(20-50GB)
5.3 副本的作用
┌─────────────────────────────────────────┐
│ Node 1 Node 2 Node 3 │
├─────────────────────────────────────────┤
│ Shard 0 Shard 1 Shard 2 │ 主分片
│ Replica 1 Replica 2 Replica 0 │ 副本分片
└─────────────────────────────────────────┘
作用:
1. 高可用:节点故障时自动切换
2. 负载均衡:读请求分散到副本
六、索引模板
6.1 为什么需要模板?
场景:每天创建一个日志索引
logs-2024-01-01
logs-2024-01-02
logs-2024-01-03
…
问题:每次都要手动设置映射和配置?
解决:使用索引模板!
6.2 创建模板
PUT /_index_template/logs_template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"timestamp": { "type": "date" },
"level": { "type": "keyword" },
"message": { "type": "text" }
}
}
}
}
自动应用:
// 创建索引时自动应用模板
PUT /logs–2024–01–17
// 自动继承 logs_template 的配置
七、实战:设计商品索引
PUT /products
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"ik_analyzer": {
"type": "custom",
"tokenizer": "ik_max_word"
}
}
}
},
"mappings": {
"properties": {
"productId": {
"type": "long"
},
"name": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"category": {
"type": "keyword"
},
"price": {
"type": "double"
},
"stock": {
"type": "integer"
},
"onSale": {
"type": "boolean"
},
"description": {
"type": "text",
"analyzer": "ik_max_word"
},
"tags": {
"type": "keyword"
},
"createTime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"images": {
"type": "keyword",
"index": false
}
}
}
}
学习检查清单
- 理解索引、文档、映射的关系
- 掌握常用数据类型的使用场景
- 理解 text 和 keyword 的区别
- 理解分片和副本的作用
- 能够设计合理的索引结构
- 了解索引模板的使用
下一步
下一节我们将学习 Elasticsearch 的基本操作和 RESTful API。
网硕互联帮助中心





评论前必须登录!
注册