← Kursa Dön
📄 Text · 30 min

Bucket Aggregations — terms, histogram, date_histogram

Giriş — Çekmeceli Dolap

Evdeki çekmeceli dolabı düşünün. Bir çekmecede çoraplar, birinde tişörtler, birinde pantolonlar. Her çekmece bir kova (bucket) ve içindeki kıyafetler o kovanın dokümanları. Sonra her çekmecedeki kıyafet sayısını sayabilir, ortalama fiyatını hesaplayabilir veya en pahalı parçayı bulabilirsiniz.

Elasticsearch'ün bucket aggregation'ları tam olarak bu mantıkla çalışır: dokümanları gruplara ayırır ve her grup üzerinde metrik hesaplamalar yapabilir. SQL'deki GROUP BY'ın Elasticsearch karşılığıdır — ama çok daha güçlü ve esnektir.


1. Bucket vs Metric Aggregation

ÖzellikMetric AggregationBucket Aggregation
ÇıktıTek sayısal değerDoküman grupları
AmaçHesaplama (avg, sum)Gruplama
SQL karşılığıAVG(), SUM()GROUP BY
Alt aggregationHayırEvet (iç içe)

Bucket aggregation'ların gerçek gücü iç içe (nested) aggregation desteğidir: önce grupla, sonra her grup içinde metrik hesapla.


2. terms Aggregation — Kategorik Gruplama

En yaygın bucket aggregation. Bir alanın benzersiz değerlerine göre gruplar:

GET sales/_search
{
  "size": 0,
  "aggs": {
    "kategoriler": {
      "terms": {
        "field": "category",
        "size": 10
      }
    }
  }
}

Yanıt:

{
  "aggregations": {
    "kategoriler": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        { "key": "Telefon", "doc_count": 3 },
        { "key": "Tablet", "doc_count": 2 },
        { "key": "Bilgisayar", "doc_count": 2 },
        { "key": "Kulaklık", "doc_count": 2 },
        { "key": "Akıllı Saat", "doc_count": 1 }
      ]
    }
  }
}

2.1 size ve shard_size

size döndürülecek bucket sayısını, shard_size her shard'dan toplanan bucket sayısını belirler:

GET sales/_search
{
  "size": 0,
  "aggs": {
    "top_markalar": {
      "terms": {
        "field": "brand",
        "size": 5,
        "shard_size": 25
      }
    }
  }
}

Doğruluk kuralı: shard_size >= size * 1.5 + 10 (varsayılan). Küçük shard_size yanlış sıralama ve sayımlara yol açabilir.

2.2 Sıralama

Varsayılan: doc_count azalan. Değiştirilebilir:

// Alfabetik sıralama
GET sales/_search
{
  "size": 0,
  "aggs": {
    "markalar_az": {
      "terms": {
        "field": "brand",
        "size": 10,
        "order": { "_key": "asc" }
      }
    }
  }
}

// Alt aggregation'a göre sıralama
GET sales/_search
{
  "size": 0,
  "aggs": {
    "markalar_ciroya_gore": {
      "terms": {
        "field": "brand",
        "size": 10,
        "order": { "toplam_ciro": "desc" }
      },
      "aggs": {
        "toplam_ciro": {
          "sum": { "field": "revenue" }
        }
      }
    }
  }
}

2.3 min_doc_count — Minimum Doküman Filtresi

GET sales/_search
{
  "size": 0,
  "aggs": {
    "populer_kategoriler": {
      "terms": {
        "field": "category",
        "min_doc_count": 2
      }
    }
  }
}

Sadece 2 veya daha fazla dokümanı olan bucket'lar döner.

2.4 include/exclude — Değer Filtresi

GET sales/_search
{
  "size": 0,
  "aggs": {
    "secili_markalar": {
      "terms": {
        "field": "brand",
        "include": ["Samsung", "Apple", "Xiaomi"],
        "exclude": ["Xiaomi"]
      }
    }
  }
}

// Regex ile
GET sales/_search
{
  "size": 0,
  "aggs": {
    "s_ile_baslayanlar": {
      "terms": {
        "field": "brand",
        "include": "S.*"
      }
    }
  }
}

2.5 İç İçe Aggregation (Sub-aggregation)

Her kategoride ortalama fiyat, toplam ciro ve benzersiz marka sayısı:

