Elasticsearch: Arama Sanatı, Tipler ve Yeniden İndeksleme [3/5]

Elasticsearch: The Art of Search, Types and Reindexing [3/5]

Cihat Solak
Intertech

--

Elasticsearch’in güçlü veri işleme ve arama yeteneklerini keşfetmeye devam ediyoruz. Bu üçüncü bölümde, Retrieve Document ve Identifiers gibi temel kavramları ele alacak, ardından Indexing süreçlerinin detaylarına ineceğiz. Aynı zamanda, Data Types’un karmaşık dünyasını keşfedecek ve Flattened ile Nested Veri Tipleri arasındaki farkları anlayacağız.

Elasticsearch, belge tabanlı bir veri tabanıdır ve verilerin depolanması, indekslenmesi ve aranması için kullanılır.

Kibana ve Elasticsearch’i Docker ortamında ayağa kaldırmak için aşağıda yer alan dockor-compose.yml kullanabilirsiniz.

version: "3.8"

services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.7.1
expose:
- 9200 #docker içerisindeki containerların birbirleriyle haberleşmesi
environment:
- xpack.security.enabled=false
- "discovery.type=single-node"
- ELASTIC_USERNAME=elastic
- ELASTIC_PASSWORD=DkIedPPSCb
networks:
- es-net
ports:
- 9200:9200
volumes:
- elasticsearch-data:/usr/share/elasticsearch/data

kibana:
image: docker.elastic.co/kibana/kibana:8.7.1
environment:
- xpack.fleet.enabled: true
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
expose:
- 5601
networks:
- es-net
depends_on:
- elasticsearch
ports:
- 5601:5601
volumes:
- kibana-data:/usr/share/kibana/data

networks:
es-net:
driver: bridge

volumes:
elasticsearch-data:
driver: local
kibana-data:
driver: local

Retrieve Document (Dokümanı Geri Al)

Sorgulama sonucunda metadata ve kaynak bilgisini almak istiyorsanız _doc rotasını, sadece kaynak bilgisini almak istiyorsanız ise _source rotasını kullanmalısınız.

[METADATA] - http://localhost:9200/products/_doc/1

{
"_index": "products",
"_id": "1",
"_version": 3,
"_seq_no": 2,
"_primary_term": 1,
"found": true,
"_source": {
"name": "red pencil",
"price": 33,
"stock_no": "CCI5W0SXFL",
"warehouse": {
"germany": 40,
"turkey": 40
}
}
}

[SOURCE] - http://localhost:9200/products/_source/1

{
"name": "kalem 1",
"price": 33,
"stock_no": "ABC123",
"warehouse": {
"germany": 40,
"turkey": 40
}
}
  • _version: Her güncelleme işlemi gerçekleştirildiğinde artan bir sayıdır.
  • _seq_no: Her işlemde (ekleme, güncelleme, silme) artan bir değere sahiptir
  • _primary_term: Eğer replika shard’lardan biri ana shard olarak atanmışsa, bu değer artar.
  • _id: Kaydedilen verinin id değeri

Eklenen verinin id (identifier) değeri metadata içerisinde bulunmaktadır.

Identifiers (Tanımlayıcılar)

[HTTP PUT] http://localhost:9200/products/_doc/1

1 numaralı id’ye sahip doküman varsa günceller yoksa ekler. Ancak eklenecek doküman mevcutta varsa hata fırlatması için aşağıdaki

[HTTP PUT] http://localhost:9200/products/_create/1 kullanılmalıdır.

Eğer eklenecek dokümana rastgele id verilmesini istiyorsak products/_doc endpoint’ini kullanmalıyız.

[HTTP PUT]: products/_doc/1 mevcutta kayıt varsa günceller, hata vermez.

[HTTP PUT]: products/_create/1 mevcutta kayıt varsa hata verir.

[HTTP POST]: products/_doc id değerinin elasticsearch belirler.

Indexing (indexleme) nasıl meydana gelir?

  • Elasticsearch veriyi indexleme işleminde ilk olarak veriyi belleğe (memory) alır.
  • Bellekte bulunan veri shard içerisindeki segmentlere aktarılır. Segmentlerdeki veri, arama için hazırdır. Eğer elasticsearch’den veri alıyorsanız, alınan veri segmentlerdedir.
  • Son aşamada veri segmentlerden file sisteme geçmektedir.

Bu aşamalarda, memory buffer (bellek arabelleği) içindeki verilerin bekleme süresi varsayılan olarak 1 saniyedir. Bu durumda, eğer 1 saniye içinde 1500 veri gelirse, bu veriler 1 saniye boyunca memory buffer içinde bekletilir. Ancak, bu bekleme süresi Elasticsearch konfigürasyonunda değiştirilebilir.

