微信公众号:运维开发故事,作者:wanger
文档Document
用户存储在es中的数据文档
索引Index
由具有相同字段的文档列表组成
节点node
一个Elasticsearch的运行实例,是集群的构成单元
集群Cluster
由一个或多个节点组成,对外提供服务
json object,由字段(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组成(6.x _type不再起作用,同_id一样)
_source:文档的原始json数据,可以从这里获取每个字段的内容
_all:整合所有字段内容到该字段,默认禁用
索引中存储具有相同结构的文档(Document)
每个索引都有自己的mapping定义,用于定义字段名和类型
一个集群可以有多个索引,比如:
nginx日志存储的时候可以按照日期每天生成一个索引来存储
elasticsearch集群对外提供restful api
rest-representational state transfer
uri指定资源,如index、document等
http method指明资源操作类型,如GET、POST、PUT、DELETE等
常用两种交互方式
curl命令行
Kibana Devtools
es有专门的index api,用于创建、更新、删除索引配置等
创建索引api如下:
PUT /test_index
es有专门的Document API
创建文档
查询文档
更新文档
删除文档
创建文档
指定id创建文档api如下:
PUT /test_index/doc/1
{
"username":"alfred",
"age":1
}
创建文档时,如果索引不存在,es会自动创建对应的index和type - 不指定id创建文档api如下:
POST /test_index/doc
{
"username":"tom",
"age":20
}
查询文档
指定要查询的文档id
GET /test_index/doc/1
GET /test_index/doc/_search
{
"query":{
"term":{
"_id":"1"
}
}
}
查询语句,json格式,放在http body中发送到es
es允许一次创建多个文档,从而减少网络传输开销,提升写入速率
endpoint为_bulk,如下:
POST _bulk
{"index":{"_index":"test_index","_type":"doc","_id":"3"}}
{"username":"alfred","age":10}
{"delete":{"_index":"test_index","_type":"doc","_id":"1"}}
es允许一次查询多个文档
endpoint为_mget,如下:
GET /_mget
{
"doc":[
{
"_index":"test_index",
"_type":"doc",
"_id":"1"
},
{
"_index":"test_index",
"_type":"doc",
"_id":"2"
}
]
}
正排索引
文档id到文档内容、单词的关联关系
文档id | 文档内容 |
---|---|
1 | elasticsearch是最流行的搜索引擎 |
2 | PHP是世界上最好的语言 |
3 | 搜索引擎是如何诞生的 |
倒排索引
单词到文档id的关联关系
单词 | 文档ID列表 |
---|---|
elasticsearch | 1 |
流行 | 1 |
搜索引擎 | 1,3 |
世界 | 2 |
查询包含"搜索引擎"的文档
通过倒排索引获得"搜索引擎"对应的文档ID有1和3
通过正排索引查询1和3的完整内容
返回用户最终结果
倒排索引是搜索引擎的核心,主要包含两部分:
单词字典(Term Dictionary)
倒排列表(Posting List)
es存储的是一个json格式的文档,其中包含多个字段,每个字段会有自己的倒排索引
单词字典(Term Dictionary)是倒排索引的重要组成部分
记录所有文档的单词,一般都比较大
记录单词到倒排列表的关联信息
单词字典的实现一般是用B+ Tree
倒排列表记录了单词对应的文档集合,由倒排索引项组成
倒排索引项主要包含如下信息:
文档ID,用于获取原始信息
单词频率,记录该单词在该文档中的出现次数,用于后续相关性算分
位置,记录单词在该文档中的分词位置(多个),用于做词语搜索
偏移,记录单词在文档的开始和结束位置,用于做高亮显示
分词器是es中专门处理分词的组件,英文为analy,它的组成如下:
针对tokenizer处理的单词进行再加工,比如转小写、删除或新增等处理
将原始文本按照一定规则切分为单词
针对原始文本进行处理,比如去除HTML特殊标记符
Character Filters
Tokenizer
Token Filters
es提供了一个测试分词的api接口,方便验证分词效果,endpoint是_analyze
可以直接指定analyzer进行测试
可以直接指定索引中的字段进行测试
可以自定义分词器进行测试
直接指定analyzer进行测试,接口如下:
POST _analyze
{
"analyzer":"standard", #分词器
"text":"hello,world" #测试文本
}
POST _analyze
{
"tokenizer":"standard",
"filterf":["lowercase"], #自定义analyzer
"text":"hello world"
}
es自带如下的分词器
Standard
Simple
Whitespace
Stop
Keyword
Pattern
Language
Standard Analyzer
按词切分,支持多语言
小写处理
默认分词器
其组成如下,特性为:
Simple Analyzer
按照非字母切分
小写处理
其组成如下,特性为:
Whitespace Analyzer
按照空格切分
其组成如下,特性为:
Stop Analyzer
相比Simple Analyzer多了stop Word处理
Stop Word指语气助词等修饰性的词语,比如the、an、的、这等等
其组成如图,特性为:
Keyword Analyzer
不分词,直接将输入作为一个单词输出
其组成如下,特性为:
Pattern Analyze
通过正则表达式自定义分隔符
默认是\W+,即非字词的符号作为分隔符
其组成如下,特性为:
language Analyze
提供了30+常见语言的分词器
Arabic,Armenian,basque,bengali,Brazilian,Bulgarian,catAlan,cjk,Czech,Danish,Dutch,English…
难点
乒乓球拍/卖/完了
乒乓球/拍卖/完了
中文分词指的是将一个汉字序列切分成一个一个单独的词。在英文中,单词之间是以空格作为自然分界符,汉语中没有一个形式上的分界符
上下文不同,分词结果迥异,比如交叉歧义问题,比如下面两种分词都合理
常用分词系统
python中最流行的分词系统,支持分词和词性标注
支持繁体分词、自定义词典、并行分词等
https://github.com/singlee/elasticsearch-jieba-plugin
实现中英文单词的切分,支持ik_smart、ik_maxword等模式
可自定义词库,支持热更新分词词典
https://github.com/medcl/elasticsearch-analysis-ik
IK
jieba
基于自然语言处理的分词系统
THU Lexical Analyzer for Chinese,由清华大学自然原因处理与社会人文计算实验室研制推出的一套中文词法分析工具包,具有中文分词和词性标注功能
https://github.com/microbun/elasticsearch-thulac-plugin
由一系列模型与算法组成的Java工具包,目标是普及自然语言处理在生产环境中的应用
https://github.com/hankcs/HanLP
Hanlp
THULAC
当自带的分词无法满足需求时,可以自定义分词
通过自定义Character Filters、Tokenizer Filter实现
Character Filters
HTML Strip去除html标签和转换html实体
Mapping进行字符替换操作
Pattern Replace进行正则匹配替换
在Tokenizer之前对原始文本进行处理,比如增加、删除或替换字符等
自带的如下:
会影响后续Tokenizer解析的position和offset信息
Character Filters测试时可以采用如下api:
POST _analyze
{
"tokenizer":"keyword", #keyword类型的Tokenizer可以直接看到输出结果
"char_filter":["html_strip"], #指明要使用的char_filter
"text":"I'm so
happy!
"
}
Tokenizer
standard按照单词进行分割
letter按照非字符类进行分割
whitespace按照空格进行分割
UAX URL Email按照standard分割,但不会分割邮箱和url
NGram和Edge NGram连词分割
Path Hierarchy按照文件路径进行分割
将原始文本按照一定规则且分为单词(term or token)
自带的如下:
Tokenizer测试时可以采用如下api:
POST _analyze
{
"tokenizer":"path_hierarchy",
"text":"/one/two/three"
}
Token Filters
lowercase将所有term转换为小写
stop删除stop words
NGram和Edge NGram连词分割
Synonym添加近义词的term
对于Tokenizer输出的单词(term)进行增加、删除、修改等操作
自带的如下:
Filter测试时可以采用如下api:
POST _analyze
{
"text":"a hello world",
"tokenizer":"standard",
"filter":[
"stop",
"lowercase",
{
"type":"ngram",
"min_gram":4,
"max_gram":4
}
]
}
自定义分词的api
自定义分词需要在索引的配置中设定,如下所示:
PUT test_index
{
"settings":{
"analysis":{
"char_filter":{},
"tokenizer":{},
"filter":{},
"analyzer":{}
}
}
}
分词会在如下两个时机使用:
创建或更新文档时(Index Time),会对相应的文档进行分词处理
查询时(Search Time),会对查询语句进行分词
索引时分词是通过配置Index Mapping中每个字段的analyzer属性实现的,如下:
不指定分词时,默认使用standard
PUT test_index
{
"mappings":{
"doc":{
"properties":{
"title":{
"type":"text",
"analyzer":"whitespace" #指定分词器
}
}
}
}
}
查询时分词的指定方式有如下几种:
查询时通过analyzer指定分词器
POST test_index/_search
{
"query":{
"match":{
"message":{
"query":"hello",
"analyzer":"standard"
}
}
}
}
PUT test_index
{
"mappings":{
"doc":{
"properties":{
"title":{
"type":"text",
"analyzer":"whitespace",
"search_analyzer":"standard"
}
}
}
}
}
明确字段是否需要分词,不需要分词的字段就将type设置为keyword,可以节省空间和提高写性能
善用_analyze API,查看文档的具体分词结果
动手测试
类似数据库中的表结构定义,主要作用如下:
定义Index下的字段名(Field Name)
定义字段的类型,比如数值型、字符串型、布尔型等
定义倒排索引相关的配置,比如是否索引、记录position等。
PUT my_index
{
"mappings":{
"doc":{
"properties":{
"title":{
"type":"text"
},
"name":{
"type":"keyword"
},
"age":{
"type":"integer"
}
}
}
}
}
Mapping中的字段类型一旦设定后,禁止直接修改,原因如下:
Lucene实现的倒排索引生成后不允许修改
重新建立新的索引,然后做reindex操作
允许新增字段
通过dynamic参数来控制字段的新增
true(默认)允许自动新增字段
false不允许自动新增字段,但是文档可以正常写入,但无法对字段进行查询等操作
strict文档不能写入,报错
copy_to
将该字段的值复制到目标字段,实现类似_all的作用
不会出现在_source中,只用来搜索
PUT my_index
{
"mappings":{
"doc":{
"properties":{
"first_name":{
"type":""text",
"copy_to":"full_name"
},
"last_name":{
"type":"text",
"copy_to":"full_name"
},
"full_name":{
"type":"text"
}
}
}
}
}
index
控制当前字段是否索引,默认为true,即记录索引,false不记录,即不可搜索
PUT my_index
{
"mappings":{
"doc":{
"properties":{
"cookie":{
"type":"text",
"index":"false"
}
}
}
}
}
index_options用于控制倒排索引记录的内容,有如下4中配置
docs只记录doc id
freqs记录doc id和term frequencies
positions记录doc id、term frequencies和term position
offsets记录doc id、term frequencies、term position和character offsets
text类型默认配置为positions,其他默认为docs
记录内容越多,占用空间越大
indexoptions设定如下所示:
PUT my_index
{
"mappings":{
"doc":{
"properties":{
"cookies":{
"type":"text",
"index_options":"offsets"
}
}
}
}
}
null_value
当字段遇到null值时的处理策略,默认为null,即空值,此时es会忽略该值。可以通过设定该值设定字段的默认值
PUT my_index
{
"mappings":{
"my_type":{
"properties":{
"status_code":{
"type":"keyword",
"null_value":"NULL"
}
}
}
}
}
核心数据类型
字符串: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
复杂数据类型
数组类型array
对象类型object
嵌套类型nested object
地理位置数据类型
geo_point
geo_shape
专用类型
记录ip地址ip
实现自动补全completion
记录分词数token_count
记录字符串hash值murmur3
percolator
join
多字段特性
允许对同一个字段采用不同的配置,比如分词,常见例子如对人名实现拼音搜素,只需要在人名中新增一个子字段为pinyin即可
{
"test_index":{
"mappings":{
"doc":{
"properties":{
"username":{
"type":"text",
"fields":{
"pinyin":{
"type":"text",
"analyzer":"pinyin"
}
}
}
}
}
}
}
}
es可以自动识别文档字段类型,从而降低用户使用成本
es是依靠json文档的字段类型来实现自动识别字段类型,支持的类型如下:
JSON类型 | es类型 |
---|---|
null | 忽略 |
Boolean | Boolean |
浮点类型 | float |
整数 | long |
object | object |
array | 有第一个非null值的类型决定 |
string | 匹配为日期则设为date类型(默认开启),匹配为数字的话设为float或long类型(默认关闭),设为text类型,并附带keyword的子字段 |
日期的自动识别可以自行配置日期格式,以满足各种需求
YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-20T15;30:50 +01:00)
默认是[“strict_date_optional_time”,“yyyy/MM/dd HH:mm:ss Z||yyyy /MM/dd Z”]
strict_date_optional_time是ISO datetime的格式,完整格式类似下面:
dynamic_date_formats可以自定义日期类型
date_detection可以关闭日期自动识别的机制
PUT mu_index
{
"mappings":{
"my_type":{
"dynamic_date_formats":["MM/dd/yyyy"],
"date_detection":false
}
}
}
字符串是数字时,默认不会自动识别为整型,因为字符串中出现数字是完全合理的
numeric_detection可以开启字符串中数字的自动识别,如下所示:
PUT my_index
{
"mappings":{
"my_type":{
"numeric_detection":true
}
}
}
允许根据es自动识别的数据类型、字段名等来动态设定字段类型,可以实现如下效果:
所有字符串类型都设定为keyword类型,即默认不分词
所有以message开头的字段都设定为text类型,即分词
所有以long_开头的字段都设定为long类型
所有自动分配为double类型的都设定为float类型,以节省空间
API如下所示:
PUT test_index
{
"mappings":{
"doc":{
"dynamic_templates":[#数组,可指定多个匹配规则
{
"strings":{ #模板名称
"match_mapping_type":"string",#匹配规则
"mapping":{
"type":"keyword"
}
}
}
]
}
}
}
匹配规则一般有如下几个参数:
match_mapping_type匹配es自动识别的字段类型,如Boolean,long,string等
match,unmatch匹配字段名
path_match,path_unmatch匹配路径
写入一条文档到es的临时索引中,获取es自动生成的mapping
修改步骤1得到的mapping,自定义相关配置
使用步骤2的mapping创建实际所需索引
首先创建一个文档
PUT my_index/doc/1
{
"referrer": "-",
"response":"200",
"remote_ip":"171.22.12.14",
"method":"POST",
"user_name":"-",
"http_version":"1.1",
"body_sent":{
"bytes":"0"
},
"url":"/analyzeVideo"
}
es会根据创建的文档动态生成映射,可以直接将动态生成的映射直接复制到需要自定义的mapping中
PUT test_index
{
"mappings": {
"doc": {
"properties": {
"body_sent": {
"properties": {
"bytes": {
"type": "long"
}
}
},
"http_version": {
"type": "keyword"
},
"method": {
"type": "keyword"
},
"referrer": {
"type": "keyword"
},
"remote_ip": {
"type": "keyword"
},
"response": {
"type": "long"
},
"url": {
"type": "text"
},
"user_name": {
"type": "keyword"
}
}
}
}
}
这样定义的映射还是比较多余,可以利用动态模板将string类型直接替换成keyword
DELETE test_index
PUT test_index
{
"mappings": {
"doc": {
"dynamic_templates":[
{
"strings":{
"match_mapping_type":"string",
"mapping":{
"type":"keyword"
}
}
}
],
"properties": {
"body_sent": {
"properties": {
"bytes": {
"type": "long"
}
}
},
"response": {
"type": "long"
},
"url": {
"type": "text"
}
}
}
}
}
索引模板,主要用于在新建索引时自动应用预先设定的配置,简化索引创建的操作步骤
可以设定索引的配置和mapping
可以有多个模板,根据order设置,order搭的覆盖小的配置
索引模板API如下所示:
PUT _template/test_template
{
"index_patterns":["te*","bar*"],
"order":0,
"settings":{
"number_of_shards":1,
"number_of_replicas":0
},
"mappings":{
"doc":{
"_source":{
"enabled":false
},
"properties":{
"name":{
"type":"keyword"
}
}
}
}
}
“index_paterns”:匹配的索引名称
“order”:匹配的优先级
“settings”:索引的配置
查询有两种形式:
es提供完备的查询语法Query DSL
操作简单,方便通过命令行测试
仅包含部分查询语法
URI search
Request Body Search
通过url query参数来实现搜索,常用参数如下:
q 指定查询的语句,语法为Query String Syntax
df q中不指定字段时默认查询的字段,如果不指定es会查询所有字段
sort 排序
timeout 指定超时时间,默认不超时
from,size用于分页
term与phrase
alfred way 等效于Alfred OR way
“alfred way” 词语查询,要求先后顺序
泛查询
alfred 等效于在所有字段去匹配该term
指定字段
name:alfred
Group分组设定,使用括号指定匹配的规则
(quick OR brown) AND fox
status:(active OR pending) title:(full text search)
布尔操作符
name:(tom NOT lee)
注意大写,不能小写
AND(&&),OR(||),NOT(!)
+—分别对应must和must_not
name:(tom +lee -alfred) #返回一定包含lee,可以包含tom,一定不包含alfred的文档
name:((lee && !alfred)) || (tom && lee && !alfred)
+在url中会被解析为空格,要使用encode后的结果才可以,为%2B
范围查询,支持数值和日期
age:>=1
age:(>=1 && <=10) 或者age:(+>=1 +<=10)
age:[1 TO 10] 意为1<=age<=10
age:[1 TO 10} 意为1<=age<10
age:[1 TO] 意为age>=1
age:[* TO 10] 意为age<=10
区间写法,闭区间用[],开区间用{}
算数符号写法
通配符查询
name:t?m
name:tom*
name:t*m
通配符匹配执行效率低,且占用较多内存,不建议使用
如无特殊需求,不要将?/*放在最前面
?代表1个字符,*代表0或多个字符
正则表达式匹配
name:/[mb]oat/
模糊匹配fuzzy query
name:roam~1
匹配与roam差一个character的词,比如foam roams等
近似度查询 proximity search
“fox quick” ~5
以term为单位进行差异比较,比如"quick fox" "quick brown fox"都会被匹配
基于json定义的查询语句,主要包含如下两种类型:
如bool查询等,包含一个或多个字段类查询或者复合查询语句
如term,match,range等,只针对某一个字段进行查询
字段类查询:
复合查询
字段类查询主要包括以下两类:
不会对查询语句做分词处理,直接去匹配字段的倒排索引,如term.terms,range等query类型
针对text 类型的字段进行全文检索,会对查询语言先进行分词处理,如match,match_phrase等query类型
全文匹配
单词匹配
GET test_index/_search
{
"query":{
"match":{ #关键词
"remote_ip":"171.22.12.14" #字段名
}
}
}
响应结果如下:
{
"took": 4, #查询总用时
"timed_out": false, #是否超时
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1, #匹配文档总数
"max_score": 0.2876821,
"hits": [ #返回文档列表
{
"_index": "test_index_index",
"_type": "doc",
"_id": "1",
"_score": 0.2876821, #文档相关度得分
"_source": { #文档原始内容
"referrer": "-",
"response": "200",
"remote_ip": "171.22.12.14",
"method": "POST",
"user_name": "-",
"http_version": "1.1",
"body_sent": {
"bytes": "0"
},
"url": "/analyzeVideo"
}
}
]
}
}
通过operator参数可以控制单词间的匹配关系,可选项为or和and
通过minimum_should_match参数可以控制需要匹配的单词数
首先对查询语句进行分词,分词后分别根据字段的倒排索引进行匹配算分,并会匹配到一个或多个文档,再将匹配到的文档进行汇总得分,根据得分排序返回多个文档
GET test_index_index/_search
{
"query":{
"match_phrase":{
"remote_ip":"171.22.12.14"
}
}
}
GET test_index_index/_search
{
"query":{
"query_string":{
"default_field":"remote_ip",
"query":"171.22.12.14"
}
}
}
类似Query String,但是会忽略错误的查询语法,并且仅支持部分查询语法
其常用的逻辑符号如下,不能使用AND、OR、NOT等关键词:
+代指AND
|代指OR
-代指NOT 常用API如下所示:
GET test_index_index/_search
{
"query":{
"simple_query_string":{
"fields":["remote_ip"],
"query":"alfred +way"
}
}
}
GET test_index_index/_search
{
"query":{
"term":{
"remote_ip":"171.22.12.14"
}
}
}
GET test_index_index/_search
{
"query":{
"range":{
"response":{#找出响应状态码大于10,小于300的文档
"gt": 10,
"lte":300
}
}
}
}
针对日期的查询如下所示:
GET test_index_index/_search
{
"query":{
"range":{
"birth":{
"gt": "1990-01-01",
"lte":"now-2h",
"gt":"2019-01-01||+1M/d"
}
}
}
}
相关性算分是指文档与查询语句间的相关度,英文为relevance
通过倒排索引可以获取与查询语句相匹配的文档列表,那么如何将最符合用户查询需求的文档放到前列呢?
本质是一个排序问题,排序的依据是相关性算分
相关性算分的几个重要概念如下:
Term Frequency(TF)词频,即单词在该文档中出现的次数。词频越高,相关度越高
Document Frequency(DF)文档频率,即单词出现的文档数
Inverse document Frequency(IDF)逆向文档频率,与文档频率相反,简单理解为1/DF。即单词出现的文档数越少,相关度越高
Field-length Norm 文档越短,相关性越高
ES目前主要有两个相关性算分模型,如下:
TF/IDF模型
BM25模型 5.x之后的默认模型
可以通过explain参数来查看具体的计算方法,但要注意:
es的算分是按照shard进行的,即shard的分数计算是相互独立的,所以在使用explain的时候注意分片数
可以通过设置索引的分片数为1来避免这个问题
GET test_index_index/_search
{
"explain": true,
"query":{
"match":{
"remote_ip":"171.22.12.14"
}
}
}
BM25模型中BM指Best Match,25指迭代了25次才计算方法,是针对TF/IDF的一个优化。
BM25相比TF/IDF的一大优化是降低了tf在过大时的权重
复合查询是指包含字段类查询或符合查询的类型,主要包括以下几类:
constant_score query
bool query
dis_max query
function_score query
boosting query
该查询将其内部的查询结果文档得分都设定为1或者boost的值
多用于结合bool查询实现自定义部分
GET test_index_index/_search
{
"query":{
"constant_score":{
"filter":{
"match": {
"response":200
}
}
}
}
}
响应如下:
{
"took": 7,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"_index": "test_index_index",
"_type": "doc",
"_id": "1",
"_score": 1,
"_source": {
"referrer": "-",
"response": "200",
"remote_ip": "171.22.12.14",
"method": "POST",
"user_name": "-",
"http_version": "1.1",
"body_sent": {
"bytes": "0"
},
"url": "/analyzeVideo"
}
}
]
}
}
子句 | 内容 |
---|---|
filter | 只过滤符合条件的文档,不计算相关性得分 |
must | 文档必须符合must中的所有条件,会影响相关性得分 |
must_not | 文档必须不符合must_not中的所有条件 |
should | 文档可以符合should中的条件,会影响相关性得分 |
GET test_index_index/_search
{
"query":{
"bool":{
"filter":[
{}
],
"should":[
{}
],
"must":[
{}
],
"must_not": [
{}
]
}
}
}
查询response为200,ip为"171.22.12.14"的值
GET test_index_index/_search
{
"query":{
"bool":{
"must":[
{
"match":{
"response":200
}
},
{
"match":{
"remote_ip": "171.22.12.14"
}
}
]
}
}
}
上下文类型 | 执行类型 | 使用方式 |
---|---|---|
Query | 查询与查询语句最匹配的文档,对所有文档进行相关性算分并排序 | 1.query 2. bool中的must和should |
Filter | 查找与查询语句相匹配的文档,只过滤不算分,经常使用过滤器,ES会自动的缓存过滤器的内容,这对于查询来说,会提高很多性能 | 1.bool中的filter与must_not 2.constant_score中的filter |
GET /_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "Search" }},
{ "match": { "content": "Elasticsearch" }}
],
"filter": [
{ "term": { "status": "published" }},
{ "range": { "publish_date": { "gte": "2015-01-01" }}}
]
}
}
}
GET test_index_index/_count
{
"query":{
"match":{
"response":200
}
}
}
响应如下:
{
"count": 1,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
}
}
GET test_index/_search
{
"_source":false
}
返回部分字段
GET test_index/_search
{
"_source":["response","name"]
}
GET test_index/_search
{
"_source":{
"includes":"*i*",
"excludes":"remote_ip"
}
}
es支持集群模式,是一个分布式系统,其好处主要有两个:
增大系统容量,如内存、磁盘,使得es集群可以支持PB级的数据
提高系统可用性,即使部分节点停止服务,整个集群依然可以正常服务
es集群由多个es实例组成
不同集群通过集群名字来区分,可通过cluster.name进行修改
每个es实例本质上是一个JVM进程,且有自己的名字,通过node.name进行修改
可以修改cluster state的节点称为master节点,一个集群只能有一个
cluster state存储在每个节点上,master维护最新版本并同步给其他节点
master节点是通过集群中所有节点选举产生的,可以被选举的节点称为master-eligible节点,相关配置如下:
node.master:true
处理请求的节点即为coordinating节点,该节点为所有节点的默认角色,不能取消
路由请求到正确的节点处理,比如创建索引的请求到master节点
存储数据的节点即为data节点,默认节点都是data类型,相关配置如下:
node.data:true
如何将数据分布于所有节点上?
引入分片解决问题
分片是es支持PB级数据的基石
分片存储了部分数据,可以分部于任意分片上
分片数在索引创建时指定且后续不允许再更改,默认为5个
分片有主分片和副本分片之分,以实现数据的高可用
副本分片的数据由主分片同步,可以有多个,从而提高读取的吞吐量
分片数的设定很重要,需要提前规划好
过小会导致后续无法通过增加节点实现水平扩容
过大会导致一个节点上分布过多过多分片,造成资源浪费,同时会影响查询性能
node2和node3发现node1无法响应一段时间后会发起master选举,比如这里选择node2为master节点。此时由于主分片P0下线,集群状态变为Red。
node2发现主分片P0未分配,将R0提升为主分片。此时由于所有主分片都正常分配,集群状态变为yellow。
node2为P0和P1生成新的副本,集群状态变为绿色
document1是如何存储到分片P1的?选择P1的依据是什么?
需要文档到分片的映射算法
目的 -使得文档均与分布在所有分片上,以充分利用资源
算法
shard=hash(routing)% number_of_primary_shards
hash算法保证可以将数据均匀的分散在分片中
routing是一个关键参数,默认是文档id,也可以自行指定
number_of_primary_shards 是主分片
该算法与主分片相关,这也是分片一旦确定后便不能更改的原因
client向node3发起创建文档的请求
node3通过routing计算该文档应该存储在shard1上,查询cluster state后确认主分片P1在node2上,然后转发创建文档的请求到node2
P1接收并执行创建文档请求后,将同样的请求发送到副本分片R1
R1接收并执行创建文档请求后,通知P1成功的结果
P1接收副本分片结果后,通知node3创建成功
node3返回结果到client
client向node3发起获取文档1的请求
node3通过routing计算该文档在shard1上,查询cluster state后获取shard1的主副分片列表,然后以轮询的机制获取一个shard,比如这里是R1,然后转发读取文档的请求到node1
R1接收并执行读取文档请求后,将结果返回node3
node3返回结果给client
client向node3发起批量创建文档的请求
node3通过routing计算所有文档对应的shard,然后按照主shard分配对应执行的操作,同时发送请求到涉及的主shard,比如这里3个主shard都需要参与
主shard接收并执行请求后,将同样的请求发送到对应的副本shard
副本shard执行结果后返回结果到主shard,主shard再返回node3
node3整合结果返回client
client向node3发起批量获取文档的请求(mget)
node3通过routing计算所有文档对应的shard,然后以轮询的机制获取要参与shard,按照shard构建mget请求,同时发送请求到涉及的shard,比如这里有两个shard需要参与
R1、R2返回文档结果
node3返回结果给client
客户端发送请求到一个coordinate node
协调节点将搜索请求转发到所有的shard对应的primary shard或replica shard也可以
query phase:每个shard将自己的搜索结果(其实就是一些doc id),返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果
fetch phase:接着由协调节点,根据doc id去各个节点上拉取实际的document数据,最终返回给客户端
脑裂问题,英文为split-brain,是分布式系统中的经典网络问题
当node1故障时,node2与node3会重新选举master,比如node2成为了新的master,此时会更新cluster state
node1自己组成集群后,也会更新cluster state
同一个集群有两个master,而且维护不同的cluster state,网络恢复后无法选择正确的master
解决方案为仅可在选举master-eligible节点数大于等于quorum时才可以进行master选举
quorum=master-eligible节点数/2+1,例如三个master-eligible节点时,quorum为2
设定discovery.zen.minimum_master_nodes为quorum即可避免脑裂
倒排索引一旦生成,不能更改
好处如下:
不用考虑并发写文件的问题,杜绝了锁机制带来的性能问题
由于文件不再更改,可以充分利用文件系统缓存,只需载入一次,只要内存足够,对该文件的读取都会从内存读取,性能高
利于生成缓存数据
利于对文件进行压缩存储,节省磁盘和内存存储空间
坏处为需要写入新文档时,必须重新构建倒排索引文件,然后替换老文件后,新文档才能被检索,导致文档实时性差
解决方案是新文档直接生成新的倒排索引文件,查询的时候同时查询所有的倒排文件,然后做结果的汇总即可
Lucene便是采用了这种方案,它构建的单个倒排索引称为segment,合在一起称为index,与ES中的index概念不同。ES中的一个shard对应一个Lucene index
Lucene会有一个专门的文件来记录所有的segment信息,称为commit point
segment写入磁盘的过程依然很耗时,可以借助文件系统缓存的特性,先将segment在缓存中创建并开放查询来进一步提升实时性,该过程在es中被称为refresh
在refresh之前文档会先存储在一个buffer中,refresh时将buffer中的所有文档清空并生成segment
es默认每一秒执行一次refresh,因此文档的实时性被提高到1秒,这也是es被称为近实时的原因
refresh发生的时机主要有如下几种情况:
间隔时间达到时,通过index.settings.refresh_interval来设定,默认是一秒
index.buffer占满时,其大小通过indices.memory.index_buffer_size设置,默认为jvm heap的10%,所有shard共享
flush发生时也会发生refresh
如果在内存中的segment还没有写入磁盘前发生了宕机,name其中的文档就无法恢复了。
es引入translog机制。写入文档到buffer时,同时将该操作写入translog
translog文件会即时写入磁盘(fsync),6.x默认每个请求都会落盘,可以修改为每五秒写一次,这样风险便是丢失5秒内的数据,相关配置为index.translog.*
es启动时会检查translog文件,并从中恢复数据
flush负责将内存中的segment写入磁盘,主要做如下的工作:
将translog写入磁盘,形成translog文件
将index buffer清空,其中的文档生成一个新的segment,相当于一个refresh操作
更新commit point并写入磁盘
执行fsync操作,将内存中的segment写入磁盘
删除旧的translog文件
flush发生的时机主要有如下几种情况:
间隔时间达到时,默认是30分钟,5.x之前可以通过index.translog.flush_threshold_period修改,之后无法修改
translog占满时,其大小可以通过index.translog.flush_threshold_size控制,默认是512mb,每个index有自己的translog
segment一旦生成就不能更改,那么如果你要删除文档该如何操作?
Lucene专门维护一个.del的文件,记录所有已经删除的文档,注意.del上记录的是文档在Lucene内部的id
在查询结果返回前会过滤掉.del中的所有文档
更新文档如何进行
首先删除文档,然后再创建新文档
随着segment的增多,由于一次查询的segment数增多,查询速度会变慢
es会定时在后台进行segment merge的操作,减少segment的数量
通过force_merge api可以手动强制做segment merge的操作
search 执行的时候实际分两个步骤运作的
Query阶段
fetch阶段
Query-Then-Fetch
node3在接收到用户的search请求后,会先进行Query阶段
node3在6个主副分片中随机选择三个分片,发送search request
被选中的3个分片会分别进行查询并排序,返回from+size个文档Id和排序值
node3整合3个分片返回的from+size个文档Id,根据排序值排序后选取from到from+size的文档Id
node3向相关的分片发送multi_get请求
3个分片返回文档详细数据
node3拼接返回的结果并返回客户端
相关性算分在shard与shard之间是相互独立的,也就意味着同一个Term的IDF等值在不同shard上是不同的。文档的相关性算分和他所处的shard相关
在文档数量不多时,会导致相关性算分严重不准的情况发生
解决思路:
一是设置分片数为1个,从根本上排除问题,在文档数量不多的时候可以考虑该方案,比如百万到千万级别的文档数量
二是使用DFS Query-then-Fetch查询方式
DFS Query-then-fetch是在拿到所有文档后再重新完整的计算一次相关性算分,耗费更多的CPU和内存,执行性能也比较低下,一般不建议使用。使用方式如下:
GET test_index/_search?search_type=dfs_query_then_fetch
GET test_search_index/_search
{
"sort":[ #关键字
{
"birth":"desc"
},
{
"_score":"desc"
},
{
"_doc":"desc"
}
]
}
GET test_search_index/_search
{
"sort":{
"username":"desc"
}
}
#会产生报错
GET test_search_index/_search
{
"sort":{
"username.keyword":"desc"
}
}
排序的过程实质是对字段原始内容排序的过程,这个过程中倒排索引无法发挥作用(term->document),需要用到正排索引,也就是通过文档id和字段可以快速得到字段原始内容。
es对此提供了两种实现方式:
fielddata默认禁用
doc values默认启用,除了text类型
文档ID | 字段值 |
---|---|
1 | 100 |
2 | 89 |
3 | 129 |
对比 | fielddata | DocValues |
---|---|---|
创建时机 | 搜索时即时创建 | 索引时创建,与倒排索引创建时机一致 |
创建位置 | JVM Heap | 磁盘 |
优点 | 不会占用额外的磁盘空间 | 不会占用Heap内存 |
缺点 | 文档过多时,即时创建会花过多时间,占用过多Heap内存 | 减慢索引的速度,占用额外的磁盘资源 |
Fielddata默认是关闭的,可以通过如下api开启:
此时字符串是按照分词后的term排序,往往结果很难符合预期
一般是在对分词做聚合分析的时候开启
PUT test_search_index/_mapping/doc
{
"properties":{
"username":{
"type":"text",
"fileddata":"true"
}
}
}
Doc Values默认是启用的,可以在创建索引的时候关闭:
如果后面要再开启doc values,需要做reindex操作
PUT test_doc_value/
{
"mappings":{
"doc":{
"properties":
}
}
}
GET test_search_index/_search
{
"docvalue_fields":[
"username",
"username.keyword",
"age"
]
}
最常用的分页方案
from指明开始位置
size指明获取总数
深度分页是一个经典问题:在数据分片存储的情况下如何获取前1000个文档?
获取从990-1000的文档时,会在每个分片上都先获取1000个文档,然后再由coordinating node局和所有分片的结果后再排序选取前1000个文档
页数越深,处理文档越多,占用内存越多,耗时越长。尽量避免深度分页,es通过index.max_result_window限定最多到10000条数据
total_page=(total+page_size-1)/page_size
total为文档总数
遍历文档集的api,以快照的方式来避免深度分页的问题
不能用来做实时搜索,因为数据不是实时的,是以快照的方式做的
尽量不要使用复杂的sort条件,使用_doc最高效
使用稍嫌复杂
第一步需要发起1个scroll search,如下所示:
es在收到该请求后会根据查询条件创建文档id合计的快照
GET test_search_index/_search?scrol=5m #该scroll快照有效时间
{
"size":1 #指明每次scroll返回的文档数
}
第二步调用scroll search的api,获取文档集合,如下所示:
不断迭代调用直到返回hits.hits数组为空时停止
post _search/scroll
{
"scroll":"5m", #指明有效时间
"scroll_id":"..." #上一步返回的id
}
DELETE /_search/scroll
{
"scroll_id":[
"DXFZAD....",
"DESGRHRH..."
]
}
DELETE /_search/scroll/_all
避免深度分页的性能问题,提供实时的下一页文档获取功能
缺点是不能使用from参数,即不能指定页数
只能下一页,不能上一页
使用简单
第一步为正常的搜索,但要指定sort值,并保证值唯一
第二步为使用上一步最后一个文档的sort值进行查询
GET test_search_index/_search
{
"size":1,
"sort":{
"age":"desc",
"_id":"desc"
}
}
GET test_search_index/_search
{
"size":1,
"search_after":[28,"2"],
"sort":{
"age":"desc",
"_id":"desc"
}
}
search_after如何避免深度分页问题
通过唯一排序值定位将每次要处理的文档数都控制在size内
类型 | 场景 |
---|---|
from/size | 需要实时获取顶部的部分文档,且需要自由翻页 |
scroll | 需要全部文档,如导出所有数据的功能 |
search_after | 需要全部文档,不需要自由翻页 |
聚合分析,英文为aggregation,是es除搜索功能外提供的针对es数据做统计分析的功能
功能丰富,提供bucket、metric。pipeline等多种分析方式,可以满足大部分的分析需求
实时性高,所有的计算都是即时返回的,而Hadoop等大数据系统一般都是T+1级别的(today+1,第二天)
聚合分析作为search的一部分,api如下所示:
GET test_search_index/_search
{
"size":0,
"aggs":{ #关键词,与query同级
"":{#定义聚合名称
"":{#定义聚合类型
},
[,"aggs":{[]+}]? #子查询
}
[,"":{...}]* #可以包含多个聚合分析
}
}
为了便于理解,es将聚合分析主要分为如下四类
Bucket,分桶类型,类似sql中的group by语法
Metric,指标分析模型,如计算最大值、最小值、平均值等等
pipline,管道分析类型,基于上一级的聚合分析结果进行再分析
Matrix,矩阵分析类型
主要分如下两类:
stats,extended stats
percentile,percentile rank
top hits
min,max,avg,sum
cardinality
单值分析,只输出过一个分析结果
多值分析,输出多个分析结果
返回数值类字段的最小值
GET test_search_index/_search
{
"size":0, #不需要返回文档列表
"aggs":{
"min_age":{
"min":{ #关键词
"field":"age"
}
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"max_age":{
"max":{
"field":"age"
}
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"count_of_job":{
"cardinality":{ #关键词
"field":"job.keyword"
}
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"stats_age":{
"stats":{
"field":"age"
}
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"stats_age":{
"extended_status":{
"field":"age"
}
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"per_age":{
"percentiles":{ #关键词
"field":"salary"
}
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"jobs":{
"terms":{
"field":"job.keyword",
"size":10
},
"aggs":{
"top_employee":{
"top_hits":{
"size":10,
"sort":[
{
"age":{
"order":"desc"
}
}
]
}
}
}
}
}
}
Bucket,意为桶,即按照一定的规则将文档分配到不同的桶中,达到分类分析的目的
按照Bucket的分桶策略,常见的Bucket聚合分析如下:
Terms
Range
Date Range
Histogram
Date Histogram
GET test_search_index/_search
{
"size":0,
"aggs":{
"jobs":{
"terms":{ #关键词
"field":"job.keyword",#指明term字段
"size":5 #指定返回数目
}
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"salary_range":{
"range":{ #关键词
"field":"salary",\
"ranges":[ #指定每个range的范围
{
"to":1000
},
{
"from":1000,
"to":2000
}
]
}
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"date_range":{
"range":{ #关键词
"field":"birth",
"format":"yyyy",#指定返回结果的日期格式
"ranges":[
{
"from":"1980",#指定日期,可以使用date math
"to":"1990"
},
{
"from":"1990",
"to":"2000"
}
]
}
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"salary_hist":{
"histogram":{ #关键词
"field":"salary",
"interval":5000, #指定间隔大小
"extended_bounds":{ #指定数据范围
"min":0,
"max":40000
}
}
}
}
}
Get test_search_index/_search
{
"size":0,
"aggs":{
"by_year":{
"date_historgram":{ #关键词
"field":"birth",
"interval":"year", #指定间隔大小
"format":"yyyy" #指定日期格式化
}
}
}
}
bucket聚合分析允许通过添加子分析来进一步进行分析,该子分析可以是bucket也可以是metric。这也使得es的聚合分析能力变得异常强大
分桶后再分桶
GET test_search_index/_search
{
"size":0,
"aggs":{
"jobs":{
"terms":{
"field":"job.keyword",
"size":10
},
"aggs"
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"jobs":{ #第一层聚合
"terms":{
"field":"job.keyword",
"size":10
},
"aggs":{
"salary":{ #第二层聚合
"stats":{
"field":"salary"
}
}
}
}
}
}
针对聚合分析的结果再次进行聚合分析,而且支持链式调用,可以回答如下问题:
订单月平均销售额是多少?
POST order/_search
{
"size":0,
"aggs":{
"sales_per_month":{
"date_histogram":{
"field":"date",
"interval":"month"
},
"aggs":{
"sales":{
"sum":{
"field":"price"
}
}
}
},
"avg_monthly_sales":{
"avg_bucket":{
"bucket_path":"sales_per_month>sales"
}
}
}
}
Pipeline的分析结果会输出到原结果中,根据输出位置的不同,分为以下两类:
Max/Min/Avg/Sum Bucket
Stats/Extended Stats Bucket
Percentiles Bucket
Derivative
Moving Average
Cumulative Sum
Parent 结果内嵌到现有的聚合分析结果中
Sibling 结果与现有聚合分析结果同级
GET test_search_index/_search
{
"size":0,
"aggs":{
"jobs":{
"terms":{
"field":"job.keyword",
"size":10
},
"aggs":{
"avg_salary":{
"avg":{
"field":"salary"
}
}
}
},
"min_salary_by_job":{
"min_bucket":{ #关键词
"buckets_path":"jobs>avg_salary"
}
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"jobs":{
"terms":{
"field":"job.keyword",
"size":10
},
"aggs":{
"avg_salary":{
"avg":{
"field":"salary"
}
}
}
},
"max_salary_by_job":{
"max_bucket":{
"buckets_path":"jobs>avg_salary"
}
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"birth":{
"date_histogram":{
"field":"birth",
"interval":"year",
"min_doc_count":0
},
"aggs":{
"avg_salary":{
"avg":{
"field":"salary"
}
},
"derivative_avg_salary":{
"derivative":{ #关键词
"bucket_path":"avg_salary"
}
}
}
}
}
}
GET test_search_index/_search
{
"size":,
"aggs":{
"birth":{
"date_histogram":{
"field":"birth",
"interval":"year",
"min_doc_count":0
},
"aggs":{
"avg_salary":{
"avg":{
"field":"salary"
}
},
"mavg_salary":{
"moving_avg":{ #关键词
"buckets_path":"avg_salary"
}
}
}
}
}
}
es聚合分析默认作用范围是query的结果集,可以通过如下的方式改变其作用范围:
filter
post_filter
global
GET test_search_index/_search
{
"size":0,
"query":{
"match":{#aggs作用域该query的结果集
"username":"alfred"
}
},
"aggs":{
"jobs":{
"terms":{
"field":"job.keyword",
"size":10
}
}
}
}
GET test_search_index/_search
{
"size":0,
"aggs":{
"jobs_salary_small":{
"filter":{
"range":{
"salary":{
"to":10000
}
}
},
"aggs":{
"jobs":{
"terms":{
"field":"job.keyword"
}
}
}
},
"jobs":{
"terms":{
"field":"job.keyword"
}
}
}
}
GET test_search_index/_search
{
"aggs":{
"jobs":{
"terms":{
"field":"job.keyword"
}
}
},
"post_filter":{#过滤条件
"match":{
"job.keyword":"java engineer"
}
}
}
GET test_search_index/_search
{
"query":{
"match":{
"job.keyword":"java engineer"
}
},
"aggs":{
"java_avg_salary":{
"avg":{
"field":"salary"
}
},
"all":{
"global":{},
"aggs":{
"avg_salary":{#过滤条件
"avg":{
"field":"salary"
}
}
}
}
}
}
可以使用自带的关键数据进行排序,比如:
_count文档数
_key按照key值排序
GET test_search_index/_search
{
"size":0,
"aggs":{
"jobs":{
"terms":{
"field":"job.keyword",
"size":10,
"order":[
{
"count":"asc"
},
{
"_key":"desc"
}
]
}
}
}
}
设置shard数为1,消除数据分散的问题,但无法承载大数据量
合理设置shard_size的大小,即每次从shard上额外多获取数据,以提升准确度
GET test_search_index/_search
{
"size":0,
"aggs":{
"jobs":{
"terms":{
"field":"job.keyword",
"size":1,
"shard_size":10
}
}
}
}
terms聚合返回结果中有如下两个统计值:
doc_count_error_upper_bound被遗漏的term可能的最大值
sum_other_doc_count返回结果bucket的term外其他term的文档总数
设定show_term_doc_count_error可以查看每个bucket误算的最大值
GET test_search_index/_search
{
"size":0,
"aggs":{
"jobs":{
"terms":{
"field":"job.keyword",
"size":2,
"show_term_doc_count_error":true
}
}
}
}
shard_size默认大小如下:
shard_size=(sizex1.5)+10
通过调整shard_size的大小降低doc_count_error_upper_bound来提升准确度
增大了整体的计算量,从而降低了响应时间
追求海量数据的准确度,用作离线计算,失去了实时性
追求海量数据的实时性,用作近似统计算法,失去了数据的准确度
追求数据的准确度和实时性,只是作为有限的数据计算,分析的数据只是片面的
在es的聚合分析中,cardinality和percentile分析使用的是近似统计算法
结果是近似准确的,但不一定精准
可以通过参数的调整使其结果精准,但同时也意味着更多的计算时间和更大的性能消耗
英文为data modeling,为创建数据模型的过程
数据模型(Data Model)
对现实世界进行抽象描述的一种工具和方法
通过抽象的实体及实体之间联系的形式去描述业务规则,从而实现对现实世界的映射
概念模型
确定系统的核心需求和范围边界,设计实体和实体间的关系
逻辑模型
进一步梳理业务需求,确定每个实体的属性、关系和约束等
物理模型
结合具体的数据库产品,在满足业务读写性能等需求的前提下确定最终的定义
Mysql、MongoDB、elasticsearch等
第三范式
enabled
true | false
仅存储,不做搜索或聚合分析
index
true | false
是否构建倒排索引
index_options
docs | freqs |positions | offsets
存储倒排索引的哪些信息
norms
true | false
是否存储归一化相关参数,如果字段仅用于过滤和聚合分析,可关闭
doc_values
true | false
c是否启用doc_values,用于排序和聚合分析
field_data
false | true
是否为text类型启用fielddata,实现排序和聚合分析
store
false | true
是否存储该字段值
coerce
true |false
是否开启自动数据类型转换功能,比如字符串转为数字、浮点转为×××等
multifields多字段
灵活使用多字段特性来解决多样的业务需求
dynamic
true | false |strict
控制mapping自动更新
date_detection
true | false
是否自动识别日期类型
是何种类型
是否需要检索
是否需要排序和聚合分析
是否需要另行存储
字符串类型
需要分词则设定为text类型,否则设定为keyword类型
枚举类型
基于性能考虑将其设定为keyword类型,即便该数据为整性
数值类型
尽量选择贴近的类型,比如byte即可表示所有数值时,即选用byte,不要用long
其他类型
比如布尔类型、日期、地理位置数据等
完全不需要检索、排序、聚合分析的字段
enabled设置为false
不需要检索的字段
index设置为false
需要检索的字段,可以通过如下配置设定需要的存储粒度
index_options结合需要设定
norms不需要归一化数据时关闭即可
不需要排序或者聚合分析功能
doc_values设定为false
fielddata设定为false
是否需要专门存储当前字段的数据?
store设定为true,即可存储该字段的原始内容(与_source中的不相关)
一般结合_source的enabled设定为false时使用
ES不擅长处理关系型数据库中的关联关系,比如文章表blog与评论表comment之间通过blog_id关联,在ES中可以通过如下两种手段变相解决
Nested Object
Parent/Child
ES 还提供了类似关系数据库中join的实现方式,使用join数据类型实现
PUT blog_index_parrent_child
{
"mappings":{
"doc":{
"properties":{
"join":{
"type":"join",#指明类型
"relations":{#指明父子关系
"blog":"comment"
}
}
}
}
}
}
#创建父文档
PUT blog_index_parent_child/doc/1
{
"title":"blog",
"join":"blog"
}
#创建子文档
PUT blog_index_parent_child/doc/comment-1?routing=1 #指定routing值,确保父子文档在一个分片上,一般使用父文档id
{
"comment":"comment world",
"join":{
"name":"comment", #指明子类型
"parent":1 #指明父文档id
}
}
常见query语法包括如下几种:
parent_id返回某父文档的子文档
GET blog_index_parent/_search
{
"query":{
"parent_id":{#关键词
"type":"comment",#指明子文档类型
"id":"2" #指明父文档id
}
}
}
GET blog_index_parent/_search
{
"query":{
"has_child":{#关键词
"type":"comment",#指明子文档类型
"query":{
"match":{
"comment":"world"
}
}
}
}
}
GET blog_index_parent/_search
{
"query":{
"has_parent":{
"has_parent":"blog",#指定父文档类型
"query":{#指明父文档查询条件
"match":{
"title":"blog"
}
}
}
}
}
对比 | Nested Object | Parent/Child |
---|---|---|
优点 | 文档存储在一起,因此读取性能高 | 父子文档可以独立更新,互不影响 |
缺点 | 更新父或子文档时需要更新整个文档 | 为了维护join的关系,需要占用部分内存,读取性能较差 |
场景 | 子文档偶尔更新,查询频繁 | 子文档更新频繁 |
指重建所有数据的过程,一般发生在如下情况:
mapping设置变更,比如字段类型变化、分词器字典更新等
index设置变更,比如分片数更改
迁移数据
ES提供了现成的API用于完成该工作
_update_by_query在现有索引上重建
_reindex在其他索引上重建
POST blog_index/_update_by_query?conflicts=proceed #如果遇到版本冲突,覆盖并继续执行
POST blog_index/_update_by_query
{
"script":{ #更新文档的字段值
"source":"ctx._source.likes++",
"lang":"painless"
},
"query":{ #可以更新部分文档
"term":{
"user":"tom"
}
}
}
POST _reindex
{
"source":{
"index":"blog_index"
},
"dest":{
"index":"blog_new_index"
}
}
POST _reindex
{
"conflicts":"proceed",#冲突时覆盖并继续
"source":{
"index":"blog_index",
"query":{
"term":{
"user":"tom"
}
}
},
"dest":{
"index":"blog_new_index"
}
}
数据重建的时间受源索引文档规模的影响,当规模越大时,所需时间越多,此时需要通过设定url参数wait_for_completion为false来异步执行,ES以task来描述此类执行任务
ES提供了Task API来查看任务的执行进度和相关数据
POST blog_index/_update_by_query?wait_for_completion=false #获取task ID
GET _tasks/_qkdskglrfodsm(task ID)
对Mapping进行版本管理
包含在代码或者以专门的文件进行管理,添加好注释,并加入Git等版本管理仓库中,方便回顾
为每个增加一个metadata字段,在其中维护一些文档相关的元数据,方便对数据进行管理
一般字段过多的原因是由于没有高质量的数据建模导致的,比如dynamic设置为true
考虑拆分多个索引来解决问题
字段过多主要由以下坏处:
难于维护,当字段成百上千时,基本很难有人能明确知道每个字段的含义
mapping的信息存储在cluster state里面,过多的字段会导致mapping过大,最终导致更新变慢
通过设置index.mapping.total_fields.limit可以限定索引中最大字段数,默认是1000
可以通过key/value的方式解决字段过多的问题,但并不完美
虽然通过这种方式可以极大地减少field数目,但也有一些明显的坏处
query语句复杂度飙升,且有一些可能无法实现,比如聚合分析相关的
不利于在kibana中做可视化分析
遵照官方建议设置所有的系统参数
参见文档“setup Elasticsearch -> Important System Configuration"
elasticsearch.yml中尽量只写必备的参数,其他可以通过api动态设置的参数都通过api来设定
随着ES版本的升级,很多网络流传的配置参数已经不再支持,因此不要随便复制别人的集群配置参数
cluster.name
node.name
node.master/node.data/node.ingest
network.host建议显示指定为内网ip,不要偷懒直接设为0.0.0.0
discovery.zen.ping.unicast.hosts 设定集群其他节点地址
discovery.zen.minimum_master_nodes一般设定为2
path.data/path.log
动态设定的参数有transient和persistent两种设置,前者在集群重启后会丢失,后者不会,但两种设定都会覆盖elasticsearch.yml中的配置
PUT /_cluster/settings
{
"persistent":{
"discovery.zen.minimum_master_nodes":2
},
"transient":{
"indices.store.throttle.max_bytes_per_sec":"50mb"
}
}
不要超过31GB
预留一半内存给操作系统,用来做文件缓存
具体大小根据该node要存储的数据量来估算,为了保证性能,在内存和数据量间有一个建议的比例
搜索类项目的比例建议在1:16以内
日志类项目的比例建议在1:48~1:96
假设总数据量大小为1TB,3个node,1个副本,那么每个node要存储的数据量为2TB/3=666TB,即700GB左右,做20%的预留空间,每个node要存储大约850GB的数据
如果是搜索类项目,每个node内存大小为850GB/16=53GB,大于31GB。31*16=496,即每个node最多存储496GB数据,所以需要至少5个node
如果是日志类型项目,每个node内存大小为850GB/48=18GB,因此3个节点足够
refresh
translog
flush
segment写入磁盘的过程依然很耗时,可以借助文件系统缓存的特性,先将segment在缓存中创建并开放查询来进一步提升实时性,该过程在es中称为refresh
在refresh之前文档会先存储在一个buffer中,refresh时将buffer中的所有文档清空并生成segment
es默认每一秒执行一次refresh,因此文档的实时性被提高到一秒,这也是es被称为近实时的原因
如果在内存中的segment还没有写入磁盘前发生了宕机,那么其中的文档就无法恢复了,如何解决这个问题?
es引入translog机制。写文档到buffer时,同时将该操作写入translog
translog文件会即时写入磁盘,6.x默认每个请求都会落盘,可以修改为每五秒写一次,这样风险便是丢失5秒内的数据,相关配置为index.translog.*
es启动时会检查translog文件,并从中恢复数据
flush负责将内存中的segment写入磁盘,主要做如下的工作:
将translog写入磁盘
将index buffer清空,其中的文档生成一个新的segment,相当于一个refresh操作
更新commit point并写入磁盘
执行fsync操作,将内存中的segment写入磁盘
删除旧的translog文件
目标是增大写吞吐量-EPS(Events Per Second)越高越好
优化方案
客户端:多线程写,批量写
ES:在高质量数据建模的前提下,主要是在refresh、translog和flush之间做文章
目标为降低refresh的频率
增大refresh_interval,降低实时性,以增大一次refresh的文档数,默认是1秒,设置为-1直接禁止自动refresh
增大index buffer size,参数为indices.memory.index_buffer_size(静态参数,需要设定在配置文件中),默认为10%
目标是降低translog写磁盘的频率,从而提高写效率,但会降低容灾能力
index.tranlog.durability设置为async,index.translog.sync_interval设置需要的大小,比如120s,那么translog会改为每120s写一次磁盘
index.translog.flush_threshold_size默认为512mb,即translog超过该大小时会触发一次flush,那么调大该大小可以避免flush的发生
副本设置为0,写入完毕再增加
合理地设计shard数,并保证shard均匀的分配在所有node上,充分利用所有node的资源
(10+10)/5=4
实际要设置为5个,防止在某个node下线时,分片迁移失败的问题
index.routing.allocation.total_shards_per_node限定每个索引在每个node上可分配的总主副分片数
5个node,某索引有10个分片,1个副本,上述值应该设置为多少?
读性能主要受以下几方面影响:
数据模型是否符合业务模型
数据规模是否过大
索引配置是否优化
查询语句是否优化
高质量的数据建模是优化的基础
将需要通过script脚本动态计算的值提前算好作为字段存到文档中
尽量使得数据模型贴近业务模型
根据不同的数据规模设定不同的sla
上万条数据与上千万条数据性能肯定存在差异
索引配置优化主要包括如下:
根据数据规模设置合理的主分片数,可以通过测试得到最适合的分片数
设置合理的副本数目,不是越多越好
查询语句调优主要有以下几种常见手段:
尽量使用filter上下文,减少算分的场景,由于filter有缓存机制,可以极大提升查询性能
尽量不使用script进行字段计算或者算分排序等
结合profile、explain api分析慢查询语句的症结所在,然后再去优化数据模型
ES的性能基本是线性扩展的,因此我们只要测出一个shard的性能指标,然后根据实际性能需求就算出需要的shard数。比如单shard写入eps是10000,而线上eps需求是50000,那么你需要五个shard(实际还要考虑副本的情况)
测试一个shard的流程如下:
搭建与生产环境相同配置的单节点集群
设定一个单分片零副本的索引
写入实际生产数据进行测试,获取写性能指标
针对数据进行查询请求,获取读性能指标
压测工具可以采用esrally
压测的流程还是比较复杂,可以根据经验来设定。如果是搜索引擎场景,单shard大小不要超过15GB,如果是日志场景,单shard大小不要超过50GB(shard越大,查询性能越低)
此时只要估算出你索引的总数据大小,然后再除以上面的单shard大小也可以得到分片数
公众号:运维开发故事
github:https://github.com/orgs/sunsharing-note/dashboard
博客**:https://www.devopstory.cn**
爱生活,爱运维
我是wanger,《运维开发故事》公众号团队中的一员,这里不仅有硬核的技术干货,还有我们对技术的思考和感悟,欢迎关注我们的公众号,期待和你一起成长!
扫码二维码
关注我,不定期维护优质内容