ElasticSearch - 深入解析 Elasticsearch Composite Aggregation 的分页与去重机制

文章目录

  • Pre
  • 概述
  • 什么是 `composite aggregation`?
  • 基本结构
  • `after` 参数的作用
    • 问题背景:传统分页的重复问题
    • `after` 的设计理念
    • 响应示例
  • `after` 如何确保数据不重复
    • 核心机制
    • Example
      • 步骤 1: 创建测试数据
        • 创建索引
        • 插入测试数据
      • 步骤 2: 查询第一页结果
        • 查询第一页
        • 返回结果
      • 步骤 3: 查询第二页结果
        • 查询第二页
        • 返回结果
      • 步骤 4: 查询第三页结果
        • 查询第三页
        • 返回结果
      • 步骤 5: 查询第四页结果
        • 查询第四页
        • 返回结果
      • 验证
      • 小结
  • 总结

在这里插入图片描述


Pre

ElasticSearch - 使用 Composite Aggregation 实现桶的分页查询

概述

在 Elasticsearch 中,composite aggregation 提供了一种高效的分页聚合方式,尤其适用于数据量较大的场景。为了避免传统分页中常见的重复数据问题,composite aggregation 引入了 after 参数。本文将详细探讨 after 参数的机制,以及它如何确保数据不重复。


什么是 composite aggregation

composite aggregation 是一种支持多字段分组的聚合类型,其独特之处在于可以实现分页功能。这种分页能力通过 size 参数控制每次返回的桶数量,并通过 after 参数获取下一页的结果。

基本结构

一个典型的 composite aggregation 查询:

GET /your_index_name/_search
{
  "size": 0,
  "aggs": {
    "my_composite_agg": {
      "composite": {
        "size": 10,
        "sources": [
          {
            "field1": {
              "terms": {
                "field": "your_field_name1"
              }
            }
          },
          {
            "field2": {
              "terms": {
                "field": "your_field_name2"
              }
            }
          }
        ]
      }
    }
  }
}

在以上查询中:

  • sources 定义了按哪些字段分组,字段顺序决定了分组键(bucket key)的生成顺序。
  • size 定义每页的桶数量。
  • 响应结果中的 after_key 用于获取下一页数据。

after 参数的作用

问题背景:传统分页的重复问题

在使用基于偏移量的分页(如 fromsize 参数)时,数据更新可能导致页码错乱或重复。例如:

  • 如果在分页过程中有新文档插入或更新,数据偏移可能导致某些文档重复出现在多页结果中。

after 的设计理念

after 参数是 composite aggregation 特有的,它记录了上一页最后一个桶的键值(after_key),并以此为起点获取下一页数据。这种方式基于排序键,确保分页过程始终连续、无重复。

响应示例

以下是一个分页查询的响应:

{
  "aggregations": {
    "my_composite_agg": {
      "buckets": [
        { "key": { "field1": "value1", "field2": "value2" }, "doc_count": 10 },
        { "key": { "field1": "value3", "field2": "value4" }, "doc_count": 8 }
      ],
      "after_key": { "field1": "value3", "field2": "value4" }
    }
  }
}

在下一页查询中,可以使用 after_key 作为起点:

GET /your_index_name/_search
{
  "size": 0,
  "aggs": {
    "my_composite_agg": {
      "composite": {
        "size": 10,
        "after": { "field1": "value3", "field2": "value4" },
        "sources": [
          {
            "field1": {
              "terms": {
                "field": "your_field_name1"
              }
            }
          },
          {
            "field2": {
              "terms": {
                "field": "your_field_name2"
              }
            }
          }
        ]
      }
    }
  }
}

after 如何确保数据不重复

核心机制

  1. 排序保证一致性

    • composite aggregation 内部按照 sources 中定义的字段顺序生成桶键,并进行字典序排序。
    • 每次查询的结果顺序是固定的,即使数据发生变动,也不会影响之前已返回的桶键。
  2. 分页起点记录

    • 每次查询都会返回 after_key,表示当前页最后一个桶的键值。
    • 在下一页查询中,Elasticsearch 从该键值开始,获取后续的桶。
  3. 跳过已处理的桶

    • Elasticsearch 在执行查询时,会严格按照 after_key 跳过已处理的桶,确保每个桶仅返回一次。
  4. 游标精准定位

    • after_key 明确表示从上次分页结果的最后一个桶之后开始读取,而不会重新读取已经返回的桶。
    • 查询总是基于 key 的排序位置,按顺序依次获取后续的桶。
  5. 无偏移计算

    • 不使用 fromsize 等偏移量参数,避免了由于数据插入或删除导致的分页偏移问题。
  6. 全局一致性排序

    • 所有桶的排序是全局确定的,即使数据分布在多个分片中,也能按照统一的顺序返回。
    • Elasticsearch 会在多个分片中进行合并排序,从而确保每次分页的桶是唯一且无重复的。

