ES 思维导图

ElasticSearch总结.xmind

ElasticSearch总结

%% 完整的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 的定位

  1. ES 是 build on top of Lucene 建立的可以集群化部署的搜索引擎。
  2. ES 可以是 document store(此处可以理解为文档仓库或者文档存储),可以结构化解决数据仓库存储的问题。在 es 中一切皆对象,使用对象对数据建模可以很好地处理万事万物的关系。
  3. ES 是海量数据的分析工具能够支持:搜索、分析(这一条其实我们很少用到)和实时统计。

ES 的架构有优越的地方:

  1. 自己使用 pacifica 协议,写入完成就达成共识。
  2. 节点对内对外都可以使用 RESTful API(or json over http)来通信,易于调试。
  3. 因为它有很多很好的默认值,api 也是 convention over configuration的,所以开箱即用。
  4. 它天生就是分布式的,可以自己管理多节点-它的路由机制是一个方便的,需要优化也可以优化的机制。

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
2
3
4
5
6
7
8
9
10
11
12
{
"query": { ... }, ← 查询条件(必选)
"aggs": { ... }, ← 聚合(可选)
"sort": [ ... ], ← 排序(可选)
"from": 0, ← 分页起始位置(可选)
"size": 10, ← 返回数量(可选)
"_source": [ ... ], ← 返回字段(可选)
"highlight": { ... }, ← 高亮(可选)
"search_after": [ ... ], ← 深度分页游标(可选)
"collapse": { ... }, ← 字段折叠/去重(可选)
"profile": true ← 性能分析(可选)
}

关键理解queryaggssort 是顶层的兄弟关系,而不是嵌套关系。

query 的内部结构

query 内部最常用的是 bool 复合查询,它包含四个子句:

1
2
3
4
5
6
7
8
9
10
{
"query": {
"bool": {
"must": [ ... ], ← 必须匹配,参与评分
"should": [ ... ], ← 应该匹配,参与评分
"must_not": [ ... ], ← 必须不匹配,不参与评分
"filter": [ ... ] ← 必须匹配,不参与评分(过滤上下文)
}
}
}

filter 的位置问题

filter 不是顶层字段,而是 bool 查询的子句。这是很多人困惑的地方。

正确写法:filter 在 bool 内部

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"query": {
"bool": {
"must": [
{ "match": { "title": "elasticsearch" } }
],
"filter": [
{ "term": { "status": "published" } },
{ "range": { "date": { "gte": "2023-01-01" } } }
]
}
}
}

错误写法:filter 不能作为顶层字段

1
2
3
4
{
"query": { ... },
"filter": { ... } ← 错误!filter 必须在 bool 内部
}

filter 可以嵌套在任意层次

filter 可以出现在 bool 查询的任何层级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"query": {
"bool": {
"must": [
{
"bool": {
"should": [],
"filter": [
{ "term": { "category": "tech" } }
]
}
}
],
"filter": [
{ "range": { "price": { "lte": 100 } } }
]
}
}
}

上例中,filter 出现在两个层级:

  • 外层 boolfilter:过滤价格
  • 内层嵌套 boolfilter:过滤分类

term/match 查询 vs filter 的区别

这是一个常见的困惑:既然 termtermsmatch 等查询已经可以做等值匹配,为什么还需要 filter

核心区别

维度 term/match 等查询(在 must/should 中) filter
上下文 查询上下文(Query Context) 过滤上下文(Filter Context)
是否计算评分 是,计算 _score 否,不计算评分
是否缓存 是,结果会被缓存
性能 较慢 较快
返回结果 文档 + 相关性评分 仅文档(评分为 0 或常量)

什么时候用 must 中的 term/match

当你需要相关性排序时,使用 mustshould 中的查询:

1
2
3
4
5
6
7
8
9
{
"query": {
"bool": {
"must": [
{ "match": { "title": "elasticsearch 入门教程" } }
]
}
}
}

这个查询会:

  1. 找出所有 title 包含这些词的文档
  2. 计算每个文档的相关性评分(包含词越多、越精确,评分越高)
  3. 按评分排序返回

