查询原理
Term 查询
FST(Finite State Transducer)
从 Lucene4 开始,为了实现 rangequery 或前缀、后缀等复杂的查询语句,Lucene 使用 FST 数据结构来存储 term 字典。
倒排链的存储
为了快速查找 docId,Lucene 采用 SkipList 数据结构,它具有以下特征:
A. 元素是排序的,lucene 按 docid 从小到大排序
B. 跳跃有固定间隔,在建立 skiplist 时指定
C. skiplist 的层次,指整个 skiplist 有几层
倒排链合并
如果某个链很短,会大幅减少比对次数,并且由于 SkipList 结构的存在,在某个倒排中定位某个 docid 的速度会比较快不需要一个个遍历。可以很快的返回最终的结果。从倒排的定位,查询,合并整个流程组成了 lucene 的查询过程,和传统数据库的索引相比,lucene 合并过程中的优化减少了读取数据的 IO,倒排合并的灵活性也解决了传统索引较难支持多条件查询的问题。
倒排链合并
如果是数值类型的范围查询,如整形、浮点型,采用 FST term 查询,潜在的 term 会非常多,查询效率很低。为了支持高效的多维数值查询,lucene 引入 BKDTree。BKDTree 基于 KDTree,对数据按照维度划分建立一棵二叉树确保树两边节点数目平衡。在一维场景下,KDTree 退化成二叉树,在二叉树中查询叶子节点对应倒排链需要 logn 时间。
在多维场景下,kdtree 建立流程如下:
A.确定切分维度,选取顺序是数据分散越开的维度,越先切分
B.切分点选择维度最中间的点
C.递归进行 A、B,可以设置阈值,点的数据少于多少后就不再切分,直到所有的点都切分好停止
BKD 是多个 KDTree 持续 merge 最终合并成一个
写入流程
Es 的任意节点都可以作为协调节点(coordinating node)接受请求,通过_routing字段找到对应的 primary shard,并将请求转发给 primary shard, primary shard 完成写入后,将写入并发发送给 replica,replica 执行写入操作后返回给 primary shard,primary shard 再讲请求返回给协调节点。
查询流程
Es 通过分区实现分布式,数据写入时,根据 routing 规则将数据写入某个 Shard,这样能将海量数据分布在多个 Shard和多台机器上。
在查询时,数据可能分布在 index 的所有 Shard 中,所以需要查询所有 Shard,同一个 Shard 的 Primary 和 Replica 选择一个就可以,查询请求分发给所有 Shard,每个 Shard 中都是一个独立的查询引擎,如果需要返回 TopN 的结果,每个 Shard 都会查询返回 TopN,然后在 Client Node 中通过优先队列二次排序,找出 top N 结果返回给用户。
一般搜索系统都是两阶段查询,第一阶段查询到匹配的 DocId,第二阶段再查询 DocId 对应的完整文档,这种在 Es 称为 query_then_fetch,还有一种是一阶段查询返回完整 Doc,es 称为 query_and_fetch,一般第二种适用于只需要查询一个 Shard 的请求。
除了一阶段,两阶段外,还有一种三阶段查询的情况。搜索里面有一种算分逻辑是根据TF(Term Frequency)和DF(Document Frequency)计算基础分,但是Elasticsearch中查询的时候,是在每个Shard中独立查询的,每个Shard中的TF和DF也是独立的,虽然在写入的时候通过_routing保证Doc分布均匀,但是没法保证TF和DF均匀,那么就有会导致局部的TF和DF不准的情况出现,这个时候基于TF、DF的算分就不准。为了解决这个问题,Elasticsearch中引入了DFS查询,比如DFS_query_then_fetch,会先收集所有Shard中的TF和DF值,然后将这些值带入请求中,再次执行query_then_fetch,这样算分的时候TF和DF就是准确的,类似的有DFS_query_and_fetch。这种查询的优势是算分更加精准,但是效率会变差。另一种选择是用BM25代替TF/DF模型。
查询阶段
在查询阶段,查询会广播到索引中的每一个分片(主分片或副本分片),每个分片在本地执行搜索并构建一个匹配文档的优先队列。一个优先队列是一个保存top-n匹配文档的有序列表,列表大小取决于分页参数from和size。
当一个查询请求被发送到某个节点时,这个节点即成为协调节点,它的任务是广播查询请求到所有相关分片并将它们的响应整合成全局排序后的结果集合,再将这个结果集合返回给客户端。查询请求可以被主分片或副本分片处理,协调节点在之后的查询中轮询所有分片分摊负载,所有更多的副本能够增加搜索吞吐率。分片返回一个轻量级的结果列表到协调节点,它仅包含文档id集合以及任何排序需要用到的值,如_score.
取回阶段
查询阶段仅仅标识哪些文档满足搜索请求,获取详细文档的过程在取回阶段实现。具体步骤如下:
- 协调节点计算出哪些文档需要被取回并向相关分片提交多个GET(multi-get request)请求
- 每个分片加载并丰富文档,并返回文档给协调节点
- 一旦所有文档都被取回了,协调节点返回结果给客户端
Scroll
scroll查询可以用于查询大批量文档而不用付出深度分页的巨大代价,游标查询会取某个时间点的快照数据,时间点(查询初始化)之后的所有变化都会被忽略。它通过保存旧的数据文件来实现这个特性,就像保留初始化时的索引视图一样。
游标查询用_doc_
字段来排序,这个指令让es仅从还有结果的分片返回下一批结果,可以为游标查询设置过期时间,es可以在空闲时间自动释放过期的游标查询占用的资源。scroll查询包含一个_scroll_id_
字段,它是一个base64编码的长字符串,可以通过传递前一次查询返回的_scroll_id_
查询下一批结果。查询时size作用于单分片,每个批次实际返回的文档数量最大为size * number_of_primary_shards.