Example

步骤 1: 创建测试数据

我们创建一个名为 test_index 的索引,并插入一些测试数据。数据包含一个字段 category,我们将根据这个字段进行聚合分页。

创建索引
PUT /test_index
{
  "mappings": {
    "properties": {
      "category": {
        "type": "keyword"
      },
      "value": {
        "type": "integer"
      }
    }
  }
}
插入测试数据
POST /test_index/_bulk
{ "index": {} }
{ "category": "A", "value": 10 }
{ "index": {} }
{ "category": "A", "value": 20 }
{ "index": {} }
{ "category": "A", "value": 30 }
{ "index": {} }
{ "category": "B", "value": 40 }
{ "index": {} }
{ "category": "B", "value": 50 }
{ "index": {} }
{ "category": "B", "value": 60 }
{ "index": {} }
{ "category": "C", "value": 70 }
{ "index": {} }
{ "category": "C", "value": 80 }
{ "index": {} }
{ "category": "C", "value": 90 }
{ "index": {} }
{ "category": "D", "value": 100 }
{ "index": {} }
{ "category": "D", "value": 110 }
{ "index": {} }
{ "category": "D", "value": 120 }
{ "index": {} }
{ "category": "E", "value": 130 }
{ "index": {} }
{ "category": "E", "value": 140 }
{ "index": {} }
{ "category": "E", "value": 150 }
{ "index": {} }
{ "category": "F", "value": 160 }
{ "index": {} }
{ "category": "F", "value": 170 }
{ "index": {} }
{ "category": "F", "value": 180 }
{ "index": {} }
{ "category": "G", "value": 190 }
{ "index": {} }
{ "category": "G", "value": 200 }
{ "index": {} }
{ "category": "G", "value": 210 }
{ "index": {} }
{ "category": "H", "value": 220 }
{ "index": {} }
{ "category": "H", "value": 230 }
{ "index": {} }
{ "category": "H", "value": 240 }
{ "index": {} }
{ "category": "I", "value": 250 }
{ "index": {} }
{ "category": "I", "value": 260 }
{ "index": {} }
{ "category": "I", "value": 270 }
{ "index": {} }
{ "category": "J", "value": 280 }
{ "index": {} }
{ "category": "J", "value": 290 }
{ "index": {} }
{ "category": "J", "value": 300 }
{ "index": {} }
{ "category": "K", "value": 310 }
{ "index": {} }
{ "category": "K", "value": 320 }
{ "index": {} }
{ "category": "K", "value": 330 }
{ "index": {} }
{ "category": "L", "value": 340 }
{ "index": {} }
{ "category": "L", "value": 350 }
{ "index": {} }
{ "category": "L", "value": 360 }
{ "index": {} }
{ "category": "M", "value": 370 }
{ "index": {} }
{ "category": "M", "value": 380 }
{ "index": {} }
{ "category": "M", "value": 390 }
{ "index": {} }
{ "category": "N", "value": 400 }
{ "index": {} }
{ "category": "N", "value": 410 }
{ "index": {} }
{ "category": "N", "value": 420 }
{ "index": {} }
{ "category": "O", "value": 430 }
{ "index": {} }
{ "category": "O", "value": 440 }
{ "index": {} }
{ "category": "O", "value": 450 }
{ "index": {} }
{ "category": "P", "value": 460 }
{ "index": {} }
{ "category": "P", "value": 470 }
{ "index": {} }
{ "category": "P", "value": 480 }
{ "index": {} }
{ "category": "Q", "value": 490 }
{ "index": {} }
{ "category": "Q", "value": 500 }
{ "index": {} }
{ "category": "Q", "value": 510 }
{ "index": {} }
{ "category": "R", "value": 520 }
{ "index": {} }
{ "category": "R", "value": 530 }
{ "index": {} }
{ "category": "R", "value": 540 }
{ "index": {} }
{ "category": "S", "value": 550 }
{ "index": {} }
{ "category": "S", "value": 560 }
{ "index": {} }
{ "category": "S", "value": 570 }
{ "index": {} }
{ "category": "T", "value": 580 }
{ "index": {} }
{ "category": "T", "value": 590 }
{ "index": {} }
{ "category": "T", "value": 600 }
{ "index": {} }
{ "category": "U", "value": 610 }
{ "index": {} }
{ "category": "U", "value": 620 }
{ "index": {} }
{ "category": "U", "value": 630 }
{ "index": {} }
{ "category": "V", "value": 640 }
{ "index": {} }
{ "category": "V", "value": 650 }
{ "index": {} }
{ "category": "V", "value": 660 }
{ "index": {} }
{ "category": "W", "value": 670 }
{ "index": {} }
{ "category": "W", "value": 680 }
{ "index": {} }
{ "category": "W", "value": 690 }
{ "index": {} }
{ "category": "X", "value": 700 }
{ "index": {} }
{ "category": "X", "value": 710 }
{ "index": {} }
{ "category": "X", "value": 720 }
{ "index": {} }
{ "category": "Y", "value": 730 }
{ "index": {} }
{ "category": "Y", "value": 740 }
{ "index": {} }
{ "category": "Y", "value": 750 }
{ "index": {} }
{ "category": "Z", "value": 760 }
{ "index": {} }
{ "category": "Z", "value": 770 }
{ "index": {} }
{ "category": "Z", "value": 780 }

