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
| Özellik | Metric Aggregation | Bucket Aggregation |
|---|---|---|
| Çıktı | Tek sayısal değer | Doküman grupları |
| Amaç | Hesaplama (avg, sum) | Gruplama |
| SQL karşılığı | AVG(), SUM() | GROUP BY |
| Alt aggregation | Hayır | Evet (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
| Uygulama | Neden |
|---|---|
size: 0 kullanın | Sadece aggregation sonuçlarını alın |
terms'de size parametresini belirtin | Varsayılan 10 — yetmeyebilir |
date_histogram'da time_zone ekleyin | UTC ile yerel saat farkı sorunları |
| İç içe aggregation'ları amaca göre yapılandırın | Her seviye bir soru yanıtlamalı |
key parametresi ile range'leri isimlendirin | Sonuçları okumak kolaylaşır |
❌ Yapmayın
| Uygulama | Neden |
|---|---|
terms'de size: 10000 yapmayın | Bellek sorunu; composite aggregation düşünün |
| 5+ seviyeli nested aggregation yapmayın | Performans ve okunabilirlik düşer |
| text field'da terms aggregation yapmayın | Fielddata gerekir — keyword kullanın |
| Çok küçük histogram interval kullanmayın | Binlerce 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, 1yHata 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 BYkarşı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 —
terms→avg,sum,cardinalityterms
sizeparametresi doğruluk için önemlidir — çok küçük tutmayındate_histogram'da `time_zone` belirtmeyi unutmayın — UTC offset hataları sık yaşanır
AI Asistan
Sorularını yanıtlamaya hazır