Nested ve Object Tipler — Inner Hits, Parent-Child
Giriş — Dosya Dolabı Benzetmesi
Bir dosya dolabı düşünün. Her dosyanın içinde birden fazla belge var. "Ahmet'in dosyası" içinde "maaş belgesi" ve "izin formu" olabilir. Eğer tüm belgeleri tek bir yığına atarsanız, hangi belgenin hangi dosyaya ait olduğunu bulamazsınız. Ama her dosyayı kendi zarfında tutarsanız, "Ahmet'in maaş belgesi 5000 TL üzerinde mi?" sorusunu doğru yanıtlayabilirsiniz.
Elasticsearch'te de aynı sorun var. Bir dokümanın içinde dizi (array) halinde nesneler olduğunda, bu nesnelerin alanları arasındaki ilişki kaybolabilir. object tipi bu ilişkiyi korumaz, nested tipi korur. Bu ders, her iki tipin farkını, ne zaman hangisinin kullanılacağını ve parent-child ilişkileri kapsamlı olarak işleyecektir.
1. Object Type — Varsayılan ve Tehlikeli
1.1 Sorun: Flattening
Elasticsearch, iç içe JSON nesnelerini (nested objects) varsayılan olarak düzleştirir (flatten):
PUT orders
{
"mappings": {
"properties": {
"customer": { "type": "keyword" },
"items": {
"properties": {
"product": { "type": "keyword" },
"quantity": { "type": "integer" },
"price": { "type": "float" }
}
}
}
}
}
POST orders/_doc/1
{
"customer": "Ahmet",
"items": [
{ "product": "Laptop", "quantity": 1, "price": 42999.99 },
{ "product": "Mouse", "quantity": 2, "price": 299.99 }
]
}Elasticsearch bu dokümanı dahili olarak şöyle saklar:
{
"customer": "Ahmet",
"items.product": ["Laptop", "Mouse"],
"items.quantity": [1, 2],
"items.price": [42999.99, 299.99]
}İlişki kayboldu! Artık "Laptop'un fiyatı 42999.99" bilgisi yok — sadece ayrı ayrı product listesi ve price listesi var.
1.2 Yanlış Sonuç Örneği
// "Mouse" ürününü 42999.99 fiyatla arıyoruz — bu ürün YOK
GET orders/_search
{
"query": {
"bool": {
"must": [
{ "term": { "items.product": "Mouse" } },
{ "range": { "items.price": { "gte": 40000 } } }
]
}
}
}Sonuç: Doküman bulunur! ❌
Neden? Çünkü items.product dizisinde "Mouse" var VE items.price dizisinde 42999.99 var. Elasticsearch bunların aynı nesneye ait olup olmadığını bilmez.
Bu cross-object matching sorunu, object type'ın en büyük tuzağıdır.
1.3 Object Type Ne Zaman Uygun?
Object type şu durumlarda güvenlidir:
Dizi olmayan tek nesne:
"address": { "city": "İstanbul", "country": "TR" }İç nesneler arası ilişki sorgulanmayacak
Sadece belirli alt alanlar aranacak
// Tek nesne — object type güvenli
POST users/_doc/1
{
"name": "Ahmet",
"address": {
"city": "İstanbul",
"district": "Kadıköy",
"postal_code": "34710"
}
}
// Bu sorgu doğru çalışır çünkü tek nesne var
GET users/_search
{
"query": {
"bool": {
"must": [
{ "term": { "address.city": "İstanbul" } },
{ "term": { "address.district": "Kadıköy" } }
]
}
}
}2. Nested Type — İlişkiyi Koruma
2.1 Nested Mapping
PUT orders_nested
{
"mappings": {
"properties": {
"customer": { "type": "keyword" },
"items": {
"type": "nested",
"properties": {
"product": { "type": "keyword" },
"quantity": { "type": "integer" },
"price": { "type": "float" }
}
}
}
}
}
POST orders_nested/_doc/1
{
"customer": "Ahmet",
"items": [
{ "product": "Laptop", "quantity": 1, "price": 42999.99 },
{ "product": "Mouse", "quantity": 2, "price": 299.99 }
]
}nested type kullandığınızda, her iç nesne ayrı bir gizli doküman olarak saklanır. Lucene seviyesinde 3 doküman oluşturulur: 1 ana doküman + 2 nested doküman.
2.2 Nested Query
Nested alanları sorgulamak için nested query kullanmak zorunludur:
// "Mouse" ürünü fiyatı >= 40000 olan sipariş var mı?
GET orders_nested/_search
{
"query": {
"nested": {
"path": "items",
"query": {
"bool": {
"must": [
{ "term": { "items.product": "Mouse" } },
{ "range": { "items.price": { "gte": 40000 } } }
]
}
}
}
}
}Sonuç: Doküman BULUNAMAZ! ✅
Çünkü nested query her iç nesneyi bağımsız değerlendirir. Mouse'un fiyatı 299.99 — 40000'den küçük.
// "Laptop" ürünü fiyatı >= 40000 olan sipariş var mı?
GET orders_nested/_search
{
"query": {
"nested": {
"path": "items",
"query": {
"bool": {
"must": [
{ "term": { "items.product": "Laptop" } },
{ "range": { "items.price": { "gte": 40000 } } }
]
}
}
}
}
}Sonuç: Doküman bulunur! ✅ — Laptop'un fiyatı 42999.99.
2.3 score_mode
Nested query'nin ana dokümanın score'una nasıl katkı yapacağını belirler:
GET orders_nested/_search
{
"query": {
"nested": {
"path": "items",
"query": {
"match": { "items.product": "Laptop" }
},
"score_mode": "max"
}
}
}| score_mode | Davranış |
|---|---|
avg | Eşleşen nested dokümanların ortalama score'u (varsayılan) |
max | En yüksek score |
min | En düşük score |
sum | Toplam score |
none | Score hesaplanmaz (0.0) |
3. Inner Hits — Eşleşen Nested Dokümanları Görme
Normal nested query sadece ana dokümanı döndürür — hangi nested nesnenin eşleştiğini görmezsiniz. inner_hits bunu çözer:
GET orders_nested/_search
{
"query": {
"nested": {
"path": "items",
"query": {
"range": { "items.price": { "gte": 1000 } }
},
"inner_hits": {
"name": "expensive_items",
"size": 5,
"_source": ["items.product", "items.price"],
"highlight": {
"fields": {
"items.product": {}
}
}
}
}
}
}Yanıt:
{
"hits": {
"hits": [
{
"_source": {
"customer": "Ahmet",
"items": [ ... ]
},
"inner_hits": {
"expensive_items": {
"hits": {
"total": { "value": 1 },
"hits": [
{
"_nested": { "field": "items", "offset": 0 },
"_source": {
"product": "Laptop",
"price": 42999.99
}
}
]
}
}
}
}
]
}
}inner_hits sayesinde:
Hangi nested nesne eşleştiğini bilirsiniz
_nested.offsetile dizideki pozisyonunu görürsünüzHighlight ve source filtering uygulayabilirsiniz
Sorting ve size ile kontrol edebilirsiniz
Inner Hits Sıralama
"inner_hits": {
"size": 3,
"sort": [{ "items.price": "desc" }],
"_source": ["items.product", "items.price"]
}4. Nested Aggregation
Nested alanlar üzerinde aggregation yapmak için nested aggregation gerekir:
// En çok satan ürünler
GET orders_nested/_search
{
"size": 0,
"aggs": {
"items_agg": {
"nested": {
"path": "items"
},
"aggs": {
"popular_products": {
"terms": {
"field": "items.product",
"size": 10
}
},
"avg_price": {
"avg": {
"field": "items.price"
}
}
}
}
}
}reverse_nested — Ana Dokümana Dönme
Nested aggregation içinden ana doküman alanlarına erişmek için:
GET orders_nested/_search
{
"size": 0,
"aggs": {
"items_agg": {
"nested": {
"path": "items"
},
"aggs": {
"products": {
"terms": {
"field": "items.product"
},
"aggs": {
"back_to_order": {
"reverse_nested": {},
"aggs": {
"unique_customers": {
"cardinality": {
"field": "customer"
}
}
}
}
}
}
}
}
}
}"Her ürünü kaç farklı müşteri aldı?" sorusunu yanıtlar. reverse_nested nested bağlamdan çıkıp ana dokümana döner.
5. Nested Sorting
Ana dokümanları nested alanın değerine göre sıralamak:
// Siparişleri en pahalı ürünlerine göre sırala
GET orders_nested/_search
{
"sort": [
{
"items.price": {
"order": "desc",
"mode": "max",
"nested": {
"path": "items"
}
}
}
]
}
// Filtreli nested sorting
GET orders_nested/_search
{
"sort": [
{
"items.price": {
"order": "asc",
"mode": "min",
"nested": {
"path": "items",
"filter": {
"term": { "items.product": "Mouse" }
}
}
}
}
]
}mode parametresi birden fazla nested doküman olduğunda hangi değerin kullanılacağını belirler.
6. Çok Seviyeli Nested
Nested içinde nested yapılar da mümkündür:
PUT deep_nested_orders
{
"mappings": {
"properties": {
"customer": { "type": "keyword" },
"items": {
"type": "nested",
"properties": {
"product": { "type": "keyword" },
"price": { "type": "float" },
"reviews": {
"type": "nested",
"properties": {
"author": { "type": "keyword" },
"rating": { "type": "integer" },
"comment": { "type": "text" }
}
}
}
}
}
}
}
// Çok seviyeli nested query
GET deep_nested_orders/_search
{
"query": {
"nested": {
"path": "items",
"query": {
"bool": {
"must": [
{ "term": { "items.product": "Laptop" } },
{
"nested": {
"path": "items.reviews",
"query": {
"range": { "items.reviews.rating": { "gte": 4 } }
}
}
}
]
}
}
}
}
}⚠️ Dikkat: Çok seviyeli nested yapılar karmaşıklığı ve performans maliyetini artırır. Mümkünse tek seviye nested ile sınırlı kalın.
7. Parent-Child (Join) İlişkisi
Nested type'ın alternatifi olan parent-child ilişkisi, farklı dokümanlar arasında ilişki kurar. Nested'dan temel farkı: parent ve child ayrı doküman olarak saklanır ve bağımsız güncellenebilir.
7.1 Join Field Tanımı
PUT company
{
"mappings": {
"properties": {
"relation": {
"type": "join",
"relations": {
"department": "employee"
}
},
"name": { "type": "text" },
"title": { "type": "keyword" },
"salary": { "type": "float" }
}
}
}7.2 Parent ve Child Doküman Ekleme
// Parent dokümanlar (departmanlar)
PUT company/_doc/dept_engineering?routing=dept_engineering
{
"name": "Yazılım Mühendisliği",
"relation": {
"name": "department"
}
}
PUT company/_doc/dept_marketing?routing=dept_marketing
{
"name": "Pazarlama",
"relation": {
"name": "department"
}
}
// Child dokümanlar (çalışanlar) — routing ZORUNLU
PUT company/_doc/emp1?routing=dept_engineering
{
"name": "Ahmet Yılmaz",
"title": "Senior Developer",
"salary": 45000,
"relation": {
"name": "employee",
"parent": "dept_engineering"
}
}
PUT company/_doc/emp2?routing=dept_engineering
{
"name": "Mehmet Kaya",
"title": "Junior Developer",
"salary": 25000,
"relation": {
"name": "employee",
"parent": "dept_engineering"
}
}
PUT company/_doc/emp3?routing=dept_marketing
{
"name": "Ayşe Demir",
"title": "Marketing Manager",
"salary": 40000,
"relation": {
"name": "employee",
"parent": "dept_marketing"
}
}⚠️ Routing zorunlu: Child doküman, parent ile aynı shard'da olmalıdır. Bu nedenle routing parametresi parent ID ile aynı olmalıdır.
7.3 has_child Query — Child'a Göre Parent Bul
"Maaşı 40.000'den yüksek çalışanı olan departmanlar":
GET company/_search
{
"query": {
"has_child": {
"type": "employee",
"query": {
"range": { "salary": { "gte": 40000 } }
},
"inner_hits": {
"size": 5,
"_source": ["name", "salary"]
}
}
}
}7.4 has_parent Query — Parent'a Göre Child Bul
"Yazılım Mühendisliği departmanındaki çalışanlar":
GET company/_search
{
"query": {
"has_parent": {
"parent_type": "department",
"query": {
"match": { "name": "Yazılım Mühendisliği" }
},
"inner_hits": {
"_source": ["name"]
}
}
}
}7.5 parent_id Query
Belirli bir parent'ın tüm child'larını getirmek:
GET company/_search
{
"query": {
"parent_id": {
"type": "employee",
"id": "dept_engineering"
}
}
}8. Nested vs Parent-Child Karşılaştırması
| Özellik | Nested | Parent-Child (Join) |
|---|---|---|
| Saklama | Aynı doküman içinde | Ayrı dokümanlar |
| Güncelleme | Tüm doküman yeniden index'lenir | Bağımsız güncelleme |
| Sorgu hızı | Hızlı | Yavaş (join maliyeti) |
| Esneklik | Düşük | Yüksek |
| Routing | Otomatik | Manuel (zorunlu) |
| Kullanım | Sık değişmeyen iç nesneler | Bağımsız yaşam döngüsü olan ilişkiler |
| Limit | index.mapping.nested_objects.limit | Shard başına child sayısı |
Ne Zaman Hangisi?
Nested kullanın:
İç nesneler ana dokümanla birlikte yazılır/okunur
İç nesne sayısı makul (< 100)
İç nesneler nadiren bağımsız güncellenir
Örnek: Sipariş → Ürünler, Blog → Yorumlar (az sayıda)
Parent-Child kullanın:
Child dokümanlar sık güncellenir
Child sayısı çok fazla olabilir (binlerce)
Child ve parent bağımsız yaşam döngüsüne sahip
Örnek: Departman → Çalışanlar, Kategori → Ürünler (çok sayıda)
9. Gerçek Dünya Senaryosu: E-Ticaret Sipariş Sistemi
PUT ecommerce_orders
{
"mappings": {
"properties": {
"order_id": { "type": "keyword" },
"customer_name": { "type": "keyword" },
"order_date": { "type": "date" },
"total_amount": { "type": "float" },
"status": { "type": "keyword" },
"items": {
"type": "nested",
"properties": {
"product_name": { "type": "text", "analyzer": "turkish",
"fields": { "keyword": { "type": "keyword" } }
},
"category": { "type": "keyword" },
"quantity": { "type": "integer" },
"unit_price": { "type": "float" },
"discount": { "type": "float" }
}
},
"shipping_address": {
"properties": {
"city": { "type": "keyword" },
"district": { "type": "keyword" },
"postal_code": { "type": "keyword" }
}
}
}
}
}
POST ecommerce_orders/_bulk
{"index":{"_id":"order1"}}
{"order_id":"ORD-001","customer_name":"Ahmet Yılmaz","order_date":"2024-02-15","total_amount":43899.97,"status":"delivered","items":[{"product_name":"Samsung Galaxy S24 Ultra","category":"Telefon","quantity":1,"unit_price":54999.99,"discount":0.2},{"product_name":"Samsung Kablosuz Kulaklık","category":"Aksesuar","quantity":2,"unit_price":499.99,"discount":0.1}],"shipping_address":{"city":"İstanbul","district":"Kadıköy","postal_code":"34710"}}
{"index":{"_id":"order2"}}
{"order_id":"ORD-002","customer_name":"Mehmet Kaya","order_date":"2024-02-20","total_amount":45999.99,"status":"shipped","items":[{"product_name":"Apple MacBook Air M3","category":"Bilgisayar","quantity":1,"unit_price":42999.99,"discount":0.0},{"product_name":"Apple Magic Mouse","category":"Aksesuar","quantity":1,"unit_price":2999.99,"discount":0.0}],"shipping_address":{"city":"Ankara","district":"Çankaya","postal_code":"06690"}}
{"index":{"_id":"order3"}}
{"order_id":"ORD-003","customer_name":"Ayşe Demir","order_date":"2024-02-25","total_amount":31499.97,"status":"processing","items":[{"product_name":"Xiaomi 14 Pro","category":"Telefon","quantity":1,"unit_price":29999.99,"discount":0.0},{"product_name":"Xiaomi Kablosuz Kulaklık","category":"Aksesuar","quantity":1,"unit_price":799.99,"discount":0.1},{"product_name":"Telefon Kılıfı","category":"Aksesuar","quantity":2,"unit_price":199.99,"discount":0.0}],"shipping_address":{"city":"İstanbul","district":"Beşiktaş","postal_code":"34340"}}
// Sorgu 1: Telefon kategorisinde 30.000 TL üzerinde ürün içeren siparişler
GET ecommerce_orders/_search
{
"query": {
"nested": {
"path": "items",
"query": {
"bool": {
"must": [
{ "term": { "items.category": "Telefon" } },
{ "range": { "items.unit_price": { "gte": 30000 } } }
]
}
},
"inner_hits": {
"_source": ["items.product_name", "items.unit_price"]
}
}
}
}
// Sorgu 2: İndirimli aksesuar içeren İstanbul siparişleri
GET ecommerce_orders/_search
{
"query": {
"bool": {
"must": [
{ "term": { "shipping_address.city": "İstanbul" } },
{
"nested": {
"path": "items",
"query": {
"bool": {
"must": [
{ "term": { "items.category": "Aksesuar" } },
{ "range": { "items.discount": { "gt": 0 } } }
]
}
},
"inner_hits": {
"_source": ["items.product_name", "items.discount"]
}
}
}
]
}
}
}
// Sorgu 3: Kategoriye göre ürün sayısı (nested aggregation)
GET ecommerce_orders/_search
{
"size": 0,
"aggs": {
"items_nested": {
"nested": { "path": "items" },
"aggs": {
"by_category": {
"terms": { "field": "items.category" },
"aggs": {
"avg_price": {
"avg": { "field": "items.unit_price" }
},
"orders_with_category": {
"reverse_nested": {},
"aggs": {
"unique_customers": {
"cardinality": { "field": "customer_name" }
}
}
}
}
}
}
}
}
}10. Java ile Nested Queries
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.query_dsl.*;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.search.*;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class NestedQueryJava {
// Nested query ile arama
public static void nestedSearch(ElasticsearchClient client) throws Exception {
SearchResponse<ObjectNode> response = client.search(s -> s
.index("ecommerce_orders")
.query(q -> q
.nested(n -> n
.path("items")
.query(nq -> nq
.bool(b -> b
.must(m -> m.term(t -> t
.field("items.category")
.value("Telefon")))
.must(m -> m.range(r -> r
.field("items.unit_price")
.gte(JsonData.of(30000))))
)
)
.innerHits(ih -> ih
.name("matched_items")
.size(5)
.source(src -> src
.filter(f -> f
.includes("items.product_name", "items.unit_price")
)
)
)
)
),
ObjectNode.class
);
for (Hit<ObjectNode> hit : response.hits().hits()) {
System.out.println("Sipariş: " + hit.source().get("order_id").asText());
var innerHits = hit.innerHits().get("matched_items");
for (var innerHit : innerHits.hits().hits()) {
System.out.printf(" Ürün: %s | Fiyat: %.2f%n",
innerHit.source().to(ObjectNode.class).get("product_name").asText(),
innerHit.source().to(ObjectNode.class).get("unit_price").asDouble()
);
}
}
}
// Nested aggregation
public static void nestedAggregation(ElasticsearchClient client) throws Exception {
SearchResponse<ObjectNode> response = client.search(s -> s
.index("ecommerce_orders")
.size(0)
.aggregations("items_nested", a -> a
.nested(n -> n.path("items"))
.aggregations("categories", sub -> sub
.terms(t -> t.field("items.category").size(10))
.aggregations("avg_price", avg -> avg
.avg(av -> av.field("items.unit_price"))
)
)
),
ObjectNode.class
);
var nested = response.aggregations().get("items_nested").nested();
var categories = nested.aggregations().get("categories").sterms();
for (var bucket : categories.buckets().array()) {
double avgPrice = bucket.aggregations().get("avg_price").avg().value();
System.out.printf("Kategori: %-15s | Ürün sayısı: %d | Ort. fiyat: %.2f%n",
bucket.key().stringValue(), bucket.docCount(), avgPrice);
}
}
}11. Best Practices
✅ Yapın
| Uygulama | Neden |
|---|---|
Dizi nesneleri arası ilişki varsa nested kullanın | Object type ilişkiyi korumaz |
inner_hits ile eşleşen nested nesneyi gösterin | Kullanıcı hangi nesnenin eşleştiğini bilmeli |
| Nested nesne sayısını sınırlayın | Her nested nesne ayrı Lucene dokümanı |
| Parent-child'ı sadece gerektiğinde kullanın | Nested'dan çok daha yavaş |
routing'i parent-child'da doğru yapın | Aynı shard'da olmalılar |
❌ Yapmayın
| Uygulama | Neden |
|---|---|
nested kullanmadan nested alan sorgulama | Yanlış sonuçlar (cross-object matching) |
| Binlerce nested nesne oluşturmayın | Performans sorunu + bellek |
| Çok seviyeli nested kullanmayın (>2) | Karmaşıklık ve performans |
| Parent-child'ı sorting/aggregation için kullanmayın | Çok yavaş |
12. Yaygın Hatalar
Hata 1: Nested Field'ı Normal Query ile Sorgulama
// ❌ Normal match — cross-object matching riski
GET orders_nested/_search
{
"query": {
"bool": {
"must": [
{ "term": { "items.product": "Mouse" } },
{ "range": { "items.price": { "gte": 40000 } } }
]
}
}
}
// Yanlış sonuç verebilir!
// ✅ Nested query kullanın
GET orders_nested/_search
{
"query": {
"nested": {
"path": "items",
"query": {
"bool": {
"must": [
{ "term": { "items.product": "Mouse" } },
{ "range": { "items.price": { "gte": 40000 } } }
]
}
}
}
}
}Hata 2: Nested Limit'i Aşmak
// Varsayılan limit: 10.000 nested nesne per doküman
// Aşarsanız hata alırsınız
// Limiti artırma (dikkatli kullanın)
PUT my_index/_settings
{
"index.mapping.nested_objects.limit": 20000
}Hata 3: Parent-Child Routing Unutmak
// ❌ Routing olmadan child ekleme
PUT company/_doc/emp1
{
"name": "Ahmet",
"relation": { "name": "employee", "parent": "dept_1" }
}
// Farklı shard'a düşebilir → has_child query çalışmaz
// ✅ Routing ekleyin
PUT company/_doc/emp1?routing=dept_1
{
"name": "Ahmet",
"relation": { "name": "employee", "parent": "dept_1" }
}13. Performans Notları
| İşlem | Nested | Parent-Child |
|---|---|---|
| Index hızı | Normal (biraz yavaş) | Hızlı (ayrı dokümanlar) |
| Sorgu hızı | Hızlı | Yavaş (join maliyeti) |
| Güncelleme | Tüm doküman yeniden index'lenir | Sadece child güncellenir |
| Bellek | Her nested nesne = gizli doküman | Routing tablosu bellekte |
| Uygun | < 100 nested nesne per doküman | Binlerce child |
Nested doküman sayısı hesabı:
1 doküman + 3 nested nesne = 4 Lucene doküman
10.000 doküman × (1 + 3 nested) = 40.000 Lucene doküman
Bu sayı segment merge, heap memory ve cache boyutunu etkiler.
Özet
Object type iç nesneleri düzleştirir — dizi nesneler arası ilişki kaybolur (cross-object matching)
Nested type her iç nesneyi ayrı Lucene dokümanı olarak saklar — ilişki korunur
Nested alanları sorgulamak için `nested` query zorunludur — normal query yanlış sonuç verir
`inner_hits` ile hangi nested nesnenin eşleştiğini görebilirsiniz
`reverse_nested` aggregation ile nested bağlamdan ana dokümana dönülür
Parent-Child (Join) ilişkisi ayrı dokümanlar arasında — bağımsız güncelleme sağlar ama sorgu performansı düşüktür
Nested = sık okunan, az güncellenen iç nesneler; Parent-Child = bağımsız yaşam döngüsü olan ilişkiler
Her nested nesne ayrı Lucene dokümanı demektir — performans etkisini göz önünde bulundurun
AI Asistan
Sorularını yanıtlamaya hazır