步骤 2: 查询第一页结果

我们使用 composite aggregation 查询第一页,设置每页返回 3 个桶。

查询第一页
GET /test_index/_search
{
  "size": 0,
  "aggs": {
    "composite_agg": {
      "composite": {
        "size": 10,
        "sources": [
          { "category": { "terms": { "field": "category" } } }
        ]
      }
    }
  }
}
返回结果
 {
  "took" : 11,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 78,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "composite_agg" : {
      "after_key" : {
        "category" : "J"
      },
      "buckets" : [
        {
          "key" : {
            "category" : "A"
          },
          "doc_count" : 3
        },
        {
          "key" : {
            "category" : "B"
          },
          "doc_count" : 3
        },
        {
          "key" : {
            "category" : "C"
          },
          "doc_count" : 3
        },
        {
          "key" : {
            "category" : "D"
          },
          "doc_count" : 3
        },
        {
          "key" : {
            "category" : "E"
          },
          "doc_count" : 3
        },
        {
          "key" : {
            "category" : "F"
          },
          "doc_count" : 3
        },
        {
          "key" : {
            "category" : "G"
          },
          "doc_count" : 3
        },
        {
          "key" : {
            "category" : "H"
          },
          "doc_count" : 3
        },
        {
          "key" : {
            "category" : "I"
          },
          "doc_count" : 3
        },
        {
          "key" : {
            "category" : "J"
          },
          "doc_count" : 3
        }
      ]
    }
  }
}

步骤 3: 查询第二页结果

我们使用第一页返回的 after_key{ "category": "J" } 来查询第二页。

查询第二页
GET /test_index/_search
{
  "size": 0,
  "aggs": {
    "composite_agg": {
      "composite": {
        "size": 10,
        "after": { "category": "J" },
        "sources": [
          { "category": { "terms": { "field": "category" } } }
        ]
      }
    }
  }
}


返回结果

在这里插入图片描述


步骤 4: 查询第三页结果

使用第二页返回的 after_key{ "category": "T" } 查询第三页。

查询第三页
GET /test_index/_search
{
  "size": 0,
  "aggs": {
    "composite_agg": {
      "composite": {
        "size": 10,
        "after": { "category": "T" },
        "sources": [
          { "category": { "terms": { "field": "category" } } }
        ]
      }
    }
  }
}
返回结果

在这里插入图片描述


步骤 5: 查询第四页结果

使用第三页返回的 after_key{ "category": "Z" } 查询第三页。

查询第四页
GET /test_index/_search
{
  "size": 0,
  "aggs": {
    "composite_agg": {
      "composite": {
        "size": 10,
        "after": { "category": "Z" },
        "sources": [
          { "category": { "terms": { "field": "category" } } }
        ]
      }
    }
  }
}
返回结果

在这里插入图片描述


验证

