# ElasticSearch 101

## 初识 ElasticSearch

**使用场景**

1. 搜索引擎：生产环境中使用最多，例如京东、淘宝、美团等使用 ES 实现高性能商品搜索，支持多条件筛选、排序和相关性排名等。
2. 日志分析与监控：ELK 作为日志分析的行业标准，是 ES 经典的使用场景。
3. 数据分析：大数据分析，通过 DSL 快速得到结果。

**与类似组件对比**

| **对比维度**       | **Elasticsearch**               | **竞品**                        | **优劣总结**                                                                 |
| -------------- | ------------------------------- | ----------------------------- | ------------------------------------------------------------------------ |
| **RDBMS**      | <p>1、高性能组合查询<br>2、适合半结构化数据</p>  | <p>1、完善的事务支持<br>2、强一致性</p>    | <p><strong>选 ES</strong>：复杂查询/分析场景<br><strong>选 RDBMS</strong>：强事务需求</p> |
| **Solr**       | <p>1、更成熟的分布式架构<br>2、近实时能力更强</p> | ES 是 Solr 是上位替代               | 新项目优先选择 ES，Solr 被淘汰。                                                     |
| **ClickHouse** | <p>1、全文检索优势<br>2、近实时响应</p>      | <p>1、超大规模聚合更快<br>2、支持二次聚合</p> | <p><strong>ES</strong>处理搜索/日志<br><strong>ClickHouse</strong>处理深度分析</p>   |

**存储场景决策树**

