深入理解 Elasticsearch 分页技术

news2024/9/23 1:35:43

原文链接:https://zhuanlan.zhihu.com/p/609576187

Elasticsearch 是一款分布式的搜索引擎,提供了灵活的分页技术。本文主要介绍 Elasticsearch(简称 ES) 的几种分页技术,并深入分析各种分页技术的优缺点及应用场景。

介绍

搜索引擎分页是指在搜索引擎中,将搜索结果分成多个页面展示,用户可以通过点击分页按钮或滚动页面来浏览不同页数的搜索结果。搜索引擎通常会在页面底部显示数字标记或其他标记以提示用户当前的位置和可用的页数。如下所示:

搜索引擎分页的目的是提高用户体验,让用户可以更轻松地从大量搜索结果中获取更精准的信息。当用户在搜索框中输入关键字并点击搜索按钮时,搜索引擎将返回与关键字相关的成千上万个搜索结果。如果将所有这些结果显示在一个页面上,页面加载时间会变得非常长,用户体验也会变得糟糕。因此,搜索引擎将搜索结果分页,相关度越高的结果越靠前,这样用户可以更快的找到想要的结果,同时减少页面加载时间和流量消耗。

本文主要关注分页技术,对于搜索引擎其他相关技术不在本文讨论范围,下面将介绍 ES 中的分页技术。

ES 分页

在讲分页技术前,这里简单介绍下 ES 的查询流程,如图:

Query_Then_Search

这里画的是最常见 Query_Then_Fetch 查询流程,整个步骤为:

  1. 协调节点接收查询请求
  2. 协调节点转发 QueryRequest 请求给数据节点
  3. 数据节点执行 QueryPhrase,查询满足条件的 TopN 文档信息(包括id、score,不包括文档内容)返回给协调节点
  4. 协调节点接收到数据节点返回的 QueryResults,然后从 k*TopN 的文档信息中选出最终的 TopN 的结果
  5. 协调节点发送 FetchRequest 给相关数据节点
  6. 数据节点执行 FetchPhrase,根据文档id获取文档内容,然后返回给协调节点
  7. 协调节点将最终的查询结果返回给客户端

到这里,我们大体了解了 ES 的查询流程,下面来看下 ES 的分页技术。Elasticsearch 提供了三种分页技术:

  • From+Size
  • Search After
  • Scroll

下面详细介绍这三种分页技术并深入分析它们的实现原理。

From+Size

介绍

在 ES 中,from 和 size 是两个控制分页查询的参数。from 参数用于指定从第几个文档开始返回结果,size 参数用于指定返回的结果集的大小。

具体来说,from 参数定义了结果集的起始点,而 size 参数定义了结果集的大小。如果想返回结果集中的前 10 个文档,可以将 from 参数设置为 0,size 参数设置为 10。

以下是一个使用 from 和 size 进行分页查询的示例:

GET /my_index/_search
{
  "from": 100,
  "size": 10,
  "query": {
    "match": {
      "content": "chatgpt"
    }
  }
}

在这个示例中,我们将 from 参数设置为 100,size 参数设置为 10,以便返回结果集中 [100,110) 共 10 个匹配 content 字段的文档,相当于返回第 11 页的结果集。那是不是我们可以在任何分页场景下都可以使用这种分页方式呢,答案是否定的,ES 对from+size 是有限制的,默认值为 10000。

那问题来了,为什么会有这样的限制呢?这个问题也是本文的重点之一。ES 官方文档给出的解释是:对于深度分页,ES 的每个 shard 都会查询 TopN(查询流程的步骤3,注意 N=from+size)的结果,即查询 [from, from+size) 的结果实际上数据节点会查询 from+size 个结果,也就是将 [0, from) 的结果也一并查出来了,这样将会导致 CPU 和 内存的开销较大,导致系统卡顿甚至 OOM(特别是协调节点,要收集多个 shard 返回的结果,内存开销更大)。因此,from+size 常常应用于分页深度较小的场景,不适合深分页场景。

好了,那问题又来了,明明只查询 size 个结果,为什么每个 shard 偏要将 [0, from+size) 的结果都查出来呢,直接返回 [from, from+size] 的结果不就完了吗?要回答这个问题,首先要来看个简单的例子,如下:

如上图,假设有 3 个shard,每个 shard 的文档按照 value 字段逆序排序,查询的 from 设置为 2,size 设置为 3。那么按照 ES 的处理逻辑,每个 shard 都会返回 [0, 5) 的文档(注意不包含文档内容),协调节点将收到 15 条文档,然后对这 15 条文档按照 value 排序,取前 [2, 5) 的文档为结果。如上图的 result 即为正确结果。

