一文带你了解elasticsearch
elasticsearch
es基本概念
es术语介绍
- 文档Document
- 用户存储在es中的数据文档
- 索引Index
- 由具有相同字段的文档列表组成
- 节点node
- 一个Elasticsearch的运行实例,是集群的构成单元
- 集群Cluster
- 由一个或多个节点组成,对外提供服务
document介绍
- 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:整合所有字段内容到该字段,默认禁用
Index介绍
- 索引中存储具有相同结构的文档(Document)
- 每个索引都有自己的mapping定义,用于定义字段名和类型
- 一个集群可以有多个索引,比如:
- nginx日志存储的时候可以按照日期每天生成一个索引来存储
rest api
- elasticsearch集群对外提供restful api
- rest-representational state transfer
- uri指定资源,如index、document等
- http method指明资源操作类型,如GET、POST、PUT、DELETE等
- 常用两种交互方式
- curl命令行
- Kibana Devtools
索引API
- es有专门的index api,用于创建、更新、删除索引配置等
- 创建索引api如下:
PUT /test_index
- 创建索引api如下:
文档api
- 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创建文档api如下:
- 查询文档
- 指定要查询的文档id
GET /test_index/doc/1
- 搜索所有文档,用到_search,如下:
- 指定要查询的文档id
GET /test_index/doc/_search
{
"query":{
"term":{
"_id":"1"
}
}
}
查询语句,json格式,放在http body中发送到es
批量创建文档api
- 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"}}
- endpoint为_bulk,如下:
批量查询文档API
- es允许一次查询多个文档
- endpoint为_mget,如下:
GET /_mget { "doc":[ { "_index":"test_index", "_type":"doc", "_id":"1" }, { "_index":"test_index", "_type":"doc", "_id":"2" } ] }
- endpoint为_mget,如下:
索引
正排索引与倒排索引
- 正排索引
- 文档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里面称为analysis
分词器
- 分词器是es中专门处理分词的组件,英文为analy,它的组成如下:
- Character Filters
- 针对原始文本进行处理,比如去除HTML特殊标记符
- Tokenizer
- 将原始文本按照一定规则切分为单词
- Token Filters
- 针对tokenizer处理的单词进行再加工,比如转小写、删除或新增等处理
Analyize API
- 针对tokenizer处理的单词进行再加工,比如转小写、删除或新增等处理
- es提供了一个测试分词的api接口,方便验证分词效果,endpoint是_analyze
- 可以直接指定analyzer进行测试
- 可以直接指定索引中的字段进行测试
- 可以自定义分词器进行测试
- Character Filters
- 直接指定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
- Stop Word指语气助词等修饰性的词语,比如the、an、的、这等等
- 其组成如图,特性为:
- 相比Simple Analyzer多了stop Word处理
- Keyword Analyzer
- 其组成如下,特性为:
- 不分词,直接将输入作为一个单词输出
- 其组成如下,特性为:
- Pattern Analyze
- 其组成如下,特性为:
- 通过正则表达式自定义分隔符
- 默认是W+,即非字词的符号作为分隔符
- 其组成如下,特性为:
- language Analyze
- 提供了30+常见语言的分词器
- Arabic,Armenian,basque,bengali,Brazilian,Bulgarian,catAlan,cjk,Czech,Danish,Dutch,English...
中文分词
- 难点
- 中文分词指的是将一个汉字序列切分成一个一个单独的词。在英文中,单词之间是以空格作为自然分界符,汉语中没有一个形式上的分界符
- 上下文不同,分词结果迥异,比如交叉歧义问题,比如下面两种分词都合理
- 乒乓球拍/卖/完了
- 乒乓球/拍卖/完了
- 常用分词系统
- IK
- 实现中英文单词的切分,支持ik_smart、ik_maxword等模式
- 可自定义词库,支持热更新分词词典
- https://github.com/medcl/elasticsearch-analysis-ik
- jieba
- python中最流行的分词系统,支持分词和词性标注
- 支持繁体分词、自定义词典、并行分词等
- https://github.com/singlee/elasticsearch-jieba-plugin
- IK
- 基于自然语言处理的分词系统
- Hanlp
- 由一系列模型与算法组成的Java工具包,目标是普及自然语言处理在生产环境中的应用
- https://github.com/hankcs/HanLP
- THULAC
- THU Lexical Analyzer for Chinese,由清华大学自然原因处理与社会人文计算实验室研制推出的一套中文词法分析工具包,具有中文分词和词性标注功能
- https://github.com/microbun/elasticsearch-thulac-plugin
- Hanlp
自定义分词
- 当自带的分词无法满足需求时,可以自定义分词
- 通过自定义Character Filters、Tokenizer Filter实现
- Character Filters
- 在Tokenizer之前对原始文本进行处理,比如增加、删除或替换字符等
- 自带的如下:
- HTML Strip去除html标签和转换html实体
- Mapping进行字符替换操作
- Pattern Replace进行正则匹配替换
- 会影响后续Tokenizer解析的position和offset信息
- Character Filters测试时可以采用如下api:
POST _analyze { "tokenizer":"keyword", #keyword类型的Tokenizer可以直接看到输出结果 "char_filter":["html_strip"], #指明要使用的char_filter "text":"<p>I'm so <b>happy</b>!</p>" }
- Tokenizer
- 将原始文本按照一定规则且分为单词(term or token)
- 自带的如下:
- standard按照单词进行分割
- letter按照非字符类进行分割
- whitespace按照空格进行分割
- UAX URL Email按照standard分割,但不会分割邮箱和url
- NGram和Edge NGram连词分割
- Path Hierarchy按照文件路径进行分割
- Tokenizer测试时可以采用如下api:
POST _analyze { "tokenizer":"path_hierarchy", "text":"/one/two/three" }
- Token Filters
- 对于Tokenizer输出的单词(term)进行增加、删除、修改等操作
- 自带的如下:
- lowercase将所有term转换为小写
- stop删除stop words
- NGram和Edge NGram连词分割
- Synonym添加近义词的term
- Filter测试时可以采用如下api:
POST _analyze { "text":"a hello world", "tokenizer":"standard", "filter":[ "stop", "lowercase", { "type":"ngram", "min_gram":4, "max_gram":4 } ] }
自定义分词的api
- 自定义分词的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" #指定分词器 } } } } }
- 不指定分词时,默认使用standard
-
查询时分词的指定方式有如下几种:
- 查询时通过analyzer指定分词器
POST test_index/_search { "query":{ "match":{ "message":{ "query":"hello", "analyzer":"standard" } } } }
- 查询时通过analyzer指定分词器
- 通过index mapping设置search_analyzer实现
PUT test_index
{
"mappings":{
"doc":{
"properties":{
"title":{
"type":"text",
"analyzer":"whitespace",
"search_analyzer":"standard"
}
}
}
}
}
- 一般不需要特别指定查询时分词器,直接使用索引时分词器即可,否则会出现无法匹配的情况下
分词使用建议
- 明确字段是否需要分词,不需要分词的字段就将type设置为keyword,可以节省空间和提高写性能
- 善用_analyze API,查看文档的具体分词结果
- 动手测试
Mapping
- 类似数据库中的表结构定义,主要作用如下:
- 定义Index下的字段名(Field Name)
- 定义字段的类型,比如数值型、字符串型、布尔型等
- 定义倒排索引相关的配置,比如是否索引、记录position等。
自定义mapping
- 自定义mapping的api如下所示:
PUT my_index { "mappings":{ "doc":{ "properties":{ "title":{ "type":"text" }, "name":{ "type":"keyword" }, "age":{ "type":"integer" } } } } }
- Mapping中的字段类型一旦设定后,禁止直接修改,原因如下:
- Lucene实现的倒排索引生成后不允许修改
- 重新建立新的索引,然后做reindex操作
- 允许新增字段
- 通过dynamic参数来控制字段的新增
- true(默认)允许自动新增字段
- false不允许自动新增字段,但是文档可以正常写入,但无法对字段进行查询等操作
- strict文档不能写入,报错
copy_to参数
- 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参数
- index
- 控制当前字段是否索引,默认为true,即记录索引,false不记录,即不可搜索
PUT my_index { "mappings":{ "doc":{ "properties":{ "cookie":{ "type":"text", "index":"false" } } } } }
- 控制当前字段是否索引,默认为true,即记录索引,false不记录,即不可搜索
index_options参数
- 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" } } } } }
- 当字段遇到null值时的处理策略,默认为null,即空值,此时es会忽略该值。可以通过设定该值设定字段的默认值
数据类型
- 核心数据类型
- 字符串: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" } } } } } } } }
- 允许对同一个字段采用不同的配置,比如分词,常见例子如对人名实现拼音搜素,只需要在人名中新增一个子字段为pinyin即可
Dynamic Mapping
- es可以自动识别文档字段类型,从而降低用户使用成本
- es是依靠json文档的字段类型来实现自动识别字段类型,支持的类型如下:
JSON类型 | es类型 |
---|---|
null | 忽略 |
Boolean | Boolean |
浮点类型 | float |
整数 | long |
object | object |
array | 有第一个非null值的类型决定 |
string | 匹配为日期则设为date类型(默认开启),匹配为数字的话设为float或long类型(默认关闭),设为text类型,并附带keyword的子字段 |
dynamic日期与数字识别
- 日期的自动识别可以自行配置日期格式,以满足各种需求
- 默认是["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 (eg 1997-07-20T15;30:50 +01:00)
- 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 } } }
- numeric_detection可以开启字符串中数字的自动识别,如下所示:
Dynamic Templates
- 允许根据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匹配路径
- 匹配规则一般有如下几个参数:
自定义mapping的建议
- 自定义Mapping的操作步骤如下:
- 写入一条文档到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":索引的配置
search API
- 查询有两种形式:
- URI search
- 操作简单,方便通过命令行测试
- 仅包含部分查询语法
- Request Body Search
- es提供完备的查询语法Query DSL
- URI search
URI Search
- 通过url query参数来实现搜索,常用参数如下:
- q 指定查询的语句,语法为Query String Syntax
- df q中不指定字段时默认查询的字段,如果不指定es会查询所有字段
- sort 排序
- timeout 指定超时时间,默认不超时
- from,size用于分页
URI Search - Query String Syntax
- 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)
- 布尔操作符
- AND(&&),OR(||),NOT(!)
- name:(tom NOT lee)
- 注意大写,不能小写
- +—分别对应must和must_not
- name:(tom +lee -alfred) #返回一定包含lee,可以包含tom,一定不包含alfred的文档
- 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] 意为age>=1
- age:[* TO 10] 意为age<=10
- 算数符号写法
- age:>=1
- age:(>=1 && <=10) 或者age:(+>=1 +<=10)
- 区间写法,闭区间用[],开区间用{}
- 通配符查询
- ?代表1个字符,*代表0或多个字符
- 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"都会被匹配
Query DSL
- 基于json定义的查询语句,主要包含如下两种类型:
- 字段类查询:
- 如term,match,range等,只针对某一个字段进行查询
- 复合查询
- 如bool查询等,包含一个或多个字段类查询或者复合查询语句
- 字段类查询:
Query DSL 字段类查询
- 字段类查询主要包括以下两类:
- 全文匹配
- 针对text 类型的字段进行全文检索,会对查询语言先进行分词处理,如match,match_phrase等query类型
- 单词匹配
- 不会对查询语句做分词处理,直接去匹配字段的倒排索引,如term.terms,range等query类型
- 全文匹配
Match Query
- 对字段作全文检索,最基本和常用的查询类型,API示例如下:
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参数可以控制需要匹配的单词数
Match Query -流程
首先对查询语句进行分词,分词后分别根据字段的倒排索引进行匹配算分,并会匹配到一个或多个文档,再将匹配到的文档进行汇总得分,根据得分排序返回多个文档
Match Phrase Query
- 对字段做检索,有顺序要求,API示例如下:
GET test_index_index/_search { "query":{ "match_phrase":{ "remote_ip":"171.22.12.14" } } }
- 通过slop参数可以控制单词间的间隔
Query String Query
-
类似于URI Search中的q参数查询
GET test_index_index/_search { "query":{ "query_string":{ "default_field":"remote_ip", "query":"171.22.12.14" } } }
Simple Query String Query
- 类似Query String,但是会忽略错误的查询语法,并且仅支持部分查询语法
-
其常用的逻辑符号如下,不能使用AND、OR、NOT等关键词:
- +代指AND
- |代指OR
- -代指NOT
常用API如下所示:GET test_index_index/_search {
"query":{
"simple_query_string":{
"fields":["remote_ip"],
"query":"alfred +way"
}
}
}
Term Query
-
将查询语句作为整个单词进行查询,即不对查询语句做分词处理,如下所示:
GET test_index_index/_search { "query":{ "term":{ "remote_ip":"171.22.12.14" } } }
Range Query
-
范围查询主要针对数值和日期类型,如下所示:
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之后的默认模型
TF/IDF模型
- 可以通过explain参数来查看具体的计算方法,但要注意:
- es的算分是按照shard进行的,即shard的分数计算是相互独立的,所以在使用explain的时候注意分片数
- 可以通过设置索引的分片数为1来避免这个问题
GET test_index_index/_search { "explain": true, "query":{ "match":{ "remote_ip":"171.22.12.14" } } }
BM25模型
- BM25模型中BM指Best Match,25指迭代了25次才计算方法,是针对TF/IDF的一个优化。
- BM25相比TF/IDF的一大优化是降低了tf在过大时的权重
Query DSL 复合查询
- 复合查询是指包含字段类查询或符合查询的类型,主要包括以下几类:
- constant_score query
- bool query
- dis_max query
- function_score query
- boosting query
Constant Score 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" } } ] } }
- 多用于结合bool查询实现自定义部分
Bool Query
- 布尔查询由一个或多个布尔子句组成,主要包含如下四个:
子句 | 内容 |
---|---|
filter | 只过滤符合条件的文档,不计算相关性得分 |
must | 文档必须符合must中的所有条件,会影响相关性得分 |
must_not | 文档必须不符合must_not中的所有条件 |
should | 文档可以符合should中的条件,会影响相关性得分 |
- Bool查询的API如下所示
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或者Filter上下文时,es执行的结果会不同,对比如下:
上下文类型 | 执行类型 | 使用方式 |
---|---|---|
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" }}}
]
}
}
}
Count API
- 只返回符合条件的文档数,endpoint为_count,不返回文档内容
API示例如下:GET test_index_index/_count { "query":{ "match":{ "response":200 } } }
响应如下:
{ "count": 1, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 } }
Source Filter
- 过滤返回结果中_source中的字段,主要有如下几种方式:
不返回_sourceGET test_index/_search { "_source":false }
返回部分字段
GET test_index/_search { "_source":["response","name"] } GET test_index/_search { "_source":{ "includes":"*i*", "excludes":"remote_ip" } }
es分布式
分布式特性
- es支持集群模式,是一个分布式系统,其好处主要有两个:
- 增大系统容量,如内存、磁盘,使得es集群可以支持PB级的数据
- 提高系统可用性,即使部分节点停止服务,整个集群依然可以正常服务
- es集群由多个es实例组成
- 不同集群通过集群名字来区分,可通过cluster.name进行修改
- 每个es实例本质上是一个JVM进程,且有自己的名字,通过node.name进行修改
Master Node
- 可以修改cluster state的节点称为master节点,一个集群只能有一个
- cluster state存储在每个节点上,master维护最新版本并同步给其他节点
- master节点是通过集群中所有节点选举产生的,可以被选举的节点称为master-eligible节点,相关配置如下:
- node.master:true
Coordinating Node
- 处理请求的节点即为coordinating节点,该节点为所有节点的默认角色,不能取消
- 路由请求到正确的节点处理,比如创建索引的请求到master节点
Data Node
- 存储数据的节点即为data节点,默认节点都是data类型,相关配置如下:
- node.data:true
副本与分片
- 如何将数据分布于所有节点上?
- 引入分片解决问题
- 分片是es支持PB级数据的基石
- 分片存储了部分数据,可以分部于任意分片上
- 分片数在索引创建时指定且后续不允许再更改,默认为5个
- 分片有主分片和副本分片之分,以实现数据的高可用
- 副本分片的数据由主分片同步,可以有多个,从而提高读取的吞吐量
- 分片数的设定很重要,需要提前规划好
- 过小会导致后续无法通过增加节点实现水平扩容
- 过大会导致一个节点上分布过多过多分片,造成资源浪费,同时会影响查询性能
故障转移
- node1所在机器宕机导致服务终止,此时集群会如何处理?
- 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即可避免脑裂
shard详解
倒排索引的不可变更
- 倒排索引一旦生成,不能更改
- 好处如下:
- 不用考虑并发写文件的问题,杜绝了锁机制带来的性能问题
- 由于文件不再更改,可以充分利用文件系统缓存,只需载入一次,只要内存足够,对该文件的读取都会从内存读取,性能高
- 利于生成缓存数据
- 利于对文件进行压缩存储,节省磁盘和内存存储空间
- 坏处为需要写入新文档时,必须重新构建倒排索引文件,然后替换老文件后,新文档才能被检索,导致文档实时性差
文档搜索实时性
- 解决方案是新文档直接生成新的倒排索引文件,查询的时候同时查询所有的倒排文件,然后做结果的汇总即可
- Lucene便是采用了这种方案,它构建的单个倒排索引称为segment,合在一起称为index,与ES中的index概念不同。ES中的一个shard对应一个Lucene index
- Lucene会有一个专门的文件来记录所有的segment信息,称为commit point
文档搜索实时性-refresh
- 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
文档搜索实时性-translog
- 如果在内存中的segment还没有写入磁盘前发生了宕机,name其中的文档就无法恢复了。
- es引入translog机制。写入文档到buffer时,同时将该操作写入translog
- translog文件会即时写入磁盘(fsync),6.x默认每个请求都会落盘,可以修改为每五秒写一次,这样风险便是丢失5秒内的数据,相关配置为index.translog.*
- es启动时会检查translog文件,并从中恢复数据
文档搜索实时性-flush
- 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 Merging
- 随着segment的增多,由于一次查询的segment数增多,查询速度会变慢
- es会定时在后台进行segment merge的操作,减少segment的数量
- 通过force_merge api可以手动强制做segment merge的操作
search 运行机制
- search 执行的时候实际分两个步骤运作的
- Query阶段
- fetch阶段
- Query-Then-Fetch
Query阶段
- node3在接收到用户的search请求后,会先进行Query阶段
- node3在6个主副分片中随机选择三个分片,发送search request
- 被选中的3个分片会分别进行查询并排序,返回from+size个文档Id和排序值
- node3整合3个分片返回的from+size个文档Id,根据排序值排序后选取from到from+size的文档Id
Fetch阶段
- node3根据Query阶段获取的文档Id列表去对应的shard上获取文档详情数据
- 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
排序
- es默认会采用相关性算分排序,用户可以通过设定sorting参数来自行设定排序规则
GET test_search_index/_search { "sort":[ #关键字 { "birth":"desc" }, { "_score":"desc" }, { "_doc":"desc" } ] }
- 按照字符串排序比较特殊,因为es有text和keyword两种类型,针对text类型排序,如下所示:
GET test_search_index/_search { "sort":{ "username":"desc" } } #会产生报错
- 按照字符串排序比较特殊,因为es有text和keyword两种类型,针对text类型排序,如下所示:
- 针对keyword类型排序,可以返回预期结果
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 vs DocValues
对比 | fielddata | DocValues |
---|---|---|
创建时机 | 搜索时即时创建 | 索引时创建,与倒排索引创建时机一致 |
创建位置 | JVM Heap | 磁盘 |
优点 | 不会占用额外的磁盘空间 | 不会占用Heap内存 |
缺点 | 文档过多时,即时创建会花过多时间,占用过多Heap内存 | 减慢索引的速度,占用额外的磁盘资源 |
Fielddata
- Fielddata默认是关闭的,可以通过如下api开启:
- 此时字符串是按照分词后的term排序,往往结果很难符合预期
- 一般是在对分词做聚合分析的时候开启
PUT test_search_index/_mapping/doc { "properties":{ "username":{ "type":"text", "fileddata":"true" } } }
Doc Values
- Doc Values默认是启用的,可以在创建索引的时候关闭:
- 如果后面要再开启doc values,需要做reindex操作
PUT test_doc_value/ { "mappings":{ "doc":{ "properties": } } }
- 如果后面要再开启doc values,需要做reindex操作
docvalue_fields
- 可以通过该字段获取fielddata或者doc values中存储的内容
GET test_search_index/_search { "docvalue_fields":[ "username", "username.keyword", "age" ] }
分页与遍历
From/Size
- 最常用的分页方案
- from指明开始位置
- size指明获取总数
- 深度分页是一个经典问题:在数据分片存储的情况下如何获取前1000个文档?
- 获取从990-1000的文档时,会在每个分片上都先获取1000个文档,然后再由coordinating node局和所有分片的结果后再排序选取前1000个文档
- 页数越深,处理文档越多,占用内存越多,耗时越长。尽量避免深度分页,es通过index.max_result_window限定最多到10000条数据
total_page=(total+page_size-1)/page_size total为文档总数
Scroll
- 遍历文档集的api,以快照的方式来避免深度分页的问题
- 不能用来做实时搜索,因为数据不是实时的,是以快照的方式做的
- 尽量不要使用复杂的sort条件,使用_doc最高效
- 使用稍嫌复杂
- 第一步需要发起1个scroll search,如下所示:
- es在收到该请求后会根据查询条件创建文档id合计的快照
GET test_search_index/_search?scrol=5m #该scroll快照有效时间 { "size":1 #指明每次scroll返回的文档数 }
- es在收到该请求后会根据查询条件创建文档id合计的快照
- 第二步调用scroll search的api,获取文档集合,如下所示:
- 不断迭代调用直到返回hits.hits数组为空时停止
post _search/scroll { "scroll":"5m", #指明有效时间 "scroll_id":"..." #上一步返回的id }
- 不断迭代调用直到返回hits.hits数组为空时停止
- 过多的scroll调用会占用大量内存,可以通过clear api删除过多的scroll快照:
DELETE /_search/scroll { "scroll_id":[ "DXFZAD....", "DESGRHRH..." ] } DELETE /_search/scroll/_all
search_after
- 避免深度分页的性能问题,提供实时的下一页文档获取功能
- 缺点是不能使用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同级 "<aggregation_name>":{#定义聚合名称 "<aggregation_type>":{#定义聚合类型 <aggregation_body> }, [,"aggs":{[<sub_aggregation>]+}]? #子查询 } [,"<aggregation_name_2>":{...}]* #可以包含多个聚合分析 } }
聚合分析分类
- 为了便于理解,es将聚合分析主要分为如下四类
- Bucket,分桶类型,类似sql中的group by语法
- Metric,指标分析模型,如计算最大值、最小值、平均值等等
- pipline,管道分析类型,基于上一级的聚合分析结果进行再分析
- Matrix,矩阵分析类型
Metric聚合分析
- 主要分如下两类:
- 单值分析,只输出过一个分析结果
- min,max,avg,sum
- cardinality
- 多值分析,输出多个分析结果
- stats,extended stats
- percentile,percentile rank
- top hits
- 单值分析,只输出过一个分析结果
- 返回数值类字段的最小值
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" } } } }
- cardinality,意为集合的势,或者基数,是指不同数值的个数,类似sql中的distinct count概念
GET test_search_index/_search { "size":0, "aggs":{ "count_of_job":{ "cardinality":{ #关键词 "field":"job.keyword" } } } }
- 返回一系列数值类型的统计值,包含min、max、avg、sum和count
GET test_search_index/_search { "size":0, "aggs":{ "stats_age":{ "stats":{ "field":"age" } } } }
- 对stats的扩展,包含了更多的统计数据,如方差、标准差等
GET test_search_index/_search { "size":0, "aggs":{ "stats_age":{ "extended_status":{ "field":"age" } } } }
- 百分位数统计percentile
GET test_search_index/_search { "size":0, "aggs":{ "per_age":{ "percentiles":{ #关键词 "field":"salary" } } } }
- top hits一般用于分桶后获取该桶内最匹配的顶部文档列表,即详情数据
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的分桶策略,常见的Bucket聚合分析如下:
- Terms
- Range
- Date Range
- Histogram
- Date Histogram
Terms
- 该分桶策略最简单,直接按照term来分桶,如果是text类型,则按照分词后的结果分桶
GET test_search_index/_search { "size":0, "aggs":{ "jobs":{ "terms":{ #关键词 "field":"job.keyword",#指明term字段 "size":5 #指定返回数目 } } } }
Range
- 通过指定数值的范围来设定分桶规则
GET test_search_index/_search { "size":0, "aggs":{ "salary_range":{ "range":{ #关键词 "field":"salary", "ranges":[ #指定每个range的范围 { "to":1000 }, { "from":1000, "to":2000 } ] } } } }
Date Range
- 通过指定日期的范围来设定分桶规则
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"
}
]
}
}
}
}
historgram
- 直方图,以固定间隔的策略来分隔数据
GET test_search_index/_search
{
"size":0,
"aggs":{
"salary_hist":{
"histogram":{ #关键词
"field":"salary",
"interval":5000, #指定间隔大小
"extended_bounds":{ #指定数据范围
"min":0,
"max":40000
}
}
}
}
}
Date Historgram
- 针对日期的直方图或者柱形图,是时序分析中常用的聚合分析类型
Get test_search_index/_search { "size":0, "aggs":{ "by_year":{ "date_historgram":{ #关键词 "field":"birth", "interval":"year", #指定间隔大小 "format":"yyyy" #指定日期格式化 } } } }
bucket和metric聚合分析
bucket+metric聚合分析
- 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" } } } } } }
pipeline聚合分析
- 针对聚合分析的结果再次进行聚合分析,而且支持链式调用,可以回答如下问题:
- 订单月平均销售额是多少?
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的分析结果会输出到原结果中,根据输出位置的不同,分为以下两类:
- Parent 结果内嵌到现有的聚合分析结果中
- Derivative
- Moving Average
- Cumulative Sum
- Sibling 结果与现有聚合分析结果同级
- Max/Min/Avg/Sum Bucket
- Stats/Extended Stats Bucket
- Percentiles Bucket
- Parent 结果内嵌到现有的聚合分析结果中
Pipeline 聚合分析Sibling-Min Bucket
- 找出所有Bucket中值最小的的Bucket名称和值
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" } } } }
Pipeline聚合分析 Sibling-Max Bucket
- 找出所有Bucket中值最大的Bucket名称和值
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" } } } }
Pipeline 聚合分析Parent-Derivative
- 计算Bucket值的导数
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" } } } } } }
Pipeline 聚合分析Parent-Moving Average
- 计算Bucket值的移动平均值
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 } } } }
作用范围-filter
- 为某个聚合分析设定过滤条件,从而在不更改整体语句的情况下修改了作用范围
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" } } } }
作用范围 - post-filter
- 作用于文档过滤但在聚合分析后生效
GET test_search_index/_search { "aggs":{ "jobs":{ "terms":{ "field":"job.keyword" } } }, "post_filter":{#过滤条件 "match":{ "job.keyword":"java engineer" } } }
作用范围-global
- 无视query过滤条件,基于全部文档进行分析
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" } ] } } } }
数据精准度问题
terms不准确的原因
- 数据分散在多shard上,coordinating node无法得悉数据全貌
terms不准确的解决方法
- 设置shard数为1,消除数据分散的问题,但无法承载大数据量
- 合理设置shard_size的大小,即每次从shard上额外多获取数据,以提升准确度
GET test_search_index/_search { "size":0, "aggs":{ "jobs":{ "terms":{ "field":"job.keyword", "size":1, "shard_size":10 } } } }
shard_size大小的设定方法
- 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等
- 第三范式
ES中的数据建模
- ES是基于Lucene以倒排索引为基础实现的存储体系,不遵循关系型数据库中的范式约定
Mapping字段的相关配置
- 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
- 是否自动识别日期类型
Mapping字段属性的设定流程
- 是何种类型
- 是否需要检索
- 是否需要排序和聚合分析
- 是否需要另行存储
是何种类型
- 字符串类型
- 需要分词则设定为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" } } } } } }
关联关系处理之Parent/Child
#创建父文档
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 } } }
- has_child返回包含某子文档的父文档
GET blog_index_parent/_search { "query":{ "has_child":{#关键词 "type":"comment",#指明子文档类型 "query":{ "match":{ "comment":"world" } } } } }
- has_parent返回某父文档的子文档
GET blog_index_parent/_search { "query":{ "has_parent":{ "has_parent":"blog",#指定父文档类型 "query":{#指明父文档查询条件 "match":{ "title":"blog" } } } } }
- parent_id返回某父文档的子文档
Nested Object vs Parent/Child
对比 | Nested Object | Parent/Child |
---|---|---|
优点 | 文档存储在一起,因此读取性能高 | 父子文档可以独立更新,互不影响 |
缺点 | 更新父或子文档时需要更新整个文档 | 为了维护join的关系,需要占用部分内存,读取性能较差 |
场景 | 子文档偶尔更新,查询频繁 | 子文档更新频繁 |
Reindex
- 指重建所有数据的过程,一般发生在如下情况:
- mapping设置变更,比如字段类型变化、分词器字典更新等
- index设置变更,比如分片数更改
- 迁移数据
- ES提供了现成的API用于完成该工作
- _update_by_query在现有索引上重建
- _reindex在其他索引上重建
Reindex- _update_by_query
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"
}
}
}
Reindex - _reindex
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"
}
}
Reindex - Task
- 数据重建的时间受源索引文档规模的影响,当规模越大时,所需时间越多,此时需要通过设定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的方式解决字段过多的问题,但并不完美
Key/Value方式详解
- 虽然通过这种方式可以极大地减少field数目,但也有一些明显的坏处
- query语句复杂度飙升,且有一些可能无法实现,比如聚合分析相关的
- 不利于在kibana中做可视化分析
集群调优建议
生产环境部署建议
- 遵照官方建议设置所有的系统参数
- 参见文档“setup Elasticsearch -> Important System Configuration"
ES设置尽量简洁
- elasticsearch.yml中尽量只写必备的参数,其他可以通过api动态设置的参数都通过api来设定
- 随着ES版本的升级,很多网络流传的配置参数已经不再支持,因此不要随便复制别人的集群配置参数
elasticsearch.yml中建议设定的基本参数
- 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" } }
jvm内存设定
- 不要超过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
ES写数据 - refresh
- segment写入磁盘的过程依然很耗时,可以借助文件系统缓存的特性,先将segment在缓存中创建并开放查询来进一步提升实时性,该过程在es中称为refresh
- 在refresh之前文档会先存储在一个buffer中,refresh时将buffer中的所有文档清空并生成segment
- es默认每一秒执行一次refresh,因此文档的实时性被提高到一秒,这也是es被称为近实时的原因
ES写数据 - translog
- 如果在内存中的segment还没有写入磁盘前发生了宕机,那么其中的文档就无法恢复了,如何解决这个问题?
- es引入translog机制。写文档到buffer时,同时将该操作写入translog
- translog文件会即时写入磁盘,6.x默认每个请求都会落盘,可以修改为每五秒写一次,这样风险便是丢失5秒内的数据,相关配置为index.translog.*
- es启动时会检查translog文件,并从中恢复数据
ES写数据 - flush
- flush负责将内存中的segment写入磁盘,主要做如下的工作:
- 将translog写入磁盘
- 将index buffer清空,其中的文档生成一个新的segment,相当于一个refresh操作
- 更新commit point并写入磁盘
- 执行fsync操作,将内存中的segment写入磁盘
- 删除旧的translog文件
写性能优化
- 目标是增大写吞吐量-EPS(Events Per Second)越高越好
- 优化方案
- 客户端:多线程写,批量写
- ES:在高质量数据建模的前提下,主要是在refresh、translog和flush之间做文章
写性能优化 - refresh
- 目标为降低refresh的频率
- 增大refresh_interval,降低实时性,以增大一次refresh的文档数,默认是1秒,设置为-1直接禁止自动refresh
- 增大index buffer size,参数为indices.memory.index_buffer_size(静态参数,需要设定在配置文件中),默认为10%
写性能优化 - translog
- 目标是降低translog写磁盘的频率,从而提高写效率,但会降低容灾能力
- index.tranlog.durability设置为async,index.translog.sync_interval设置需要的大小,比如120s,那么translog会改为每120s写一次磁盘
- index.translog.flush_threshold_size默认为512mb,即translog超过该大小时会触发一次flush,那么调大该大小可以避免flush的发生
写性能优化 - flush
- 目标为降低flush的次数,在6.x可优化的点不多,多为es自动完成
写性能优化 - 其他
- 副本设置为0,写入完毕再增加
- 合理地设计shard数,并保证shard均匀的分配在所有node上,充分利用所有node的资源
- index.routing.allocation.total_shards_per_node限定每个索引在每个node上可分配的总主副分片数
- 5个node,某索引有10个分片,1个副本,上述值应该设置为多少?
- (10+10)/5=4
- 实际要设置为5个,防止在某个node下线时,分片迁移失败的问题
读性能优化
- 读性能主要受以下几方面影响:
- 数据模型是否符合业务模型
- 数据规模是否过大
- 索引配置是否优化
- 查询语句是否优化
读性能优化 - 数据建模
- 高质量的数据建模是优化的基础
- 将需要通过script脚本动态计算的值提前算好作为字段存到文档中
- 尽量使得数据模型贴近业务模型
- 根据不同的数据规模设定不同的sla
- 上万条数据与上千万条数据性能肯定存在差异
读性能优化 - 索引配置调优
- 索引配置优化主要包括如下:
- 根据数据规模设置合理的主分片数,可以通过测试得到最适合的分片数
- 设置合理的副本数目,不是越多越好
读性能优化 - 查询语句调优
- 查询语句调优主要有以下几种常见手段:
- 尽量使用filter上下文,减少算分的场景,由于filter有缓存机制,可以极大提升查询性能
- 尽量不使用script进行字段计算或者算分排序等
- 结合profile、explain api分析慢查询语句的症结所在,然后再去优化数据模型
如何设定shard数
- ES的性能基本是线性扩展的,因此我们只要测出一个shard的性能指标,然后根据实际性能需求就算出需要的shard数。比如单shard写入eps是10000,而线上eps需求是50000,那么你需要五个shard(实际还要考虑副本的情况)
- 测试一个shard的流程如下:
- 搭建与生产环境相同配置的单节点集群
- 设定一个单分片零副本的索引
- 写入实际生产数据进行测试,获取写性能指标
- 针对数据进行查询请求,获取读性能指标
- 压测工具可以采用esrally
- 压测的流程还是比较复杂,可以根据经验来设定。如果是搜索引擎场景,单shard大小不要超过15GB,如果是日志场景,单shard大小不要超过50GB(shard越大,查询性能越低)
- 此时只要估算出你索引的总数据大小,然后再除以上面的单shard大小也可以得到分片数