![](https://raw.githubusercontent.com/L2ncE/images/main/PicGo/deepseek_mermaid_20250501_2fedbe.png)

## 快速开始

仓库地址：[L2ncE/es101](https://github.com/L2ncE/es101)

克隆仓库后进入到 [docker-compose 文件](https://github.com/L2ncE/es101/blob/main/docker-es-78/docker-compose.yaml) 目录运行 `docker-compose up`。

> ⚠️ **注意**：自行下载 Docker 以及 docker-compose 工具

### Kibana 快速开始

通过 `localhost:5601` 进入：

![](https://raw.githubusercontent.com/L2ncE/images/main/PicGo/20250501164451963.png)

使用 Dev Tools：

![](https://raw.githubusercontent.com/L2ncE/images/main/PicGo/20250501164524157.png)

### Logstash 快速开始

运行 `docker-compose up` 命令后可以到日志中查看是否导入数据成功。

![](https://raw.githubusercontent.com/L2ncE/images/main/PicGo/20250501172441134.png)

### Cerebro 快速开始

通过 `localhost:9000` 进入：

![](https://raw.githubusercontent.com/L2ncE/images/main/PicGo/20250501172501845.png)

可以看到刚导入的 movies 数据。

## ElasticSearch 入门

| **概念名称**  | **描述**                                                         | **关键点 / 特点**                                                                                                                              |
| --------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **文档**    | Elasticsearch 中的最小数据单元，以 JSON 格式存储数据。                          | <p>1、类比于关系型数据库中的一行数据。<br>2、包含实际数据字段（如 <code>title</code>, <code>content</code> 等）。</p>                                                    |
| **文档元数据** | 描述文档自身属性的系统字段，用于唯一标识和管理文档。                                     | <p>包含以下核心元数据：<br>- <code>\_id</code>：文档唯一标识符（可自定义或自动生成）。<br>- <code>\_index</code>：文档所属的索引。<br>- <code>\_version</code>：文档版本号（支持乐观锁）。</p> |
| **索引**    | 一类结构相似文档的集合，类似于关系型数据库中的「表」。                                    | 通过 Mapping 定义字段类型和属性（如文本、数值、日期等）。支持倒排索引，优化全文搜索性能。                                                                                         |
| **节点**    | Elasticsearch 集群中的一个运行实例，本质是一个 Java 进程。                        | <p>节点类型包括：<br>- <strong>主节点</strong>：管理集群状态。<br>- <strong>数据节点</strong>：存储数据。<br>- <strong>协调节点</strong>：处理请求路由。</p>                      |
| **分片**    | 索引的物理子集，用于分布式存储和计算。分片分为主分片（Primary Shard）和副本分片（Replica Shard）。 | <p><strong>主分片</strong>：数据存储和写入的基本单元，数量在索引创建时固定。<br><strong>副本分片</strong>：主分片的冗余拷贝，提供高可用和查询负载均衡。</p>                                      |

### 与关系型数据库类比

| RDBMS  | ElasticSearch |
| ------ | ------------- |
| Table  | Index         |
| Row    | Document      |
| Column | Field         |
| Schema | Mapping       |
| SQL    | DSL           |

### 健康状况

1、在 Kibana 中查询，使用命令 `GET _cluster/health`

![](https://raw.githubusercontent.com/L2ncE/images/main/PicGo/20250501183053718.png)

2、在 Cerebro 中查询

![](https://raw.githubusercontent.com/L2ncE/images/main/PicGo/20250501172501845.png)

#### 测试节点下线

![](https://raw.githubusercontent.com/L2ncE/images/main/PicGo/20250501183551380.png)

此时状态为 Yellow。

### CRUD

| **操作**     | **HTTP 方法**         | **响应码**                    | **Source 处理**                  | **幂等性**   | **备注**                                                     |
| ---------- | ------------------- | -------------------------- | ------------------------------ | --------- | ---------------------------------------------------------- |
| **Create** | `PUT` 或 `POST`      | <p>201（成功）<br>409（冲突）</p>  | 必须提供完整文档 Source                | 是（需指定 ID） | 仅当文档不存在时创建。需指定 ID（`PUT`）或自动生成（`POST`）。                     |
| **Get**    | `GET`               | <p>200（存在）<br>404（不存在）</p> | 可指定 `_source` 过滤返回字段           | 是         | 仅用于查询文档，不修改数据。                                             |
| **Index**  | `PUT` 或 `POST`      | <p>201（新建）<br>200（更新）</p>  | 必须提供完整文档 Source                | 否         | 若文档存在则替换（全量更新）。可指定 ID（`PUT`）或自动生成（`POST`）。                 |
| **Update** | `POST`（带 `_update`） | <p>200（成功）<br>404（不存在）</p> | 提供部分字段或脚本（支持 `doc` 或 `script`） | 否         | 部分更新。支持 `upsert`（不存在时插入）。需启用 `_source` 字段。默认返回更新后的 Source。 |

#### Create

**Demo1**

```json
# Req
# create document 自动生成 _id
POST users/_doc
{
	"user" : "LanLance"
}

# Resp
{
  "_index" : "users",
  "_type" : "_doc",
  "_id" : "64IRqpYBLb6gKsOx04Rm",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}
```

**Demo2**

```json
# Req
# create document 指定 ID
PUT users/_doc/1?op_type=create
{
    "user" : "LanLance_2"
}

# Resp
{
  "_index" : "users",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}
```

**Demo3**

```json
# Req
# create document 指定 ID；如果已经存在就报错
PUT users/_create/1
{
    "user" : "LanLance"
}

# Resp
{
  "error" : {
    "root_cause" : [
      {
        "type" : "version_conflict_engine_exception",
        "reason" : "[1]: version conflict, document already exists (current version [1])",
        "index_uuid" : "J6rYdyr6TY-H9asFHjyTwQ",
        "shard" : "0",
        "index" : "users"
      }
    ],
    "type" : "version_conflict_engine_exception",
    "reason" : "[1]: version conflict, document already exists (current version [1])",
    "index_uuid" : "J6rYdyr6TY-H9asFHjyTwQ",
    "shard" : "0",
    "index" : "users"
  },
  "status" : 409
}
```

#### Get

**Demo1**

```json
# Req
# Get 指定 ID
GET users/_doc/1

# Resp
{
  "_index" : "users",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 1,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "user" : "LanLance_2"
  }
}
```

#### Index & Update

**Demo1**

```json
# Req
# Update 指定 ID (先删除，在写入)
PUT users/_doc/1
{
	"user" : "LanLance"
}

# Resp
{
  "_index" : "users",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 2,
  "_primary_term" : 1
}
```

**Demo2**

```json
# Req
# Update 在原文档上增加字段
POST users/_update/1/
{
    "doc":{
        "message" : "trying out Elasticsearch"
    }
}

# Resp
{
  "_index" : "users",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 3,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 3,
  "_primary_term" : 1
}
```

#### Delete

**Demo1**

```json
# Req
# Delete 指定 ID
DELETE users/_doc/1

# Resp
{
  "_index" : "users",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 4,
  "result" : "deleted",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 4,
  "_primary_term" : 1
}
```

#### Bulk、MGet、MSearch

| **API**     | **HTTP 方法**  | **操作类型**                          | **主要用途**   | **响应码**          | **幂等性** | **性能优化点**     |
| ----------- | ------------ | --------------------------------- | ---------- | ---------------- | ------- | ------------- |
| **Bulk**    | `POST`       | 批量增删改（Create/Index/Update/Delete） | 批量写入或更新数据  | 200（整体成功，可能部分失败） | 否       | 单次请求处理大量操作    |
| **mget**    | `GET`/`POST` | 批量读取文档                            | 批量获取多个文档内容 | 200（包含每个文档状态）    | 是       | 减少网络请求次数      |
| **msearch** | `POST`       | 批量搜索请求                            | 批量执行多个搜索查询 | 200（包含每个查询结果）    | 是       | 合并多个查询请求，减少延迟 |

**功能与场景对比**

| **特性**    | **Bulk API**                  | **mget**              | **msearch**       |
| --------- | ----------------------------- | --------------------- | ----------------- |
| **核心操作**  | 增删改（CRUD）                     | 读取文档                  | 执行搜索查询            |
| **数据量优化** | 单次请求处理数千操作                    | 单次请求获取数百文档            | 单次请求合并多个复杂查询      |
| **错误处理**  | 响应中标记每个操作的 `error` 和 `status` | 响应中标记每个文档的 `found` 状态 | 每个查询独立返回状态码和结果    |
| **适用场景**  | 数据迁移、日志流写入                    | 批量加载关联数据、初始化页面        | 仪表盘批量拉取数据、跨索引聚合分析 |
| **原子性**   | 非原子（部分成功需重试）                  | 非原子                   | 非原子               |

**性能与限制对比**

| **维度**   | **Bulk API**    | **mget**             | **msearch**   |
| -------- | --------------- | -------------------- | ------------- |
| **网络开销** | 单次请求处理大量操作（高吞吐） | 减少多次 GET 请求（中吞吐）     | 合并多个查询（中高吞吐）  |
| **内存消耗** | 高（需缓存批量数据）      | 中（文档数量和大小决定）         | 高（复杂查询可能占用内存） |
| **超时风险** | 大数据量可能触发请求超时    | 大文档列表可能超时            | 复杂查询或大数据集可能超时 |
| **分片影响** | 写入压力分散到多个分片     | 读取压力分散到多个分片          | 搜索压力分散到多个分片   |
| **限制规避** | 分批提交（每批 5-15MB） | 分批查询（每批 100-1000 文档） | 控制单个查询复杂度     |

**使用**

```json
# bulk
## 执行第1次
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test2", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }

## 执行第2次
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test2", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }

# mget
GET /_mget
{
    "docs" : [
        {
            "_index" : "test",
            "_id" : "1"
        },
        {
            "_index" : "test",
            "_id" : "2"
        }
    ]
}

## URI 中指定 index
GET /test/_mget
{
    "docs" : [
        {

            "_id" : "1"
        },
        {

            "_id" : "2"
        }
    ]
}

GET /_mget
{
    "docs" : [
        {
            "_index" : "test",
            "_id" : "1",
            "_source" : false
        },
        {
            "_index" : "test",
            "_id" : "2",
            "_source" : ["field3", "field4"]
        },
        {
            "_index" : "test",
            "_id" : "3",
            "_source" : {
                "include": ["user"],
                "exclude": ["user.location"]
            }
        }
    ]
}
```

**清除数据**

```json
DELETE users
DELETE test
DELETE test2
```

### 倒排索引

倒排索引是搜索引擎和全文检索系统的核心数据结构，核心思想是通过建立「单词到文档」的映射关系从而实现 Keyword 快速定位包含该词的所有文档。例如当用户搜索「ElasticSearch」时，系统可直接通过倒排索引找到包含这一词汇的所有文档集合。

倒排索引的实现依赖于两个关键结构：**单词词典**（Term Dictionary）和**倒排列表**（Posting List）。

{% @mermaid/diagram content="flowchart TD
A\["文档集合"] --> B("分词处理")
B --> C{"单词词典"}
C --> D\["B+树/哈希表"] & E\["倒排列表"]
E --> F\["文档ID"] & G\["词频 TF"] & H\["位置 Position"]" %}

### Analysis & Analyzer

Analysis 是通过 Analyzer 来实现的。

#### 分词器

由三部分组成：

* Character Filters：针对原始文本处理，例如去除 html。
* Tokenizer：按照规则切分为单词。
* Token Filter：将切分的的单词进行加工，例如小写、删除 stopwords、增加同义词等。

```
Character Filters => Tokenizer => Token Filters
```

**ElasticSearch 内置分词器**

| 分词器                       | 使用场景                          | 分词逻辑                                                             |
| ------------------------- | ----------------------------- | ---------------------------------------------------------------- |
| **Standard**              | 通用文本处理，支持大多数语言（默认选择）。         | 按 Unicode 标准分词，移除标点符号，转小写，支持多语言基础处理。                             |
| **Simple**                | 快速简单分词，忽略标点符号和数字。             | 在非字母字符处分割文本，删除非字母字符，转小写（如 `Hello-World` → `["hello", "world"]`）。 |
| **Whitespace**            | 按空格严格分割，保留原始格式（如代码、特定标识）。     | 仅按空格分割，保留大小写和标点（如 `Quick-Brown` → `["Quick-Brown"]`）。            |
| **Stop**                  | 需过滤常见停用词（如英文中的“the”、“is”）的文本。 | 按非字母字符分割出连续字母词条，转小写后移除停用词（如 `The fox` → `["fox"]`）。              |
| **Keyword**               | 需精确匹配的字段（如 ID、状态码）。           | 将整个输入作为单一词条，不进行任何处理（如 `Hello World` → `["Hello World"]`）。        |
| **Pattern**               | 需自定义分隔规则（如按特定符号分割）的文本。        | 通过正则表达式（默认 `\W+`）分割文本，转小写（可自定义正则）。                               |
| **Language**（如 `english`） | 针对特定语言优化（如英文词干提取、停用词过滤）。      | 按语言规则分词，处理停用词、转小写、词干提取等（如 `running` → `["run"]`）。                |

**analyzer API**

通过 analyzer API 能够快速得到分词结果进行测试，以下提供了一些例子可以去到 Kibana 的 Dev Tools 进行使用。

```json
#standard
GET _analyze
{
  "analyzer": "standard",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#simple
GET _analyze
{
  "analyzer": "simple",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#stop
GET _analyze
{
  "analyzer": "stop",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#whitespace
GET _analyze
{
  "analyzer": "whitespace",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#keyword
GET _analyze
{
  "analyzer": "keyword",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#pattern
GET _analyze
{
  "analyzer": "pattern",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#english
GET _analyze
{
  "analyzer": "english",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}
```

**示例：standard 分词器的分词结果**

```json
{
  "tokens" : [
    {
      "token" : "2",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "<NUM>",
      "position" : 0
    },
    {
      "token" : "running",
      "start_offset" : 2,
      "end_offset" : 9,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "quick",
      "start_offset" : 10,
      "end_offset" : 15,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "brown",
      "start_offset" : 16,
      "end_offset" : 21,
      "type" : "<ALPHANUM>",
      "position" : 3
    },
    {
      "token" : "foxes",
      "start_offset" : 22,
      "end_offset" : 27,
      "type" : "<ALPHANUM>",
      "position" : 4
    },
    {
      "token" : "leap",
      "start_offset" : 28,
      "end_offset" : 32,
      "type" : "<ALPHANUM>",
      "position" : 5
    },
    {
      "token" : "over",
      "start_offset" : 33,
      "end_offset" : 37,
      "type" : "<ALPHANUM>",
      "position" : 6
    },
    {
      "token" : "lazy",
      "start_offset" : 38,
      "end_offset" : 42,
      "type" : "<ALPHANUM>",
      "position" : 7
    },
    {
      "token" : "dogs",
      "start_offset" : 43,
      "end_offset" : 47,
      "type" : "<ALPHANUM>",
      "position" : 8
    },
    {
      "token" : "in",
      "start_offset" : 48,
      "end_offset" : 50,
      "type" : "<ALPHANUM>",
      "position" : 9
    },
    {
      "token" : "the",
      "start_offset" : 51,
      "end_offset" : 54,
      "type" : "<ALPHANUM>",
      "position" : 10
    },
    {
      "token" : "summer",
      "start_offset" : 55,
      "end_offset" : 61,
      "type" : "<ALPHANUM>",
      "position" : 11
    },
    {
      "token" : "evening",
      "start_offset" : 62,
      "end_offset" : 69,
      "type" : "<ALPHANUM>",
      "position" : 12
    }
  ]
}
```

### Search API

* URI Search
* Request Body Search

**示例**

```json
# URI Search
GET kibana_sample_data_ecommerce/_search?q=customer_first_name:Eddie
GET kibana*/_search?q=customer_first_name:Eddie
GET /_all/_search?q=customer_first_name:Eddie

# Request Body Search
POST kibana_sample_data_ecommerce/_search
{
	"profile": true,
	"query": {
		"match_all": {}
	}
}
```

#### 指定查询的索引

| 语法                      | 范围              |
| ----------------------- | --------------- |
| /\_search               | 所有索引            |
| /index1/\_search        | index1          |
| /index1,index2/\_search | index1 和 index2 |
| /index\*/\_search       | 以 index 开头的索引   |

#### URI Search

**示例**

```json
GET /movies/_search?q=2012&df=title&sort=year:desc&from=0&size=10&timeout=1s
{
	"profile":"true"
}
```

参数：

* q 指定查询语句，使用 Query String Syntax。
* df 指定默认字段，不指定时会对所有字段进行查询。
* sort 代表排序。
* from 和 size 用于分页。
* profile 可以查看查询是如何被执行的。

**泛查询**

```json
GET /movies/_search?q=2012
{
	"profile":"true"
}
```

未指定 `df` 参数时 ES 会搜索所有字段，可能触发跨字段匹配，性能消耗较大。分析结果如下：

```json
{  
    "type": "DisjunctionMaxQuery",  
    "description": "(title.keyword:2012 | id.keyword:2012 | year:[2012 TO 2012] | genre:2012 | @version:2012 | @version.keyword:2012 | id:2012 | genre.keyword:2012 | title:2012)"  
}
```

可以看到在所有字段进行了匹配。

**显式字段查询**

```json
GET /movies/_search?q=title:2012
{
	"profile":"true"
}
```

通过 `title:2012` 显式指定字段，精准限定搜索范围，比泛查询更高效。分析结果如下：

```json
{  
    "type": "TermQuery",  
    "description": "title:2012"  
}
```

**短语分割问题**

```json
GET /movies/_search?q=title:Beautiful Mind
{
	"profile":"true"
}
```

实际执行 `title:Beautiful OR Mind`，空格被识别为 OR 逻辑，返回包含任意词的文档。

**精确短语匹配**

```json
GET /movies/_search?q=title:"Beautiful Mind"
{
	"profile":"true"
}
```

使用双引号包裹词组，强制进行短语搜索，要求词语按顺序完整出现。

**分组查询**

```json
GET /movies/_search?q=title:(Beautiful Mind)
{
	"profile":"true"
}
```

括号实现逻辑分组，等效于 `title:Beautiful OR title:Mind`，优先执行组内操作。

**布尔运算符**

```json
GET /movies/_search?q=title:(Beautiful AND Mind)
```

显式布尔查询，要求同时包含两个词（AND 逻辑）。

**范围查询**

```json
GET /movies/_search?q=title:beautiful AND year:[2002 TO 2018%7D
```

`[2002 TO 2018}` 表示闭区间包含 2002，开区间不包含 2018（`%7D` 为 URL 编码的 `}` 符号）。

**通配符搜索**

```json
GET /movies/_search?q=title:b*
{
	"profile":"true"
}
```

`b*` 匹配以 b 开头的任意长度字符，支持 `?` 匹配单个字符，注意通配符在前端影响性能。

**模糊匹配**

```json
GET /movies/_search?q=title:beautiful~1
{
	"profile":"true"
}

GET /movies/_search?q=title:"Lord Rings"~2
{
	"profile":"true"
}
```

* `~1` 允许 1 个字符的编辑距离（拼写纠错）。
* `"Lord Rings"~2` 表示短语中允许间隔 2 个单词。

#### Request Body Search & Query DSL

通常生成环境都使用这种方法，更加强大、功能更丰富。

**示例**

```json
POST movies/_search
{
  "from":0,
  "size":10,
  "query": {
    "match": {
      "title": {
        "query": "last christmas",
        "operator": "and"
      }
    }
  }
}
```

更多 DSL 语法请参考 [官方文档](https://elasticsearch-dsl.readthedocs.io/en/latest/)。

**基本 Match 查询**

```json
POST movies/_search
{
  "query": {
    "match": {
      "title": "last christmas"
    }
  }
}
```

* 默认使用 OR 逻辑匹配分词结果。
* 自动对搜索词进行分词处理。

**精确 AND 匹配**

```json
POST movies/_search
{
  "query": {
    "match": {
      "title": {
        "query": "last christmas",
        "operator": "and"
      }
    }
  }
}
```

通过 `operator:"and"` 强制要求所有分词必须同时存在。

**短语搜索**

```json
POST movies/_search
{
  "query": {
    "match_phrase": {
      "title": {
        "query": "one love"
      }
    }
  }
}
```

* 要求词语按顺序完整出现。
* 等效于 URI Search 中的引号。

**模糊短语匹配**

```json
POST movies/_search
{
  "query": {
    "match_phrase": {
      "title": {
        "query": "one love",
        "slop": 1
      }
    }
  }
}
```

`slop` 参数允许词语间隔位置数（此处允许间隔 1 个词）。

**Query String 搜索**

```json
GET /movies/_search
{
  "query": {
    "query_string": {
      "default_field": "title",
      "query": "Beafiful AND Mind"
    }
  }
}
```

* 支持 AND/OR/NOT 布尔逻辑。

**多字段搜索**

```json
GET /movies/_search
{
  "query": {
    "query_string": {
      "fields": ["title","year"],
      "query": "2012"
    }
  }
}
```

* `fields` 数组定义多个搜索字段。
* 自动进行跨字段联合查询。

**Simple Query 搜索**

```json
GET /movies/_search
{
  "query": {
    "simple_query_string": {
      "query": "Beautiful +mind",
      "fields": ["title"]
    }
  }
}
```

* `+` 代替 AND 操作。
* 自动忽略无效语法。
* 适合直接暴露给前端搜索框使用。

**跨索引查询**

```json
POST /movies,404_idx/_search?ignore_unavailable=true
{
  "query": {
    "match_all": {}
  }
}
```

* 逗号分隔多个索引名称。
* `ignore_unavailable=true` 忽略不存在索引。

**源过滤**

```json
POST kibana_sample_data_ecommerce/_search
{
  "_source":["order_date"],
  "query": {
    "match_all": {}
  }
}
```

`_source` 过滤返回字段。

**脚本字段**

```json
GET kibana_sample_data_ecommerce/_search
{
  "script_fields": {
    "new_field": {
      "script": {
        "lang": "painless",
        "source": "doc['order_date'].value+'hello'"
      }
    }
  }
}
```

动态计算返回字段。

### Mapping

Mapping 类似数据库中的 schema 定义，用于定义字段名称、类型、相关配置。

#### 字段的数据类型

| **字段类型**   | **使用场景**                 | **底层实现**                            | **实例数据**                             |
| ---------- | ------------------------ | ----------------------------------- | ------------------------------------ |
| text       | 全文搜索（如文章内容、长文本）。         | 倒排索引（分词后存储），支持模糊匹配和相关性评分。           | "LanTech 指南 "                        |
| keyword    | 精确匹配（如 ID、状态码）、聚合和排序。    | 未分词的原始字符串，基于精确值匹配。                  | "user\_123"                          |
| 数值类型       | 范围查询（如价格、年龄）、数学运算和聚合。    | Lucene 的数值索引优化（如 `integer`、`long`）。 | 42 / 3.14                            |
| date       | 时间序列数据（如日志时间、事件时间戳）。     | 存储为长整型时间戳（毫秒级），支持时区转换。              | "2023-10-05T12:30:00Z"               |
| boolean    | 真/假状态（如开关、是否有效）。         | 布尔索引结构，仅存储 `true` 或 `false`。        | true                                 |
| binary     | 存储二进制数据（如图片、文件）。         | Base64 编码存储，不支持直接查询。                | "U29tZSBoZWxsbyB3b3JsZA=="           |
| range      | 区间查询（如价格区间、年龄区间）。        | Lucene 的数值/日期范围索引，支持 `>=`、`<=` 等操作。 | {"gte": 10, "lte": 20}               |
| object     | 嵌套 JSON 对象（如用户信息中的地址字段）。 | 内部文档结构，支持嵌套查询。                      | {"city": "Beijing", "zip": "100000"} |
| nested     | 复杂嵌套关系（如订单与多个商品的关联）。     | 独立索引的子文档，需通过 `nested` 查询访问。         | \[{"name": "book", "price": 15}]     |
| geo\_point | 地理位置数据（如经纬度）。            | 存储为坐标对，支持地理距离计算和范围查询。               | {"lat": 40.7128, "lon": -74.0060}    |
| 数组类型       | 存储多个相同类型值（如标签列表、商品分类）。   | 多值字段（无需显式声明），底层以扁平化多值形式存储。          | \["red", "blue", "green"]            |
| ip         | IP 地址存储与查询（如访问日志中的 IP）。  | 存储为 32 位或 128 位整数，支持 CIDR 范围查询。     | "192.168.1.1"                        |

#### Dynamic Mapping

在写入文档时如果索引不存在会自动创建索引，该机制使得我们不用手动定义 Mappings，但是通常不用这个，因为容易推算错误，并且 ES 禁止对有数据写入的字段修改定义。

**示例**

```json
# 插入测试数据
PUT mapping_test/_doc/1
{
    "uid" : "123",
    "isVip" : false,
    "isAdmin": "true",
    "age":19,
    "heigh":180
}

# 查看字段类型
GET mapping_test/_mapping

# Resp
{
  "mapping_test" : {
    "mappings" : {
      "properties" : {
        "age" : {
          "type" : "long"
        },
        "heigh" : {
          "type" : "long"
        },
        "isAdmin" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "isVip" : {
          "type" : "boolean"
        },
        "uid" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }
  }
}

```

如上结果所示，Dynamic Mapping 机制会自动推断类型，同时 text 类型会新增一个 keyword 类型支持精确查找。

**控制 Dynamic Mappings**

`dynamic` 字段不同情况下的表现。

|             | true | false | strict |
| ----------- | ---- | ----- | ------ |
| 文档可索引       | YES  | YES   | NO     |
| 字段可索引       | YES  | NO    | NO     |
| Mapping 被更新 | YES  | NO    | NO     |

#### Index

`index` 用于控制字段是否被索引。

**示例**

```json
"mobile" : {
  "type" : "text",
  "index": false
}
```

**Index Options**

| **Index Options** | **记录内容**                         | **作用**                                            | **默认类型**      |
| ----------------- | -------------------------------- | ------------------------------------------------- | ------------- |
| `docs`            | 仅文档编号（doc id）                    | 仅记录文档 ID，节省空间，适用于只需文档匹配的场景                        | 非 text 类型字段默认 |
| `freqs`           | 文档编号（doc id）+ 词频（term frequency） | 记录文档 ID 和词频，支持基于频率的查询优化                           |               |
| `positions`       | 文档编号 + 词频 + 位置（position）         | 记录位置信息，支持短语查询（Phrase Query）和邻近查询（Proximity Query） | text 类型字段默认   |
| `offsets`         | 文档编号 + 词频 + 位置 + 偏移量（offset）     | 记录字符偏移量，支持高亮显示等精细文本处理                             |               |

`index_options` 用于控制是否存储文档 ID、词频、位置和偏移量等，从而影响搜索效率和功能支持（如短语查询、高亮等）。同时记录的内容越多，占用存储空间越大。

#### null\_value

* 需要对 Null 值实现搜索。
* 只有 Keyword 类型支持设定 null\_value。

**示例**

```json
"mobile" : {
  "type" : "keyword",
  "null_value": "NULL"
}
```

#### 自定义 Analyzer

**Character Filter**

**示例**

```json
POST _analyze
{
  "tokenizer":"keyword",
  "char_filter":["html_strip"],
  "text": "<b>hello world</b>"
}
```

能够将 html 的标签去除。

```json
POST _analyze
{
  "tokenizer": "standard",
  "char_filter": [
      {
        "type" : "mapping",
        "mappings" : [ "- => _"]
      }
    ],
  "text": "123-456, I-test! test-990 650-555-1234"
}
```

能够将 text 中的 `-` 替换为 `_`。

**Tokenizer**

**示例**

```json
POST _analyze
{
  "tokenizer":"path_hierarchy",
  "text":"/user/ymruan/a/b/c/d/e"
}
```

能够按照目录层级进行切分。

**Token Filter**

**示例**

```json
GET _analyze
{
  "tokenizer": "whitespace",
  "filter": ["lowercase","stop","snowball"],
  "text": ["The girls in China are playing this game!"]
}
```

* `lowercase` - 仅小写
* `stop` - 停用词过滤
* `snowball` - 词干提取

#### Index Template

用于自动设定 Mappings 和 Settings，并按照一定的规则自动匹配到新创建的索引中。

* 仅在索引被新创建时才回起作用。
* 可以设置多个模版，设置会 merge 在一起。
* 可以控制 order 的数值控制 merge 的过程。先应用 order 低的，后续高的会覆盖之前的设定。

**示例**

```json
PUT /_template/template_test
{
    "index_patterns" : ["test*"],
    "order" : 1,
    "settings" : {
    	"number_of_shards": 1,
        "number_of_replicas" : 2
    },
    "mappings" : {
    	"numeric_detection": true
    }
}
```

表示当一个新索引以 `test` 开头时，会自动将索引的分片数设置为 2，同时会自动探测数字类型。

#### Dynamic Template

动态设定字段类型。例如：

* 将所有字符串类型设定为 keyword。
* is 开头的字段都设置为 boolean。

**示例**

```json
PUT my_index
{
  "mappings": {
    "dynamic_templates": [
      {
        "strings_as_boolean": {
          "match_mapping_type": "string",
          "match": "is*",
          "mapping": {
            "type": "boolean"
          }
        }
      },
      {
        "strings_as_keywords": {
          "match_mapping_type": "string",
          "mapping": {
            "type": "keyword"
          }
        }
      }
    ]
  }
}
```

表示当字符串类型为 is 开头时设置为 boolean 类型，其余设置为 keyword 类型。

### 聚合（Aggregation）

* ElasticSearch 除了提供搜索以外，还提供了针对 ES 进行同喜分析的功能。
* 聚合是一个分析总结全套的数据，而不是寻找单个文档。
* 性能高且实时性高（不用 T+1）。

> 本节示例需要在 Kibana 中添加官方提供的 Sample flight data 样例数据。

#### Bucket

![](https://raw.githubusercontent.com/L2ncE/images/main/PicGo/20250509181031736.png)

**示例**

```json
GET kibana_sample_data_flights/_search
{
	"size": 0,
	"aggs":{
		"flight_dest":{
			"terms":{
				"field":"DestCountry"
			}
		}
	}
}

# Resp
"aggregations" : {
"flight_dest" : {
  "doc_count_error_upper_bound" : 0,
  "sum_other_doc_count" : 3187,
  "buckets" : [
	{
	  "key" : "IT",
	  "doc_count" : 2371
	},
	{
	  "key" : "US",
	  "doc_count" : 1987
	},
	// ...
	]
  }
}
```

将国家分成了桶。

#### Metric

* 基于数据集计算结果。
* 大多数是数学计算，仅输出一个值。

**示例**

```json
GET kibana_sample_data_flights/_search
{
	"size": 0,
	"aggs":{
		"flight_dest":{
			"terms":{
				"field":"DestCountry"
			},
			"aggs":{
				"avg_price":{
					"avg":{
						"field":"AvgTicketPrice"
					}
				},
				"max_price":{
					"max":{
						"field":"AvgTicketPrice"
					}
				},
				"min_price":{
					"min":{
						"field":"AvgTicketPrice"
					}
				}
			}
		}
	}
}

# Resp
// ...
{
  "key" : "IT",
  "doc_count" : 2371,
  "max_price" : {
	"value" : 1195.3363037109375
  },
  "min_price" : {
	"value" : 100.57646942138672
  },
  "avg_price" : {
	"value" : 586.9627099618385
  }
},
// ...
```

进行了平均值、最大值、最小值的计算。

#### 嵌套

**示例**

```json
GET kibana_sample_data_flights/_search
{
	"size": 0,
	"aggs":{
		"flight_dest":{
			"terms":{
				"field":"DestCountry"
			},
			"aggs":{
				"stats_price":{
					"stats":{
						"field":"AvgTicketPrice"
					}
				},
				"wather":{
				  "terms": {
				    "field": "DestWeather",
				    "size": 5
				  }
				}
			}
		}
	}
}

# Resp
// ...
{
  "key" : "IT",
  "doc_count" : 2371,
  "wather" : {
	"doc_count_error_upper_bound" : 0,
	"sum_other_doc_count" : 506,
	"buckets" : [
	  {
		"key" : "Clear",
		"doc_count" : 428
	  },
	  {
		"key" : "Sunny",
		"doc_count" : 424
	  },
	  {
		"key" : "Rain",
		"doc_count" : 417
	  },
	  {
		"key" : "Cloudy",
		"doc_count" : 414
	  },
	  {
		"key" : "Heavy Fog",
		"doc_count" : 182
	  }
	]
  },
  "stats_price" : {
	"count" : 2371,
	"min" : 100.57646942138672,
	"max" : 1195.3363037109375,
	"avg" : 586.9627099618385,
	"sum" : 1391688.585319519
  }
},
// ...
```

在使用国家分桶后再进行票价统计和 5 组最常见天气的分布。

## 深入搜索

### Term 搜索与全文搜索

| **对比维度**   | **Term 搜索**                    | **全文搜索**                             |
| ---------- | ------------------------------ | ------------------------------------ |
| **查询类型**   | 精确匹配                           | 模糊匹配                                 |
| **分析过程**   | 不分词，直接匹配索引中的词项（Term）           | 分词处理，匹配词条（Token）                     |
| **适用字段类型** | keyword、数字、日期等精确值字段            | text 类型字段                            |
| **使用场景**   | 过滤（Filter）、聚合（Aggregation）     | 自由文本搜索（如搜索框输入）                       |
| **性能特点**   | 高效，适合大数据集和实时过滤                 | 相对耗时，依赖分词和相关性计算                      |
| **查询语法**   | `{"term": {"field": "value"}}` | `{"match": {"field": "user_input"}}` |
| **倒排索引使用** | 直接定位词项的文档列表                    | 通过词条组合计算相关性得分                        |

#### Term 搜索

**示例**

1、插入数据

```json
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "productID" : "XHDK-A-1293-#fJ3","desc":"iPhone" }
{ "index": { "_id": 2 }}
{ "productID" : "KDKE-B-9947-#kL5","desc":"iPad" }
{ "index": { "_id": 3 }}
{ "productID" : "JODL-X-1937-#pV7","desc":"MBP" }
```

2、Term 查询

```json
POST /products/_search
{
  "query": {
    "term": {
      "desc": {
        // "value": "iPhone"
        // "value": "iphone"
      }
    }
  }
}
```

使用 `iPhone` 不能搜索出结果，而小写的 `iphone` 可以。因为是精确查询，而原始数据在分词后的结果为 `iphone`。

```json
POST /products/_search
{
  "query": {
    "term": {
      "desc.keyword": {
        // "value": "iPhone"
        // "value": "iphone"
      }
    }
  }
}
```

此时使用 keyword 类型能成功获取数据。

3、Constant Score 转为 Filter

```json
POST /products/_search
{
  "explain": true,
  "query": {
    "constant_score": {
      "filter": {
        "term": {
          "productID.keyword": "XHDK-A-1293-#fJ3"
        }
      }
    }
  }
}
```

* 将 Query 转为 Filter，忽略 TF-IDF 计算，避免相关性算分的开销。
* Filter 可以有效利用缓存。

#### 全文搜索

Match / Match Phrase / Query String

* 索引和搜索时都会进行分词，查询字符串先传递到一个合适的分词器，然后生成一个供查询的列表。
* 查询会对每个词项逐个查询再将结果进行合并，并为每个文档生成一个算分。

| 场景            | 推荐查询类型                                    |
| ------------- | ----------------------------------------- |
| 普通全文搜索        | `match`                                   |
| 精确短语匹配        | `match_phrase`                            |
| 高级搜索（用户输入带逻辑） | `query_string`                            |
| 用户输入框 + 安全性优先 | `match` / `multi_match`，避免 `query_string` |

### 结构化搜索

结构化数据是指具有固定格式和明确字段的数据，每个字段都有特定的类型（如字符串、数字、日期等），并且数据是可预测、易于解析的。结构化搜索即对结构化数据进行搜索。

**示例**

1、插入数据

```json
DELETE products
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" }
```

2、Bool

```json
POST products/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": {
          "avaliable": true
        }
      }
    }
  }
}
```

3、数字

```json
POST products/_search
{
  "query": {
    "term": {
      "price": 30
    }
  }
}
```

4、Range

```json
POST products/_search
{
    "query" : {
        "constant_score" : {
            "filter" : {
                "range" : {
                    "price" : {
                        "gte" : 20,
                        "lte"  : 30
                    }
                }
            }
        }
    }
}
```

### 搜索相关性

* 搜索的相关性算分，描述了一个文档和查询语句匹配的程度。ES 会对每个匹配查询条件的结果进行算分 \_score。
* 打分的本质是排序，需要把最符合用户需求的文档排在前面。ES5 之前，默认的相关性算分采用 TF-IDF，现在采用 BM 25。

#### 词频 TF

* TF 即是词在一篇文档中出现的频率。
* 度量一条查询和结果文档相关性的简单方法：将搜索中的每一个词 TF 相加。
* Stop Word 不应考虑，类似 「the」、「的」。

#### 逆文档频率 IDF

* DF：检索词在所有文档中出现的评率。
* Inverse Document Frequency：$log(全部文档数/检索词出现过的文档总数)$
* TF-IDF 的本质就是将 TF 求和变成了加权求和：$TF(X)\*IDF(X)$

#### BM 25

与 TF-IDF 相比，当一个词的 TF 无限增加时，BM 25 算分会趋于一个稳定值。

#### Boosting

Boosting 是控制相关度的一种手段。

* 当 boost > 1 时打分的相关度相对性提升。
* 当 0 < boost < 1 时打分的权重相对性降低。
* 当 boost < 0 时，贡献负分。

### 多字段多字符串查询

#### bool 查询

一个 bool 查询是一个或者多个查询子句的组合。

| 子句        | 描述                          |
| --------- | --------------------------- |
| must      | 必须匹配，贡献算分。                  |
| should    | 选择性匹配，贡献算分。                 |
| must\_not | Filter Context 查询字句，必须不能匹配。 |
| filter    | Filter Context 必须匹配，不贡献算分。  |
| **示例**    |                             |

1、bool 查询

```json
POST /products/_search
{
  "query": {
    "bool" : {
      "must" : {
        "term" : { "price" : "30" }
      },
      "filter": {
        "term" : { "avaliable" : "true" }
      },
      "must_not" : {
        "range" : {
          "price" : { "lte" : 10 }
        }
      },
      "should" : [
        { "term" : { "productID.keyword" : "JODL-X-1937-#pV7" } },
        { "term" : { "productID.keyword" : "XHDK-A-1293-#fJ3" } }
      ],
      "minimum_should_match" :1
    }
  }
}
```

* 子查询可以任意顺序出现。
* 可以嵌套多个查询。
* 如果 bool 查询中没有 must 条件，那么 should 中必须至少满足一条查询。

2、boost 控制查询分数

```json
POST /news/_bulk  
{ "index": { "_id": 1 }}  
{ "content":"Apple Mac" }  
{ "index": { "_id": 2 }}  
{ "content":"Apple iPad" }  
{ "index": { "_id": 3 }}  
{ "content":"Apple employee like Apple Pie and Apple Juice" }

POST news/_search
{
  "query": {
    "boosting": {
      "positive": {
        "match": {
          "content": "apple"
        }
      },
      "negative": {
        "match": {
          "content": "pie"
        }
      },
      "negative_boost": 0.5
    }
  }
}
```

此时会将苹果产品放前边，而 id 为 3 的显示在最后。

### 多字段单字符串查询

#### Disjunction Max Query

将任何与任一查询匹配的文档作为结果返回。采用字段上最匹配的评分最终评分返回。

**示例**

1、插入数据

```json
PUT /blogs/_doc/1
{
    "title": "Quick brown rabbits",
    "body":  "Brown rabbits are commonly seen."
}

PUT /blogs/_doc/2
{
    "title": "Keeping pets healthy",
    "body":  "My quick brown fox eats rabbits on a regular basis."
}
```

2、bool 测试

```json
POST /blogs/_search
{
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}
```

结果可以看到 1 号文档在前，是因为 bool 会对两个进行加和平均，不符合直觉。

2、dis\_max 测试

```json
POST blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}
```

此时 2 号文档在前，符合直觉。

```json
POST blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ]
        }
    }
}
```

结果还是 1 号文档在前，同时分数相同。因为最高分数的 quick 都只出现一次。但 2 号文档中有 pet ，直觉上应该 2 号文档更高，但 dis\_max 只取最大的一条。

```json
POST blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ],
            "tie_breaker": 0.2
        }
    }
}
```

加入 tie\_breaker 后会把最匹配一条之外的分数与其值相乘，这样 2 号文档就能更高，符合直觉。

#### MultiMatch

`multi_match` 是 Elasticsearch 中一种用于在多个字段中执行全文搜索的查询方式。它扩展了 `match` 查询，允许你在多个字段上同时进行匹配。

| 类型              | 描述         | 使用场景                           |
| --------------- | ---------- | ------------------------------ |
| `best_fields`   | 匹配最佳字段（默认） | 标题或正文关键词搜索                     |
| `most_fields`   | 多字段尽量都匹配   | 多语言字段匹配                        |
| `cross_fields`  | 字段合并为整体匹配  | 姓名拆分（first\_name + last\_name） |
| `phrase`        | 短语匹配       | 查找完整短语                         |
| `phrase_prefix` | 短语前缀匹配     | 自动补全                           |
| `bool_prefix`   | 前缀词项布尔匹配   | 高效前缀搜索                         |

**示例**

1、插入数据

```json
PUT /titles
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "english",
        "fields": {"std": {"type": "text","analyzer": "standard"}}
      }
    }
  }
}

POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }
```

2、使用 most\_fields

```json
GET /titles/_search
{
   "query": {
        "multi_match": {
            "query":  "barking dogs",
            "type":   "most_fields",
            "fields": [ "title", "title.std" ]
        }
    }
}
```

结果显示 2 号文档在前，符合直觉。若不使用 MultiMatch 则会 1 号文档在前，因为 English 分词器会把 barking dogs 分为 bark 和 dog，1 号文档分数更高。

### 自然语言与查询

当处理人类自然语言时，有时尽管搜索和原文不完全匹配，但是希望搜到一些内容。

可以采取的措施：

* 归一化词元：例如消除变音符号（西语，拼音）。
* 抽取词根：消除单复数等。
* 包含同义词。
* 拼写错误处理。

#### 混合多语言的挑战

不同的索引使用不同的语言；同一个索引中，不同的字段使用不同的语言；一个文档的一个字段内混合不同的语言。

* 词干提取：以色列文档，包含了希伯来语，阿拉伯语，俄语和英文。
* 不正确的文档频率：英文为主的文章中，德文算分高（稀有）。
* 需要判断用户搜索时使用的语言。

#### 中文分词（IK）

由于生产环境中最常见的中文分词器为 IK，因此本篇也以 IK 为主。

**安装**

仓库地址：[L2ncE/es101](https://github.com/L2ncE/es101)

克隆仓库后进入到 [docker-compose 文件](https://github.com/L2ncE/es101/blob/main/docker-es-78/docker-compose.yaml) 目录，将 `docker.elastic.co/elasticsearch/elasticsearch:7.8.0` 改为 `zingimmick/elasticsearch-ik:7.8.0`。运行 `docker-compose up`。

**示例**

```json
POST _analyze
{
  "analyzer": "ik_smart",
  "text": ["剑桥分析公司多位高管对卧底记者说，他们确保了唐纳德·特朗普在总统大选中获胜"]
}
```

### Search Template

用于解耦程序和搜索 DSL。

**示例**

````json
POST _scripts/tmdb
{
  "script": {
    "lang": "mustache",
    "source": {
      "_source": [
        "title",
        "overview"
      ],
      "size": 20,
      "query": {
        "multi_match": {
          "query": "{{q}}",
          "fields": [
            "title",
            "overview"
          ]
        }
      }
    }
  }
}

POST tmdb/_search/template
{
    "id":"tmdb",
    "params": {
        "q": "basketball with cartoon aliens"
}
}```

上游可以不感知模版的变化，避免耦合。

### Index Alias

实现零停机运维。比如在进行索引重建、版本升级、滚动更新等操作时，无需中断服务。

**示例**

1、插入数据

```json
PUT movies-2019/_doc/1
{
  "name":"the matrix",
  "rating":5
}

PUT movies-2019/_doc/2
{
  "name":"Speed",
  "rating":3
}
````

2、设置别名

```json
POST _aliases
{
  "actions": [
    {
      "add": {
        "index": "movies-2019",
        "alias": "movies-latest"
      }
    }
  ]
}

POST movies-latest/_search
{
  "query": {
    "match_all": {}
  }
}
```

### Function Score Query

可以在查询结束后对每一个匹配的文档进行一系列的重新算分，根据新生成的分数进行排序。

* Weight：为每一个文档设置一个简单而不被规范化的权重。
* Field Value Factor：使用该数值来修改 `\_score`，例如将「热度」和「点赞数」作为算分的参考因素。
* Random Score： 为每一个用户使用一个不同的，随机算分结果。
* 衰减函数：以某个字段的值为标准，距离某个值越近，得分越高。
* Script Score：自定义脚本完全控制所需逻辑。

**示例**

1、插入数据

```json
DELETE blogs
PUT /blogs/_doc/1
{
  "title":   "About popularity",
  "content": "In this post we will talk about...",
  "votes":   0
}

PUT /blogs/_doc/2
{
  "title":   "About popularity",
  "content": "In this post we will talk about...",
  "votes":   100
}

PUT /blogs/_doc/3
{
  "title":   "About popularity",
  "content": "In this post we will talk about...",
  "votes":   1000000
}
```

2、使用多个参数测试

```json
POST /blogs/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": {
        "field": "votes",
        "modifier": "log1p" ,
        "factor": 0.1
      }
    }
  }
}
```

* 初始逻辑：$新的算分 = 老的算分 \* 投票数$
* 使用 modifier 平滑参数后：$新的算分 = 老的算分 \* log(1 + 投票数)$
* Factor 参数：$新的算分 = 老的算分 \* log(1 + factor \* 投票数)$

3、一致性随机函数

```json
POST /blogs/_search
{
  "query": {
    "function_score": {
      "random_score": {
        "seed": 911119
      }
    }
  }
}
```

* 使用场景：网站的广告需要提高展现率。
* 具体需求：让每个用户能看到不同的随机排名，但是也希望同一个用户访问时，结果的相对顺序保持一致。

4、Boost Mode 和 Max Boost

```json
POST /blogs/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": {
        "field": "votes",
        "modifier": "log1p" ,
        "factor": 0.1
      },
      "boost_mode": "sum",
      "max_boost": 3
    }
  }
}
```

Max Boost 可以将算分控制在一个最大值。Boost Mode 用于进行算分的数学运算。

### Suggester

实现搜索引擎中纠错的功能。原理是将文本分解为 Token 然后在索引的字典中查找相似的 Term 并返回。

四种 Suggester 的不同之处：

| Suggester 类型             | 功能与特点                                               | 适用场景                         |
| ------------------------ | --------------------------------------------------- | ---------------------------- |
| **Term Suggester**       | 对输入文本的每个词条进行纠错或建议，基于索引中的词典查找相似 Term。                | 单个词条级别的纠错和建议（如拼写错误修正）。       |
| **Phrase Suggester**     | 在 Term Suggester 基础上，考虑多个词条之间的关系（如是否共同出现、相邻程度等）。    | 多个词组成的短语级别的纠错和建议（如句子片段的修正）。  |
| **Completion Suggester** | 提供前缀匹配的自动补全功能，支持快速搜索建议（如用户输入时的提示）。                  | 快速自动补全（如搜索框输入提示）。            |
| **Context Suggester**    | 基于 Completion Suggester，增加了上下文信息的支持（如地理位置、类别等过滤条件）。 | 需要结合上下文信息的自动补全（如特定分类下的搜索提示）。 |
| **示例**                   |                                                     |                              |

1、插入数据

```json
DELETE articles

POST articles/_bulk
{ "index" : { } }
{ "body": "lucene is very cool"}
{ "index" : { } }
{ "body": "Elasticsearch builds on top of lucene"}
{ "index" : { } }
{ "body": "Elasticsearch rocks"}
{ "index" : { } }
{ "body": "elastic is the company behind ELK stack"}
{ "index" : { } }
{ "body": "Elk stack rocks"}
{ "index" : {} }
{  "body": "elasticsearch is rock solid"}
```

2、Term Suggester

几种 Suggestion Mode

* Missing：如果索引中已经存在，就不提供建议。
* Popular：推荐出现频率更加高的词。
* Always：无论是否存在，都提供建议。

```json
POST /articles/_search
{
  "size": 1,
  "query": {
    "match": {
      "body": "lucen rock"
    }
  },
  "suggest": {
    "term-suggestion": {
      "text": "lucen rock",
      "term": {
        "suggest_mode": "missing",
        "field": "body"
      }
    }
  }
}
```

3、Phrase Suggester

在 Term Suggester 的基础上增加了一些额外的逻辑。

* Max Errors：最多可以拼错的 Terms 数。
* Confidence：控制返回建议的置信度阈值。只有当建议短语的原始得分加上长度归一化后 ≥confidence 时才会被返回，默认为 1。

```json
POST /articles/_search
{
  "suggest": {
    "my-suggestion": {
      "text": "lucne and elasticsear rock hello world ",
      "phrase": {
        "field": "body",
        "max_errors":2,
        "confidence":2,
        "direct_generator":[{
          "field":"body",
          "suggest_mode":"always"
        }],
        "highlight": {
          "pre_tag": "<em>",
          "post_tag": "</em>"
        }
      }
    }
  }
}
```

4、Completion Suggester

提供了自动补全功能。对性能要求比较严苛，采用了非倒排索引的数据结构，将 Analyze 数据编码成 FST 和索引一起存放。FST 会整个加载进内存，速度很快。同时 FST 仅支持前缀查找。

```json
DELETE articles
PUT articles
{
  "mappings": {
    "properties": {
      "title_completion":{
        "type": "completion"
      }
    }
  }
}

POST articles/_bulk
{ "index" : { } }
{ "title_completion": "lucene is very cool"}
{ "index" : { } }
{ "title_completion": "Elasticsearch builds on top of lucene"}
{ "index" : { } }
{ "title_completion": "Elasticsearch rocks"}
{ "index" : { } }
{ "title_completion": "elastic is the company behind ELK stack"}
{ "index" : { } }
{ "title_completion": "Elk stack rocks"}
{ "index" : {} }
```

```json
POST articles/_search?pretty
{
  "size": 0,
  "suggest": {
    "article-suggester": {
      "prefix": "elk ",
      "completion": {
        "field": "title_completion"
      }
    }
  }
}
```

5、Context Suggester

是 Completion Suggester 的拓展，能够在搜索中加入更多的上下文信息。

可以定义两种类型的 Context：

* Category 一任意的字符串。
* Geo—地理位置信息。

实现 Context Suggester 的具体步骤：

* 定制一个 Mapping。
* 索引数据，并且为每个文档加入 Context 信息。
* 结合 Context 进行 Suggestion 查询。

```json
DELETE comments
PUT comments
PUT comments/_mapping
{
  "properties": {
    "comment_autocomplete":{
      "type": "completion",
      "contexts":[{
        "type":"category",
        "name":"comment_category"
      }]
    }
  }
}

POST comments/_doc
{
  "comment":"I love the star war movies",
  "comment_autocomplete":{
    "input":["star wars"],
    "contexts":{
      "comment_category":"movies"
    }
  }
}

POST comments/_doc
{
  "comment":"Where can I find a Starbucks",
  "comment_autocomplete":{
    "input":["starbucks"],
    "contexts":{
      "comment_category":"coffee"
    }
  }
}
```

```json
POST comments/_search
{
  "suggest": {
    "MY_SUGGESTION": {
      "prefix": "sta",
      "completion":{
        "field":"comment_autocomplete",
        "contexts":{
          "comment_category":"coffee"
        }
      }
    }
  }
}
```

## 分布式

### 节点

* 节点是一个 ElasticSearch 示例
  * 其本质就是一个 Java 进程
  * 一个机器上可以运行多个示例但生产环境推荐只运行一个
* 每一个节点都有名字，通过配置文件配置
* 每一个节点启动后都会分配一个 UID，保存在 data 目录下

#### Coordinating Node

* 处理请求的节点，叫 Coordinating Node
  * 路由请求到正确的节点，例如创建索引的请求，需要路由到 Master
* 所有节点默认都是 Coordinating Node
* 通过将其他类型设置成 False，使其成为 Dedicated Coordinating Node

#### Data Node

* 可以保存数据的节点，叫做 Data Node
  * 节点启动后，默认就是数据节点。可以设置 node.data:false 禁止
* Data Node 的职责
  * 保存分片数据。在数据扩展上起到了至关重要的作用（由 Master Node 决定如何把分片分发到数据节点上）
* 通过增加数据节点
  * 可以解决数据水平扩展和解决数据单点问题

#### Master Node

* Master Node 的职责
  * 处理创建，删除索引等请求/决定分片被分配到哪个节点 /负责索引的创建与删除
  * 维护并且更新 Cluster State
* Master Node 的最佳实践
  * Master 节点非常重要，在部署上需要考虑解决单点的问题
  * 为一个集群设置多个 Master 节点/每个节点只承担 Master 的单一角色

#### Master Eligible Nodes

* 一个集群，支持配置多个 Master Eligible 节点。这些节点可以在必要时（如 Master 节点出现故障，网络故障时）参与选主流程，成为 Master 节点
* 每个节点启动后，默认就是一个 Master Eligible 节点
  * 可以设置 node.master: false 禁止
* 当集群内第一个 Master Eligible 节点启动时候，它会将自己选举成 Master 节点

#### 选主过程

* 互相 Ping 对方，Node ld 低的会成为被选举的节点
* 其他节点会加入集群，但是不承担 Master 节点的角色。一旦发现被选中的主节点丢失，就会选举出新的 Master 节点

#### 脑裂问题

* Split-Brain，分布式系统的经典网络问题，当出现网络问题，一个节点和其他节点无法连接
  * Node 2 和 Node 3 会重新选举 Master
  * Node 1 自己还是作为 Master 组成一个集群，同时更新 Cluster State
  * 导致 2 个 Master 维护不同的 Cluster State，当网络恢复时，无法选择正确恢复

**解决方法**

* 限定选举条件，设置 quorum（仲裁），只有当 Master Eligible 节点数大于 quorum 时才能进行选举
* 7.0 后无需配置

### 分片

#### Primary Shard

* 分片是 ElasticSearch 分布式存储的基石（主分片 / 副本分片）
* 通过主分片，将数据分布在所有节点上
  * Primary Shard 可以将一份索引的数据分散在多个 Data Node 上，实现存储的水平扩展
  * 主分片数在索引创建时候指定，后续默认不能修改，如要修改需重建索引

#### Replica Shard

* 数据可用性
  * 通过引入副本分片（Replica Shard）提高数据的可用性。一旦主分片丢失，副本分片可以 Promote 成主分片。副本分片数可以动态调整。每个节点上都有完备的数据。如果不设置副本分片，一旦出现节点硬件故障，就有可能造成数据丢失
* 提升系统的读取性能
  * 副本分片由主分片（Primary Shard）同步。通过支持增加 Replica 个数，一定程度可以提高读取的吞吐量

#### 分片数的设定

* 如何规划一个索引的主分片数和副本分片数
  * 主分片数过小：例如创建了 1 个 Primary Shard 的 Index。如果该索引增长很快，集群无法通过增加节点实现对这个索引的数据扩展
  * 主分片数设置过大：导致单个 Shard 容量很小，引发一个节点上有过多分片，影响性能
  * 副本分片数设置过多，会降低集群整体的写入性能

#### 集群健康状态

```json
GET /_cluster/health

{
  "cluster_name" : "lanlance",
  "status" : "green",
  "timed_out" : false,
  "number_of_nodes" : 2,
  "number_of_data_nodes" : 2,
  "active_primary_shards" : 21,
  "active_shards" : 42,
  "relocating_shards" : 0,
  "initializing_shards" : 0,
  "unassigned_shards" : 0,
  "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" : 100.0
}
```

* Green：健康状态，所有的主分片和副本分片都可用
* Yellow：亚健康，所有的主分片可用，部分副本分片不可用
* Red：不健康状态，部分主分片不可用

#### 文档到分片的路由算法

* $shard = hash(routing) / 主分片数$
  * Hash 算法确保文档均匀分散到分片中
  * 默认 routing 值是文档 id
  * 可以自行制定 routing 值，与业务逻辑绑定也可以
  * 是 Primary Shard 数不能修改的根本原因

**删除一个文档的流程**

![](https://raw.githubusercontent.com/L2ncE/images/main/PicGo/20250526185743387.png)

#### 分片的内部原理

**倒排索引的不可变性**

倒排索引采用 Immutable Design，一旦生成，不可更改

不可变性，带来了的好处如下：

* 无需考虑并发写文件的问题，避免了锁机制带来的性能问题
* 一旦读入内核的文件系统缓存，便留在哪里。只要文件系统存有足够的空间，大部分请求就会直接请求内存，不会命中磁盘，提升了很大的性能
* 缓存容易生成和维护/数据可以被压缩

但坏处是如果需要让一个新的文档可以被搜索，需要重建整个索引。

**Lucene Index**

* 在 Lucene 中，单个倒排索引文件被称为 Segment。Segment 是自包含的，不可变更的。多个 Segments 汇总在一起称为 Lucene 的 Index，其对应的就是 ES 中的 Shard
* 当有新文档写入时，会生成新 Segment，查询时会同时查询所有 Segments，并且对结果汇总。Lucene 中有一个文件用来记录所有 Segments 信息，叫做 Commit Point

![](https://raw.githubusercontent.com/L2ncE/images/main/PicGo/20250526191215481.png)

**Refresh**

* 将 Index buffer 写入 Segment 的过程叫 Refresh。Refresh 不执行 fsync 操作
* Refresh 默认 1 秒发生一次，可通过 index.refresh\_interval 配置。Refresh 后数据就可以被搜索到了。这也是为什么 ElasticSearch 被称为近实时搜索
* 如果系统有大量的数据写入，那就会产生很多的 Segment
* Index Buffer 被占满时会触发 Refresh，默认值是 JVM 的 10％

**Transaction Log**

* Segment 写入磁盘的过程相对耗时，借助文件系统缓存，Refresh 时先将 Segment 写入缓存以开放查询
* 为了保证数据不会丢失，所以在 Index 文档时同时写 Transaction Log，高版本开始 Transaction Log 默认落盘。每个分片有一个 Transaction Log
* 在 ES Refresh 时 Index Buffer 被清空，Transaction log 不会清空

**Flush**

* 调用 Refresh，清空 Index Buffer
* 调用 fsync，将缓存中的 Segments 写入磁盘
* 清空 Transaction Log

默认 30 分钟调用一次，当 Transaction Log 满时（默认 512 MB）也会调用

**Merge**

* Segment 很多，需要被定期合并
  * 减少 Segments / 真正删除已经删除的文档
* ES 和 Lucene 会自动进行 Merge 操作
  * POST my\_index / \_forcemerge

#### 分布式搜索的运行机制

ElasticSearch 的搜索会分为 Query 和 Fetch 两阶段进行。

![](https://raw.githubusercontent.com/L2ncE/images/main/PicGo/imagesPasted%20image%2020250601172246.png)

**Query**

* 用户发出搜索请求到 ES 节点。节点收到请求后，会以 Coordinating 节点的身份，在 6 个主副分片中随机选择 3 个分片，发送查询请求。
* 被选中的分片执行查询，进行排序。每个分片都会返回 From+Size 个排序后的文档 Id 和排序值给 Coordinating 节点。

**Fetch**

* Coordinating Node 会将 Query 阶段从每个分片获取的排序后的文档 Id 列表重新进行排序。选取 From 到 From+Size 个文档的 Id。
* 以 multiget 请求的方式到相应的分片获取详细的文档数据。

潜在有性能不好和相关性算分不准的问题。

**解决算分不准的问题**

* 数据量不大的时候主分片数设置为 1，数据量大的时候保证文档均匀分散在各个分片上。
* 使用 DFS Query Then Fetch。会进行一次完整的相关性算法，耗费更多资源，性能不好。

#### 排序

* 排序是针对字段原始内容进行的，倒排索引无法发挥作用，需要正排索引。
* ElasticSearch 中有两种实现方法。
  * FieldData
  * Doc Values（列式存储，对 Text 类型无效）

Doc Values 和 Field Data 比较：

| 特性       | Doc Values                                    | Field Data                       |
| -------- | --------------------------------------------- | -------------------------------- |
| **存储位置** | **磁盘** (内存映射访问)                               | **堆内存 (JVM Heap)**               |
| **加载时机** | **按需加载** (惰性加载到 OS 缓存)                        | **按需构建** (首次用于聚合/排序时构建在内存中)      |
| **数据结构** | **列式存储** (按文档 ID 组织值)                         | **列式存储** (按段构建)                  |
| **适用字段** | `keyword`, `numeric`, `date`, `ip`, `boolean` | `text` (默认关闭)，其他字段类型 (已废弃)       |
| **默认启用** | **是** (对于支持它的字段类型)                            | **否** (尤其对于 `text` 字段，7.0+ 默认关闭) |
| **内存占用** | **低** (利用 OS 文件缓存，不直接占用 JVM 堆)                | **高** (直接占用 JVM 堆内存)             |
| **垃圾回收** | **无影响** (由 OS 管理缓存)                           | **显著影响** (对象在堆上，易引发 GC 压力)       |
| **适用操作** | 聚合、排序、脚本 (高效)                                 | `text` 字段聚合 (分词后的词条)             |
| **安全性**  | **高** (不易引发 OOM)                              | **低** (不当配置易导致节点 OOM)            |
| **版本趋势** | **推荐并默认**                                     | **仅限 `text` 字段聚合需求** (其他字段已弃用)   |

### 分页和遍历

#### 分布式系统中深度分页的问题

* ES 天生就是分布式的。查询信息同时数据保存在多个分片、多台机器上，ES 天生就需要满足排序的需要（按照相关性算分）。
* 当一个查询：From=990，Size =10。会在每个分片上先都获取 1000 个文档。通过 Coordinating Node 聚合所有结果。最后再通过排序选取前 1000 个文档。
* 页数越深，占用内存越多。为了避免深度分页带来的内存开销。ES 有一个设定，默认限定到 10000 个文档。

#### 使用 Search After 避免深度分页问题

* 避免深度分页的性能问题，可以实时获取下一页文档信息
  * 不支持指定页数 (From)
  * 只能往下翻
* 第一步搜索需要指定 sort，并且保证值是唯一的 (可以通过加入 id 保证唯一性)
* 然后使用上一次最后一个文档的 sort 值进行查询。

**示例**

1、插入数据

```json
POST users/_doc
{"name":"user1","age":10}
POST users/_doc
{"name":"user2","age":11}
POST users/_doc
{"name":"user2","age":12}
POST users/_doc
{"name":"user2","age":13}
```

2、执行查询

```json
POST users/_search
{
    "size": 1,
    "query": {
        "match_all": {}
    },
    "sort": [
        {"age": "desc"} ,
        {"_id": "asc"}    
    ]
}

POST users/_search
{
    "size": 1,
    "query": {
        "match_all": {}
    },
    "search_after":
        [
          10,
          "ZQ0vYGsBrR8X3IP75QqX"],
    "sort": [
        {"age": "desc"} ,
        {"_id": "asc"}    
    ]
}
```

#### Scroll API

**Scroll API** 是 Elasticsearch 为**大数据集深度遍历**设计的查询机制，通过创建**快照式上下文**（Snapshot Context）保证分页一致性，适用于离线导出、全量迁移等场景。

**示例**

```json
DELETE users
POST users/_doc
{"name":"user1","age":10}
POST users/_doc
{"name":"user2","age":20}
POST users/_doc
{"name":"user3","age":30}
POST users/_doc
{"name":"user4","age":40}

POST /users/_search?scroll=5m
{
    "size": 1,
    "query": {
        "match_all" : {
        }
    }
}

// 这条数据无法查到
POST users/_doc
{"name":"user5","age":50}

POST /_search/scroll
{
    "scroll" : "1m",
    "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAWAWbWdoQXR2d3ZUd2kzSThwVTh4bVE0QQ=="
}
```

**Scroll API 与 Search After 的对比**

| **特性**     | **Search After**     | **Scroll API**                   |
| ---------- | -------------------- | -------------------------------- |
| **设计目标**   | 实时深度分页（用户交互场景）       | 大数据集离线遍历（导出/迁移）                  |
| **实时性**    | 基于**当前索引状态**（实时可见变更） | **快照冻结**（创建后索引变更不可见）             |
| **内存消耗**   | 低（无服务端状态）            | 高（服务端维护上下文，占用堆内存）                |
| **分页一致性**  | 依赖 PIT 保障一致性         | 天然一致性（快照隔离）                      |
| **适用场景**   | 用户界面逐页浏览（如订单列表翻页）    | 全量数据导出、ETL 迁移、离线分析               |
| **是否支持跳页** | ❌ 仅顺序连续分页            | ❌ 仅顺序连续遍历                        |
| **资源释放**   | 无状态（客户端自主管理游标）       | 需显式删除 Scroll ID（否则超时释放）          |
| **性能开销**   | 低（分片级游标定位）           | 中（维护上下文，但比 `from/size` 高效）       |
| **最大深度**   | 仅受文档总数限制             | 同左                               |
| **推荐排序方式** | 业务字段 + `_id`（确保唯一性）  | `["_doc"]`（最高效，避免排序计算）           |
| **版本演进**   | 主流实时分页方案（结合 PIT 使用）  | 逐渐被 **Async Search** 替代（大数据异步查询） |

### 并发控制

ES 使用乐观锁进行并发控制。

#### ES 的乐观并发控制

ES 中的文档是不可变更的。如果你更新一个文档，会将就文档标记为删除，同时增加一个全新的文档。同时文档的 version 字段加 1。

**示例**

```json
DELETE products
PUT products
PUT products/_doc/1
{
  "title":"iphone",
  "count":100
}

// success
PUT products/_doc/1?if_seq_no=1&if_primary_term=1
{
  "title":"iphone",
  "count":100
}

// fail
PUT products/_doc/1?if_seq_no=1&if_primary_term=1
{
  "title":"iphone",
  "count":102
}

// success
PUT products/_doc/1?version=30000&version_type=external
{
  "title":"iphone",
  "count":100
}
```

## 数据建模

### 在 ElasticSearch 中处理关联关系

关系型数据库，一般会考虑 Normalize 数据；在 Elasticsearch，往往考虑 Denormalize 数据。

> Denormalize 的好处：读的速度变快/无需表连接/无需行锁

Elasticsearch 并不擅长处理关联关系。我们一般采用以下四种方法处理关联：

* 对象类型
* 嵌套对象 (Nested Object)
* 父子关联关系 (Parent/Child）
* 应用端关联

| 特性       | 对象类型            | 嵌套对象           | 父子关联                            | 应用端关联             |
| -------- | --------------- | -------------- | ------------------------------- | ----------------- |
| **本质**   | 默认处理 JSON 对象的方式 | 特殊的对象类型        | 同一索引内文档间的逻辑链接                   | 由应用程序维护关联         |
| **存储结构** | 对象属性扁平化存储到父文档   | 作为父文档内部的独立隐藏文档 | 父子文档独立存储在同一分片                   | 关联数据存储在不同文档或外部系统  |
| **数据边界** | 无边界，对象属性合并到父文档  | 有边界，每个嵌套对象独立存储 | 父子文档完全独立                        | 完全独立，无 ES 内部关联    |
| **查询特点** | 可能匹配不同对象的字段组合   | 确保条件匹配同一嵌套对象内部 | 需用 `has_child`、`has_parent` 等查询 | 需多次查询，先查主文档再查关联文档 |
| **更新特点** | 更新需重索引整个父文档     | 更新需重索引整个父文档    | 可单独更新子文档，不影响父文档                 | 每个文档可单独更新         |
| **适用场景** | 简单键值对对象，无需独立查询  | 对象数组需独立查询和匹配   | 子文档频繁更新或数量大                     | 关系简单、查询量小或数据分散    |
| **缺点**   | 无法精确匹配数组内对象组合   | 更新成本高，嵌套数量有限制  | 查询性能低，内存消耗大                     | 网络开销大，应用逻辑复杂      |

#### Nested Data Type

* Nested 数据类型：允许对象数组中的对象被独立索引。
* 使用 nested 和 properties 关键字，将所有数组索引到多个分隔的文档。
* 在内部，Nested 文档会被保存在两个 Lucene 文档中，在查询时做 Join 处理。

**示例**

```json
PUT my_movies
{
    "mappings": {
        "properties": {
            "actors": {
                "type": "nested",
                "properties": {
                    "first_name": {
                        "type": "keyword"
                    },
                    "last_name": {
                        "type": "keyword"
                    }
                }
            },
            "title": {
                "type": "text",
                "fields": {
                    "keyword": {
                        "type": "keyword",
                        "ignore_above": 256
                    }
                }
            }
        }
    }
}
```

若没有指定 `"type": "nested"` 时，数组数据会扁平化存储，搜索会出现不该出现的内容。

#### Parent / Child

对象和 Nested 对象每次更新都需要重新索引整个对象。

ES 提供了类似关系型数据库中 Join 的实现。可通过维护 Parent / Child 的关系，从而分离两个对象。

* 父文档和子文档是两个独立的文档。
* 更新父文档无需重新索引子文档。子文档被添加，更新或者删除也不会影响到父文档和其他的子文档。

**示例**

1、添加数据

```json
PUT my_blogs
{
  "settings": {
    "number_of_shards": 2
  },
  "mappings": {
    "properties": {
      "blog_comments_relation": {
        "type": "join",
        "relations": {
          "blog": "comment"
        }
      },
      "content": {
        "type": "text"
      },
      "title": {
        "type": "keyword"
      }
    }
  }
}

// 索引父文档
PUT my_blogs/_doc/blog2
{
  "title":"Learning Hadoop",
  "content":"learning Hadoop",
    "blog_comments_relation":{
    "name":"blog"
  }
}


// 索引子文档
PUT my_blogs/_doc/comment1?routing=blog1
{
  "comment":"I am learning ELK",
  "username":"Jack",
  "blog_comments_relation":{
    "name":"comment",
    "parent":"blog1"
  }
}

// 索引子文档
PUT my_blogs/_doc/comment2?routing=blog2
{
  "comment":"I like Hadoop!!!!!",
  "username":"Jack",
  "blog_comments_relation":{
    "name":"comment",
    "parent":"blog2"
  }
}

// 索引子文档
PUT my_blogs/_doc/comment3?routing=blog2
{
  "comment":"Hello Hadoop",
  "username":"Bob",
  "blog_comments_relation":{
    "name":"comment",
    "parent":"blog2"
  }
}
```

2、父子关系查询

```json
// 根据父文档ID查看
GET my_blogs/_doc/blog2

// 根据 Parent Id 查询
POST my_blogs/_search
{
  "query": {
    "parent_id": {
      "type": "comment",
      "id": "blog2"
    }
  }
}

// has_child 查询，返回父文档
POST my_blogs/_search
{
  "query": {
    "has_child": {
      "type": "comment",
      "query" : {
            "match": {
                "username" : "Jack"
            }
        }
    }
  }
}

// has_parent 查询，返回相关的子文档
POST my_blogs/_search
{
  "query": {
    "has_parent": {
      "parent_type": "blog",
      "query" : {
			"match": {
				"title" : "Learning Hadoop"
			}
		}
    }
  }
}
```

### 索引重建

需要索引重建的场景如下：

* 索引的 Mappings 发生变更：字段类型更改，分词器及字典更新。
* 索引的 Settings 发生变更：索引的主分片数发生改变。
* 集群内，集群间需要做数据迁移。

索引重建的方式：

* Update By Query：在现有索引上重建。
* Reindex：在其他索引上重建索引。

| 特性   | Update By Query          | Reindex                 |
| ---- | ------------------------ | ----------------------- |
| 核心操作 | 原地更新现有索引文档               | 复制数据到新索引                |
| 操作对象 | 单个索引                     | 源索引和目标索引                |
| 数据迁移 | 不支持                      | 支持                      |
| 索引结构 | 不可更改 Mapping/Settings/分片 | 可更改 Mapping/Settings/分片 |
| 主要用途 | 批量修改文档内容或删除文档            | 重建索引结构/迁移数据             |
| 性能影响 | 对原索引读写压力大                | 源索引读压力，目标索引写压力          |
| 原子性  | 非原子操作，失败需手动处理            | 目标索引独立，失败可重试            |
| 适用场景 | 字段值更新、文档删除               | 更改字段类型、分片数或跨集群迁移等       |

### 字段建模

{% @mermaid/diagram content="flowchart LR

    字段类型\["字段类型"] --> 是否要搜索及分词\["是否要搜索及分词"]

    是否要搜索及分词 --> a1\["是否要聚合及排序"]

    a1 --> 是否要额外的存储
" %}

Mapping 字段的相关设置：

* Enabled —— 设置成 false，仅做存储，不支持搜索和聚合分析（数据保存在 \_source 中）。
* Index —— 是否构建倒排索引。设置成 false，无法被搜索，但还是支持 aggregation，并出现在 \_source 中。
* Norms —— 如果字段用来过滤和聚合分析，可以关闭，节约存储。
* Doc\_values —— 是否启用 doc\_values，用于排序和聚合分析。
* Field\_data —— 如果要对 text 类型启用排序和聚合分析，fielddata 需要设置成 true。
* Store —— 默认不存储，数据默认存储在 \_source。
* Coerce —— 默认开启，是否开启数据类型的自动转换（例如，字符串转数字）。
* Multifields 多字段特性。
* Dynamic —— true/false/strict 控制 Mapping 的自动更新。

## DevOps

### 集群部署最佳实践

在生产环境中建议设置单一角色的节点。

* Dedicated master eligible nodes：负责集群状态的管理。使用低配置的 CPU，RAM 和磁盘。
* Dedicated data nodes：负责数据存储及处理客户端请求使用高配置的 CPU，RAM 和磁盘。
* Dedicated ingest nodes：负责数据处理使用高配置 CPU，中等配置的 RAM，低配置的磁盘。

从高可用 & 避免脑裂的角度出发，一般生产环境需要配置 3 台 Delicate Master Node。

#### 拓展方式

* 当磁盘容量无法满足需求时，可以增加数据节点；磁盘读写压力大时，增加数据节点。
* 当系统中有大量的复杂查询及聚合时候，增加 Coordinating 节点，增加查询的性能。

### Hot & Warm Architecture

* Hot & Warm Architecture
  * 数据通常不会有 Update 操作；适用于 Time based 索引数据（生命周期管理），同时数据量比较大的场景。
  * 引入 Warm 节点，低配置大容量的机器存放老数据，以降低部署成本。
* 两类数据节点，不同的硬件配置
  * Hot 节点（通常使用 SSD）：索引有不断有新文档写入。通常使用 SSD。
  * Warm 节点（通常使用 HDD）：索引不存在新数据的写入；同时也不存在大量的数据查询。

### 分片设计

#### 单个分片

* 7.0 开始，新创建一个索引时，默认只有一个主分片。
  * 单个分片，查询算分，聚合不准的问题都可以得以避免。
* 单个索引，单个分片时候，集群无法实现水平扩展。
  * 即使增加新的节点，无法实现水平扩展。

#### 两个分片

新增节点后，ElasticSearch 会自动进行分片的移动（Shard Rebalancing）。也就实现了水平拓展。

#### 如何设计分片数

* 当分片数＞节点数时
  * 一旦集群中有新的数据节点加入，分片就可以自动进行分配。
  * 分片在重新分配时，系统不会有 downtime。
* 多分片的好处：一索引如果分布在不同的节点，多个节点可以并行执行
  * 查询可以并行执行。
  * 数据写入可以分散到多个机器。

**如何确定主分片数**

* 从存储的物理角度看
  * 日志类应用，单个分片不要大于 50GB
  * 搜索类应用，单个分片不要超过 20GB
* 为什么要控制分片存储大小
  * 提高 Update 的性能
  * Merge 时，减少所需的资源
  * 丢失节点后，具备更快的恢复速度/便于分片在集群内 Rebalancing

**如何确定副本分片数**

* 副本是主分片的拷贝
  * 提高系统可用性：相应查询请求，防止数据丢失。
  * 需要占用和主分片一样的资源。
* 对性能的影响
  * 副本会降低数据的索引速度：有几份副本就会有几倍的 CPU 资源消耗在索引上。
  * 会减缓对主分片的查询压力，但是会消耗同样的内存资源。
    * 如果机器资源充分，提高副本数，可以提高整体的查询 QPS。

### 集群容量规划

* 一个集群总共需要多少个节点？一个索引需要设置几个分片?
  * 规划上需要保持一定的余量，当负载出现波动，节点出现丢失时，还能正常运行。
* 做容量规划时，一些需要考虑的因素
  * 机器的软硬件配置。
  * 单条文档的尺寸 / 文档的总数据量 / 索引的总数据量（Time base 数据保留的时间）/ 副本分片数。
  * 文档是如何写入的（Bulk 的尺寸)。
  * 文档的复杂度，文档是如何进行读取的（怎么样的查询和聚合）。

#### 业务性能评估

* 数据呑吐及性能需求
  * 数据写入的吞吐量，每秒要求写入多少数据？
  * 查询的吞吐量？
  * 单条查询可接受的最大返回时间？
* 了解你的数据
  * 数据的格式和数据的 Mapping。
  * 实际的查询和聚合长的是什么样的。

#### 硬件配置

* 数据节点尽量用 SSD。
* 日志类或并发查询低的场景可以用机械硬盘。
* 单节点数据控制在 2TB 以内，最高不超过 5TB。
* JVM 配置机器内存的一半，不建议超过 32G。

#### 集群扩容

* 增加 Coordinating / Ingest Node
  * 解决 CPU 和内存开销的问题。
* 增加数据节点
  * 解决存储的容量的问题。
  * 为避免分片分布不均的问题，要提前监控磁盘空间，提前清理数据或增加节点（70%）。

### 监控 & 排查 API

| **类别**         | **API 路径**                   | **核心用途**  | **关键返回字段说明**                                                             | **生产环境关注点**                                 |
| -------------- | ---------------------------- | --------- | ------------------------------------------------------------------------ | ------------------------------------------- |
| **集群健康**       | `GET /_cluster/health`       | 集群整体状态概览  | `status`(green/yellow/red), `unassigned_shards`, `active_shards_percent` | 红色状态立即处理；黄色状态检查未分配分片                        |
| **节点资源**       | `GET /_nodes/stats`          | 节点级资源监控   | `heap_used_percent`, `cpu.percent`, `disk_free_percent`                  | Heap >75% 需扩容；磁盘<20% 需清理或扩容                 |
| **索引性能**       | `GET /_cat/indices?v`        | 索引存储与文档统计 | `docs.count`, `store.size`, `pri.store.size`                             | 分片数合理性；定期清理无用索引                             |
| **慢查询定位**      | `GET /_search?pretty&q=slow` | 定位高延迟请求   | `took` 时间（需预设慢日志阈值）                                                      | 优化 `took` >1s 的查询/索引设计                      |
| **线程池积压**      | `GET /_cat/thread_pool?v`    | 线程队列监控    | `search.queue`, `write.queue`                                            | 队列>0 表示资源瓶颈，需扩容或限流                          |
| **分片分布**       | `GET /_cat/shards?v`         | 分片负载均衡检测  | `node`, `state`, `store`                                                 | 均衡分片分布；避免单节点负载过高                            |
| **任务监控**       | `GET /_tasks?detailed`       | 长任务跟踪     | `running_time_in_nanos`, `description`                                   | 监控耗时任务（如 reindex），避免阻塞集群                    |
| **磁盘水位**       | `GET /_cat/allocation?v`     | 节点磁盘使用率   | `disk.avail`, `disk.used`                                                | 预警 `disk.avail` <30% 的节点                    |
| **Segment 状态** | `GET /_cat/segments?v`       | 索引碎片监控    | `segment.count`, `size`                                                  | 过多小 segment 增加 Heap 压力；触发 `force_merge` 需谨慎 |
| **JVM 状态**     | `GET /_nodes/stats/jvm`      | 垃圾回收监控    | `young.collection_count`, `old.collection_time_in_millis`                | Young GC >2 次/秒或 Old GC >10 秒/次需调优堆大小       |

#### 关键监控指标 API 详解

1. **集群健康 (`/_cluster/health`)**
   * `status: red` 表示主分片缺失（数据丢失风险）
   * `unassigned_shards` 常见原因：节点离线或分片分配规则冲突 。
2. **节点资源 (`/_nodes/stats`)**
   * **JVM Heap 示例**：

     ```json
     "jvm": { "mem": { "heap_used_percent": 65 } }
     ```

持续>75% 需扩容或优化内存（如减少 fielddata/cache）。

3. **线程池积压 (`/_cat/thread_pool`)**

   ```bash
   node2 search 8 0 8 0 # search.queue=8 表示队列积压
   ```

`queue>0` 需扩容或优化查询并发度 。

4. **慢查询阈值配置**

   在 `elasticsearch.yml` 中设置：

   ```yaml
   index.search.slowlog.threshold.query.warn: 10s
   index.search.slowlog.threshold.query.info: 5s
   ```

#### 生产环境排查流程建议

1. **集群异常** → 用 `GET /_cluster/allocation/explain` 定位未分配分片原因。
2. **节点负载高** → 执行 `GET /_nodes/hot_threads` 抓取热点线程栈。
3. **查询延迟** → 使用 `GET /_search?profile=true` 分析执行计划。
4. **磁盘告警** → 优先清理大型临时索引或扩容冷节点 。

#### 分片没有被分配的一些原因

* INDEX\_CREATE：创建索引导致。在索引的全部分片分配完成之前，会有短暂的 Red，不一定代表有问题。
* CLUSTER\_RECOVER：集群重启阶段会有这个问题。
* INDEX\_REOPEN：Open 一个之前 Close 的索引。
* DANGLING\_INDEX\_IMPORTED：一个节点离开集群期间，有索引被删除。这个节点重新返回时，会导致问题。

#### 集群变红的常见问题与解决方法

* 集群变红，需要检查是否有节点离线。如果有，通常通过重启离线的节点可以解决问题。
* 由于配置导致的问题，需要修复相关的配置（例如错误的 box\_type，错误的副本数）。如果是测试的索引，可以直接删除。
* 因为磁盘空间限制，分片规则（ShardFiltering）引发的，需要调整规则或者增加节点。

### 读写性能优化

#### 写性能优化

目标增大写吞吐量，越高越好。

* 客户端：多线程批量写，通过性能测试确定最佳并发度。
* 服务端：
  * 使用更好的硬件，观察 CPU / IO Block。
  * 降低 IO 操作，使用 ES 自动生成文档 id。
  * 降低 CPU 和存储开销，减少不必要的分词。
  * 尽可能做到写入和分片的均衡负载，实现水平扩展。

一切优化都基于高质量的数据建模。

**Bulk、线程池和队列大小**

* 客户端
  * 单个 bulk 请求体的数据量不要太大，官方建议大约 5-15mb。
  * 写入端的 bulk 请求超时需要足够长，建议 60s 以上。
  * 写入端尽量将数据轮询打到不同节点。
* 服务器端
  * 索引创建属于计算密集型任务，应该使用固定大小的线程池来配置。来不及处理的放入队列，线程数应该配置成 CPU 核心数 +1，避免过多的上下文切换。
  * 队列大小可以适当增加，不要过大，否则占用的内存会成为 GC 的负担。

#### 读性能优化

尽可能 Denormalize 数据，从而获取最佳的性能。

* 避免查询的时候使用脚本。
* 避免使用通配符开头的正则。
* 控制聚合的数量。

**优化分片**

* 避免 Over Sharding。
* 控制单个分片的大小，查询场景尽量 20GB 以内。
* 将只读的索引进行 force merge。

### 段合并优化

ES 和 Lucene 会自动进行 Merge 操作，该操作较重，需要优化降低对系统的影响。

1. 降低分段产生的数量/频率

* 可以将 Refresh Interval 调整到分钟级别。
* 尽量避免文档的更新操作。

2. 降低最大分段大小，避免较大的分段继续参与 Merge，节省系统资源。

* `Index.merge.policy.max_merged_segment` 默认 5GB，操作此大小以后就不再参与后续的合并操作。

#### Force Merge

当 Index 不再有写入操作的时候，建议对其进行 force merge。能够提升查询速度/减少内存开销。

**最终分成几个 segments 比较合适?**

* 越少越好，最好可以 merge 成 1 个，但是 Force Merge 会占用大量的网络、IO 和 CPU。
* 如果不能在业务高峰期之前做完，就需要考虑增大最终的分段数。包括 Shard 的大小、`Index.merge.policy.max_merged_segment` 的大小。

### 缓存与内存

| 缓存类型                                | 作用域 | 主要作用                        | 触发条件               | 失效条件                          |
| ----------------------------------- | --- | --------------------------- | ------------------ | ----------------------------- |
| **Node Query Cache (Filter Cache)** | 节点级 | 缓存过滤查询（Filter）的结果（文档 ID 位图） | `filter` 上下文查询     | 索引数据变更、手动清除、段合并、LRU 淘汰        |
| **Shard Request Cache**             | 分片级 | 缓存整个搜索请求的完整结果（如聚合查询）        | `size:0` 的请求或显式启用  | 索引数据变更、手动清除、分片 Refresh、LRU 淘汰 |
| **Fielddata Cache**                 | 分片级 | 缓存字段值到文档 ID 的映射（用于排序、聚合）    | 对 `text` 字段进行聚合/排序 | 手动清除、段合并、LRU 淘汰               |

#### 管理内存的重要性

ElasticSearch 高效运维依赖于内存的合理分配。可用内存一半分配给 JVM，一半留给操作系统缓存索引文件。

内存问题可能会导致 GC 和 OOM 问题。

#### 一些常见的内存问题

* Segments 个数过多，导致 full GC。集群响应慢但没有特别多是读写，节点在持续 full GC。查看内存发现 `segments.memory` 占用很大空间。通过 force merge 段合并。
* Field data cache 过大，导致 full GC。集群响应慢但没有特别多是读写，节点在持续 full GC。查看内存发现 `fielddata.memory.size` 占用很大空间。将 `indices.fielddata.cache.size` 设小，重启节点，堆内存恢复正常。

#### Circuit Breaker

| 熔断器名称                                  | 主要功能                              | 触发条件（默认阈值）                   |
| -------------------------------------- | --------------------------------- | ---------------------------- |
| **Parent Circuit Breaker**             | 总内存保护，防止所有子熔断器总和超过节点内存            | 所有子熔断器申请内存总和 > **95% JVM 堆** |
| **Fielddata Circuit Breaker**          | 防止加载字段数据（如 `text` 字段聚合）耗尽堆内存      | 字段数据内存 > **40% JVM 堆**       |
| **Request Circuit Breaker**            | 限制单个请求（查询、聚合等）的内存使用，防止大请求压垮节点     | 单个请求内存 > **60% 父熔断器剩余内存**    |
| **In Flight Requests Circuit Breaker** | 限制传输中请求（未完成）的总内存，保护网络和内存资源        | 传输中请求总内存 > **100% JVM 堆**    |
| **Accounting Circuit Breaker**         | 跟踪分片级资源（如 Lucene 段内存），确保资源释放后及时回收 | 未释放资源 > **100% JVM 堆**       |
| **Script Compilation Circuit Breaker** | 限制一定时间窗口内脚本编译次数，防止频繁编译消耗 CPU      | **15 分钟内编译次数 > 75** (默认)     |
| **Disk Usage Circuit Breaker**         | 防止节点磁盘写满导致集群故障（触发只读保护）            | 磁盘空间 > **低水位线 (默认 85%)**     |

> 💡 注：阈值可通过配置调整（如 `indices.breaker.fielddata.limit`），触发后返回 `429 (Too Many Requests)` 错误。

## Demo

### 全文搜索

仓库地址：[L2ncE/es101](https://github.com/L2ncE/es101)

`tmdb-search` 是电影搜索项目，主要用于索引和搜索 TMDB（The Movie Database）的电影数据。

#### 项目组成部分

1. **数据处理脚本**：

* `ingest_tmdb_from_file.py`: 将 TMDB 数据导入 Elasticsearch
* `ingest_tmdb_to_appserarch.py`: 将数据导入 AppSearch（可选功能）
* `query_tmdb.py`: 搜索接口实现

2. **映射配置**：

* `mapping/english_analyzer.json`: 默认英文分析器配置
* `mapping/english_english_3_shards.json`: 3 分片的配置版本

3. **查询示例**：

* 多个针对 "Space Jam" 电影的查询示例，展示不同的查询策略

#### 快速开始

1. **启动服务**

确保已安装所需 Python 包和 Python3 环境：

```bash
pip install -r requirements.txt
```

ElasticSearch 的安装详见上方快速开始章节内容。

2. **导入数据**：

```bash
python ingest_tmdb_from_file.py
```

* 运行后会提示选择 mapping 配置。
* 选择 0 使用默认配置，或选择其他预定义配置。

3. **搜索电影**：

```bash
# 普通搜索
python query_tmdb.py

# 带高亮显示的搜索
python query_tmdb.py highlight
```

* 运行后会提示选择查询文件
* 结果会显示相关度分数和标题
* 使用 highlight 参数可以显示匹配高亮

#### 整体流程

{% @mermaid/diagram content="graph LR
subgraph 数据层
TF\[tmdb.json] --> IF\[ingest\_tmdb\_from\_file.py]
M1\[mapping配置] --> IF
end

```
subgraph ES索引层
    IF -->|创建索引/导入| IDX[TMDB索引]
    IDX -->|字段映射| F1[title:text+keyword]
    IDX -->|字段映射| F2[overview:text]
    IDX -->|字段映射| F3[popularity:float]
end

subgraph 搜索层
    QF[query/*.json文件]
    QS[query_tmdb.py]
    QF -->|加载查询DSL| QS
    QS -->|multi_match查询| IDX
    IDX -->|返回结果| QS
    QS -->|格式化输出| OUT[搜索结果]
end

subgraph 查询策略
    ST1[默认查询]
    ST2[带权重查询<br>title^10]
    ST3[most_fields查询]
    ST1 & ST2 & ST3 -.->|定义在| QF
end" %}
```

## 参考

1. <https://github.com/onebirdrocks/geektime-ELK/>
2. <https://www.elastic.co/elasticsearch>