那么如果每个 shard 只返回各自的 [2, 5) 的文档,结果将会如何呢?请看下图:

上图表示每个 shard 返回 [2, 5) 的文档集合并后的结果。很明显,这个结果是不正确的,原因便是每个 shard 并不知道全局的排序结果,因此为了保证能够得到正确的结果,必须返回 [0, 5] 的结果集,因为它们中的任意一个都可能在全局序的 [2, 5) 范围内。

实现原理

上一节介绍了 from+size 的用法和基本原理,这一节来分析下 from+size 在 ES 中是如何实现的,只有真正明白了实现原理,才能更好地掌握使用。下面分几种情况讨论:

  • 不打分场景(打分的特殊场景,打分为常量)
  • 打分场景
  • 排序场景

第一种情况不打分场景, DSL 如下:

{
  "from": 0,
  "size": 5,
  "query": { // 这里如果没有 query 其实也相当于不打分,因为每个 doc 的打分都是常量 1
    "bool": {
      "filter": [ // filter 查询不打分
        {
          "term": {
            "clientip.keyword": "110.47.202.158"
          }
        }
      ]
    }
  }
}

我们来看下上述 DSL 执行的关键流程,为了便于说明,引用部分源码:

// ContextIndexSearcher.java 该类继承自 lucene 的 IndexSearcher 
protected void search(List<LeafReaderContext> leaves, Weight weight, Collector collector) throws IOException {
    for (LeafReaderContext ctx : leaves) { // search each subreader
        searchLeaf(ctx, weight, collector);
    }
}

leaves 代表 lucene 的 segment,在该方法中会遍历查询每个 segment。本文的重点不是源码解析,这里只贴少量代码,我们直接看查询 segment 的关键代码:

// DefaultBulkScorer.scoreRange 方法部分代码
while (currentDoc < end) {
    if (acceptDocs == null || acceptDocs.get(currentDoc)) {
        collector.collect(currentDoc);
    }
    currentDoc = iterator.nextDoc();
}

这里开始遍历 segment 中的 doc,然后用 collector(shard级,所有segment共用) 取收割 doc,这里的 collector 实际是 SimpleTopScoreDocCollector.getLeafCollector 获取的匿名类对象。collector.collect 的逻辑如下:

public void collect(int doc) throws IOException {
    float score = scorer.score();

    totalHits++;
    hitsThresholdChecker.incrementHitCount();

    if (minScoreAcc != null && (totalHits & minScoreAcc.modInterval) == 0) {
      updateGlobalMinCompetitiveScore(scorer);
    }

    // pqTop 的doc和score初始为<2147483647, 负无穷>,只有当pq满后,才会将其挤出去
    if (score <= pqTop.score) { 
      if (totalHitsRelation == TotalHits.Relation.EQUAL_TO) {
        updateMinCompetitiveScore(scorer);
      }
      return;
    }
    // 把 doc 加入 pqTop
    pqTop.doc = doc + docBase;
    pqTop.score = score;
    pqTop = pq.updateTop();
    // 关键,更新文档所需最小的 score
    updateMinCompetitiveScore(scorer);
  }
所有 collector 都会维护一个 堆 pq 来收集 topN 的 doc

这里关注下方法 updateMinCompetitiveScore,当 pq 满,下一个 doc 进来会走到第 14 行,里面会更新所需 doc 的最小 score 值为大于 pqTop.score 的第一个 float 值,而且由于是不打分场景,所以还会调用 DocIdSetIterator.empty 重新生成一个 iterator,这个 iterator 的 nextDoc 方法永远只返回一个常量 NO_MORE_DOCS(2147483647),因此外层 while 继续遍历时将终止循序。如果当前 segment 遍历完毕后,pq 还没满,那么将继续查询下一个 segment。

好了,如果查询完当前 segment,pq 已经满了,那剩下的 segment 还需要继续遍历吗?答案是否定的,原因是,查询下一个 segment 时,调用 collector.setScorer 内部会调用 updateMinCompetitiveScore 生成一个永远只返回 NO_MORE_DOCS 的 DocIdSetIterator,因此,scoreRange 里面的 while 循环永远走不进去。

通过上述的分析,from+size 在这种不打分的场景下,性能是最好的。

第二种情况打分场景,DSL 如下:

GET kibana_sample_data_logs_1/_search
{
  "from": 0,
  "size": 5,
  "query": {
    "match": {
      "message": "GET"
    }
  }
}