什么时候用 filter

当你只需要是/否判断,不关心匹配程度时,使用 filter

1
2
3
4
5
6
7
8
9
10
{
"query": {
"bool": {
"filter": [
{ "term": { "status": "published" } },
{ "range": { "price": { "gte": 100, "lte": 500 } } }
]
}
}
}

这个查询会:

  1. 找出所有 status=publishedprice 在 100-500 之间的文档
  2. 不计算评分(所有匹配文档评分相同)
  3. 结果会被缓存,下次相同过滤条件直接复用

最佳实践:组合使用

实际场景中,通常组合使用两者:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"query": {
"bool": {
"must": [
{ "match": { "title": "elasticsearch" } }
],
"filter": [
{ "term": { "status": "published" } },
{ "range": { "publish_date": { "gte": "2023-01-01" } } }
]
}
}
}

上述查询中:

  • must 中的 match:需要评分,用于全文搜索
  • filter 中的 termrange:不需要评分,用于精确过滤

决策原则

  • 需要评分/排序 → 放在 mustshould
  • 只需要是/否判断 → 放在 filter

性能对比示例

假设有 100 万文档,查询条件是 status=active AND category=tech

方式一:全部用 must(不推荐)

1
2
3
4
5
6
7
8
9
10
{
"query": {
"bool": {
"must": [
{ "term": { "status": "active" } },
{ "term": { "category": "tech" } }
]
}
}
}
  • 每个文档都要计算评分
  • 结果不缓存
  • 每次查询都要重新计算

方式二:使用 filter(推荐)

1
2
3
4
5
6
7
8
9
10
{
"query": {
"bool": {
"filter": [
{ "term": { "status": "active" } },
{ "term": { "category": "tech" } }
]
}
}
}
  • 不计算评分,二元判断
  • 结果被缓存到 filter cache
  • 后续相同查询直接命中缓存

总结

场景 推荐方式
全文搜索(需要相关性排序) must + match
精确匹配(状态、类型、ID) filter + term
范围过滤(时间、价格) filter + range
组合场景 must 放需要评分的,filter 放不需要评分的

常见误区:SQL 思维导致的 term 滥用

问题现象:很多开发者(尤其是有 SQL 背景的)习惯这样写查询:

1
2
3
4
5
6
7
8
9
10
{
"query": {
"bool": {
"must": [
{ "term": { "status": "active" } },
{ "terms": { "category": ["tech", "science"] } }
]
}
}
}

他们的思维是:term 类似 SQL 的 =terms 类似 SQL 的 IN,放在 must 里就像 SQL 的 WHERE 子句。这种写法能工作,但是错误的做法

为什么这是错的?

  1. 官方明确推荐: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.

  2. term 查询的正确用法(来自 Opster 官方指南):

    “Using the term filter in a filter context can improve performance as it skips the scoring phase.”

  3. 性能差异

    • 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 时间计算评分,但这个评分完全没有用

唯一有意义的场景termmust 中计算评分唯一有意义的场景是与其他需要评分的查询组合时,用于调整最终评分(如 should 中的加分项)。但即使这种场景,也可以用 boost 参数更精确地控制。

ES 为什么这样设计? 这是 ES 的统一抽象bool.mustbool.should 运行在 Query Context,所有查询都会计算评分;bool.filterbool.must_not 运行在 Filter Context,所有查询都不计算评分。ES 不会根据查询类型自动切换上下文,上下文由你放置的位置决定

查询类型 评分是否有意义 应该放在哪里
match(全文搜索) ✅ 有意义(匹配程度不同) mustshould
term(精确匹配) ❌ 无意义(要么匹配要么不匹配) filter
terms(多值匹配) ❌ 无意义 filter
range(范围查询) ❌ 无意义 filter
exists(字段存在) ❌ 无意义 filter

正确写法

