前言
Elasticsearch
是一个底层基于Lucene
的全文搜索和分析引擎,支持近乎实时地存储、搜索和分析大量数据的能力,最常用于网站搜索、日志搜索、数据分析等场景。
本文主要针对日常工作中Elasticsearch
使用的一些基础概念、使用规范、注意事项、常见优化以及工具使用进行总结,如有不当的地方,欢迎指正。
一、Elasticsearch建索引规范
索引建立时需搞清楚每个字段存在的用途(这里用途不仅仅是业务上的定义,还需关心该字段是会做索引,还是会聚合计算,还是会有排序,或者仅仅只是文档),在建立mapping
时应当根据字段的不同用途,不同数据类型来匹配合适的Elasticsearch
中的数据类型。
搞清楚中Elasticsearch中的数据类型
1. string类型
Keyword
和Text
都属于string
类的基本数据类型,但使用场景完全不同。
Keyword
与Text
差异对比
Keyword
(不分词):如果文本上有精确搜索、排序、聚合查询的需求时,可以使用,大文本用不要用keyword
类型。
Text
(分词):按分词器进行分词,用于全文检索,不能排序,也禁止用来聚合。
以下测试案例,具体说明了两种类型分别在match
和term
查询时反映出来的差异性。
# 建立名为emp的索引
PUT /emp/
{
"mappings": {
"_doc": {
"properties": {
"nickname": {
"type": "text"
},
"name": {
"type": "keyword"
},
"age": {
"type": "integer"
}
}
}
}
}
# 插入测试数据
POST /emp/_doc/_bulk
{"index":{"_id":"1"}}
{"nickname":"zhang san","name":"zhang san","age":"28"}
{"index":{"_id":"2"}}
{"nickname":"zhang san feng","name":"zhang san feng","age":"88"}
{"index":{"_id":"3"}}
{"nickname":"zhang san bu feng","name":"zhang san bu feng","age":"8"}
# 3条全部能够查到
GET /emp/_search
{"query":{"match":{"nickname":"zhang san"}}}
# 3条全都查不到
GET /emp/_search
{"query":{"term":{"nickname":"zhang san"}}}
# 换成查name,match和term都只能查到id为1的这条数据
GET /emp/_search
{"query":{"match":{"name":"zhang san"}}}
GET /emp/_search
{"query":{"term":{"name":"zhang san"}}}
# match和term都是3条全部能够查到
GET /emp/_search
{"query":{"match":{"nickname":"zhang"}}}
GET /emp/_search
{"query":{"term":{"nickname":"zhang"}}}
# match和term都是一条也查不到
GET /emp/_search
{"query":{"match":{"name":"zhang"}}}
GET /emp/_search
{"query":{"term":{"name":"zhang"}}}
# 可以排序
GET /emp/_search
{"sort":[{"name":{"order":"desc"}}]}
# 报错
GET /emp/_search
{"sort":[{"nickname":{"order":"desc"}}]}
2. 数值类型
数组类型包括:long, integer, short, byte, double, float, half_float, scaled_float
请按实际需求选择,因为这不仅仅是能够节省空间,同时索引和搜索都将变的更有效率,不过Elasticsearch
会根据存储的实际情况进行优化。
以下案例具体说明了,当实际存储类型与数据类型精度不一致时,所导致的“怪异”现象,主要原因就是因为Elasticsearch会根据存储的实际情况进行优化。
# 注意age字段指定的是integer类型
PUT /emp/
{
"mappings": {
"_doc": {
"properties": {
"nickname": {
"type": "text"
},
"name": {
"type": "keyword"
},
"age": {
"type": "integer"
}
}
}
}
}
# 插入的测试数据,age字段实际上保存的都是字符串,但完全不影响插入
POST /emp/_doc/_bulk
{"index":{"_id":"1"}}
{"nickname":"zhang san","name":"zhang san","age":"28"}
{"index":{"_id":"2"}}
{"nickname":"zhang san feng","name":"zhang san feng","age":"88"}
{"index":{"_id":"3"}}
{"nickname":"zhang san bu feng","name":"zhang san bu feng","age":"8"}
# 同样,保存小数也没问题
POST /emp/_doc/_bulk
{"index":{"_id":"6"}}
{"nickname":"li gang","name":"li gang","age":16.9}
不过问题会出在查询时,直接查16.9
是查不到的。
# 查不出结果
GET /emp/_search
{"query":{"match":{"age":16.9}}}
而查16
却可以查到结果。
# 能查出结果
GET /emp/_search
{"query":{"match":{"age":16}}}
当然如果改为使用double
类型存储小数后,则可正常查询,以下是测试案例:
# 新建一个double类型的字段
PUT /emp_double/
{"mappings":{"_doc":{"properties":{"salary":{"type":"double"}}}}}
# 构建数据
POST /emp_double/_doc/_bulk
{"index":{"_id":"1"}}
{"salary":1000.5}
{"index":{"_id":"2"}}
{"salary":2000.25}
{"index":{"_id":"3"}}
{"salary":3000}
{"index":{"_id":"4"}}
{"salary":4000.1234}
# 4条数据查1000.5,2000.25,3000,4000.1234都没问题
GET /emp_double/_search
{"query":{"match":{"salary":4000.1234}}}
所以,实际生产环境不要存储比字段类型精度更高的数据。
当然数值类型也是支持聚合和排序的。同样,注意字段类型与实际存储类型产生的差异即可。
并没有按照实际存储的数值进行排序,因为实际精度并没有保留到小数。
这里还需要注意的是,在Elasticsearch
中存储时并不是所有数值数据都一定要被映射为数值类型的,通常情况是当数值需要进行范围查询时,则建议使用数值类型,而术语级别的查询,则使用keyword
更合适。
例如像ID、编号等这样的数据通常不会被用来做范围查询,而是经常用来做术语查询,因此应当建立为keyword
类型,尽管它本身是一串数字。
3. 日期类型
Elasticsearch
中日期类型既可以是格式化后的字符串,也可以是时间戳,Elasticsearch
内部统一会将其转换为UTC,并按照long number
类型进行存储。
多种时间格式的测试案例:
POST /my_date/_doc/_bulk
{"index":{"_id":"1"}}
{"date":"2015-01-01"}
{"index":{"_id":"2"}}
{"date":"2015-01-01T12:10:30Z"}
{"index":{"_id":"3"}}
{"date":1420070400001}
GET my_date/_search
{"sort":{"date":"desc"}}
也支持指定的日期格式:
PUT my_date
{
"mappings": {
"_doc": {
"properties": {
"date": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
}
}
}
}
}
# 此时插入第3条时间戳的格式将会报错
POST /my_date/_doc/_bulk
{"index":{"_id":"1"}}
{"date":"2015-01-01"}
{"index":{"_id":"2"}}
{"date":"2015-01-01 12:10:30"}
{"index":{"_id":"3"}}
{"date":1420070400001}
4. boolean类型
boolean
类型只能是true
和false
这两种,但不区分字符串,意思就是传入true
和"true"
都可以,没有区别。
POST /my_boolean/_doc/_bulk
{"index":{"_id":"1"}}
{"is_published":"true"}
{"index":{"_id":"2"}}
{"is_published":true}
{"index":{"_id":"3"}}
{"is_published":false}
# 查询出两条记录
GET my_boolean/_search
{"query":{"term":{"is_published":"true"}}}
# 聚合true两条,false一条
GET my_boolean/_search
{"aggs":{"published_count":{"terms":{"field":"is_published"}}}}
规范建议
1. fielddata尽量不使用
PUT /emp/
{
"mappings": {
"_doc": {
"properties": {
"nickname": {
"type": "text"
},
"name": {
"type": "keyword"
},
"age": {
"type": "integer"
}
}
}
}
}
如果对text
类型进行排序,得到的报错信息如下,注意有提到Fielddata
默认是关闭的,可以为nickname
设置fielddata=true
这样的属性。
GET /emp/_search
{"sort":[{"nickname":{"order":"desc"}}]}
# 报错信息如下:
{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [nickname] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."
}
],
"type": "search_phase_execution_exception",
"reason": "all shards failed",
"phase": "query",
"grouped": true,
"failed_shards": [
{
"shard": 0,
"index": "emp",
"node": "CKN5Zo86QTmwjnK7NHHNQQ",
"reason": {
"type": "illegal_argument_exception",
"reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [nickname] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."
}
}
],
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [nickname] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.",
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [nickname] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."
}
}
},
"status": 400
}
按照如下方式在构建text
类型时,设置fielddata=true
即可进行排序。
PUT /emp_fielddata/
{
"mappings": {
"_doc": {
"properties": {
"nickname": {
"type": "text",
"fielddata": true
}
}
}
}
}
POST /emp_fielddata/_doc/_bulk
{"index":{"_id":"1"}}
{"nickname":"zhang san"}
{"index":{"_id":"2"}}
{"nickname":"zhang san feng"}
{"index":{"_id":"3"}}
{"nickname":"zhang san bu feng"}
GET /emp_fielddata/_search
{"sort":[{"nickname":{"order":"desc"}}]}
关于fielddata
的使用,如官方介绍,它会大量消耗堆内存,并且驻留在内存中的生命周期是跟随segment
的,所以生产上应当尽量避免使用。
官方介绍
替代方案
解决方案也很简单了,在一开始的报错信息中已经说的很清楚了,可以用keyword
来代替。
Fielddata is disabled on text fields by default. Set fielddata=true on [nickname] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.
当然keyword
不会分词,不过为什么一个字段既要分词又要排序或聚合?当你仔细思考使用场景后你会发现这样做通常是没有意义的。
2. 索引与评分
- 不需要索引的字段,
index
属性设置为false
。 - 如果不需要计算文档评分,建议将
norms
设置为false
。
根据字段的实际使用情况,确定是否需要索引、文档评分,这样将会减少磁盘存储(每条文档中每个开启了norms
的字段需占用1
个字节),大多数字段类型默认是开启的。
PUT my_index/_mapping/_doc
{
"properties": {
"title": {
"type": "text",
"norms": false
}
}
}
3. source存储
source
是用来存储原始数据的,默认情况下都是存储的,但如果文本本身比较大,确实会消耗一定的存储资源,如果该文本字段本身不常被用来展示,可以考虑不进行存储,偶尔需要展示时,可以通过id
再去mysql
或其他数据库查出来。
注意,虽然不存储source
了,但索引信息还是正常存储的,因此不影响检索,请参考以下的示例:
# remark信息不存储到_source中
PUT my_source
{"mappings":{"_doc":{"_source":{"excludes":["remark"]},"properties":{"remark":{"type":"text"}}}}}
POST /my_source/_doc/_bulk
{"index":{"_id":"1"}}
{"remark":"这是一段用于测试source作用的文本"}
# 可以搜索到结果,但没有_source信息
GET my_source/_search
{"query":{"match":{"remark":"本"}}}
# 搜索结果信息
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 0.2876821,
"hits" : [
{
"_index" : "my_source",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.2876821,
"_source" : { }
}
]
}
}
4. doc_values
doc_values
与fielddata
作用是类似的,都是用来做正排索引的,只不过doc_values
是按列的方式构建与存储,且都是在磁盘上完成的,而fielddata
是从磁盘中构建之后再存储到JVM
堆内存中。
关于倒排索引和正排索引
倒排索引:解决的是找到那些包含某个特定术语的文档。
正排索引:解决的是该字段所在的文档具体值是什么。
因此一旦涉及到类似聚合、排序这样的需求时,再使用倒排索引就不太合适了,比如排序场景,如果是用倒排索引就需要遍历所有索引,然后提取出对应的文档,再进行去重、排序。
了解了这个背景以后,我们就知道了为什么Elasticsearch
默认会对除text
以外的所有数据类型默认都开启了doc_values
(text
因为本身会被分词,所以没法按列的方式构建)。
以下案例说明了当doc_values
关闭时,无法被排序的情况。
# age字段关闭了doc_values
PUT /emp_doc_values/
{"mappings":{"_doc":{"properties":{"name":{"type":"keyword"},"age":{"type":"integer","doc_values":false},"nick_name":{"type":"text"}}}}}
POST /emp_doc_values/_doc/_bulk
{"index":{"_id":"1"}}
{"nickname":"zhang san","name":"zhang san","age":28}
{"index":{"_id":"2"}}
{"nickname":"zhang san feng","name":"zhang san feng","age":88}
{"index":{"_id":"3"}}
{"nickname":"zhang san bu feng","name":"zhang san bu feng","age":8}
# name可排序
GET /emp_doc_values/_search
{"sort":[{"name":{"order":"desc"}}]}
# age字段由于关闭了doc_values,因此无法排序
GET /emp_doc_values/_search
{"sort":[{"age":{"order":"desc"}}]}
# nickname本身就是text字段,因此也无法排序
GET /emp_doc_values/_search
{"sort":[{"nickname":{"order":"desc"}}]}
基于上述结果可得,如果当前字段没有排序、聚合、脚本操作的需求,可以考虑关闭doc_values
,节省磁盘存储空间。
二、优化建议
1. index.refresh_interval
此参数用于控制文档从被写入到可被搜索的处理时间,默认频率为每秒执行一次(仅针对在过去index.search.idle.after
秒内至少接收到一次搜索请求的索引),也就是说数据至少要在写入1s
以后才能被搜索到,如果索引搜索请求不高,可以不调整,如果索引请求频率较高,则建议适当延长这个时间以减少集群压力(建议调到30s
)。
2. Use bulk requests
几乎所有涉及到I/O
的操作,一定都有批量的能力,而且批量的能力要远远大于单条逐一执行,至于生产上实际一批设置多大,这得根据实际情况测试,具体使用方式在上面的一些案例中都有用到。
3. id采用自动生成
如果ID
非自动生成,那么Elasticsearch
就需要检查同一分片内是否已经存在具有相同ID
的文档,这是一个成本较高的操作,并且随着索引的增长,这个操作的成本会变得更高。通过使用自动生成的ID
,Elasticsearch
可以跳过这个检查,这样索引速度会更快。
4. 按需查询
通过_source指定返回实际需要的字段,减少网络传输,提升查询效率。
以下查询展示了如何只返回nickname
字段信息的方式。
GET /emp/_search
{
"_source": ["nickname"],
"query": {
"match": {
"nickname": "zhang"
}
}
}
5. 深度分页
为什么会有深度分页的问题?应该说所有分布式存储的架构都会涉及到这个问题,假设我的数据均匀存储在5
个分片中,我现在要按照每页10
条,请求第1000
页的数据,即:1001~1010
,所以5
个分片都必须现在查出前1010
条结果,然后全部汇总到协调节点中,协调节点再对5050
条数据进行排序,取出前10
条,丢弃5040
条。
所以,如果是第2000
页,第5000
页呢?页数越深,协调节点一次性要处理的数据就越多(分片数 * (第几页 + 每页条数)
),这就是深度分页的问题所在。
为此Elasticsearch
还特意做了保护,通过index.max_result_window
参数来约束页数的大小,默认是10000
。
解决方式
Elasticsearch
提供了两种解决方式,一种是scroll api
,一种是search_after
,scroll api
本身也存在一些弊端,建议直接使用search_after
,它是根据一个游标位来处理,通过上一页的结果来帮助检索下一页。
以下案例说明了如何使用search_after
实现分页查询:
普通的分页查询,得到前两条记录分别为id为:2和1
的数据。
使用search_after
时,是根据上一页最后一条数据中的信息辅助完成,比如上一页最后一条数据的age
值为28
,所以下一页的查询方式如下图所示:
需要注意一个问题,如果排序的值不是唯一的,则有可能会出现问题,比如有两条数据年龄都是16
(之前的演示案例,age
是integer
类型,实际上索引记录的是16
)
把每页条数改为1
,查age为17
的下一条,查出是id为5
的记录。
再查age为16
的下一条,查出的是id为13
的记录,很明显id为4
的被跳过了。
因此必须带上一个唯一值才行,比如带上id
一起排序就可以了。
6. 通配符
关于通配符的使用,官方文档中有说明,由于需要遍历多个分词,所以查询可能会很慢,同时更也不应该以通配符*或?
开头。
实际生产中,不建议使用。
7. 使用filter而不是query
直接使用query
或者bool must/should
都会对匹配出的文档进行打分,而filter
并不会,且filter
带有缓存,特定场景中查询性能会更好。
以下案例展示了不带评分查询的方式
# 不带评分查询
GET /emp/_search
{
"query": {
"bool": {
"filter": {
"term": {
"nickname": "wang"
}
}
}
}
}
# 带评分查询
GET /emp/_search
{
"query": {
"term": {
"nickname": "wang"
}
}
}
# 效果与直接使用query term一样
GET /emp/_search
{
"query": {
"bool": {
"must": {
"term": {
"nickname": "wang"
}
}
}
}
}
不带评分的filter
查询结果展示
带评分的must
查询结果展示
8. 避免嵌套查询,尤其是嵌套聚合
Elasticsearch
的聚合查询,也都是将数据加载到堆内存中进行处理,嵌套越多,堆内存消耗也就越大。
# 双层嵌套聚合查询
GET emp/_search
{
"aggs": {
"age": {
"terms": {
"field": "age"
},
"aggs": {
"name_count": {
"terms": {
"field": "name"
}
}
}
}
},
"size": 0
}
生产上使用时需注意。
9. 避免在高基数上进行聚合
高基数聚合会提取大量存储桶,占用大量堆内存。
例如像uid
这样唯一性非常高的字段,就不建议对其进行聚合。
三、优化小技巧
1. 只需要聚合结果,不需要具体文档
指定size为0
,只返回聚合信息,不返回具体文档,减少数据提取损耗,且当size为0
时,Elasticsearch
还会对结果进行缓存。
以下案例演示了实际查询效果:
GET my_boolean/_search
{
"aggs": {
"published_count": {
"terms": {
"field": "is_published"
}
}
},
"size": 0
}
# 返回结果,不包含具体文档信息
{
"took" : 160,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 3,
"max_score" : 0.0,
"hits" : [ ]
},
"aggregations" : {
"published_count" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 1,
"key_as_string" : "true",
"doc_count" : 2
},
{
"key" : 0,
"key_as_string" : "false",
"doc_count" : 1
}
]
}
}
}
2. 多字段合并搜索
查询字段越多,搜索速度越慢,可以通过将经常同时搜索的多字段合并到一个字段中,然后每次只搜索这个合并后的字段即可,Elasticsearch
提供了在mappings
创建时使用copy_to
的方式。
PUT movies
{
"mappings": {
"properties": {
"name_and_plot": {
"type": "text"
},
"name": {
"type": "text",
"copy_to": "name_and_plot"
},
"plot": {
"type": "text",
"copy_to": "name_and_plot"
}
}
}
}
3. 数据预处理
根据实际的查询需求,预先就建立好相关的索引,最简单的例子如根据范围聚合的情况。
如下案例说明了当需要根据age
字段进行范围统计时,一般的方式是通过聚合查询提供的ranges
能力来实现。
# 新建演示索引
PUT /emp_range/
{
"mappings": {
"_doc": {
"properties": {
"name": {
"type": "keyword"
},
"age": {
"type": "integer"
}
}
}
}
}
# 批量插入11条数据
POST /emp_range/_doc/_bulk
{"index":{"_id":"1"}}
{"name":"a","age":1}
{"index":{"_id":"2"}}
{"name":"b","age":5}
{"index":{"_id":"3"}}
{"name":"c","age":10}
{"index":{"_id":"4"}}
{"name":"d","age":15}
{"index":{"_id":"5"}}
{"name":"e","age":20}
{"index":{"_id":"6"}}
{"name":"f","age":25}
{"index":{"_id":"7"}}
{"name":"g","age":30}
{"index":{"_id":"8"}}
{"name":"h","age":35}
{"index":{"_id":"9"}}
{"name":"i","age":40}
{"index":{"_id":"10"}}
{"name":"j","age":45}
{"index":{"_id":"11"}}
{"name":"k","age":50}
# 三段年龄范围的统计查询
GET emp_range/_search
{
"aggs": {
"age_ranges": {
"range": {
"field": "age",
"ranges": [
{ "to": 10 },
{ "from": 10, "to": 30 },
{ "from": 30 }
]
}
}
}
}
为了加快聚合的速度,在文档记录的同时就将该条文档所归属的范围确定下来,之后便可直接根据terms
进行聚合,这样比ranges
会快很多。
# 新建演示索引
PUT /emp_pre_range/
{
"mappings": {
"_doc": {
"properties": {
"name": {
"type": "keyword"
},
"age": {
"type": "integer"
},
"age_range": {
"type": "keyword"
}
}
}
}
}
# 数据插入时,写下age_range
POST /emp_pre_range/_doc/_bulk
{"index":{"_id":"1"}}
{"name":"a","age":1,"age_range":"*-10"}
{"index":{"_id":"2"}}
{"name":"b","age":5,"age_range":"*-10"}
{"index":{"_id":"3"}}
{"name":"c","age":10,"age_range":"10-30"}
{"index":{"_id":"4"}}
{"name":"d","age":15,"age_range":"10-30"}
{"index":{"_id":"5"}}
{"name":"e","age":20,"age_range":"10-30"}
{"index":{"_id":"6"}}
{"name":"f","age":25,"age_range":"10-30"}
{"index":{"_id":"7"}}
{"name":"g","age":30,"age_range":"30-50"}
{"index":{"_id":"8"}}
{"name":"h","age":35,"age_range":"30-50"}
{"index":{"_id":"9"}}
{"name":"i","age":40,"age_range":"30-50"}
{"index":{"_id":"10"}}
{"name":"j","age":45,"age_range":"30-50"}
{"index":{"_id":"11"}}
{"name":"k","age":50,"age_range":"30-50"}
# 直接根据terms聚合
GET emp_pre_range/_search
{
"aggs": {
"age_ranges": {
"terms": {
"field": "age_range"
}
}
}
}
4. 日期四舍五入范围查询
有时会查询过去某个时间点开始,一直到当前时间点结束这段时间范围内的数据,一般查询方式可能是如下这样:
PUT my_date
{
"mappings": {
"_doc": {
"properties": {
"date": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss Z"
}
}
}
}
}
POST /my_date/_doc/_bulk
{"index":{"_id":"1"}}
{"date":"2000-01-01 20:00:30 +0800"}
{"index":{"_id":"2"}}
{"date":"2000-01-01 20:00:50 +0800"}
{"index":{"_id":"3"}}
{"date":"2000-01-01 19:00:30 +0800"}
{"index":{"_id":"4"}}
{"date":"2000-01-01 19:00:50 +0800"}
# 假设当前时间是:2000-01-01 20:00:40
# 则如下查询条件相当于:2000-01-01 19:00:40 ~ 2000-01-01 20:00:40
# 因此可以命中文档id为1和4的数据
GET my_date/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"date": {
"gte": "now-1h",
"lte": "now"
}
}
}
}
}
}
该案例查询方式的问题在于,从2000-01-01 20:00:40
开始,如果每一秒都有查询,一直到2000-01-01 20:00:49
秒结束,查询命中的结果是一样的,但因此查询条件一直在变化,并不能命中结果缓存。
在实际应用场景中,许多场景对时间其实并没有那么严格的要求,比如并不用精确到秒级的时间,可能分钟级、甚至小时级。
所以,如果场景允许,通常可以通过四舍五入的方式来查询,这样便可命中缓存,提升查询效率。
如下案例,解释了具体的查询方式。
# 这是到分钟级别的四舍五入,假设当前时间是:2000-01-01 20:00:40
# 则实际查询情况相当于:2000-01-01 19:00:00.000 ~ 2000-01-01 20:00:59.999
# 所以文档id为1,2,3,4这四条数据都可以命中
GET my_date/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"date": {
"gte": "now-1h/m",
"lte": "now/m"
}
}
}
}
}
}
# 这是按小时四舍五入的
GET my_date/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"date": {
"gte": "now-1h/h",
"lte": "now/h"
}
}
}
}
}
}
# 这是按天四舍五入的
GET my_date/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"date": {
"gte": "now-1h/d",
"lte": "now/d"
}
}
}
}
}
}
5. composite(多桶聚合)
通过多字段聚合的时候,会使用嵌套的方式来实现,如下面案例所示:
PUT /composite_agg/
{
"mappings": {
"_doc": {
"properties": {
"salary": {
"type": "double"
},
"dept": {
"type": "keyword"
},
"sex": {
"type": "keyword"
}
}
}
}
}
POST /composite_agg/_doc/_bulk
{"index":{"_id":"1"}}
{"salary":2000, "dept":"a","sex":"m"}
{"index":{"_id":"2"}}
{"salary":4000, "dept":"a","sex":"m"}
{"index":{"_id":"3"}}
{"salary":5000, "dept":"a","sex":"m"}
{"index":{"_id":"4"}}
{"salary":3000, "dept":"a","sex":"f"}
{"index":{"_id":"5"}}
{"salary":3500, "dept":"b","sex":"m"}
{"index":{"_id":"6"}}
{"salary":6000, "dept":"b","sex":"m"}
{"index":{"_id":"7"}}
{"salary":3000, "dept":"b","sex":"f"}
{"index":{"_id":"8"}}
{"salary":3500, "dept":"b","sex":"f"}
{"index":{"_id":"9"}}
{"salary":6000, "dept":"b","sex":"f"}
# 嵌套聚合方式
GET /composite_agg/_search
{
"size": 0,
"aggs": {
"dept": {
"terms": {
"field": "dept"
},
"aggs": {
"sex": {
"terms": {
"field": "sex"
}
}
}
}
}
}
composite
也提供了同样的能力,如下所示:
# 使用composite方式来实现
GET /composite_agg/_search
{
"size": 0,
"aggs": {
"dept_sex": {
"composite": {
"sources": [
{
"dept": {
"terms": {
"field": "dept"
}
}
},
{
"sex": {
"terms": {
"field": "sex"
}
}
}
]
}
}
}
}
除此之外,与嵌套聚合不同的是,composite
还提供了一种类似分页滚动(scroll
)的方式来输出文档内容,如下案例所示:
前一页结果集返回的after_key
,在下一次请求时再作为after
入参带入查询。
GET /composite_agg/_search
{
"size": 0,
"aggs": {
"dept_sex": {
"composite": {
"size": 2,
"sources": [
{
"dept": {
"terms": {
"field": "dept",
"order": "desc"
}
}
},
{
"sex": {
"terms": {
"field": "sex",
"order": "desc"
}
}
}
],
"after": {
"dept": "a",
"sex": "f"
}
}
}
}
}
这个在聚合出大量数据集时,scroll
的方式可能会更稳定。
四、基础架构评估
下图体现了Elasticsearch
整体架构的概要图,Elasticsearch
客户端侧部署了大量不同的节点类型,而数据节点中看到有index/shard/segment/replica
这些组件。
Elasticsearch的节点类型
1. master-eligible node
每个节点启动后,默认就是一个master-eligible
节点。
master-eligible
节点可以参与选主,成为master
节点,只有master
节点才能修改集群中的状态信息,它可以管理整个集群的设置及变化,包括:index
的创建,更新,删除;添加或删除node
;为node
分配 shard
。
master
节点的稳定性对整个Elasticsearch
的集群运行状态至关重要,又因为索引和搜索数据本身会对CPU、内存、I/O都带来较大消耗,所以也是建议要将master-eligible
节点与data
节点区分开来使用。
同样的道理,最后也不要让主节点来做协调节点的事情,总之,要尽量让该节点少做事情,保证其稳定性。
按照如下设置,即可创建一个专用的主节点:
node.master: true
node.data: false
node.ingest: false
cluster.remote.connect: false
2. data node
顾名思义,就是用来处理数据的节点,包含索引、文档,数据节点所负责的工作都是非常消耗CPU、内存、I/O的,应当重点关注对这些资源的使用情况,并在必要对其进行扩充。
按照如下设置,即可创建一个专用的数据节点:
node.master: false
node.data: true
node.ingest: false
cluster.remote.connect: false
3. coordination node
coordination
节点主要是用来路由并汇聚数据的,有些时候可能会请求涉及到保存在不同数据节点的数据,这时就需要有一个节点专门来负责先将请求分发到各个数据节点,并等待每个数据节点执行完成后,将结果返回给该节点,再由该节点进行汇总后返回给客户端。
所以,很明显,对于这样的节点应当对CPU和内存都有较高的要求,尤其是内存,由于需要一次性汇总所有数据节点返回的数据,因此使用量将会非常高。
实际上,默认情况下每个节点都是coordination node
,并且不能设置,那如何设置一个专用的coordination node
呢?
按照如下设置,即可创建一个专用的协调节点:
node.master: false
node.data: false
node.ingest: false
cluster.remote.connect: false
协调节点是否有必要独立为专用节点,这个可根据实际情况来决定,过多的协调节点,可能会加重整个集群的负担。
哪些情况下考虑添加协调节点?
- 聚合查询:聚合查询可能会占用较多资源,让协调节点负责将结果进行合并汇总,将会减轻数据节点的压力。
- 高QPS时:协调节点可以进行负载均衡,处理请求分发与响应的过程,减少数据节点的压力。
4. ingest node
数据接入节点,默认情况下每个节点也都是ingest node
,它主要用来在对文档进行索引之前做预处理,比如:pipepline
,这个节点一般到不会设置为专用。
按照如下设置,即可创建一个专用的数据接入节点:
node.master: false
node.data: false
node.ingest: true
cluster.remote.connect: false
5. machine learning node
专门负责跑机器学习任务的节点,实际使用场景不多。
按照如下设置,即可创建一个专用的机器学习节点:
node.master: false
node.data: false
node.ingest: false
cluster.remote.connect: false
node.ml: true
xpack.ml.enabled: true
关于分片
分片是Elasticsearch
数据分布存储的单位,每个索引都由一个或多个分片组成,每个分片又是Lucene
索引的一个实例,可以独立完成搜索工作,因此让数据分布在多分片上也提升了Elasticsearch
并行处理的能力,同时,分片也承担着高可用的作用。
1. 分片数量多少合适?
默认情况下,Elasticsearch 7.x
以下版本默认一个索引创建5
个主分片,每个主分片默认1
个副本分片,Elasticsearch 7.x
及以上版本默认一个索引创建1
个主分片以及1
个副本分片。
从这个调整上也能看出分片数量并不是越多越好,这主要体现在以下三点:
- 分片过多,数据就会比较分散,那么可能本来一个分片就能处理的事情,现在要交给多个分片来处理,自然会降低整体的吞吐量。
- 同理,如果每个分片上只有少量的数据,则在计算文档相关性上也会表现的比较差。
- 分片过多,一次涉及到较多分片的请求则也会造成查询线程的大量消耗,可能导致大量排队或
rejected
情况产生。
副本分片数量建议
副本在一定程度上是可以帮助提升吞吐量的,比如有一个由三个节点组成的Elasticsearch
集群,此时如果新建了一个只有一个主分片的索引,那么应该为这个主分片配多少个副本分片呢?答案是:2个
,因为这样总计就是3个
分片,每个节点各一个,便能充分的将三个节点利用起来。
按照这个思路,如果建了一个有三个主分片的索引,那么应该配多少个副本分片呢?如果单从性能上考虑,答案就是:0个
,原则就是每个节点1个分片的效果是最好的,因为这样可以充分利用文件系统缓存,这对Elasticsearch
来说至关重要,但需要注意的是,从可用性的角度考虑,没有副本的分片在发生节点故障时数据就会丢失了,所以这是性能与可用性之间的权衡问题。
在兼容性能和可用性上有个计算公式:如果您有一个num_nodes节点的集群,总共有num_primaries个主分片,并且如果您想一次最多能够应对max_failures节点故障,那么适合您的正确副本数量是:max(max_failures, ceil(num_nodes / num_primaries) - 1)
2. 分片大小多少合适?
虽然Elasticsearch
并没有对分片的大小进行限制,但通常建议单分片的大小上限控制在30GB ~ 50GB
之间,原因主要有两方面,一方面过大的分片将会影响数据检索的效率,另一方面当一个节点出现故障时,Elasticsearch
会进行分片重平衡,太大的分片也将会影响重平衡的效率。
3. 如何避免单分片过大?
常见的方法当然是按某个属性进行拆分,比如按照每14天
一个索引进行维护即可。
以下案例建立了一个索引模板,定义匹配以order
名称开头的索引,并建立两个索引别名,一个是保存全量的数量:all_order
,一个是保存近期14天的数据:recent_14d_order
# 建立模板
PUT _template/order_template
{
"index_patterns": [
"order*"
],
"template": {
"settings": {
"number_of_shards": 1
},
"mappings": {
"properties": {
"timestamp": {
"type": "date"
},
"message": {
"type": "text"
}
}
}
},
"aliases": {
"all_order": {},
"recent_14d_order": {
"filter": {
"range": {
"timestamp": {
"gte": "now-14d/d"
}
}
}
}
}
}
# 测试数据
PUT order-2024/_doc/1
{
"timestamp":"2024-02-02T12:00:00",
"message":"2024第1条消息"
}
# 测试数据
PUT order-2024/_doc/2
{
"timestamp":"2024-05-28T12:00:00",
"message":"2024第2条消息"
}
# 测试数据
PUT order-2023/_doc/1
{
"timestamp":"2023-02-02T12:00:00",
"message":"2023第1条消息"
}
# 可以查到所有数据
GET /all_order/_search
{
"query": {
"match": {
"message": "消息"
}
}
}
# 只能查到近14天产生的数据
GET /recent_14d_order/_search
{
"query": {
"match": {
"message": "消息"
}
}
}
五、Elasticsearch集群基本信息查看
Elasticsearch
提供了cat命令
用来查看集群中各种运行情况的相关信息,大多数命令对生产上运行情况的监控都是非常有帮助的,建议结合问题发现与监控一起使用。
命令汇总
cat aliases 别名
cat allocation shard分配情况
cat count doc数量
cat fielddata fielddata内存使用情况
cat health 集群健康状态
cat indices 索引
cat master master节点信息
cat nodeattrs 节点自定义信息
cat nodes 节点信息
cat pending tasks pending task信息
cat plugins 节点plugin信息
cat recovery shard的recovery过程
cat repositories 查看注册的快照存储库
cat thread pool 各thread pool的使用情况
cat shards 分片使用情况
cat segments segments使用情况
cat snapshots 快照信息
cat templates 模板信息
查询语法
v
表示显示详细信息
GET /_cat/aliases?v
help
用于输出其他一些列的信息
GET /_cat/aliases?help
h
显示指定的列信息
GET /_cat/aliases?h=index,alias
Numeric formats
单位格式化输出,包括存储单位、时间单位等
GET /_cat/fielddata?v&bytes=b
json
格式输出
GET /_cat/fielddata?format=json&pretty
sort
排序
GET /_cat/fielddata?v&s=size:desc