这种场景也是使用 SimpleTopScoreDocCollector 来收割 doc,所以也是走的第一种情况的 collect 方法,不同的是,当 pq 满后,还会继续遍历 doc,因为后面可能还有 doc 的 score 大于 pqTop.score。因此,打分场景需要遍历完所有 segment 的所有 doc 才能确定最终的结果。不难看出,该场景的性能会比上一种场景性能差,所以,如果没有打分需求,应该避免使用打分的 query 语句。

第三种场景排序场景,DSL 如下:

GET kibana_sample_data_logs_1/_search
{
  "from": 0,
  "size": 5,
  // query(打分或不打分) 省略
  "sort": [
    {
      "bytes": {
        "order": "desc"
      }
    }
  ]
}

查询时指定了排序字段,那么便需要按排序字段的顺序返回 topN 的结果。这种场景会使用 SimpleFieldCollector 来收割 doc,代码太长,这里不贴代码了,collect 的逻辑和上面场景类似,不同的是比较当前 doc 和 pqTop 时,比较的是排序字段的 docValue。

如果文档写入时不是按查询时的排序字段排序的,那么将会遍历完 shard 的所有 doc 才能得到最终结果。反之,如果文档本来是按排序字段排序的,那么查询时 collect 里面有个优化分支,当 pq 满后,会触发提前终止,裁剪掉当前 segment 剩下的 doc,查询后面的 segment 时,逻辑一样。

Search After

介绍

上一小节介绍了 from+size 的原理,也分析了它的弊端 - 即不适合深分页。好了,如果我们需要获取前 1000 页,每页 10 条文档怎么办?针对这种深分页场景,ES 提供了一种新的分页方式 – search_after。Elasticsearch 中的 search_after 机制是一种更有效的分页方法,它可以在不加载整个数据集的情况下快速地获取下一页数据。

search_after 是一种基于游标的分页方法,使用 search_after 查询时必须指定排序字段(可以有多个),它使用排序字段值作为游标,从而能够更快地获取下一页的数据。在进行第一次搜索时,ES 会返回第一页的结果,当需要获取下一页数据时,可以使用上一页最后一个文档的排序字段值作为游标进行搜索。通过这种方式,可以逐步遍历整个数据集而无需一次性加载所有数据。

使用 search_after 机制需要注意以下几点:

  1. 搜索请求必须指定排序字段,用于指定搜索结果的顺序
  2. 搜索第一页不必指定 search_after 参数,从第二页开始必须指定 search_after 为上一页的最后一个游标
  3. 游标必须是唯一的,否则可能会出现重复的数据

search_after 用法举例:

GET twitter/_search
{
    "query": {
        "match": {
            "title": "elasticsearch"
        }
    },
    "sort": [
        {"date": "asc"},
        {"tie_breaker_id": "asc"}      
    ]
}
tie_breaker_id 是 _id 的拷贝,开启 doc_values 用于排序

tie_breaker_id 的目的是保证游标的唯一性,继续搜索下一页:

GET twitter/_search
{
    "query": {
        "match": {
            "title": "elasticsearch"
        }
    },
    "search_after": [1463538857, "654323"], // 上一页最后一个排序字段的值
    "sort": [
        {"date": "asc"},
        {"tie_breaker_id": "asc"}
    ]
}

可以看到,search_after 的使用特别灵活,只要指定了游标值,便能根据游标值查询下一页文档。由于查询过程中,可能还会有数据写入,那么多次查询使用一个游标可能得到的结果不一致,如果业务有一致性需求,需要使用 point in time(PIT) 来创建一个临时的快照,查询时使用该快照保证数据一致性。

实现原理

search_after 使用 PagingFieldCollector 来收割 doc,原理和 from+size 的第三种情况类似,收割从 doc 0 开始,不同的是,collect 时会多一次过滤,即会比较当前 doc 的排序字段值和 search_after 值的大小,如果不满足条件则直接过滤掉,也就意味着 pq 操作更少。因此,整体来看性能会更好。

当然,如果写入的数据已按排序字段排序,那么当 pq 满后,会走优化分支触发提前终止,裁剪掉当前 segment 剩下的 doc。

Scroll

介绍

上一节讲了 search_after 可以用来做深分页,ES 还提供了一种分页技术 – Scroll 查询,Scroll 查询是一种在 ES 中扫描大量数据的常用方法。它通过在搜索结果中建立一个保持状态的 scroll_id 来实现。当您开始滚动时,ES 会返回第一批结果,并返回一个保持状态的 ID。使用此 ID,可以执行下一个滚动请求,以检索下一批结果。此过程可以重复进行,直到所有数据都被扫描完毕为止。

Scoll 查询的使用例子如下:

POST /my-index-000001/_search?scroll=1m
{
  "size": 100,
  "query": {
    "match": {
      "message": "foo"
    }
  }
}