1
2
3
4
5
6
7
8
9
10
{
"query": {
"bool": {
"filter": [
{ "term": { "status": "active" } },
{ "terms": { "category": ["tech", "science"] } }
]
}
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"query": {
"bool": {
"must": [
{ "match": { "title": "elasticsearch 教程" } }
],
"filter": [
{ "term": { "status": "published" } },
{ "terms": { "category": ["tech", "tutorial"] } },
{ "range": { "publish_date": { "gte": "2023-01-01" } } }
]
}
}
}

这个查询的执行逻辑:

  1. filter 先执行:快速过滤出符合条件的文档子集(利用缓存)
  2. must 后执行:只对过滤后的文档计算相关性评分
  3. 结果排序:按 _score 降序返回

query vs aggs 的关系

维度 query aggs
作用 筛选文档 对文档进行统计分析
位置 顶层字段 顶层字段
关系 兄弟关系 兄弟关系
执行顺序 先执行 query 筛选 再对筛选结果进行聚合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"query": {
"bool": {
"filter": [
{ "term": { "status": "active" } }
]
}
},
"aggs": {
"by_category": {
"terms": { "field": "category.keyword" }
}
}
}

执行流程:先通过 query 筛选出 status=active 的文档,再对这些文档按 category 进行聚合。

Metrics vs Bucket 聚合的关系

两者都是 aggs 内部的聚合类型,可以组合使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"aggs": {
"by_category": {
"terms": { "field": "category.keyword" },
"aggs": {
"avg_price": {
"avg": { "field": "price" }
},
"by_brand": {
"terms": { "field": "brand.keyword" }
}
}
}
}
}

上例结构说明:

  • by_category:Bucket 聚合,按分类分桶
  • avg_price:Metrics 聚合,计算每个桶的平均价格
  • by_brand:嵌套 Bucket 聚合,在每个分类桶内再按品牌分桶
聚合类型 作用 输出
Bucket 将文档分组到不同的桶 多个桶,每个桶包含文档子集
Metrics 对文档进行数值计算 单个或多个数值

完整请求体示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"query": {
"bool": {
"must": [
{ "match": { "title": "elasticsearch 教程" } }
],
"filter": [
{ "term": { "status": "published" } },
{ "range": { "publish_date": { "gte": "2023-01-01" } } }
]
}
},
"aggs": {
"by_author": {
"terms": { "field": "author.keyword", "size": 10 },
"aggs": {
"avg_views": { "avg": { "field": "views" } }
}
}
},
"sort": [
{ "publish_date": "desc" },
{ "_score": "desc" }
],
"from": 0,
"size": 20,
"_source": ["title", "author", "publish_date"],
"highlight": {
"fields": {
"title": {}
}
}
}

结构层次总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
请求体(顶层)
├── query(查询条件)
│ └── bool(复合查询)
│ ├── must(必须匹配,计算评分)
│ ├── should(应该匹配,计算评分)
│ ├── must_not(必须不匹配,不计算评分)
│ └── filter(必须匹配,不计算评分,可缓存)
├── aggs(聚合)
│ ├── bucket 聚合(分桶)
│ │ └── 嵌套 aggs
│ │ ├── metrics 聚合(计算指标)
│ │ └── bucket 聚合(继续分桶)
│ └── metrics 聚合(计算指标)
├── sort(排序,与 query 同级)
├── from / size(分页)
├── _source(返回字段)
├── highlight(高亮)
└── ...其他顶层选项

ES 中的搜索

全文搜索

按照《全文搜索》中的例子,使用 match 总会触发全文搜索。因为在Elasticsearch中,每一个字段的数据都是默认被索引的。也就是说,每个字段专门有一个反向索引用于快速检索。

注意"index": "not_analyzed" 是 ES 2.x 及更早版本的语法。在 ES 5.0+ 中,应使用 "index": false 禁用索引,或直接使用 keyword 类型实现不分词的精确匹配。

精确(短语)搜索

精确查询的方法有:

短语查询

1
2
3
4
5
6
7
{
"query" : {
"match_phrase" : {
"about" : "rock climbing"
}
}
}

这种查询可以查出rock climbinga rock climbing