indexing.buffer.flush.interval: 500ms # Bu örnekte, bekleme süresi 500 milisaniyeye ayarlanmıştır.

Örnek Senaryo

Memory buffer içerisinde verilerin bekleme süresini 15 saniye yaptığımızı varsayalım.

Memory buffer, sorgu işlemlerini hızlandırmak için kullanılan bellek alanıdır.

Ancak farklı senaryolarda gönderdiğim verinin bekleme yapmadan segmentlere kaydedilmesini, yani sorgulama için hazır olmasını istiyorum. Bu durumda veriyi kaydederken query string refresh değeri belirtmeliyiz.

Memory buffer, sorgu işlemlerini hızlandırmak için kullanılan bellek alanıdır.

Refresh parametresi 3 farklı değer almaktadır. Bunlar; true, false (default) ve wait_for.

?refresh=false default değerdir. Veri kaydedilmek istenildiğinde, in memory buffer’daki süre sonunda (default 1 sn) veri segmentlere geçmez. Elasticsearch arka plandaki algoritması belirli bir süre sonra (sistem yoğunluğuna bağlıdır) segmentlere geçer. Dolayısıyla veri kaydederken herhangi bir refresh değeri belirtmediğinizde varsayılan olarak false atandığı için ilgili veriyi hemen sorgulayamazsınız.

?refresh=wait_for istenilen verinin, memory buffer’da belirlenen bekleme süresi (default 1 sn) sonrasında hemen segmentlere yazılması için gerekli değerdir. Veri memory bufferdaki süresini tamamlayıp segmentlere geçiş yaptığında sorgulanabilir durumda olacaktır.

?refresh=true kaydedilen verinin anlık olarak sorgulanabilir olması için atanması gereken parametredir. Sisteme en çok yük getirecek değerdir. Çünkü veri hiç beklemeden memory buffer’a ardından segmentlere kaydedilecektir. Dolayısıyla ne kadar çok true ile işlem yapılırsa o kadar çok sistemi yoracağımız anlamına gelir.

Data Types (Veri Tipleri)

Elasticsearch’te sayılar, metinler, tarihler ve coğrafi veriler gibi çeşitli veri tipleri bulunmaktadır.

Örneğin date (tarih) tipi structered text türündedir. Dolayısıyla parçalanmaz ve analiz sürecinden geçmez. Date tipi default olarak yyyy-MM-dd formatında kabul görüyor fakat bu TR için uygun format değildir. Eğer uygun bir tarih formatı gönderilmezse elasticsearch bunu keyword tipinde belirleyebilir. Keyword tipinde belirleme yapılırsa tarih alanı için büyüktür/küçüktür gibi sorgulamalar yapılamaz.

Structered textlere mapping işlemlerinde dikkat edilmelidir.

Elasticsearch’te sayılar, metinler, tarihler ve coğrafi veriler gibi çeşitli veri tipleri bulunmaktadır.

Keyword özel bir tiptir ve analiz sürecine dahil edilmeden kaydedilmektedir. Örneğin, e-posta adresi keyword türü olduğundan, bu alan analiz sürecinden geçmeyecektir.

Analiz sürecinden geçmesi istemeyen alanların tipi keyword olarak belirlenmelidir. Örneğin banka adı (bankName) alanının tipini keyword olarak belirlerseniz, herhangi bir analiz sürecinden geçmeyerek index verisine kaydedilecektir.

Keyword tipine sahip alanlarda da arama yapabiliriz, ancak veri analiz sürecinden geçmeden bütün olarak kaydedildiği için performansı düşük olacaktır. Sorgulama yeteneği kazandırmak için veriyi bütün olarak kaydedeceğimiz alanların tipinin keyword olması makul bir seçenektir.

Keyword Type (Structered Text) : Bank, Account, Phone Number, Pincodes, SGK No, TC No

Text Type (Knows as full text and unstructered text): blog post, blog title, article, news content

Flattened Data Type (Düzleştirilmiş Veri Tipi)

  • Kaydedilecek veriyi bir bütün olarak keyword tipinde alır.
  • Object veri tipinden farklıdır.
  • Performans odaklı ve bazı senaryolarda kullanılacak tiptir.
flattened veri tipi, karmaşık iç içe veri yapılarını düzleştirerek tek seviyeli bir belgeye dönüştüren bir veri tipidir.

Her insert işleminde değişken şekilde veri (örneğin herhangi bir insert işleminde 3 property, başka birinde 2 property) gönderilecekse flattened tipinin tercih edilmesi uygundur.

Keyword tipine sahip veriler analiz sürecinden geçmez.

flattened veri tipi, karmaşık iç içe veri yapılarını düzleştirerek tek seviyeli bir belgeye dönüştüren bir veri tipidir.