GET sales/_search
{
  "size": 0,
  "aggs": {
    "kategoriler": {
      "terms": {
        "field": "category",
        "size": 10
      },
      "aggs": {
        "ortalama_fiyat": {
          "avg": { "field": "price" }
        },
        "toplam_ciro": {
          "sum": { "field": "revenue" }
        },
        "benzersiz_marka": {
          "cardinality": { "field": "brand" }
        },
        "fiyat_stats": {
          "stats": { "field": "price" }
        }
      }
    }
  }
}

Yanıt:

{
  "buckets": [
    {
      "key": "Telefon",
      "doc_count": 3,
      "ortalama_fiyat": { "value": 49999.99 },
      "toplam_ciro": { "value": 264999.94 },
      "benzersiz_marka": { "value": 3 },
      "fiyat_stats": {
        "count": 3, "min": 29999.99, "max": 64999.99,
        "avg": 49999.99, "sum": 149999.97
      }
    },
    ...
  ]
}

2.6 Çok Seviyeli Gruplama

Kategori → Marka → Metrikler:

GET sales/_search
{
  "size": 0,
  "aggs": {
    "kategoriler": {
      "terms": { "field": "category" },
      "aggs": {
        "markalar": {
          "terms": { "field": "brand" },
          "aggs": {
            "ort_fiyat": { "avg": { "field": "price" } },
            "toplam_adet": { "sum": { "field": "quantity" } }
          }
        }
      }
    }
  }
}

SQL karşılığı: SELECT category, brand, AVG(price), SUM(quantity) FROM sales GROUP BY category, brand


3. histogram Aggregation — Sayısal Aralıklar

Sayısal değerleri eşit aralıklara böler:

GET sales/_search
{
  "size": 0,
  "aggs": {
    "fiyat_araliklari": {
      "histogram": {
        "field": "price",
        "interval": 10000
      }
    }
  }
}

Yanıt:

{
  "buckets": [
    { "key": 0.0, "doc_count": 2 },       // 0-9999
    { "key": 10000.0, "doc_count": 1 },    // 10000-19999
    { "key": 20000.0, "doc_count": 2 },    // 20000-29999
    { "key": 30000.0, "doc_count": 2 },    // 30000-39999
    { "key": 40000.0, "doc_count": 1 },    // 40000-49999
    { "key": 50000.0, "doc_count": 1 },    // 50000-59999
    { "key": 60000.0, "doc_count": 1 }     // 60000-69999
  ]
}

Boş Bucket'ları Dahil Etme

GET sales/_search
{
  "size": 0,
  "aggs": {
    "fiyat_dagilimi": {
      "histogram": {
        "field": "price",
        "interval": 10000,
        "min_doc_count": 0,
        "extended_bounds": {
          "min": 0,
          "max": 100000
        }
      }
    }
  }
}

min_doc_count: 0 boş bucket'ları da gösterir. extended_bounds aralığı genişletir.

Alt Aggregation ile

GET sales/_search
{
  "size": 0,
  "aggs": {
    "fiyat_segmentleri": {
      "histogram": {
        "field": "price",
        "interval": 20000
      },
      "aggs": {
        "toplam_satis": { "sum": { "field": "quantity" } },
        "ort_rating": { "avg": { "field": "rating" } }
      }
    }
  }
}

4. date_histogram Aggregation — Zaman Serisi

Tarih alanlarını belirli zaman aralıklarına göre gruplar. Dashboard'lardaki zaman serisi grafiklerinin temelidir:

4.1 Calendar Interval

GET sales/_search
{
  "size": 0,
  "aggs": {
    "aylik_satislar": {
      "date_histogram": {
        "field": "sale_date",
        "calendar_interval": "month"
      }
    }
  }
}

Yanıt:

{
  "buckets": [
    {
      "key_as_string": "2024-01-01T00:00:00.000Z",
      "key": 1704067200000,
      "doc_count": 2
    },
    {
      "key_as_string": "2024-02-01T00:00:00.000Z",
      "key": 1706745600000,
      "doc_count": 4
    },
    {
      "key_as_string": "2024-03-01T00:00:00.000Z",
      "key": 1709251200000,
      "doc_count": 4
    }
  ]
}

Calendar interval değerleri: minute, hour, day, week, month, quarter, year

4.2 Fixed Interval

Sabit süre aralıkları (takvim bağımsız):

GET sales/_search
{
  "size": 0,
  "aggs": {
    "haftalik": {
      "date_histogram": {
        "field": "sale_date",
        "fixed_interval": "7d"
      }
    }
  }
}

Fixed interval değerleri: 1000ms, 30s, 5m, 2h, 7d

