目录
ES检索的两阶段过程
文档的唯一性可以由 _index(索引名称)和 _id(文档id)的组合来确定,这表示我们可以确切知道集群中哪个分片含有此文档。
而当我们需要通过id之外的条件进行搜索时,就需要一种更加复杂的执行模型,因为我们不知道查询会命中哪些文档,这些文档有可能在集群的任何分片上。一个搜索请求必须询问我们关注的索引的所有分片的某个副本来确定它们是否含有任何匹配的文档。
但是找到所有的匹配文档仅仅完成事情的一半。 在 search
接口返回一个 page
结果之前,多分片中的结果必须组合成单个排序列表。 为此,搜索被执行成一个两阶段过程:query then fetch
查询阶段
在初始 查询阶段 时, 查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的 优先队列(一个 优先队列 仅仅是一个存有 top-n 匹配文档的有序列表。优先队列的大小取决于分页参数 from
和 size
)。
当一个搜索请求被发送到某个节点时,这个节点就变成了协调节点。 这个节点的任务是广播查询请求到所有相关分片并将它们的响应整合成全局排序后的结果集合,这个结果集合会返回给客户端。
查询阶段包含以下三个步骤:
- 客户端发送一个
search
请求到Node 3
,Node 3
会创建一个大小为from + size
的空优先队列。 Node 3
将查询请求转发到索引的每个主分片或副本分片中。每个分片在本地执行查询并添加结果到大小为from + size
的本地有序优先队列中。- 每个分片返回各自优先队列中所有文档的 ID 和排序值给协调节点,也就是
Node 3
,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。
取回阶段
查询阶段标识哪些文档满足搜索请求,但是我们仍然需要取回这些文档,这是取回阶段的任务。
取回阶段由以下步骤构成:
- 协调节点辨别出哪些文档需要被取回并向相关的分片提交多个
GET
请求。 - 每个分片加载并 丰富 文档,如果有需要的话,接着返回文档给协调节点。
- 一旦所有的文档都被取回了,协调节点返回结果给客户端。
深分页问题
先查后取的过程支持用
from
和size
参数分页,但是这是 有限制的 。 要记住需要传递信息给协调节点的每个分片必须先创建一个from + size
长度的队列,协调节点需要根据number_of_shards * (from + size)
排序文档,来找到被包含在size
里的文档。取决于你的文档的大小,分片的数量和你使用的硬件,给 10,000 到 50,000 的结果文档深分页( 1,000 到 5,000 页)是完全可行的。但是使用足够大的
from
值,排序过程可能会变得非常沉重,使用大量的CPU、内存和带宽。因为这个原因,我们强烈建议你不要使用深分页。实际上, “深分页” 很少符合人的行为。当2到3页过去以后,人会停止翻页,并且改变搜索标准。会不知疲倦地一页一页的获取网页直到你的服务崩溃的罪魁祸首一般是机器人或者web spider。
解决深分页问题一般会用两种方式:scroll 或 search_after
scroll
scroll 类似于sql中的cursor,使用scroll,每次只能获取一页的内容,然后会返回一个scroll_id。根据返回的这个scroll_id可以不断地获取下一页的内容,所以 scroll 并不适用于有跳页的情景。
深度分页的代价根源是每次查询,都需要将结果集全局排序,如果去掉全局排序的特性的话查询结果的成本就会很低。而 scroll 就是一次把要用的数据都排完了,分批取出,从而达到性能的提升。
因此,scroll 查询取的是某个时间点的快照数据,查询初始化之后索引上的任何变化会被它忽略。
scroll 并不适合实时查询,适合于后台数据批量遍历计算的场景。
GET /old_index/_search?scroll=1m
{
"query": { "match_all": {}},
"sort" : ["_doc"],
"size": 100
}
- 1m 指 scroll_id 保留1分钟可用;
- sort 为排序的字段,关键字
_doc
是最有效的排序顺序; - size 为每次返回的数量,from不需要指定,或指定为0。
这个查询的返回结果包括一个字段 _scroll_id
, 它是一个base64编码的长字符串。现在我们能传递字段 _scroll_id
到 _search/scroll
查询接口获取下一批结果:
GET /_search/scroll
{
"scroll": "1m",
"scroll_id" : "cXVlcnlUaGVuRmV0Y2g7NTsxMDk5NDpkUmpiR2FjOFNhNnlCM1ZDMWpWYnRROzEwOTk1OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MTA5OTM6ZFJqYkdhYzhTYTZ5QjNWQzFqVmJ0UTsxMTE5MDpBVUtwN2lxc1FLZV8yRGVjWlI2QUVBOzEwOTk2OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MDs="
}
search_after
想要实现实时的滚动分页,可以通过search_after来实现。
search_after 分页的方式是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到结果上。但是需要注意,因为每一页的数据依赖于上一页最后一条数据,所以无法实现跳页请求。
为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,官方推荐使用 _id 作为全局唯一值。
GET /test_index/_search
{
"query": {
"match_all": {}
},
"size": 10,
"search_after":[1003],
"sort": [
{
"_id": {
"order": "desc"
}
}
]
}
- search_after 对应的值是上一次查询最后一条记录中 sort 字段的值;
- from属性可以省略,或者是0或-1;
- sort 对应的字段应该是全局唯一的,推荐使用_id。
笔记内容基于官方文档整理:https://www.elastic.co/guide/cn/elasticsearch/guide/current/distributed-search.html