Elasticsearch 8.X 聚合查询下的精度问题及其解决方案

news2024/9/22 1:36:57

1、线上环境问题

咕泡同学提问:我在看runtime文档的时候做个测试, agg求avg的时候不管是double还是long,数据都不准确,这种在生产环境中如何解决啊?

737b136799fdb8d9b14bda46f0de0414.jpeg

37dbd666e63f5fd02159ffc2913546df.png

2、问题归类及出现场景

上述问题可以归类为:Elasticsearch聚合查询下的精度问题。

在日常的数据处理工作中,我们经常会遇到使用Elasticsearch进行大数据查询、统计、聚合等操作。Elasticsearch在实践中表现出优秀的搜索性能,但在一些复杂的聚合操作,如求平均值(avg)时,可能会出现数据精度不准的问题。

接下来我们将详细介绍这个问题的出现场景、可能的原因以及解决方案。

在Elasticsearch中,数据精度问题主要出现在聚合(aggregation)操作中。比如我们在做一些大数运算时,如求和(sum)、求平均值(avg),可能会遇到数据类型(double或long)导致的精度问题。这是因为Elasticsearch在进行聚合操作时,为了提高性能和效率,会使用一种叫做“浮点数计算”的方式来做大数运算,而这种计算方式在处理大数时往往会丢失一些精度

3、问题最小化复现

以一个简单的例子来说明这个问题。我们在Elasticsearch中存储了一些商品数据,现在我们想要计算所有商品的平均价格。

数据和查询的DSL如下(已在 Elasticsearch 8.X 环境下验证过):

  • 数据:

POST /product/_bulk
{ "index" : { "_id" : "1" } }
{ "name" : "商品1", "price" : 1234.56 }
{ "index" : { "_id" : "2" } }
{ "name" : "商品2", "price" : 7890.12 }
  • 查询DSL:

GET /product/_search
{
  "size": 0,
  "aggs": {
    "avg_price": {
      "avg": {
        "field": "price"
      }
    }
  }
}

虽然我们期望得到的平均价格是 (1234.56 + 7890.12) / 2 = 4562.34,但是由于浮点数计算的精度问题,返回的结果可能会略有偏差,如下图所示。

e4cdb04ce9266742828c7d6d576221ce.png

4、解决方案探讨与实现

如何解决上述聚合后精度问题呢?我们结合 Elasticsearch 基础知识和实战经验,给出如下三种解决方案。

  • 方案一:借助 scaled_float 类型提升精度。

  • 方案二:使用 scripted_metric 提升精度。

  • 方案三:业务层面自己写代码实现。

接下来,我们逐一实战且解读上述三个方案。

4.1 借助 scaled_float 类型提升精度

4.1.1 什么是scaled_float?

scaled_float 是 Elasticsearch提供的一种特殊的数字数据类型,用于存储带小数的数字。

float double 不同, scaled_float 实际上是一个 long 类型,只是它将实际的浮点数乘以一个给定的缩放因子进行存储。

在许多应用场景中,我们需要存储具有小数的数字,例如价格、评分等。float double 是常用的数据类型,但它们有一些问题:例如,它们在存储和排序时可能会丢失精度,而且它们占用的存储空间比整数类型要多。而  scaled_float 实际上是将浮点数乘以一个 scaling factor,然后将结果存储为  long

例如,如果 scaling factor 是100,那么数字12.34将会被存储为1234。在查询和返回结果时,Elasticsearch将会除以 scaling factor ,返回原始的浮点数。

4.1.2 scaled_float的优势

  • 精度更准确可控

与float和double相比,scaled_float在存储和排序时更准确,因为它实际上是存储的长整数,不存在浮点数的精度问题。

  • 性能更好

由于scaled_float使用的是long类型,因此占用的存储空间更小,性能也更好。

  • 灵活性更强

可以根据需要设置scaling factor,以平衡精度和性能。如果需要更高的精度,可以使用较大的scaling factor。如果项目需求关注性能和存储空间,可以使用较小的scaling factor。

