【1】概念性知识
数据类型
字符串#
- text:用于全文索引,该类型的字段将通过分词器进行分词
- keyword:不分词,只能搜索该字段的完整的值
数值型#
- long、integer、short、byte、double、float、half_float、scaled_float
布尔#
- boolean
二进制#
- binary:该类型的字段把值当做经过base64编码的字符串,默认不存储,且不可搜索
范围类型#
- 范围类型表示值是一个范围,而不是一个具体的值
- integer_range、float_range、long_range、double_range、date_range
- 比如age类型是integer_range,那么值可以是{"gte":20,"lte":40};搜索"term":{"age":21}可以搜索该值
日期-date#
由于json类型没有date类型,所以es通过识别字符串是否符合format定义的格式来判断是否为date类型
format默认为:strict_date_optional_time || epoch_millis
格式
"2022-01-01" "2022/01/01 12:10:30" 这种字符串格式
从开始纪元(1970年1月1日0点)开始的毫秒数
Search API 概述
- URI Search
- 在URL中使用查询参数
- Requests Body Search
- 使用ES提供的,基于JSON格式的更加完备的 query Domain Specific Language(DSL)
指定查询的索引
URI查询
Request body
查询返回结果解析
衡量相关性(Precision,Recall,Ranking)
information retrieval
- Precision(查准率):尽可能的返回较少的无关文档
- Recall(查全率):尽量返回较多的相关文档
- Ranking:是否能够按照相关度进行排序?
URI Search 详解
- q:指定查询语句,使用 Query String Syntax
- df: 默认字段,不指定是,会对所有字段进程查询
- Sort:排序 / from 和 size 用于分页
- Profile 可以查看查询是如何被执行的
(1)指定字段与泛查询
q=title:2012 / q=2012
GET /movies/_search?q=2012&df=title #泛查询,但默认只搜索 title字段
GET /movies/_search?q=2012 #泛查询,对应_all,搜索所有字段
GET /movies/_search?q=title:2012 #指定字段
GET /users4/_search?q=user:"lisi"&q=age:30 # 多字段查询
GET /movies/_search?q=title:2012
{
"profile":"true"
}
GET /movies/_search?q=title:beautiful Mind #查找美丽心灵,在title中查询beautiful,Mind为泛查询
(2)分词与词组查询 (Term、phrase)
# 双引号括起来,就相当于PhraseQuery,整个标题这2个单词都出现过,且还要求前后顺序保持一致
GET /movies/_search?q=title:"Beautiful Mind"
# 查找美丽心灵,在title中查询beautiful,Mind为泛查询(每个字段都查)
GET /movies/_search?q=title:beautiful Mind
# 分组,Bool查询,两个term在括号中默认是 or的关系,查询 title中包含 Beautiful分词 或者 Mind 分词
GET /movies/_search?q=title:(Beautiful Mind)
(3)分组()与引号""
- title:(Beautiful Mind):表示查询 title 中的出现 beautiful Mind 或者 Mind 的 =》 Beautiful and Mind
- title="Beautiful Mind":表示 "Beautiful Mind" 是一个整体,查询 title 中这2个词都出现过的,并且还要求前后顺序保持一致
具体案例见上面(2)中的代码;
(4)Bool 布尔操作
布尔操作:
AND / OR / NOT 或者 && / || / !
- b必须大写
- title:(matrix NOT reloaded)
- 默认为 OR,如:GET /movies/_search?q=title:(Beautiful Mind) #查询 title中包含 Beautiful 或者 Mind
分组
- +表示 must
- -表示 must_not
- titile:(+matrix - reloaded)
(5)范围查询 [] 与 {} (闭区间,开区间)
区间表示:[] 闭区间,{} 开区间
- year:{2019 TO 2018}
- year:[* TO 2018]
算数符号:
- year:>2012
- year:(>2010 && <=2018)
- year(+>2012 +<=2018)
(6)通配符查询、正则、模糊匹配与近似查询
通配符查询:不推荐,很费性能
?代表1个字符,*表示0个或者多个字符
- title:mi?d
- title:be*
正则
- title:[bt]oy
模糊匹配与近似查询
- title:beautifl~1 :比如 本来应该是 beautiful,但是我们少打了一个 ful 打成了 fl;用模糊查询,可以自动识别出 beautiful
- title:"lord rings"~2:比如 lord of the rings 就会被搜索出来
Request body search(Query DSL)
将查询语句通过 http Request body 发送给 ES
(1)一般形式
(2)分页
- From 默认从0开始,默认返回10个结果;
- size 表示获取多少个结果;
- 上图中的分页表示,从10开始,获取后面的20个结果
- 注意,获取靠后的翻页成本较高
(3)排序
如上图,对搜索结果进行了排序
- 最好在 数字类型、日期类型 字段上排序
- 因为对于多值类型或分析过的字段排序,系统会选一个值,无法得知该值
(4)_source filtering
_source 里面包含了该文档所有内容
过滤:
- 如果_source 没有存储,那么就只返回匹配的文档的元数据
- 是 _source 支持使用通配符,如 "_source":["name*","desc*"]
(5)脚本字段(拼接、计算)
7.11测试
(6)query match 与 match_phrase
(1)query match
-
如果是直接写,如上图中的上半区,那么会是两个term 都以 or 的形式出来;即包含 Last 或者 包含 Christmas 的;
-
下半区,可以加上操作符 operator:and 就表示是两个分词是 and ,要同时包含才行;
(2)query match_phrase
- 如果我们直接查,不加slop 参数,则默认是2个词要连在一起才出来,比如 aaa one love;
- 如果我们加上 slop:1 参数,则 如上图,One I Love 也会被检索出来 (类似于 title:beautifl~1 )
(7)query string query / simple query string
(1)query string query
- 这里面的 AND 是逻辑符
- 在右图中,也可以使用分组
(2)simple query string
- 类似 Query String,但是会忽略错误的语法,同时只支持部分语法
- 不支持在 query 中直接使用 AMD OR NOT ,会当做字符串处理
- Term之间的默认关系是 OR,可以指定 Operator
- 支持 部分逻辑
- + 替代 AND
- | 替代 OR
- - 替代 NOT
(8)query(exist,prefix,wildcard,regexp,ids)
- Term query 精准匹配查询(查找号码为23的球员)
- Exsit Query 在特定的字段中查找空值的文档(查找队名空的球员)
- Prefix Query 查找包含带有指定前缀term的?档(查找队名以Rock开头的球员)
- Wildcard Query 支持通配符查询,*表示任意字符,?表示任意单个字符(查找?箭队的球员)
- Regexp Query 正则表达式查询(查找?箭队的球员)
- Ids Query(查找id为1和2的球员),这个id为 _id 元数据
Dynamic-Mapping
(1)什么是 Mapping
Mapping 类似于数据库中的schema的定义,作用如下
- 定义索引中的字段的名称
- 定义字段的数据类型,例如字符串,数字,布尔....
- 字段,倒排索引的相关配置,(Analyzed or Not Analyzed,analyzer)
Mapping会把 JSON文档映射成 Lucene 所需要的扁平格式
一个Mapping 属于一个索引的 Type
- 每个文档都属于一个Type
- 一个Type有一个 Mapping 定义
- 7.0开始不需要在 Mapping 定义中指定 Type,因为有且只有一个,那就是_doc
(2)字段的数据类型
(3)Dynamic-Mapping
- 就是说当写入文档时,如果索引不存在,会自动创建索引;
- 无需手动定义,ES会自动根据文档信息,推算出字段类型
- 但有时候不一定对,比如地理位置
- 当类型如果设置的不对,会导致一些功能无法使用,比如无法对字符串类型使用 range 查询
可以通过 GET /users/_mapping
能否更改 Mapping 字段类型?
- 情况1:新增加字字段
- Dynamic 默认为 True:新增字段可增加,数据可被索引,Mapping 也更新
- Dynamic 如果为 False:新增字段无法被检索,文档可被搜索,_source中也有,但Mapping 无法被更新
- Dynamic 设置为 Strict:不符合当前Mapping 的新文档无法被写入
- 对易游字段,一旦已经有数据写入,就不再支持修改字段定义
- Lucene 实现的倒排索引,一旦生成后,就不允许修改
- 如果希望改变字段类型,必须Reindex API 重建索引
- 原因
- 如果修改了字段的数据类型,会导致已被索引的术语无法被搜索
- 但如果是增加新的字段,就不会有这样的影响
自定义 Mapping
(1)建议
- 可以参考API手册,纯手写
- 为了减少出错率,提高效率,可以按照下列步骤
- 创建一个临时的 index,写入一些样本数据
- 通过访问 GET /indexname/_mapping 获取该临时索引的动态 Mapping定义
- 自定义修改,达到自己想要的效果并创建自己的索引
- 删除临时索引
(2)控制当前字段是否被索引
index - 控制当前字段是否被索引。默认为 ture。如果设置为 false,则该字段不可被搜索,如下面代码中的 mobile 字段
PUT users5
{
"mappings":{
"properties":{
"firstName":{"type":"text"},
"lastName":{"type":"text","index_options:"offsets"},
"mobile":{"type":"text","index":false}
}
}
}
(3)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 offects
Text类型,默认记录 positions,其他默认为 docs
记录的内容越多,占用的存储空间越大
配置:如(2)中的 "lastName":{"type":"text","index_options:"offsets"}
(3)null_value
如果有需要对 NULL 值实现搜索,那就要使用 null_value:"null",且只有 keyword 类型支持设置 Null_value
在mapping中设置如下:
"messages":{"type":"keyword","null_value":"NULL"}
(4)ES7中 copy_to 替代 _all
- _all 在 7 中被 copy_to 所替代
- 满足一些特定的搜索需求
- copy_to 将字段的数值拷贝到目标字段,实现类似 _all 的作用
- copy_to 的目标字段不出现在 _source 中
- 如下列代码,就可以用 fullname 来搜索 这两个字段;
PUT users5
{
"mappings":{
"properties":{
"firstName":{"type":"text","copy_to":"fullName"},
"lastName":{"type":"text","copy_to":"fullName"}
}
}
}
(5)数组
POST users5/_doc/
{
"firstName":["tom","tony"],
"lastName": "jack"
}
发现结果,数组字段,依旧是 text 类型
(6)多字段类型
(7)keyword 与 text 区别
Excat values VS Full Text
- Exact Value: 包括数字 / 日期 / 具体一个字符串(例如 "app Store")
- 其实就是 ES 中的 Keyword 类型,不需要分词,全内容为索引内容
- 全文本,非结构化的全文本数据
- ES 中的 text ,一般针对该类字段做 分词
Index-Template 和 Dynamic-Template
(1)Index-Template 介绍
-
Index Templates:帮助你设定 Mappings 和 Settings ,并且按照一定的规则,自动匹配到新创建的索引之上
- 模板仅在一个索引被新创建时,才会起作用。修改模板不会影响已创建的索引
- 你可以设定多个索引模板,这些设置会被 " merge " 在一起
- 你可以指定 " order " 的数值,控制 "merging " 的过程
-
演示
PUT _template/template_default
{
"index_patterns": ["*"],
"order": 0
, "version": 1
, "settings": {
"number_of_replicas": 1
, "number_of_shards": 1
}
}
PUT _template/template_test
{
"index_patterns": ["test*"]
, "order": 1
, "settings": {
"number_of_shards": 1
, "number_of_replicas": 2
}
, "mappings": {
"date_detection": false
, "numeric_detection": true
}
}
- date_detection:默认情况下,是否在字符串中的日期,自动转换为日期数据类型
- numeric_detection:默认情况下,字符串如果是纯数字字符串,是否自动转换成数字类型
(2)Index Template 的工作方式
- 当一个索引被新建时
- 应用 ES 默认的 settings 和 mappings
- 应用 order 数值低的 Index Template 中的设定
- 应用 order 高的 Index Template中的设定,之前的设定会被覆盖
- 应用创建索引时,用户显示指定的 Settings 和 Mappings,并覆盖之前模板中的设定
(3)Index Template 演示案例
如上图,我们发现真的应用了 template_test 模板的 mapping
如下图,我们发现真的应用了 template_test 模板的 setting
疑惑:为什么不应用 template_default 模板 呢?
- 因为我们之前上面(2)中说了,会先应用 order 低的,再应用 order 高的,且高的配置会覆盖低的
- 所以,template_test 的 order 是 1 ,比 template_default 的 order 高,所以 test 开头的索引,会应用 template_test的配置,覆盖 template_default 上的配置;
(4)Dynamic Template 介绍
-
根据 ES 识别的数据类型,结合字段名称,来动态设定字段类型
-
比如说:
- 所有的字符串类型都设置成 Keyword,或者关闭 Keyword 字段
- is 开头的字段都设置成 boolean
- long_开头的都设置成 long 类型
-
基本形式参考如下:
-
PUT test4 { "mappings": { "dynamic_templates":[ { "string_as_boolean":{ "match_mapping_type":"string", "match":"is*", "mapping":{ "type":"boolean" } } }, { "string_as_keyword":{ "match_mapping_type":"string", "mapping":{ "type":"keyword" } } } ] } } PUT test4/_doc/1 { "user":"zhangsan", "isVip":"true" } GET test4/_mapping
(5)Dynamic Template 演示案例
如上图,证明我们配置成功!
其他案例:
DELETE test4
PUT test4
{
"mappings": {
"dynamic_templates":[
{
"full_name":{
"path_match":"name.*",
"path_unmatch":"*.middle",
"mapping":{
"type":"text",
"copy_to":"full_name"
}
}
}
]
}
}
PUT test4/_doc/1
{
"name":{
"first":"john",
"middle":"winston",
"last":"lennon"
}
}
GET test4/_search?q=full_name:lennon
【2】搜索、结构化搜索
聚合(Aggregation)的简介
(1)聚合的介绍
- ES 除搜索外,提供的针对 ES 数据进行统计分析的功能
- 实时性高
- Hadoop(T+1):也就是说如果是 Hadoop 来分析,怕是要一整天
- 通过聚合,我们会得到一个数据的概览,是分析和总结全套的数据,而不是寻找单个文档
- 尖沙咀和香港岛的可烦数量
- 不同的价格区间,可预订的经济型酒店和五星级酒店的数量
- 高性能,只需要一条语句,就可以从 ES 得到分析结果
- 无需在客户端自己去实现分析逻辑
(2)聚合的分类
- Bucket Aggregation:一些列满足特定条件的文档的集合
- Metric Aggregation:一些数据运算,可以对文档字段进行统计分析
- Pipeline Aggregation:对其他的聚合结果进行二次聚合
- Matrix Aggregration:支持对多个字段的操作,并提供一个结果矩阵
Bucket:一组满足条件的文档:可以初步理解是关系型数据库 group by 后面的
Metric:一些系统的统计方法:可以理解成是关系型数据库中的聚合运算,如 count() max(),stats 包含 count,min,max,avg,sum
(3)Bucket 演示案例
这个就相当于 select * from group by column ,Bucket 就相当于是 group by 操作
Bucket的例子,关键词是 aggs
- 如果使用的聚合字段是 text 类型,那么它的聚合分组是 text 分词后的数据
- 如果使用的聚合字段是 text 类型,那么它的 mapping 设置必须开启 fielddata 参数
- 如:
- 常规的形式如下:
GET users4/_search
{
"size":0,
"aggs": {
"user_group": {
"terms": {
"field": "user"
"size":3 #这里面也可以写 size,如果是3,那就去分组后,前三行
}
}
}
如下图:查询年龄大于20的 根据Job字段分组查询
如果是text类型:
- 如下图,第一个查询中 job 字段为 text 类型,查出的结果会根据 job 内容分词后聚合查询
- 如下图,第二个查询中,写的是 job.keyword,这样的话,就把整个 text 类型字段的文本做为一个单位来分组聚合,就不会分词了;
不同工作类别中,查看年纪最大的3个员工的具体信息
(4)优化 Term 聚合查询
我们上面的信息,都可以称之为 Term 聚合查询
- 可以通过在 keyword 字段上把 eager_global_ordinals 参数打开
- 这样在有新数据写入时,会把新数据的 term 加入到缓存中来,这样再做 term aggs 的时候,性能会得到提升
- 什么时候需要打开该参数?
- 聚合查询非常频繁
- 对聚合查询操作的性能要求较高
- 持续不断的高频文档写入
(5)Metric 计算类聚合(max,min等)
GET users4/_search
{
"size":0,
"aggs": {
"user_group": {
"terms": {
"field": "user"
}
, "aggs": {
"max_age": {
"max": {
"field": "age"
}
},
"avg_age":{
"avg":{
"field": "age"
}
}
}
}
}
}
还可以相互嵌套!!
(6)range 范围查询分桶
(7)Histogram
相当于SQL中的 where salary>=0 and salary <=100000 group by salary/5000
(8)嵌套聚合
相当于 select max,min,avg,sum,count from tab group by job
相当于: select max,min,avg,sum,count from tab group by job,gender
(9)Pipline: min_bucket
主要是用来在一个聚合的结果集上 再次聚合
(10)聚合分析的原理,精准度分析
如下的 top 操作,就结果不一定准确,因为可能某一个节点包含了
基于Term与text的搜索
(1)基于 Term 的查询
Term 是表达语意的最小单位
特点:
- Term Level Query / Term Query / Range Query / Exists Query / Prefix Query / Wildcard Query
- 在ES 中,Term 查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项,并且使用相关度算分公式,为每个包含该词项的文档进项相关度算分;
- 可以通过 Constant Score 将查询转换成一个 Filtering ,避免算分,并利用缓存,提高性能
基本形式:
POST users4/_search
{
"query":{
"term": {
"user": {
"value": "lisi"
}
}
}
}
- 注意,大多数分词器会自动把分词信息转换为小写,如果这里写大写的 lisi ,就检索不到内容;
- 而且,这里的 term 中的 value 已经是最小单位的一个整体,不会再拆分;
完全匹配,当做 Keyword来匹配,如下:
POST users4/_search
{
"query":{
"term": {
"user.keyword": {
"value": "lisi"
}
}
}
}
- 而且,针对与数组类型的多值字段,比如 a:["q","w"] 的字段;
- term查询的值,是包含该值,而不是完全匹配
- 解决方案:增加一个genre_count字段进行计数。会在组合 bool query 给出解决方案
(2)基于全文的查找(match query)
其实包含:
- Match Query / Match Phrase Query / Query String Query
特点:
- 索引和搜索时都会进行分词,需要查询的字符串会先传递到一个合适的分词器,然后生成一个供查询的词项列表
- 查询的时候,会先对输入的查询进行分词,然后每个词项逐个进行底层的查询,最终将结果进行合并。并为每个文档生成一个算分; - 例如查 "hello world" ,会查到 包含 hellp 或者 world 的所有结果
一般形式
GET users4/_search
{
"query":{
"match":{
"user":{
"query":"lisi wangwu"
"operator":"OR"
"mininum_should_match":2
}
}
}
}
#多字段 multi_match
GET /customer/doc/_search/
{
"query": {
"multi_match": {
"query" : "blog",
"fields": ["name","title"] #只要里面一个字段包含值 blog 既可以
}
}
}
"mininum_should_match":2
(3)Match query原理
(4)复合查询 -- Constant Score 转为 Filter
-
将 Query 转成 Filter ,忽略 TF-IDF 计算,避免相关性 score算分的开销
-
Filter 可以有效利用缓存
(5)总结
- 基于词项的查找 VS 基于全文的查找
- 通过字段 Mapping 控制字段的分词
- "Text" VS "Keyword"
- 通过参数控制查询的 Precision & Recall
- 复合查询 -- Constant Score 查询
- 即使是对 Keyword 进行 Term 查询,同样会进行算分
- 可以将查询转为 Filtering,取消相关性算分环节,提升性能
相关性算分
- 相关性 - Relevance
- 搜索的相关性算分,描述了一个文档的查询语句匹配的程序。ES 会对每个匹配查询条件的结果进行算分 _score
- 打分的本质是排序,需要把最符合用户需求的文档排在前面。ES 5 之前,默认的相关性算分采用 TF-IDF,之后采用BM25;
(1)TF-IDF
比如这么一段文本:区块链的应用
被分为: 区块链 、 的 、 应用 ,三个 Term
- TF(词频):Term Frequency:检索词在一篇文档中出现的评率 -- 检索词出现的次数 / 文档的总字数
- 度量一条查询和结果文档相关性的简单办法:简单讲搜索中每一个词的 TF 进行相加
- TF(区块链) + TF(的) + TF(应用)
- Stop word
- "的" 在文档中出现了很多次,但是对于贡献相关度几乎没有用处,所以不应该考虑他们的 TF
- 度量一条查询和结果文档相关性的简单办法:简单讲搜索中每一个词的 TF 进行相加
- -----------------分割线 -------------------------
- DF:检索词在所有文档中出现的频率
评分公式:
(2)BM 25
- 从ES 5 开始,默认算法改为 BM 25
- 和经典的 TF - IDF 相比,当 TF 无限增长时,BM 25 算分会趋于一个数值
- 可以通过 explain api 查看 TF-IDF 的算分过程
- PUT users/_search{ "explan":true , query:..... }
(3)Boosting & Bootsting query 控制查询打分
(4)Bool 查询
- 一个 bool 查询,是一个或者多个查询自居的组合
- 总共包括4种自居,其中2种会影响算分,2种不影响算分
- 可以嵌套查询,不同层次算分情况不同,越高层次算分越高
- 相关性算分不只是全文检索的专利。也适用于 yes|no 的自居,匹配的自居越多,相关性评分越高;
- 如果多条查询自居被合并为一条符合查询语句,比如 bool 查询,则每个查询自居计算得出的评分会被合并到总的相关性评分中;
-
must :必须匹配,贡献算分
-
should:选择性匹配,贡献算分
-
must_not:Filer Context,查询子句,必须不能匹配
-
filter:Filter Context,必须匹配,但不算贡献分
【分布式】配置跨集群搜索
(1)水平扩展的痛点
- 单集群 - 当水平扩展石,节点数不能无限增加
- 当集群的 meta 信息(元数据信息,节点、索引、集群状态)过多,会导致更新压力变大,单个 Active Master 会成为性能瓶颈,导致整个集群无法正常工作
- 早期版本:通过Tribe Node 可以实现多集群访问的需求,但还是存在一定的问题
- Tribe Node 会以 Client Node 的方式加入每个集群。集群中的 Master 节点的任务变更需要 Tribe Node 的回应才能继续
- Tribe Node 不保存 Cluster state 的信息,一旦重启,初始化很慢
- 当多个集群存在索引重名的情况,只能设置一种 Prefer 规则
(2)跨集群搜索 -- Cross Cluster Search
- ES 5.3 引入了跨集群搜索的功能(Cross Cluster Search),推荐使用
- 允许任何节点扮演 federated 节点,以轻量的方式,将搜索请求进行代理
- 不需要以 client Node 的形式加入其它集群
- 集群配置如下图:
- 左边的是设置单个集群的主机发现信息(每个集群都需要操作设置),就是写集群包含了哪些节点,会搜索哪些节点
- 右边第一个是查询 cluster_one集群下的 tmdb,movies 索引
- 右边第二个是配置,当某个远程集群不可用了,就跳过搜索这个集群
(3)CURL 发送ES 请求
测试数据构造:
- 在本机启动了 3个单实例 构造成 3个集群
- 每个节点搜索
(4)测试跨集群搜索
【分布式】文档的分布式存储
(1)文档在集群上的存储概述
- 单个文档直接会存在(记住是作为一个整体单位) 某一个主分片和副本分片上
- 文档到分片的映射算法
- 确保文档能均匀的分部在所用的分片,充分利用硬件资源,避免部分机器空闲,部分机器繁忙
- 潜在的算法
- 随机 / Round Robin。当查询文档1,分片数很多的时候,需要多次查询才可能查到文档1(因为要扫描各个分片直到找到文档1所在的分片,和全表扫描似得)
- 维护文档到分片的映射关系,当文档数量大的时候,维护成本高
- 实时计算,通过文档1,自动算出需要去哪个分片上获取文档
(2)文档到分片的路由算法(_routing)
-
shard = hash(_routing) % number_of_primary_shards
-
hash 算法宝珠文档均匀分散到分片中
-
默认的 _routing 是文档的 id 值
-
可以自行制定 routing数值,例如用相同国家的商品,都分配到相同指定的 shard,例如下图:
-
设置 index Setting 后,primary 片数 不能随意修改,因为这个算法是得出的 hash值数字,%主分片数;一旦打乱则会找不到数据所在的正确分片,从而导致问题;
-
(3)更新、删除一个文档的过程
《1》更新一个文档
《2》删除一个文档
【分布式】分片原理及其生命周期
(1)分片的内部原理
-
什么是 ES 分片?
- ES 中最小的工作单元 / 是一个lucene 的 index
-
一些问题:
- 为什么 ES 的搜索是近实时的(1S 后被搜到)
- ES 如何保证断电时数据也不会丢失
- 为什么删除文档,并不会立刻释放空间
(2)倒排索引不可变性
- 倒排索引采用 Immutabe Design , 一旦生成,不可更改
- 不可变性,带来的好处如下:
- 无需考虑并发写文件的问题,避免了锁机制带来的性能问题
- 一旦读入内核的文件系统缓存,便留在那里。只要文件系统存有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能
- 缓存容易生成和维护 / 数据可以被压缩
- 不可变更性引起的问题:如果需要让一个新的文档可以被搜索,需要重建整个索引
(3)Lucene Index(删除文档不会立即释放空间)
- 在 Lucene 中,单个倒排索引文件被称为 Segment. Segment 是自包含的,不可变更的,多个 Segments 汇总一起,称为 Lucene 的 Index,其实对应的就是 ES 中的 Shard;
- 当有新文档写入时,会生成新的 Segment。查询时会同时查询所有的 Segments,并且对结果汇总。Lucene 中有一个文件,用来记录所有的 Segments 信息,叫做 Commit Point
- 删除文档的信息,保存在 .del 文件中,检索的都是会过滤掉 .del 文件中记录的 Segment
- Segment 会定期 Merge,合并成一个,同时删除已删除文档
(4)ES 的 Refresh(近实时)
- 将index buffer 写入 Segment buffer 的过程叫做 Refresh. Refresh 不执行 fsync 操作
- Refresh 频率:默认1s 发生一次,可以通过 index.refresh_interval配置。Refresh 后,数据就可以被搜索到了;这也是为什么ES 被称为近实时搜索;
- 如果系统有大量的数据写入,那就会产生很多的 Segment
- Index Buffer 被占满时,会触发 Refresh 默认值是 JVM 的 10%
(5)ES 的 TransLog(断电不丢失)
- Segment 写入磁盘的过程相对耗时,借助文件系统缓存, Refresh 时,先将Document信息 写入Segment 缓存,以开放查询
- 为了保证数据不会丢失,所以在 index/操作 文档时,同时写 Tranaction Log,高版本开始,Transaction Log 默认落盘,每个分片有一个 Transaction Log
- translog 是实时 fsync 的,既写入 es 的数据,其对应的 translog 内容是实时写入磁盘的,并且是以顺序 append 文件的方式,所以写磁盘的性能很高。只要数据写入 translog 了,就能保证其原始信息已经落盘,进一步就保证了数据的可靠性。
- 在 ES Refresh 时, Index Buffer 被清空, Transaction log 不会清空;
- 如果断电, Segment buffer 会被清空,这个时候就根据 Transaction log 来进行恢复
- 操作参考
index.translog.flush_threshold_ops,执行多少次操作后执行一次flush,默认无限制
index.translog.flush_threshold_size,translog的大小超过这个参数后flush,默认512mb
index.translog.flush_threshold_period,多长时间强制flush一次,默认30m
index.translog.interval,es多久去检测一次translog是否满足flush条件
index.translog.sync_interval 控制translog多久fsync到磁盘,最小为100ms
index.translog.durability translog是每5秒钟刷新一次还是每次请求都fsync,这个参数有2个取值:request(每次请求都执行fsync,es要等translog fsync到磁盘后才会返回成功)和async(默认值,translog每隔5秒钟fsync一次)
(7)ES 的 Flush
- ES Flush & Lucene Commit
- 调用 Refresh,Index Buffer 清空并且 Refresh
- 调用 fsync,缓存中的 Segments 写入磁盘,且写入commit point 信息
- 清空(删除)旧的 Transaction Log
- 默认30分钟调用一次
- Transaction Log 满(默认 512MB)
index.translog.flush_threshold_ops,执行多少次操作后执行一次flush,默认无限制 index.translog.flush_threshold_size,translog的大小超过这个参数后flush,默认512mb index.translog.flush_threshold_period,多长时间强制flush一次,默认30m index.translog.interval,es多久去检测一次translog是否满足flush条件
- 上面的参数是es多久执行一次flush操作,在系统恢复过程中es会比较translog和segments中的数据来保证数据的完整性,为了数据安全es默认每隔5秒钟会把translog刷新(fsync)到磁盘中,也就是说系统掉电的情况下es最多会丢失5秒钟的数据,如果你对数据安全比较敏感,可以把这个间隔减小或者改为每次请求之后都把translog fsync到磁盘,但是会占用更多资源;这个间隔是通过下面2个参数来控制的:
index.translog.sync_interval 控制translog多久fsync到磁盘,最小为100ms
index.translog.durability translog是每5秒钟刷新一次还是每次请求都fsync,这个参数有2个取值:request(每次请求都执行fsync,es要等translog fsync到磁盘后才会返回成功)和async(默认值,translog每隔5秒钟fsync一次)
(8)ES 的 Merge
- Segment 很多,需要被定期合并
- 减少 Segments / 删除已经删除的文档
- ES 和 Lucene 会自动进行 Merge 操作
- 手动操作:POST my_index/_forcemerge
【分布式】分布式查询
(1)Query 阶段
- 用户发出搜索请求到 ES 节点(假设一共6个分配,3个 M 3个 S )
- ES 节点收到请求后,会以 coordinating 节点的身份,在6个中的其中3个分配,发送查询请求
- 被选中的分片执行查询,进行排序(算分排序)
- 然后每个分配都会返回 From + Size 个排序后的文档 ID 和排序值给 Coordinating 节点
(2)Fetch
- Coordinating 节点将会从 Query阶段产生的数据;从每个分配获取的排序后的文档 id 列表、排序值列表,根据排序值重新排序。选取 From 到 From + Size 个文档的 Id
- 以 multi get 请求的方式,到想要的分片获取详细的文档数据
(3)Query then Fetch 潜在的问题
- 性能问题
- 每个分配上需要查的文档个数 = From + Size
- 最终协调节点需要处理: number_of_shard * ( from + size )
- 深度分页 引起的性能问题
- 相关性算分
- 每个分配都 基于自己的分片上的数据 进行相关度计算。这回导致打分偏离的情况,特别是数据量很少的时候。
- 相关性算分在分片之间是相互独立的。当文档总数很少的情况下,如果主分片大于1,主分片越多,相关性算分越不准
(4)解决算分不准的问题
- 数据量不大的时候,可以将主分片设置为 1
- 当数据量足够大的时候,只要保证文档均匀的分散在各个分片上,结果一般就不会出现偏差
- 使用 DFS Query Then Fetch
- 搜索的 URL 中指定参数 "_search?search_type=dfs_query_then_fetch"
- 到每个分配,把各个分片的磁盘和文档频率进行搜集,然后网站的进行一次相关性算分,耗费很多的CPU、内存 资源,执行性能低下,一般不建议使用
sort 排序及 Doc-Values与Fielddata
(1)排序的过程
- 排序是针对字段原始内容进行的。倒排索引无法发挥作用
- 需要用到正牌索引。通过文档 ID 和字段快速得到字段原始内容
- ES 有两种实现方式(排序、聚合分析等都是依靠它)
- Fielddata (一般用于开启text类型)
- 默认启用,可以通过 Mapping 设置关闭(增加索引的速度 / 减少磁盘空间)
- 如果重新打开,需要重建索引
- 明确不需要做排序、聚合分析等操作时,才手动关闭
- 手动关闭代码: 在 _mapping 下 properties 下 字段名下 "doc_values":"false"
- Doc Values(默认开启,列式存储,对 Text 类型无效),ES2.X之后,默认使用它;
- Fielddata (一般用于开启text类型)
- 关闭 Doc values
(2)两种排序方式的对比
分页与遍历:From-Size-Search-After-Scroll
(1)基本分页形式
- 默认情况下,按照相关度算分排序,返回前10条记录
- 容易理解的分页关键词方案:
- From :开始位置
- Size:期望获取文档的数量
(2)分布式系统中深度分页问题
-
如上图,当一个查询: From = 990 , Size = 10
-
会在每个分配上都先获取1000个文档。
然后在通过 Coordinating Node 聚合所有结果。最后在通过排序选取前1000和文档
-
页数越深,占用内存越多。为了避免深度分页带来的内存开销。ES 有一个设定,默认限定到 10000 个文档
即 Index.max_result_window 参数
-
(3)Search After 避免深度分页
- 避免深度分页的性能问题,可以实时获取下一页的文档信息
- 不支持指定页数( From )
- 只能往下翻(局限性)
- 第一步收缩需要指定 Sort,并且保证 Sort字段值是唯一的(可以如上图加上 _id 保证唯一性 )
- 然后使用上一次,最后一个文档查询出结果的 Sort 值进行查询
如何执行?
- 第一次运行,不需要加入 Search_after 参数,因为不知道其 sort值是多少
第二次执行,就要把上次查询出来的 sort值,放入 search_after 关键字中;如上图
Search After 是如何解决深度分页的问题的?
- 假设 Size 是 10
- 当查询 990 - 1000 时
- 通过 唯一排序值,快速利用正排索引定位文档读取位置,然后读取该位置下面的 10个文档,这样就将每次要处理的文档都控制在10
(4)Scroll 查全索引
- 这个时间其实指的是es把本次快照的结果缓存起来的有效时间。
scroll 参数相当于告诉了 ES我们的search context要保持多久,后面每个 scroll 请求都会设置一个新的过期时间,以确保我们可以一直进行下一页操作。 - 快照只能生成当前的,当你利用这个 scroll_id 一直往下滚动搜索的话,那么只能获取到使用 _search?scroll=5 的时候的快照数据,之后的数据是看不到的
(5)总结:搜索类型和使用场景
- 默认的 Regular:
- 需要试试获取顶部的部分文档。例如查询最新的订单
- 默认 from = 0 , size = 10
- 滚动的 Scroll:
- 需要全部文档,例如导出全部数据
- 使用 Scroll = 5m ,快照生成
- 页码的 Pagination:
- From 和 Size
- 如果需要深度分页,则选用 Search After
并发控制
(1)并发控制的必要性
例子:
- 两个程序同时更新某个文档,如上图,两个程序同时修改某个文档的字段,ES是没有锁的,所以如果不做并发控制,会导致更新丢失问题
- 悲观并发控制
- 嘉定有变更冲突的可能,会对资源加锁,防止冲突。例如数据库行锁
- 但我们知道 ES 是没有锁的,所以不会使用这个
- 乐观并发控制
- 嘉定冲突是不会发生的,不会阻塞正在尝试的操作。如果数据在读写中被修改,更新将会失败。应用程序决定如何解决冲突,例如重试更新,使用新的数据,或将错误报告给用户
- ES 采用的是乐观并发控制
(2)ES 的乐观并发控制
- ES 中的文档是不可变更的。如果你更新一个文档,会将旧文档标记为删除,同时增加一个全新的文档。同时文档的 Version 字段加1
- 内部版本控制
- 使用: if_seq_no + if_primary_term
- 如:PUT products/doc/1?if_seq_no=1&if_primary_term=1
- 使用外部版本(使用其他数据库作为主要数据存储,如从mysql同步数据到 ES )
- version + version_type = external
- 如:PUT products/_doc/1?version=30000&version_type=external
内部版本控制案例:
- 如上图,不管是查询、还是更新,还是索引操作,都会显示 _seq_no 以及 _primary_term 元数据信息
- 然后我们根据这个值,用 PUT products/doc/1?if_seq_no=1&if_primary_term=1 格式来判断该值,在本会话查询到提交更新之后是否有其他会话并发更新、索引;
外部版本控制案例:
- 如上图,是指定版本的,如果版本号已经存在,则报错
- 所以我们可以先查询,然后以查询出的 version+1 作为更新参数,如果有并发在本回话之前更新了
- 版本号是只能更高不能更低的
- 如:
- session 1:GET /products/doc/1 ,获取到 version=100
- session 2:GET /products/doc/1 ,获取到 version=100 并发操作也获取到100
- session 2:修改 count:为 1100,提交的版本号为 version+1 即 101,操作成功
- session 1:修改 count:为 1001,提交的版本号为 version+1 即 101,更新操作发现当前版本号>=我们本次提交的版本号 101的了,所以会报错;
- 最终这样实现了 乐观并发控制
- 如:
【总结】
(1)【match,match_phrase,query_string,term,bool查询的区别】
参考:https://blog.csdn.net/weixin_46792649/article/details/108055763
参考:http://blog.majiameng.com/article/2819.html
参考:https://www.pianshen.com/article/66431547985/
首先,我们要明白 keyword 和 text类型的区别;
《1》keyword:不参与分词 《2》text:参与分词
所以:
-
term:某个字段,完全匹配分词
- 精确查询,搜索前不会再对搜索词进行分词
- 如:"term":{ "foo": "hello world" }
- 那么只有在字段中存储了“hello world”的数据才会被返回,如果在存储时,使用了分词,原有的文本“I say hello world”会被分词进行存储,不会存在“hello world”这整个词,那么不会返回任何值。
- 但是如果使用“hello”作为查询条件,则只要数据中包含“hello”的数据都会被返回,分词对这个查询影响较大。
-
match_phase:完全、精准匹配
- 查询确切的phrase,keyword需要完全匹配,text需要完全匹配(多个分词均在且顺序相同)
- 如果换个顺序,例如:有字符串 "深圳鹏开信息技术有限公司",分词为 深圳[0] , 鹏开[1] , 信息技术[2] , 信息[3] , 技术[4] , 有限公司[5] , 有限[6] , 公司[7];
- 1.es会先过滤掉不符合的query条件的doc:
- 在搜索"鹏开技术信息"时,被分词成 鹏开[0] , 技术信息[1] , 技术[2] , 信息[3]
- 很明显,技术信息这个分词在文档的倒排索引中不存在,所以被过滤掉了
- 2.es会根据分词的position对分词进行过滤和评分,这个是就slop参数,默认是0,意思是查询分词只需要经过距离为0的转换就可以变成跟doc一样的文档数据
- 在搜索"鹏开信息"时,如果不加上slop参数,那么在原文档的索引中,"鹏开"和"信息"这两个分词的索引分别为1和3,并不是紧邻的,中间还存在一个"信息技术"分词,很显然还需要经过1的距离,才能与搜索词相同。所以会被过滤。
- 那么我们加上slop参数就好了 {"query":"鹏开信息","slop":1}
-
match:某个字段,出现一个或多个分词的模糊匹配
- 先对输入值进行分词,对分词后的结果进行查询,文档只要包含match查询条件的一部分就会被返回。
-
query_string:多个分词逻辑操作时使用,比如 与或非
- 语法查询,同match_phase的相同点在于,输入的查询条件会被分词,但是不同之处在与文档中的数据可以不用和query_string中的查询条件有相同的顺序。
- 可以使用
-
bool:多字段多条件查询
- 参数1:must 必须匹配
- 参数2:must_not 必须不匹配
- 参数3:should 默认情况下,should语句一个都不要求匹配,只有一个特例:如果查询中没有must语句,那么至少要匹配一个should语句
【参考文档】
本文参考学习笔记自:阮一鸣 极客时间教程