Elasticsearch 4 — Aggregations & phân tích

13 thg 7, 2026 2 lượt xem
#analytics
#data-engineering
#elasticsearch
#aggregations
#kibana

Elasticsearch 4 — Aggregations & phân tích

Trong bài Query DSL chúng ta đã học cách tìm document liên quan và sắp xếp chúng theo điểm. Nhưng rất nhiều câu hỏi thực tế không phải "cho tôi các bản ghi khớp" mà là "cho tôi con số tổng hợp trên các bản ghi khớp": mỗi giờ có bao nhiêu giao dịch? Top 10 merchant theo tổng số tiền là ai? Trong tháng này có bao nhiêu khách hàng khác nhau phát sinh giao dịch? Đây là địa hạt của aggregation — tính năng biến Elasticsearch (ES) từ một cỗ máy tìm kiếm thành một công cụ phân tích thời gian gần thực.

Nếu bạn quen SQL, hãy nghĩ aggregation là GROUP BY + các hàm tổng hợp (COUNT, SUM, AVG...), nhưng chạy trên nền tảng tìm kiếm phân tán, có thể gộp trên hàng trăm triệu document trong vài chục mili-giây, và lồng nhau linh hoạt hơn nhiều so với GROUP BY phẳng của SQL. Đây cũng chính là bộ máy đứng sau mọi biểu đồ bạn thấy trong Kibana.

Lưu ý xuyên suốt: aggregation là JSON gửi tới ES, không phải SQL. Các block dưới đây KHÔNG chạy trên sandbox PostgreSQL — đừng nhầm. Khi bài có nhắc SQL để so sánh, đó chỉ là minh họa khái niệm.

Ba loại aggregation

ES chia aggregation thành ba nhóm, và hiểu rõ ranh giới giữa chúng là chìa khóa để đọc mọi truy vấn phân tích:

LoạiTrả về gìVí dụ
Metricmột hoặc vài con số trên tập documentavg, sum, min, max, stats, cardinality
Bucketchia document thành các nhóm (bucket)terms, date_histogram, histogram, range, filters
Pipelinetính trên kết quả của agg khácderivative, cumulative_sum, moving_avg

Trực giác: bucket giống mệnh đề GROUP BY (chia dữ liệu thành nhóm), metric giống các hàm tổng hợp áp lên mỗi nhóm, còn pipeline là lớp tính toán hậu kỳ chạy trên chuỗi kết quả đã tổng hợp (kiểu tính chênh lệch, cộng dồn, trung bình trượt).

Cấu trúc chung của một request là đặt khối aggs (hoặc viết đầy đủ aggregations) song song với query. Phần query lọc tập document trước, aggregation chỉ tổng hợp trên những document đã khớp. Đặt "size": 0 để không trả về document nào, chỉ lấy kết quả tổng hợp — tiết kiệm băng thông đáng kể khi bạn chỉ cần con số:

GET /transactions/_search
{
  "size": 0,
  "query": { "range": { "created_at": { "gte": "2026-06-01", "lt": "2026-07-01" } } },
  "aggs": {
    "tong_tien": { "sum": { "field": "amount" } }
  }
}

Truy vấn này lọc giao dịch trong tháng 6/2026 rồi tính tổng amount, không trả về document nào. Mỗi aggregation có một tên do bạn đặt (tong_tien) — tên này xuất hiện lại trong kết quả để bạn tra cứu.

Metric aggregation

Metric agg gom nhiều document thành vài con số. Các loại thông dụng:

  • avg / min / max / sum: trung bình / nhỏ nhất / lớn nhất / tổng của một trường số.
  • stats: gói count, min, max, avg, sum trong một lần quét — gọn khi cần cả cụm.
  • extended_stats: thêm phương sai, độ lệch chuẩn.
  • percentiles: các phân vị (p50, p95, p99) — cực hữu ích khi phân tích độ trễ log hay phân bố số tiền.
  • value_count: đếm số giá trị của một trường.
  • cardinality: đếm số giá trị khác nhau (distinct count).
{
  "size": 0,
  "aggs": {
    "thong_ke_so_tien": { "stats": { "field": "amount" } },
    "phan_vi_so_tien":  { "percentiles": { "field": "amount", "percents": [50, 95, 99] } }
  }
}