4.1.3 在Elasticsearch中使用scaled_float

要在Elasticsearch中使用scaled_float,需要在映射中定义字段类型,并提供一个scaling factor。例如:

{
  "properties": {
    "price": {
      "type": "scaled_float",
      "scaling_factor": 100.0
    }
  }
}

这个映射定义了一个名为price的scaled_float字段,它的scaling factor是100。这意味着所有的价格都将乘以100,然后作为long存储。

例如,价格12.34将会被存储为1234。

总的来说,scaled_float是一个非常有用的工具,可以在需要存储浮点数的情况下提供更好的精度和性能。

4.1.4 实战一把,解决开篇类似问题

在这个例子中,我们有两个产品,它们的价格是浮点数。

如果想要使用scaled_float,首先需要设置一个映射(mapping)。假设想要以精确到分的精度存储价格,那么可以设置scaling_factor为100.0。以下是如何定义映射的步骤:

首先,创建一个新的索引并定义映射:

PUT /product
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "price": {
        "type": "scaled_float",
        "scaling_factor": 100.0
      }
    }
  }
}

这个命令创建了一个新的索引 product,并定义了两个字段:name(类型为text)和price(类型为scaled_float,scaling_factor为100.0)。

然后,批量 bulk 插入数据如下:

POST /product/_bulk
{ "index" : { "_id" : "1" } }
{ "name" : "商品1", "price" : 1234.56 }
{ "index" : { "_id" : "2" } }
{ "name" : "商品2", "price" : 7890.12 }

在这个过程中,price字段的值会自动乘以scaling_factor(在这里是100.0),然后存储为long类型。所以实际存储的值是123456和789012。

查询时,Elasticsearch会自动将价格除以scaling_factor,返回原始的浮点数。例如,如果执行以下查询:

GET /product/_doc/1

返回的结果将会是:

{
  "_index": "product",
  "_id": "1",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "name": "商品1",
    "price": 1234.56
  }
}

尽管price在存储时被乘以了100,但在查询时它又被除以了100,所以看到的价格仍然是1234.56。

这样,可以在保持较高精度的同时,使用更少的存储空间和更好的性能来存储和查询价格了。

最终咱们实现如下:

GET product/_search
{
  "size": 0,
  "aggs": {
    "avg_price": {
      "avg": {
        "field": "price"
      }
    }
  }
}

如下所示,结果精度达到预期值。

b1c23a9853a3cdb9b44e273c31bc852a.png

4.2 使用scripted_metric提高精度

面对这种情况,我们可以使用Elasticsearch的另一个强大功能 —— 脚本计算(scripted_metric)来解决。

scripted_metric允许我们自定义复杂的聚合逻辑,比如下面的DSL:

####务必要删除索引
DELETE product

POST /product/_bulk
{ "index" : { "_id" : "1" } }
{ "name" : "商品1", "price" : 1234.56 }
{ "index" : { "_id" : "2" } }
{ "name" : "商品2", "price" : 7890.12 }

GET /product/_search
{
  "size": 0,
  "aggs": {
    "avg_price": {
      "scripted_metric": {
        "init_script": "state.total = 0.0; state.count = 0",
        "map_script": "state.total += params._source.price; state.count++",
        "combine_script": "HashMap result = new HashMap(); result.put('total', state.total); result.put('count', state.count); return result",
        "reduce_script": """
  double total = 0.0; long count = 0; 
  for (state in states) { 
    total += state['total']; 
    count += state['count']; 
  }
  double average = total / count;
  DecimalFormat df = new DecimalFormat("#.00");
  return df.format(average);
        """
      }
    }
  }
}

Elasticsearch是一个分布式搜索和分析引擎,意味着数据可以在多个分片上存储和处理。为了处理分布式数据,Elasticsearch使用了一种名为map-reduce的编程模型。这个模型分为两个步骤:映射(Map)和归约(Reduce)。init_script,map_script,combine_script和reduce_script都是这个模型的组成部分,用于实现更复杂的聚合。

