Elasticsearch 的基数统计在大数据量下有什么办法能做到 100% 准确度吗?

共 7967字,需浏览 16分钟

 ·

2024-06-19 07:30




球友提问:Elasticsearch 的基数统计在大数据量下有什么办法能做到 100% 准确度吗?



https://t.zsxq.com/VYDcW


在Elasticsearch中,基数统计(如基数聚合)在大数据量下通常使用 HyperLogLog++算法,该算法是近似算法,因此会有一定误差。


1、构造 100万条数据


我这边随机构造了 100万条记录写入 Elasticsearch 以便测试。


先说一下构造代码的逻辑:


随机生成代码生成大量随机中文数据,并将其批量导入到Elasticsearch索引中。通过循环创建包含随机中文词汇和随机整数的文档,每批生成2000个文档就使用Elasticsearch的 bulk API进行批量导入,以提高导入效率,直到所有指定数量的文档全部导入完成。


导入 Elasticsearch 后的结果如下图所示。



数据样例如下图所示。



为了方便真实统计结果,我这边又借助 scroll 将 写入 Elasticsearch 的文本导出到 out_title.txt 文件。



最终用如下脚本去重后的结果为:632483 条。



Elasticsearch 如果需要100%的准确度,可以考虑以下几种解决方案。


先做验证,最后说结论。


1. 方案1:使用相对“精准”的cardinality基数聚合


构造索引 test_index_0618 的映射结构如下所示:


{
  "test_index_0618": {
    "mappings": {
      "properties": {
        "id": {
          "type""integer"
        },
        "title": {
          "type""text",
          "fields": {
            "keyword": {
              "type""keyword"
            }
          },
          "analyzer""ik_max_word"
        }
      }
    }
  }
}

Elasticsearch从7.10版本开始引入了 cardinality 聚合的 precision_threshold 参数,当设置为较高的值时,可以提供更准确的基数统计。


https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html


配置方法:


POST test_index_0618/_search
{
  "aggs": {
    "unique_count": {
      "cardinality": {
        "field""title.keyword",
        "precision_threshold": 40000
      }
    }
  }
}

precision_threshold 选项在Elasticsearchcardinality聚合中,用于在内存消耗和计数准确性之间进行平衡。 


设置该值可以控制在多少唯一值以下时计数结果非常准确,而超过该值时计数结果可能会稍有误差。


最大支持的值为40000,超过该值将没有额外效果,默认情况下,这个阈值设为3000



但对比真实去重结果:632483 条,会有接近 633011-632483=多出528大小的偏差。


2. 方案2:使用terms聚合结合 cardinality基数统计


如下查询通过terms聚合获取title.keyword字段的前10000个唯一值,并使用cardinality聚合计算该字段的唯一值总数。


实操方法:


{
  "size": 0,
  "aggs": {
    "unique_values": {
      "terms": {
        "field""title.keyword",
        "size": 10000
      }
    },
    "unique_count": {
      "cardinality": {
        "field""title.keyword"
      }
    }
  }
}

在terms 聚合中设置足够大的size,以覆盖所有可能的唯一值。



结果值依然不是精准值,会有 632483-631915= 568 大小的偏差。


但是分桶值足够大也不能非常大,否则会报错,因为缺省值是 65536。侧面印证,如果聚合结果值查过65536 会不精确。


{
  "error": {
    "root_cause": [],
    "type""search_phase_execution_exception",
    "reason""",
    "phase""fetch",
    "grouped"true,
    "failed_shards": [],
    "caused_by": {
      "type""too_many_buckets_exception",
      "reason""Trying to create too many buckets. Must be less than or equal to: [65536] but this number of buckets was exceeded. This limit can be set by changing the [search.max_buckets] cluster level setting.",
      "max_buckets": 65536
    }
  },
  "status": 400
}

获取 search.max_buckets 值:


GET /_cluster/settings?include_defaults=true&filter_path=defaults.search.max_buckets

我们把 search.max_buckets 调整到和数据量一致:


PUT /_cluster/settings
{
  "persistent": {
    "search.max_buckets": 1000000
  }
}

执行会报错:



猜测就是数据量太大,处理不过来!


我把分桶大小改成 700000 后,可以执行,但结果依然不是精准值。


POST test_index_0618/_search
{
  "size": 0,
  "aggs": {
    "unique_values": {
      "terms": {
        "field""title.keyword",
        "size": 700000
      }
    },
    "unique_count": {
      "cardinality": {
        "field""title.keyword"
      }
    }
  }
}


结果比真实结果值依然是有出入,多了 635954- 632483=3471。


3. 方案3:分区统计和汇总


如果数据量非常大,可以考虑将数据分片(按时间、地理位置等字段分区),在各个分区内分别进行基数统计,然后汇总各个分区的结果。


步骤1:将数据按某个字段进行分区(如时间)。


步骤2:对每个分区分别进行基数统计。


步骤3:汇总所有分区的基数统计结果。


这其实是借助分而治之的算法思想来求解。


但,由于咱们的构造数据字段受限,该方案我没有求证。


4. 方案4:借助外部工具如 redis 实现


该方案是将 Elasticsearch 数据同步迁移到 redis,借助 redis 实现的聚合统计。






































def export_to_redis(es, redis_client, index_name):    try:        # 清空Redis Set        redis_client.delete("unique_values")
# Scroll API 获取所有数据 scroll_size = 1000 data = es.search(index=index_name, body={"query": {"match_all": {}}}, scroll='2m', size=scroll_size) scroll_id = data['_scroll_id'] total_docs = data['hits']['total']['value']
print(f"Total documents to process: {total_docs}")
while scroll_size > 0: for doc in data['hits']['hits']: field_value = doc['_source']['title'] redis_client.sadd("unique_values", field_value)
data = es.scroll(scroll_id=scroll_id, scroll='2m') scroll_id = data['_scroll_id'] scroll_size = len(data['hits']['hits'])
unique_count = redis_client.scard("unique_values") print(f"Unique values count: {unique_count}")
# 清理scroll上下文 es.clear_scroll(scroll_id=scroll_id)
except redis.RedisError as e: print(f"Redis error: {e}") except Exception as e: print(f"Unexpected error: {e}")










借助 redis 实现,写入后统计实现如下:


unique_count = redis_client.scard("unique_values")

上述代码作用是获取Redis集合unique_values中的唯一元素数量。它利用了Redis集合的去重特性,通过scard方法返回集合中元素的总数。去重后结果如下:



借助 redis 客户端查看结果也和统计结果一致。



5. 小结


为了在大数据量下实现100%准确的基数统计,可以结合以下思路和方法:


提高precision_threshold参数。使用terms聚合结合bucket selector。分区统计和汇总。借助外部大数据处理工具(如 redis)进行统计。


这些方法各有优缺点,具体选择可以根据实际的业务需求、数据规模和系统性能来决定。


实操验证发现基于 Elasticsearch 统计几乎没法实现精准去重结果。


在实际应用中,可能需要综合运用多种方法,以达到既满足性能要求又保证统计准确度的目的。






2024星球专享:Elastic 8.1 认证全部知识点 脑图 + 视频


https://articles.zsxq.com/id_njwt7kus4r42.html 


新时代写作与互动:《一本书讲透 Elasticsearch》读者群的创新之路







短时间快习得多干货!


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


elastic6.cn——ElasticStack进阶助手








比同事抢先一步学习进阶干货




浏览 100
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报