4.3 Zaman Dilimi ve Format

GET sales/_search
{
  "size": 0,
  "aggs": {
    "gunluk_satislar": {
      "date_histogram": {
        "field": "sale_date",
        "calendar_interval": "day",
        "time_zone": "Europe/Istanbul",
        "format": "yyyy-MM-dd",
        "min_doc_count": 0,
        "extended_bounds": {
          "min": "2024-01-01",
          "max": "2024-03-31"
        }
      }
    }
  }
}

4.4 Zaman Serisi Dashboard Verisi

GET sales/_search
{
  "size": 0,
  "aggs": {
    "aylik_rapor": {
      "date_histogram": {
        "field": "sale_date",
        "calendar_interval": "month",
        "format": "yyyy-MM"
      },
      "aggs": {
        "toplam_ciro": { "sum": { "field": "revenue" } },
        "satis_adedi": { "sum": { "field": "quantity" } },
        "benzersiz_musteri": { "cardinality": { "field": "customer_id" } },
        "ort_fiyat": { "avg": { "field": "price" } },
        "top_kategori": {
          "terms": {
            "field": "category",
            "size": 3
          }
        }
      }
    }
  }
}

Her ayın cirosu, satış adedi, benzersiz müşteri sayısı ve en popüler 3 kategorisi — tek sorguda.


5. range Aggregation — Özel Aralıklar

Histogram'dan farklı olarak, eşit olmayan aralıklar tanımlayabilirsiniz:

GET sales/_search
{
  "size": 0,
  "aggs": {
    "fiyat_segmentleri": {
      "range": {
        "field": "price",
        "ranges": [
          { "key": "Bütçe Dostu", "to": 10000 },
          { "key": "Orta Segment", "from": 10000, "to": 30000 },
          { "key": "Üst Segment", "from": 30000, "to": 50000 },
          { "key": "Premium", "from": 50000 }
        ]
      }
    }
  }
}

Yanıt:

{
  "buckets": [
    { "key": "Bütçe Dostu", "to": 10000, "doc_count": 2 },
    { "key": "Orta Segment", "from": 10000, "to": 30000, "doc_count": 2 },
    { "key": "Üst Segment", "from": 30000, "to": 50000, "doc_count": 4 },
    { "key": "Premium", "from": 50000, "doc_count": 2 }
  ]
}

⚠️ Aralık kuralı: from dahil, to hariçtir. "from": 10000, "to": 30000 → [10000, 30000)

date_range — Tarih Aralıkları

GET sales/_search
{
  "size": 0,
  "aggs": {
    "donemler": {
      "date_range": {
        "field": "sale_date",
        "format": "yyyy-MM-dd",
        "ranges": [
          { "key": "Ocak", "from": "2024-01-01", "to": "2024-02-01" },
          { "key": "Şubat", "from": "2024-02-01", "to": "2024-03-01" },
          { "key": "Mart", "from": "2024-03-01", "to": "2024-04-01" }
        ]
      },
      "aggs": {
        "ciro": { "sum": { "field": "revenue" } }
      }
    }
  }
}

6. filter ve filters Aggregation

6.1 filter — Tek Filtre

GET sales/_search
{
  "size": 0,
  "aggs": {
    "pahali_urunler": {
      "filter": {
        "range": { "price": { "gte": 40000 } }
      },
      "aggs": {
        "ort_rating": { "avg": { "field": "rating" } },
        "toplam_ciro": { "sum": { "field": "revenue" } }
      }
    }
  }
}

6.2 filters — Çoklu Filtre

GET sales/_search
{
  "size": 0,
  "aggs": {
    "sehirler": {
      "filters": {
        "filters": {
          "istanbul": { "term": { "city": "İstanbul" } },
          "ankara": { "term": { "city": "Ankara" } },
          "izmir": { "term": { "city": "İzmir" } }
        }
      },
      "aggs": {
        "toplam_ciro": { "sum": { "field": "revenue" } },
        "satis_sayisi": { "value_count": { "field": "product" } }
      }
    }
  }
}

7. Diğer Önemli Bucket Aggregation'lar

7.1 missing — Değeri Olmayan Dokümanlar

GET sales/_search
{
  "size": 0,
  "aggs": {
    "fiyati_olmayan": {
      "missing": {
        "field": "discount_rate"
      }
    }
  }
}

7.2 significant_terms — İstatistiksel Olarak Anlamlı Terimler