Flattened tip, gönderilecek JSON verinin tamamının tipini keyword olarak belirlemektedir. Bu senaryoda ilgili alan bir bütün olarak flattened olarak işaretlendiği için price ≥ 2000 gibi bir sorgulama yapılamaz.

Eğer price ≥ 2000 gibi bir sorgulama yapılması gerekiyorsa ilgili alanın tipini object olarak belirlemek uygun olacaktır. Flattened, search yeteneğinden mahrum bıraksa da performans anlamında yardımcı olacaktır.

Nested Data Type (İç İçe Veri Tipi)

  • Özelleştirilmiş obje tipidir.
  • Veriler listelendiğinde elasticsearch tek bir obje içerisindeki her bir property’i birbirinden bağımsız olarak düşünür.
  • bank.name = ‘Denizbank’ && bank.city = ‘Ankara’ şeklinde bir sorgu oluşturduğumuzda aşağıdaki sonucu dönmektedir. (Dikkat!) Name alanında denizbank olan, city alanında da ankara olanları listeledi.
[HTTP PUT] - http://localhost:9200/banks/_doc/1

{
"bankId": 1,
"bank": [
{
"name": "Denizbank",
"city": "Istanbul"
},
{
"name": "Garanti",
"city": "Ankara"
}
]
}

Her iki koşulunda tek bir obje içerisinde sağlandığı senaryoyu elde etmek için tipi nested olarak belirlenmelidir. Nested tipi tek bir obje içerisindeki property’leri birbirleriyle ilişkili olacak şekilde değerlendirmektedir.

Obje tipinde her bir property birbirinden bağımsız olarak değerlendirilir. Nested tipinde objeler birbiriyle ilişkili olarak değerlendirilir.

Mapping Nedir? Tipleri Nelerdir?

Kaydedilecek veri tiplerinin belirlediği, şema oluşturma sürecidir. Elasticsearch de dahil olmak üzere birçok NoSQL veritabanında schema oluşturma süreci zorunlu değildir. Şema oluşturmadan da direkt olarak veri kaydı yapılabilir. Ancak şema belirlemediği zaman elasticsearch her bir property’e varsayılan olarak tip ataması gerçekleştirir ve atanan tip doğru olmayabilir.

[HTTP PUT] - http://localhost:9200/banks/_doc/1

{
"name": "Denizbank", // string field
"stars": 5, // number field
"created_date": "1995-05-01", // date field
"location": { // inner field
"city": "Istanbul",
"plate": 34
}
}
  • Index oluşturmayıp, yukarıda yer alan veri kaydedilmek istenildiğinde önce banks isminde yeni bir index oluşturulur.
  • Ardından her bir property’e karşılık gelen değerden property’nin tipi (type) tespit edilir.
  • Son olarak belge (document) kaydedilir.

Bu işlemlerden sonra bir daha şema oluşturma işlemi gerçekleştirilmemektedir.

Best practices açısından şema (schema) belirlemek gereklidir. Çünkü elasticsearch tip belirleme işleminde başarılı olamayabilir. Örneğin ‘stars’ alanı float? double? integer? ne olacak? gibi bir çok durumla karşı karşıya kalabiliriz.

Dynamic ve Explicit olmak üzere 2 tür tip belirleme alternatifi vardır. Elasticsearch tarafından mapping işlemine dynamic, kendimizin yaptığı mapping işlemine ise explicit olarak adlandırıyoruz.

Mapping işleminden sonra tip değişikliği iyi bir şey değildir. Çünkü tip değişikliği yeniden indexleme (re-index) gerektirmektedir Mümkün olduğunca bir tipi belirledikten sonra değişiklik yapmayınız. Ek olarak yeni property/ler eklemekte veya tek tipe sahip property’e ikinci tip eklemekte sakınca yoktur.

Var olan property’e ek bir tip eklediğinizde ya da tip değişikliği yaptığınızda mutlaka re-index yapmalısınız. Çünkü eklenen tipe göre inverted table güncellenmez. Tekrardan indexleme yapmamız gerekmektedir. Yeni bir property eklemekte herhangi bir problem bulunmamaktadır.

Mevcut bir tipi tamamen değiştirmeniz mümkün değildir. Yanlış bir mapping yapıldığında yeni bir index oluşturup, eski kayıtları yeniye tekrardan indexletmelisiniz.

Re-Index Nasıl yapılır?

Mapping işlemlerinden sonra verileri kaydettiğinizde var olan şemada değişiklik yapamayız.

re-index işlemi index içerisindeki veri boyutuna göre dakika, saat ya da günler sürebilir.

Searcing Types (Arama Türleri)

Elasticsearch içerisinde tutulan veri tipine göre structured/unstructured olmak üzere 2 farklı tür bulunmaktadır.

1.1 — Structered Types — A term-level query functionality

