本笔记分析研究对于ES的search功能的原理
一、查询分页
通过之前的学习,我们可以知道搜索时可以通过from和size字段来配置分页信息
1 | POST /ecommerce/product/_search |
我们先进行结果分析,
- took:查询使用12毫秒
- timed_out:没有超时
- _shards:查询请求了多少个分片,成功请求多少个分片
- hits:命中数量,最大匹配分数,具体命中数据
当我们看数据时,发现_id为2的在前,_id为1的在后,这是es底层分页导致的。
es查询分页原理:举例3个shard,3000条数据,查询from=100,size=10
- 因为from100,size10,即需要100-109的数据
- es协调节点向3个shard发出请求,要求获取每个shard的0-109的数据,注意是0-109
- 每个shard接收到请求后,会创建一个
from+size
大小的priority queue
,一个优先队列,将查询出的数据放入队列再全部返回给协调节点。 - 协调节点获取到这些数据,总数据量为330条,将这些数据放到自己的
priority queue
,然后对这些数据进行排序 - 排序后将序号为100-109的数据返回给客户端
这时我们会发现,如果数据量增加到30万条,而用户需要最后10条,那么es的协调节点需要临时存储30万条数据且进行排序,会导致大量资源耗费,所以对于ES的分页,要尽量避免DEEP PAGING的情况。
二、全域搜索
ES提供了一种query string的字段,可以将关键词分词再进行搜索,并且可以全域搜索,即不指定查询哪个field
1 | POST /ecommerce/product/_search |
问题提出:不指定field,ES底层是如何处理并查询所有域呢?难道是对每个field都进行查询吗?
如果对每个field都进行一次查询,太繁琐且性能低,ES的底层不是这样的,它有一个元数据_all field
,在建立索引保存document的时候,会讲所有域的值串成一个字符串并赋值给元数据_all field。
例如:
1 | { |
当全域搜索的时候只需搜索_all field
元数据即可
三、mapping
我们之前了解mapping就类似一张表的数据结构,定义了多个字段,我们回过头来再研究一下
1)在es里直接插入数据时PUT shop/book/1
及数据信息时,es会自动建立索引、type和mapping
2)mapping会根据数据自动定义field的数据类型,如2018-01-01日期格式的数据类型为date,100数字格式的为long
3)不同的数据类型可能有的是exact value精准匹配,有的是full text全文检索。比如text就为全文检索,date就为精准匹配
4)exact value在建立倒排索引时,是整个词进行建立,full text是使用分词器分词后再对不同的词进行建立。所以在搜索时也不同
5)mapping的建立可以让es自动建,或者手动设置mapping的field,包括数据类型,分词器,是否存储,是否索引等等
这里我们举个例子来看一看精准匹配与全文检索
创建两条数据,website/article,因为是直接插入的,所以mapping是es自动创建的,我们看下mapping格式
可以看到author_id是long,post_date是date,和我们上面说的一样。
我们通过上面说的全域搜索query string进行查询一下试一试:
可以看到我们全域搜索2017可以查询出2条数据
如果我们指定域post_date查询,只能查到一条,为什么呢?
全域搜索:查询的是_all field的字段,其类型text属于全文检索,即对2017-01-01这个数据分词后再查询,所以有2条
指定域:查询的是post_date字段,其类型date属于精准搜索,即对日期没有分词,又因为es5.x之后的优化,所以能查出一条记录,其实在5.x之前的版本是一条都查不出来的。即只能用2017-01-01这个搜索关键词才能查出。
四、Filter和Query
在前面已经学习了如何过滤数据。
这样一看,发现filter和query的功能好像差不多,能相互转化,那么到底应该用什么呢?
query:每次查询需要进行计算匹配值并排序,无法缓存结果
filter:每次只按搜索条件过滤,不排序计算,会缓存结果
这样一看,使用场景显而易见了,这里要说下,看着filter不计算,效率肯定比query高,其实不然,在第一次查询时,query的效率可能还会比filter高,因为filter需要缓存结果,但是除了第一次查询,filter的速度比query快几十上百倍。
五、ES的相关度评分
在进行查询操作时,前面介绍说排序默认是按_score分数来排的,且es底层的计算排序也是如此,那么到底是如何排序计算呢?就是我们要讲的相关度评分,TF/IDF(词频/逆向文档频率)
官方文档告诉我们,分数的由来主要有三个因素决定
5.1.词频(TF)
词频即词在文档中出现的频率,频率越高,权重越高,计算公式为:
$$
tf(td) = √frequency
$$
词t
在文档d
的频率是:该词在文档中出现次数的平方根
例如:搜索hello world
doc1:hello you, and world is very good
doc2:hello, how are you
那么分词后在doc1出现2次,doc2出现1次,即doc1分数高
如果不在意词在某个字段中出现的频次,而只在意是否出现过,则可以在字段映射中禁用词频统计:
1 | PUT /my_index |
设置其field的index_options为docs,即计算分数时不会再计算词频TF
5.2.逆向文档频率(IDF)
即词在总文档中出现的频率,频率越高,分数越低
$$
idf(t) = 1 + log ( numDocs / (docFreq + 1))
$$
词 t
的逆向文档频率( idf
)是:索引中文档数量除以所有包含该词的文档数,然后求其对数。
例如:搜索hello world
doc1:world is very good,bro(world在总document中出现1000次)
doc2:hello, how are you(hello在总)
因为doc1的出现次数更多,所以doc2的分数更高
5.3.字段长度归一值(flnorm)
Field-length norm:字段的长度越长,其分数越低
$$
norm(d) = 1 / √numTerms
$$
字段长度归一值( norm
)是字段中词数平方根的倒数
例如:搜索hello world
doc1:world is very good,bro
doc2:hello, how are you
因为doc2的长度更短,所以doc2的分数更高
Norm和TF一样也可以禁用的
1 | PUT /my_index |
设置其field的norms为false,即计算分数时不会再计算Norm
六、SearchType
ES是分布式的全文检索系统,其特点就是分片存储数据,ES如何获取用户需要的数据且排序呢?ES搜索排序分为两步:
1)ES向5个分片发出请求,叫Scatter
2)5个分片独立搜索,将符合条件的数据返回,ES将数据集中起来再进行重新排序,返回给用户,即Gather
这样就会出现问题:
1)数量问题:用户需要10条数据,ES向5个分片发出前10条数据的请求,得到结果会有50条数据,ES再进行排序返回,用户就会获得50条数据,不符合请求数量。
2)排序问题:ES的排序是基于分片传来的分数的,即每个分片有不同的使用频率,举个例子:分片1的name字段的mike使用200次,allie使用100次,用户查询1条最高使用数据,当然分片1返回mike,这时分片2的name字段bob使用10次,jasper使用5次,就会返回bob,这样的话明显allie的使用频率更高。所以如果要解决这种情况,ES要先获取分片上频率,然后统一计算后再排序查询。
这两个问题,估计ES也没有什么较好的解决方法,最终把选择的权利交给用户,方法就是在搜索的时候指定query type。
有三种SearchType:
- Query and fetch:即ES请求所有分片,返回n*5的数据整合排序,就会出现上面的问题,返回数量是用户要求的n倍。这种方法以及被弃用了
- query then fetch:ES请求所有分片,获取排名相关的信息,而不是数据,es获取后进行计算然后再根据计算结果向不同分片请求数据,这样可以解决上面数量问题和排序问题。默认使用,也最常用
- DFS query then fetch:同样多了初始化散发过程。
七、零停机重建索引
前提条件:Java应用中使用的别名查询而不是索引名
经过之前的学习知道field属性一旦创建就不能被修改,比如创建时是text,你不能后面再修改成date类型,那么我们模拟一下真实生产场景:
7.1.问题的提出
1)创建document,使用dynamic mapping,但是不小心把title的类型设置成date了,实际是text类型
1 | PUT my_index/my_type/1 |
2)向索引添加text类型的title数据,因为类型不匹配报错
3)因为不能直接修改,所以需要重建索引
7.2.问题的解决
1)创建索引,配置正确的field类型
1 | PUT /new_index |
2)设置别名,将新索引加入到旧索引的别名上(Java程序使用的是别名)
1 | POST /_aliases |
3)使用reindex api同步索引数据
1 | POST _reindex |
4)查看新索引数据是否同步成功
同步成功
5)删除别名中的旧索引
1 | POST /_aliases |
八、深入document读写机制底层
在前面讲document如何写入时,是说协调节点分配通过路由计算分配给不同的节点进行写入,今天我们来讲讲具体节点中primary shard是如何把document写入到磁盘中的。
8.1.写入原理
1)shard
将数据先写到内存buffer
中,在内存中的数据是搜索不到的
2)buffer
每隔一秒将数据刷到file上,即segment file
,但是如果这时直接将file
写入磁盘,效率会非常慢,而且用户就无法NRT近实时搜索数据,所以会先将segment file
数据刷到os cache
中,即系统缓存
3)buffer
数据进入os cache
中后,buffer清空,数据可以被搜索到
4)buffer
清空了,数据存在os cache
,宕机不就全部丢失了?这时就出现了translog
日志文件,数据进入shard
后,不止将其写入到buffer
中,还将一份写入到translog
中,当然也是先保存在translog
的cache
中,然后每隔5s会将translog cache
中的数据写入到translog
中,即保存到磁盘上(所以这5s如果宕机,可能会丢失5s的数据)
5)重复上面的操作,buffer
每隔一秒写入到新的segment file
中,然后写入os cache
中,并每隔5秒刷数据到translog
中
6)当translog
越来越大,会进行一次commit
(flush)操作,默认30分钟flush一次
6.1)将buffer
所有数据保存到新的segment file
,然后刷到os cache
中
6.2)将一个commit point
写入到磁盘,这个commit point
保存了对应所有的segment file
6.3)将os cache
中的所有index segment file
缓存数据,被fsync
强行刷到磁盘上
6.4)清空translog
,创建一个新的translog
,commit结束
实际上你在这里,如果面试官没有问你 es 丢数据的问题,你可以在这里给面试官炫一把,你说,其实 es 第一是准实时的,数据写入 1 秒后可以搜索到;可能会丢失数据的。有 5 秒的数据,停留在 buffer、translog os cache、segment file os cache 中,而不在磁盘上,此时如果宕机,会导致 5 秒的数据丢失。
8.2.更新/删除原理
当数据写入buffer后,并刷入os cache,这时会有一个commit point操作,标志这些数据是属于哪个segment file的,这时就会生成一个.del
文件,将里面某个docment标为deleted状态,这样搜索时便会过滤掉。
8.3.再次优化
我们知道,每过一秒写入一个新的segment file,那么segment file会非常庞大,所以一段时间后,es会自动进行merge操作,将一些segment合并,并且会将标记为deleted数据物理删除
merge执行流程
1)选择大小相近的一些segment file,merge成一个大的segment
2)将新的segment file刷到os cache中
3)写一个新的commit point,包括新的segment以及排除旧的segment
4)将新的segment打开提供搜索,删除旧的segment