Elasticsearch 的基數(shù)統(tǒng)計(jì)在大數(shù)據(jù)量下有什么辦法能做到 100% 準(zhǔn)確度嗎?
共 7967字,需瀏覽 16分鐘
·
2024-06-19 07:30
球友提問:Elasticsearch 的基數(shù)統(tǒng)計(jì)在大數(shù)據(jù)量下有什么辦法能做到 100% 準(zhǔn)確度嗎?
https://t.zsxq.com/VYDcW
在Elasticsearch中,基數(shù)統(tǒng)計(jì)(如基數(shù)聚合)在大數(shù)據(jù)量下通常使用 HyperLogLog++算法,該算法是近似算法,因此會(huì)有一定誤差。
1、構(gòu)造 100萬條數(shù)據(jù)
我這邊隨機(jī)構(gòu)造了 100萬條記錄寫入 Elasticsearch 以便測(cè)試。
先說一下構(gòu)造代碼的邏輯:
隨機(jī)生成代碼生成大量隨機(jī)中文數(shù)據(jù),并將其批量導(dǎo)入到Elasticsearch索引中。通過循環(huán)創(chuàng)建包含隨機(jī)中文詞匯和隨機(jī)整數(shù)的文檔,每批生成2000個(gè)文檔就使用Elasticsearch的 bulk API進(jìn)行批量導(dǎo)入,以提高導(dǎo)入效率,直到所有指定數(shù)量的文檔全部導(dǎo)入完成。
導(dǎo)入 Elasticsearch 后的結(jié)果如下圖所示。
數(shù)據(jù)樣例如下圖所示。
為了方便真實(shí)統(tǒng)計(jì)結(jié)果,我這邊又借助 scroll 將 寫入 Elasticsearch 的文本導(dǎo)出到 out_title.txt 文件。
最終用如下腳本去重后的結(jié)果為:632483 條。
Elasticsearch 如果需要100%的準(zhǔn)確度,可以考慮以下幾種解決方案。
先做驗(yàn)證,最后說結(jié)論。
1. 方案1:使用相對(duì)“精準(zhǔn)”的cardinality基數(shù)聚合
構(gòu)造索引 test_index_0618 的映射結(jié)構(gòu)如下所示:
{
"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 參數(shù),當(dāng)設(shè)置為較高的值時(shí),可以提供更準(zhǔn)確的基數(shù)統(tǒng)計(jì)。
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 選項(xiàng)在Elasticsearch的cardinality聚合中,用于在內(nèi)存消耗和計(jì)數(shù)準(zhǔn)確性之間進(jìn)行平衡。
設(shè)置該值可以控制在多少唯一值以下時(shí)計(jì)數(shù)結(jié)果非常準(zhǔn)確,而超過該值時(shí)計(jì)數(shù)結(jié)果可能會(huì)稍有誤差。
最大支持的值為40000,超過該值將沒有額外效果,默認(rèn)情況下,這個(gè)閾值設(shè)為3000。
但對(duì)比真實(shí)去重結(jié)果:632483 條,會(huì)有接近 633011-632483=多出528大小的偏差。
2. 方案2:使用terms聚合結(jié)合 cardinality基數(shù)統(tǒng)計(jì)
如下查詢通過terms聚合獲取title.keyword字段的前10000個(gè)唯一值,并使用cardinality聚合計(jì)算該字段的唯一值總數(shù)。
實(shí)操方法:
{
"size": 0,
"aggs": {
"unique_values": {
"terms": {
"field": "title.keyword",
"size": 10000
}
},
"unique_count": {
"cardinality": {
"field": "title.keyword"
}
}
}
}
在terms 聚合中設(shè)置足夠大的size,以覆蓋所有可能的唯一值。
結(jié)果值依然不是精準(zhǔn)值,會(huì)有 632483-631915= 568 大小的偏差。
但是分桶值足夠大也不能非常大,否則會(huì)報(bào)錯(cuò),因?yàn)槿笔≈凳?65536。側(cè)面印證,如果聚合結(jié)果值查過65536 會(huì)不精確。
{
"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 調(diào)整到和數(shù)據(jù)量一致:
PUT /_cluster/settings
{
"persistent": {
"search.max_buckets": 1000000
}
}
執(zhí)行會(huì)報(bào)錯(cuò):
猜測(cè)就是數(shù)據(jù)量太大,處理不過來!
我把分桶大小改成 700000 后,可以執(zhí)行,但結(jié)果依然不是精準(zhǔn)值。
POST test_index_0618/_search
{
"size": 0,
"aggs": {
"unique_values": {
"terms": {
"field": "title.keyword",
"size": 700000
}
},
"unique_count": {
"cardinality": {
"field": "title.keyword"
}
}
}
}
結(jié)果比真實(shí)結(jié)果值依然是有出入,多了 635954- 632483=3471。
3. 方案3:分區(qū)統(tǒng)計(jì)和匯總
如果數(shù)據(jù)量非常大,可以考慮將數(shù)據(jù)分片(按時(shí)間、地理位置等字段分區(qū)),在各個(gè)分區(qū)內(nèi)分別進(jìn)行基數(shù)統(tǒng)計(jì),然后匯總各個(gè)分區(qū)的結(jié)果。
步驟1:將數(shù)據(jù)按某個(gè)字段進(jìn)行分區(qū)(如時(shí)間)。
步驟2:對(duì)每個(gè)分區(qū)分別進(jìn)行基數(shù)統(tǒng)計(jì)。
步驟3:匯總所有分區(qū)的基數(shù)統(tǒng)計(jì)結(jié)果。
這其實(shí)是借助分而治之的算法思想來求解。
但,由于咱們的構(gòu)造數(shù)據(jù)字段受限,該方案我沒有求證。
4. 方案4:借助外部工具如 redis 實(shí)現(xiàn)
該方案是將 Elasticsearch 數(shù)據(jù)同步遷移到 redis,借助 redis 實(shí)現(xiàn)的聚合統(tǒng)計(jì)。
def export_to_redis(es, redis_client, index_name):try:# 清空Redis Setredis_client.delete("unique_values")# Scroll API 獲取所有數(shù)據(jù)scroll_size = 1000data = 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 實(shí)現(xiàn),寫入后統(tǒng)計(jì)實(shí)現(xiàn)如下:
unique_count = redis_client.scard("unique_values")
上述代碼作用是獲取Redis集合unique_values中的唯一元素?cái)?shù)量。它利用了Redis集合的去重特性,通過scard方法返回集合中元素的總數(shù)。去重后結(jié)果如下:
借助 redis 客戶端查看結(jié)果也和統(tǒng)計(jì)結(jié)果一致。
5. 小結(jié)
為了在大數(shù)據(jù)量下實(shí)現(xiàn)100%準(zhǔn)確的基數(shù)統(tǒng)計(jì),可以結(jié)合以下思路和方法:
提高precision_threshold參數(shù)。使用terms聚合結(jié)合bucket selector。分區(qū)統(tǒng)計(jì)和匯總。借助外部大數(shù)據(jù)處理工具(如 redis)進(jìn)行統(tǒng)計(jì)。
這些方法各有優(yōu)缺點(diǎn),具體選擇可以根據(jù)實(shí)際的業(yè)務(wù)需求、數(shù)據(jù)規(guī)模和系統(tǒng)性能來決定。
實(shí)操驗(yàn)證發(fā)現(xiàn)基于 Elasticsearch 統(tǒng)計(jì)幾乎沒法實(shí)現(xiàn)精準(zhǔn)去重結(jié)果。
在實(shí)際應(yīng)用中,可能需要綜合運(yùn)用多種方法,以達(dá)到既滿足性能要求又保證統(tǒng)計(jì)準(zhǔn)確度的目的。
2024星球?qū)O恚?/span>Elastic 8.1 認(rèn)證全部知識(shí)點(diǎn) 腦圖 + 視頻
https://articles.zsxq.com/id_njwt7kus4r42.html
新時(shí)代寫作與互動(dòng):《一本書講透 Elasticsearch》讀者群的創(chuàng)新之路
更短時(shí)間更快習(xí)得更多干貨!
和全球超2000+ Elastic 愛好者一起精進(jìn)!
elastic6.cn——ElasticStack進(jìn)階助手