cardinality — đếm distinct bằng HyperLogLog

cardinality đáng bàn riêng vì nó không đếm chính xác trên khối dữ liệu lớn. Đếm chính xác số giá trị khác nhau đòi hỏi giữ toàn bộ giá trị đã thấy trong bộ nhớ — với hàng trăm triệu khách hàng, điều đó tốn RAM khổng lồ và không mở rộng được trên nhiều shard.

Thay vào đó ES dùng thuật toán HyperLogLog++ (HLL) — một thuật toán xấp xỉ (approximate) ước lượng số phần tử distinct bằng bộ nhớ cố định rất nhỏ. Đánh đổi là kết quả có sai số nhỏ (thường dưới vài phần trăm, cấu hình qua tham số precision_threshold — càng cao càng chính xác nhưng tốn RAM hơn; mặc định 3000, tối đa 40000). Với dưới ngưỡng precision_threshold giá trị, kết quả gần như chính xác tuyệt đối.

{
  "size": 0,
  "aggs": {
    "so_khach_distinct": {
      "cardinality": {
        "field": "customer_id",
        "precision_threshold": 3000
      }
    }
  }
}

Nguyên tắc thực chiến: dùng cardinality cho các bảng số liệu kiểu "số khách hoạt động", "số IP truy cập", "số thẻ giao dịch" — nơi sai số vài phần trăm chấp nhận được. Không dùng nó khi con số phải khớp tuyệt đối với sổ sách kế toán (ví dụ đối soát số lượng giao dịch quyết toán) — lúc đó cần value_count hoặc truy vấn chính xác trên hệ thống nguồn.

Bucket aggregation

Bucket agg chia document thành các nhóm. Mỗi bucket kèm doc_count (số document rơi vào nhóm) và có thể lồng thêm agg con bên trong.

terms — nhóm theo giá trị (như GROUP BY)

terms gom document theo giá trị một trường và trả về top N nhóm theo số lượng (hoặc theo một metric). Đây là agg hay dùng nhất: top merchant, phân bố theo loại giao dịch, top chi nhánh...

{
  "size": 0,
  "aggs": {
    "top_merchant": {
      "terms": { "field": "merchant.keyword", "size": 10, "order": { "_count": "desc" } }
    }
  }
}

date_histogram — nhóm theo mốc thời gian

date_histogram chia trục thời gian thành các khoảng đều (calendar_interval như 1h, 1d, 1M hoặc fixed_interval). Đây là nền tảng của mọi biểu đồ chuỗi thời gian — "số giao dịch theo giờ", "doanh số theo ngày".

{
  "size": 0,
  "aggs": {
    "theo_gio": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "1h",
        "time_zone": "Asia/Ho_Chi_Minh",
        "min_doc_count": 0
      }
    }
  }
}

Lưu ý time_zone: mốc thời gian trong ES lưu ở UTC, muốn "theo giờ" đúng múi giờ Việt Nam phải khai báo Asia/Ho_Chi_Minh, nếu không biểu đồ sẽ lệch 7 tiếng. min_doc_count: 0 giữ cả các bucket rỗng (giờ không có giao dịch) để đường biểu đồ liền mạch.

histogram, range, filters

  • histogram: giống date_histogram nhưng cho trường số — chia theo interval (ví dụ phân bố số tiền theo khoảng 1 triệu).
  • range: chia theo các khoảng do bạn tự định nghĩa (ví dụ: dưới 1 triệu, 1–10 triệu, trên 10 triệu). date_range là phiên bản cho ngày.
  • filters: mỗi bucket là một truy vấn tùy ý — linh hoạt nhất, dùng khi các nhóm không đều nhau (ví dụ: "giao dịch nội địa" vs "giao dịch quốc tế" định nghĩa bằng hai filter khác nhau).
{
  "aggs": {
    "khoang_so_tien": {
      "range": {
        "field": "amount",
        "ranges": [
          { "to": 1000000 },
          { "from": 1000000, "to": 10000000 },
          { "from": 10000000 }
        ]
      }
    }
  }
}

