Elasticsearch是一个底层基于Lucene的全文搜索和分析引擎,支持近乎实时地存储、搜索和分析大量数据的能力,最常用于网站搜索、日志搜索、数据分析等场景。
本文主要针对日常工作中Elasticsearch使用的一些基础概念、使用规范、注意事项、常见优化以及工具使用进行总结,如有不当的地方,欢迎指正。
索引建立时需搞清楚每个字段存在的用途(这里用途不仅仅是业务上的定义,还需关心该字段是会做索引,还是会聚合计算,还是会有排序,或者仅仅只是文档),在建立mapping时应当根据字段的不同用途,不同数据类型来匹配合适的Elasticsearch中的数据类型。
string类型Keyword和Text都属于string类的基本数据类型,但使用场景完全不同。
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"}}]}
数值类型
数组类型包括: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类型,尽管它本身是一串数字。
日期类型
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}
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"}}}}
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不会分词,不过为什么一个字段既要分词又要排序或聚合?当你仔细思考使用场景后你会发现这样做通常是没有意义的。
索引与评分
根据字段的实际使用情况,确定是否需要索引、文档评分,这样将会减少磁盘存储(每条文档中每个开启了norms的字段需占用1个字节),大多数字段类型默认是开启的。
PUT my_index/_mapping/_doc
{
"properties": {
"title": {
"type": "text",
"norms": false
}
}
}
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" : { }
}
]
}
}
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
,节省磁盘存储空间。
index.refresh_interval 此参数用于控制文档从被写入到可被搜索的处理时间,默认频率为每秒执行一次(仅针对在过去index.search.idle.after
秒内至少接收到一次搜索请求的索引),也就是说数据至少要在写入1s以后才能被搜索到,如果索引搜索请求不高,可以不调整,如果索引请求频率较高,则建议适当延长这个时间以减少集群压力(建议调到30s)。
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。