第一次查询要指定 scroll 参数,参数值代表 scroll 上下文的保留时长,保留时间过期后,scroll_id 将失效。接着继续查询下一批数据:

POST /_search/scroll                                                               
{
  "scroll" : "1m",                                                                 
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" 
}

scroll 的用法很简单,除了第一次查询,后面的查询每次传入 scroll_id 便可以拉取下一页数据。由此可以看出,scroll 只能顺序往下翻页,不能往回翻。

实现原理

分三种情况讨论:

  1. 不带 query,不带 sort
  2. 带 query,不带 sort
  3. 带 sort
Scroll 查询会保留一个上下文 ScrollContext,上下文中包含了快照信息和上一页文档信息。

第一种情况其实就是将全量数据按存储顺序往后翻页,这种情况使用 PagingTopScoreDocCollector 来收割 doc,特殊的是收割的 doc 不再时从 0 开始,而是从上一页最后一个 doc+1 开始,往后收割 size 个 doc 即完成本次 scoll。由此可以看出,对于这种情况,查询效率非常高。

第二种情况会使用 PagingTopScoreDocCollector 来收割 doc,收割从 doc 0 开始,由于 Scroll 上下文保存了上一页 doc 信息(pq中存储了上一页的doc),会过滤掉分数低于 pqTop 的 doc(如果当前 doc 分数和 pqTop 分数相同,则比较 doc 值)。

第三种情况会使用 PagingFieldCollector 来收割 doc,收割从 doc 0 开始,收割的逻辑和 search_after 类似,collect 时会比较上一页最后一个 doc 的排序字段值,如果不满足条件则过滤掉当前 doc。

小结

这节介绍了三种分页方式的概念和原理,可以得出下面结论:

  • from+size 适合翻页灵活且页数不大的场景,即适合浅分页场景
  • search_after 适合深分页,也可以来回翻页(需要业务存储每页所需的search_after)
  • scroll 可以做分页,但是只能顺序往后翻页,不够灵活。所以它最适合做大量数据的导出(对顺序无要求,scan完所有数据即可)

总结

本文主要介绍了 Elasticsearch 中三种分页方式的概念,并详细分析了它们各自的实现原理,最后分析了它们各自适合的使用场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1922657.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于AT89C51单片机篮球计时计分器的设计(含文档、源码与proteus仿真,以及系统详细介绍)

本篇文章论述的是基于AT89C51单片机篮球计时计分器的设计的详情介绍&#xff0c;如果对您有帮助的话&#xff0c;还请关注一下哦&#xff0c;如果有资源方面的需要可以联系我。 目录 绪论 原理图 ​编辑 仿真图 系统总体设计图 代码实现 系统论文 资源下载 绪论 本次…

内网服务器通过squid代理访问外网

一、背景 现在要对172.16.58.158服务器进行openssh升级操作,我用之前写好的升级脚本执行后,发现没有备份旧的ssh程序文件,然后还卸载了oenssl-devel,然后我发现其他服务器ssh该服务器失败。同时脚本执行时报错“ configure: error: *** zlib.h missing - please install first …

windows查看局域网所有设备ip

windows如何查看局域网所有设备ip 操作方法 一 . 在搜索栏里输入cmd 二 .在命令行黑窗口输入arp -a 三 . 最上面显示的动态地址就是所有设备ip

day20、21、22补卡

235. 二叉搜索树的最近公共祖先 这道题的解题思路&#xff0c;我想了一会都没想出来&#xff0c;看了题解想&#xff1a;对于二叉搜索树&#xff0c;当我们从上向下去递归遍历&#xff0c;第一次遇到 cur节点是数值在[q, p]区间中&#xff0c;那么cur就是 q和p的最近公共祖先。…

Database数据库 vs Data Warehouse数据仓库 vs Data Mart数据集市 vs Data Lake数据湖

1.DATABASE 数据库 数据库是一个结构化的数据集合&#xff0c;用于存储、管理和检索数据。数据库设计用于支持事务处理&#xff08;OLTP&#xff0c;Online Transaction Processing&#xff09;和日常操作。 数据库通常由数据库管理系统&#xff08;DBMS&#xff09;控制&…

webRtc架构与目录结构

整体架构 目录结构 webrtc Modules目录

基于PCIe总线架构的2路1GSPS AD、4路1GSPS DA信号处理平台(100%国产化)

板卡概述 PCIE723-165是基于PCIE总线架构的2通道1GSPS采样率14位分辨率、4通道1GSPS采样率16位分辨率信号处理平台&#xff0c;该板卡采用国产16nm FPGA作为实时处理器&#xff0c;支持2路高速采集以及4路高速数据回放&#xff0c;板载2组DDR4 SDRAM大容量数据缓存&#xff0c;…

