1 Elastic Stack
1. 概述
ElasticStack 相比传统大数据分析工具的优势:
- 使用门槛低, 开发周期短, 上线块
- 性能好, 查询快, 实时展示结果
- 扩容方便, 快速制成增长迅速的数据
ElasticStack 包含的内容(ELK+Beats):
- Kibaba
- 可视化工具, 负责数据探索与可视化分析
- ElasticSearch
- 搜索引擎, 负责数据存储, 查询和分析
- Logstash + Beats: 负责数据的收集和处理
- ETL: Extract Transform Load
- 支持多种数据源
- 数据文件, 如日志, Excel
- 数据库, 如 MySQL, Oracle
- Http 服务
- 网络数据
- 支持自定义扩展
ElasticStack 常见用途:
- 搜索引擎
- 日志分析
- 指标分析
1.2 安装
1.2.1 Elasticsearch
docker
从 dockerhub
上安装镜像
1 | $ docker pull elasticsearch:7.11.1 |
编写 docker 启动 Elasticsearch 脚本
1 |
|
其中需要注意:
discovery.type=single-node
用来指定以单节点模式启动数据挂载路径
usr/share/elasticsearch/data
和日志挂载路径usr/share/elasticsearch/logs
, 是根据docker inspect elasticsearch:7.11.1
中的WorkingDir": "/usr/share/elasticsearch
确定1
2
3
4
5
6
7
8
9
10{
// ...
"WorkingDir": "/usr/share/elasticsearch",
"Entrypoint": [
"/bin/tini",
"--",
"/usr/local/bin/docker-entrypoint.sh"
],
// ...
}再根据Entrypoint 确定启动脚本中的内容
1
2
3
4
5
6if [[ "$(id -u)" == "0" ]]; then
# If requested and running as root, mutate the ownership of bind-mounts
if [[ -n "$TAKE_FILE_OWNERSHIP" ]]; then
chown -R 1000:0 /usr/share/elasticsearch/{data,logs}
fi
fi
1.2.2 LogStash
1.2.3 Beats
1.2.4 Kinbana
2. Elasticsearch
2.1 概述
2.1.1 常见术语
ES 中的概念 | 描述 | 类比 |
---|---|---|
文档 Document | 用户存储在 es 中的数据文档 | 类似 MySQL 中的记录 |
索引 Index | 由具有相同字段的文档列表组成 | 类似 MySQL 中的 table |
映射 Mapping | 定义索引包含的字段名和类型等元信息 | 类似 MySQL 的 schema |
节点 Node | 一个 Elasticsearch 的运行实例, 是集群的构成单元 | |
集群 Cluster | 由一个或多个节点组成, 对外提供服务 |
Document
类型: JsonObject, 由字段(Field)组成, 常见数据类型如下:
- 字符串: text, keyword
- 数值型: long, integer, short, byte, double, float, half_float, scaled_float
- 布尔: boolean
- 日期: date
- 二进制: binary
- 范围类型: integer_range, float_range, long_range, double_range, date_range
每个文档都有唯一的 id 标识
自行指定
es 自动生成
元数据, 用于标注文档的相关信息
字段名 | 功能 |
---|---|
_index |
文档所在的索引名 |
_type |
文档所在的类型名(即将废弃) |
_id |
文档唯一 id |
_uid |
组合 id, 由 _type 和 _id 组成(未来 type 废弃后会和 id 相同) |
_source |
文档的原始 json 数据, 可以从这里获取每个字段的内容 |
_all |
整合所有字段内容到该字段(比较占空间且查询效率不高, 6.0 已经默认禁用) |
Index
索引中存储具有相同结构的文档, 每个文档都有自己的 mapping
定义, 用于定义字段名和类型等元信息
一个集群可以有多个索引
2.1.2 RESTAPI
Elasticsearch 集群对外提供 RESTful API:
- REST: REpresentational State Transfer(表现层状态转移, 表现层指资源)
- URI 指定资源, 如 Index, Document 等
- Http Method 指明资源操作类型, 如 Get, Post, Put, Delete 等
常见交互方式:
- Curl 命令行
- Kibana DevTools
2.1.3 索引 API
es 有专门的 IndexAPI, 用于创建, 更新, 删除索引配置等
创建索引 api 如下
1
2
3
4
5
6
7
8PUT /test_index
resp:
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "test_index"
}查看现有索引
1
2
3
4
5
6
7
8
9
10GET /_cat/indices
green open .apm-custom-link RkPmPmV0TTiqjDY1BcuZHg 1 0 0 0 208b 208b
green open kibana_sample_data_ecommerce UVvkmSlpRv6oGuXDQwmtiA 1 0 4675 0 4mb 4mb
green open .kibana-event-log-7.11.1-000001 hY3-zLbTSJurWBhAUxRs-g 1 0 4 0 21.8kb 21.8kb
green open .kibana_task_manager_1 CV0RqH_eTf6Z6UN4EJkG1Q 1 0 8 1288 247.1kb 247.1kb
green open .apm-agent-configuration c5zj1ZBgRduWHKhbNo8kgw 1 0 0 0 208b 208b
green open .async-search tgGPCSDGTKWYppN0_aHO6w 1 0 0 0 5.6kb 5.6kb
yellow open test_index 8BG1bULAS-2D2HiEqkIdWw 1 1 0 0 208b 208b
green open .kibana_1 inVHSYXITsKGc0TFas8wcQ 1 0 117 54 3mb 3mb
2.1.4 文档 API
创建文档
创建文档时, 如果索引不存在, es 会自动创建对应的 index 和 type
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21PUT /test_index/_doc/1 // PUT 代表新增, test_index 代表 index, doc 代表 type, 1 代表 id
{
"username": "alfred",
"age": 1
}
resp:
{
"_index" : "test_index",
"_type" : "doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 2
}不指定 id 创建文档 api, 返回的 id 是 es 自动生成的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21POST /test_index/_doc
{
"username": "tom",
"age": 20
}
resp:
{
"_index" : "test_index",
"_type" : "_doc",
"_id" : "3dapWHgBNjqeYmKhg89n",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 2
}
批量创建文档
es 允许一次创建多个文档, 从而减少网络传输开销, 提升写入速率, endpoint 为
_bulk
bulk 的每一个操作都需要两个 json 串, 语法如下:
-
{"action": {"metadata"}}
-
{"data"}
-
可执行的操作
- delete: 删除一个文档, 只需要一个 json 串
- create: 执行
PUT /index/id/_create
, 强制创建 - index: 普通的 put 操作, 可以创建/替换文档
- update: 执行 partial update 操作
bulk api对json的语法,有严格的要求,每个json串不能换行,只能放一行,同时一个json串和一个json串之间,必须有一个换行
bulk操作中,任意一个操作失败,是不会影响其他的操作的,但是在返回结果里,会告诉你异常日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46POST _bulk
{"index":{"_index":"test_index","_type":"doc","_id":"3"}}
{"username":"alfred","age":10}
{"delete":{"_index":"test_index","_type":"doc","_id":"1"}}
// resp
{
"took" : 14,
"errors" : false,
"items" : [
{
"index" : {
"_index" : "test_index",
"_type" : "doc",
"_id" : "3",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 3,
"_primary_term" : 2,
"status" : 201
}
},
{
"delete" : {
"_index" : "test_index",
"_type" : "doc",
"_id" : "1",
"_version" : 2,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 4,
"_primary_term" : 2,
"status" : 200
}
}
]
}
查询文档
指定要查询的文档 id,
_source
存储了文档的完整原始信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16GET /test_index/_doc/1
resp:
{
"_index" : "test_index",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 2,
"found" : true,
"_source" : {
"username" : "alfred",
"age" : 1
}
}如果 id 不存在, 会返回 404
1
2
3
4
5
6
7
8
9GET /test_index/_doc/3
resp:
{
"_index" : "test_index",
"_type" : "_doc",
"_id" : "3",
"found" : false
}搜索所有文档, 需要使用
_search
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39GET /test_index/_search
{
"query": {
"term": {
"_id": "1"
}
}
}
resp:
{
"took" : 1, // 查询耗时, 单位 ms
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1, // 符合条件的总文档数
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [ // 返回文档的详情数组, 默认返回前 10 个
{
"_index" : "test_index",
"_type" : "doc",
"_id" : "1",
"_score" : 1.0, // 文档的得分
"_source" : {
"username" : "alfred",
"age" : 1
}
}
]
}
}
批量查询文档
1 | GET /_mget |
更新文档
删除文档
2.2 倒排索引与分词
2.2.1 简介
正排索引: 对应书的目录页, 文档 id 到文档内容, 单词的关联关系
文档 id 文档内容 1 Elasticsearch is a search engine 2 Google is a Search Engine 3 Search engine is important for applications 倒排索引: 对应书的索引页, 单词到文档 id 的关联关系
单词 文档 id 列表 Elasticsearch 1 is 1, 2, 3 a 1, 2 search 1, 2, 3 engine 1, 2, 3 google 2 important 3 for 3 applications 3
倒排索引查询流程:
- 查询包含 SearchEngine 的文档
- 通过倒排索引获取
SearchEngine
对应的文档: 1, 2, 3 - 通过正排索引查询 1, 2, 3 的完整内容
- 返回用户最终结果
- 通过倒排索引获取
2.2.2 倒排索引的组成
倒排索引是搜索引擎的核心, 主要包含两部分:
单词词典(Term Dictionary)
- 记录所有文档的单词, 一般都比较大
- 记录单词到倒排列表的相关信息
- 单词字典的实现一般采用 B+Tree
倒排列表(Posting List), 记录了单词对应的文档集合, 由倒排索引项组成, 倒排索引项的组成如下:
以
search
为例:DocId TF Position Offset 1 1 1 <18, 24> 2 1 1 <11, 17> 3 1 0 <0, 6> - 文档 id, 用于获取原始信息
- 单词频率, 记录该单词在对应文档中的出现次数, 用于后续相关性计算
- 位置: 记录单词在文档中的分词位置(多个), 用于做词语搜索
- 偏移, 记录单词在文档的开始和结束位置, 用于做高亮展示
单词字典和倒排索引整合在一起的结构为: 单词字典的 B+树每一个叶子节点都指向倒排列表的下标
es 存储的是一个 json 格式的文档, 每个字段都会有自己的倒排索引, 如
1
2
3
4
5
6{
"username": "alfred",
"job": "programmer"
}
此时 username 和 job 分别会创建对应的倒排索引
2.2.3 分词
分词是指将文本转换成一系列单词的过程, 也可以叫做文本分析, 在 es 中被称为 Analysis
1 | "Elasticsearch 是最流程的搜索引擎" => ["Elasticsearch", "流行", "搜索引擎" ] |
分词器是 es 中专门处理分词的组件, 英文为 Analyzer, 它的组成如下:
- Character Filters: 针对原始文本进行处理, 比如去除 html 特殊标记
- Tokenizer: 将原始文本按照一定规则切分为单词
- Token Filters: 针对
Tokenizer
处理的单词进行再加工, 比如转小写, 删除或新增等处理
Analyze API
直接指定 analyzer 进行测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46POST _analyze
{
"analyzer": "standard",
"text": "Im van, Im an artist"
}
// resp:
{
"tokens" : [
{
"token" : "im", // 分词结果
"start_offset" : 0, // 起始偏移
"end_offset" : 2, // 结束偏移
"type" : "<ALPHANUM>",
"position" : 0 // 分词位置
},
{
"token" : "van",
"start_offset" : 3,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "im",
"start_offset" : 8,
"end_offset" : 10,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "an",
"start_offset" : 11,
"end_offset" : 13,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "artist",
"start_offset" : 14,
"end_offset" : 20,
"type" : "<ALPHANUM>",
"position" : 4
}
]
}直接指定索引中的字段进行测试(调试时使用, 查看该字段实际的分词方式):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46POST test_index/_analyze
{
"field": "username",
"text": "Im van, Im an artist"
}
// resp:
{
"tokens" : [
{
"token" : "im",
"start_offset" : 0,
"end_offset" : 2,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "van",
"start_offset" : 3,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "im",
"start_offset" : 8,
"end_offset" : 10,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "an",
"start_offset" : 11,
"end_offset" : 13,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "artist",
"start_offset" : 14,
"end_offset" : 20,
"type" : "<ALPHANUM>",
"position" : 4
}
]
}自定义分词器进行测试(按照自己的需求定制分词器, 主要用于调试自己实际需要的分词器):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47POST _analyze
{
"tokenizer": "standard",
"filter": ["lowercase"], // token filter
"text": "Im van, Im an artist"
}
// resp:
{
"tokens" : [
{
"token" : "im",
"start_offset" : 0,
"end_offset" : 2,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "van",
"start_offset" : 3,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "im",
"start_offset" : 8,
"end_offset" : 10,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "an",
"start_offset" : 11,
"end_offset" : 13,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "artist",
"start_offset" : 14,
"end_offset" : 20,
"type" : "<ALPHANUM>",
"position" : 4
}
]
}
预定义的分词器
- Standard Analyzer:
- 默认分词器, 组成如下:
- Tokenizer:
standard
- TokenFilters: [
standard
,Lower case
,Stop(默认不开启)
]
- Tokenizer:
- 按词切分, 支持多语言
- 小写处理
- 默认分词器, 组成如下:
- Simple Analyzer:
- 组成:
- Tokenizer:
Lower Case
- Tokenizer:
- 按照非字母切分
- 小写处理
- 组成:
- Whitespace Analyzer
- 组成:
- Tokenizer:
Whitespace
- Tokenizer:
- 仅按照空格切分
- 组成:
- Stop Analyzer(Stop Word: 指语气助词等修饰性词语):
- 组成:
- Tokenizer:
LowerCase
- Token Filters: [
Stop
]
- Tokenizer:
- 组成:
- Keyword Analyzer
- 组成:
- Tokenizer:
keyword
- Tokenizer:
- 不分词, 直接将输入作为一个单词输出
- 组成:
- Pattern Analyzer
- 组成:
- Tokenizer: Pattern
- Token Filters: [
Lower case
,Stop(默认不开启)
]
- 通过正则表达式自定义分隔符, 默认为
\W+
, 即非字符的符号作为分隔符
- 组成:
- Language Analyzer
- 提供了 30+常见语言的分词器
中文分词
受限于中文语境, 难度较大
常见分词系统:
- IK
- 实现中英文单词的切分, 支持 ik_smart, ik_maxword 等模式
- 可以自定义词库, 支持热更新分词词典
- https://github.com/medcl/elasticsearch-analysis-ik
- jieba
- python 中最流行的分词系统, 支持分词和词性标注
- 支持繁体分词, 自定义词典, 并行分词等
- https://github.com/sing1ee/elasticsearch-jieba-plugin
基于自然语言处理的分词系统:
- Hanlp
- 由一系列模型与算法组成的 java 工具包, 目标是普及自然语言处理在生产环境中的应用
- https://github.com/hankcs/HanLP
- THULAC
- THU Lexical Analyzer for Chinese, 由清华大学自然语言处理与人文社会计算实验室研制推出的一套中文词法分析工具包, 具有中文分词和词性标注功能
- https://github.com/microbun/elasticsearch-thulac-plugin
自定义分词
通过自定义 Character Filters, Tokenizer 和 Token Filter 实现:
- CharacterFilters:
- 在 Tokenizer 之前对原始文本进行处理, 比如增加, 删除或替换字符等
- 自带功能如下:
- HTML Strip 去除 html 标签和转换 html 实体
- Mapping 进行字符串替换操作
- Pattern Replace 进行正则匹配替换
- 会影响后续 Tokenizer 解析的 postion 和 offset 信息
2.3 Mapping
2.3.1 简介
类似数据库中的表结构定义, 主要作用如下:
- 定义 index 下的字段名
- 定义字段类型
- 定义倒排索引相关的配置, 比如是否索引, 是否记录 position 等
1 | GET /test_index/_mapping |
2.3.2 自定义 mapping
1 | /PUT my_index |
Mapping 中的字段类型一旦设定成功后, 禁止直接修改, 因为 Lucene 实现的倒排索引生成后不允许修改, 需要重新建立新的索引, 然后做 reindex
操作
dynamic
- 可以通过
dynamic
参数来控制字段的新增:- true(默认): 允许自动新增字段
- false: 不允许自动新增字段, 但是文档可以正常写入, 但无法对字段进行查询等操作
- strict: 文档不能写入, 直接报错
-
dynamic
字段可以给设置到 index 级别, 或者可以设置到 object 级别
- 可以通过
copy_to
- 将该字段的值复制到目标字段, 实现类似
_all
的作用 - 不会出现在
_source
中, 只用来搜索
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38PUT my_index
{
"mappings": {
"properties": {
"first_name": {
"type": "text",
"copy_to": "full_name"
},
"last_name": {
"type": "keyword",
"copy_to": "full_name"
},
"full_name": {
"type": "text"
}
}
}
}
// 写值
PUT my_index/doc/1
{
"first_name": "John",
"last_name": "Smith"
}
// 搜索
GET my_index/_search
{
"query": {
"match": {
"full_name": {
"query": "John Smith",
"operator": "and"
}
}
}
}- 将该字段的值复制到目标字段, 实现类似
index
- 控制当前字段是否生成搜索, 默认为 true, 即记录索引, false 为不记录, 即不可搜索
1
2
3
4
5
6
7
8
9
10
11PUT my_index
{
"mappings": {
"properties": {
"cookie": {
"type": "text",
"index": false // 此时cookie 字段不会生成索引, 因此查询时会报错
}
}
}
}index_options
: 用于控制记录倒排索引的内容- 取值如下:
- docs: 只记录 docId
- freqs: 记录 docId 和 termFrequencies
- positions: 记录 docId, termFrequencies 和 termPosition
- offset: 记录 docId, termFrequencies 和 termPosition 和 characterOffset
- text 类型默认配置为
position
, 其他默认为 docs - 记录内容越多, 占用空间越大
1
2
3
4
5
6
7
8
9
10
11PUT my_index
{
"mappings": {
"properties": {
"cookie": {
"type": "text",
"index_options": "offsets"
}
}
}
}- 取值如下:
null_value
: 指明 es 对 null 值得处理策略, 默认为 null, 即空值, 此时 es 会忽略该值, 可以通过设定该值设定字段的默认值1
2
3
4
5
6
7
8
9
10
11PUT my_index
{
"mappings": {
"properties": {
"cookie": {
"type": "keyword",
"null_value": "NULL" // 标明该字段如果传 null 时默认值为 "NULL"
}
}
}
}
2.3.3 核心数据类型
核心数据类型:
- 字符串型: text(分词), keyword(不分词)
- 数值型: long, integer, short, byte, double, float, half_float, scaled_float
- 日期类型: date
- 布尔类型: boolean
- 二进制类型: binary
- 范围类型: integer_range, float_range, long_range, double_range, date_range
负责数据类型:
- 数组类型: array(由于倒排索引的特性, es 所有数据类型天生支持索引)
- 对象类型: object
- 嵌套类型: nested object(独立存在, 没有和父文档混在一起)
地理位置数据类型:
- geo_point
- geo_shape
专用类型:
- ip: 记录 pi 地址
- completion: 实现自动补全
- token_count: 记录分词数
- murmur3: 记录字符串 hash 值
- percolator
- join
多字段特性(multi_fields
): 允许对同一字段采用不同配置, 比如分词, 常见例子如对人名实现拼音搜索, 只需要在人名中新增一个子字段 pinyin
即可
1 | { |
2.3.4 Dynamic Mapping
es 可以自动识别文档字段类型, 从而降低用户使用成本
1 | PUT /test_index/doc/1 |
es 是依靠 json 文档的字段类型来实现自动识别字段类型, 支持的字段类型如下:
JSON 类型 | es 映射类型 |
---|---|
null | 忽略(如果 es 设置 null_value , 会将 null 值映射为预设的值并推断相应类型) |
boolean | boolean |
浮点 | float |
整型 | long |
object | object |
array | 由第一个非 null 值得类型决定 |
string | 匹配为日期则设为 date 类型(默认开启) 匹配为数字的话设为 float 或 long 类型(默认关闭) 设为 text 类型, 并附带 keyword 的子字段 |
1 | PUT /test_index/_doc/1 |
2.3.5 日期与数字的识别
- 日期的自动识别可以可以自行配置日期格式, 以满足各种需求
默认是["strict_date_optional_time", "yyyy/MM/dd HH:mm:ss Z || yyyy/MM/dd Z"]
- strict_date_optional_time 是 ISO datetime, 完整格式类似:
YYYY-MM-DDThh:mm:ssTZD(e.g. 2021-03-29T20:24:05 +08:00)
- dynamic_date_formats 可以自动以日期类型
- date_detection 可以关闭日期自动识别机制
1 | // 自定义日期识别格式 |
2.3.6 DynamicTemplates
- 执行顺序默认由上到下执行, 创建索引时会以此比较所有
dynamic_template
- 允许根据 es 自动识别的数据类型, 字段名来动态设定字段类型, 可实现如下效果:
- 将所有字符串类型都设定为 keyword 类型, 即默认不分词
- 所有以 message 开头的字段都设定为 text 字段
- 所有以 long_开头的字段都设定为 long 类型
- 所有识别为 double 类型的字段都设定为 float
- 建议:
- 写入一条文档到 es 的临时索引中, 获取 es 自动生成的 mapping
- 修改步骤一得到的 mapping, 自定义相关配置
- 使用步骤二的 mapping 创建实际索引
1 | PUT test_index |
关键词 | 功能 |
---|---|
match_mapping_type |
匹配 es 自动识别的字段类型, 如 boolean, long, string 等 |
match / unmatch |
匹配字段名 |
path_match / path_unmatch |
匹配路径(对象内部的字段) |
2.3.7 索引模板
- IndexTemplate, 主要用于在新建索引时自动应用预先设定的配置, 简化索引创建的操作步骤
- 可以设定索引的配置和 mapping
- 可以有多个模板, 根据 order 配置, order 大的覆盖小的配置
1 | PUT _template/test_template // template 名称 |
2.4 Search API
搜索方式
- match: 查询语句会被分词, 返回包含一个或多个分词结果的文档
- match_phrase: 查询语句会被分词, 返回同时包含所有分词结果的文档
- term: 完全匹配, 不进行分词器分析, 文档中必须包含整个查询语句
实现对 es 中存储的数据进行查询分析, endpoint 为
_search
:-
GET /_search
-
GET /my_index/_search
-
GET /my_index1,my_index2/_search
-
GET /my_*/_search
-
主要有两种查询形式
URI search
1
GET /my_index/_search?q=user:alfred
- 操作简单, 方便通过命令行测试
- 仅包含部分查询语法
Request Body Search
1
2
3
4
5
6
7
8GET /my_index/_search
{
"query": {
"term": {
"user": "alfred"
}
}
}- es 提供完备的查询语法 Query DSL(Domain Specific Language)
2.4.1 URI Search
通过 url query 参数来实现搜索, 常用参数如下:
1 | GET /my_index/_search?q=alfred&df=user&sort=age:asc&from=4&size=10&timeout=1s |
- q: 指定查询的语句, 语法为
Query String Syntax
- df: q 中不指定字段时默认查询的字段, 如果不指定, es 会查询所有的字段
- sort: 排序
- timeout: 指定超时时间, 默认不超时
- from, size: 分页
QueryStringSyntax:
- term 与 phrase:
- term 标识多个单词:
alfred way
等效于alfred OR way
- phrase 标识一个词语(先后顺序):
"alfred way"
词语查询, 要求先后顺序
- term 标识多个单词:
- 泛查询
- alfred 等效于在所有字段去匹配该 term
- 指定字段
- name:alfred
- Group: 分组设定, 使用括号指定匹配的规则
- (quick OR brown) AND fox
- status:(active OR pending) title:(full text search), 如果不加括号
status:active OR pending
会变成status 字段为 active 或任意字段包含 pending
- 布尔操作符
- AND(&&), OR(||), NOT(!)
- name:(tom NOT lee): name 中包含 tom 但不包含 lee 的所有文档
- 必须大写不能小写
+ / -
分别对应 must 和 must_not- name:(tom +lee -alfred): name 一定包含 lee, 一定不包含 alfred, 可以包含 tom 的文档
- 对应的 AND/OR/NOT 写法为: name((lee && !alfred) || (tom && lee && !alfred))
-
+
在 url 中会被解析为空格, 要使用 encode 后的结果才可以, 为%2B
- AND(&&), OR(||), NOT(!)
- 范围查询, 支持数值和日期
- 区间写法, 闭区间用
[]
, 开区间用{}
- age: [1 TO 10], 意为 1<=age<=10
- age: [1 TO 10}, 意为 1<=age<10
- age: [1 TO ], 意为 1<= age
- age: [* TO 10], 意为 age <= 10
- 算数符号写法
- age: >=1
- age:(>=1 && <= 10) 或者 age:(+>=1+<=10)
- 区间写法, 闭区间用
- 通配符查询:
?
代表 1 个字符,*
代表多个字符- name:t?m
- name:tom*
- name:t*m
- 通配符匹配执行效率低, 且占用较多内存, 不建议使用
- 如无特殊要求, 不要将
?/*
放在最前面
- 正则表达式匹配
- name:/[mb]oat/
- 模糊匹配(fuzzy query)
- name:roam~1
- 匹配与 roam 差 1 个 character 的词, 比如 foam roams 等
- 近似度查询(proximity search)
- “for quick” ~5
- 以 term 为单位进行差异比较, 比如
quick fox
和quick brown fox
都会被匹配
2.4.2 实验:
给一个新索引中插入以下记录
1 | POST test_search_index/_bulk |
得到如下数据:
_id | username | job | age | birth | isMarried |
---|---|---|---|---|---|
1 | alfred way | java engineer | 18 | 1990-01-02 | false |
2 | alfred | java senior engineer and java specialist | 28 | 1980-05-07 | true |
3 | lee | java and ruby engineer | 22 | 1985-08-07 | false |
4 | alfred junior way | ruby engineer | 23 | 1989-08-07 | false |
q=alfred
查询任何字段值包含 alfred 的记录
1 | GET test_search_index/_search?q=alfred |
- type: DisjunctionMaxQuery
- description: es 实际的执行过程(或关系)
- username.keyword: alfred
- job: alfred
- username: alfred
- job.keyword: alfred
- 其他字段不是 string 类型, 不参与比较
q=username:alfred way
查询 username 列包含 alfred 或任意列包含 way 的记录
1 | GET test_search_index/_search?q=username:alfred way |
q=username:”alfred way”
查询 username 包含 “alfred way” 的所有记录
1 | GET test_search_index/_search?q=username:"alfred way" |
q=username:(alfred way)
1 | GET test_search_index/_search?q=username:(alfred way) |
q=username:alfred AND way
返回 username 包含 alfred, 且任意字段包含 way 的所有文档
1 | GET test_search_index/_search?q=username:alfred AND way |
q=username:(alfred AND way)
返回 username 同时包含 alfred 和 way 的所有文档
1 | GET test_search_index/_search?q=username:(alfred AND way) |
q=username:(alfred NOT way)
返回 username 包含 alfred 但不能包含 way 的所有文档
1 | GET test_search_index/_search?q=username:(alfred NOT way) |
q=username:(alfred +way)
返回 username 一定包含 way, 可以包含 alfred 的所有文档
但在 url 中+会被解析为空格, 按照上面的内容输入的话会被识别成空格, 需要转化为
%2B
1 | GET test_search_index/_search?q=username:(alfred +way) |
q=username:(alfred %2Bway)
这才是真正的+
1 | GET test_search_index/_search?q=username:(alfred %2Bway) |
q=username:/[a]?.*/
1 | GET test_search_index/_search?q=username:/[a]?.*/ |
2.4.3 Request Body Search
- 将查询语句通过 http request body 发送到 es, 主要包含以下参数:
1
2
3
4
5
6
7
8GET /my_index/_search
{
"query": {
"term": {
"user": "alfred"
}
}
}- query 符合 Query DSL 语法的查询语句
- from, size
- timeout
- sort
QueryDSL: 基于 JSON 定义的查询语言, 主要包含如下两种类型
- 字段类查询: 如 term, match, range 等, 只针对某个字段进行查询, 主要包含以下两类:
- 全文匹配: 针对 text 类型的字段进行全文检索, 会对查询语句先进行分词处理, 再拿分词结果和 es 中的倒排索引去匹配, 如 match, match_phrase 等 query 类型
- 单词匹配: 不会对查询语句做分词处理, 直接去匹配字段的倒排索引, 如 term, terms, range 等 query 类型
- 复合查询: 如 bool 查询等, 包含一个或多个字段类查询或者复合类查询
2.4.3.1 相关性算分
相关性算分(relevance)是指文档与查询语句间的相关度:
- 通过倒排索引可以获取与查询语句相匹配的文档列表, 那么如何将最符合用户查询需求的文档放在前列呢
- 本质是一个排序问题, 排序的依据是相关性算分
相关性算分的几个重要概念如下:
- TF(Term Frequency, 词频): 即单词再该文档中出现的次数, 词频越高, 相关度越大
- DF(Document Frequency, 文档频率): 即单词出现的文档数
- IDF(Inverse Document Frequency, 逆向文档频率): 与文档频率相反, 即单词出现的文档越少, 相关度越高
- Field-length Norm: 文档越短, 相关性越高
ES 目前主要有两个相关性算分模型:
TF/IDF 模型: Lucene 的经典模型, 计算公式如下:
$$
score(q,d)=coord(q,d)·queryNorm(q)·\sum_{t\ in\ q}(tf(t\ in\ d)·idf(t)^2·t.getBoost()·norm(t,d))
$$对分词后的每一个 term 计算相关性并求和, 从公式可以看到, 词频越高, 文档频率越小, 文档越短, 最终的相关性得分就会越高
- score: 相关性得分
- q: 查询语句
- d: 匹配的文档
- coord(q,d)/queryNorm(q): 正则化处理
- t in q: t 为查询语句分词后的单词
- tf: 词频计算
- idf: 逆向文档频率计算
- t.getBoost(): 是否做过特定加权
- norm: Field Length Norm 计算
BM25 模型(5.x 后默认的模型)
$$
score(D,Q)=\sum_{i=1}IDF(qi)·\frac{f(q_i,D)·(k_1+1)}{f(q_i,D)+k_1·(1-b+b·\frac{|D|}{avgd1})}
$$BM25 模型中的 BM 值 BaseMatch, 25 指迭代了 25 次才计算方法, 是针对 TF/IDF 的一个优化
- f(qi,D)值 qi(即查询语句分词后的单词) 在文档 D 中的 TF
- 相比 TF/IDF 的一大优化是降低了 tf 在过大时的权重
2.4.3.2 MatchQuery
对字段做全文检索, 是最基本和常用的查询类型, 默认情况下会将查询内容分词, 然后返回每个分词能够匹配到的文档
已知 username 的倒排索引为:
单词 | 文档 id 列表 |
---|---|
alfred | 1,2 |
way | 1 |
1 | GET test_search_index/_search |
通过 operator 参数可以控制单词间的匹配关系, 可选项为 or 和 and, 此时只会匹配 username 同时包含 alfred
和 way
的文档
1 | GET test_search_index/_search |
- 通过
minimun_should_match
参数可以控制需要匹配的单词数: 需要至少包含查询语句的 n 的单词
1 | GET test_search_index/_search |
2.4.3.3 Match Phrase Query
对字段检索有顺序要求:
1 | GET test_search_index/_search |
通过 slop 参数可以控制单词间的间隔
1
2
3
4
5
6
7
8
9
10
11GET test_search_index/_search
{
"query": {
"match_phrase": {
"job": {
"query": "java engineer",
"slop": 1 // 允许 java 和 engineer 之间有一个单词间隔
}
}
}
}
2.4.3.4 Query String Query
类似于 URISearch 中的 q 参数查询
1 | GET test_search_index/_search |
1 | GET test_search_index/_search |
2.4.3.5 Simple Query String Query
- 类似 Query String, 但是会忽略错误的查询语法, 并且仅支持部分查询语法
- 常用的逻辑符号如下, 不能使用 AND, OR, NOT 等关键词
-
+
: 代指 AND -
|
: 代指 OR -
-
: 代指 NOT
-
2.4.3.6 Term Query
将查询语句作为整个单词进行查询, 即不对查询语句做分词处理
1 | GET test_search_index/_search |
2.4.3.7 RangeQuery
范围查询主要针对数值和日期类型
转义符 | 翻译 | 对应功能 |
---|---|---|
gt | greater than | > |
gte | greater than or equal to | >= |
lt | less than | < |
lte | less than or equal to | <= |
1 | GET test_search_index/_search |
针对日期的查询:
1 | GET test_search_index/_search |
2.4.3.8 QueryDSL 符合查询
复合查询是指包含字段类查询或复合查询的类型, 主要包括以下几类:
constant_score query: 将其内部的查询结果文档得分都设定为 1 或者 boost 值, 多用于结合 bool 查询实现自定义得分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73GET test_search_index/_search
{
"query": {
"constant_score": {
"filter": {
"match": {
"username": "alfred"
}
},
"boost": 1.2
}
}
}
// resp
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 1.2,
"hits" : [
{
"_index" : "test_search_index",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.2,
"_source" : {
"username" : "alfred way",
"job" : "java engineer",
"age" : 18,
"birth" : "1990-01-02",
"isMarried" : false
}
},
{
"_index" : "test_search_index",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.2,
"_source" : {
"username" : "alfred",
"job" : "java senior engineer and java specialist",
"age" : 28,
"birth" : "1980-05-07",
"isMarried" : true
}
},
{
"_index" : "test_search_index",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.2,
"_source" : {
"username" : "alfred junior way",
"job" : "ruby engineer",
"age" : 23,
"birth" : "1989-08-07",
"isMarried" : false
}
}
]
}
}bool query: 一般由一个或多个 bool 字句构成, 主要包含如下 4 个:
类型 | 功能 |
---|---|
filter | 只过滤符合条件的文档, 不计算相关性得分, 做简单匹配查询且不考虑算分时, 推荐使用 filter 代替 query |
must | 文档必须符合 must 中的所有条件, 会影响相关性得分, 多个 must 条件筛选出的文档最终得分为多个查询的得分加和 |
must_not | 文档必须不符合 must_not 中的所有条件 |
should | 文档可以符合 should 中的条件, 会影响相关性得分 只包含 should 时, 文档必须满足一个条件( minimum_should_match 可以控制满足条件的个数或者百分比)同时包含 must 和 should 时, 文档不必满足 should 中的条件, 但如果满足会增加相关性得分 |
- dis_max query
- funcation_score query
- boosting query
当一个查询语句位于 Query 或者 Filter 上下文时, es 执行的结果会有不同:
- 使用时, 尽量将不印象算分的查询放在 filter 中, 如日期, 数字, 不分词的 string 等字段
上下文类型 | 执行类型 | 使用方式 |
---|---|---|
Query | 查找与查询语句最匹配的文档, 对所有文档进行相关性算分并排序 | query bool 中的 must 和 should |
Filter | 查找与查询语句相匹配的文档 | bool 中的 filter 与 must_not constant_score 中的 filter |
2.4.3.9 Count
1 | GET test_search_index/_count |
2.4.3.10 Source Filtering
过滤返回结果中的 _source
字段, 节省网络开销
1 | // url 参数 |
2.5 分布式特性
ES 支持集群模式, 是一个分布式系统, 有如下优势:
- 增大容量, 支持快速横向扩容, 使得 ES 集群可以支持 PB 级数据
- 提高系统可用性, 即使部分节点停止服务, 整个集群仍可以正常服务
ES 集群由多个 ES 示例组成
- 不同集群通过名称来区分, 可通过
cluster.name
设置, 默认为elasticsearch
- 每个 es 实例本质上是一个 JVM 进程, 且有自己的名字, 通过
node.name
进行修改
2.5.1 cerebro 的安装和运行
cerebro 是一个 es 运维工具, 可以方便的查看 elasticsearch 的集群, 节点信息, 如磁盘空间, JVM 状态, 节点状态等, 并且还支持类似 kibana 的 Rest 访问, 创建索引, 检查分词, 修改集群配置等功能
下载 docker 镜像
1 | $ docker pull lmenezes/cerebro |
编辑 docker 启动脚本, 加入 cerebro:
1 |
|
容器启动后, 就可以通过 http://localhost:9000 访问 cerebro, 输入连接 es 集群的地址后即可进入管理界面
需要注意的是, 在 docker 模式下, 使用
localhost:9200
可能无法识别, 需要保证的是在 cerebro 容器中能够访问到的 URL, 如宿主机的 IP
2.5.2 节点概念
1 | // 运行如下命令可以启动一个 elasticsearch 节点: |
es 集群的相关数据称为 cluster state
, 主要记录如下信息:
- 节点信息: 如节点名称, 连接地址等
- 索引信息: 如索引名称, 配置等
MasterNode(☆):
- 可以修改 cluster state 的节点被称为 master 节点, 一个集群只能有一个
- cluster state 存储在每个节点上, master 维护最新版本并同步给其他节点
- master 节点是通过集群中所有节点选举产生的, 可以被选举的节点成为
master-eligible
节点, 相关配置为node.master: true
CoordinatongNode(○):
- 处理请求的节点, 该节点为所有节点默认角色, 不能取消
- 路由请求到正确的节点处理, 比如创建索引的请求到 master 节点
DataNode(□):
- 存储数据的节点, 默认节点都是 data 类型, 相关配置如下:
node.data:true
2.5.3 副本与分片
服务可用性: 多个节点的情况下, 能够容忍其中小于半数的节点停止服务
数据可用性: 引入副本机制解决, 每个节点上都有完整的数据
ES 引入分片(shard)来解决数据在多个节点上分布的问题, 分片是 es 支持 PB 级数据的基石:
- 分片存储了部分数据, 可以分布于任意节点上
- 分片数在索引创建时指定, 且后续不允许再更改, 默认为 5 个
- 分片有主分片和副本分片之分, 以实现数据的 HA
- 副本分片的数据由主分片同步, 可以有多个, 从而提高读取的吞吐量
假设创建如下索引:
1 | PUT test_index |
意为该条索引由 3 个分片组成, 每个分片还有一个备份
node | 主分片 | 副分片 |
---|---|---|
node1 | 0 | 1 |
node2 | 2 | 0 |
node3 | 1 | 2 |
此时增加节点是否能提高 test_index 的数据容量?
不能, 该索引已经指定 3 个分片, 且已经分布在三个节点上, 新增节点无法被使用
此时增加副本数是否能提高 test_index 的读吞吐量?
不能, 因为新增的副本也是分布在这三个节点上, 还是利用了相同的资源, 如果要增加吞吐量, 需要新增节点
分片数设定很重要, 需要提前规划好
- 过小会导致后续无法通过增加节点实现水平扩容
- 过大会导致一个节点上分布过多分片, 造成资源浪费, 同时印象查询性能
2.5.4 集群状态
1 | GET _cluster/health |
通过接口可以查询集群健康状况, 包括如下三种:
- green: 健康状态, 所有主副分片都正常分配
- yellow: 所有主分片都正常分配, 但有副本分片未正常分配
- red: 有主分片未分配
健康情况不代表能否继续对外提供服务
2.5.5 故障转移
假设当前集群状态如下
node | 主分片 | 副分片 |
---|---|---|
node1(master) | P0 | R1 |
node2 | P2 | R0 |
node3 | P1 | R2 |
若此时 node1 所在的机器宕机导致服务终止:
- node2 和 node3 发现 node1 宕机后会发起 master 选举, 比如这里选择 node2 为 master 节点, 此时由于主分片 P0 下线, 集群状态变为
red
- node2 发现主分片 P0 未分配, 将 R0 提升为主分片, 此时由于所有主分片都正常分配, 集群状态变为
Yellow
- node2 为 P0 和 P1 生成新的副本, 集群状态变为
green
2.5.6 文档分布式存储
文档最终会存储在分片上
文档分片算法:
- 使得文档均匀分布在所有分片上, 以充分利用资源
- 算法要求:
- 可逆, 能够准确知道某个 id 的文档应该在哪个分片上
- shard = hash(
routing
) &number_of_primary_shards
- 保证将数据均匀的分布在多个分片中
-
routing
是一个关键参数, 默认是文档 id, 也可以自行指定 -
number_of_primary_shards
是主分片数
- 分片数一旦确定就不能再修改
文档的创建流程:
文档的读取流程:
脑裂问题:
- 假设此时 node1 的网络和其他两个节点割裂, 此时 node2 和 node3 会重新选举 master, 比如 node2 成为新 master, 此时会更新 cluster state
- node1 自己组成集群后, 也会更新 cluster state
- 原本同一个集群现在出现了两个 master, 且维护不同的 cluster state, 网络恢复后无法选择正确的 master
解决方案:
- 尽在集群节点下线不超过半数时才可进行 master 选举
- quorum = master-eligible / 2 + 1
- 设置
discory.zen.minimum_master_nodes
为quorum
即可避免脑裂
2.5.7 Shard
倒排索引不可变更:
- 优点:
- 不用考虑并发写文件的问题, 杜绝了枷锁带来的性能问题
- 由于文件不可更改, 可以充分利用文件系统缓存, 只需载入一次, 只要内存足够, 对该文件的读取都会从内存读取, 性能高
- 便于生存缓存
- 利于对文件进行压缩, 节省磁盘和内存存储空间
- 坏处:
- 需要写入新文档时, 必须重新构建倒排索引文件, 然后替换老文件后, 新文件才能被检索, 实时性差
因此 ES 做了折中: 新文档直接生成新的倒排索引文件, 查询时同时查询所有的倒排文件, 然后做结果汇总计算
Lucene 就采用了这种方案, 它构建的单个倒排索引成为
segment
,segment
的集合成为Index
, 与 ES 中的index
概念不同, ES 中的一个 shard 对应一个 Lucene IndexLucene 会有一个专门的文件来记录所有的
segment
信息 , 成为commit point
操作 | ES 的行为 |
---|---|
新增文档 | 批量将新增的文档生成新的 segment, 多个 segment 一起参与查询 |
删除文档 | Lucene 维护了一个 .del 的文件, 记录所有已删除的文档, 记录文档在 Lucene 内部的 id, 在查询结果返回前会过滤掉 .del 中的所有文档 |
修改文档 | 删除原有文档, 再创建新文档 |
ES 和 Lucene 概念对照
-
ES Index
是一个逻辑概念, 由多个 Shard 组成 - 每个 Shard 就是一个 Lucene 进程, 包含倒排索引(segment),
.del
, 以及Commit Point
2.5.7.1 Refresh:
- segment 写入磁盘的过程(fsync)依然很耗时, 可以借助文件系统缓存的特性, 先将 segment 在缓存中创建并开放查询来进一步提升实时性, 该过程在 ES 中被称为
refresh
- 在 refresh 的间隙创建的文档会先存储在一个 buffer 中, refresh 时将 buffer 中所有的文档清空并生成 segment
- ES 默认每秒执行一次 refresh, 因此文档的实时性被提高到 1 秒, 这也是 es 被称为近实时的原因
refresh 发生的时机主要有如下几种情况:
- 间隔时间到达时, 通过
index.settings.refresh_interval
来设定, 默认 1 秒 -
index.buffer
占满时, 大小通过indices.memory.index_buffer_size
设置, 默认为 jvm heap 的 10%, 所有 shard 共享 - flush 发生时也会执行 refresh
2.5.7.2 Translog:
- 如果内存中的 segment 还没有写入磁盘, 此时发生了宕机, 那么其中的文档就无法恢复了, 这就需要
translog
来解决 - 写入文档到 buffer 时, 同时将该操作写入 translog
- translog 文件会即时写入磁盘, 默认每个请求都会落盘, 也可以修改为 n 秒一次
index.translog.*
- ES 启动时会检查 translog 文件, 并从中恢复
- 类似于 MySQL 的 RedoLog
2.5.7.3 flush
flush 负责将内存中的 segment 写入磁盘:
- 将 translog 写入磁盘
- 将
index buffer
清空, 其中的文档生成一个新的 segment, 相当于一个 refresh 操作 - 执行 fsync 操作, 将内存中的 segment 写入磁盘
- 删除旧的 translog 文件
flush 发生的时机:
- 间隔时间到达, 通过
index.translog.flush_threshold_period
设置, 默认 30 分钟, 设置后无法修改 - translog 占满时, 大小通过
index.translog.flush_threshold_size
控制, 默认是 512MB, 每个 index 都有自己的 translog
2.5.7.4 Segment Merging
- 随着 segment 的增多, 用于一次查询的 segment 数增多, 查询速度会变慢
- ES 会定时在后台完成 segment merge 操作, 减少 segment 的数量
- 通过
force_merge api
可以手动强制做 segment merge 的操作
2.6 Search 的运行机制
Search 在执行时候实际分成两个步骤运作的:
- Query 阶段
- 收到 search 请求的节点会在每组主副分片中随机选择一组涵盖了所有数据的分片, 发送 search request
- 被选中的分片会分别执行查询并排序, 返回 from+size 个文档 id 和排序值
- 接收请求的节点将所有返回的数据汇总并完成排序
- Fetch 阶段: 根据 Query 阶段获取的文档 id 列表去对应的 shard 上获取文档详情
- 向相关分片发送
multi_get
请求 - 各个分片返回文档的详细数据
- 最终将结果返回给用户
- 向相关分片发送
相关性算分的问题:
- 相关性算分在 shard 之间是独立的, 因此同一个 Term 的 IDF 及其他相关值在不同的 shard 上是不同的, 文档的相关性算分和它所处的 shard 有关
- 在文档数量不多时, 会导致相关性算分严重不准的情况发生
- 可以使用
DFS Query-then-Fetch
: 指收到请求的节点拿到所有需要的文档后再重新完整的计算一次相关性算分, 耗费更多的 CPU 和内存, 执行性能也比较低下
2.7.1 排序
ES 默认会采用相关性算分, 用户可以通过设定 sorting 参数来自行设定排序规则
1 | // 先按照生日降序排列, 生日相同的按照相关性算分来排列, 最后按内部 id 排列 |
字符串的排序比较特殊, 因为 ES 有 text
和 keyword
两种类型:
- 针对 text 类型, 由于 ES 存储文档时直接做了分词, 因此对 text 字做排序时会直接报错 “Fielddata is disabled on text fields by default”
- 针对 keyword 类型, 可以返回预期结果
ES 的排序过程是对字段原始内容的排序过程, 此时倒排索引无法发挥作用, 需要使用正排索引, 即通过文档 id 和字段可以快速得到字段原始内容, ES 提供了两种实现方式:
- fielddata 默认禁用
- doc values 默认启用, 除了 text 类型, doc value 在实现时会使用列式存储, 并且引入了大量的压缩算法以节省存储空间
对比 | FieldData | DocValue |
---|---|---|
创建实际 | 搜索时即时创建 | 索引时创建, 与倒排索引创建时机一致 |
创建位置 | JVM Heaper | 磁盘 |
优点 | 不会占用额外的磁盘空间 | 不会占用 heap 空间 |
缺点 | 文档过多时, 即时创建会花费过多时间, 占用过多的 head | 降低索引的速度, 占用额外的磁盘空间 |
2.7.2 分页&遍历
ES 提供了 3 种方式来解决分页与遍历问题:
类型 | 场景 |
---|---|
From/Size | 需要实时获取顶部的部分文档, 且需要自由分页 |
Scroll | 需要全部文档, 如导入所有数据的功能 |
SearchAfter | 需要全部文档, 不需要自由翻页 |
2.7.2.1 from/size
from: 指明开始位置
size: 指明获取总数
深分页: 页数越深, 处理文档越多, 占用内存越多, 耗时越长, 应尽量避免. ES 通过
index.max_result_window
限定最多到 10000 条数据
2.7.2.2 scroll
以快照方式来避免深分页问题
不能用来做实时搜索, 因为数据不是实时的
尽量不要使用复杂的 sort 条件, 使用
_doc
最高效使用比较复杂
第一步需要发起一个 scroll search, ES 会在收到请求后根据查询条件创建文档 id 合集的快照
1
2
3
4
5
6
7
8
9GET test_search_index/_search?scroll=5m // 指定快照的有效期
{
"size": 1 // 指定每次 scroll 返回的文档数
}
// resp
{
"_scroll_id": "DXF1ZXWAE797BAAASDUOAWIUDWJKdjliawjdo=="
}第二步调用 scroll search 的 api, 获取文档集合, 不断迭代调用直到返回的
hits.hits
数组为空时停止1
2
3
4
5
6
7
8
9
10
11POST _search/scroll
{
"scroll": "5m",
"scroll_id": "DXF1ZXWAE797BAAASDUOAWIUDWJKdjliawjdo=="
}
// resp
{
"_scroll_id": "..."
""
}过多的 scroll 会占用大量内存, 可以通过 clear api 删除过多的 scroll 快照
2.7.2.3 search_after
- 避免深度分页, 提供实时取下一页文档的获取功能
- 缺点是不能使用 from 参数, 即不能指定页数
- 只能下一页, 不能上一页
- 使用简单:
- 第一步为正常搜索, 但要指定
sort
值, 并保证唯一 - 第二步为使用上一步最后一个文档的
sort
值进行查询
- 第一步为正常搜索, 但要指定
1 | GET test_search_index/_search |
SearchAfter 如何避免深度分页问题:
通过唯一排序值定位将每次要处理的文档数都控制在 size 内
2.7 聚合分析
聚合分析(Aggregation), 是 ES 除搜索外提供的针对数据统计分析的功能, 特点如下:
- 功能丰富, 提供
Bucket
,Metric
,Pipeline
等多种分析方式, 可以满足大部分的分析需求 - 实时性高, 所有的计算结果都是即时返回的
聚合分析作为 search
的一部分, api 如下所示:
1 | GET test_search_index/_search |
2.8 数据建模
数据建模(DataModeling), 创建数据模型的过程:
- 对现实世界进行抽象描述的一种工具和方法
- 通过抽象的实体及实体间联系的形式去描述业务规则, 从而实现对现实世界的映射
2.8.1 ES 中的数据建模
ES 是基于 Lucene, 以倒排索引为基础实现的存储体系, 不遵循关系型数据库中的范式约定
- 根据业务需求和数据需求创建概念和逻辑模型, 进而产出
实体列表
,实体属性描述
和实体间关系描述
- 再根据部署需求和性能需求产出物理模型, 即
ES IndexTemplate
和ES IndexMapping
配置
mapping 字段的相关设置:
字段 | 取值 | 功能 |
---|---|---|
enbaled | `true | false` |
index | `true | false` |
index_options | `docs | freqs |
norms | `true | false` |
doc_values | `true | false` |
field_data | `false | true` |
store | `false | true` |
crerce | `true | false` |
multifields多字段 | 灵活使用多字段特性来解决多样的业务需求 | |
dynamic | `true | false |
date_delection | `true | false` |
mapping 字段属性的特定流程:
- 是什么类型
- string: 需要分词则设定为 text 类型, 否则为 keyword 类型
- enum: 基于性能考虑将其设定为 keyword 类型
- 数值类型: 尽量选择贴近的类型
- 其他类型: bool, date, 地理位置等
- 是否需要检索
- 完全不需要检索, 排序和聚合分析:
enabled = false
- 不需要检索的字段:
index=false
- 需要检索, 可以通过如下配置设定需要的存储粒度:
-
index_options
结合需要设定 -
norms
: 不需要归一化数据时关闭即可
-
- 完全不需要检索, 排序和聚合分析:
- 是否需要排序和聚合分析
- 不需要排序或聚合分析功能:
doc_values = false, fielddata = false
- 不需要排序或聚合分析功能:
- 是否需要另行存储
- 存储该字段原始内容:
store = true
- 一般结合
_source
的 enable 设定为 false 时使用
- 存储该字段原始内容:
2.8.2 示例
假设有如下场景需要使用 ES 进行数据建模:
实体: 博客文章
字段如下:
- 标题 title
- 发布日期 publish_date
- 作者 author
- 摘要 abstract
- 内容 content
- 网络地址 url
mapping 设置如下:
1 | PUT blog_index |
2.8.3 关联关系处理
引入评论实体, 字段如下:
- 文章 id: blog_id
- 评论人: username
- 评论日期: date
- 内容: content
ES 并不擅长处理关系型数据库中的关联关系, 比如文章表 blog 与评论表 comment 之间通过 blog_id
关联, 在 ES 中可以通过如下两种手段变相解决:
- NestedObject
- Parent/Child
2.8.3.1 NestedObject
1 | { |
此时查询方式如下:
1 | GET blog_index/_search |
Comments 默认是 Object Array
, 存储结构类似下面的形式, 将所有元素按照字段聚合起来
1 | { |
解决这个问题需要将 blog 的 mapping 设置为 nested 形式:
1 | PUT blog_index |
此时查询方式也需要改变:
1 | GET blog_index/_search |
2.8.3.2 Parent/Child
ES 还提供了类似关系型数据库中 join 的实现方式
1 | PUT blog_index_parent_child |