Pipeline aggregation

Pipeline agg không quét document — nó chạy trên kết quả của các agg khác (thường là các bucket của một date_histogram). Đây là điểm ES vượt SQL GROUP BY phẳng: bạn có thể tính chuỗi phái sinh ngay trong một request.

  • derivative: chênh lệch giữa bucket này và bucket trước — tốc độ thay đổi (ví dụ: mỗi ngày doanh số tăng/giảm bao nhiêu so với ngày trước).
  • cumulative_sum: cộng dồn qua các bucket — đường doanh số lũy kế từ đầu tháng.
  • moving_avg (hoặc moving_fn ở bản mới): trung bình trượt — làm mượt đường chuỗi thời gian, lọc nhiễu để thấy xu hướng.

Pipeline agg tham chiếu tới agg khác qua tham số buckets_path:

{
  "size": 0,
  "aggs": {
    "theo_ngay": {
      "date_histogram": { "field": "created_at", "calendar_interval": "1d" },
      "aggs": {
        "doanh_so": { "sum": { "field": "amount" } },
        "luy_ke": { "cumulative_sum": { "buckets_path": "doanh_so" } },
        "chenh_lech": { "derivative": { "buckets_path": "doanh_so" } }
      }
    }
  }
}

Ở đây date_histogram tạo bucket theo ngày, mỗi bucket có metric doanh_so; rồi hai pipeline agg (luy_ke, chenh_lech) đọc doanh_so của chuỗi bucket để tính lũy kế và chênh lệch ngày-qua-ngày.

Lồng aggregation — sức mạnh thực sự

Điểm khiến aggregation vượt xa GROUP BY là khả năng lồng nhiều tầng: bên trong mỗi bucket, bạn lại đặt metric agg và/hoặc bucket agg khác. ES sẽ tính đệ quy: chia bucket cấp ngoài, rồi với từng bucket lại chia/tổng hợp tiếp.

Ví dụ kinh điển trong phân tích giao dịch: chia theo giờ (date_histogram), rồi trong mỗi giờ tính trung bình số tiền và liệt kê top 5 merchant:

GET /transactions/_search
{
  "size": 0,
  "query": { "range": { "created_at": { "gte": "2026-06-30T00:00:00", "lt": "2026-07-01T00:00:00" } } },
  "aggs": {
    "theo_gio": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "1h",
        "time_zone": "Asia/Ho_Chi_Minh"
      },
      "aggs": {
        "so_tien_tb":   { "avg": { "field": "amount" } },
        "khach_distinct": { "cardinality": { "field": "customer_id" } },
        "top_merchant": {
          "terms": { "field": "merchant.keyword", "size": 5, "order": { "tong_tien": "desc" } },
          "aggs": {
            "tong_tien": { "sum": { "field": "amount" } }
          }
        }
      }
    }
  }
}

Đọc từ ngoài vào: bucket theo giờ → trong mỗi giờ, tính avg(amount), đếm khách distinct, và lấy top 5 merchant sắp theo tổng tiền (chú ý: top_merchant lại lồng thêm metric tong_tien để sắp xếp theo nó, chứ không theo số lượng mặc định). Sơ đồ cây aggregation lồng nhau của truy vấn trên:

Kết quả trả về là cây JSON lồng tương ứng: mảng theo_gio.buckets, mỗi phần tử có key_as_string (mốc giờ), doc_count, các giá trị metric và mảng top_merchant.buckets bên trong. Front-end hoặc Kibana chỉ việc duyệt cây này để vẽ.

doc_values — vì sao aggregation nhanh, và vì sao dùng keyword

Aggregation gần như luôn cần đọc giá trị của một trường trên nhiều document (để cộng, để nhóm, để đếm distinct). Inverted index — cấu trúc phục vụ tìm kiếm — được tổ chức theo hướng "từ token → danh sách document", tối ưu cho câu hỏi "document nào chứa token này". Nó không phù hợp cho câu hỏi ngược "document này có giá trị gì ở trường X", vốn là cái aggregation cần.

