← Kursa Dön
📄 Text · 30 min

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 Pro

3. 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şamaNerede ÇalışırNe Yapar
init_scriptHer shard'daState objesini başlatır
map_scriptHer shard'da, her doküman içinDoküman verisini state'e ekler
combine_scriptHer shard'daO shard'ın sonuçlarını birleştirir
reduce_scriptCoordinating node'daTü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:

  1. Query çalışır → eşleşen dokümanlar bulunur

  2. Global aggregation tüm dokümanlar üzerinde çalışır (query'den bağımsız)

  3. Bucket aggregation'lar dokümanları gruplara ayırır

  4. Metric aggregation'lar her bucket içinde hesaplanır

  5. 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ın

Hata 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

UygulamaNeden
Aggregation derinliğini 3-4 ile sınırlayınBucket patlamasını önler
size: 0 kullanın (hits gerekmiyorsa)Network ve bellek tasarrufu
filter context'i tercih edinCache'lenir, daha hızlı
Nested agg'larda reverse_nested unutmayınParent field'lara erişim gerekir
Scripted metric yerine built-in agg tercih edinPerformans farkı 10x olabilir
shard_sizesize'dan büyük tutunDaha doğru sonuçlar (varsayılan: size * 1.5 + 10)
Çok büyük veri setlerinde sampler kullanınYaklaşı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 — nested wrapper şarttır

  • Reverse 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, b karşılığı

  • Scripted metric tamamen özel metrikler hesaplar — init, map, combine, reduce döngüsü