// Telefon kategorisinde diğer kategorilere göre anlamlı şekilde farklı olan markalar
GET sales/_search
{
  "size": 0,
  "query": {
    "term": { "category": "Telefon" }
  },
  "aggs": {
    "anlamli_markalar": {
      "significant_terms": {
        "field": "brand"
      }
    }
  }
}

Telefon kategorisinde "Xiaomi" beklenenden daha sık geçiyorsa, bu anlamlı bir terim olarak döner.

7.3 multi_terms — Çoklu Alan Gruplama

GET sales/_search
{
  "size": 0,
  "aggs": {
    "kategori_marka": {
      "multi_terms": {
        "terms": [
          { "field": "category" },
          { "field": "brand" }
        ],
        "size": 20
      },
      "aggs": {
        "toplam_ciro": { "sum": { "field": "revenue" } }
      }
    }
  }
}

SQL karşılığı: GROUP BY category, brand — ama tek aggregation ile.


8. Gerçek Dünya: E-Ticaret Dashboard

GET sales/_search
{
  "size": 0,
  "aggs": {
    "aylik_trend": {
      "date_histogram": {
        "field": "sale_date",
        "calendar_interval": "month",
        "format": "yyyy-MM"
      },
      "aggs": {
        "ciro": { "sum": { "field": "revenue" } },
        "siparis_sayisi": { "value_count": { "field": "product" } }
      }
    },
    "kategori_dagilimi": {
      "terms": { "field": "category", "size": 10 },
      "aggs": {
        "ciro": { "sum": { "field": "revenue" } },
        "ort_rating": { "avg": { "field": "rating" } }
      }
    },
    "sehir_performansi": {
      "terms": { "field": "city", "size": 10, "order": { "ciro": "desc" } },
      "aggs": {
        "ciro": { "sum": { "field": "revenue" } },
        "musteri_sayisi": { "cardinality": { "field": "customer_id" } }
      }
    },
    "fiyat_segmentleri": {
      "range": {
        "field": "price",
        "ranges": [
          { "key": "0-10K", "to": 10000 },
          { "key": "10K-30K", "from": 10000, "to": 30000 },
          { "key": "30K-50K", "from": 30000, "to": 50000 },
          { "key": "50K+", "from": 50000 }
        ]
      },
      "aggs": {
        "adet": { "sum": { "field": "quantity" } }
      }
    },
    "genel_metrikler": {
      "filter": { "match_all": {} },
      "aggs": {
        "toplam_ciro": { "sum": { "field": "revenue" } },
        "toplam_satis": { "sum": { "field": "quantity" } },
        "benzersiz_musteri": { "cardinality": { "field": "customer_id" } },
        "ort_siparis_tutari": { "avg": { "field": "revenue" } }
      }
    }
  }
}

Tek sorguda tüm dashboard verileri: aylık trend, kategori dağılımı, şehir performansı, fiyat segmentleri ve genel metrikler.


9. Java ile Bucket Aggregations

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.aggregations.*;
import co.elastic.clients.elasticsearch.core.*;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class BucketAggregationJava {

    // terms + sub-aggregation
    public static void categoryReport(ElasticsearchClient client) throws Exception {
        SearchResponse<Void> response = client.search(s -> s
            .index("sales")
            .size(0)
            .aggregations("kategoriler", a -> a
                .terms(t -> t.field("category").size(10))
                .aggregations("toplam_ciro", sub -> sub
                    .sum(su -> su.field("revenue"))
                )
                .aggregations("ort_fiyat", sub -> sub
                    .avg(av -> av.field("price"))
                )
            ),
            Void.class
        );

        var buckets = response.aggregations()
            .get("kategoriler").sterms().buckets().array();

        System.out.println("=== Kategori Raporu ===");
        for (var bucket : buckets) {
            double ciro = bucket.aggregations().get("toplam_ciro").sum().value();
            double ortFiyat = bucket.aggregations().get("ort_fiyat").avg().value();
            System.out.printf("%-15s | Adet: %d | Ciro: ₺%.2f | Ort. Fiyat: ₺%.2f%n",
                bucket.key().stringValue(), bucket.docCount(), ciro, ortFiyat);
        }
    }

    // date_histogram
    public static void monthlyTrend(ElasticsearchClient client) throws Exception {
        SearchResponse<Void> response = client.search(s -> s
            .index("sales")
            .size(0)
            .aggregations("aylik", a -> a
                .dateHistogram(dh -> dh
                    .field("sale_date")
                    .calendarInterval(CalendarInterval.Month)
                    .format("yyyy-MM")
                )
                .aggregations("ciro", sub -> sub
                    .sum(su -> su.field("revenue"))
                )
            ),
            Void.class
        );

        var buckets = response.aggregations()
            .get("aylik").dateHistogram().buckets().array();

        System.out.println("=== Aylık Trend ===");
        for (var bucket : buckets) {
            double ciro = bucket.aggregations().get("ciro").sum().value();
            System.out.printf("%s | Satış: %d | Ciro: ₺%.2f%n",
                bucket.keyAsString(), bucket.docCount(), ciro);
        }
    }

    // range aggregation
    public static void priceSegments(ElasticsearchClient client) throws Exception {
        SearchResponse<Void> response = client.search(s -> s
            .index("sales")
            .size(0)
            .aggregations("fiyat_seg", a -> a
                .range(r -> r
                    .field("price")
                    .ranges(
                        rng -> rng.key("Bütçe").to("10000"),
                        rng -> rng.key("Orta").from("10000").to("30000"),
                        rng -> rng.key("Üst").from("30000").to("50000"),
                        rng -> rng.key("Premium").from("50000")
                    )
                )
                .aggregations("adet", sub -> sub
                    .sum(su -> su.field("quantity"))
                )
            ),
            Void.class
        );

        var buckets = response.aggregations()
            .get("fiyat_seg").range().buckets().array();

        for (var bucket : buckets) {
            System.out.printf("%-10s | Ürün: %d | Adet: %.0f%n",
                bucket.key(), bucket.docCount(),
                bucket.aggregations().get("adet").sum().value());
        }
    }
}