通过四次分页查询,我们验证以下几点:

  1. 结果无重复

    • 每页的结果是唯一的,没有重复桶。例如:
      • 第 1 页返回桶:A, B, CJ
      • 第 2 页返回桶:K, L, MT
      • 第 3 页返回桶:U, VZ
      • 第 4 页返回桶:已到最后
  2. 顺序一致

    • 所有结果按照 category 字段值排序,顺序为 A, B, C, …, Z
  3. after_key 确保正确游标定位

    • 使用 after_key 明确标识分页起点,每次从上页的最后一个桶的 key 开始查询,没有遗漏或重复。

小结

  • composite aggregation 使用基于 after_key 的游标机制,可以确保分页查询中数据无重复、无遗漏。
  • composite aggregation 的设计特别适合大规模数据的聚合和分页,是传统 from + size 分页方法的高效替代方案。

通过 after_key 的分页,可以看到每页数据互不重叠,且严格按照 category 字段排序。


总结

传统分页 (from + size)Composite Aggregation (游标)
基于偏移计算,容易因数据变动重复基于游标,桶的顺序和定位稳定无重复
数据量大时性能下降明显高效处理大数据,避免偏移的性能开销
不支持跨分片排序跨分片排序一致性,返回结果无重复或遗漏
  • composite aggregation 使用基于 after_key 的游标机制,可以确保分页查询中数据无重复、无遗漏。
  • composite aggregation 的设计特别适合大规模数据的聚合和分页,是传统 from + size 分页方法的高效替代方案。

composite aggregation 的设计通过排序和 after_key 机制,确保分页查询的每页数据互不重复,且顺序一致。这种特性使其在大数据量的分页聚合中表现出色。如果应用场景需要可靠的分页聚合,可以尝试 composite aggregation

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL 8.0:explain analyze 分析 SQL 执行过程

介绍 MySQL 8.0.16 引入一个实验特性:explain formattree ,树状的输出执行过程,以及预估成本和预估返 回行数。在 MySQL 8.0.18 又引入了 EXPLAIN ANALYZE,在 formattree 基础上,使用时,会执行 SQL &#…

【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567

本文涉及知识点 打开打包代码的方法兼述单元测试 C动态规划 C图论 LeetCode3243. 新增道路查询后的最短距离 I 给你一个整数 n 和一个二维整数数组 queries。 有 n 个城市,编号从 0 到 n - 1。初始时,每个城市 i 都有一条单向道路通往城市 i 1&#…

120页PPT讲解ChatGPT如何与财务数字化转型的业财融合

此方案主要聚焦于利用ChatGPT技术与数字化转型推动业财融合,实现企业的价值最大化。首先,通过ChatGPT技术,企业可以构建生成式对话机器人,自动回答常见问题,减轻人工客服的压力,提高响应速度。这种机器人具…

旋转框目标检测自定义数据集训练测试流程

文章目录 前言一、数据集制作二、模型训练2.1 划分训练集验证集:2.2 配置yaml文件:2.3 训练 前言 旋转框目标检测(Rotated bounding box object detection)是计算机视觉领域的一项技术,它用于检测图像中具有任意方向的目标。与传统的水平矩形…

【QSS样式表 - ⑥】:QPushButton控件样式

