Nested ve Multi-Level Aggregations
Giriş — Soruların İç İçe Geçtiği Nokta
Bir e-ticaret sitesinin analiz ekranını düşünün. "Hangi kategoride kaç ürün var?" basit bir terms aggregation'dır. Ama "Her kategoride, markaların ortalama fiyatı nedir?" sorusu artık iki katmanlı. "Her kategoride, her markada, yıllara göre satış trendi nasıl?" diye sorduğunuzda üç katmanlı bir aggregation'a ulaşıyorsunuz.
Elasticsearch aggregation'ları iç içe koyarak (nesting) sınırsız derinlikte analiz yapabilirsiniz. Ama dikkatli olmalısınız — her seviye kardinalite çarpanıyla çalışır. 50 kategori × 100 marka × 12 ay = 60.000 bucket. İşler hızla karmaşıklaşabilir.
Bu ders, multi-level aggregation'ları derinlemesine inceleyecek. Nested object aggregation, reverse_nested, adjacency_matrix, sampler ve scripted metric gibi ileri konuları öğreneceksiniz.
1. Sub-Aggregation Zincirleri
1.1 İki Seviyeli Aggregation
GET products/_search
{
"size": 0,
"aggs": {
"by_category": {
"terms": { "field": "category.keyword", "size": 20 },
"aggs": {
"avg_price": { "avg": { "field": "price" } },
"max_price": { "max": { "field": "price" } }
}
}
}
}Her kategori bucket'ı içinde ortalama ve maksimum fiyat hesaplanır. Bu en temel sub-aggregation kalıbıdır.
1.2 Üç Seviyeli Aggregation
GET products/_search
{
"size": 0,
"aggs": {
"by_category": {
"terms": { "field": "category.keyword", "size": 10 },
"aggs": {
"by_brand": {
"terms": { "field": "brand.keyword", "size": 10 },
"aggs": {
"price_stats": {
"stats": { "field": "price" }
},
"top_product": {
"top_hits": {
"size": 1,
"sort": [{ "price": "desc" }],
"_source": ["name", "price"]
}
}
}
}
}
}
}
}Bu sorgu şu hiyerarşiyi oluşturur:
Kategori (Elektronik, Giyim, ...)
└─ Marka (Samsung, Apple, ...)
├─ Fiyat istatistikleri (min, max, avg, sum, count)
└─ En pahalı ürün (top_hits)1.3 Dört Seviyeli Aggregation
GET orders/_search
{
"size": 0,
"aggs": {
"by_year": {
"date_histogram": {
"field": "order_date",
"calendar_interval": "year"
},
"aggs": {
"by_region": {
"terms": { "field": "region.keyword", "size": 10 },
"aggs": {
"by_category": {
"terms": { "field": "category.keyword", "size": 10 },
"aggs": {
"total_revenue": { "sum": { "field": "total_amount" } },
"order_count": { "value_count": { "field": "_id" } }
}
}
}
}
}
}
}
}4 seviye: Yıl → Bölge → Kategori → Gelir/Sipariş Sayısı.
⚠️ Dikkat: Derinlik arttıkça bucket sayısı çarpımsal olarak artar. 5 yıl × 7 bölge × 20 kategori = 700 bucket. Her bucket'ta 2 metrik = 1400 değer. Performans sorunlarına dikkat edin.
2. Nested Aggregation
2.1 Nested Object Hatırlatma
Elasticsearch'te nested tip, iç objelerin bağımsız olarak aranmasını ve aggregate edilmesini sağlar:
PUT ecommerce
{
"mappings": {
"properties": {
"product_name": { "type": "text" },
"reviews": {
"type": "nested",
"properties": {
"author": { "type": "keyword" },
"rating": { "type": "integer" },
"comment": { "type": "text" },
"date": { "type": "date" }
}
}
}
}
}
POST ecommerce/_bulk
{"index":{"_id":"1"}}
{"product_name":"Samsung Galaxy S24","reviews":[{"author":"Ali","rating":5,"comment":"Harika telefon","date":"2024-01-15"},{"author":"Ayşe","rating":4,"comment":"İyi ama pahalı","date":"2024-02-20"},{"author":"Mehmet","rating":3,"comment":"Ortalama","date":"2024-03-10"}]}
{"index":{"_id":"2"}}
{"product_name":"iPhone 15 Pro","reviews":[{"author":"Zeynep","rating":5,"comment":"Mükemmel","date":"2024-01-20"},{"author":"Can","rating":5,"comment":"En iyi telefon","date":"2024-02-15"}]}
{"index":{"_id":"3"}}
{"product_name":"Pixel 8 Pro","reviews":[{"author":"Ali","rating":4,"comment":"Kamerası çok iyi","date":"2024-03-01"},{"author":"Deniz","rating":2,"comment":"Yazılım hataları var","date":"2024-04-05"}]}2.2 Nested Aggregation
GET ecommerce/_search
{
"size": 0,
"aggs": {
"reviews_agg": {
"nested": {
"path": "reviews"
},
"aggs": {
"avg_rating": {
"avg": { "field": "reviews.rating" }
},
"rating_distribution": {
"terms": { "field": "reviews.rating" }
},
"reviews_over_time": {
"date_histogram": {
"field": "reviews.date",
"calendar_interval": "month"
}
}
}
}
}
}nested aggregation ile iç objelerin kendi bağlamında (context) aggregate edilmesini sağlarsınız. Normal aggregation kullanırsanız, nested objeler düzleştirilir (flatten) ve yanlış sonuçlar alırsınız.
2.3 Reverse Nested — İç Objeden Dış Dokümanına Dönme
Nested context'teyken parent dokümanın alanlarına erişmek için reverse_nested kullanılır:
GET ecommerce/_search
{
"size": 0,
"aggs": {
"reviews_agg": {
"nested": { "path": "reviews" },
"aggs": {
"by_author": {
"terms": { "field": "reviews.author", "size": 10 },
"aggs": {
"avg_rating": {
"avg": { "field": "reviews.rating" }
},
"back_to_product": {
"reverse_nested": {},
"aggs": {
"product_names": {
"terms": { "field": "product_name.keyword", "size": 5 }
}
}
}
}
}
}
}
}
}Bu sorgu: Her review yazarı için → ortalama puanı VE hangi ürünlere yorum yaptığını verir. reverse_nested olmasaydı, nested context'te product_name'e erişemezdiniz.
Çıktı yapısı:
Ali → avg_rating: 4.5
└─ Ürünler: Samsung Galaxy S24, Pixel 8 Pro
Ayşe → avg_rating: 4.0
└─ Ürünler: Samsung Galaxy S24
Zeynep → avg_rating: 5.0
└─ Ürünler: iPhone 15 Pro3. Adjacency Matrix Aggregation
Adjacency matrix, birden fazla filtre arasındaki kesişimleri (overlap) analiz eder. Venn diyagramı gibi düşünün.
GET products/_search
{
"size": 0,
"aggs": {
"interactions": {
"adjacency_matrix": {
"filters": {
"premium": { "range": { "price": { "gte": 10000 } } },
"samsung": { "term": { "brand.keyword": "Samsung" } },
"five_star": { "range": { "avg_rating": { "gte": 4.5 } } }
}
}
}
}
}Sonuç:
{
"buckets": [
{ "key": "premium", "doc_count": 150 },
{ "key": "samsung", "doc_count": 80 },
{ "key": "five_star", "doc_count": 120 },
{ "key": "premium&samsung", "doc_count": 35 },
{ "key": "premium&five_star", "doc_count": 60 },
{ "key": "samsung&five_star", "doc_count": 25 },
{ "key": "premium&samsung&five_star", "doc_count": 15 }
]
}15 ürün hem premium hem Samsung hem 5 yıldız. Bu, segment analizi ve kullanıcı davranışı kesişimlerinde çok değerlidir.
4. Sampler ve Diversified Sampler
4.1 Sampler Aggregation
Büyük veri setlerinde aggregation performansını artırmak için örnekleme yapar:
GET logs/_search
{
"size": 0,
"aggs": {
"sample": {
"sampler": {
"shard_size": 200
},
"aggs": {
"significant_errors": {
"significant_terms": {
"field": "error_message.keyword",
"size": 10
}
}
}
}
}
}Her shard'dan en alakalı 200 doküman alınır, onların üzerinde significant_terms çalıştırılır. Milyonlarca log'dan yalnızca anlamlı bir örneklem analiz edilir.
4.2 Diversified Sampler
Örneklemenin belirli bir field'a göre çeşitlendirilmesini sağlar:
GET products/_search
{
"size": 0,
"query": {
"match": { "description": "akıllı telefon" }
},
"aggs": {
"diverse_sample": {
"diversified_sampler": {
"shard_size": 100,
"field": "brand.keyword",
"max_docs_per_value": 5
},
"aggs": {
"common_features": {
"significant_terms": {
"field": "specs.keyword",
"size": 10
}
}
}
}
}
}Her markadan en fazla 5 doküman alınır — Samsung 50 sonuç getirse bile sadece 5'i örneklenir. Böylece sonuçlar tek bir markaya yönelmez.
5. Rare Terms ve Multi Terms
5.1 Rare Terms
terms aggregation en yaygın değerleri bulur. rare_terms ise en nadir değerleri bulur:
GET logs/_search
{
"size": 0,
"aggs": {
"rare_errors": {
"rare_terms": {
"field": "error_code.keyword",
"max_doc_count": 5
}
}
}
}5'ten az dokümanda geçen error code'ları bulur. Nadir hataları tespit etmek, anomali tespiti ve debugging için çok faydalıdır.
💡 İpucu: rare_terms, terms aggregation'ın tersidir. terms yüksek frekans → düşük frekans sıralar, rare_terms düşük frekans → yüksek frekans sıralar. Küçük kovalarda Cuckoo filter kullanır, bu yüzden yaklaşık sonuçlar verir.
5.2 Multi Terms
Birden fazla field'ı aynı anda gruplayarak aggregate eder:
GET orders/_search
{
"size": 0,
"aggs": {
"category_brand_combo": {
"multi_terms": {
"terms": [
{ "field": "category.keyword" },
{ "field": "brand.keyword" }
],
"size": 20
},
"aggs": {
"total_revenue": { "sum": { "field": "total_amount" } }
}
}
}
}Sonuç:
{
"buckets": [
{ "key": ["Elektronik", "Samsung"], "key_as_string": "Elektronik|Samsung", "doc_count": 450, "total_revenue": { "value": 12500000 } },
{ "key": ["Elektronik", "Apple"], "key_as_string": "Elektronik|Apple", "doc_count": 380, "total_revenue": { "value": 15200000 } },
{ "key": ["Giyim", "Nike"], "key_as_string": "Giyim|Nike", "doc_count": 220, "total_revenue": { "value": 3800000 } }
]
}SQL'deki GROUP BY category, brand'in karşılığı. Daha önce bunu nested terms aggregation ile yapardınız, multi_terms daha temiz bir alternatiftir.
⚠️ Dikkat: multi_terms yaklaşık sonuçlar verebilir (tıpkı terms gibi). Kesin sonuç için composite aggregation tercih edin.
6. Scripted Metric Aggregation
Tamamen özel bir metrik hesaplamak istediğinizde Painless script kullanırsınız. Dört aşamadan oluşur: init, map, combine, reduce.
6.1 Temel Yapı
GET orders/_search
{
"size": 0,
"aggs": {
"profit_calculation": {
"scripted_metric": {
"init_script": "state.profits = []",
"map_script": """
double revenue = doc['total_amount'].value;
double cost = doc['cost'].value;
state.profits.add(revenue - cost);
""",
"combine_script": """
double total = 0;
for (p in state.profits) { total += p; }
return total;
""",
"reduce_script": """
double total = 0;
for (s in states) { total += s; }
return total;
"""
}
}
}
}Aşamalar:
| Aşama | Nerede Çalışır | Ne Yapar |
|---|---|---|
init_script | Her shard'da | State objesini başlatır |
map_script | Her shard'da, her doküman için | Doküman verisini state'e ekler |
combine_script | Her shard'da | O shard'ın sonuçlarını birleştirir |
reduce_script | Coordinating node'da | Tüm shard sonuçlarını birleştirir |
6.2 Karmaşık Örnek: Ağırlıklı Ortalama
GET reviews/_search
{
"size": 0,
"aggs": {
"weighted_avg_rating": {
"scripted_metric": {
"init_script": """
state.totalWeight = 0.0;
state.weightedSum = 0.0;
""",
"map_script": """
double rating = doc['rating'].value;
double recency = doc['days_since_review'].value;
double weight = 1.0 / (1.0 + recency / 30.0);
state.weightedSum += rating * weight;
state.totalWeight += weight;
""",
"combine_script": """
def result = new HashMap();
result.put('weightedSum', state.weightedSum);
result.put('totalWeight', state.totalWeight);
return result;
""",
"reduce_script": """
double totalWeightedSum = 0;
double totalWeight = 0;
for (s in states) {
totalWeightedSum += s.weightedSum;
totalWeight += s.totalWeight;
}
return totalWeight > 0 ? totalWeightedSum / totalWeight : 0;
"""
}
}
}
}Bu, yeni review'lara daha fazla ağırlık veren bir ortalama hesaplar. 30 günden eski review'lar yarı ağırlıkla sayılır.
⚠️ Performans Notu: Scripted metric her doküman için script çalıştırır. Büyük veri setlerinde yavaş olabilir. Mümkünse built-in aggregation kullanın.
7. Aggregation Execution Order ve Optimization
7.1 Execution Order
Aggregation'lar belirli bir sırayla çalışır:
Query çalışır → eşleşen dokümanlar bulunur
Global aggregation tüm dokümanlar üzerinde çalışır (query'den bağımsız)
Bucket aggregation'lar dokümanları gruplara ayırır
Metric aggregation'lar her bucket içinde hesaplanır
Pipeline aggregation'lar diğer agg sonuçları üzerinde çalışır
GET products/_search
{
"query": {
"match": { "description": "telefon" }
},
"size": 0,
"aggs": {
"filtered_categories": {
"terms": { "field": "category.keyword" },
"aggs": {
"avg_price": { "avg": { "field": "price" } }
}
},
"all_categories": {
"global": {},
"aggs": {
"categories": {
"terms": { "field": "category.keyword" }
}
}
}
}
}filtered_categories: Sadece "telefon" eşleşen dokümanlar. all_categories (global): Query'den bağımsız, tüm dokümanlar. E-commerce'de "tüm kategorilerdeki ürün sayısı" göstermek için kullanılır.
7.2 Filter Aggregation — Performans Tüyosu
Her bucket için ayrı bir filter uygulamak:
GET products/_search
{
"size": 0,
"aggs": {
"premium_products": {
"filter": {
"range": { "price": { "gte": 10000 } }
},
"aggs": {
"brands": { "terms": { "field": "brand.keyword" } },
"avg_price": { "avg": { "field": "price" } }
}
},
"budget_products": {
"filter": {
"range": { "price": { "lt": 1000 } }
},
"aggs": {
"brands": { "terms": { "field": "brand.keyword" } },
"avg_price": { "avg": { "field": "price" } }
}
}
}
}7.3 Filters Aggregation — Çoklu Named Filter
GET orders/_search
{
"size": 0,
"aggs": {
"order_segments": {
"filters": {
"filters": {
"small": { "range": { "total_amount": { "lt": 100 } } },
"medium": { "range": { "total_amount": { "gte": 100, "lt": 1000 } } },
"large": { "range": { "total_amount": { "gte": 1000 } } }
}
},
"aggs": {
"avg_items": { "avg": { "field": "item_count" } },
"total_revenue": { "sum": { "field": "total_amount" } }
}
}
}
}8. Yaygın Hatalar ve Çözümleri
Hata 1: Nested Aggregation Olmadan Nested Field Aggregate Etmek
// ❌ Nested field düzleştirilir, yanlış sonuçlar verir
{
"aggs": {
"avg_rating": { "avg": { "field": "reviews.rating" } }
}
}
// ✅ nested agg ile sarmalayın
{
"aggs": {
"reviews_nested": {
"nested": { "path": "reviews" },
"aggs": {
"avg_rating": { "avg": { "field": "reviews.rating" } }
}
}
}
}Hata 2: Reverse Nested Unutmak
// ❌ Nested context'te parent field'a erişilemez
{
"nested": { "path": "reviews" },
"aggs": {
"by_author": {
"terms": { "field": "reviews.author" },
"aggs": {
"products": {
"terms": { "field": "product_name.keyword" } // HATA!
}
}
}
}
}
// ✅ reverse_nested ile parent'a dönün
{
"nested": { "path": "reviews" },
"aggs": {
"by_author": {
"terms": { "field": "reviews.author" },
"aggs": {
"back_to_parent": {
"reverse_nested": {},
"aggs": {
"products": {
"terms": { "field": "product_name.keyword" }
}
}
}
}
}
}
}Hata 3: Çok Derin Aggregation = Bellek Patlaması
// ❌ 5 seviye × yüksek kardinalite = milyonlarca bucket
{
"aggs": {
"level1": { // 100 bucket
"terms": { "field": "field1", "size": 100 },
"aggs": {
"level2": { // × 50 = 5.000
"terms": { "field": "field2", "size": 50 },
"aggs": {
"level3": { // × 30 = 150.000 bucket!
"terms": { "field": "field3", "size": 30 }
}
}
}
}
}
}
}
// ✅ Composite aggregation kullanın veya derinliği sınırlayınHata 4: Multi Terms'te Sıralama Tuzağı
// ❌ multi_terms varsayılan olarak doc_count'a göre sıralar
// İlk N sonucu alır — bazı kombinasyonlar atlanabilir
// ✅ Kesin sonuç gerekiyorsa composite aggregation kullanın
{
"aggs": {
"all_combos": {
"composite": {
"size": 100,
"sources": [
{ "category": { "terms": { "field": "category.keyword" } } },
{ "brand": { "terms": { "field": "brand.keyword" } } }
]
}
}
}
}9. Java ile Multi-Level Aggregation
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch._types.aggregations.*;
import java.util.Map;
// 3 seviyeli aggregation: Kategori → Marka → İstatistikler
SearchResponse<Void> response = client.search(s -> s
.index("products")
.size(0)
.aggregations("by_category", a -> a
.terms(t -> t.field("category.keyword").size(10))
.aggregations("by_brand", a2 -> a2
.terms(t -> t.field("brand.keyword").size(10))
.aggregations("price_stats", a3 -> a3
.stats(st -> st.field("price"))
)
)
),
Void.class
);
// Sonuçları parse etme
StringTermsAggregate categories = response.aggregations()
.get("by_category").sterms();
for (StringTermsBucket catBucket : categories.buckets().array()) {
System.out.printf("Kategori: %s (%d ürün)%n",
catBucket.key().stringValue(), catBucket.docCount());
StringTermsAggregate brands = catBucket.aggregations()
.get("by_brand").sterms();
for (StringTermsBucket brandBucket : brands.buckets().array()) {
StatsAggregate stats = brandBucket.aggregations()
.get("price_stats").stats();
System.out.printf(" Marka: %s — Ort: %.0f TL, Min: %.0f, Max: %.0f (%d ürün)%n",
brandBucket.key().stringValue(),
stats.avg(),
stats.min(),
stats.max(),
brandBucket.docCount()
);
}
}Nested aggregation parse:
// Nested → reverse_nested örneği
SearchResponse<Void> nestedResp = client.search(s -> s
.index("ecommerce")
.size(0)
.aggregations("reviews_nested", a -> a
.nested(n -> n.path("reviews"))
.aggregations("by_author", a2 -> a2
.terms(t -> t.field("reviews.author").size(10))
.aggregations("avg_rating", a3 -> a3
.avg(avg -> avg.field("reviews.rating"))
)
.aggregations("back_to_product", a3 -> a3
.reverseNested(rn -> rn)
.aggregations("products", a4 -> a4
.terms(t -> t.field("product_name.keyword").size(5))
)
)
)
),
Void.class
);
NestedAggregate nested = nestedResp.aggregations()
.get("reviews_nested").nested();
StringTermsAggregate authors = nested.aggregations()
.get("by_author").sterms();
for (StringTermsBucket authorBucket : authors.buckets().array()) {
double avgRating = authorBucket.aggregations()
.get("avg_rating").avg().value();
ReverseNestedAggregate reverseNested = authorBucket.aggregations()
.get("back_to_product").reverseNested();
StringTermsAggregate products = reverseNested.aggregations()
.get("products").sterms();
System.out.printf("Yazar: %s (ort. puan: %.1f)%n",
authorBucket.key().stringValue(), avgRating);
for (StringTermsBucket prodBucket : products.buckets().array()) {
System.out.printf(" → %s%n", prodBucket.key().stringValue());
}
}10. Best Practices
| Uygulama | Neden |
|---|---|
| Aggregation derinliğini 3-4 ile sınırlayın | Bucket patlamasını önler |
size: 0 kullanın (hits gerekmiyorsa) | Network ve bellek tasarrufu |
filter context'i tercih edin | Cache'lenir, daha hızlı |
Nested agg'larda reverse_nested unutmayın | Parent field'lara erişim gerekir |
| Scripted metric yerine built-in agg tercih edin | Performans farkı 10x olabilir |
shard_size'ı size'dan büyük tutun | Daha doğru sonuçlar (varsayılan: size * 1.5 + 10) |
Çok büyük veri setlerinde sampler kullanın | Yaklaşık ama hızlı sonuçlar |
Özet
Sub-aggregation zincirleri ile çok seviyeli analiz yapılır — her seviye önceki bucket'ın içinde çalışır
Nested aggregation nested objeleri kendi bağlamında aggregate eder —
nestedwrapper şarttırReverse nested nested context'ten parent dokümanına döner — cross-level analiz için
Adjacency matrix filtrelerin kesişim analizini yapar — segment ve Venn diyagramı analizi
Sampler/Diversified sampler büyük veri setlerinde örnekleme ile hızlı analiz sağlar
Rare terms nadir değerleri bulur — anomali tespiti ve debugging
Multi terms birden fazla field'ı aynı anda gruplar — SQL
GROUP BY a, bkarşılığıScripted metric tamamen özel metrikler hesaplar — init, map, combine, reduce döngüsü
AI Asistan
Sorularını yanıtlamaya hazır