Để giải quyết, ES lưu thêm doc_values: một cấu trúc theo cột (columnar) trên đĩa, ánh xạ document → giá trị. Nhờ đó agg đọc tuần tự cả cột giá trị rất nhanh, tận dụng nén và cache OS. doc_values bật mặc định cho hầu hết trường (số, ngày, keyword, boolean) và là lý do aggregation có thể quét hàng trăm triệu document trong tích tắc.

Điểm mấu chốt với trường chữ:

  • Trường kiểu text (phân tích full-text) KHÔNG có doc_values mặc định. Muốn agg trên nó phải bật fielddata — cấu trúc nạp toàn bộ term vào heap JVM, cực tốn RAM và dễ làm sập cluster. Vì vậy ES chặn agg trên text mặc định.
  • Trường kiểu keyword (không phân tích, lưu nguyên văn) doc_values — đây là trường bạn nên dùng cho terms, cardinality, sort.

Do đó quy tắc: luôn aggregation trên trường keyword (hoặc số/ngày), không trên text. Với trường vừa full-text vừa cần thống kê, mapping chuẩn để nó là text kèm sub-field .keyword (multi-field) — dùng match trên merchant để tìm, dùng terms trên merchant.keyword để tổng hợp. Đây chính là lý do trong mọi ví dụ trên ta viết merchant.keyword chứ không phải merchant.

Độ chính xác của terms — chuyện của phân tán

terms agg tiềm ẩn một sai số không hiển nhiên do kiến trúc phân tán. Index của bạn chia thành nhiều shard. Khi bạn xin "top 10 merchant", node điều phối không gom toàn bộ giá trị của mọi shard rồi mới xếp hạng (quá tốn mạng). Thay vào đó, nó hỏi mỗi shard trả về top của riêng nó, rồi gộp lại.

Vấn đề: một merchant có thể không lọt top của bất kỳ shard nào nhưng khi cộng dồn qua tất cả shard lại phải nằm trong top toàn cục — và nó bị bỏ sót, khiến doc_count của các nhóm khác cũng có thể sai lệch. ES báo mức sai số này qua hai trường trong kết quả: doc_count_error_upper_bound (cận trên của sai số) và sum_other_doc_count (số document rơi ngoài các bucket trả về).

Giải pháp là tham số shard_size: số nhóm mỗi shard trả về (mặc định size * 1.5 + 10). Tăng shard_size lên cao hơn size (ví dụ size: 10, shard_size: 100) khiến mỗi shard trả nhiều ứng viên hơn, giảm mạnh khả năng bỏ sót — đổi lại tốn thêm chút bộ nhớ và mạng. Với các báo cáo cần độ chính xác cao trên index nhiều shard, luôn cân nhắc nâng shard_size.

{
  "aggs": {
    "top_merchant": {
      "terms": { "field": "merchant.keyword", "size": 10, "shard_size": 100 }
    }
  }
}

Kibana — dashboard dựng trên aggregation

Bạn hiếm khi phải viết JSON aggregation bằng tay cho dashboard. Kibana — công cụ trực quan hóa trong bộ Elastic Stack — cung cấp giao diện kéo-thả để dựng biểu đồ, và mỗi biểu đồ thực chất biên dịch thành một request aggregation gửi tới ES:

  • Trục X thời gian của biểu đồ đường → một date_histogram.
  • "Chia theo" (split series/slice) của biểu đồ cột/tròn → một terms.
  • Trục Y (giá trị) → một metric agg (sum, avg, cardinality...).
  • "Unique count" trong Kibana → cardinality (kèm sai số HLL đã nói).

Khi bạn hiểu ba loại aggregation, mọi bảng điều khiển Kibana trở nên minh bạch: một dashboard giám sát giao dịch chỉ là tập các bucket + metric lồng nhau, chạy lại mỗi khi bạn đổi khoảng thời gian. Đây là mối liên hệ tự nhiên nhất giữa lý thuyết aggregation ở bài này và công cụ vận hành hằng ngày.