term 查询(这种查询是客户端查询里最常用的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"query": {
"bool": {
"must": [
{
"term": {
"about" : "rock climbing"
}
}
],
"must_not": [],
"should": [],
"filter": []
}
},
"from": 0,
"size": 10,
"sort": [],
"profile": false
}

这种查询可以查出rock climbing,不可以查出a rock climbing

另一种 phrase 查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"from": 0,
"size": 200,
"query": {
"bool": {
"must": {
"match": {
"about": {
"query": "rock climbing",
"type": "phrase"
}
}
}
}
}
}

精确和不精确查询有好几种方法,详见《elasticsearch 查询(match和term)》《finding_exact_values》

注意,ES 的查询可以有很多的变种,以下几个查询请求体在语义上等价:

基础 match 查询

1
2
3
4
5
6
7
{
"query": {
"match": {
"content": "我的宝马多少马力"
}
}
}

嵌套形式(content 是字段名,query 是查询值):

1
2
3
4
5
6
7
8
9
{
"query": {
"match": {
"content": {
"query": "我的宝马多少马力"
}
}
}
}

使用 bool 子句进行查询

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "我的宝马多少马力"
}
}
]
}
}
}

使用 term 进行精确查询

注意:即使文档的原始 content 是"我的宝马多少马力",如果该字段被分词器处理过,term 查询可能无法匹配,因为 term 查询不会对查询词进行分词。

1
2
3
4
5
6
7
{
"query": {
"term": {
"content": "我的宝马多少马力"
}
}
}

使用 terms 进行多值匹配

1
2
3
4
5
6
7
{
"query": {
"terms": {
"content": ["我的宝马多少马力"]
}
}
}

历史语法说明filtered 查询是 ES 2.x 及更早版本的语法,在 ES 5.0+ 中已被移除,应使用 bool 查询的 filter 子句替代。
elastic-search查询字句整理
elastic-search查询字句整理

search api 的搜索

参考《空查询》

倒排索引简析

倒排索引的 key 是 term,value 是文档的指针,可以参考这个《倒排索引》文档。

过滤机制

Elasticsearch 的查询分为两种上下文:查询上下文(Query Context)过滤上下文(Filter Context)

查询上下文 vs 过滤上下文

特性 查询上下文 过滤上下文
核心问题 文档与查询的匹配程度如何? 文档是否匹配查询条件?
返回结果 匹配的文档 + 相关性评分(_score) 匹配的文档(无评分)
是否缓存 不缓存 自动缓存,可复用
性能 较慢(需计算评分) 较快(二元判断 + 缓存)
适用场景 全文搜索、需要排序 精确匹配、范围过滤

使用方式

bool 查询中,mustshould 子句运行在查询上下文,而 filtermust_not 子句运行在过滤上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"query": {
"bool": {
"must": [
{ "match": { "title": "搜索引擎" } }
],
"filter": [
{ "term": { "status": "published" } },
{ "range": { "publish_date": { "gte": "2023-01-01" } } }
]
}
}
}

最佳实践:对于不需要评分的条件(如状态过滤、时间范围),应放入 filter 子句以获得更好的性能和缓存效果。

HTTP 头机制

Elasticsearch 支持通过 HTTP 头传递元信息,用于请求追踪、兼容性控制等场景。

X-Opaque-Id

X-Opaque-Id 是一个用于请求追踪的 HTTP 头,从 ES 6.2.0 开始支持。通过为每个请求指定唯一标识符,可以在日志中追踪请求来源。

使用方式

1
2
curl -H "X-Opaque-Id: my-request-123" \
-X GET "localhost:9200/_search?pretty"