在如上的脚本中,我们定义了四个步骤:

  • init_script:初始化脚本,在每个分片上为每个聚合创建一个新的状态。

  • map_script:映射脚本,用于处理输入文档,并将其状态转化为一个可以合并的格式。

  • combine_script:组合脚本,用于在节点级别合并每个分片的状态。

  • reduce_script:归约脚本,用于在全局范围内合并状态。

通过这种方式,我们可以得到一个更精确的平均值。

上述脚本的具体含义解释如下:

  • init_script:这个脚本在每个分片上执行一次,为每个分片创建一个新的状态。

在上述脚本中,它创建了一个状态对象,其中包含了一个总和(total)和一个计数器(count)。这个状态对象被初始化为{total: 0.0, count: 0}。

  • map_script:这个脚本在每个文档上执行一次。

在上述脚本中,它读取每个文档的price字段,并将这个值添加到total,同时增加count的值。这样,total会包含所有文档价格的总和,count会包含处理过的文档数量。

  • combine_script:这个脚本在每个分片上执行一次,对每个分片的状态进行组合。

在上述脚本中,它只是将totalcount放入一个HashMap中返回。如果有很多状态需要合并,可能会在这个脚本中进行一些预处理。

  • reduce_script:这个脚本在结果合并时执行一次,将所有分片的状态进行归约,计算出最终结果。

在上述脚本中,它遍历所有分片的状态,计算总的totalcount,然后计算平均价格。DecimalFormat用于将平均价格格式化为两位小数的字符串。

简单来说,这就是一个分步计算平均值的过程:首先初始化状态,然后为每个文档更新状态,接着在每个分片上合并状态,最后在全局范围内合并状态并计算结果。

最终结果如下图所示,达到预期精度。

77ca765af6678045470c08d8f11974d2.png

4.3 业务层面自己写代码实现。

在应用层面进行精度控制:将原始数据获取到应用层,然后在应用层进行精确的计算。这种方法的优点是可以得到非常精确的结果,但缺点是可能需要处理大量的数据,增加了网络传输和计算的负担。

在应用层面处理数据的精度问题通常需要两个步骤:

  • 首先,需要从 Elasticsearch 获取原始数据;

  • 然后,在应用层进行精确的计算。

以下是一个使用Java处理数据精度的例子:

假设系统应用是用 Java 编写的,可以使用 Java 的 BigDecimal 类进行精确的浮点数计算。以下是一个简单的例子:

BigDecimal price1 = new BigDecimal("1234.56");
BigDecimal price2 = new BigDecimal("7890.12");
BigDecimal average = price1.add(price2).divide(new BigDecimal(2), 2, RoundingMode.HALF_UP);

System.out.println(average);  // 输出:4562.34

上述示例中,我们首先创建了两个 BigDecimal 对象,代表两个价格。然后我们调用add方法将它们加起来,然后调用 divide 方法计算平均值。最后,我们使用 RoundingMode.HALF_UP 参数来控制舍入模式。

请注意,这种方法需要在应用层处理所有的数据,如果数据量很大,那么这可能会导致性能问题。为了减少数据传输和计算的负担,可能需要在Elasticsearch中使用更精确的查询来只获取需要的数据,或者使用Elasticsearch的聚合功能来减少返回的数据量。

此外,可能还需要在应用层做一些优化,比如使用并行处理、缓存等技术来提高处理性能。具体的方法会根据应用的具体情况和需求来决定。

5、小结

总的来说,虽然Elasticsearch在进行聚合操作时可能会出现数据精度不准的问题,但是通过借助 scaled_float 类型提升精度、使用 scripted_metric 提升精度以及业务层面自己写代码实现三种方案得到较为精确的结果。

在遇到类似的问题时,我们需要根据实际情况选择最适合的解决方案。一方面要考虑精度的要求,另一方面也要考虑查询性能和资源消耗。我们应该根据业务的实际需求,适时地使用脚本计算来提高聚合操作的精度。