Use case thực tế

Bối cảnh: Đội giám sát vận hành NCB muốn một dashboard realtime cho index transactions (~180 triệu document, merchanttext + .keyword, customer_idkeyword, amount số, created_at date). Yêu cầu ba câu hỏi trên dữ liệu 24 giờ gần nhất:

  1. Số giao dịch theo từng giờ (phát hiện giờ cao điểm / bất thường).
  2. Top 5 merchant theo tổng số tiền trong mỗi giờ.
  3. Số khách hàng khác nhau giao dịch (đo mức độ hoạt động, không cần chính xác tuyệt đối).

Chỉ cần một request duy nhất — chính là truy vấn lồng ở mục trên: date_histogram 1 giờ (múi giờ VN) ở tầng ngoài, trong mỗi bucket đặt avg(amount), cardinality(customer_id)terms(merchant.keyword) lồng sum(amount) để sắp top merchant. Đặt "size": 0 nên ES không trả document nào, chỉ trả cây tổng hợp.

Kết quả đo được (minh họa): Nhờ query range lọc trước còn ~7,5 triệu document trong 24 giờ, và các trường được tổng hợp (amount, customer_id, merchant.keyword) đều có doc_values nên ES quét cột rất nhanh — cả request trả về trong khoảng vài trăm mili-giây. Con số khách distinct dùng HLL, sai số dưới ~1% với precision_threshold mặc định — hoàn toàn đủ cho mục đích giám sát. Để top merchant không bị bỏ sót trên index nhiều shard, đội đặt shard_size: 100. Toàn bộ dashboard trong Kibana chính là request này được dựng bằng kéo-thả và tự chạy lại mỗi 30 giây.

Về mặt khái niệm, aggregation này tương đương một loạt GROUP BY lồng nhau trong SQL, nhưng ES cho phép gói tất cả vào một truy vấn phân tán, tính cả top-N theo metric và distinct xấp xỉ ngay trong mỗi bucket thời gian — điều mà GROUP BY phẳng khó biểu diễn gọn. Cách phân tích theo cửa sổ thời gian này cũng bổ trợ cho các mẫu giám sát giao dịch mà ta bàn ở bài giám sát giao dịch AML.

Ghi nhớ

  • Aggregation biến ES thành công cụ phân tích — giống GROUP BY + hàm tổng hợp nhưng phân tán, lồng nhiều tầng và chạy trên khối log/giao dịch lớn. Đây là bộ máy sau mọi biểu đồ Kibana.
  • Ba loại: metric (avg/min/max/sum/stats/cardinality — trả con số), bucket (terms/date_histogram/histogram/range/filters — chia nhóm), pipeline (derivative/cumulative_sum/moving_avg — tính trên kết quả agg khác qua buckets_path).
  • cardinality đếm distinct bằng HyperLogLog — xấp xỉ, sai số nhỏ đổi lấy RAM cố định; chỉnh qua precision_threshold. Đừng dùng khi cần con số khớp sổ sách tuyệt đối.
  • Lồng aggregation: đặt agg con trong bucket cha (ví dụ date_histogram → mỗi giờ có avg, cardinality, và terms top merchant lồng sum để sắp xếp).
  • doc_values (lưu theo cột) là thứ khiến aggregation nhanh; bật mặc định cho số/ngày/keyword. Trường text không có doc_valuesluôn agg trên keyword, không trên text (dùng sub-field .keyword).
  • Độ chính xác terms: do phân tán, top có thể sai/bỏ sót — xem doc_count_error_upper_bound. Tăng shard_size để giảm sai số, đổi lấy chút chi phí.
  • Kibana dựng dashboard trên aggregation: trục thời gian → date_histogram, split → terms, trục giá trị → metric. Dùng "size": 0 khi chỉ cần con số.

Bài viết liên quan

CSV/JSON/Parquet/Avro, lưu trữ theo hàng vs cột, nén, và OLTP vs OLAP.

13 thg 7, 2026 8

Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.

13 thg 7, 2026 5

Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.

13 thg 7, 2026 5

Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.

13 thg 7, 2026 5