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

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 productPOST /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.hqwc.cn/news/15340.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

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

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

在tplink路由器xdr6088中运行Docker潘多拉(pengzhile/pandora)遇到无法访问的问题

在xdr6088中搜索安装pengzhile/pandora一切正常,但是按照常规运行docker后,直接访问8899端口无法打开页面,进入终端 运行如下命令 /usr/local/bin/python /usr/local/bin/pandora-cloud -s 0.0.0.0:8899 即可成功运行,然后客户端…

面试交流会

面试交流 目录: 01:关于人生目标 02:关于心态 03:关于选择 04:关于学习 05:关于简历 06:关于找工作 1:关于人生目标 1.01、自己想成为什么样的人? 1.02、自己的人生目标是…

如何用https协议支持小程序

步骤一:下载SSL证书 登录数字证书管理服务控制台。在左侧导航栏,单击SSL 证书。在SSL证书页面,定位到目标证书,在操作列,单击下载。 在服务器类型为Nginx的操作列,单击下载。 解压缩已下载的SSL证书压缩…

kubernetes环境搭建及部署

一、kubernetes 概述 1、kubernetes 基本介绍 kubernetes,简称 K8s,是用 8 代替 8 个字符“ubernete”而成的缩写。是一个开源 的,用于管理云平台中多个主机上的容器化的应用,Kubernetes 的目标是让部署容器化的 应用简单并且高效…

flutter tabBar 的属性及自定义实现

flutter tabBar 的属性及自定义实现 前言一、TabBar是什么?二、TabBar 自定义三、 Tab 自定义总结 前言 在Flutter中,TabBar的indicatorPadding属性用于设置指示器的内边距,而不是用于调整指示器和文字之间的间距。要调整TabBar中指示器和文字…

C#学习之路-常量

C# 常量 常量是固定值,程序执行期间不会改变。常量可以是任何基本数据类型,比如整数常量、浮点常量、字符常量或者字符串常量,还有枚举常量。 常量可以被当作常规的变量,只是它们的值在定义后不能被修改。 整数常量 整数常量可…

【密码学篇】GM密码行业标准下载方法

【密码学篇】GM密码行业标准下载方法 截止到2023年07月08日,密码行业标准化技术委员会共发布了144个密码行业标准,可点击链接预览或使用IDM下载器下载标准,此外该方法很多场景都适用,自行尝试—【蘇小沐】 文章目录 【密码学篇】…

【Linux】文件描述符 (上篇)

文章目录 📖 前言1. 文件的预备知识2. 复习C语言的文件操作3. Linux系统级文件接口3.1 open、 close、 read、 write 接口:3.2 内核当中实现的映射关系:3.3 如何理解Linux下一切皆文件: 📖 前言 本章开始,…

LVS负载均衡集群 keepalived

目录 1.实现方法 1.故障自动切换 (failover) 2.节点健康状态检查 (health checking) 2.实现LVS负载调度器 节点服务器的高可用(HA) 3.keepalived高可用故障切换原理 4.三个主要模块 5.案例 1.实现方法 1.故障自动切换 (failover) 主…

【Linux | Shell】Linux 安全系统 —— 用户、组、文件权限 - 阅读笔记

目录 一、Linux 的安全性1.1 /etc/passwd 文件1.2 /etc/shadow 文件1.3 添加新用户 —— useradd1.4 删除用户 —— userdel1.5 修改用户 —— usermod、passwd、chpasswd 二、使用 Linux 组2.1 /etc/group 文件2.2 创建新组 —— groupadd2.3 修改组 —— groupmod 三、理解文…

Java字符串类

string类的理解(以JDK8为例说明) 1.1的声明 public final class String implements java.io.Serializable&#xff0c; Comparable<String>&#xff0c; CharSequence final:String是不可被继承的 Serializable:可序列化的接口。凡是实现此接口的类的对象就可以通过…