ES和传统数据库的对比
首先下载ElasticSearch包
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wChCMAhW-1653812907262)(ES.assets\image-20210704235849426.png)]
然后进行解压
tar -zxvf elasticsearch-7.9.0-linux-x86_64.tar.gz
修改配置文件
# 现在elasticsearch-7.9.0目录下创建data和logs文件夹,如果已经存在则无需创建
# 进入config文件夹
vim elasticsearch.yml
# 修改内容包括
cluster.name: elasticsearch
node.name: es-node0
path.data: /usr/local/elasticsearch-7.6.2/data
path.logs: /usr/local/elasticsearch-7.6.2/logs
http.port: 9200
network.host: 0.0.0.0
cluster.initial_master_nodes: ["es-node0"]
#然后再来修改另一个文件夹
vim jvm.options
#需要把运行内存从1G改为128m,本地运行不需要这么大
-Xms128m
-Xmx128m
创建用户
ES启动不能以root用户来启动
useradd user-es
# 授权
chown -R user-es:user-es /usr/local/software/elasticsearch-7.9.0
修改系统限制
vi /etc/security/limits.conf
vi /etc/sysctl.conf
保存退出后可通过下面命令进行刷新
sysctl -p
elasticsearch提供了9300、9200两个端口,一个是共有的、一个是私有的
通过./elasticsearch启动
通过http://ip:9200/访问
启动的一些命令
./elasticsearch -d # 后台启动
jps
kill '进程号'
ps -ef | grep elasticsearch
进行解压
tar -zxvf kibana-7.9.0-linux-x86_64.tar.gz
修改配置文件
vim config/kibana.yml
# 修改内容
server.port: 5601
server.host: "0.0.0.0"
elasticsearch.url: "http://192.168.202.128:9200"
kibana.index: ".kibana"
通过http://ip:5601进行访问
# 查看防火墙装状态
systemctl status firewalld
# 关闭防火墙
systemctl stop firewalld
# 永久关闭防火墙
systemctl disable firewalld
# 重启防火墙
systemctl enable firewalld
curl -H "Content-Type: application/json" -XPOST "{ip}:{port}/bank/_bulk?pretty&refresh" --data-binary "@/{path}/accounts.json"
其中accounts.json为导入的数据文件,下面为其中一条数据例子
{"create":{}}
{ "process": { "parent": { "name": "powershell.exe", "entity_id": "{42FC7E13-C11D-5C05-0000-0010C6E90401}", "executable": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" }, "name": "cmd.exe", "pid": 2012, "entity_id": "{42FC7E13-CB3E-5C05-0000-0010A0125101}", "command_line": "\"C:\\WINDOWS\\system32\\cmd.exe\" /c \"for /R c: %%f in (*.docx) do copy %%f c:\\temp\\\"", "executable": "C:\\Windows\\System32\\cmd.exe", "ppid": 7036 }, "logon_id": 217055, "@timestamp": 131883571822010000, "event": { "category": "process", "type": "creation" }, "user": { "full_name": "bob", "domain": "ART-DESKTOP", "id": "ART-DESKTOP\\bob" } }
查看索引状态
curl "{ip}:{port}/_cat/indices?v=true" | grep {index的name}
GET /bank/_search
{
"query": { "match_all": {} },
"sort": [
{ "account_number": "asc" }
]
}
查询结果
返回字段解释
GET /bank/_search
{
"query": { "match_all": {} },
"sort": [
{ "account_number": "asc" }
],
"from": 10,
"size": 10
}
从第10条开始往后查询10条
GET /bank/_search
{
"query": { "match": { "address": "mill lane" } }
}
在字段中搜索特定字词,可以使用match,上面就是查询address字段中包含mill或lane的数据
TIP:由于ES底层是按照分词索引的,所以上述查询结果是address字段中包含mill或者lane的数据
和上面的match相反,如果希望查询的条件是address字段中包含mill lane,则可以使用match_phrase
GET /bank/_search
{
"query": { "match_phrase": { "address": "mill lane" } }
}
使用bool可以组合多个查询条件
例如:搜素40岁客户的账户,但不包括男性
GET /bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "age": "40" } }
],
"must_not": [
{ "match": { "sex": "男" } }
]
}
}
}
TIP:must,should,must_not,filter都是bool查询的子句
在bool中可以同时具备query/must 和filter
著作权归https://pdai.tech所有。
链接:https://www.pdai.tech/md/db/nosql-es/elasticsearch-x-usage.html
GET /bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"state": "ND"
}
}
],
"filter": [
{
"term": {
"age": "40"
}
},
{
"range": {
"balance": {
"gte": 20000,
"lte": 30000
}
}
}
]
}
}
}
那么query和filter的区别是什么?
query上下文的条件是用来给文档打分的,匹配越好_source越高,而filter的条件只产生两种结果:符合和不符合,后者被过滤掉
ES的Aggregation(聚合运算),其实也就是SQL中的group by
比如统计出account每个州的统计数量,使用aggs关键字对state字段聚合,被聚合的字段无需分词统计,所以使用state.keywork对整个字段统计
GET /bank/_search
{
"size": 0,
"aggs": {
"group_by_state": {
"terms": {
"field": "state.keyword"
}
}
}
}
结果
当size为0时,代表不会返回具体数据,所以hits为空,而doc_count表示每个bucket中每个州的数据条数
ES可以处理聚合条件的嵌套
比如,计算每个州的平均结余,设计的是在对state分组的基础上,嵌套计算avg(balance):
GET /bank/_search
{
"size": 0,
"aggs": {
"group_by_state": {
"terms": {
"field": "state.keyword"
},
"aggs": {
"average_balance": {
"avg": {
"field": "balance"
}
}
}
}
}
}
结果
GET /bank/_search
{
"size": 0,
"aggs": {
"group_by_state": {
"terms": {
"field": "state.keyword",
"order": {
"average_balance": "desc"
}
},
"aggs": {
"average_balance": {
"avg": {
"field": "balance"
}
}
}
}
}
}
在增加文档时,如果ES还没有该文档所对应的index,那么就会动态创建一个index,但如果我们需要对这个建立索引的过程做更多的控制,比如想要确保索引有数量适中的主分片,并且在我们索引任何数据之前,分析器和映射器已经被建立好了,这时有如下两种方案,禁止自动创建索引/手动创建索引
可以在config/elasticsearch.yml的每个节点下添加下面的配置
action.auto_create_index: false
PUT /my_index
{
"settings": { ... any settings ... },
"mappings": {
"properties": { ... any properties ... }
}
}
创建一个user索引text-index-users,其中包含三个属性,name,age,remarks,存储在一个分片一个副本上
PUT /text-index-user
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"name":{
"type": "text",
"fields": {
"keyword":{
"type":"keyword",
"ignore_above":256
}
}
},
"age":{
"type": "long"
},
"remarks":{
"type": "text"
}
}
}
}
可以通过该查看索引状态
curl 'localhost:9200/_cat/indices?v' | grep user
索引的状态是yellow的,因为测试的环境是单点环境,无法创建副本,但是在上述number_of_replicas
配置中设置了副本数是1; 所以在这个时候我们需要修改索引的配置
现在来修改副本数
PUT /text-index-user/_settings
{
"settings":{
"number_of_replicas":0
}
}
再次查看索引状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-snuVPNbg-1653812907266)(ES.assets\image-20210708003122794.png)]
关闭索引
关闭索引后就不能进行读写操作,只能显示元数据信息
POST /text-index-user/_close
打开索引
打开索引后就又可以进行读写操作了
POST /text-index-user/_open
DELETE /test-index-users
index template是啥?
索引模板其实就是一种告诉ES在创建索引时如何配置索引的方法
使用方法
在创建索引之前可以先配置模板,这样在创建索引(手动创建索引或通过对文档建立索引)时,模板设置将用作创建索引的基础
模板类型
分为两种:索引模板和组件模板
索引模板中的优先级
内置索引模板
ES具有内置索引模板,每个模板的优先级为100,适用以下索引模式
logs-*-*
metrics-*-*
synthetics-*-*
创建两个索引组件模板:
PUT _component_template/component_template1
{
"template": {
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
}
}
}
}
}
PUT _component_template/runtime_component_template
{
"template": {
"mappings": {
"runtime": {
"day_of_week": {
"type": "keyword",
"script": {
"source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
}
}
}
}
}
}
创建使用组件模板的索引模板
PUT _index_template/template_1
{
"index_patterns": ["bar*"],
"template": {
"settings": {
"number_of_shards": 1
},
"mappings": {
"_source": {
"enabled": true
},
"properties": {
"host_name": {
"type": "keyword"
},
"created_at": {
"type": "date",
"format": "EEE MMM dd HH:mm:ss Z yyyy"
}
}
},
"aliases": {
"mydata": { }
}
},
"priority": 500,
"composed_of": ["component_template1", "runtime_component_template"],
"version": 3,
"_meta": {
"description": "my custom"
}
}
创建一个匹配bar* 的索引bar-test
PUT /bar-test
然后获取mapping
GET /bar-test/_mapping
复合查询,也就是多种条件组合的查询,在ES中提供了5种符合查询,bool query(布尔查询),boosting query(提高查询),constans_source(固定分数查询),dis_max(最佳匹配查询),function_score(函数查询)
通过布尔逻辑将较小的查询组合成较大的查询
概念特点:
bool查询包含四种操作符,分别是must,should,must_not,filter,他们都是一种数组,数组里面是对应的判断条件
案例
POST _search
{
"query": {
"bool" : {
"must" : {
"term" : { "user.id" : "kimchy" }
},
"filter": {
"term" : { "tags" : "production" }
},
"must_not" : {
"range" : {
"age" : { "gte" : 10, "lte" : 20 }
}
},
"should" : [
{ "term" : { "tags" : "env1" } },
{ "term" : { "tags" : "deployed" } }
],
"minimum_should_match" : 1,
"boost" : 1.0
}
}
}
tip:在filter元素下指定的查询对评分没有影响,评分返回为0,分数仅受已指定查询的影响
GET _search
{
"query": {
"bool": {
"filter": {
"term": {
"status": "active"
}
}
}
}
}
tip:该案例查询为所有文档分配0分,因为没有指定评分查询
GET _search
{
"query": {
"bool": {
"must": {
"match_all": {}
},
"filter": {
"term": {
"status": "active"
}
}
}
}
}
tip:该案例查询具有match_all查询,该查询为所有文档指定1.0分
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { "name.first": { "query": "shay", "_name": "first" } } },
{ "match": { "name.last": { "query": "banon", "_name": "last" } } }
],
"filter": {
"terms": {
"name.last": [ "banon", "kimchy" ],
"_name": "test"
}
}
}
}
}
tip:每个query条件都有一个_name属性,可以用来追踪搜索出的数据到底match了哪个条件
不同于bool查询,bool查询只要一个子查询条件不匹配,那么搜索的数据就不会出现,而boosting query则是降低显示的权重/优先级(即score)
比如搜索逻辑是name = ‘apple’ and type = ‘fruit’,对于只满足部分条件的数据,不是不显示,而是降低显示的优先级(即score)
GET /test-dsl-boosting/_search
{
"query": {
"boosting": {
"positive": {
"term": {
"content": {
"value": "apple"
}
}
},
"negative": {
"term": {
"content": {
"value": "pie"
}
}
},
"negative_boost": 0.5
}
}
}
查询某个条件时,固定的返回指定的score;显然当不需要计算score时,只需filter条件即可,以为filter 会忽略score
GET /test-dsl-constant/_search
{
"query": {
"constant_score": {
"filter": {
"term": { "content": "apple" }
},
"boost": 1.2
}
}
}
tip:此时查询返回的数据的score就都是1.2
分离最大化查询(Dijunction Max Query)指的是:将任何与任意查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回
案例
用户输入词组 “Brown fox” 然后点击搜索按钮。事先,我们并不知道用户的搜索项是会在 title 还是在 body 字段中被找到,但是,用户很有可能是想搜索相关的词组。用肉眼判断,文档 2 的匹配度更高,因为它同时包括要查找的两个词:
GET /test-dsl-dis-max/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
可以通过这样查询去看每个词的命中分数
不使用bool查询,可以使用dis_max即分离最大化查询,分离的意思是或(or),这与可以把结合(conjunction)理解成与(and)相对应,分离最大化查询指的的:将任何与任意查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回
GET /test-dsl-dis-max/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
],
"tie_breaker": 0
}
}
}
dis_max条件的计算分数
分数 = 第一匹配条件+ tie_breaker * 第二个匹配的条件分数…
GET /test-dsl-dis-max/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
],
"tie_breaker": 0
}
}
}
这样就通过dix_max将doc2置前了,如果这里缺省tie_breaker字段的话,默认就是0,还可以设置它的比例(在0到1之间)来控制排名(值为1时和should查询是一致的)
简而言之,其实就是自定义function的方式来计算_score
案例
random_score为例
GET /_search
{
"query": {
"function_score": {
"query": {"match_all": {}},
"boost": "5",
"random_score": {},
"boost_mode": "multiply"
}
}
}
还可以使用function的组合(functions)
GET /_search
{
"query": {
"function_score": {
"query": {"match_all": {}},
"boost": "5",
"functions": [
{
"filter": {"match":{"test": "bar"}},
"random_score": {},
"weight": 23
},
{
"filter": {"match":{"test":"cat"}},
"weight": 42
}
],
"max_boost": 42,
"score_mode": "max",
"boost_mode": "multiply"
, "min_score": 42
}
}
}
script_score的使用方式
GET /_search
{
"query": {
"function_score": {
"query": {
"match": { "message": "elasticsearch" }
},
"script_score": {
"script": {
"source": "Math.log(2 + doc['my-int'].value)"
}
}
}
}
}
GET /test-dsl-match/_search
{
"query": {
"match": {
"title": "QUICK!"
}
}
}
ES执行上面这个match查询的步骤:
检查字段类型
title字段是一个string类型(analyzed)已分析的全文字段,这意味着查询的字符串本身也应该被分析
分析查询字符串
将查询的字符串QUIOCK!传入标准分析器中,输出结果时单个项quick,因为只有一个单词项,所以match查询执行的单个底层term查询
查询匹配文档
用term查询在倒排索引中查找quick然后获取一组包含该项的文档
为每个文档评分
用term查询计算每个文档相关度评分_score,这是种将词频(即词quick在相关文档的title字段中出现的频率)和反向文档频率(即词quick在所有文档的title字段中出现的频率),以及字段的长度(即字段越短相关度越高)相结合的计算方式
本质
GET /test-dsl-match/_search
{
"query": {
"match": {
"title": "BROWN DOG"
}
}
}
因为match查询必须查找两个词([“brown”,“dog”]),它在内部实际上先执行两次term查询,然后将两次查询的结果合并作为最终结果输出,为了做到这点,它将两个term包入一个Bool查询中,所以上面的查询语句,实际上和下面的查询相同
GET /test-dsl-match/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"title": "brown"
}
},
{
"term": {
"title": "dog"
}
}
]
}
}
}
逻辑
上面等同于should(任意一个满足),是因为match还有一个operator参数,默认是or,所以对应的是should
所以上面的查询还可以是下面的这样
GET /test-dsl-match/_search
{
"query": {
"match": {
"title": {
"query": "BROWN DOG",
"operator": "or"
}
}
}
}
那么如果需要and操作呢?
GET /test-dsl-match/_search
{
"query": {
"match": {
"title": {
"query": "BROWN DOG",
"operator": "and"
}
}
}
}
这又可以这么来写
GET /test-dsl-match/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"title": "brown"
}
},
{
"term": {
"title": "dog"
}
}
]
}
}
}
如果用户给定三个查询词,想找到只包含其中两个的文档,该怎么做?使用or 或and好像都不合适
match查询支持minimum_should_match最小匹配参数,这就可以指定匹配的词项数用来表示一个文档是否相关,可以将其设置为某个具体数字,也可以将其设置为一个百分数(常用)
GET /test-dsl-match/_search
{
"query": {
"match": {
"title": {
"query": "quick brown dog",
"minimum_should_match": "75%"
}
}
}
}
tip:当给定百分比时,minimum_should_match会做合适的事情,比如在之前的三词项例子中,75%会自动被截断成66%,即三个词里面两个词,无论这个词被设置成这个,至少包含一个词项的文档才会认为是匹配的
上面的查询也等同于下面
GET /test-dsl-match/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "quick" }},
{ "match": { "title": "brown" }},
{ "match": { "title": "dog" }}
],
"minimum_should_match": 2
}
}
}
match_pharse
GET /test-dsl-match/_search
{
"query": {
"match_phrase": {
"title": {
"query": "quick brown"
}
}
}
}
match_phrase本质是连续的term的查询
match_pharse_prefix
ES在match_phrase基础上提供了一种可以查询最后一个词项是前缀的方法
GET /test-dsl-match/_search
{
"query": {
"match_phrase_prefix": {
"title": {
"query": "quick brown f"
}
}
}
}
TIP:prefix的意思并不是整个text的开始匹配,而是最后一个词项满足term的prefix查询
match_bool_prefix
除了match_phrase_prefix,ES还提供了match_bool_prefix查询
GET /test-dsl-match/_search
{
"query": {
"match_bool_prefix": {
"title": {
"query": "quick brown f"
}
}
}
}
那么这种方式和match_phrase_prefix有啥区别呢?
match_bool_prefix本质上可以转换为:
GET /test-dsl-match/_search
{
"query": {
"bool" : {
"should": [
{ "term": { "title": "quick" }},
{ "term": { "title": "brown" }},
{ "prefix": { "title": "f"}}
]
}
}
}
所以match_bool_prefix查询中quick,brown是无序的
multi_match
如果希望一次对多个字段进行查询,可以使用以下方式
{
"query": {
"multi_match" : {
"query": "Will Smith",
"fields": [ "title", "*_name" ]
}
}
}
*表示前缀匹配字段
此查询使用语法根据运算符(and,or)来分析和拆分提供的查询字符串NOT,然后查询在返回匹配的文档之前独立分析每个拆分的文本
可以使用query_string查询创建一个复杂的搜索,其中包括通配符,跨多个字段的搜索等等,尽管用途广泛,单查询是严格的,如果查询字符串中包含任何无效语法,则返回错误
GET /test-dsl-match/_search
{
"query": {
"query_string": {
"query": "(lazy dog) OR (brown dog)",
"default_field": "title"
}
}
}
query_string_simple
该查询使用一种简单的语法来解析提供的查询字符串并将其拆分为基于特殊运算符的术语,然后查询在返回匹配的文档之前独立分析每个术语,尽管其语法比query_string查询更受限制,但simple_query_string查询不会针对无效语法返回错误,而是,它将忽略查询字符串的任何无效部分
GET /test-dsl-match/_search
{
"query": {
"simple_query_string" : {
"query": "\"over the\" + (lazy | quick) + dog",
"fields": ["title"],
"default_operator": "and"
}
}
}
Intervals是时间间隔的意思,本质上将多个规则按照顺序匹配
GET /test-dsl-match/_search
{
"query": {
"intervals" : {
"title" : {
"all_of" : {
"ordered" : true,
"intervals" : [
{
"match" : {
"query" : "quick",
"max_gaps" : 0,
"ordered" : true
}
},
{
"any_of" : {
"intervals" : [
{ "match" : { "query" : "jump over" } },
{ "match" : { "query" : "quick dog" } }
]
}
}
]
}
}
}
}
}
tip:因为interval之间是可以组合的,所以它可能会表现的很复杂
由于多种原因,文档字段的索引值可能不存在
所以exist表示查找是否存在字段
GET /test-dsl-term-level/_search
{
"query": {
"exists": {
"field": "remarks"
}
}
}
ids既对id查找
GET /test-dsl-term-level/_search
{
"query": {
"ids": {
"values": [3, 1]
}
}
}
通过前缀查找某个字段
GET /test-dsl-term-level/_search
{
"query": {
"prefix": {
"name": {
"value": "Jan"
}
}
}
}
根据分词查询
GET /test-dsl-term-level/_search
{
"query": {
"term": {
"programming_languages": {
"value": "php"
}
}
}
}
按照单个分词term匹配,它们是or的关系
GET /test-dsl-term-level/_search
{
"query": {
"terms": {
"programming_languages": ["php","c++"]
}
}
}
这种查询方式的初衷是用文档中的数字字段动态匹配查询满足term的个数
GET /test-dsl-term-level/_search
{
"query": {
"terms_set":{
"programming_languages":{
"terms":["java","php"],
"minimum_should_match_field":"required_matches"
}
}
}
}
TIP: minimum_should_match_field -> 意思就是最小匹配度,这里的意思就是拿required_matches的值作为最小匹配度,这里就是最少要匹配到2个term的意思
通配符匹配,比如*
GET /test-dsl-term-level/_search
{
"query": {
"wildcard": {
"name": {
"value": "D*ai",
"boost": 1.0,
"rewrite":"constant_score" //(可选,字符串)用于重写查询的方法
}
}
}
}
常被用在数字或者日期范围的查询
GET /test-dsl-term-level/_search
{
"query": {
"range": {
"required_matches": {
"gte": 3,
"lte": 4
}
}
}
}
以"Jan"开头的name字段
GET /test-dsl-term-level/_search
{
"query": {
"regexp": {
"name":{
"value": "Ja.*"
}
}
}
}
官方文档对模糊匹配:编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数,这个更改可以包括以下:
GET /test-dsl-term-level/_search
{
"query": {
"fuzzy":{
"remarks": {
"value": "hell"
}
}
}
}
ES中的聚合其实就是相对于sql中得分group by
SELECT COUNT(color)
FROM table
GROUP BY color
ES中桶在概念上类似于SQL的分组(Group by),而指标类似于count()、sum()、MAX()等统计方法
ES包含三种聚合(Aggregation方式)
GET /test-agg-cars/_search
{
"size": 0,
"aggs": {
"popular_colors": {
"terms": {
"field": "color.keyword"
}
}
}
}
同时计算两种桶的结果:对color和对make
GET /test-agg-cars/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color.keyword"
}
},
"make_by" : {
"terms" : {
"field" : "make.keyword"
}
}
}
}
新的聚合层让我们可以将avg度量嵌套置于terms桶内,实际上,这就为每个颜色生成了平均价格
GET /test-agg-cars/_search
{
"size": 0,
"aggs": {
"colors": {
"terms": {
"field": "color.keyword"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
ES支持一些基于脚本(生成运行时的字段)的复杂的动态聚合
GET /test-agg-cars/_search
{
"runtime_mappings": {
"make.length": {
"type": "long",
"script": "emit(doc['make.keyword'].value.length())"
}
},
"size" : 0,
"aggs": {
"make_length": {
"histogram": {
"interval": 1,
"field": "make.length"
}
}
}
}
在当前文档集上下文中定义与指定过滤器(Filter)匹配的所有文档的单个存储桶,通常,这将用于将当前聚合上下文缩小到一组特定的文档
GET /test-agg-cars/_search
{
"size": 0,
"aggs": {
"make_by": {
"filter": {"term":{"type":"honda"}},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
就是说先匹配filter中term命中的文档,再对命中的文档进行聚合
场景:对不同日志类型的日志进行分组,这时就需要filters
GET /test-agg-logs/_search
{
"size": 0,
"aggs": {
"messages": {
"filters": {
"other_bucket_key": "other_messages",
"filters": {
"infos": {"match":{"body":"info"}},
"warnings": {"match":{"body":"waring"}}
}
}
}
}
}
基于多桶值源的聚合,使用户能够定义一组范围,每个范围代表一个桶,在聚合过程中,将从每个存储区范围中检查每个文档中提取的值,并存储相关/匹配的文档
TIP:这种聚合包括from值,但不包括to每个范围的值、
GET /test-agg-cars/_search
{
"size": 0,
"aggs": {
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{
"to": 20000
},
{"from": 20000,"to":40000},
{
"from": 40000
}
]
}
}
}
}
专用于IP值的范围聚合
GET /ip_addresses/_search
{
"size": 0,
"aggs": {
"ip_ranges": {
"ip_range": {
"field": "ip",
"ranges": [
{
"to": "10.0.0.5"
},
{
"from": "10.0.0.5"
}
]
}
}
}
}
GET /ip_addresses/_search
{
"size": 0,
"aggs": {
"ip_ranges": {
"ip_range": {
"field": "ip",
"ranges": [
{ "mask": "10.0.0.0/25" },
{ "mask": "10.0.0.127/25" }
]
}
}
}
}
GET /ip_addresses/_search
{
"size": 0,
"aggs": {
"ip_ranges": {
"ip_range": {
"field": "ip",
"ranges": [
{ "to": "10.0.0.5" },
{ "from": "10.0.0.5" }
],
"keyed": true // here
}
}
}
}
GET /ip_addresses/_search
{
"size": 0,
"aggs": {
"ip_ranges": {
"ip_range": {
"field": "ip",
"ranges": [
{ "key": "infinity", "to": "10.0.0.5" },
{ "key": "and-beyond", "from": "10.0.0.5" }
],
"keyed": true
}
}
}
}
专用于日期值的范围聚合
GET /test-agg-cars/_search
{
"size": 0,
"aggs": {
"range": {
"date_range": {
"field": "sold",
"format": "yyyy-MM-dd",
"ranges": [
{
"from": "2014-01-01",
"to": "2014-12-31"
}
]
}
}
}
}
TIP:此聚合与Range聚合之间的主要区别在于from和to值可以在Date Math表达式中表示,并且可以指定日期格式,通过该日期格式将返回from and to 响应字段,这里还需注意,此聚合包括from值,但不包括to每个范围的值
GET /test-agg-cars/_search
{
"size" : 0,
"aggs":{
"price":{
"histogram":{
"field": "price.keyword",
"interval": 20000
},
"aggs":{
"revenue": {
"sum": {
"field" : "price"
}
}
}
}
}
}
histogram桶要求两个参数:一个数值字段以及一个定义桶大小间隔
sum度量嵌套在每个售价区间内,用来显示每个区间内的总收入
上面的查询是围绕price聚合构建的,它包含一个histogram桶,它要求字段的类型必须是数值型的同时需要设定分组的间隔范围,间隔设置为20000意味着我们将会得到如[0-19999,20000-39999,…]这样的区间
接着,在直方图内定义嵌套的度量,这个sum度量,它会对落入某一具体售价区间的文档中price字段的值进行求和,这可以为我们提供每个售价区间的收入,从而可以发现到底是普通家用车赚钱还是奢侈车赚钱
GET /test-agg-cars/_search
{
"size": 0,
"aggs": {
"makes": {
"terms": {
"field": "make.keyword",
"size": 10
},
"aggs": {
"stats": {
"extended_stats": {
"field": "price"
}
}
}
}
}
}
上面例子会按受欢迎程度返回制造商列表以及它们各自的统计信息,也就是stats种的信息,包括平均值,总值,最大最小值等
POST /exams/_search?size=0
{
"aggs": {
"avg_grade": { "avg": { "field": "grade" } }
}
}
POST /sales/_search?size=0
{
"aggs": {
"max_price": { "max": { "field": "price" } }
}
}
POST /sales/_search?size=0
{
"aggs": {
"min_price": { "min": { "field": "price" } }
}
}
POST /sales/_search?size=0
{
"query": {
"constant_score": {
"filter": {
"match": { "type": "hat" }
}
}
},
"aggs": {
"hat_prices": { "sum": { "field": "price" } }
}
}
POST /sales/_search?size=0
{
"aggs" : {
"types_count" : { "value_count" : { "field" : "type" } }
}
}
POST /exams/_search
{
"size": 0,
"aggs": {
"weighted_grade": {
"weighted_avg": {
"value": {
"field": "grade"
},
"weight": {
"field": "weight"
}
}
}
}
}
POST /sales/_search?size=0
{
"aggs": {
"type_count": {
"cardinality": {
"field": "type"
}
}
}
}
GET reviews/_search
{
"size": 0,
"aggs": {
"review_average": {
"avg": {
"field": "rating"
}
},
"review_variability": {
"median_absolute_deviation": {
"field": "rating"
}
}
}
}
stats包含avg,max,sum和count
POST /exams/_search?size=0
{
"aggs": {
"grades_stats": { "stats": { "field": "grade" } }
}
}
以下例子使用矩阵统计量来描述收入与贫困之间的关系
GET /_search
{
"aggs": {
"statistics": {
"matrix_stats": {
"fields": [ "poverty", "income" ]
}
}
}
}
根据从汇总文档中提取的数值计算统计信息
GET /exams/_search
{
"size": 0,
"aggs": {
"grades_stats": { "extended_stats": { "field": "grade" } }
}
}
上面的汇总计算了所有文档的成绩统计信息,聚合类型为extended_stats,并且字段设置定义将在其上计算统计信息的文档的数字字段
用于计算从聚合文档中提取的字符串值的统计信息,这些值可以从特定的关键字字段中检索出来
POST /my-index-000001/_search?size=0
{
"aggs": {
"message_stats": { "string_stats": { "field": "message.keyword" } }
}
}
针对从聚合文档中提取的数值计算一个或多个百分位数
GET latency/_search
{
"size": 0,
"aggs": {
"load_time_outlier": {
"percentiles": {
"field": "load_time"
}
}
}
}
tip:默认情况下,百分位度量标准将生成一定范围的百分位:【1,5,25,50,75,95,99】
根据从汇总文档中提取的数值计算一个多个百分位等级
GET latency/_search
{
"size": 0,
"aggs": {
"load_time_ranks": {
"percentile_ranks": {
"field": "load_time",
"values": [ 500, 600 ]
}
}
}
}
上面结果表示90.01%的页面加载在500ms内完成,而100%的页面加载在600ms内完成
POST /museums/_search?size=0
{
"query": {
"match": { "name": "musée" }
},
"aggs": {
"viewport": {
"geo_bounds": {
"field": "location",
"wrap_longitude": true
}
}
}
}
上面汇总展示了如何针对具有商店业务类型的所有文档计算位置字段的边界框
POST /museums/_search
{
"aggs": {
"centroid": {
"geo_centroid": {
"field":"location"
}
}
}
}
上面汇总显示了如何针对具有犯罪类型的盗窃文件计算位置字段的质心
POST /test/_search?filter_path=aggregations
{
"aggs": {
"line": {
"geo_line": {
"point": {"field": "my_location"},
"sort": {"field": "@timestamp"}
}
}
}
}
将存储桶中的所有geo_point值聚合到由所选排序字段排序的LineString
POST /sales/_search?size=0
{
"aggs": {
"top_tags": {
"terms": {
"field": "type",
"size": 3
},
"aggs": {
"top_sales_hits": {
"top_hits": {
"sort": [
{
"date": {
"order": "desc"
}
}
],
"_source": {
"includes": [ "date", "price" ]
},
"size": 1
}
}
}
}
}
}
POST /test/_search?filter_path=aggregations
{
"aggs": {
"tm": {
"top_metrics": {
"metrics":{
"field":"m"
},
"sort":{"s":"desc"}
}
}
}
}
管道聚合(Pipeline Aggregration):简单而言其实就是让上一步的聚合结果成为下一个聚合的输入,这就是管道
举例解释管道
当一个request过来的时候,需要对这个request做一系列的加工,使用责任链模式可以对每个加工组件化,减少耦合,也可以使用在当一个request过来的时候,需要找到合适的加工方式,当一个加工方式不适合这个request的时候,传递到下一个加工方法,该加工方式再尝试对request加工
第一个维度:管道聚合有很多不同类型,每种类型都与其他聚合计算不同的信息,但是可以将这些类型分为两类
第二个维度:根据功能设计的意图
比如前置聚合可能是Bucket聚合,后置聚合可能是基于Metric聚合,那么它就可以成为一类管道
POST _search
{
"size": 0,
"aggs": {
"sales_per_month": {
"date_histogram": {
"field": "date",
"calendar_interval": "month"
},
"aggs": {
"sales": {
"sum": {
"field": "price"
}
}
}
},
"avg_monthly_sales": {
// tag::avg-bucket-agg-syntax[]
"avg_bucket": {
"buckets_path": "sales_per_month>sales",
"gap_policy": "skip",
"format": "#,##0.00;(#,##0.00)"
}
// end::avg-bucket-agg-syntax[]
}
}
}
字段类型
POST /sales/_search
{
"size": 0,
"aggs": {
"sales_per_month": {
"date_histogram": {
"field": "date",
"calendar_interval": "month"
},
"aggs": {
"sales": {
"sum": {
"field": "price"
}
}
}
},
"stats_monthly_sales": {
"stats_bucket": {
"buckets_path": "sales_per_month>sales"
}
}
}
}
先提出以下疑问:
*foo-bar*
无法匹配 foo-bar
?云上的集群
云里面的每个白色正方型盒子代表一个节点-Node
在一个或者多个节点之间,多个绿色小方块在一起形成一个ElaticSearch的索引
在一个索引下,分布在多个节点里的绿色小方块称为分片–Shard
一个ElasticSearch的Shard本质上是一个Lucene Index
Lucene是一个Full Text搜索库(也有很多其她形式的搜索库),ES是建立在Lucene之上的
Segment
在Lucene里面有很多小的segment,我们可以把它们看成Lucene内部的mini-index
Inverted Index
Inverted Index主要包括两部分
tip:当我们搜索时,首先会将搜索的内容分解,然后在字典里面找到对应的Term,从而查到与搜索相关的文件内容
查找以字母c开头的字符,可以简单的通过二分查找(Binary Search)在Inverted Index表中找到例如“choice”,"comign"这样的词(Term)
查找所有包含“our”字符的单词,那么系统会扫描整个Inverted Index,这是非常昂贵的
在这种情况下,如果想要做优化,那么我们面对的问题是如何生成合适的Term
对于此类的问题,可能有以下几种可行的解决方案
* suffix -> xiffus *
如果我们想以后缀为搜索条件,可以为Term做反向处理
(60.6384, 6.5017) -> u4u8gyykk
对于GEO位置信息,可以将它转换为GEO Hash
123 -> {1-hundreds, 12-tens, 123}
对于简单的数字,可以为他生成多重形式的Term
使用Python库为单词生成一个包含错误拼写信息的树形状态机,解决拼写错误的问题
当想要查找包含某个特定标题内容的文件时,Inverted Index就不能很好的解决整个问题,所以Lucene提供了另一种数据结构Stored Field来解决这个问题,本质上,Stored是一个简单的键值对key-value,默认情况下,ES会存储整个文件的JSON source
以上结构仍然无法解决诸如:排序,聚合,facet,因为我们可能要读取大量不需要的信息
而Document Values这种结构解决了此种问题,这种结构本质上是一个列式的存储,他高度优化了具有相同类型的数据的存储结构
为了提高效率,ES可以将索引下某一个Document Value全部读取到内存中进行操作,这大大提升访问速度,但是也同时会消耗大量的内存空间,但总而言之,这些数据结构Inverted Index,Stored Fields,Document Values及其缓存,都在segment内部
ES的cluster中有很多Node -》 每个Node里面又有很多的shard -》 一个或多个Node中的shard组合在一起形成一个ElasticSearch的索引 -》每个shard 相当于Lucene Index,一个ElasticSearch的shard本质上就是一个Lucene Index -》 每个Lucene里面又有很多的小segment,segment可以看成是Lucene内部的mini-index -》每个segment内部又有着许多的内部结构,举例:Inverted Index、Stored Fields、Document Values、Cache等,Es的搜索其实是基于这些数据结构的
搜索时,Lucene会搜索所有的segment然后将每个segment的搜索结果返回,最后合并呈现给客户
Lucene的一些特性使得这个过程非常重要
当ES索引一个文件时,会为文件建立相应的缓存,并且会定期(每秒)刷新这些数据,然后这些文件就可以被搜索到
随着时间的增加,会有很多的segment,所以ES会将这些segment合并,在这个过程中,segment会最终被删除掉
这也就是为什么增加文件可能会使索引所占空间变小,它会引起merge,从而可能会有更多的压缩
例子:
有两个segment将会merge
这两个segment最终会被删除,然后合并成一个新的segment
这时这个新的segment在缓存中处于cold状态,但是大多数segment仍然保持不变,处于warm状态。
以上场景经常在Lucene Index内部发生的。
ES从shard中搜索的过程与Lucene Segment中搜索的过程类似
tip:与在Lucene Segement中搜索不同的是,shard可能是分布在不同Node上,所以在搜索与返回结果时,所有的信息都会通过网络传输
注意:一次搜索查找两个shard == 2次分别搜索shard
对日志文件处理
当想要搜索特定日期产生的日志时,通过根据时间戳对日志文件进行分块和索引,会极大提高搜索效率
当想要删除旧的数据时,只需删除老的索引即可
在上图中,每个index有两个shards
如何scale
tip:shard不会进行更进一步的拆分,但是shard可能会被转移到不同的节点上
所以当集群节点压力增长到一定的程度时,就可能会考虑增加新的节点,这就会要求我们对所有数据进行重新索引,这是我们所不太希望的,所以需要在规划的时候就考虑清楚,如何去平衡足够多的节点与不足节点之间的关系
每个节点,每个都存留一份路由表,所以当请求到任何一个节点时,ES都有能力将请求转发到期望节点的shard进一步处理
这里的query有一个类型filtered,以及一个multi_match的查询
根据作者进行聚合,得到top10的hits的top10作者的信息
这个请求可能会被分发到集群里的任意一个节点
这个节点会成为当前请求的协调者,它会根据索引信息,判断请求会被路由到哪个核心节点,以及哪个副本是可用的等等
ES会将Query转换成Lucene Query
TIP:但queries不会被缓存,所以如果相同的Query重复执行,应用程序自己需要做缓存
搜索结束之后,结果会沿着下行的路径向上逐层返回!
Lucene的索引结构中有哪些文件呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0BxOoq8v-1653812907296)(ES.assets\image-20210716230241459.png)]
分析:包含下main的过程
分析器执行上面的工作,分析器实际上是将三个功能封装到了一个包里
TIP:Elasticsearch提供了开箱即用的字符过滤器,分词器和token过滤器,这些可以组合起来形成自定义的分析器以用于不同的目的
测试文本
"Set the shape to semi-transparent by calling set_trans(5)"
标准分析器是Elasticsearch默认使用的分析器,它是分析各种语言文本最常用的选择,它根据unicode联盟定义的单词边界划分文本,删除绝大部分标点,最后,将词条小写,它会产生
set, the, shape, to, semi, transparent, by, calling, set_trans, 5
简单分析器在任何不是字符的地方分隔文本,将词条小写,它会产生
set, the, shape, to, semi, transparent, by, calling, set, trans
空格分析器在空格的地方划分文本,它会产生
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
特定语言分析器用于很多语言,它们可以考虑指定语言的特点,例如,英语分析器附带了一组英语无用词(常用单词,例如and或者the,它们对相关性没有多少影响),它们会被删除,由于理解英语语法的规则,这个分词器可以提取英语的词干
英语分词器会产生下面的词条
set, shape, semi, transpar, call, set_tran, 5
TIP:这里注意,当使用英语分析器时,transparent,calling和set_trans已经变为词根格式
当我们索引一个文档,它的全文域被分析成词条以用来创建倒排索引,但是,当我们在全文域搜索的时候,我们需要将查询字符串通过相同的分析过程,以保证我们搜索的词条格式与索引中的词条格式一致
全文查询,理解每个域是如何定义的,因此它们可以做正确的事:
比如:
那么为什么会返回这样的结果呢?
当我们在_all查询2014时,它匹配所有的12条推文,因为它们都含有2014
GET /_search?q=2014 # 12 results
当我们在_all域查询2014-09-15,它首先分析字符串,产生匹配2014,09,15中任意词条的查询,这也会匹配所有12条推文,因为它们都含有2014
GET /_search?q=2014-09-15 # 12 results !
当我们在date域查询2014-09-15时,它会寻找精确日期,只找到一个推文:
GET /_search?q=date:2014-09-15 # 1 result
当我们在date域查询2014时,它找不到任何文档,因为没有文档含有这个精确日期
GET /_search?q=date:2014 # 0 results !
新建单个文档所需的步骤顺序:
使用bulk修改多个文档步骤顺序
整体的索引流程
shard = hash(document_id) % (num_of_primary_shards)
通过分步骤看数据持久化过程:write -> refresh -> flush -> merge
一个新文档过来,会存储在in-memory buffer内存缓存区中,顺便会记录到Transog(ElasiticSearch增加了一个tanslog,或者叫事务日志,在每一次对ElasticSearch进行操作时进行了日志记录),这时候还没有到segment,是搜索不到这个新文档的,数据只有被refresh后,才可以被搜索到
refresh默认1秒,执行一次上图流程,ES是支持修改这个值的,通过index.refresh_interval设置refresh(冲刷)间隔时间
refresh的流程大致如下:
每隔一段时间,例如translog变得越来越大了,索引被刷新flush:一个新的translog被创建,并且一个全量提交被执行
上一个过程中,segment在文件系统中缓存,会有意外故障文档丢失,那么,为了保证文档不会丢失,需要将文档写入磁盘,那么文档从文件缓存写入磁盘的过程就是flush,写入磁盘后,清空translog
具体过程如下:
由于自动刷新流程每秒会创建一个新的段,这样会导致短时间内的段数量暴增,而段数量太多会带来较大麻烦,每一个段都会消耗文件句柄,内存和cpu运行周期,更重要的是,每个搜索请求都必须轮流检查每个段,所以段越多,搜索也就越慢
ElasticSearch通过在后台进行Merge Segment来解决这个问题,小的段会被合并到大的段,然后这些大的段再被合并到更大的段
当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用,合并进程选择一小部分大小相似的段,并在后台将它们合并到更大的段中,这并不会中断索引和搜索
一旦合并结束,老的段会被删除
合并大的段需要消耗大量的I/O和cpu资源,如果任其发展会影响搜索性能,ES在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好的执行
考虑和分析一个分布式系统的写操作时,一般需要从下面几个方面考虑:
TIP:ES作为分布式系统,在写入时也必须满足上述的特点
ElasticSearch内部使用了Lucene完成索引创建和搜索功能,Lucene中写操作主要时通过IndexWriter类实现,IndexWriter提供三个接口:
public long addDocument();
public long updateDocuments();
public long deleteDocuments();
通过这三个接口可以完成单个文档的写入,更新和删除功能,包括分词,倒排创建,正排创建等所有与搜索相关的流程,只要Doc通过IndexWriter写入后,后面就可以通过IndexSearcher搜索了,但这样就出现了一些疑问
ES采用多shard方式,通过配置rounting规则将数据分成多个数据子集,每个数据子集提供独立的索引和搜索功能,当写入文档时,根据rounting规则,将文档发送给特定shard中建立索引,这样就实现分布式了
ES整体架构采用的是一主多副的方式
每个Index由多个shard组成,每个shard有一个主节点和多个副本节点,副本个数可配,每次写入的时候,写入请求会先根据rounting规则选择发送给哪个shard,Index request中可以设置使用哪个Field的值作为路由参数,如果没有设置,则使用mapping中的配置,如果mapping也没有配置,则使用_id作为路由参数,然后通过rounting的Hash值选择出shard(在OperationRounting类中),最后从集群的Meta中找出该shard的primary节点
请求接着会发送给primary Shard,在primary shard上执行成功,再从primary shard上将请求同时发送给多个Replilca Shard,请求在多个Replica Shard上执行成功并返回给Primary shard后,写入请求执行成功,返回结果给客户端
这种模式下,写入操作的延时就等于latency = Latency(primary write) + Max(Replicas write),只要有副本在,写入延时最小也是两次单shard的写入时延总和,写入效率会较低,但是这样的好处也很明显,避免写入后,单机或磁盘故障导致数据丢失,在数据重要性和性能方面,一般都是优先选择数据,除非一些允许丢失数据的特殊场景
采用多个副本后,避免了单机或磁盘故障发生时,对已经持久化后的数据造成损害,但是ES为了减少磁盘IO保证读写性能,一般是每隔一段时间(比如5分钟)才会把Lucene的Segment写入磁盘持久化,对于写入内存,但还未Flush到磁盘的Lucene数据,如果发生机器宕机或者掉电,那么内存中的数据也会丢失,这时候如何保证?
对于上面的问题,ES学习了数据库中的处理方式,增加commitLog模块,ES中称为TransLog
在每一个shard中,写入流程分为两部分,先写入Lucene,再写入TransLog
写请求到达shard后,先写Lucene文件,创建好索引,此时索引还在内存黎曼,接着去写TransLog,写完TransLog后,刷新TransLog数据到磁盘上,写入磁盘成功后,请求返回用户,这里有几个关键点:
Lucene中不支持部分字段的update,所以需要在ElasticSearch中实现该功能,具体流程如下:
ES中写入请求主要包括这几个:Index(create), Update, Delete和Bulk,其中前三个是单文档操作,后一个是多文档操作,其中Bulk中可以包括index,update,delete
TIP:在6.0.0以及之后的版本中,前3个文档操作的实现基本和Bulk操作一致,甚至有些就是通过调用Bulk的接口实现的
Bulk请求的写入流程
红色:client Node、绿色:Primary Node、蓝色:Replica Node
Ingest Pipeline
可以在这一步对原始文档做一些处理,比如HTML解析,自定义处理,具体的处理逻辑可以通过插件来实现,在ES中,由于Ingest Pipeline会比较消耗CPU等资源,可以设置专门的Ingest Node,专门用来处理Ingest Pipepline逻辑,如果当前Node不能执行Ingest Pipeline,则会将请求发给另一台可以执行Ingest Pipeline 的Node
Auto Create Index
判断当前Index是否存在,如果不存在,则需要自动创建Index,这里需要和master交互,也可以通过配置关闭自动创建Index的功能
set rounting
设置路由条件,如果Request中指定了路由条件,则直接使用Request 中的Rounting,否则使用Mapping中配置的,如果Mapping中无配置,则使用默认的_id字段值
在这一步中,如果没有指定id字段,则会自动生一个唯一的_id字段,目前使用的是UUID
Construct BulkShardRequest
由于Bulk Request中会包含多个(Index/Update/Delete)请求,这些请求会根据rounting可能会落在多个shard上执行,这一步会按Shard挑选Single write Request,同一个Shard中的请求聚集在一起,构建Bulk Shard Request,每个Bulk Shard Requesrt对应一个Shard
Send Request To Primary
这一步会将每一个BulkShardRequest请求发送给相应Shard 的Primary Node
Index or Uodate or Delete
循环执行每个Single write Request ,对于每个Request,根据操作类型(CREATE/INDEX/UPDATE/DELETE)选择不同的处理逻辑,其中,create/Index是直接增加Doc,Delete是根据_id删除Doc,Update会稍微复杂些,下面以Update为例:
Translate Update to Index or Delete
这一步是update操作特有的步骤,在这里,会将update请求转为Index或者Delete请求,首先,会通过GetRequest查询到已经存在的同_id Doc(如果有)的完整字段和值(依赖_score字段),然后和请求中Doc合并,同时,这里会获取到读到的DOC版本号,记做V1
Parse DOC
这里会解析DOC各个字段,生成ParsedDocument对象,同时会生成uid Term,在ES中,_uid = type # _id,对用户,_id 可见,而ES中存储的是uid,这一部分生成的ParsedDocument中也有ES的系统字段,大部分会根据当前内容填充,部分未知的会在后面继续填充ParsedDocument
Update Mapping
ES中有个自动更新Mapping的功能,在这一步生效,会先挑选出Mapping中为包含的新Field,然后判断是否运行自动更新Mapping,如果允许,则更新Mapping
Get Sequence Id And Version
由于当前是Primary Shard,则会从SequenceNumber Service获取一个SequenceID和Version,SequenceId在Shard级别每次递增1,SequenceID在写入DOC成功后,会用来初始化LocalCheckpoint,Version则是根据当前DOC的最大Version递增1
Add Doc To Lucene
这一步开始的时候会给特定的_uid加锁,然后判断该uid对应的version是否等于之前Translate update to index步骤里后去的version,如果不相等,则说明刚才读取doc后,该doc发生了变化,出现了版本冲突,这时候会抛出一个versionConfilct的异常,该异常会在primary Node最开始处捕获,重新从Translate update to index or delete开始执行
如果version相等,则继续执行,如果已经存在同id的doc,则会调用Lucene的UpdateDocument(uid,doc)接口,先根据uid删除Doc,然后再Index新的Doc,如果是首次写入,则直接调用Lucene的AddDocment接口完成Doc的Index,AddDocment也是通过UpdateDocument实现的
这一步中的问题是,如何保证Delete-Then-Add的原子性,怎么避免中间状态时被Refresh?答案是在开始Delete之前,会加一个Refresh Lock,禁止被Refresh,只有等Add完后释放了Refresh Lock后才能被Refresh,这样就保证了Delete-Then-Add的原子性
Lucene的UpdateDocument接口中只是处理多个Field,会遍历每个Field逐个处理,处理顺序是invert index,store field,doc values,point demension
Write TransLog
写完Lucene的Segment后,会以keyvalue的形式写TransLog,key是_id,value是doc的内容,当查询的时候,如果请求是getDocById,则是可以直接根据 _id 从TransLog中读取到,满足NoSQL场景下的实时性
需要注意的是,这里只是写入到内存的TransLog,是否Sync到磁盘的逻辑还在后面,这一步的最后,会标记当前SequenceID已经成功执行,接着会更新当前Shard的LocalCheckPoint
Renew Bulk Request
这里会从新构造Bulk Request,原因是前面已经将UpdateRequest翻译成了Index或Delete请求,则后续所有Replica中只需执行Index或Delete请求就可以了,不需要再执行Update逻辑,一是保证Replica中逻辑更简单,性能更好,而是保证同一个请求再Primary 和Replica中的执行结果一样
Flush TransLog
这里会根据TransLog的策略,选择不同的执行方式,要么是立即Flush到磁盘,要么是等到以后再Flush,Flush的频率越高,可靠性越高,对写入性能的影响越大
send Requests To Replicas
这里会将刚才构造的新的Bulk Request并行发送给多个Replica,然后等待Replica的返回,这里需要等待所有的Replica返回后(可能有成功,可能有失败),Primary Node才会返回用户,如果某个Replica失败了,则Primary 会给master发送一个Remove Shard请求,要求master将该Replica Shard从可用节点中移除
这里,同时会将SequenceID,PrimaryTerm,GlobalCheckPoint等传递给Replica
发送给Replica的i请求中,Action Name 等于原始ActionName+【R】,这里的R表示Replica,通过这个[R]的不同,可以找到处理Replica请求的Handler
Receive Response From Replicas
Replicas中请求都处理完后,会更新Primary Node 的LocalCheckPoint
Index or Delete
根据请求类型是Index还是Delete,选择不同的执行逻辑,这里没有Update,是因为在PrimaryNode中已经将Update转换成了Index或Delete请求了
Parse doc(同primary Node相同)
Update mapping(同primary Node相同)
Get Sequence Id and version
Primary Node中会生成Sequence Id和Version,然后放入Replica Request中,这里只需要从Request中获取到就行
Add Doc To Lucene
由于已经在Primary Node中将部分update请求转换成了Index或Delete请求,这里只需处理Index和Delete两种请求,不再处理Update请求了,比primary node会简单一些。
write TransLog
Flush TransLog
以上都和Primary中 Node逻辑一致
分布式系统中的六大特性
可靠性:由于Lucene的设计中不考虑可靠性,在ES中通过Replica和TransLog两套机制保证数据的可靠性
一致性:Lucene中的Flush锁只保证Update接口里面的Delete和Add中间不会Fluesh,但是Add完成后仍然有可能立即发生Flush,导致Segment可读,这样就没法保证Primary和所有其他Replica可以同一时间Flush,就会出现查询不稳定的情况,这里只能实现最终一致性
原子性:add和delete都是直接调用lucene的接口,是原子的,当部分更新时,使用version和锁保证更新是原子的
隔离性:仍然采用version和局部锁来保证更新的是特定版本的数据
实时性:使用定期Refresh Segment到内存,并且Reopen Segment方式保证搜索可以在较短时间(比如1秒)内被搜索到,通过将未被刷新到硬盘数据记入TransLog,保证对未提交数据可以通过ID实时访问到
性能:性能是一个系统性工程,所有环节都要考虑对性能的影响,在ES中,在很多地方的设计都考虑到了性能,
一是不需要所有Replica都返回才能返回给用户,只需要返回特定数目就行
二是生成的Segment先在内存中提供服务,等一段时间后才刷新到磁盘,Segment在内存这段时间的可靠性由TransLog保证
三是TransLog可以配置为周期行的Flush,但这个会给可靠性带来伤害
四是每个线程持有一个Segment,多线程时相互不影响,相互独立,性能更好
五是系统的写入流程对版本依赖较重,读取频率较高,因此采用了versionMap,减少热点数据的多次磁盘IO开销,Lucene中针对性能做了大量的优化
以下是从主分片或者副本分片检索文档的步骤顺序:
在处理读请求时,协调节点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡
在文档被检索时,已经被检索的文档可能已经存在于主分片上但是还没有复制到副本分片,在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档,一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的
使用mget取回多个文档的步骤顺序:
所有的搜索系统一般都是两阶段查询,第一阶段查询到匹配的DocID,第二阶段再查询DocID对应的完整文档,这种在ES中称为query_then_fetch
在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或副本分片)。每个分片在本地执行搜索并构建一个匹配文档的大小为from + size的优先队列。
PS:在2.搜索的时候是会查询FileSystem Cache的,但是有部分数据还在Memory Buffer,所以搜索是近实时的
每个分片返回各自优先队列中所有文档的ID和排序值给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表
接下来就是取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个GET请求,每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点,一旦所有文档都被取回了,协调节点返回结果给客户端
一致性指的是写入成功后,下一次读操作一定要能读取到最新的数据,对于搜索,这个要求会低一些,可以有一些延迟,但是对于NoSQL数据库,则一般要求最好是强一致性
结果匹配上,NoSQL作为数据库,查询过程中只有不符合两种情况,而搜索里面还有是否相关,类似于NoSQL的结果只能是0或1,而搜索里面可能会有0.1,0.5,0.9等部分匹配或更相关的情况
结果召回上,搜索一般只需要召回最满足条件的Top N结果即可,而NoSQL一般需要返回满足条件的所有结果
搜索系统一般都是两阶段查询,第一阶段查询到对应的Doc ID,也就是PK;第二阶段再通过DOC ID去查询完整文档,而NoSQL数据库一般是一阶段就返回结果,在ES中两种都支持。
目前NoSQL的查询,聚合,分析和统计等功能上都要比搜索弱的
ES使用Lucene作为搜索引擎,通过Lucene完成特定字段的搜索等功能,在Lucene中这个功能是通过IndexSearcher的下列接口实现的:
public TopDocs search(Query query, int n);
public Document doc(int docID);
public int count(Query query);
......(其他)
第一个Search接口实现搜索功能,返回最满足Query的N个结果,第二个接口通过doc id查询Doc内容,第三个count接口通过Query获取到命中数
这三个概念是搜索中最基本的三个功能点,对于大部分ES中的查询都是比较复杂的,直接使用这个接口是无法满足需求的,比如分布式问题,这些就都留给了ES解决
ES中每个Shard都会有多个Replica,主要是为了保证数据可靠性,除此之外,还可以增加都能力,因为写的时候虽然要写大部分Replica Shard,但是查询的时候只需查询Primary 和Replica中的任何一个就可以了
在上图中,该Shard有1个Primary和2个Replica Node,当查询的时候,从三个节点中根据Request中的preference参数选择一个节点查询,preference可以设置 _local, _primary, _replica以及其它选项,如果选择了primary,则每次查询都是直接查询primary,可以保证每次查询都是最新的,如果设置了其他参数,那么可能会查询到R1或者R2,这时候就有可能查询不到最新的数据
ES中通过分区实现分布式,数据写入的时候根据 _rounting 规则将数据写入某一个shard中,这样就能将海量数据分布在多个shard以及多台机器上,已达到分布式的目标,这样就导致了查询的时候,潜在数据会在当前index的所有shard中,所以ES查询的时候需要查询所有Shard,同一个Shard的primary和replica选择一个即可,查询请求会分发给所有shard,每个shard中都是一个独立的查询引擎,比如需要返回top 10 的结果,那么每个shard都会查询并且返回top 10的结果,然后在client Node里面会接收所有shard的结果,然后通过优先级队列二次排序,选择出TOP 10的结果返回给用户
这里有一个问题就是请求膨胀,用户的一个搜索请求在ES内部会变成shard个请求,这里有个优化点,虽然是shard个请求,但是shard个数不一定要是当前index中的shard个数,只要是当前查询相关的shard即可,这个需要基于业务和请求内容优化,通过这种方式可以优化请求膨胀数
ES中的查询主要分为两类:GET请求:通过ID查询特定DOC;Search请求:通过Quert查询匹配Doc
PS:上图中内存中的Segment是指刚Refresh Segment,但是还没持久化到磁盘的新Segment,而非从磁盘加载到内存中的Segment
对于Search类请求,查询的时候是一起查询内存和磁盘上的Segment,最后将结果合并返回,这种查询是近实时的,主要是由于内存中的Index数据需要一段时间后才会刷新成为Segment
对于Get类请求,查询的时候是先查询内存中的TransLog,如果成功就立即返回,如果没找到再查询磁盘上的TransLog,如果还没有则再去查询磁盘上的Segment,这种查询是实时的,这种查询顺序可以保证查询到的DOC是最新版本的DOC,这个功能也是为了保证NoSQL场景下的实时要求
所有的搜索系统一般都是两阶段查询,第一阶段查询匹配到的doc ID,第二阶段再查询Doc ID对应的完整文档,这种在ES中称为query_then_fetch,还有一种是一阶段查询的时候就返回完整DOC,在ES中称为query_and_fetch,一般第二种适用于只需要查询一个shard的请求
题外话:
除了一阶段,二阶段外,还有一种三阶段查询的情况,搜索里面有一种算分逻辑是根据TF(Term Frequency)和DF(Document Frequency)计算基础分,但是ES中查询的时候,是在每个shard中独立查询的,每个shard中的TF和DF都是独立的,虽然在写入的时候通过 _rounting保证DOC分布均衡,但是没法保证TF和DF均衡,那么就会导致局部的TF和DF不准的情况出现,这个时候基于TF,DF的算分就不准了,为了解决这一问题,ES引入了DFS查询,比如 DFS_query_then_fetch,会先收集所有shard中的TF和DF值,然后将这些值代入请求中,再次执行query_then_fetch,这样算分的时候TF和DF就是准确的,类似的有DFS_query_and_fetch,这种查询的优势是算分更加精确,但是效率会变差,另一种选择是用BM25代替TF/DF模型
新版本ES中,用户无法指定DFS_query_and_fetch和query_and_fetch,这两种只能被ES系统改写
Get Remove Cluster Shard
判断是否需要跨集群访问,如果需要,则获取要访问的shard列表
Get Search Shard Iterator
获取当前cluster中要访问的shard,和上一步中的Remove Cluster Shard合并,构建出最终要访问的完整的shard列表
这一步中,会根据Request请求中的参数从Primary Node和Replica Node中选择出一个要访问的Shard
For Every Shard:Perform
遍历每个Shard ,对每个shard执行后面逻辑
Send Request To Query Shard
将查询阶段请求发送给相应的Shard
Merge Docs
上一步将请求发送给多个Shard后,这一步就是异步等待返回结果,然后对结果合并,这里的合并策略是维护一个Top N大小的优先级队列,每当收到一个shard的返回,就把结果放入优先队列做一次排序,直到所有的shard都返回
翻页逻辑也是在这里,如果需要取Top 30 - TOP 40的结果,这个意思是所有shard查询结果中的30-40的结果,那么在每个shard中无法确定最终的结果,每个shard需要返回top 40的结果给client Node,然后client Node中在merge Doc的时候,计算出top 40的结果,最后再去除top 30,剩余的10个结果就是需要的top 30 -top 40的结果
上述翻页逻辑有一个明显的缺点就是每次Shard返回的数据中包括了已经翻过的历史结果,如果翻页很深,则在这里需要排序的Docs会很多,比如Shard有1000,取第9990到10000的结果,那么这次查询,Shard总共需要返回1000 * 10000,也就是一千万Doc,这种情况很容易导致OOM。
另一种翻页方式是使用search-after,这种方式会更轻量级,如果每次只需要返回10条结构,则每个shard只需要返回search_after之后的10个结果即可,返回的总数据量只是和shard个数以及本次需要的个数有关,和历史已读取的个数无关,推荐使用这种方式
Send Request To Fetch Shard
选出TOP N 个DOC ID 发送这些DOC ID 所在的shard执行fetch phase ,最后返回top N 的DOC的内容
第一阶段查询的步骤:
在系统层面能够影响应用性能的一般包括三个因素:CPU,内存和IO,可以从这三方面进行性能优化
一般来说,CPU繁忙的原因有以下几个:
大多数ES部署往往对CPU要求不高,因此,相对于其他资源,具体配置多少个(CPU)不是那么关键,应该选择具有多个内核的现代处理器,常见的集群使用2到8个核的机器,如果要在更快的CPU和更多核数之间选择,选更多核数更好,多个内核提供的额外并发远胜于稍微快一点的时钟频率
如果中哪一种资源是最先被耗尽的,它可能是内存,排序和聚合都很消耗内存,所以有足够的堆空间来应付它们是很重要的,即使堆空间是比较小的时候,也能为操作系统文件缓存提供额外的内存,因为Lucene使用的许多数据结构是基于磁盘的格式,ES利用操作系统缓存能产生很大的效果
64G内存的机器是非常理想的,但是32G和16G机器也很常见,少于8G会适得其反(因为最终可能需要很多的小机器),大于64G也会有问题
由于ES构建基于Lucene,而Lucene设计强大之处在于Lucene能够很好的利用操作系统来缓存索引数据,以提供快速的查询性能,Lucene的索引文件Segment是存储在单文件中的,并且不可变,对于OS来说,能够很友好的将索引文件保持在Cache中,以便快速访问,因此,我们很有必要将一半的物理内存留给Lucene,另一半的物理内存留给ES(JVM heap)
当机器的内存大于64时,遵循以下原则:
禁止swap,一旦允许内存和磁盘的交换,会引起致命的性能问题,可以通过在elasticsearch.yml中 bootstrap.memory_lock:true,以保持JVM锁定内存,保证ES的性能
索引优化主要是在ES的插入层面优化,ES本身索引速度还是很快的,可以根据不同的需求,针对索引优化
当有大量数据提交的时候,建以采用批量提交(Bulk操作);此外使用bulk请求时,每个请求不超过几十M,因为太大会导致内存使用过大
例如在做ELK过程中,Logstash indexer提交数据到ES中,batch size就可以作为一个优化功能点,但是优化size大小需要根据文档大小和服务器性能而定
像Logstash 中提交文档大小通过20MB,Logstash会将一个批量请求切分为多个批量请求
如果在提交过程中,遇到EsRejectedExecutionException异常的话,则说明集群的索引性能已经达到极限了,这种情况下,要么提高服务器集群的资源,要么根据业务规则,减少数据收集速度,比如只收集warn,error级别以上的日志
为了提高索引性能,Es在写入数据的时候,采用延迟写入的策略,即数据先写入内存,当超过默认1秒(index.refresh_interval)会进行一次写入操作,就是将内存中segment数据刷新到磁盘中,此时我们才能将数据搜索出来,所以这就是为什么ES提供的是近实时搜索功能,而不是实时搜索功能
如果系统对数据延迟性要求不高的话,我们可以通过延长refresh时间间隔,可以有效的减少segment合并压力,提高索引速度,比如在做全链路跟踪的过程中,我们就将index.refresh_interval设置为30S,减少refresh次数,再比如,在进行全量索引时,可以将refresh次数临时关闭,即index.refresh_interval设置为-1,数据导入成功后再打开到正常模式,比如30S
TIP:在加载大量数据时可以暂时不用refresh和replicas,index.refresh_interval设置为-1,index.number_of_replicas设置为0
索引缓存的设置可以控制多少内存分配给索引进程,这时一个全局配置,会应用于一个节点上所有不同的分片上
indices.memory.index_buffer_size: 10%
indices.memory.min_index_buffer_size: 48mb
一是控制数据从内存到硬盘的操作频率,以减少IO,可将sync_interval的时间设置大一些,默认为5S
index.translog.sync_interval: 5s
也可以控制transLog数据块的大小,达到threshold大小时,才会fluish到Lucene索引文件,默认为512m
index.translog.flush_threshold_size: 512mb
_id 字段的使用,应尽可能避免自定义 _id ,以避免针对ID的版本控制,建以使用ES的默认ID生成策略或者使用数字类型ID作为主键
_all 字段以及 _source字段使用,应该注意场景和需要, _all 字段包含了所有的索引字段,方便做全文检索,如果没有这种需求,可以禁用, _source存储了原始的document内存,如果没有获取原始文档数据的需求,可以通过设置includes, excludes属性来定义放入 _source的字段
合理的配置使用index属性,analyzed和not_analyzed,根据业务需求来控制字段是否分词或不分词,只有groupby需求的字段,配置时就设置成not-analyzed,以提高查询或聚类的效率
ES默认副本数量为3个,虽然这样会提高集群的可用性,增加搜索的并发数,但是同时也会影响写入索引的效率
在索引过程中,需要把更新的文档发到副本节点上,等副本节点生效后再进行返回结束,使用ES做业务搜索的时候,建以副本数目还是设置为3个,但是像内部ELK日志系统,分布式跟踪系统中,完全可以将副本数目设置为1个
当查询文档的时候,ES如何直到一个文档存放在哪一个分片中呢,它是通过以下公式计算出来的
shard = hash(rounting) % number_of_primary_shards
rounting默认值是文档的id,也可以采用自定义值,比如用户ID
在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为2个步骤
查询的时候,可以直接根据rounting信息定位到某个分片查询,不需要查询所有的分片,经过协调节点排序
像上面自定义的用户查询,如果rounting设置为userid的话,可以直接查询出数据来,效率会提升很多
尽可能使用过滤上下文(Filter)替代查询上下文(Query)
ES针对Filter查询只需回答是 、否就可以,不需要像Query查询一样计算相关性分数,同时Filter 结果可以缓存
ES应该尽量避免深度翻页
正常翻页查询都是从from开始size条数据,这样就需要在每个分片中查询打分排名在前面的fron+size条数据,协同节点收集每个分配的前from +siez条数据,协同节点一共会收到N * (from + size)条数据,然后进行排序,再将其中from 到 from +size 条数据返回出去,如果from +size很大的话,导致参加排序的数量会同步扩大很多,最终导致CPU资源消耗增大
ES深度翻页可以使用ES的 scorll 和scroll-scan高效滚动的方式来解决这样的问题
也可以结合实际业务特点,文档id大小如果和文档创建时间是一致有序的话,可以以文档id作为分页的偏移量,并将其作为分页查询的一个条件
脚本使用主要有3中形式,内联动态编译方式,_script索引库中存储和文件脚本存储的形式;一般脚本的使用场景是粗排,尽量用第二种方式先将脚本存储在 _script索引库中,起到提前编译,然后通过引用脚本id,并结合params参数使用,既可以达到模型(逻辑)和数据进行分离,同时又便于脚本模块的扩展与维护
query cache:ES查询的时候,使用filter查询会使用query cache,如果业务场景中的过滤查询比较多,建议将query cache设置大一些,以提高查询速度
indices.queries.cache.size:10%(默认),可设置成百分比,也可设置成具体值,如256mb
当然也可以禁用查询缓存(默认是开启的),通过index.queries.cache.enabled:fales设置
FieldDataCache:在聚类或排序时,field data cache会使用频繁,因此,设置字段数据缓存的大小,在聚类或排序场景较多的情景下很有必要,可通过indices.fielddata.cache.size:30%或具体值10GB来设置,但是如果场景或数据变更比较频繁,设置cache并不是好的做法,因为缓存加载的开销也是特别大的
ShardRequestCache:查询请求发起后,每个分片会将结果返回给协调节点(Coordinating Node),由协调节点将结果整合,如果有需求,可以设置开启,通过设置index.requests.cache,enable:true来开启,不过,shard request cache只缓存hits.tootal,aggregations,suggestions类型的数据,并不会缓存hits的内容,也可以通过设置indices.requests.cache.size:1%(默认)来控制缓存空间大小
不论是数据库还是搜索引擎,对于问题的排查,开启慢查询日志是十分必要的,ES开启慢查询的方式有很多种,但是最常用的是调用模板API进行全局设置
PUT /_template/{TEMPLATE_NAME}
{
"template":"{INDEX_PATTERN}",
"settings" : {
"index.indexing.slowlog.level": "INFO",
"index.indexing.slowlog.threshold.index.warn": "10s",
"index.indexing.slowlog.threshold.index.info": "5s",
"index.indexing.slowlog.threshold.index.debug": "2s",
"index.indexing.slowlog.threshold.index.trace": "500ms",
"index.indexing.slowlog.source": "1000",
"index.search.slowlog.level": "INFO",
"index.search.slowlog.threshold.query.warn": "10s",
"index.search.slowlog.threshold.query.info": "5s",
"index.search.slowlog.threshold.query.debug": "2s",
"index.search.slowlog.threshold.query.trace": "500ms",
"index.search.slowlog.threshold.fetch.warn": "1s",
"index.search.slowlog.threshold.fetch.info": "800ms",
"index.search.slowlog.threshold.fetch.debug": "500ms",
"index.search.slowlog.threshold.fetch.trace": "200ms"
},
"version" : 1
}
PUT {INDEX_PAATERN}/_settings
{
"index.indexing.slowlog.level": "INFO",
"index.indexing.slowlog.threshold.index.warn": "10s",
"index.indexing.slowlog.threshold.index.info": "5s",
"index.indexing.slowlog.threshold.index.debug": "2s",
"index.indexing.slowlog.threshold.index.trace": "500ms",
"index.indexing.slowlog.source": "1000",
"index.search.slowlog.level": "INFO",
"index.search.slowlog.threshold.query.warn": "10s",
"index.search.slowlog.threshold.query.info": "5s",
"index.search.slowlog.threshold.query.debug": "2s",
"index.search.slowlog.threshold.query.trace": "500ms",
"index.search.slowlog.threshold.fetch.warn": "1s",
"index.search.slowlog.threshold.fetch.info": "800ms",
"index.search.slowlog.threshold.fetch.debug": "500ms",
"index.search.slowlog.threshold.fetch.trace": "200ms"
}
这样,在日志目录下的慢查询日志就会有输出记录必要的信息了
{CLUSTER_NAME}_index_indexing_slowlog.log
{CLUSTER_NAME}_index_search_slowlog.log
基于ES的使用场景,文档数据结构尽量和使用场景进行结合,去掉没用及不合理的数据
如果ES用于业务搜索服务,一些不需要用于搜索的字段最好不存到ES中,这样即节省空间,同时在相同数据量下,也能提高搜索性能
避免使用动态值作字段,动态递增的mapping,会导致集群崩溃,同样,也需要控制字段的数量,业务中不适用的字段,就不要索引,控制索引的字段数量,mapping深度,索引字段的类型,对于ES的性能优化是重中之重
ES对于字段数,mapping深度的一些默认设置:
index.mapping.nested_objects.limit: 10000
index.mapping.total_fields.limit: 1000
index.mapping.depth.limit: 20
尽量避免使用nested或parent/child的字段,能不用就不用;nested query慢,parent/child query更慢,比nested query慢上百倍,因此能在mapping设计阶段搞定的(大宽表设计或采用比较smart 的数据结构,就不要用父子关系的mapping)
如果一定要使用nested fields,保证nested fields字段不能过多,目前ES默认限制是50,因此针对一个document,每一个nsted field都会产生一个独立的document,这就使得doc数量剧增,影响查询效率,尤其是join的效率
index.mapping.nested_fields.limit: 50
尽量避免使用动态映射,这样有可能会导致集群崩溃,此外,动态映射有可能会带来不可控制的数据类型,进而有可能导致在查询端出现相关异常,影响业务
tip:ES作为搜索引擎时,主要承载query的匹配和排序功能,那数据的存储类型基于这两种功能的用途分为两类,一是需要匹配的字段,用来建立倒排索引对query匹配用,另一类字段是用作粗排用到的特征字段,如点击数,评论数等等
对于mysql,我们经常有一些复杂的关联查询,在es中复杂的关联查询尽量别用,一旦使用了性能一般较差
最好是先在java系统里完成关联,将关联好的数据直接写入es中,搜索的时候,就不需要利用es的搜索语法来完成join之类的关联搜索了
document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。
合理的部署ES有助于提高服务的整体可用性
ES集群在架构拓扑时,采用主节点,数据节点和负载均衡节点分离的架构,在5.x版本以后,又可以将数据节点再细分为Hot-Warm的架构模式
ES的配置文件中有两个参数,node.master和node.data,这两个参数搭配使用时,能够帮助提高服务器性能
配置node.master:true和node.data:false,该node服务器只作为一个主节点,但不存储任何索引数据,推荐每个集群运行3个专用的master节点来提供最好的弹性,使用时,还需要将discovery.zen.minimum_master_nodes setting参数设置为2,以免出现脑裂(split_brain)的情况,用三个专用的master节点,专门负责处理集群的管理以及加强状态的整体稳定性,因为这3个master节点不包含数据也不会实际参与搜索以及索引操作,在JVM上它们不用做相同的事,例如繁重的索引或者耗时,资源耗费很大搜索,因此不太可能因为垃圾回收而导致停顿,因此,master节点的CPU,内存以及磁盘设置可以比data节点少很多
配置node.master:false和node.data:false,该node服务器只作为一个数据节点,只用于存储索引数据,使该node服务器功能单一,只用于数据存储和数据查询,降低其资源消耗率
在ES 5.x版本之后,data节点又可以再细分为Hot-Warm架构,即分为热节点(hot node)和暖节点(warm node)
hot节点主要使索引节点(写入节点),同时会保存近期的一些频繁被查询的索引,由于进行索引非常耗费CPU和IO,即属于IO和CPU密集型操作,建以使用SSD的磁盘类型,保持良好的写入性能,推荐部署最少化的3个Hot节点来保证高可用,根据近期需要收集以及查询的数据量,可以增加服务器数量来获取的想要的性能
将节点设置为Hot类型需要elasticsearch.yml如下的设置
node.attr.box_type: hot
如果是针对执行的index操作,可以通过settings设置,index.rounting.allocation.require.box_type:hot将索引写入hot节点
这种类型的节点是为了处理大量的,而且不经常访问的只读索引索引而设计的,由于这些索引是只读的,warm节点倾向于挂载大量磁盘(普通磁盘)来替代SSD,内存,CPU的配置跟hot节点保持一致即可,节点数量一般也是大于等于3个
将节点设置为warm类型需要elasticsearch.yml如下设置
node.attr.box_type: warm
同时,也可以再elasticsearch.yml中设置index.code:best_compression保证warm节点的压缩配置
当索引不再被频繁查询时,可以通过index.rounting.allocation.require.box_type:warm,从而保证索引不写入hot节点,以便将SSD资源用在刀刃上,一旦设置了这个属性,ES会自动将索引合并到warm节点
协调节点用于分布式里的协调,将各分片或节点返回的数据整合后返回,该节点不会被选做主节点,也不会存储任何索引数据,该服务器主要用于查询负载均衡,在查询的时候,通常会涉及到从多个node服务器上查询数据,并将请求分发到多个指定的node服务器,并对各个node服务器返回的数据统一进行一个汇总处理,最终返回给客户端。在ES集群中,所有的节点都有可能是协调节点,但是,可以通过设置node.master,node.data,node.ingest都为false来设置专门的协调节点,需要较好的CPU和较高的内存
针对ES集群中的所有数据节点,不用开启http服务,将其中的配置参数这样设置,http.enabled;false,同时也不要安装head,bigdesk,marvel等监控插件,这样保证data节点服务器只需要处理创建、更新、删除、查询索引数据等操作
http功能可以在非数据节点服务器上开启,上述相关的监控插件也安装到这些服务器上,用于监控ES集群状态等数据信息,这样做一来出于安全考虑,而来处于服务性能考虑
一台物理服务器上可以启动多个node服务器节点(通过设置不同的启动port),但一台服务器上的CPU,内存,硬盘等资源毕竟有限,从服务性能考虑,不建议一台服务器上启动多个node节点
ES 一旦创建好索引后,就无法调整分片的设置,而在 ES 中,一个分片实际上对应一个 lucene 索引,而 lucene 索引的读写会占用很多的系统资源,因此,分片数不能设置过大;所以,在创建索引时,合理配置分片数是非常重要的。一般来说,我们遵循一些原则:
控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置(一般设置不超过 32 G,参考上面的 JVM 内存设置原则),因此,如果索引的总容量在 500 G 左右,那分片大小在 16 个左右即可;当然,最好同时考虑原则 2。 考虑一下 node 数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了 1 个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以,一般都设置分片数不超过节点数的 3 倍。