ElasticSearch 101

初识 ElasticSearch

使用场景

  1. 搜索引擎:生产环境中使用最多,例如京东、淘宝、美团等使用 ES 实现高性能商品搜索,支持多条件筛选、排序和相关性排名等。

  2. 日志分析与监控:ELK 作为日志分析的行业标准,是 ES 经典的使用场景。

  3. 数据分析:大数据分析,通过 DSL 快速得到结果。

与类似组件对比

对比维度

Elasticsearch

竞品

优劣总结

RDBMS

1、高性能组合查询 2、适合半结构化数据

1、完善的事务支持 2、强一致性

选 ES:复杂查询/分析场景 选 RDBMS:强事务需求

Solr

1、更成熟的分布式架构 2、近实时能力更强

ES 是 Solr 是上位替代

新项目优先选择 ES,Solr 被淘汰。

ClickHouse

1、全文检索优势 2、近实时响应

1、超大规模聚合更快 2、支持二次聚合

ES处理搜索/日志 ClickHouse处理深度分析

存储场景决策树

快速开始

仓库地址:L2ncE/es101

克隆仓库后进入到 docker-compose 文件 目录运行 docker-compose up

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

Kibana 快速开始

通过 localhost:5601 进入:

使用 Dev Tools:

Logstash 快速开始

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

Cerebro 快速开始

通过 localhost:9000 进入:

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

ElasticSearch 入门

概念名称

描述

关键点 / 特点

文档

Elasticsearch 中的最小数据单元,以 JSON 格式存储数据。

1、类比于关系型数据库中的一行数据。 2、包含实际数据字段(如 title, content 等)。

文档元数据

描述文档自身属性的系统字段,用于唯一标识和管理文档。

包含以下核心元数据: - _id:文档唯一标识符(可自定义或自动生成)。 - _index:文档所属的索引。 - _version:文档版本号(支持乐观锁)。

索引

一类结构相似文档的集合,类似于关系型数据库中的「表」。

通过 Mapping 定义字段类型和属性(如文本、数值、日期等)。支持倒排索引,优化全文搜索性能。

节点

Elasticsearch 集群中的一个运行实例,本质是一个 Java 进程。

节点类型包括: - 主节点:管理集群状态。 - 数据节点:存储数据。 - 协调节点:处理请求路由。

分片

索引的物理子集,用于分布式存储和计算。分片分为主分片(Primary Shard)和副本分片(Replica Shard)。

主分片:数据存储和写入的基本单元,数量在索引创建时固定。 副本分片:主分片的冗余拷贝,提供高可用和查询负载均衡。

与关系型数据库类比

RDBMS
ElasticSearch

Table

Index

Row

Document

Column

Field

Schema

Mapping

SQL

DSL

健康状况

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

2、在 Cerebro 中查询

测试节点下线

此时状态为 Yellow。

CRUD

操作

HTTP 方法

响应码

Source 处理

幂等性

备注

Create

PUTPOST

201(成功) 409(冲突)

必须提供完整文档 Source

是(需指定 ID)

仅当文档不存在时创建。需指定 ID(PUT)或自动生成(POST)。

Get

GET

200(存在) 404(不存在)

可指定 _source 过滤返回字段

仅用于查询文档,不修改数据。

Index

PUTPOST

201(新建) 200(更新)

必须提供完整文档 Source

若文档存在则替换(全量更新)。可指定 ID(PUT)或自动生成(POST)。

Update

POST(带 _update

200(成功) 404(不存在)

提供部分字段或脚本(支持 docscript

部分更新。支持 upsert(不存在时插入)。需启用 _source 字段。默认返回更新后的 Source。

Create

Demo1

# 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

# 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

# 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

# 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

# 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

# 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

# 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)

读取文档

执行搜索查询

数据量优化

单次请求处理数千操作

单次请求获取数百文档

单次请求合并多个复杂查询

错误处理

响应中标记每个操作的 errorstatus

响应中标记每个文档的 found 状态

每个查询独立返回状态码和结果

适用场景

数据迁移、日志流写入

批量加载关联数据、初始化页面

仪表盘批量拉取数据、跨索引聚合分析

原子性

非原子(部分成功需重试)

非原子

非原子

性能与限制对比

维度

Bulk API

mget

msearch

网络开销

单次请求处理大量操作(高吞吐)

减少多次 GET 请求(中吞吐)

合并多个查询(中高吞吐)

内存消耗

高(需缓存批量数据)

中(文档数量和大小决定)

高(复杂查询可能占用内存)

超时风险

大数据量可能触发请求超时

大文档列表可能超时

复杂查询或大数据集可能超时

分片影响

写入压力分散到多个分片

读取压力分散到多个分片

搜索压力分散到多个分片

限制规避

分批提交(每批 5-15MB)

分批查询(每批 100-1000 文档)

控制单个查询复杂度

使用

# 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"]
            }
        }
    ]
}

