六、ElasticSearch中Search的运行机制
Search执行的时候,实际分为两个步骤执行:
---> Query阶段:搜索
---> Fetch阶段:获取
1、Query—Then—Fetch:
假设集群my_cluster中存在三个节点node1、node2、node3,其中master为node1,其余的为data节点。
1)Query阶段:
假设node3接收到Client发送的查询请求之后,先进行query:
A、node3在6个主副分片上随机选择3个分片,发送search request;
B、被选中的3个分片分别执行查询并排序,返回from+size个文档id和排序值;
C、node3整合3个分片返回的from+size个文档id,根据排序值排序选取from到from+size的文档id。
2)Fetch阶段:
node3根据Query阶段获取的文档id列表去对应shard上获取详情数据:
A、node3向相关分片发送multi_get请求;
B、3个分片返回文档详细数据;
C、node3拼接返回的结果返回给Client。
2、相关性算分:
相关性算分在shard和shard之间是相互独立的。也就意味着:同一个单词term在不同的shard上的TDF等值也可能是不同的。得分与shard有关。当文档数量不多是,会导致相关性算分严重不准的情况发生。
解决方案:
1)设置分片数为1个,从根本上排除问题。(此方案只适用于百万/少千万级的少量数据)
2)使用DFS Query-then-Fetch查询方式。
DFS Query-then-Fecth:
在拿到所有文档后,再重新进行完整的计算一次相关性得分,耗费更多的CPU和内存,执行性能也较低。所以也不推荐。
#使用DLS Query-then-Fetch进行查询:
GET my_index/_search?search_type=dfs_query_then_fetch
{
"query":{
"match":{
...
}
}
}
3、排序相关:
默认采用相关性算分结果进行排序。可通过sort参数自定义排序规则,如:
#使用sort关键词进行排序
GET my_index/_search
{
"sort":{ #关键词
"birth":"desc"
}
}
#或使用数组形式定义多字段排序规则
GET my_index/_search
{
"sort":[ #使用数组
{
"birth":{
"order":"asc"
}
},
{
"age":{
"order":"desc"
}
}
]
}
1)直接按数字/日期排序,如上例中birth。
2)按字符串进行排序:字符串排序较特殊,因为在ES中有keyword和text两种:
A、针对text类型排序:
#直接对text类型进行排序
GET my_index/_search
{
"sort":{
"username":"desc" #针对username字段进行倒序排序
}
}
返回结果:
B、针对keyword类型排序:
#针对keyword进行排序
GET my_index/_search
{
"sort":{
"username.keyword":"desc" #针对username的子类型keyword类型进行倒叙排序
}
}
3)关于fielddata和docvalues:
排序的实质是对字段的原始内容排序的过程,此过程中倒排索引无法发挥作用,需要用到正排索引。即:通过文档ID和字段得到原始内容。ES提供2中实现方式:
A、Fielddata。 默认禁用。
B、DocValues。 默认启用,除了text类型。
Fielddata 对比 DocValues。
对比 | Fielddata | DocValues |
创建时机 | 搜索时即时创建 | 创建索引时创建,和倒排索引创建时间一致 |
创建位置 | JVM Heap | 磁盘 |
优点 | 不占用额外磁盘空间 | 不占用Heap内存 |
缺点 | 文档较多时,同时创建会花费过多时间,占用过多Heap内存 | 减慢索引的速度,占用额外的磁盘空间 |
4)Fielddata的开启:
Fielddata默认关闭,可通过如下api进行开启,且在后续使用时随时可以开启/关闭:
#开启字段的fielddata设置
PUT my_index/_mapping/doc
{
"properties":{
"username":{
"type":"text",
"fielddata":true #关键词
}
}
}
使用场景:一般在对分词做聚合分析的时候开启。
5)Docvalues的关闭:
Docvalues默认开启,可在创建索引时关闭,且之后不能再打开,要打开只能做reindex操作。
#关闭字段的docvalues设置
PUT my_index
{
"mappings":{
"doc":{
"properties":{
"username":{
"type":"keyword",
"doc_values":false #关键词
}
}
}
}
}
使用场景:当明确知道,不会使用这个字段排序或者不做聚合分析的时候,可关闭doc_values,减少磁盘空间的占用。
4、分页与遍历:
ES提供了三种方式来解决分页和遍历的问题:
from/size,scroll,search_after。
1)from/size:
from:指明开始位置;
size:指明获取总数
#使用from——size
GET my_index/_search
{
"from":1, #从第2个开始搜索
"size":2 #获取2个长度
}
A、此时产生了一个经典的问题,也是分布式文件系统必定面对的问题:深度分页。
问题:如何在数据分片存储的情况下, 获取前1000个文档?
答案:先从每个分片上获取前1000个文档, 然后由处理节点聚合所有分片的结果之后,再排序获取前1000个文档。
此时页数越深,处理的文档就越多,占用的内存就越大,耗时就越长。这就是深度分页问题。
为了尽量避免深度分页为题,ES通过设定index.max_result_window限定最多到10000条数据。
B、在设计分页系统时,有一个分页数十分重要:
total_page=(total + page_size -1) / page_size
总分页数= (文档总数+认为设定的文档大小-1) / 人为设定的文档大小
但是在搜索引擎中的意义并不大,因为如果排在前面的结果都不能让用户满意,那么越往后,越不能让用户满意。
2)scroll:
遍历文档集的API,以快照的方式来避免深度分页问题。
A、不能用来做实时搜索,因为数据不是实时的;
B、尽量不用复杂的sort条件,使用_doc最高效;
C、使用比较复杂。
步骤:
a、发起一个scroll search:
#发起一个scroll search
GET my_index/_search?scroll=5m #该快照的有效时间为5min
{
"size"1 #指明每次scroll返回的文档数
}
会返回后续会用到的_scroll_id:
b、调用scroll search 的api,获取文档集合,不断迭代至返回hits数组为空时停止:
POST _search/scroll
{
"scroll":"5m", #指明有效时间
"scroll_id":"xxxxxx" #上一步返回的_scroll_id
}
之后不断返回新的_scroll_id,使用新的_scroll_id进行查询,直到返回数组为空。
当不断的进行迭代,会产生很多scroll,导致大量内存被占用,可以通过clear api进行删除:
#使用clear api对scroll进行删除
DELETE /_search/scroll
{
"scroll_id":[
"xxxxxx", #_scroll_id
"xxxxxx", #_scroll_id
......
]
}
#删除所有的scroll
DELETE /_search/scroll/_all
3)search_after:
避免深度分页的性能问题,提供实时的下一页文档获取功能。
缺点:不能使用from参数,即:不能指定页数。且只能下一页,不能上一页。
使用步骤:
A、第一步:正常搜索,但是要指定sort值,并保证值唯一:
#第一步,正常搜索
GET my_index/_search
{
"size":1,
"sort":{
"age":"desc",
"_id":"desc"
}
}
B、第二步:使用上一步最后一个文档的sort值进行查询:
#第二步,使用sort值进行查询
GET my_index/_search
{
"size":1,
"search_after":[28,"2"], #28,"2",是上一次搜索返回的sort值
"sort":{
"age":"desc",
"_id":"desc"
}
}
4)如何避免深度分页问题:
这个问题目前连google都没能解决,所以只能最大程度避免,通过唯一排序值定位每次要处理的文档数都控制在size内:
应用场景:
A、from/size:需实时获取顶部的部分文档,且需自由翻页(实时);
B、scroll:需全部文档,如:导出所有数据的功能(非实时);
C、search_after:需全部文档,不需自由翻页(实时)。