ElasticSearch 总结
ES 思维导图

%% 完整的ES实战决策图 - 基于Leads系统真实案例
flowchart TD
%% ========== 字段类型演进 ==========
TypeEvolution([ES 5.0 类型演进]) --> StringSplit{string类型拆分}
StringSplit -->|全文搜索| TextType[text类型]
StringSplit -->|精确匹配/聚合/排序| KeywordType[keyword类型]
%% ========== 字段设计部分 ==========
Start([开始ES字段设计]) --> FieldType{需要索引该字段吗?}
FieldType -->|否| Skip[不索引该字段]
FieldType -->|是| DataType{数据类型?}
DataType -->|文本| SearchType{需要分词搜索吗?}
DataType -->|数值/日期| NumericType[数值/日期类型<br/>天然支持聚合排序]
DataType -->|对象数组| NestedDecision{需要保持对象边界?}
SearchType -->|是| TextType
SearchType -->|否| KeywordType
%% text类型配置
TextType --> TextConfig[配置text字段]
TextConfig --> TextAnalyzer{选择分词器}
TextAnalyzer -->|中文| IKAnalyzer[ik_max_word]
TextAnalyzer -->|英文| StandardAnalyzer[standard]
TextConfig --> NeedExact{需要精确匹配/排序/聚合?}
NeedExact -->|是| AddKeyword[添加keyword子字段<br/>fields特性]
NeedExact -->|否| TextOnly[仅text类型]
%% text限制说明
TextOnly --> TextLimit[/"⚠️ text限制:<br/>倒排索引分词后丢失原始值<br/>token→docs结构不支持聚合排序"/]
AddKeyword --> KeywordName{子字段命名}
KeywordName -->|通用| Raw[.raw]
KeywordName -->|业务| Keyword[.keyword]
%% keyword类型配置
KeywordType --> KeywordConfig[配置keyword字段]
AddKeyword --> LengthCheck1{字段可能超长吗?}
KeywordConfig --> LengthCheck2{字段可能超长吗?}
LengthCheck1 -->|是| SetLimit1[设置ignore_above]
LengthCheck1 -->|否| NoLimit1[不设置ignore_above]
LengthCheck2 -->|是| SetLimit2[设置ignore_above]
LengthCheck2 -->|否| NoLimit2[不设置ignore_above]
%% ========== nested vs object ==========
NestedDecision -->|是| NestedType[nested类型<br/>每个对象独立隐藏文档]
NestedDecision -->|否| ObjectType[object类型<br/>字段扁平化合并]
ObjectType --> ObjectWarning[/"⚠️ 风险:跨对象错误匹配<br/>A.street + B.city 可能被错误关联"/]
NestedType --> NestedBenefit[/"✓ 保持对象完整性<br/>查询时边界清晰"/]
%% ========== 聚合方案演进 ==========
AggEvolution([聚合方案演进]) --> OldAgg{早期方案}
OldAgg --> Fielddata[fielddata<br/>内存密集型]
Fielddata --> FielddataIssue[/"⚠️ 问题:<br/>高内存消耗 + GC压力<br/>ES 7.x+ 标记为legacy"/]
OldAgg --> ModernAgg[现代方案]
ModernAgg --> DocValues[doc_values<br/>列式存储]
DocValues --> DocValuesBenefit[/"✓ 优势:<br/>磁盘友好 + OS缓存管理"/]
%% ========== 查询构建部分 ==========
QueryStart([开始构建查询]) --> QueryType{查询类型?}
QueryType -->|精确匹配| TermQuery[term/terms]
QueryType -->|范围查询| RangeQuery[range]
QueryType -->|模糊搜索| MatchQuery[match/match_phrase]
QueryType -->|通配符| WildcardQuery[wildcard]
QueryType -->|嵌套对象| NestedQuery[nested]
%% 组合查询
ComplexQuery{需要组合条件?} -->|是| BoolQuery[bool查询]
ComplexQuery -->|否| SimpleQuery[单条件查询]
BoolQuery --> MustClause[must - 必须满足]
BoolQuery --> ShouldClause[should - 满足其一]
BoolQuery --> MustNotClause[must_not - 必须不满足]
%% 高级特性
Advanced{高级需求?} -->|大数据分页| SearchAfter[search_after分页]
Advanced -->|字段去重| Collapse[collapse去重]
Advanced -->|聚合统计| Aggregation[aggs聚合]
%% search_after详细流程
SearchAfter --> SearchFlow[search_after流程]
SearchFlow --> CheckCursor{是否有cursor?}
CheckCursor -->|是| GetLastDoc[获取最后一条记录]
CheckCursor -->|否| FirstPage[第一页查询]
GetLastDoc --> SetSearchAfter[设置search_after参数]
SetSearchAfter --> ExecuteQuery[执行查询]
FirstPage --> ExecuteQuery
ExecuteQuery --> ReturnResults[返回结果]
ReturnResults --> NextPage{需要下一页?}
NextPage -->|是| UpdateCursor[更新cursor]
UpdateCursor --> GetLastDoc
NextPage -->|否| EndQuery[结束查询]
%% ========== Mapping与Fields本质 ==========
MappingEssence([Mapping本质]) --> MappingDef["每个顶层key = 字段名<br/>对应对象 = 完整配置集合"]
MappingDef --> MappingContent["包含:类型、分析器、<br/>存储选项等属性"]
FieldsEssence([fields vs nested]) --> FieldsUse["fields: 同一字段多视角索引<br/>如 name.text + name.keyword"]
FieldsEssence --> NestedUse["nested: 子对象边界维护<br/>数组中每个对象独立"]
%% ========== 实际案例 ==========
Examples([实际案例]) --> Case1["用户姓名: text + keyword.raw"]
Examples --> Case2["电话号码: keyword(ignore_above:20)"]
Examples --> Case3["创意名称: text + keyword.keyword"]
Examples --> Case4["分页查询: search_after + sort"]
Examples --> Case5["去重查询: collapse + field"]
Examples --> Case6["地址数组: nested类型保持对象边界"]
Examples --> Case7["商品属性: doc_values支持聚合"]
%% 连接关系
SetLimit1 --> Examples
SetLimit2 --> Examples
QueryStart --> Examples
TypeEvolution --> Start
AggEvolution --> Aggregation
NestedBenefit --> Examples
DocValuesBenefit --> Examples
%% 样式定义
classDef decisionBox fill:#e1f5fe,stroke:#01579b,stroke-width:2px
classDef configBox fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
classDef exampleBox fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
classDef searchBox fill:#fff3e0,stroke:#e65100,stroke-width:2px
classDef queryBox fill:#fce4ec,stroke:#c2185b,stroke-width:2px
classDef warningBox fill:#ffebee,stroke:#c62828,stroke-width:2px
classDef evolutionBox fill:#fff8e1,stroke:#f57f17,stroke-width:2px
classDef essenceBox fill:#e0f2f1,stroke:#00695c,stroke-width:2px
class FieldType,SearchType,NeedExact,LengthCheck1,LengthCheck2,KeywordName,QueryType,ComplexQuery,Advanced,CheckCursor,NextPage,DataType,NestedDecision,OldAgg decisionBox
class TextConfig,KeywordConfig,SetLimit1,SetLimit2,SearchFlow,SearchAfter,Collapse,Aggregation,NestedType,ObjectType,Fielddata,DocValues configBox
class Examples,Case1,Case2,Case3,Case4,Case5,Case6,Case7 exampleBox
class TermQuery,RangeQuery,MatchQuery,WildcardQuery,NestedQuery,BoolQuery,MustClause,ShouldClause,MustNotClause queryBox
class GetLastDoc,SetSearchAfter,ExecuteQuery,ReturnResults,UpdateCursor,EndQuery searchBox
class TextLimit,ObjectWarning,FielddataIssue warningBox
class TypeEvolution,AggEvolution,StringSplit evolutionBox
class MappingEssence,MappingDef,MappingContent,FieldsEssence,FieldsUse,NestedUse,NestedBenefit,DocValuesBenefit essenceBox
ES 的定位
- ES 是 build on top of Lucene 建立的可以集群化部署的搜索引擎。
- ES 可以是 document store(此处可以理解为文档仓库或者文档存储),可以结构化解决数据仓库存储的问题。在 es 中一切皆对象,使用对象对数据建模可以很好地处理万事万物的关系。
- ES 是海量数据的分析工具能够支持:搜索、分析(这一条其实我们很少用到)和实时统计。
ES 的架构有优越的地方:
- 自己使用 pacifica 协议,写入完成就达成共识。
- 节点对内对外都可以使用 RESTful API(or json over http)来通信,易于调试。
- 因为它有很多很好的默认值,api 也是 convention over configuration的,所以开箱即用。
- 它天生就是分布式的,可以自己管理多节点-它的路由机制是一个方便的,需要优化也可以优化的机制。
ES 的架构
ES 是基于 Lucene 的,集群上的每个 node 都有一个 Lucene 的实例。而 Lucene 本身是没有 type 的,所以 ES 最终也去掉了 type。
ES 中每个节点自己都能充当其他节点的 proxy,每个节点都可以成为 primary。用户需要设计拓扑的时候只需要关注种子节点和 initial master 即可。
字段类型演进
Elasticsearch 5.0 的重大变更:将早期的单一 string 类型拆分为两种专用类型:
- text:用于全文搜索,会经过分词器处理
- keyword:用于精确匹配、聚合和排序,保持原始值不变
这一设计变更的根本原因在于两种场景对数据结构的需求完全不同:
- text 字段的限制根源:倒排索引的分词特性会将原始值拆分为独立 token,不仅丢失原始完整值,其
token → documents的数据结构本质也不支持高效的聚合和排序操作 - keyword 字段的优势:保持原始值完整,配合
doc_values列式存储,天然支持聚合和排序
ES 请求体结构解析
理解 Elasticsearch 请求体的层次结构是正确使用 ES 的基础。很多初学者容易混淆各个组件之间的关系,本节将系统性地梳理这些概念。
顶层结构
一个完整的 ES 搜索请求体由以下顶层字段组成,它们是兄弟关系:
1 | |
关键理解:query、aggs、sort 是顶层的兄弟关系,而不是嵌套关系。
query 的内部结构
query 内部最常用的是 bool 复合查询,它包含四个子句:
1 | |
filter 的位置问题
filter 不是顶层字段,而是 bool 查询的子句。这是很多人困惑的地方。
正确写法:filter 在 bool 内部
1 | |
错误写法:filter 不能作为顶层字段
1 | |
filter 可以嵌套在任意层次
filter 可以出现在 bool 查询的任何层级:
1 | |
上例中,filter 出现在两个层级:
- 外层
bool的filter:过滤价格 - 内层嵌套
bool的filter:过滤分类
term/match 查询 vs filter 的区别
这是一个常见的困惑:既然 term、terms、match 等查询已经可以做等值匹配,为什么还需要 filter?
核心区别
| 维度 | term/match 等查询(在 must/should 中) | filter |
|---|---|---|
| 上下文 | 查询上下文(Query Context) | 过滤上下文(Filter Context) |
| 是否计算评分 | 是,计算 _score |
否,不计算评分 |
| 是否缓存 | 否 | 是,结果会被缓存 |
| 性能 | 较慢 | 较快 |
| 返回结果 | 文档 + 相关性评分 | 仅文档(评分为 0 或常量) |
什么时候用 must 中的 term/match
当你需要相关性排序时,使用 must 或 should 中的查询:
1 | |
这个查询会:
- 找出所有 title 包含这些词的文档
- 计算每个文档的相关性评分(包含词越多、越精确,评分越高)
- 按评分排序返回
什么时候用 filter
当你只需要是/否判断,不关心匹配程度时,使用 filter:
1 | |
这个查询会:
- 找出所有
status=published且price在 100-500 之间的文档 - 不计算评分(所有匹配文档评分相同)
- 结果会被缓存,下次相同过滤条件直接复用
最佳实践:组合使用
实际场景中,通常组合使用两者:
1 | |
上述查询中:
must中的match:需要评分,用于全文搜索filter中的term和range:不需要评分,用于精确过滤
决策原则:
- 需要评分/排序 → 放在
must或should中 - 只需要是/否判断 → 放在
filter中
性能对比示例
假设有 100 万文档,查询条件是 status=active AND category=tech:
方式一:全部用 must(不推荐)
1 | |
- 每个文档都要计算评分
- 结果不缓存
- 每次查询都要重新计算
方式二:使用 filter(推荐)
1 | |
- 不计算评分,二元判断
- 结果被缓存到 filter cache
- 后续相同查询直接命中缓存
总结
| 场景 | 推荐方式 |
|---|---|
| 全文搜索(需要相关性排序) | must + match |
| 精确匹配(状态、类型、ID) | filter + term |
| 范围过滤(时间、价格) | filter + range |
| 组合场景 | must 放需要评分的,filter 放不需要评分的 |
常见误区:SQL 思维导致的 term 滥用
问题现象:很多开发者(尤其是有 SQL 背景的)习惯这样写查询:
1 | |
他们的思维是:term 类似 SQL 的 =,terms 类似 SQL 的 IN,放在 must 里就像 SQL 的 WHERE 子句。这种写法能工作,但是错误的做法。
为什么这是错的?
-
官方明确推荐:Elastic 官方博客指出:
“A rule of thumb is to use filters when you can and queries when you must: when you need the actual scoring from the queries. Filter First.”
-
term 查询的正确用法(来自 Opster 官方指南):
“Using the term filter in a filter context can improve performance as it skips the scoring phase.”
-
性能差异:
must中的term:每个文档都要计算 TF-IDF 评分,结果不缓存filter中的term:跳过评分计算,结果自动缓存
深入理解:为什么 term 查询计算评分没有意义?
你可能会问:term 是等值匹配,计算评分有什么意义?答案是:确实没有意义。
当 term 查询放在 must 中时,ES 仍然会使用 BM25(或 TF-IDF)算法计算评分:
| 评分因子 | 对于 term 查询的实际意义 |
|---|---|
| TF(词频) | 对于 keyword 字段,值要么完全匹配(TF=1),要么不匹配(TF=0)。没有"匹配程度"的概念 |
| IDF(逆文档频率) | 表示这个值在整个索引中有多"稀有"。但你查 status=active,你关心的是"是否等于 active",而不是"active 有多稀有" |
| fieldNorm(字段长度归一化) | 对于 keyword 字段,长度固定,这个因子也没有意义 |
实际例子:假设你查询 status = "active",ES 会对每个匹配文档计算 score = 1 × log(N/df) × 1/√length。但所有匹配文档的评分都是相同的(因为它们都是精确匹配)——你花费了 CPU 时间计算评分,但这个评分完全没有用!
唯一有意义的场景:term 在 must 中计算评分唯一有意义的场景是与其他需要评分的查询组合时,用于调整最终评分(如 should 中的加分项)。但即使这种场景,也可以用 boost 参数更精确地控制。
ES 为什么这样设计? 这是 ES 的统一抽象:bool.must 和 bool.should 运行在 Query Context,所有查询都会计算评分;bool.filter 和 bool.must_not 运行在 Filter Context,所有查询都不计算评分。ES 不会根据查询类型自动切换上下文,上下文由你放置的位置决定。
| 查询类型 | 评分是否有意义 | 应该放在哪里 |
|---|---|---|
match(全文搜索) |
✅ 有意义(匹配程度不同) | must 或 should |
term(精确匹配) |
❌ 无意义(要么匹配要么不匹配) | filter |
terms(多值匹配) |
❌ 无意义 | filter |
range(范围查询) |
❌ 无意义 | filter |
exists(字段存在) |
❌ 无意义 | filter |
正确写法:
1 | |
SQL 到 ES 的正确映射:
| SQL 语法 | ❌ 错误的 ES 写法 | ✅ 正确的 ES 写法 |
|---|---|---|
WHERE status = 'active' |
bool.must + term |
bool.filter + term |
WHERE category IN ('a','b') |
bool.must + terms |
bool.filter + terms |
WHERE price BETWEEN 100 AND 500 |
bool.must + range |
bool.filter + range |
WHERE title LIKE '%keyword%' |
- | bool.must + match(需要评分时) |
核心原则:
只有当你需要相关性评分来排序结果时,才使用 Query Context(must/should)。
对于精确匹配、范围过滤等"是/否"判断,永远使用 Filter Context。
实际业务场景对照:
| 业务场景 | 是否需要评分 | 推荐写法 |
|---|---|---|
| 按状态筛选订单 | 否 | filter + term |
| 按用户 ID 查询 | 否 | filter + term |
| 按时间范围筛选 | 否 | filter + range |
| 按分类筛选商品 | 否 | filter + terms |
| 搜索商品标题 | 是(需要按相关性排序) | must + match |
| 搜索文章内容 | 是(需要按相关性排序) | must + match |
混合场景的正确写法:
当你既需要全文搜索(需要评分),又需要精确过滤(不需要评分)时:
1 | |
这个查询的执行逻辑:
- filter 先执行:快速过滤出符合条件的文档子集(利用缓存)
- must 后执行:只对过滤后的文档计算相关性评分
- 结果排序:按
_score降序返回
query vs aggs 的关系
| 维度 | query | aggs |
|---|---|---|
| 作用 | 筛选文档 | 对文档进行统计分析 |
| 位置 | 顶层字段 | 顶层字段 |
| 关系 | 兄弟关系 | 兄弟关系 |
| 执行顺序 | 先执行 query 筛选 | 再对筛选结果进行聚合 |
1 | |
执行流程:先通过 query 筛选出 status=active 的文档,再对这些文档按 category 进行聚合。
Metrics vs Bucket 聚合的关系
两者都是 aggs 内部的聚合类型,可以组合使用:
1 | |
上例结构说明:
by_category:Bucket 聚合,按分类分桶avg_price:Metrics 聚合,计算每个桶的平均价格by_brand:嵌套 Bucket 聚合,在每个分类桶内再按品牌分桶
| 聚合类型 | 作用 | 输出 |
|---|---|---|
| Bucket | 将文档分组到不同的桶 | 多个桶,每个桶包含文档子集 |
| Metrics | 对文档进行数值计算 | 单个或多个数值 |
完整请求体示例
1 | |
结构层次总结
1 | |
ES 中的搜索
全文搜索
按照《全文搜索》中的例子,使用 match 总会触发全文搜索。因为在Elasticsearch中,每一个字段的数据都是默认被索引的。也就是说,每个字段专门有一个反向索引用于快速检索。
注意:
"index": "not_analyzed"是 ES 2.x 及更早版本的语法。在 ES 5.0+ 中,应使用"index": false禁用索引,或直接使用keyword类型实现不分词的精确匹配。
精确(短语)搜索
精确查询的方法有:
短语查询:
1 | |
这种查询可以查出rock climbing和a rock climbing。
term 查询(这种查询是客户端查询里最常用的):
1 | |
这种查询可以查出rock climbing,不可以查出a rock climbing。
另一种 phrase 查询:
1 | |
精确和不精确查询有好几种方法,详见《elasticsearch 查询(match和term)》 和《finding_exact_values》。
注意,ES 的查询可以有很多的变种,以下几个查询请求体在语义上等价:
基础 match 查询:
1 | |
嵌套形式(content 是字段名,query 是查询值):
1 | |
使用 bool 子句进行查询:
1 | |
使用 term 进行精确查询:
注意:即使文档的原始 content 是"我的宝马多少马力",如果该字段被分词器处理过,term 查询可能无法匹配,因为 term 查询不会对查询词进行分词。
1 | |
使用 terms 进行多值匹配:
1 | |
历史语法说明:
filtered查询是 ES 2.x 及更早版本的语法,在 ES 5.0+ 中已被移除,应使用bool查询的filter子句替代。
elastic-search查询字句整理
search api 的搜索
参考《空查询》。
倒排索引简析
倒排索引的 key 是 term,value 是文档的指针,可以参考这个《倒排索引》文档。
过滤机制
Elasticsearch 的查询分为两种上下文:查询上下文(Query Context) 和 过滤上下文(Filter Context)。
查询上下文 vs 过滤上下文
| 特性 | 查询上下文 | 过滤上下文 |
|---|---|---|
| 核心问题 | 文档与查询的匹配程度如何? | 文档是否匹配查询条件? |
| 返回结果 | 匹配的文档 + 相关性评分(_score) | 匹配的文档(无评分) |
| 是否缓存 | 不缓存 | 自动缓存,可复用 |
| 性能 | 较慢(需计算评分) | 较快(二元判断 + 缓存) |
| 适用场景 | 全文搜索、需要排序 | 精确匹配、范围过滤 |
使用方式
在 bool 查询中,must 和 should 子句运行在查询上下文,而 filter 和 must_not 子句运行在过滤上下文:
1 | |
最佳实践:对于不需要评分的条件(如状态过滤、时间范围),应放入 filter 子句以获得更好的性能和缓存效果。
HTTP 头机制
Elasticsearch 支持通过 HTTP 头传递元信息,用于请求追踪、兼容性控制等场景。
X-Opaque-Id
X-Opaque-Id 是一个用于请求追踪的 HTTP 头,从 ES 6.2.0 开始支持。通过为每个请求指定唯一标识符,可以在日志中追踪请求来源。
使用方式:
1 | |
应用场景:
- 在慢查询日志(Slow Log)中关联请求来源
- 在任务管理 API 中追踪长时间运行的任务
- 在废弃日志中定位使用了过时 API 的请求
其他常用 HTTP 头
| HTTP 头 | 用途 |
|---|---|
Content-Type |
指定请求体格式,通常为 application/json |
Accept |
指定响应格式 |
Authorization |
身份认证信息(Basic Auth 或 Bearer Token) |
X-Pack
X-Pack 是 Elasticsearch 的官方扩展功能包,从 ES 6.3 开始,基础安全功能已免费开放。ES 8.0 后,安全功能默认启用。
核心功能模块
安全(Security)
- 身份认证:支持多种认证方式(Native、LDAP、Active Directory、PKI、SAML、OIDC)
- 授权控制:基于角色的访问控制(RBAC),支持索引级、字段级、文档级权限
- 传输加密:节点间通信和客户端通信的 TLS/SSL 加密
- 审计日志:记录安全相关事件
监控(Monitoring)
实时监控集群健康状态、节点性能、索引统计等指标,可通过 Kibana 可视化展示。
告警(Alerting/Watcher)
基于条件触发的告警机制,支持邮件、Webhook、Slack 等通知方式。
机器学习(Machine Learning)
异常检测、预测分析等功能,用于日志分析、APM 等场景。
SQL
提供 SQL 查询接口,降低 DSL 学习成本:
1 | |
版本控制机制
Elasticsearch 使用乐观并发控制(Optimistic Concurrency Control)来处理并发写入冲突。
版本控制演进
早期方案:_version 字段
ES 6.x 及更早版本使用 _version 字段进行并发控制:
1 | |
局限性:在分布式环境下,主分片切换时可能导致版本号不一致。
现代方案:_seq_no + _primary_term
ES 6.7+ 引入了更健壮的并发控制机制:
- _seq_no:序列号,每次写操作递增
- _primary_term:主分片任期号,主分片切换时递增
使用方式:
1 | |
如果文档在读取后被其他请求修改,更新将失败并返回 version_conflict_engine_exception,客户端需要重新读取后再次尝试更新。
冲突处理策略
| 策略 | 说明 |
|---|---|
| 重试 | 使用 retry_on_conflict 参数自动重试 |
| 最后写入胜出 | 不使用版本控制,接受数据覆盖 |
| 业务层处理 | 捕获冲突异常,由业务逻辑决定如何处理 |
容量与分片
为何分片不宜过大?
- 增加索引读压力-不利于并行查询。
- 不利于集群扩缩容(分片迁移耗费较长时间)。
- 集群发生故障时,恢复时间较长。
类似的问题也会发生在 Redis 之类的架构方案里。
解决大分片的方法
- 索引按月、日进行分割。
- delete_by_query 方法删除索引对索引进行缩容。
- 模板增加主分片数量-存疑,通常只能增加副本数,而无法改变切片比例。
- 扩容数据节点。
- 减小单位分片大小。
一个可实操的方案:
- 修改写入逻辑和 mapping,让新写入的 doc 的 field 变少。
- 对老的索引进行 delete_by_query。
常用查询段
- aggs
- match
- 等于分词以后的 or 查询,依赖于 analyzer。“hello world”如果被分词为 hello 和 world,则类似
term = hello or term = wolrd。
- 等于分词以后的 or 查询,依赖于 analyzer。“hello world”如果被分词为 hello 和 world,则类似
- match_phrase
- term 出现在一个句子中,句子包含 term,依赖于 analyzer。类似
contains("hello world") - 这是 String.contains。
- 不匹配 slop,还不如用 term。
- term 出现在一个句子中,句子包含 term,依赖于 analyzer。类似
- page:
- range:是大于等于小于的意思,不是 from 和 size
- search_after
- term
- term 类似于 Collection.contains
"[hello, world]".contains("hello")和"[hello]".terms("hello")都成立。
- term 类似于 Collection.contains
- terms:term 的数组形式,类似
in,是多值或的意思。 - wildcard:
- 允许指定匹配的正则式。它使用标准的 shell 通配符查询:? 匹配任意字符,* 匹配 0 或多个字符。
写入流程