应用场景

  • 在慢查询日志(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
2
3
4
POST /_sql?format=json
{
"query": "SELECT * FROM my_index WHERE status = 'active' LIMIT 10"
}

版本控制机制

Elasticsearch 使用乐观并发控制(Optimistic Concurrency Control)来处理并发写入冲突。

版本控制演进

早期方案:_version 字段

ES 6.x 及更早版本使用 _version 字段进行并发控制:

1
2
3
4
PUT /my_index/_doc/1?version=1&version_type=external
{
"title": "文档标题"
}

局限性:在分布式环境下,主分片切换时可能导致版本号不一致。

现代方案:_seq_no + _primary_term

ES 6.7+ 引入了更健壮的并发控制机制:

  • _seq_no:序列号,每次写操作递增
  • _primary_term:主分片任期号,主分片切换时递增

使用方式

1
2
3
4
5
6
7
8
# 获取文档时记录 _seq_no 和 _primary_term
GET /my_index/_doc/1

# 更新时携带这两个参数
PUT /my_index/_doc/1?if_seq_no=10&if_primary_term=1
{
"title": "更新后的标题"
}

如果文档在读取后被其他请求修改,更新将失败并返回 version_conflict_engine_exception,客户端需要重新读取后再次尝试更新。

冲突处理策略

策略 说明
重试 使用 retry_on_conflict 参数自动重试
最后写入胜出 不使用版本控制,接受数据覆盖
业务层处理 捕获冲突异常,由业务逻辑决定如何处理

容量与分片

为何分片不宜过大?

  1. 增加索引读压力-不利于并行查询。
  2. 不利于集群扩缩容(分片迁移耗费较长时间)。
  3. 集群发生故障时,恢复时间较长。

类似的问题也会发生在 Redis 之类的架构方案里。

解决大分片的方法

  1. 索引按月、日进行分割。
  2. delete_by_query 方法删除索引对索引进行缩容。
  3. 模板增加主分片数量-存疑,通常只能增加副本数,而无法改变切片比例。
  4. 扩容数据节点。
  5. 减小单位分片大小。

一个可实操的方案:

  1. 修改写入逻辑和 mapping,让新写入的 doc 的 field 变少。
  2. 对老的索引进行 delete_by_query。

常用查询段

  • aggs
  • match
    • 等于分词以后的 or 查询,依赖于 analyzer。“hello world”如果被分词为 hello 和 world,则类似term = hello or term = wolrd
  • match_phrase
    • term 出现在一个句子中,句子包含 term,依赖于 analyzer。类似contains("hello world")
    • 这是 String.contains。
    • 不匹配 slop,还不如用 term。
  • page:
  • range:是大于等于小于的意思,不是 from 和 size
  • search_after
  • term
    • term 类似于 Collection.contains"[hello, world]".contains("hello")"[hello]".terms("hello") 都成立。
  • 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前缀和通配符查询等。内部结构如下:fst前缀树
  • BKD-Tree:BKD-Tree是一种保存多维空间点的数据结构,主要用于数值类型(包括空间点)的快速查找。

Mapping 本质

Mapping 是 ES 中定义文档结构的核心配置:

  • 结构定义:mapping 中每个顶层 key 代表一个字段名,其对应的对象是该字段的完整配置集合(包含类型、分析器、存储选项等属性)
  • 示例
1
2
3
4
5
6
7
8
9
10
11
{
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": { "type": "keyword" }
}
}
}
}

fields 多字段特性

适用范围:并非所有字段都需要 fields,该特性主要用于 text 字段(如创建 keyword 子字段)。数值、日期等类型通常无需 fields,因其本身已支持聚合/排序。

本质fields 是同一字段的多视角索引

  • name(text 视角):用于全文搜索
  • name.keyword(keyword 视角):用于精确匹配、聚合和排序

nested 嵌套类型

必要性:当字段是对象数组时,必须使用 nested 类型。否则倒排索引会将数组内所有对象的同名字段合并为扁平列表,导致跨对象的错误匹配。

问题示例:假设有如下地址数组

1
2
3
4
5
6
{
"addresses": [
{ "city": "北京", "street": "长安街" },
{ "city": "上海", "street": "南京路" }
]
}

如果使用普通 object 类型,ES 会将其扁平化为:

1
2
3
4
{
"addresses.city": ["北京", "上海"],
"addresses.street": ["长安街", "南京路"]
}

此时查询 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
2
3
4
5
6
7
8
9
10
11
12
13
{
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}

聚合时使用 title.keyword 而非 title

指标聚合(Metrics Aggregation)