推荐阅读

  1. 全网首发!从 0 到 1 Elasticsearch 8.X 通关视频

  2. 重磅 | 死磕 Elasticsearch 8.X 方法论认知清单

  3. 如何系统的学习 Elasticsearch ?

  4. 2023,做点事

f61a4e975f2aec87d8cd8470e352d7a7.jpeg

更短时间更快习得更多干货!

和全球 近2000+ Elastic 爱好者一起精进!

cc625dfb83be0e5364b17070320e54ab.gif

大模型时代,抢先一步学习进阶干货!

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

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

相关文章

【计算机组成与体系结构Ⅰ】实验3 微程序控制器实验

一、实验目的 了解微程序控制器的组成原理 二、实验设备 TEC-4实验系统、万用表 三、实验内容 1:阅读微指令格式和微程序控制器的组成,微指令由操作控制和顺序控制两部分组成,1条微指令的字长35位(一个存储单元35位,采…

xml建模----详细完整,易懂结合代码分析

目录 一.XML建模是什么 二.XML建模有什么作用??? 三.XML建模的案例 以config.xml为例 一.XML建模是什么 将XML配置文件中的元素、属性、文本信息转换成对象的过程叫做XML建模 二.XML建模有什么作用??? …

Git命令操作【全系列】

Git常用命令操作 1 基础命令 ①git config --global user.name [‘你的用户名’]:查看/设置 git config --global user.name ziyi:设置用户名为ziyi git config --global user.name:查看用户名 ②git config --global user.email [‘你的邮…

React 在Dva项目中修改路由配置,并创建一个自己的路由

之后的话 我们还是来看一下Dva路由的配置 首先 我们在项目刚创建完 他就给了我们一个路径 叫routes 然后 IndexPage.js 是最初的一个组件 之前我们也用过了 然后 我们看到 src目录下的 index.js 这里 就有一个路由的匹配 他加载的就是 同目录下 一个 router的文件 我们点开…

外币兑换----贪心1 (爱思创)

源代码 #include <bits/stdc.h> using namespace std; int main() {double money,maxn-1,r;cin>>money;for(int i1;i<12;i){cin>>r;maxnmax(maxn,money*r);}printf("%0.2f",maxn);return 0; }

阿里云通义万相官网申请地址

通义万相刻削生千变&#xff0c;丹青图“万相”。我是通义万相&#xff0c;一个不断进化的AI绘画创作模型https://wanxiang.aliyun.com/现在申请的人少 应该好通过吧

软考高级之系统架构师系列之系统配置与性能评价、信息化基础

系统配置与性能评价 性能 计算机系统的性能一般包括两个大的方面&#xff1a; 可用性&#xff0c;也就是计算机系统能正常工作的时间&#xff0c;其指标可以是能够持续工作的时间长度&#xff0c;也可以是在一段时间内&#xff0c;能正常工作的时间所占的百分比处理能力&…

MySQL练习题(3)

创建数据库插入数据 CREATE TABLE emp ( empno int(4) NOT NULL, ename varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, job varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, mgr int(4) NULL DEFAULT N…

ndoe中express框架的基本使用,接收get、post请求,以及处理回调地狱的优雅解决方法

一、express框架的基本使用 Express框架是Node.js中最受欢迎的web开发框架&#xff0c;它的设计简洁而且功能强大&#xff0c;有着大量的插件和社区支持。 基于Express使用Node.js创建web应用的基本步骤如下&#xff1a; 首先你需要安装Node.js和npm&#xff08;Node包管理器…

拥抱简洁:探索Stylus的简洁语法与CSS预处理器之美

文章目录 1. 简洁的语法2. 强大的功能3. 嵌套规则4. 变量支持5. Mixin 混合6. 扩展支持7. 条件语句8. 内置函数9. 可扩展性10. 轻量高效附录&#xff1a;前后端实战项目&#xff08;简历必备&#xff09; 推荐&#xff1a;★★★★★ Stylus 是一种 CSS 预处理器&#xff0c;具…

OpenCV 入门教程:Laplacian算子和Canny边缘检测

OpenCV 入门教程&#xff1a; Laplacian 算子和 Canny 边缘检测 导语一、Laplacian 算子二、Canny 边缘检测三、示例应用3.1 图像边缘检测3.2 边缘增强 总结 导语 边缘检测在图像处理和计算机视觉领域中起着重要的作用。 Laplacian 算子和 Canny 边缘检测是两种常用的边缘检测…

pytorch-Tensor

神经网络的数据存储中都使用张量&#xff08;Tensor&#xff09;&#xff0c;那张量又是什么呢&#xff1f; py 张量这一概念的核心在于&#xff0c;它是一个数据容器。它包含的数据几乎总是数值数据&#xff0c;因此它是数字的容器。你可能对矩阵很熟悉&#xff0c;它是二…

商城小程序页面展示

——首页登录&#xff08;wx.login()&#xff0c;getPhoneNumber&#xff09; 进入首页时&#xff0c;加载商品列表数据展示在页面。从缓存中获取token信息&#xff0c;判断用户登录状态&#xff0c;如果用户没有登录&#xff0c;调用微信小程序的login方法&#xff0c;进行登…

Spring Boot原理分析(二):项目启动(下)——自动装配

文章目录 一、Spring手动装配1.使用XML配置文件2.使用Java注解3.使用Java类 二、Spring Boot自动装配1.AutoConfigurationPackage2.Import(AutoConfigurationImportSelector.class) 一、Spring手动装配 Spring Framework提供了多种手动装配的方式&#xff0c;其中比较常见的有…

硬盘被程序使用

diskutil list diskutil umount /dev/disk2s1退出该进程 硬盘即可成功退出

springboot+redis+mysql+quartz-通过Java操作jedis使用pipeline获取缓存数据定时更新数据库

一、重点 代码讲解&#xff1a;6-点赞功能-定时持久化到数据库-pipelinelua-优化pipeline_哔哩哔哩_bilibili https://www.bilibili.com/video/BV1yP411C7dr 代码&#xff1a; blogLike_schedule/like06 xin麒/XinQiUtilsOrDemo - 码云 - 开源中国 (gitee.com) https://g…

ubuntu下,verdi语法错误Syntax error: “(“ unexpected

【问题】/home/EDA_TOOLS/synopsys/verdi/verdi/Verdi_O-2018.09-SP2/bin/verdi: 56: /home/EDA_TOOLS/synopsys/verdi/verdi/Verdi_O-2018.09-SP2/bin/verdi: Syntax error: "(" unexpected 【解析】 代码对于标准bash而言没有错&#xff0c;因为Ubuntu/Debian为了加…

网络应用基础交换机的基础操作(NETBASE第六课)

网络应用基础交换机的基础操作&#xff08;NETBASE第六课&#xff09; 1 回顾代码实操 主题背景的转换 字体设置 背景的设置 第一点 在操作ENSP个人建议要关闭防火墙 第二点 在操作ENSP软件是观察下面的软件是否全部关闭了 第三点 打开软件 ENSP软件注册信息 操作如下 注册前…

Oracle之Scott用户

Oracle增删改查&#xff0c;事务与序列 前言 1、解锁scott用户 2、雇员表&#xff08;emp&#xff09; 3、部门表&#xff08;dept&#xff09; 4、工资等级表&#xff08;salgrade&#xff09;了解 5、奖金表&#xff08;bonus&#xff09;了解 1、解锁scott用户 --解锁scot…

对卷积和全连接之间关系的学习(1*1卷积与全连接层可以互换吗?)

1.对于卷积和全连接 首先我们看一张图&#xff0c;它是一张关于卷积的操作&#xff1a; 然后在看关于全连接的操作&#xff1a; 从上面两张图中可以看出卷积的过程和全连接的过程&#xff0c;我们利用粉色的卷积核在image上进行卷积&#xff0c;进行内积计算得到输出值3&#…