我们将讨论一下在分布式环境中搜索是怎么执行的。
一个 CRUD 操作只对单个文档进行处理,文档的唯一性由 _index
, _type
, 和 routing
values (通常默认是该文档的 _id
)的组合来确定。 这表示我们确切的知道集群中哪个分片含有此文档。
搜索需要一种更加复杂的执行模型因为我们不知道查询会命中哪些文档: 这些文档有可能在集群的任何分片上。 一个搜索请求必须询问我们关注的索引(index or indices)的所有分片的某个副本来确定它们是否含有任何匹配的文档。
但是找到所有的匹配文档仅仅完成事情的一半。 在 search
接口返回一个 page
结果之前,多分片中的结果必须组合成单个排序列表。 为此,搜索被执行成一个两阶段过程,我们称之为 query then fetch 。
1.查询阶段
在初始 查询阶段 时, 查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的 优先队列。
优先队列
一个 优先队列 仅仅是一个存有 top-n 匹配文档的有序列表。优先队列的大小取决于分页参数 from
和 size
。例如,如下搜索请求将需要足够大的优先队列来放入100条文档。
GET /_search { "from": 90, "size": 10 }
查询阶段包含以下三个步骤:
- 客户端发送一个
search
请求到Node 3
,Node 3
会创建一个大小为from + size
的空优先队列。 Node 3
将查询请求转发到索引的每个主分片或副本分片中。每个分片在本地执行查询并添加结果到大小为from + size
的本地有序优先队列中。- 每个分片返回各自优先队列中所有文档的 ID 和排序值给协调节点,也就是
Node 3
,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。
2.取回阶段
查询阶段标识哪些文档满足搜索请求,但是我们仍然需要取回这些文档。这是取回阶段的任务, 正如 Figure 15, “分布式搜索的取回阶段” 所展示的。
分布式阶段由以下步骤构成:
- 协调节点辨别出哪些文档需要被取回并向相关的分片提交多个
GET
请求。 - 每个分片加载并 丰富 文档,如果有需要的话,接着返回文档给协调节点。
- 一旦所有的文档都被取回了,协调节点返回结果给客户端。
协调节点首先决定哪些文档 确实 需要被取回。例如,如果我们的查询指定了 { "from": 90, "size": 10 }
,最初的90个结果会被丢弃,只有从第91个开始的10个结果需要被取回。这些文档可能来自和最初搜索请求有关的一个、多个甚至全部分片。
协调节点给持有相关文档的每个分片创建一个 multi-get request ,并发送请求给同样处理查询阶段的分片副本。
分片加载文档体-- _source
字段—如果有需要,用元数据和 search snippet highlighting 丰富结果文档。 一旦协调节点接收到所有的结果文档,它就组装这些结果为单个响应返回给客户端。
深分页(Deep Pagination)
先查后取的过程支持用 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
查询禁用排序使这个取回行为更有效率。
3.搜索选项
有几个 查询参数可以影响搜索过程。
3.1偏好
偏好这个参数 preference
允许 用来控制由哪些分片或节点来处理搜索请求。 它接受像 _primary
, _primary_first
, _local
, _only_node:xyz
, _prefer_node:xyz
, 和 _shards:2,3
这样的值, 这些值在 search preference
文档页面被详细解释。
3.2超时问题
通常分片处理完它所有的数据后再把结果返回给协同节点,协同节点把收到的所有结果合并为最终结果。
这意味着花费的时间是最慢分片的处理时间加结果合并的时间。如果有一个节点有问题,就会导致所有的响应缓慢。
参数 timeout
告诉 分片允许处理数据的最大时间。如果没有足够的时间处理所有数据,这个分片的结果可以是部分的,甚至是空数据。
搜索的返回结果会用属性 timed_out 标明分片是否返回的是部分结果:
"timed_out": true,
3.3路由
在 路由一个文档到一个分片中 中, 我们解释过如何定制参数 routing
,它能够在索引时提供来确保相关的文档,比如属于某个用户的文档被存储在某个分片上。 在搜索的时候,不用搜索索引的所有分片,而是通过指定几个 routing
值来限定只搜索几个相关的分片:
GET /_search?routing=user_1,user2
这个技术在设计大规模搜索系统时就会派上用场,我们在 扩容设计 中详细讨论它。
3.4搜索类型
缺省的搜索类型是 query_then_fetch
。 在某些情况下,你可能想明确设置 search_type
为 dfs_query_then_fetch
来改善相关性精确度:
GET /_search?search_type=dfs_query_then_fetch
4.游标查询 Scroll
scroll
查询 可以用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。
这个游标查询返回的下一批结果。 尽管我们指定字段 size
的值为1000,我们有可能取到超过这个值数量的文档。 当查询的时候, 字段 size
作用于单个分片,所以每个批次实际返回的文档数量最大为 size * number_of_primary_shards
。
注意游标查询每次返回一个新字段 _scroll_id
。每次我们做下一次游标查询, 我们必须把前一次查询返回的字段 _scroll_id
传递进去。 当没有更多的结果返回的时候,我们就处理完所有匹配的文档了。
备注:文章参考Elasticsearch: 权威指南
https://www.elastic.co/guide/cn/elasticsearch/guide/current/distributed-search.html