10. Best Practices

✅ Yapın

UygulamaNeden
size: 0 kullanınSadece aggregation sonuçlarını alın
terms'de size parametresini belirtinVarsayılan 10 — yetmeyebilir
date_histogram'da time_zone ekleyinUTC ile yerel saat farkı sorunları
İç içe aggregation'ları amaca göre yapılandırınHer seviye bir soru yanıtlamalı
key parametresi ile range'leri isimlendirinSonuçları okumak kolaylaşır

❌ Yapmayın

UygulamaNeden
terms'de size: 10000 yapmayınBellek sorunu; composite aggregation düşünün
5+ seviyeli nested aggregation yapmayınPerformans ve okunabilirlik düşer
text field'da terms aggregation yapmayınFielddata gerekir — keyword kullanın
Çok küçük histogram interval kullanmayınBinlerce boş bucket oluşur

11. Yaygın Hatalar

Hata 1: terms size vs Doğruluk

// ❌ size: 3 ile top markalar — eksik veya yanlış sıralı olabilir
"terms": { "field": "brand", "size": 3 }

// ✅ size'ı ihtiyacınıza göre artırın
"terms": { "field": "brand", "size": 20 }

doc_count_error_upper_bound > 0 ise sayımlarda hata olabilir demektir.

Hata 2: date_histogram calendar vs fixed

// ❌ calendar_interval ile "2w" geçersiz
"calendar_interval": "2w"  // Hata!

// ✅ Çoklu takvim aralığı için fixed_interval kullanın
"fixed_interval": "14d"

// calendar_interval tek birim kabul eder: 1m, 1h, 1d, 1w, 1M, 1q, 1y

Hata 3: range Aralık Kapsama

// ❌ 30000 hangi aralıkta? İkisi de mi?
{ "to": 30000 },         // 0-29999.99
{ "from": 30000 }        // 30000+
// from dahil, to hariç → 30000 ikinci aralıkta ✓

// Ama boşluk bırakmayın:
{ "to": 29999 },        // ❌ 29999-30000 arası boş kalır!
{ "from": 30000 }

Özet

  • terms aggregation kategorik alanları gruplar — SQL GROUP BY karşılığı

  • histogram sayısal değerleri eşit aralıklara böler — fiyat dağılımı grafiği için ideal

  • date_histogram zaman serisi oluşturur — dashboard grafiklerinin temel taşı

  • range özel aralıklar tanımlar — eşit olmayan segmentler için (bütçe/orta/premium)

  • filter/filters belirli koşullara göre gruplar — birden fazla filtre sonucunu karşılaştırma

  • Sub-aggregation ile her bucket içinde metrik hesaplamaları yapılabilir — termsavg, sum, cardinality

  • terms size parametresi doğruluk için önemlidir — çok küçük tutmayın

  • date_histogram'da `time_zone` belirtmeyi unutmayın — UTC offset hataları sık yaşanır