Elasticsearch Mapping: text ve keyword, Multi-Field, Dynamic Mapping, nested ve Mapping Değiştirilemezliği
Elasticsearch'te mapping her alanın tipini ve nasıl index'leneceğini belirler; tahmine bırakırsan ISBN sayı, kesirli fiyat tam sayı olur ve hata görmeden yanlış sonuç alırsın, bu yüzden production index'i dynamic: strict ve elle yazılmış bir mapping ister. Gerçek çıktılarla dynamic mapping tuzakları, text ile keyword, title.keyword ve title.folded multi-field'ları, scaled_float fiyat, object ile nested farkı ve .NET'te nested sorgu, flattened, mapping'in neden değiştirilemediği ve alan sınırı.
Sürüm notu: Bu yazıdaki örnekler Elasticsearch 9.5.6, Kibana 9.5.6 Dev Tools, .NET 10 ve Elastic.Clients.Elasticsearch 9.5.3 ile, 2. yazıda kurduğumuz lab'da çalıştırılmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için arama index'ini aceleyle açan bir geliştirici, hiçbir mapping vermeden ilk kitabı gönderiyor. Serializer ISBN'i (International Standard Book Number; kitabın uluslararası standart numarası) bir sayı olarak yazmış, fiyat da tesadüfen tam sayı:
PUT /books-dynamic/_doc/1?refresh=true
{ "isbn": 9781000000016, "title": "İnce Memed", "edition": "1955", "publishedOn": "2021-03-15", "price": 385, "stockQuantity": 42, "authors": [ { "name": "Yaşar Kemal", "role": "author" } ] }
PUT /books-dynamic/_doc/2
{ "isbn": "978-1-00-000002-3", "title": "İnce Memed 2", "price": 395.50 }
PUT /books-dynamic/_doc/3?refresh=true
{ "isbn": "9781000000030", "title": "Kürk Mantolu Madonna", "price": 165.75 }
GET /books-dynamic/_search
{ "query": { "range": { "price": { "gte": 165.5 } } } }
Elasticsearch isbn, edition, publishedOn ve price için hangi tipleri seçer? İkinci kitap index'lenir mi? Üçüncüsünün fiyatı 165,75; son arama onu bulur mu?
Dynamic Mapping'in Tahminleri
Mapping, bir index'teki alanların tipini ve nasıl index'leneceğini söyleyen şema. Vermezsen Elasticsearch ilk gördüğü değere bakıp tahmin ediyor. İlk document'tan sonra:
GET /books-dynamic/_mapping
"isbn" : { "type" : "long" },
"title" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } },
"edition" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } },
"publishedOn" : { "type" : "date" },
"price" : { "type" : "long" },
"stockQuantity" : { "type" : "long" },
"authors" : { "properties" : { "name" : { "type" : "text", ... }, "role" : { "type" : "text", ... } } }
Kurallar basit: JSON sayısı tam sayıysa long, ondalıklıysa float; string tarih biçimine uyuyorsa date, uymuyorsa text ve yanına keyword alt alanı; iç içe JSON object'i object. "1955" string olduğu için text oldu; string içindeki sayıları algılayan numeric_detection varsayılan olarak kapalı. Tahminin sonuçları:
PUT /books-dynamic/_doc/2 →
"type" : "document_parsing_exception",
"reason" : "[1:10] failed to parse field [isbn] of type [long] in document with id '2'. Preview of field's value: '978-1-00-000002-3'"
PUT /books-dynamic/_doc/3 → "result" : "created"
GET /books-dynamic/_search (price >= 165.5)
→ 1 hit: İnce Memed (price 385)
İkinci kitap reddedildi: alanın tipi ilk document'ta long olarak kilitlendi, tireli ISBN artık sayı değil. Üçüncüsü daha sinsi: index'lendi ama aramada yok. Sebebini docvalue_fields gösteriyor:
GET /books-dynamic/_search
{ "query": { "term": { "_id": "3" } }, "docvalue_fields": ["price", "isbn"] }
"_source" : { "isbn" : "9781000000030", "title" : "Kürk Mantolu Madonna", "price" : 165.75 },
"fields" : { "price" : [ 165 ], "isbn" : [ 9781000000030 ] }
_source gönderdiğin JSON'un aynısı, document'ı geri okurken bunu görüyorsun. Arama, sıralama ve aggregation ise index'lenmiş değeri kullanıyor ve o değer 165: long alan 165,75'i sessizce kırptı (bu davranışın adı coerce). Ekranda fiyat doğru görünürken filtre yanlış çalışıyor; bu hatayı fark etmek günler sürebilir. Aynı şekilde "publishedOn": "unknown" gönderen bir document da failed to parse field [publishedOn] of type [date] ile reddediliyor.
dynamic: strict ile Kontrollü Şema
Dynamic mapping denemek için iyi, production index'i için değil. Mapping'i kendin yazıp "dynamic": "strict" verirsen tanımadığı alanı içeren document reddediliyor:
PUT /books-strict
{ "mappings": { "dynamic": "strict", "properties": { "title": { "type": "text" } } } }
PUT /books-strict/_doc/1
{ "title": "Dune", "titel": "typo" }
"type" : "strict_dynamic_mapping_exception",
"reason" : "[1:25] mapping set to strict, dynamic introduction of [titel] within [_doc] is not allowed"
Üç seçenek var: true (varsayılan, yeni alanı ekler), false (yeni alanı _source'ta tutar ama index'lemez, aranamaz) ve strict (reddeder). Bookstore'da strict kullanıyoruz: C# tarafındaki Book record'una eklenen bir property, mapping'e eklenmeden index'e giremesin; yazım hatası bir alan sessizce yeni bir alan yaratmasın.
text ve keyword
Dynamic mapping'in string'lere verdiği ikili yapı, Elasticsearch'teki en önemli ayrımı gösteriyor. text alan analyzer'dan geçer ve term'lere bölünür; full-text arama içindir. keyword alan değeri olduğu gibi tek bir term olarak saklar; birebir eşleşme, filtre, sıralama ve aggregation içindir.
GET /books-dynamic/_search
{ "query": { "term": { "title": "İnce Memed" } } } → 0 hit
{ "query": { "term": { "title.keyword": "İnce Memed" } } } → 1 hit
{ "query": { "term": { "title.keyword": "ince memed" } } } → 0 hit
{ "query": { "match": { "title": "ince memed" } } } → 1 hit
{ "query": { "term": { "title": "memed" } } } → 1 hit
term sorgusu aranan değeri analiz etmiyor. title alanında "İnce Memed" diye bir term yok, analyzer onu ince ve memed olarak saklamış; term "memed" bu yüzden buluyor. title.keyword'de ise tek term var ve büyük/küçük harfe duyarlı. match sorgusu aranan metni alanın analyzer'ından geçirdiği için text alanda doğal sonuç veriyor. Kural: text alanda match, keyword alanda term; bunun tersi neredeyse her zaman hata. Query DSL'in (Domain Specific Language; Elasticsearch'ün JSON tabanlı sorgu dili) ayrıntısı 6. yazıda.
text alanda sıralama ya da aggregation denersen:
GET /books-dynamic/_search
{ "aggs": { "titles": { "terms": { "field": "title" } } } }
"reason" : "Fielddata is disabled on [title] in [books-dynamic]. Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. ..."
Mesaj iki yol öneriyor ve ikincisi (fielddata=true) bir tuzak: inverted index'i heap'te tersine çevirip büyük bellek kullanıyor. Doğru yol sıralanacak ya da gruplanacak alanı keyword (ya da sayı, tarih) yapmak. Kategori, dil, para birimi gibi alanlar kullanıcının yazdığı serbest metin değil, sabit değerler; bunlar baştan keyword olmalı.
Multi-Field: Aynı Değeri Birkaç Şekilde Index'lemek
Başlık hem aranacak hem sıralanacak hem de Türkçe karakter yazmayan kullanıcıya cevap verecek. Tek alan bunların hepsini yapamaz; fields ile aynı değeri farklı şekillerde index'leyebiliyoruz. Bookstore'un mapping'inde başlık şöyle:
"title": {
"type": "text",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 },
"folded": { "type": "text", "analyzer": "folded" }
}
}
folded, settings'te tanımladığımız küçük bir custom analyzer: standard tokenizer, ardından lowercase ve Türkçe karakterleri ASCII (İngiliz alfabesindeki harflerle temel işaretlerden oluşan karakter seti) karşılığına indiren asciifolding. Etkisi _analyze ile görülüyor:
POST /books/_analyze
{ "field": "title.folded", "text": "Kürk Mantolu Madonna, Çalıkuşu, İnce Memed, Işık" }
kurk | mantolu | madonna | calikusu | ince | memed | isik
GET /books/_search
{ "query": { "match": { "title": "kurk" } } } → 0 hit
{ "query": { "match": { "title.folded": "kurk" } } } → 1 hit: Kürk Mantolu Madonna
Document'ı bir kez gönderiyorsun, Elasticsearch üç ayrı index yapısı kuruyor; _source'ta değer tek kez duruyor. Bedeli disk ve index'leme süresi, bu yüzden her alana her varyantı eklemiyoruz. ignore_above: 256 uzun değerlerin keyword'e hiç yazılmamasını sağlıyor; 256 karakterden uzun bir başlıkta title.keyword boş kalır, document reddedilmez. folded burada geçici bir çözüm: asciifolding "ı"yı da "i"ye çeviriyor ve Türkçe'ye özgü küçük harf kurallarını bilmiyor. Bunu 4. yazıda turkish analyzer ve ICU (International Components for Unicode; dillerin sıralama ve harf kurallarını bilen Unicode kütüphanesi) ile düzgün kuracağız.
keyword ile sıralamanın Türkçe'deki bir sonucu da var. Türkçe kitapları title.keyword'e göre sıraladığımızda liste "Aylak Adam, Benim Adım Kırmızı, …, Yaprak Dökümü, Çalıkuşu, İnce Memed, İnce Memed 2, İçimizdeki Şeytan" diye bitiyor: keyword byte sırasına göre sıralıyor, Ç ve İ alfabenin sonuna düşüyor. Türkçe alfabe sırası için ICU collation gerekiyor (4. yazı).
Sayılar, Tarih ve Fiyat
Sayı tipleri arasında seçim, değerin aralığına ve kesinliğine bağlı: stok için integer, ortalama puan gibi az kesinlik isteyen değerler için half_float yeterli. Fiyat ise ayrı bir konu. C# tarafında decimal olan fiyatı float ya da double alanda saklamak, kayan nokta yuvarlamasını aramaya taşır. scaled_float değeri bir çarpanla (scaling_factor) çarpıp long olarak saklıyor; 100 ile fiyat kuruş cinsinden tam sayı oluyor:
PUT /price-test
{ "mappings": { "properties": { "price": { "type": "scaled_float", "scaling_factor": 100 } } } }
PUT /price-test/_doc/1 { "price": 165.755 }
PUT /price-test/_doc/2 { "price": 165.75 }
GET /price-test/_search
{ "docvalue_fields": ["price"], "sort": ["_doc"] }
"_id" : "1", "_source" : { "price" : 165.755 }, "fields" : { "price" : [ 165.76 ] }
"_id" : "2", "_source" : { "price" : 165.75 }, "fields" : { "price" : [ 165.75 ] }
Üç haneli değer en yakın kuruşa yuvarlandı; term 165.76 bu document'ı buluyor. Fiyatı zaten iki ondalıkla tutuyorsan kayıp yok. Fiyatın asıl kaydı SQL Server'daki decimal(10, 2); Elasticsearch'teki değer filtre ve sıralama için bir kopya.
Tarihte biçimi açıkça yazmak iyi bir alışkanlık: "publishedOn": { "type": "date", "format": "yyyy-MM-dd" }. .NET'in DateOnly değeri tam bu biçimde gidiyor (2. yazıda "2019-08-01" olarak gördük). Elasticsearch tarihleri içeride UTC (Coordinated Universal Time; saat dilimi farkı içermeyen referans saat) milisaniye olarak saklıyor; saat dilimi içeren değerlerde (DateTimeOffset) biçimi ona göre seçmek gerekiyor.
object, nested ve flattened
Suç ve Ceza'nın iki kişisi var: yazar Fyodor Dostoyevski ve çevirmen Ada Demir. Kullanıcı "Dostoyevski'nin çevirmen olduğu kitaplar"ı ararsa ne olur? Önce authors alanı varsayılan object tipindeyken:
PUT /authors-object/_doc/1?refresh=true
{ "title": "Suç ve Ceza", "authors": [ { "name": "Fyodor Dostoyevski", "role": "author" }, { "name": "Ada Demir", "role": "translator" } ] }
GET /authors-object/_search
{ "query": { "bool": { "must": [
{ "match": { "authors.name": "dostoyevski" } },
{ "term": { "authors.role": "translator" } }
] } } }
→ 1 hit: Suç ve Ceza
Yanlış sonuç. Lucene'de iç içe object diye bir şey yok; object alan düzleştiriliyor ve document içeride authors.name: [fyodor, dostoyevski, ada, demir], authors.role: [author, translator] gibi iki bağımsız liste olarak saklanıyor. Hangi ismin hangi rolle geldiği bilgisi kayboluyor. nested tip her object'i ayrı, gizli bir Lucene document'ı olarak saklıyor ve nested sorgusu koşulları aynı object içinde arıyor. Bookstore'un books index'inde authors nested:
GET /books/_search
{ "query": { "nested": { "path": "authors", "query": { "bool": { "must": [
{ "match": { "authors.name": "dostoyevski" } },
{ "term": { "authors.role": "translator" } }
] } } } } }
→ 0 hit
Aynı sorgu .NET'te, lab konsolundaki translators komutunda:
SearchResponse<Book> response = await client.SearchAsync<Book>(s => s
.Query(q => q.Nested(n => n
.Path("authors")
.Query(nq => nq.Bool(b => b.Must(
m => m.Match(mt => mt.Field("authors.name").Query(name).Operator(Operator.And)),
m => m.Term(t => t.Field("authors.role").Value("translator"))))))));
dotnet run --project src/Bookstore.SearchLab -- translators ada demir
Translated by 'ada demir': 2
Suç ve Ceza (tr) - Fyodor Dostoyevski [author], Ada Demir [translator]
Hobbit (tr) - J. R. R. Tolkien [author], Ada Demir [translator]
dotnet run --project src/Bookstore.SearchLab -- translators dostoyevski
Translated by 'dostoyevski': 0
nested'in bedeli de görünür:
GET /books/_count → "count" : 36
GET /_cat/indices/books?h=index,docs.count → books 78
36 kitap var ama Lucene seviyesinde 78 document: her kitap artı 42 yazar kaydı. Nested sorgular bu gizli document'ları kitapla birleştirmek zorunda olduğu için normal sorgulardan pahalı, ve bir yazarı güncellemek bütün kitabı yeniden index'lemek demek. Bu yüzden nested'i yalnız "aynı object içinde birden çok alan birlikte eşleşmeli" sorusu gerçekten soruluyorsa kullan. Sadece "yazarlarından biri Tolkien olan kitaplar" aranacaksa object yeterli.
Üçüncü seçenek flattened: anahtarları önceden bilinmeyen bir object'i (kitaba göre değişen özellikler gibi) mapping'e tek bir alan olarak koyar, her yeni anahtar için yeni alan açmaz:
PUT /attr-test
{ "mappings": { "properties": { "title": { "type": "text" }, "attributes": { "type": "flattened" } } } }
PUT /attr-test/_doc/1 { "title": "Dune", "attributes": { "format": "paperback", "pages": "624", "series": "Dune Chronicles" } }
PUT /attr-test/_doc/2 { "title": "The Hobbit", "attributes": { "format": "hardcover", "illustrated": "yes", "pages": "310" } }
{ "query": { "term": { "attributes.format": "paperback" } } } → 1 hit: Dune
{ "query": { "range": { "attributes.pages": { "gte": "400" } } } } → 1 hit: Dune
{ "query": { "range": { "attributes.pages": { "gte": "1000" } } } } → 2 hit: Dune, The Hobbit
Son satır flattened'ın sınırını gösteriyor: bütün değerler keyword gibi string olarak saklanıyor, aralık sorgusu sayısal değil alfabetik çalışıyor ve "624" "1000"den büyük çıkıyor. Analiz yok, skor yok, sayı yok; yalnız birebir eşleşme ve basit filtre için.
Bookstore'un books Mapping'i
Bu kararların hepsi repo'daki search/books.index.json'da:
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"analysis": {
"analyzer": {
"folded": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding"]
}
}
}
},
"mappings": {
"dynamic": "strict",
"properties": {
"isbn": { "type": "keyword" },
"title": {
"type": "text",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 },
"folded": { "type": "text", "analyzer": "folded" }
}
},
"authors": {
"type": "nested",
"properties": {
"name": {
"type": "text",
"fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }
},
"role": { "type": "keyword" }
}
},
"categories": { "type": "keyword" },
"language": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"currency": { "type": "keyword" },
"stockQuantity": { "type": "integer" },
"publishedOn": { "type": "date", "format": "yyyy-MM-dd" },
"averageRating": { "type": "half_float" },
"description": {
"type": "text",
"fields": {
"folded": { "type": "text", "analyzer": "folded" }
}
}
}
}
}
ISBN keyword: sayı gibi görünüyor ama üzerinde aritmetik yapmıyoruz, birebir arıyoruz. categories bir string listesi; Elasticsearch'te her alan birden çok değer alabildiği için ayrı bir array tipi yok. Kategori ve dil keyword, çünkü facet ve filtre olacaklar (9. yazı). Index'i lab konsolu kuruyor: JSON'u Elastic.Transport'un düşük seviyeli API'siyle olduğu gibi gönderiyor, sonra 36 kitabı tek bir bulk isteğiyle yüklüyor:
// Mapping bir JSON dosyasında; düşük seviyeli transport onu olduğu gibi gönderiyor
string definition = await File.ReadAllTextAsync(Path.Combine("search", "books.index.json"));
StringResponse created = await client.Transport.RequestAsync<StringResponse>(
HttpMethod.PUT, $"/{indexName}", PostData.String(definition));
Console.WriteLine($"PUT /{indexName}: HTTP {created.ApiCallDetails.HttpStatusCode} {created.Body}");
if (!created.ApiCallDetails.HasSuccessfulStatusCode)
{
return;
}
IReadOnlyList<Book> books = await BookCatalog.LoadAsync(Path.Combine("data", "books.json"));
BulkResponse bulk = await client.BulkAsync(b => b
.Index(indexName)
.IndexMany(books)
.Refresh(Elastic.Clients.Elasticsearch.Refresh.WaitFor));
Console.WriteLine($"Bulk: {books.Count} books, errors: {bulk.Errors}, took {bulk.Took} ms");
# Dev Tools'ta, 2. yazıdaki deneme index'ini silmek için:
DELETE /books
dotnet run --project src/Bookstore.SearchLab -- create-index
PUT /books: HTTP 200 {"acknowledged":true,"shards_acknowledged":true,"index":"books"}
Bulk: 36 books, errors: False, took 1005 ms
Mapping'i JSON olarak tutmanın sebebi, onun Dev Tools'ta denenen, review'da okunan ve sürümlenen bir sözleşme olması. Client'ın typed mapping API'si de var; C# ile mapping kurmayı 10. yazıda karşılaştırıyoruz. took 1005 ms'in neredeyse tamamı Refresh.WaitFor'un bir sonraki refresh'i beklemesi; bulk ve refresh 5. yazının konusu. 2. yazıda ürettiğimiz API key'in create_index yetkisi olduğu için index'i uygulama kendisi açabildi; silemiyor, DELETE'i Dev Tools'ta elastic ile yapıyoruz.
Mapping Neden Sonradan Değişmiyor?
Bir alanın tipini ya da analyzer'ını değiştirmeyi dene:
PUT /books/_mapping
{ "properties": { "language": { "type": "text" } } }
"reason" : "mapper [language] cannot be changed from type [keyword] to [text]"
PUT /books/_mapping
{ "properties": { "title": { "type": "text", "analyzer": "turkish" } } }
"reason" : "Mapper for [title] conflicts with existing mapper:\n\tCannot update parameter [analyzer] from [default] to [turkish]"
PUT /books/_mapping
{ "properties": { "publisher": { "type": "keyword" } } }
"acknowledged" : true
Yeni alan eklemek serbest, var olanı değiştirmek değil. Sebep inverted index'in kendisi: mevcut document'lar diske eski tipe ve eski analyzer'a göre yazılmış segment'lerde duruyor. Lucene segment'leri değiştirilemez; "language artık text" demek, yazılmış bütün term'leri yeniden üretmek demek. Bunun tek yolu yeni mapping'le yeni bir index açıp veriyi oraya yeniden yazmak (reindex). Uygulamanın index adına doğrudan bağlı olmaması için arada bir alias kullanılıyor: uygulama books'a yazar ve okur, books aslında books-v1'i, sonra books-v2'yi gösterir. Kesintisiz geçiş 11. yazıda. Bu seride 4. yazıda analyzer'ları değiştireceğimiz için index'i orada yeniden kuracağız; lab'da bu basit, production'da planlanması gereken bir iş.
Dynamic mapping'in diğer riski mapping explosion: kullanıcıdan ya da dış sistemden gelen anahtarlar alan olarak eklenirse alan sayısı kontrolsüz büyür ve her alan cluster state'te yer kaplar. Elasticsearch varsayılan olarak bir index'te 1000 alanla sınırlıyor:
PUT /explode-test/_doc/1
{ "attr0": 0, "attr1": 1, ..., "attr1000": 1000 }
document_parsing_exception [1:15794] failed to parse: Limit of total fields [1000] has been exceeded while adding new fields [1001]
Sınırı index.mapping.total_fields.limit ile yükseltmek mümkün ama sorunu çözmez; anahtarları bilinmeyen veri için flattened ya da key/value çiftlerini tutan bir nested yapı doğru araç.
Üretimde Dikkat
Mapping'i kodla birlikte sürümle. books.index.json repo'da duruyor, değişikliği pull request'te görünüyor; Dev Tools'ta elle yapılan değişiklik bir sonraki ortamda unutulur. Mapping değişikliği gerektiren bir sürüm, uygulama deploy'undan önce yeni index'in oluşturulup doldurulmasını gerektirir; bunu deploy planına yaz.
Index template'lerini unutma. Production'da index'leri elle değil, bir index template üzerinden açmak, adı belli bir kalıba uyan her yeni index'in aynı mapping'i almasını sağlıyor (11. yazı). Template olmadan, yanlışlıkla yazılan bir index adına gelen ilk document dynamic mapping'le yeni bir index açar.
Alan sayısını izle. GET /books/_mapping çıktısını büyüten her yeni alan bellekte ve cluster state'te yer tutuyor; özellikle çok kiracılı senaryolarda (Kurumsal Sipariş'te her kurumun kendi alanları gibi) flattened ya da ayrı index stratejisi baştan düşünülmeli.
Sık Düşülen Tuzaklar
Her şeyi text bırakmak. Kategoriye göre filtre, fiyata göre sıralama, dile göre facet text alanda ya çalışmaz ya da yanlış çalışır. Serbest metin olmayan her string keyword olmalı.
ISBN, posta kodu, telefon gibi değerleri sayı yapmak. Baştaki sıfır kaybolur, tire reddedilir, üzerinde zaten toplama yapılmaz; keyword kullan.
İlk document'ın tipi belirlemesine izin vermek. Fiyatı tam sayı olan ilk kitap, sonraki bütün fiyatların kırpılmasına yol açtı ve kimse hata görmedi.
nested'i gereksiz yere kullanmak. Her nested object ayrı bir Lucene document'ı; sorgular yavaşlar, index büyür. "Aynı object'in iki alanı birlikte eşleşmeli" sorusu yoksa object yeterli.
text alanda term sorgusu. Analiz edilmiş term'lerle analiz edilmemiş arama değerini karşılaştırır ve genelde boş döner.
Küçük Bir Deneme
★☆☆ books-dynamic index'ine sırayla üç document gönder: { "title": "Huzur", "pageCount": 420 }, { "title": "Yaban", "pageCount": "abc" } ve { "title": "Yaban", "pageCount": "312" }. Hangisi reddediliyor, string olan "312" neden kabul ediliyor? Aynı üç document'ı ters sırayla, yeni bir index'e gönderince ne değişiyor?
★★☆ books index'inde title.keyword üzerinde bir terms aggregation çalıştır, sonra aynısını authors.name.keyword için dene. İkincisi neden boş bucket listesi döndürüyor? Bir nested aggregation ile düzelt ve adı en çok kitapta geçen kişileri bul; bucket'lardaki doc_count kitap mı sayıyor, yazar kaydı mı?
★★★ authors'ı nested yerine object yapan ama yanına authorsWithRole adında, değerleri "translator:Ada Demir" biçiminde keyword olan bir alan ekleyen alternatif bir mapping yaz. Çevirmen sorusunu bu alanla cevapla ve Lucene document sayısını (_cat/indices) iki tasarım arasında karşılaştır. Hangi sorgular bu tasarımda zorlaşıyor?
Bir sonraki yazıda folded'ı bırakıp Türkçe metni ciddi biçimde ele alıyoruz: analyzer'ın parçaları, _analyze ile her adımı izlemek, turkish analyzer'ın apostrophe ve turkish_lowercase filtreleri, stemmer'ın nerede fazla kestiği, ICU ile Türkçe sıralama ve eş anlamlılar.