清除数据

DELETE users
DELETE test
DELETE test2

倒排索引

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

倒排索引的实现依赖于两个关键结构:单词词典(Term Dictionary)和倒排列表(Posting List)。

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 进行使用。

#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 分词器的分词结果

{
  "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

示例

# 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 开头的索引

示例

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 可以查看查询是如何被执行的。

泛查询

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

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

{  
    "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)"  
}

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

显式字段查询

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

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

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

短语分割问题

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

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

精确短语匹配

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

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

分组查询

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

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

布尔运算符

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

显式布尔查询,要求同时包含两个词(AND 逻辑)。

范围查询

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

[2002 TO 2018} 表示闭区间包含 2002,开区间不包含 2018(%7D 为 URL 编码的 } 符号)。

通配符搜索

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

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

模糊匹配

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

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

示例

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

更多 DSL 语法请参考 官方文档

基本 Match 查询

POST movies/_search
{
  "query": {
    "match": {
      "title": "last christmas"
    }
  }
}
  • 默认使用 OR 逻辑匹配分词结果。

  • 自动对搜索词进行分词处理。

精确 AND 匹配

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

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

短语搜索

POST movies/_search
{
  "query": {
    "match_phrase": {
      "title": {
        "query": "one love"
      }
    }
  }
}
  • 要求词语按顺序完整出现。

  • 等效于 URI Search 中的引号。

模糊短语匹配

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

slop 参数允许词语间隔位置数(此处允许间隔 1 个词)。

Query String 搜索

GET /movies/_search
{
  "query": {
    "query_string": {
      "default_field": "title",
      "query": "Beafiful AND Mind"
    }
  }
}
  • 支持 AND/OR/NOT 布尔逻辑。

多字段搜索

GET /movies/_search
{
  "query": {
    "query_string": {
      "fields": ["title","year"],
      "query": "2012"
    }
  }
}
  • fields 数组定义多个搜索字段。

  • 自动进行跨字段联合查询。

Simple Query 搜索

GET /movies/_search
{
  "query": {
    "simple_query_string": {
      "query": "Beautiful +mind",
      "fields": ["title"]
    }
  }
}
  • + 代替 AND 操作。

  • 自动忽略无效语法。

  • 适合直接暴露给前端搜索框使用。

跨索引查询

POST /movies,404_idx/_search?ignore_unavailable=true
{
  "query": {
    "match_all": {}
  }
}
  • 逗号分隔多个索引名称。

  • ignore_unavailable=true 忽略不存在索引。

源过滤

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

_source 过滤返回字段。

脚本字段

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 的数值索引优化(如 integerlong)。

42 / 3.14

date

时间序列数据(如日志时间、事件时间戳)。

存储为长整型时间戳(毫秒级),支持时区转换。

"2023-10-05T12:30:00Z"

boolean

真/假状态(如开关、是否有效)。

布尔索引结构,仅存储 truefalse

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 禁止对有数据写入的字段修改定义。

示例

# 插入测试数据
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 用于控制字段是否被索引。

示例

"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。

示例

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

自定义 Analyzer

Character Filter

示例

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

能够将 html 的标签去除。

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

能够将 text 中的 - 替换为 _

Tokenizer

示例

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

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

Token Filter

示例

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 低的,后续高的会覆盖之前的设定。

示例

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。

示例

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

示例

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

  • 基于数据集计算结果。

  • 大多数是数学计算,仅输出一个值。

示例

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
  }
},
// ...

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

嵌套

示例

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、插入数据

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 查询

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

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

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

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

3、Constant Score 转为 Filter

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、插入数据

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

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

3、数字

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

4、Range

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 查询

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 控制查询分数

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、插入数据

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 测试

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

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

2、dis_max 测试

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

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

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

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

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、插入数据

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

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

克隆仓库后进入到 docker-compose 文件 目录,将 docker.elastic.co/elasticsearch/elasticsearch:7.8.0 改为 zingimmick/elasticsearch-ik:7.8.0。运行 docker-compose up

示例

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

Search Template

用于解耦程序和搜索 DSL。

示例

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、设置别名

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、插入数据

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、使用多个参数测试

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、一致性随机函数