指标聚合用于对文档字段进行数值计算,类似于 SQL 中的聚合函数。

单值指标聚合

聚合类型 说明 SQL 等价
avg 平均值 AVG()
sum 求和 SUM()
min 最小值 MIN()
max 最大值 MAX()
value_count 值计数 COUNT(field)
cardinality 基数(去重计数) COUNT(DISTINCT)

示例:计算商品平均价格

1
2
3
4
5
6
7
{
"aggs": {
"avg_price": {
"avg": { "field": "price" }
}
}
}

多值指标聚合

聚合类型 说明
stats 一次返回 count、min、max、avg、sum
extended_stats 在 stats 基础上增加方差、标准差等
percentiles 百分位数统计
percentile_ranks 百分位排名

示例:获取价格的完整统计信息

1
2
3
4
5
6
7
{
"aggs": {
"price_stats": {
"stats": { "field": "price" }
}
}
}

桶聚合(Bucket Aggregation)

桶聚合将文档分组到不同的桶中,类似于 SQL 的 GROUP BY

常用桶聚合类型

聚合类型 说明 适用场景
terms 按字段值分组 分类统计
range 按数值范围分组 价格区间统计
date_range 按日期范围分组 时间段统计
histogram 按固定间隔分组 数值分布
date_histogram 按时间间隔分组 时间序列分析
filter 单个过滤条件分组 特定条件统计
filters 多个过滤条件分组 多条件对比

示例:按商品类别统计数量和平均价格

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"size": 0,
"aggs": {
"by_category": {
"terms": { "field": "category.keyword" },
"aggs": {
"avg_price": {
"avg": { "field": "price" }
}
}
}
}
}

示例:按月统计订单数量

1
2
3
4
5
6
7
8
9
10
11
{
"size": 0,
"aggs": {
"orders_over_time": {
"date_histogram": {
"field": "order_date",
"calendar_interval": "month"
}
}
}
}

嵌套聚合

桶聚合可以嵌套指标聚合或其他桶聚合,实现多维度分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"aggs": {
"by_category": {
"terms": { "field": "category.keyword" },
"aggs": {
"by_brand": {
"terms": { "field": "brand.keyword" },
"aggs": {
"avg_price": { "avg": { "field": "price" } }
}
}
}
}
}
}

性能分析

Elasticsearch 提供了多种工具用于诊断和优化查询性能。

慢查询日志(Slow Log)

慢查询日志记录执行时间超过阈值的搜索和索引操作,是定位性能问题的首要工具。

配置方式(动态设置):

1
2
3
4
5
6
7
8
9
PUT /my_index/_settings
{
"index.search.slowlog.threshold.query.warn": "10s",
"index.search.slowlog.threshold.query.info": "5s",
"index.search.slowlog.threshold.query.debug": "2s",
"index.search.slowlog.threshold.query.trace": "500ms",
"index.search.slowlog.threshold.fetch.warn": "1s",
"index.search.slowlog.threshold.fetch.info": "800ms"
}

日志级别说明

  • query 阶段:查询匹配文档的时间
  • fetch 阶段:获取文档内容的时间

Profile API

Profile API 提供查询执行的详细分析,展示每个查询组件的耗时。

使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"profile": true,
"query": {
"bool": {
"must": [
{ "match": { "title": "elasticsearch" } }
],
"filter": [
{ "term": { "status": "published" } }
]
}
}
}

返回信息包括

  • 每个查询子句的执行时间
  • 每个分片的查询耗时分布
  • 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
{"error":{"root_cause":[{"type":"cluster_block_exception","reason":"blocked by: [FORBIDDEN/12/index read-only / allow delete (api)];"}],"type":"cluster_block_exception","reason":"blocked by: [FORBIDDEN/12/index read-only / allow delete (api)];"},"status":403}

原始配置

1
2
3
4
5
6
7
8
9
10
# 配置文件
# 允许监控磁盘阈值
cluster.routing.allocation.disk.threshold_enabled: true

# 超过这两个阈值会触发 es 的写保护而且还需要触发分片迁移
cluster.routing.allocation.disk.watermark.high: "95%"
cluster.routing.allocation.disk.watermark.low: "93%"