文章目录 QPushBUtton控件样式QSS示例 QPushBUtton控件样式 常用子控件 常用伪状态 QSS示例 代码: QPushButton {background-color: #99B5D1;color: white;font-weigth: bold;border-radius: 20px; }QPushButton:hover {background-color: red; }QPushButton:p…

C# Random 随机数 全面解析

总目录 前言 一、Random 是什么? 1. 简介 表示伪随机数生成器,这是一种能够产生满足某些随机性统计要求的数字序列的算法。 public class Random继承:Object → Random 2. 构造函数 3. 属性 4. 方法 二、Random 的使用 1. Next() 或 Nex…

Linux网络——UDP的运用

Linux网络——UDP的运用 文章目录 Linux网络——UDP的运用一、引入二、服务端实现2.1 创建socket套接字2.2 绑定bind2.3 启动服务器2.4 IP的绑定的细节2.5 读取数据recvfrom 三、用户端实现3.1 绑定问题3.2 发送数据sendto 四、代码五、UDP实现网络聊天室(简易版&am…

IDEA使用Alt + Enter快捷键自动接受返回值一直有final修饰的问题处理

在使用IDEA的过程中,使用快捷键Alt Enter在接收返回值时,可以快速完成参数接收,但前面一直会出现接收参数前面有final修饰的情况,效果如下所示: 看着真烦人呢,我们会发现在接受到返回值是上方有个 Declare…

【小白51单片机专用教程】protues仿真AT89C51入门

课程特点 无需开发板0基础教学软件硬件双修辅助入门 本课程面对纯小白,因此会对各个新出现的知识点在实例基础上进行详细讲解,有相关知识的可以直接跳过。课程涉及protues基本操作、原理图设计、数电模电、kell使用、C语言基本内容,所有涉及…

软件设计与体系结构

1.简要说明什么是软件体系结构,软件体系结构模型,为什么要建立软件体系结构模型? 答:软件体系结构指一个软件系统在高层次上的结构化组织方式,包括系统的组成部分和各个部分之间的关系,以及它们与环境之间的…

位置式PID-控制步进电机-位置环-stm32

基本原理 1、软件设计 本闭环控制例程是在步进电机编码器测速例程的基础上编写的,这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请参考本章配套的工程。 我们创建了4个文件:bsp_pid.c和bsp_pid.h文件用来存放PID控制器相关程序,bsp_s…

汽车IVI中控开发入门及进阶(47):CarPlay开发

概述: 车载信息娱乐(IVI)系统已经从仅仅播放音乐的设备发展成为现代车辆的核心部件。除了播放音乐,IVI系统还为驾驶员提供导航、通信、空调、电源配置、油耗性能、剩余行驶里程、节能建议和许多其他功能。 ​ 驾驶座逐渐变成了你家和工作场所之外的额外生活空间。2014年,…

电力通信规约-104实战

电力通信规约-104实战 概述 104规约在广泛应用于电力系统远动过程中,主要用来进行数据传输和转发,本文将结合实际开发实例来讲解104规约的真实使用情况。 实例讲解 因为个人技术栈是Java,所以本篇将采用Java实例来进行讲解。首先我们搭建一…

AWS Transfer 系列:简化文件传输与管理的云服务

在数字化转型的今天,企业对文件传输、存储和管理的需求日益增长。尤其是对于需要大量数据交换的行业,如何高效、可靠地传输数据成为了一大挑战。为了解决这一难题,AWS 提供了一系列的文件传输服务,统称为 AWS Transfer 系列。这些…

动态规划<四> 回文串问题(含对应LeetcodeOJ题)

目录 引例 其余经典OJ题 1.第一题 2.第二题 3.第三题 4.第四题 5.第五题 引例 OJ 传送门Leetcode<647>回文子串 画图分析&#xff1a; 使用动态规划解决 原理&#xff1a;能够将所有子串是否是回文的信息保存在dp表中 在使用暴力方法枚举出所有子串&#xff0c;是…

stm32定时器输出比较----驱动步进电机

定时器输出比较理论 OC(Output Compare)输出比较输出比较可以通过比较CNT与CCR寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形每个高级定时器和通用定时器都拥有4个输出比较通道高级定时器的前3个通道额外拥有死区生成和互补输出…

Windows电脑部署SD 3.5结合内网穿透随时随地生成高质量AI图像

文章目录 前言1. 本地部署ComfyUI2. 下载 Stable Diffusion3.5 模型3. 演示文生图4. 公网使用Stable Diffusion 3.5 大模型4.1 创建远程连接公网地址 5. 固定远程访问公网地址 前言 在数字化创意时代&#xff0c;AI技术的发展为我们带来了无限可能。尤其是对于那些追求高效和高…

Easysearch Java SDK 2.0.x 使用指南(二)

在 上一篇文章 中&#xff0c;我们介绍了 Easysearch Java SDK 2.0.x 的基本使用和批量操作。本文将深入探讨索引管理相关的功能&#xff0c;包括索引的创建、删除、开关、刷新、滚动等操作&#xff0c;以及新版 SDK 提供的同步和异步两种调用方式。 SDK 的对象构建有两种方式…

Scala——身份证号码查询籍贯

object Test_身份证查询籍贯 { def main(args: Array[String]): Unit { val code "42005200210030051".substring(0,2) println(code) //判断42是哪个省的 //湖北 // if(code "42"){ // println("42对应省份为&#xff1a;湖北") // }else…

分布式系统架构:限流设计模式

1.为什么要限流&#xff1f; 任何一个系统的运算、存储、网络资源都不是无限的&#xff0c;当系统资源不足以支撑外部超过预期的突发流量时&#xff0c;就应该要有取舍&#xff0c;建立面对超额流量自我保护的机制&#xff0c;而这个机制就是微服务中常说的“限流” 2.四种限流…