POST /blogs/_search
{
  "query": {
    "function_score": {
      "random_score": {
        "seed": 911119
      }
    }
  }
}
  • 使用场景:网站的广告需要提高展现率。

  • 具体需求:让每个用户能看到不同的随机排名,但是也希望同一个用户访问时,结果的相对顺序保持一致。

4、Boost Mode 和 Max Boost

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、插入数据

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:无论是否存在,都提供建议。

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。

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 仅支持前缀查找。

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" : {} }
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 查询。

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"
    }
  }
}
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 容量很小,引发一个节点上有过多分片,影响性能

    • 副本分片数设置过多,会降低集群整体的写入性能

集群健康状态

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 数不能修改的根本原因

删除一个文档的流程

分片的内部原理

倒排索引的不可变性

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

不可变性,带来了的好处如下:

  • 无需考虑并发写文件的问题,避免了锁机制带来的性能问题

  • 一旦读入内核的文件系统缓存,便留在哪里。只要文件系统存有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能

  • 缓存容易生成和维护/数据可以被压缩

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

Lucene Index

  • 在 Lucene 中,单个倒排索引文件被称为 Segment。Segment 是自包含的,不可变更的。多个 Segments 汇总在一起称为 Lucene 的 Index,其对应的就是 ES 中的 Shard

  • 当有新文档写入时,会生成新 Segment,查询时会同时查询所有 Segments,并且对结果汇总。Lucene 中有一个文件用来记录所有 Segments 信息,叫做 Commit Point

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 两阶段进行。

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、插入数据

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、执行查询

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)保证分页一致性,适用于离线导出、全量迁移等场景。

示例

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。

示例

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_childhas_parent 等查询

需多次查询,先查主文档再查关联文档

更新特点

更新需重索引整个父文档

更新需重索引整个父文档

可单独更新子文档,不影响父文档

每个文档可单独更新

适用场景

简单键值对对象,无需独立查询

对象数组需独立查询和匹配

子文档频繁更新或数量大

关系简单、查询量小或数据分散

缺点

无法精确匹配数组内对象组合

更新成本高,嵌套数量有限制

查询性能低,内存消耗大

网络开销大,应用逻辑复杂

Nested Data Type

  • Nested 数据类型:允许对象数组中的对象被独立索引。

  • 使用 nested 和 properties 关键字,将所有数组索引到多个分隔的文档。

  • 在内部,Nested 文档会被保存在两个 Lucene 文档中,在查询时做 Join 处理。

示例

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、添加数据

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、父子关系查询

// 根据父文档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/分片

主要用途

批量修改文档内容或删除文档

重建索引结构/迁移数据

性能影响

对原索引读写压力大

源索引读压力,目标索引写压力

原子性

非原子操作,失败需手动处理

目标索引独立,失败可重试

适用场景

字段值更新、文档删除

更改字段类型、分片数或跨集群迁移等

字段建模

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 示例

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

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

  1. 线程池积压 (/_cat/thread_pool)

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

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

  1. 慢查询阈值配置

    elasticsearch.yml 中设置:

    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 调整到分钟级别。

  • 尽量避免文档的更新操作。

  1. 降低最大分段大小,避免较大的分段继续参与 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

tmdb-search 是电影搜索项目,主要用于索引和搜索 TMDB(The Movie Database)的电影数据。

项目组成部分

  1. 数据处理脚本

  • ingest_tmdb_from_file.py: 将 TMDB 数据导入 Elasticsearch

  • ingest_tmdb_to_appserarch.py: 将数据导入 AppSearch(可选功能)

  • query_tmdb.py: 搜索接口实现

  1. 映射配置

  • mapping/english_analyzer.json: 默认英文分析器配置

  • mapping/english_english_3_shards.json: 3 分片的配置版本

  1. 查询示例

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

快速开始

  1. 启动服务

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

pip install -r requirements.txt

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

  1. 导入数据

python ingest_tmdb_from_file.py
  • 运行后会提示选择 mapping 配置。

  • 选择 0 使用默认配置,或选择其他预定义配置。

  1. 搜索电影

# 普通搜索
python query_tmdb.py

# 带高亮显示的搜索
python query_tmdb.py highlight
  • 运行后会提示选择查询文件

  • 结果会显示相关度分数和标题

  • 使用 highlight 参数可以显示匹配高亮

整体流程

参考

  1. https://github.com/onebirdrocks/geektime-ELK/

  2. https://www.elastic.co/elasticsearch

最后更新于