# 设置这个文件才能备份某个索引
path.repo: ["/data/es/index_backup"]
1
2
# 原始启动命令
/usr/local/services/jdk8-1.0/bin/java -Xms3g -Xmx3g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+AlwaysPreTouch -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -XX:-OmitStackTraceInFastThrow -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Djava.io.tmpdir=/tmp/elasticsearch.tJnDMs83 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=data -XX:ErrorFile=logs/hs_err_pid%p.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -Xloggc:logs/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=32 -XX:GCLogFileSize=64m -Des.path.home=/data/es/elasticsearch-6.5.3 -Des.path.conf=/data/es/elasticsearch-6.5.3/config -Des.distribution.flavor=default -Des.distribution.type=tar -cp /data/es/elasticsearch-6.5.3/lib/* org.elasticsearch.bootstrap.Elasticsearch -d

这个 -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
[2024-11-11T16:15:54,205][INFO ][o.e.c.r.a.AllocationService] [node-1] Cluster health status changed from [RED] to [YELLOW] (reason: [shards started [[leads][4]] ...]).

这时候集群是黄色的,意味着所有主分片都已分配,但有副本分片未被分配。数据可用,但容错性降低。

诊断命令

  • curl -X GET "http://9.147.246.82:9201/_cluster/health?pretty"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"cluster_name" : "elasticsearch-test",
"status" : "yellow",
"timed_out" : false,
"number_of_nodes" : 1,
"number_of_data_nodes" : 1,
"active_primary_shards" : 88,
"active_shards" : 88,
"relocating_shards" : 0,
"initializing_shards" : 0,
"unassigned_shards" : 70,
"delayed_unassigned_shards" : 0,
"number_of_pending_tasks" : 0,
"number_of_in_flight_fetch" : 0,
"task_max_waiting_in_queue_millis" : 0,
"active_shards_percent_as_number" : 55.69620253164557
}

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
2
3
4
5
6
7
8
9
10
# 将所有索引的只读删除状态解除
PUT /_all/_settings
{
"index.blocks.read_only_allow_delete": null
}

curl -X PUT "http://localhost:9200/_all/_settings" -H 'Content-Type: application/json' -d'
{
"index.blocks.read_only_allow_delete": null
}'

从 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 刷新 .kibana 索引
curl -X POST "http://localhost:9200/.kibana/_refresh"
# 打开 .kibana 索引
curl -X POST "http://localhost:9200/.kibana/_open"
# 获取 .kibana 索引的信息
curl -X GET "http://localhost:9200/_cat/indices/.kibana?v"
# 创建快照存储库
curl -X PUT "http://localhost:9200/_snapshot/backup_repository" -H 'Content-Type: application/json' -d'
{
"type": "fs",
"settings": {
"location": "/data/es/index_backup",
"compress": true
}
}'
# 创建 .kibana 索引的快照
curl -X PUT "http://localhost:9200/_snapshot/backup_repository/snapshot_1?wait_for_completion=true" -H 'Content-Type: application/json' -d'
{
"indices": ".kibana",
"ignore_unavailable": true,
"include_global_state": false
}'
# 删除 .kibana 索引,以后kibana会自己重建 .kibana_1 或者其他 alias
curl -X DELETE "http://localhost:9200/.kibana"

# 恢复备份的 .kibana 索引(如果有备份):
curl -X POST "http://localhost:9200/_snapshot/backup_repository/snapshot_1/_restore"
{
"indices": ".kibana",
"ignore_unavailable": false,
"include_global_state": false,
"rename_pattern": "(.+)",
"rename_replacement": "$1"
}

然后备份 kibana 的data 文件夹,在kibana的配置里加上日志:

1
logging.dest: /usr/local/services/kibana/kibana-6.5.3-linux-x86_64/log/kibana.log

然后执行kibana的命令:

1
2
# 注意看 node 的ps结果和日志的migration过程,不行就继续删除data、es索引并且 kill kibana 父子进程
bin/kibana