-
同步双写:对数据的实时性要求极高,通常在一个事务中完成数据的双写动作,保证数据层面的强一致性;
-
异步解耦:在完成数据库的写动作之后,基于MQ消息解耦索引的写入,流程存在轻微的延迟,如果消费失败会导致数据缺失;
-
定时任务:通过任务调度的方式,以指定的时间周期执行新增数据的同步机制,存在明显的时效问题;
-
组件同步:采用合适的同步组件,比如官方提供的组件或者一些第三方开源的组件,在原理上与任务同步类似;
数据同步的选型方案有多种,如何选择完全看具体的场景,在过往的使用过程中,对于核心业务会采用同步双写,对于内部的活动类业务会采用异步的方式,对于业务日志会采用任务调度,对于系统的监控或执行日志则多是依赖同步组件;
数据同步

设计难点
翻页
ES中常用From/Size进行分页查询,但是存在一个限制,在索引的设置中存在max_result_window分页深度的限制,6.8版本默认值是10000条,即10000之后的数据无法使用From/Size翻页;
先从实际应用场景来分析,大多数的翻页需求最多也就前10页左右,所以从这个角度考虑,ES的翻页限制在合理区间,在实践中也存在对部分索引调高的情况,暂未出现明显问题;
再从技术角度来思考一下,如果翻页的参数过大意味着更多的数据过滤,那计算资源的占用也会升高,ES引擎的强大在于搜索能力,检索出符合要求的数据即可;
索引设计
ES的底层是Lucene,可以说Lucene的查询性能就决定了ES的查询性能。Lucene内最核心的倒排索引,本质上就是Term到所有包含该Term的文档的DocId列表的映射。ES 默认会对写入的数据都建立索引,并且常驻内存,主要采用了以下几种数据结构:
- 倒排索引:保存了每个term对应的docId的列表,采用skipList的结构保存,用于快速跳跃。
- FST(Finite State Transducer):原理上可以理解为前缀树,用于保存term字典的二级索引,用于加速查询,可以在FST上实现单Term、Term范围、Term前缀和通配符查询等。内部结构如下:

- BKD-Tree:BKD-Tree是一种保存多维空间点的数据结构,主要用于数值类型(包括空间点)的快速查找。
Mapping 本质
Mapping 是 ES 中定义文档结构的核心配置:
- 结构定义:mapping 中每个顶层 key 代表一个字段名,其对应的对象是该字段的完整配置集合(包含类型、分析器、存储选项等属性)
- 示例:
1 | |
fields 多字段特性
适用范围:并非所有字段都需要 fields,该特性主要用于 text 字段(如创建 keyword 子字段)。数值、日期等类型通常无需 fields,因其本身已支持聚合/排序。
本质:fields 是同一字段的多视角索引
name(text 视角):用于全文搜索name.keyword(keyword 视角):用于精确匹配、聚合和排序
nested 嵌套类型
必要性:当字段是对象数组时,必须使用 nested 类型。否则倒排索引会将数组内所有对象的同名字段合并为扁平列表,导致跨对象的错误匹配。
问题示例:假设有如下地址数组
1 | |
如果使用普通 object 类型,ES 会将其扁平化为:
1 | |
此时查询 city=北京 AND street=南京路 会错误匹配该文档!
解决方案:使用 nested 类型,将数组中的每个对象视为独立隐藏文档,确保查询时对象完整性。
fields vs nested 本质对比
| 特性 | fields | nested |
|---|---|---|
| 目的 | 同一字段的多视角索引 | 子对象边界维护 |
| 场景 | text 字段需要聚合/排序 | 对象数组需要精确查询 |
| 存储 | 同一字段多种索引方式 | 每个对象独立隐藏文档 |
| 示例 | name.text + name.keyword |
地址数组中每个地址独立 |
字段存储
- 行存(stored fields , _source)
- 列存(doc_value)
聚合
聚合方案演进
早期方案 - fielddata(已废弃):
- 通过内存密集型的
fielddata支持 text 字段聚合 - 问题:导致高内存消耗和 GC 压力
- 当前状态:在 ES 7.x+ 中已被标记为 legacy,官方强烈建议避免使用
- 特殊场景:仅在需要对 text 字段的分词结果进行聚合时才显式启用,且必须配合适当的内存限制和频率过滤
现代方案 - doc_values + keyword 子字段:
- 为 text 字段添加 keyword 子字段
- 依赖列式存储的
doc_values(磁盘友好、OS 缓存管理) - 优势:
- 磁盘存储,不占用 JVM 堆内存
- 由操作系统管理缓存,更高效
- 支持聚合、排序、脚本访问
1 | |
聚合时使用 title.keyword 而非 title。
指标聚合(Metrics Aggregation)
指标聚合用于对文档字段进行数值计算,类似于 SQL 中的聚合函数。
单值指标聚合
| 聚合类型 | 说明 | SQL 等价 |
|---|---|---|
avg |
平均值 | AVG() |
sum |
求和 | SUM() |
min |
最小值 | MIN() |
max |
最大值 | MAX() |
value_count |
值计数 | COUNT(field) |
cardinality |
基数(去重计数) | COUNT(DISTINCT) |
示例:计算商品平均价格
1 | |
多值指标聚合
| 聚合类型 | 说明 |
|---|---|
stats |
一次返回 count、min、max、avg、sum |
extended_stats |
在 stats 基础上增加方差、标准差等 |
percentiles |
百分位数统计 |
percentile_ranks |
百分位排名 |
示例:获取价格的完整统计信息
1 | |
桶聚合(Bucket Aggregation)
桶聚合将文档分组到不同的桶中,类似于 SQL 的 GROUP BY。
常用桶聚合类型
| 聚合类型 | 说明 | 适用场景 |
|---|---|---|
terms |
按字段值分组 | 分类统计 |
range |
按数值范围分组 | 价格区间统计 |
date_range |
按日期范围分组 | 时间段统计 |
histogram |
按固定间隔分组 | 数值分布 |
date_histogram |
按时间间隔分组 | 时间序列分析 |
filter |
单个过滤条件分组 | 特定条件统计 |
filters |
多个过滤条件分组 | 多条件对比 |
示例:按商品类别统计数量和平均价格
1 | |
示例:按月统计订单数量
1 | |
嵌套聚合
桶聚合可以嵌套指标聚合或其他桶聚合,实现多维度分析:
1 | |
性能分析
Elasticsearch 提供了多种工具用于诊断和优化查询性能。
慢查询日志(Slow Log)
慢查询日志记录执行时间超过阈值的搜索和索引操作,是定位性能问题的首要工具。
配置方式(动态设置):
1 | |
日志级别说明:
query阶段:查询匹配文档的时间fetch阶段:获取文档内容的时间
Profile API
Profile API 提供查询执行的详细分析,展示每个查询组件的耗时。
使用方式:
1 | |
返回信息包括:
- 每个查询子句的执行时间
- 每个分片的查询耗时分布
- Lucene 层面的详细执行计划
注意:Profile API 会增加查询开销,仅用于调试,不要在生产环境常态化开启。
常见性能问题及优化
| 问题 | 原因 | 优化方案 |
|---|---|---|
| 深度分页慢 | from + size 需要协调节点汇总排序 | 使用 search_after |
| 通配符查询慢 | 前缀通配符无法利用倒排索引 | 使用 ngram 分词器 |
| 聚合内存溢出 | 高基数字段聚合 | 使用 composite 聚合分页 |
| 写入延迟高 | refresh_interval 过短 | 适当增大刷新间隔 |
Quantitative Cluster Sizing
It depends.
ES 运行,是要解决 scale 和 speed 的矛盾。
有4种 basic computing resources:
- storage: 热数据放在贵硬件里。分成 warm tier 和 cold tier。
- memory:heap 使用 50% 内存就行了,堆不需要超过30gb,否则 gc 会是大问题:biz object、meta data(index、cluster)、segment。剩下的内存仍然有用,os cache、file cache 用来存储搜索和分析用的数据 in a cache format。es 会有很棒的缓存机制,既缓存最热的,也缓存长尾的(也要关注长尾是很多人意想不到的)。
- ML 节点很消耗内存。
- 如果有必要,使用 dedicated server 来 host master 节点。
- compute:
- 一定要知道我们的核心数、线程数和队列数的关系。
- network:
- 跨级群搜索的时候需要考虑网络问题。这时候我们要考虑“联合客户端”tribe node。
基本操作:
- index: store for future retrieval.
- delete
- update
- search
线上事故修复
磁盘满导致无法写入
症状
1 | |
原始配置
1 | |
1 | |
这个 -d 好像可以用守护进程的方式执行服务,但实际上还是需要使用 nohup。
有时候 cp 会导致某些 jar not found,解法是准备一个 lib1,把 jar 拷贝过去,然后-cp /lib1/*:/lib/*。有时候可以把 lib1 下的 jar 名字明确写出来,但如果有2个文件夹有同一个 jar,会导致Caused by: java.lang.IllegalStateException: jar hell! class: org.elasticsearch.cli.ExitCodes
独立的日志配置在 log4j2.properties 里。
1 | |
这时候集群是黄色的,意味着所有主分片都已分配,但有副本分片未被分配。数据可用,但容错性降低。。
诊断命令
curl -X GET "http://9.147.246.82:9201/_cluster/health?pretty"
1 | |
unassigned_shards 太高,就会导致 kibana 里集群状态为黄色。
具体可以使用curl -X GET "http://9.147.246.82:9201/_cluster/allocation/explain"来查看分配失败的分片,但这个命令不一定看得到全部的分片。同样可以用_cat/shards?v和_cat/nodes?v来查看不同颗粒度的结论。
curl -X GET "http://9.147.246.82:9201/_cat/indices?v&s=index&bytes=b",可以看到全部索引的状态。
修复命令
1 | |
从 Elasticsearch 7.4 版本开始,当节点的磁盘使用率降到高水位线以下时,Elasticsearch 会自动移除 index.blocks.read_only_allow_delete 设置。这意味着一旦磁盘空间恢复到安全水平,索引将不再是只读的,写操作将再次被允许。
在较早的 Elasticsearch 版本(7.4 之前),当磁盘使用率过高导致索引被设置为只读后,即使磁盘空间释放,index.blocks.read_only_allow_delete 也不会自动恢复。需要手动将该设置修改为 false,以重新允许写入操作。
因此,如果使用的是 7.4 或更高版本的 Elasticsearch,并且启用了 cluster.routing.allocation.disk.threshold_enabled: true,那么当磁盘占用变少、低于高水位线后,Elasticsearch 会自动解除索引的只读限制,您无需手动干预。
总结:如果磁盘占用降低到安全水平,Elasticsearch 会自动更新 index.blocks.read_only_allow_delete 设置,不再禁止写入。
kibana 索引损毁
1 | |
然后备份 kibana 的data 文件夹,在kibana的配置里加上日志:
1 | |
然后执行kibana的命令:
1 | |