宝兰德参编金融智能体标准,深耕大模型场景化落地

随着数智化浪潮的不断推进&#xff0c;人工智能技术正深刻影响着金融服务的模式和流程&#xff0c;金融智能体在大模型的加持下&#xff0c;业务场景的应用能力得到强化。然而&#xff0c;作为新型技术&#xff0c;金融智能体在隐私保护、透明性、数据泄露等方面仍存在诸多风险…

图片存储问题总结

参考博客&#xff1a; https://blog.csdn.net/BUPT_Kwong/article/details/100972964 今天发现图片保存的一个神奇的问题&#xff0c;就是说原始的jpg图片打开后&#xff0c;重新保存成jpg格式&#xff0c;会发现这个结果不是很对的 example from PIL import Image import n…

房屋出租管理系统小程序需求分析及功能介绍

房屋租赁管理系统适用于写字楼、办公楼、厂区、园区、商城、公寓等商办商业不动产的租赁管理及租赁营销&#xff1b;提供资产管理&#xff0c;合同管理&#xff0c;租赁管理&#xff0c; 物业管理&#xff0c;门禁管理等一体化的运营管理平台&#xff0c;提高项目方管理运营效率…

【qt】QTcpSocket相关的信号

QTcpSocket可以在这里找到相关的信号 进行信号槽的关联 connect():这个信号在connectToHost()被调用并且连接已经成功建立之后发出 disconnected():该信号在套接字断开连接时发出 stateChanged(QAbstractSocket::SocketState socketState):每当QAbstractSocket的状态发生变化…

基于Adaboost的数据分类算法matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于Adaboost的数据分类算法matlab仿真,分别对比线性分类和非线性分类两种方式。 2.测试软件版本以及运行结果展示 MATLAB2022A版本运行 &#xff08;完整程序…

Python - Word转TXT文本,或TXT文本转Word

Word文档&#xff08;.doc或.docx&#xff09;和纯文本文件&#xff08;.txt&#xff09;是两种常用的文件格式。Word文档通常用于复杂的文档处理和排版&#xff0c;而纯文本文件则用于存储和传输纯文本信息。了解如何在这两种格式之间进行转换能提高工作效率&#xff0c;并便于…

Spring Boot中@Async注解的使用及原理 + 常见问题及解决方案

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

AV1 编码标准帧间预测技术详细说明

AV1 编码标准帧间预测 AV1&#xff08;AOMedia Video1&#xff09;是一种开源的视频编码格式&#xff0c;它在帧间预测技术上做出了显著的改进和扩展&#xff0c;以提供比现有标准更高的压缩效率和更好的视频质量。以下是AV1帧间预测技术的几个关键点&#xff1a; 参考帧扩展&a…

关于正点原子的alpha开发板的启动函数(汇编,自己的认识)

我傻逼了&#xff0c;这里的注释还是不要用&#xff1b; 全部换成 /* */ 这里就分为两块&#xff0c;一部分是复位中断部分&#xff0c;第二部分就是IRQ部分&#xff08;中断部分最重要&#xff09; 我就围绕着两部分来展开我的认识 首先声明全局 .global_start 在 ARM 架…

基于SpringBoot+VueJS+微信小程序技术的图书森林共享小程序设计与实现:7000字论文+源代码参考

博主介绍&#xff1a;硕士研究生&#xff0c;专注于信息化技术领域开发与管理&#xff0c;会使用java、标准c/c等开发语言&#xff0c;以及毕业项目实战✌ 从事基于java BS架构、CS架构、c/c 编程工作近16年&#xff0c;拥有近12年的管理工作经验&#xff0c;拥有较丰富的技术架…

4.4 文件管理

大纲 文件结构 真题 树形文件 真题 空间存储 一般考位示图法 真题

vue3使用Echarts图表生成项目进度甘特图

先看效果 代码展示 <template><h1>项目进度甘特图</h1><div id"app"><!-- Echarts 图表 --><div ref"progressChart" class"progressChart"></div></div> </template><script setup&…

The Web3 社区 Web3 产品经理课程

概述 / 深耕区块链行业 11 年&#xff0c;和很多产品经理都打过交道&#xff1b;遇到过优秀的产品经理&#xff0c;也遇到过比较拉垮的产品经理。多年工作中&#xff0c;曾在某些团队&#xff0c;承载技术兼产品经理的角色&#xff1b;也参与过很多 Web3 外包项目&#xff0c;包…