created_date, price, is_stock gibi alanları kapsamaktadır. Örneğin date tipindeki created_date alanı ya da boolean tipindeki is_stock alanı structered veri tipine dahildir. Bu tipler üzerinde kolay ve performanslı şekilde büyüktür/küçüktür, tarih aralığı, stoğu olan gibi aramalar yapılabilmektedir.

Score değerine sahip değildir. Çünkü aranan verinin şartları bellidir. Örneğin stok durumu 25'den büyük olanları getir dediğimizde burada şartı sağlayan her veri dönecektir.

1.2 — Unstructured Types — Full text searching functionality

based score (ne kadar yakın?) ve relevancy score (puan ne kadar yüksek olursa alaka düzeyi de o kadar yüksek olur) sahiptir. Elasticsearch’ün asıl gücünün çıktığı yer unstructered tipler üzerinde arama yapabilmesidir. Bu aynı zamanda full-text searching olarak adlandırılmaktadır.

Structered veriler üzerinde arama yapıldığında score değeri bulunmaz. Score değeri kullanıcın aramış olduğu veri ile çıkan sonucun birbiriyle ilişkilendirilmesi üzerine üretilir. En yüksek skora sahip olan veri kullanıcının aramasıyla ilişkili en yüksek değerdir.

Searcing Yapısı Nasıl Çalışır?

Varsayılan (default) olarak 1 primary shard, 1 replika shard olmak üzere 2 shard oluşmaktadır. Primary shard asıl verinin tutulduğu, replikaysa adı üstünde primary shard’ı replike etmektedir.

Index oluştururken shard sayısını belirleyebiliriz.

4 node’a sahip elastic search cluster’ımız bulunduğunu düşünelim. bank-index’ini de 4 shard’lı oluşturduk (4 primary shard).

Örneğin bir arama işlemi başlatıldığında ilgili istek dört node’dan bir tanesi tarafından karşılanır. İlgili node isteği karşıladığı andan itibaren aktif koordinasyon görevini üstlenmektedir. Yani ilgili node önce kendi shard’ında arama yapar ardından diğer nodelarda bulunan ilişkili shardlarına asenkron olarak istek gönderir. Bu işlemler sonucunda ilgili node 4 adet shard’dan topladığı verileri response olarak dönmektedir.

Bir node’da “bank-index” ile ilgili bir shard tutulduğunda ilgili node başka hangi node’ların bu veriyi bu shard’ı tuttuğunu biliyor. Dolayısıyla aktif koordinasyon görevini üstlenen node, alakasız node’lara isteklerde bulunmaz. (örneğin banka aramasında Node 4’e istek yapılmaz.)

Search Rest API

Elasticsearch tarafında arama yapılmak istenildiğinde Query DSL ve Query Requests olmak üzere 2 önemli ayrım bulunmaktadır.

Query DSL (domain spesific language), istek yapılırken search parametreleri request’in gövdesinde gönderilir. Bu yöntem best practicestir.

Query Requests, search parametreleri query string olarak gönderilir.

Search Context

Filter Context ve Query Context olmak üzere iki tip context bulunmaktadır. Eğer context türü belirtilmezse varsayılan olarak Query Context sağlanmaktadır.

Filter context daha çok skor değerinin önemsenmediği boolean, datetime gibi structered veriler üzerinde uygulanmaktadır.

Query context sorgulamasında skor değeri bulunurken filter context sorgulamasında skor değeri bulunmamaktadır. Filter context sorgulamalarında skor değeri bulunmadığı için daha çok structered veriler üzerinde uygulanır. Filter context ile birlikte filter keyword’ünü de belirtirseniz performans için sorgular cachelenir.

Sorgulama sonucundaki verinin ne kadar ilişkili olduğuyla ilgili skor değerine ihtiyaç yoksa filter context kullanarak verinin cache’lenmesini sağlamak performans artışı sağlayacaktır.

Sorgu sonucundaki verinin cachelenmesini isteniyor ve skor değerini önemsenmiyorsa açık şekilde filter ifadesi yazılmalıdır.

Search Response

execution time: Elastic search içerisinden ne kadar sürede ilgili veri alınmış?

how many shard returned: Veri hangi shard’larda bulunmuş?

number of results (hits): Arama sonucuyla eşleşen kayıt sayısı (100)

max_score: Eşleşen kayıtlar içerisinde en yüksek skor değerini alan verinin skor değeri. Elasticsearch default olarak sayfalama yapar ve 100 kaydın hepsini dönmez. 10 tanesini döner. İkinci 10 tanesi talep edilirse size ve from parametresi gönderilmelidir. Default’da 10 tane döner ve en yüksek den en düşüğe skor değerine göre sıralama yapar. Score değerleri elasticsearch’ün sahip olduğu algoritmalara göre belirlenir.

--

--