Elasticsearch Notlarım

Cem Topkaya
46 min readNov 15, 2020

--

Daha çok benim işime yarayan hızlı yardım ve hata giderme notlarımı tutuyorum. Bir düzen içinde adım adım anlatan bir makale olmayacak. Hızlıca söz dizimlerini görmek için https://elasticsearch-cheatsheet.jolicode.com/

  • Güvenlik ve Şifre İşlemleri
  • Elasticsearch Şifresini Değiştirme
  • Kibananın Şifresini Değiştirme
  • Elasticsearch TemelleriVeri Tipleri
  • Veri Tipleri
  • Inverted Index
  • Haritalama (mapping)
  • Elle mapping Oluşturalım
  • dynamic: false > Yeni belgelerin tanımsız alanları mapping içinde oluşturulmaz!
  • dynamic: strict İle yeni alanların güncellenmesinde hata fırlatılır
  • Sadece mapping içindeki alanlara sahip belgelerin kayıt edilmesi
  • Analizörler (standard, whitespace, simple)
  • Analizör Nasıl Seçilir?
  • Kibana İle Konsol’dan İşlemleri Yapmak
  • PUT ve POST Farkı
  • Tüm İndeksleri Listelemek
  • _cat/indices?v
  • Yeni İndeks Oluşturmak
  • Yeni Kayıt Oluşturmak
  • Belgenin Tümünü veya Bir Kısmını Çekmek
  • _source İle Sonuçlarda Belgenin Alanlarını Süzmek
  • filter_path
  • filter_path ve _source Birlikte Kullanıldığında
  • _source & filter_path’in URL de Kullanımı
  • Belgeyi Programlayarak Güncelleyelim
  • Belge ve Endeks Silmek
  • bulk ile Toplu Kayıt Girişi
  • mget İle Birden Fazla Kayıt Çekmek
  • Tüm komutların gist hali
  • Tüm Endeksin Haritasını Çekmek
  • Bir Alanın Haritasını Çekmek (<endeks>/_mapping/field/<alan>)
  • Filtered Query
  • QUERY
  • match ile arama
  • match_phrase ile arama
  • match_all İle Her Şeyi, match_none İle Hiçbir Belgeyi Eşleştirmek
  • TERM Seviyesi Sorgular
  • term Sorguları
  • terms Sorguları
  • null_value Boş değerli alanların endekslenmesi ve aranabilir olması
  • exists İle bol olmayan alanların belgelerini çekmek
  • must_not İle şartın (condition) tersini (değilini) sorgulayalım
  • Küme İşlemleri (aggregation)
  • x İsimli alanda kaç farklı değer var? (aggs-<y>-terms-field:<x>)
  • Tire (hypen, dash) olunca neden kümelenmiyor?
  • Dinamik haritalama ve Standard Analyzer Kullanıldığını Görelim
  • Elle haritalama ve Analizör olarak whitespace Kullanalım
  • _reindex İle Eski Endeksten Yenisine Veri Aktarmak
  • {“statusCode”:502,”error”:”Bad Gateway”,”message”:”Client request timeout”}
  • Metrik Kümeleme
  • Sadece erkeklerin ortalama yaşını bulalım
  • Tüm istatistik çıktıları görmek için “stats
  • Dizi tipinde bir alanın değerlerinde veya basit bir alanın alabileceği değerlere göre süzmek
  • field_data İle Gruplama
  • size İle 0–10 Arasında Gelecek Sonuçların Sayısı Getirme
  • Hiç Sonuç Getirme, Verinin Verisini (metadata) Getir
  • Komut Çalıştırma (SCRIPT)
  • Sıralama (Sorting)
  • Sonuçların Sıralanması (sorting hits)
  • Kümeleme Sonuçlarının Sıralanması (sorting aggregation results)

Güvenlik ve Şifre İşlemleri

Elasticsearch Şifresini Değiştirme

Kibananın Şifresini Değiştirme

Elasticsearch için tanımladığınız şifreyi Kibana için de kullanmak isterseniz basitçe şunu yapmalısınız:

C:\> cd C:\kibana-7.9.3-windows-x86_64\binC:\...\bin>kibana-keystore add elasticsearch.username
Enter value for elasticsearch.username: *******
C:\...\bin>kibana-keystore add elasticsearch.password
Enter value for elasticsearch.password: *******
C:\...\bin>kibana-keystore list
elasticsearch.username
elasticsearch.password

Ref: https://www.elastic.co/guide/en/kibana/7.10/secure-settings.html

Elasticsearch Temelleri

Veri Tipleri

https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html

Inverted Index

Elasticsearch içine basılan tüm belgelerin içindeki kelimeler (kelimeleri ayrıştırmak için tokenizer kullanılır) bir liste içinde tekrarsız şekilde tutulur. Hangi kelimenin hangi belgelerde geçtiği işaretlenir. Böylece inverted index oluşturulmuş olur. Bir sorgu yapıldığında, sorguda geçen kelimeler hangi belgelerde geçmişse, geçme sıklığına ve kelimelerin arama cümlesindeki yakınlıklarına en yakın sonuçlar üst sırada gelecek şekilde puanlanır. Bu fikir adamların aklına gelince oturup bunu programlamışlar ve karşımıza bizim aramalarımızı kolaylaştıran ve sonuçların keskinliğini arttıran elasticsearch çıkmış. Peki belgelerin analizi nasıl gerçekleşiyor ona bakalım.

Haritalama (mapping)

Hiç mapping oluşturmadan, doğrudan kayıtları sadece endeks alanını tayin ederek girdiğinizde “dinamik haritalama” yapılır Elasticsearch tarafından. Dinamik haritlamada;

  • girilen verilerin tiplerini, endekslenip endekslenmeyeceklerini (ki elasticsearch varsayılan olarak her alanı endeksler),
  • hangi sorguya göre aranabileceğini (tam metin araması -full text search-, terim seviyesi -term level- vs.) otomatik olarak yapar.

Ancak elle bu ayarları oluşturmamız da mümkün ;)

Endeks bir kez oluştuktan sonra tekrar oluşturulması ve verilerin yeni endekse göre yapılandırılması mümkün değildir (bazı güncellemeleri yapabilirsiniz ama örneğin kayıt geldiyse endekse, alanın tipini değiştiremezsiniz). Tüm endeks silinip, yeniden yaratılıp, kayıtların tekrar girişi yapılabilir. Veya yeni endeks yaratılıp eskisinden _reindex ile verilerin kopyalanmasını sağlayabilirsiniz (timeout alırsanız paniklemeyin!).

Önce dinamik olarak mapping oluşacak şekilde kayıt girişi yapalım ve oluşan haritalamayı görelim:

DELETE logsPOST logs/_doc
{
"Log_Time" : "2020/12/08 09:00:06",
"Level" : "ALARM_NORMAL",
"ID" : "140589228087040",
"File" : "Service.cpp:169->OnDatabaseStatusNotification"
}
GET logs/_mapping# Aslında aşağıda dönen bir şey yok. Azıcık Elasticsearch'e alışınca tipik mapping elemanları olarak aşina olacaksınız. Gözden geçirin ama şifre çözecekmiş hissine kapılıp takılıp kalmayın. Ekstra alanlar var ama dikkatinizi çekmesini istemediğim yerleri ... ile maskeledim#{
#"logs" : {
# "mappings" : {
# "properties" : {
# "File" : {
# "type" : "text",
# "fields" : {..}
# },
# "ID" : {
# "type" : "text",
# "fields" : {..}
# },
# "Level" : {
# "type" : "text",
# "fields" : {..}
# },
# "Log_Time" : {
# "type" : "date",
# "format" : "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis"
# }
# }
# }
#}
#}

Elle mapping Oluşturalım

Önce endeksi silelim ki tekrar oluşturabilelim.

DELETE logs

File alanını haritalamayalım ve kayıt eklerken oluştuğunu görelim:

PUT logs
{
"mappings": {
"properties": {
"ID": {
"type": "text"
},
"Level": {
"type": "text"
},
"Log_Time": {
"type": "date",
"format": "yyyy/MM/dd hh:mm:ss"
}
}
}
}

Şimdi _mapping ile oluşan belge haritasına bakalım ve kayıt ekledikten sonra File alanının haritaya eklendiğini görelim:

{
"logs" : {
"mappings" : {
"properties" : {
"ID" : {
"type" : "text"
},
"Level" : {
"type" : "text"
},
"Log_Time" : {
"type" : "date",
"format" : "yyyy/MM/dd hh:mm:ss"
}
}
}
}
}

Şimdi kayıt ekleyelim ve File alanının oluştuğunu _mapping ile görelim:

POST logs/_doc
{
"Log_Time" : "2020/12/08 09:00:06",
"Level" : "ALARM_NORMAL",
"ID" : "140589228087040",
"File" : "Service.cpp:169->OnDatabaseStatusNotification"
}
# Kendi kafasından bir ID değeri oluşturdu
# {
# "_index" : "logs",
# "_type" : "_doc",
# "_id" : "bmMfaHYBDOf7SY4fE4Di",
# "_version" : 1,
# "result" : "created",
# "_shards" : {
# "total" : 2,
# "successful" : 1,
# "failed" : 0
# },
# "_seq_no" : 0,
# "_primary_term" : 1
# }
GET logs/_search#{
# "took" : 610,
# "timed_out" : false,
# "_shards" : {...},
# "hits" : {
# "total" : {
# "value" : 1,
# "relation" : "eq"
# },
# "max_score" : 1.0,
# "hits" : [
# {
# "_index" : "logs",
# "_type" : "_doc",
# "_id" : "bmMfaHYBDOf7SY4fE4Di",
# "_score" : 1.0,
# "_source" : {
# "Log_Time" : "2020/12/08 09:00:06",
# "Level" : "ALARM_NORMAL",
# "ID" : "140589228087040",
# "File" : "Service.cpp:169->OnDatabaseStatusNotification"
# }
# }
# ]
# }
#}
# Haritaya bakalım:
GET logs/_mapping
# File alanı dinamik olarak oluşturuldu ve elle oluşturduklarımızdan biraz daha detaylı olmuş:
#{
# "logs" : {
# "mappings" : {
# "properties" : {
# "File" : {
# "type" : "text",
# "fields" : {
# "keyword" : {
# "type" : "keyword",
# "ignore_above" : 256
# }
# }
# },
# "ID" : {
# "type" : "text"
# },
# "Level" : {
# "type" : "text"
# },
# "Log_Time" : {
# "type" : "date",
# "format" : "yyyy/MM/dd hh:mm:ss"
# }
# }
# }
# }
#}

dynamic: false > Yeni belgelerin tanımsız alanları mapping içinde oluşturulmaz!

Buraya kadar dinamik alan oluşturulmasını engellememiştik haydi şimdi engelleyerek bakalım ve mapping içinde yeni bir File alanının oluşmadığını görelim:

DELETE logs
PUT logs
{
"mappings": {
"dynamic": false,
"properties": {
"ID": {
"type": "text"
},
"Level": {
"type": "text"
},
"Log_Time": {
"type": "date",
"format": "yyyy/MM/dd hh:mm:ss"
}
}
}
}
GET logs/_mapping
# dynamic özelliği false olarak geldi:
#{
# "logs" : {
# "mappings" : {
# "dynamic" : "false",
# "properties" : {
# "ID" : {
# "type" : "text"
# },
# "Level" : {
# "type" : "text"
# },
# "Log_Time" : {
# "type" : "date",
# "format" : "yyyy/MM/dd hh:mm:ss"
# }
# }
# }
# }

# Şimdi kayıt olmadığını _count ile görelim ve bir kayıt ekleyerek mapping içinde File oluştmadığını görelim (dynamic:false):
GET logs/_count
#{
# "count" : 0,
# "_shards" : {
# "total" : 1,
# "successful" : 1,
# "skipped" : 0,
# "failed" : 0
# }
#}
POST logs/_doc
{
"Log_Time" : "2020/12/08 09:00:06",
"Level" : "ALARM_NORMAL",
"ID" : "140589228087040",
"File" : "Service.cpp:169->OnDatabaseStatusNotification"
}
GET logs/_mapping
#{
# "logs" : {
# "mappings" : {
# "dynamic" : "false",
# "properties" : {
# "ID" : {
# "type" : "text"
# },
# "Level" : {
# "type" : "text"
# },
# "Log_Time" : {
# "type" : "date",
# "format" : "yyyy/MM/dd hh:mm:ss"
# }
# }
# }
# }
#}

dynamic: strict İle yeni alanların güncellenmesinde hata fırlatılır

Strict olarak işaretlendiğinde mapping içinde olmayan yeni bir alan ile mapping güncellenmek isteniyor ama Elasticsearch hata fırlatıyor:

DELETE logs
PUT logs
{
"mappings": {
"dynamic": "strict",
"properties": {
"ID": {
"type": "text"
},
"Level": {
"type": "text"
},
"Log_Time": {
"type": "date",
"format": "yyyy/MM/dd hh:mm:ss"
}
}
}
}
POST logs/_doc
{
"Log_Time" : "2020/12/08 09:00:06",
"Level" : "ALARM_NORMAL",
"ID" : "140589228087040",
"File" : "Service.cpp:169->OnDatabaseStatusNotification"
}
# File alanı mapping içinde tanımlı değil ancak
# belgenin kayıt edilmesi sırasında dynamic: strict olarak
# işaretlendiği için mapping güncellenmek isteniyor ancak
# hata fırlatılıyor.
#{
# "error" : {
# "root_cause" : [
# {
# "type" : "strict_dynamic_mapping_exception",
# "reason" : "mapping set to strict, dynamic introduction of [File] within [_doc] is not allowed"
# }
# ],
# "type" : "strict_dynamic_mapping_exception",
# "reason" : "mapping set to strict, dynamic introduction of [File] within [_doc] is not allowed"
# },
# "

Sadece mapping içindeki alanlara sahip belgelerin kayıt edilmesi

Yeni belge eklenirken şemamızda olmayan alanların girişini engellemek istiyorsak dynamic: strict kullanarak hata fırlatır ve sadece _mapping de tanımlı alanlara sahip belgelerin endekse kaydına müsade etmiş oluruz!

{“statusCode”:502,”error”:”Bad Gateway”,”message”:”Client request timeout”}

Elimdeki endeksin içinde 313 bin kayıt var ve bu kayıtları, tiplerini daha düzgün tanımladığım (daha iyi bir mapping’e sahip) yeni bir endekse kaydederek daha düzgün sorgular yazmak istedim. Ben Kibanadan bu isteği gönderdim ama iş uzun zaman aldığı için Kibananın beklediği zamanı aştı (timeout) yanıt alamadı ve bu hatayı verdi:

{“statusCode”:502,”error”:”Bad Gateway”,”message”:”Client request timeout”}

Aktif bir _reindex sürecinin durumunu görebilmek için şu komutu bir çok defa çalıştırıp, kaç kayıttan kaçının kopyalandığını görebilirsiniz:

GET _tasks?actions=*reindex&detailed

Analizörler

Benim anlamsız !!! gözyaşlarım ve ilkokul 3 aşkım :) ile anılarım.

Cümlesini içeren bir belgeyi Elasticsearch analiz edecek ki endekslesin ama nasıl analiz edecek ona bakalım:

  • Karakter süzgecinden geçirecek. Sayıları yazıya çevirecek (3 yerine üç), gereksiz karakterleri yok edecek (!!! veya :) veya html etiketleri vs.)
  • Tokenizer ile cümlede geçen kelimeleri ayrıştıracak. Cümlenin diline göre bazen tire işaretiyle (orta tire diye bir şey yok!) kelimeler bir araya geliyor (örn. use-case veya ‘Ankara-Kars tren yolu..’) veya kelimeler boşluk karakteriyle ayrılıyor olabilir. Kelimeleri ayrıştırırken hangi tokenizer kullanılacağını seçmemiz veya kendimize göre analizör yazmamız gerekir.
  • Token süzgeciyle kelimeleri ayrıştırdıktan sonra tek başına anlamları olmayan (stop words) kelimeleri atmamız gerekir. Örneğin ile, ve, de, ke, and, the, with gibi.

Elasticsearch beraberinde çeşitli analizörler ile gelir.

standard analizör ile bir çok dilde genel amaçlı kullanılabilen, noktalama işaretlerini yok sayıp(mesela parantez işaretleri kaldırıldıktan sonra kelimeleri ayrıştırır), küçük harfler halinde terimlere ayrıştırabiliriz.

Whitespace analizörü ise boşluk karakterine göre cümleyi analiz ederek, işaretleme karakterlerini ihmal etmeden, tire ile birleşen kelimeler ayrıştırılmadan, kelimelerin büyük ya da küçük harfe dönüştürülmeden ayrıştırır.

Bir GUID değeri standart analizör ile ayrıştırdığımızda tire karakterlerinden ayırarak her bir bloğu ALPHANUM olarak ayrıştırırken whitespace analizörü sayesinde bir bütün olarak tokenlaştırabilirsiniz.

a0fa3cb5-ddfd-4d31-bab7–79f35f7a8d62

simple Analizörüyse sayı olmayan tüm metinleri küçük harf ile ayrıştırır.

Analizör Nasıl Seçilir?

Bir alanın hangi tip analizör ile ayrıştırılacağını haritalama sırasında verebiliriz:

Tarih bilgisini formatlı olarak parçalamak istersek format alanını haritalamada kullanabiliriz. Aşağıdaki örnekte tarih olarak parse edilecek post_date alanının hangi formata göre ayrıştırılacağı format: YYYY-MM-DD ile verilmiş:

Format bilgisi dışında gelen belge kaydı hata üretecektir:

Hata:

_source Bilgisini Elasticsearch Üstünden SaklaMAMAK

Verileri saklamamak öncelikle size ne kaybettirir ona bakalım:

  • Update, update_by_query ve reindex API’lerini kullanamazsınız.
  • Anında vurgulayarak çıktıları çekmek (highlight).
  • Eşlemeleri (mappings) veya analizi değiştirmek veya bir dizini yeni bir ana sürüme yükseltmek için bir Elasticsearch indeksinden diğerine yeniden indeksleme yeteneği (reindex).
  • Dizin zamanında kullanılan orijinal belgeyi görüntüleyerek sorgularda veya toplamalarda hata ayıklama yeteneği.
  • Potansiyel olarak gelecekte, dizin bozulmasını otomatik olarak onarma yeteneği.
https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html

Disk alanından kaygılanıyorsanız bize sıkıştırma seviyesini değiştirerek verileri saklamamız salık veriliyor Elasticsearch tarafından.

Şimdi gelelim neden yapmak isteriz ve nasıl yapabiliriz kısmına.

Elastic varsayılan olarak gelen belgeyi endekslerken (tersine dizinlenmiş olarak kelimelerin sıklığı ve belgede geçişine göre endeks oluştururken) aynı zamanda _source alanı içinde belgenin kendisini de tutar. Eğer veriyi hem harici bir veritabanında tutup hem endekslenmesi için Elasticsearch içine gönderdiyseniz veriyi ikinci kez depoluyor olacaksınız. Buda endeksinizin şişmesine neden olacak. Dilerseniz _source alanını elastic üstünde tutmayarak sadece orjinal verilerinizin olduğu veritabanında erişebilecek ID alanını Elasticsearch üstünde tutmak isterseniz aşağıdaki gibi _source.enabled: false ve ilgili ID alanı için store: true değerlerini haritalamanızda belirtebilirsiniz.

Varsayılan olarak _source alanı tutulmazken özellikle şu şekilde bir aramayla /{belge_id}?field=user_id depolanmış veriye ulaşabilirsiniz.

_aliases İle Birden Fazla Endeksi Birbirine Bağlı Şekilde Sorgulamak

En genel haliyle günlük alınan logların kaydını Elasticsearch üstünde tarih bilgisi son ekiyle oluşturuyoruz.

Yukarıdaki ekran çıktısında Ağustos ayının ilk gününe ait günlük kayıtları bir endeks üstünde oluşmuşken, sonraki günün kayıtları eventlog-2014–08–02 isimli endeks üstünde oluşuyor. Bu endekslerin her birinin içinde sorgu çalıştırıp sonuçlarını birleştirmek yerine _aliases ile bir takma ad altında tüm endeskleri birleştirerek sorgumuzu takma ad üstünde gerçekleştirebiliriz.

POST /_aliases
{
"actions" : [
{ "add": { "index" : "eventlog-2014–08–01", "alias": "gunluk" } },
{ "add": { "index" : "eventlog-2014–08–02", "alias": "gunluk" } },
{ "add": { "index" : "eventlog-2014–08–03", "alias": "gunluk" } }...
]
}

Veya

POST /_aliases
{
"actions" : [
{ "add": { "indices" : [
"eventlog-2014–08–01",
"eventlog-2014–08–02",
"eventlog-2014–08–03"
], "alias": "gunluk" } }
]
}

Veya

POST /_aliases
{
"actions" : [
{ "add": { "index" : "eventlog*", "alias": "gunluk" } }
]
}

Böylece sorgumuzu takma ad üstünde çalıştırabiliriz:

GET gunluk/_search

Kibana İle Konsol’dan İşlemleri Yapmak

Kibana kurulumuyla ilgili çeşitli makaleler bulabilirsiniz.

PUT ve POST Farkı

PUT ile yeni kayıt yaratıp güncelleyebilirsiniz,
POST ile sadece güncelleyebilirsiniz.

Tüm İndeksleri Listelemek

http://localhost:9200/_cat/indices

_cat/indices

_cat/indices?v

http://localhost:9200/_cat/indices?v&pretty
curl -XGET “http://localhost:9200/_cat/indices"

pri: shard sayısı

rep: replika sayısı

Her indeks ön tanımlı olarak 5 shard ve 1 replika ile gelir.

docs. count: İndeks içinde kaç kayıt olduğunu gösterir

health: rep sayısı 0 olanlar için health değeri green!
Çünkü ekstra bir düğüm (node) üstünde saklanmaları gerekmediğini rep:0 ile vermişiz.
Ancak health değeri yellow olanların replika sayıları 1 olduğundan ve sadece bir elasticsearch düğümü (node) çalıştırdığımızdan dolayı yedeklenmediklerini ve bu yüzden sarı işaretlendiğini görüyoruz. Yani tüm istekler sadece birincil shard üstünde çalıştırılacak demektir.

store. size: saklanan verilerin indeks içindeki kapladığı alanı verir. Elasticsearch belge temellidir (document base) ve içinde hiç belge olmayan indekslerin 280 byte olduğunu görüyoruz. Oysa .kibana_1 indeksinin yaklaşık 21 megabyte yer kapladığını ve içinde 32 belge olduğunu görüyoruz

Yeni İndeks Oluşturmak

PUT metoduyla ister sadece indeks adı isterseniz veri yapısını (mapping) da girebilirsiniz

curl -XPUT “http://localhost:9200/yeni_index"

Yeni Kayıt Oluşturmak

Elasticsearch 6 öncesindeki yapıların İlişkisel Veri tabanı Yönetim Sistemlerindeki (RDBMS) karşılıkları aşağıdaki gibiYDİ.

Aşağıdaki hatayı görünce “hani type bilgisi” diye sordum ve izi takip edince gördüm ki; ARTIK TYPE YOK!

Yukarıdaki ekran çıktısında iki şey dikkatinizi çekecektir:

  1. Kırmızıyla belirttiği artık geçersiz olan belgenin ID bilgisinin şu şekilde girmemizi istemesi: yeni_index/_doc/id_değerimiz veya yeni_index/_create/id_değerimiz . Type bilgisi nereye gitti diye okumak isterseniz (google çevirisi de epeyce anlaşılır) bu adrese alayım sizi.
PUT yeni_index/yeni_tablo/1?pretty
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz"
}
veya curl ifadesiyle:curl -XPUT "http://localhost:9200/yeni_index/yeni_tablo/1" -H 'Content-Type: application/json' -d'
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz"
}'

Peki komutu bu şekilde çalıştırınca ne oluyor arkada?

{
"yeni_index" : {
"aliases" : { },
"mappings" : {
"properties" : {
"email" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"id" : {
"type" : "long"
},
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"username" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
},
"settings" : {
"index" : {
"creation_date" : "1605429709442",
"number_of_shards" : "1",
"number_of_replicas" : "1",
"uuid" : "2blrcK6vTo6qfzplep0a6w",
"version" : {
"created" : "7090399"
},
"provided_name" : "yeni_index"
}
}
}
}
http://localhost:9200/yeni_index

yeni_tablo bilgisi ihmal ediliyor ve her şey yeni_index’e bağlanıyor. Özetle tavsiyesinde diyor ki:

type artık yok çünkü ilişkisel veri tabanlarıyla benzeşim yaparak 7. sürüme kadar geldiğimiz bu noktada her şeyi index içine bağlamak daha doğru olacaktı. Yok siz illa type kullanmak isterseniz, kaydedeceğiniz belgelere bir type bilgisi ekleyin ve hangi tablonun kaydı olduğunu orada belirtin. Sorgu çaktığınızda filter kısmına zaten endekslediğimiz type alanını yazın:

GET twitter/_search
{
"query": {
"bool": {
"must": {
"match": {
"user_name": "kimchy"
}
},
"filter": {
"match": {
"type": "tweet"
}
}
}
}
}

Buna göre belge yaratmanın Elasticsearch 7 sürümüne göre:

curl -XPUT “http://localhost:9200/yeni_index/_doc/1?pretty" -H ‘Content-Type: application/json’ -d’{ “id”: 1, “name”: “Leanne Graham”, “username”: “Bret”, “email”: “Sincere@april.biz”}’

_type kısmına artık _doc yazıyor. _id bilgisini biz 1 olarak girdiğimiz için 1 oldu, eğer /yeni_index/_doc deseydik kendi bir değer üretecekti. ID değeri 1 olan belgeyi yeni oluşturduğumuz için result bilgisi created değeri alıyor. Ve yine ilk kez yaratıldığı için _versiyon bilgisi 1. Eğer güncellersek bu değer her güncellemede 1 artacak.

_shards olarak 1 adet primary ve 1 adet replica olmak üzere 2 shard tanımlıydı ancak sadece 1 Elasticsearch düğümü çalıştığı için health bilgisinde yellow yazıyordu. Şimdi bu kaydı girdiğimizde kaç shard bu kayıtla ilgili olmalıydı ve kaçı başarılı kaçı başarısız olarak bu işlemden etkilendi bilgisini görüyoruz:

GET _cat/indices?v veya http://localhost:9200/_cat/indices?v
1 pri + 1 rep Olacak şekilde 2 shard bu endeks ile ilgili ancak sadece 1 tanesi başarıyla (successful) bu kaydı oluşturabilmiş. Diğeri aktif ve çalışıyor olmadığı için başarısız (failed) bile olamamış.
curl -XPUT "http://localhost:9200/yeni_index/_doc/1?pretty" \
-H 'Content-Type: application/json' \
-d'{ \
"id": 1, \
"name": "Leanne Graham", \
"username": "Bret", \
"email": "Sincere@april.biz" \
}'

Oluşan kayıtların tümünü listelemek için:

http://localhost:9200/yeni_index/_search

Belgenin Tümünü veya Bir Kısmını Çekmek

GET test/_search

_type: 7 ile artık _doc olarak kaydediyoruz
_id: belgenin eşsiz değeri
_version: 1, ilk kez eklenmiş ve güncellenmemiş
found: belge bulunduysa true döner
_source: belgenin son halini tam olarak görüyoruz

Olmayan ID’li kayıt sorgulandığında:

Olmayan type sorgulandığında (artık type kullanma be kardeşim):

Belgeyi değil, belgenin meta datasını (belgenin üst verisini) getirmek için sorgu sonuçlarındaki belgeleri ihmal etmek için _source=false yazıyoruz. Zaten sonuç JSON verisine bakarsanız _source kısmında kayıtları görürsünüz ve false dediğiniz zaman kayıtlara dair hiçbir şeyi görüntülemezsiniz.

GET yeni_index/_doc/1?_source=false veya curl -XGET “http://localhost:9200/yeni_index/_doc/1?_source=false"

_source İle Sonuçlarda Belgenin Alanlarını Süzmek

Belgenin bazı özelliklerini getirmek için _source=alan1,alan3…

curl -XGET "http://localhost:9200/yeni_index/_doc/1?_source=id,email"
GET yeni_index/_doc/1?_source=id,email

_source hem URL üstünde hem de isteğin gövdesinde yer alabilir (not: filter_path sadece URL üstünde yer alır).

filter_path

*** Tüm REST API’leri, Elasticsearch tarafından döndürülen yanıtı azaltmak için kullanılabilecek bir filter_path parametresi kabul eder. Bu parametre, nokta gösterimi ile ifade edilen virgülle ayrılmış bir filtre listesi alır. ** ile iç içe her şeyi, * ile bulunduğu hizada her şeyi, kelime* ile başında “kelime” olan her şeyi seçer.

filter_path ve _source Birlikte Kullanıldığında

filter_path ile _source birlikte kullanıldığında önce filter_path çalışır ve ardından eğer _source içinde belirtilen alanlar varsa görüntülenir. Eğer filter_path sonucunda bir çok alan dönüyor ve _source içindekiler ile daha da süzülüyorsa bu kez sadece _source içindeki alanlar görüntülenir.

filter_path ile süzülen alanların içinden _source ile sadece ikisini çekiyoruz

Bir filter_path Örneği Daha

GET kibana_sample_data_ecommerce/_search?filter_path=hits.total,**.customer_*,**.products*.p*
{
"query": {
"match": {
"customer_full_name": "Eddie Underwood"
}
}
}

Eğer sadece sonuç kayıtları görmek üst veriyi görmemek isterseniz

hits.hits._source

Sadece sonuç kayıtlarını çekelim:

GET test/_search?filter_path=hits.hits._source

_source & filter_path’in URL de Kullanımı

_source isteğin gövdesinde de kullanılabilirken filter_path sadece URL üstünde tanımlanabilir. Burada hem _source hem de filter_path’in URL de kullanımını göreceğiz. Sadece sonuç kayıtlarında bazı alanları ve metadata içindeki istediğimiz bilgileri çekelim:

GET test/_search?filter_path=took,hits.total.relation,hits.hits._source&_source=age

Tüm Belgeyi veya Bir Kısmını Güncellemek

Tüm Belgeyi Güncelleme

# Belgenin tüm alanlarını güncellemek
PUT yeni_index/_doc/1
{
"id" : 1,
"name" : "Cem Topkaya",
"username" : "cemmmm",
"email" : "ben@cemtopkaya.com"
}

veya

curl -XPUT "http://localhost:9200/yeni_index/_doc/1" -H 'Content-Type: application/json' -d'{
"id" : 1,
"name" : "Cem Topkaya",
"username" : "cemmmm",
"email" : "ben@cemtopkaya.com"
}'

Sonuçta güncellemenin başarılı olduğu (result) shard sayısı (_shard.successful) ve _version bilgisi 1 artmış olacak.

curl -XGET “http://localhost:9200/yeni_index/_doc/1"

Kısmi Güncelleme

https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html

Kısmi güncellemede POST metodunu kullanacağız.

Belge oluştururken create kullanılırken güncellemede update öneriliyor.

curl -XPOST "http://localhost:9200/yeni_index/_update/1" -H 'Content-Type: application/json' -d'
{
"doc": {
"name" : "Canan Topkaya",
"username" : "cano"
}
}'

veya

# Belgenin bazı alanlarını güncelleyelim
POST yeni_index/_update/1
{
"doc": {
"name" : "Canan Topkaya",
"username" : "cano"
}
}

Belgeyi Programlayarak Güncelleyelim

İsim bilgisini anonim olarak değiştirip eposta adresini yıldız karakteriyle gizleyelim

# Belgeyi programlayarak güncelleyelim
POST yeni_index/_update/1
{
"script": "ctx._source.name = 'anonim'; ctx._source.email = '***'"
}
veyacurl -XPOST "http://localhost:9200/yeni_index/_update/1"
-H 'Content-Type: application/json'
-d '{
"script": "ctx._source.name = '\''anonim'\''; ctx._source.email = '\''***'\''"
}'

Belge ve Endeks Silmek

Belge Silmek

https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html

ID değeri bilinen bir belgeyi silmek için DELETE metodu kullanılır.

DELETE yeni_index/_doc/1
curl -XDELETE "http://localhost:9200/yeni_index/_doc/1"

Silerken bile versiyonunu 1 arttırdığına dikkat

Sildiğimiz belgenin olmadığını GET ile sorgulayarak görebiliriz:

Bu sorgu curl ile curl -XGET "http://localhost:9200/yeni_index/_doc/1" yazılabilirken curl -XHEAD "http://localhost:9200/yeni_index/_doc/1" ile de yapılır ve 404 hatası alınarak görülebilir:

-i bayrağıyla http cevabının başlığını (header) görmemizi sağlıyor

Aynı şekilde eğer kayıt varsa curl sonucu HTTP 200 durum koduyla dönecekti:

curl -i -XHEAD “http://localhost:9200/yeni_index/_doc/1"

Endeks Silmek

Önce silmek için yaratalım

PUT silinecek_endeks

Endeksleri listeleyelim

GET _cat/indices?v

Ve silinecek_endeks’i silelim

Endeksi görüntülemek istediğimizde “böyle bir endeks yoktur” mesajını alalım:

# ENDEKS silmek için yaratalım
PUT silinecek_endeks
# Endeksleri listeleyelim
GET _cat/indices?v
# Endeksi Silelim
DELETE silinecek_endeks
# Endeksi görüntüleyelim
GET silinecek_endeks

bulk ile Toplu Kayıt Girişi

https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html

JSON olarak birden fazla satırı bir kerede girmek, güncellemek, silmek için bulk kullanılır.

POST _bulk
{"index":{"_index":"calisan","_id":"1"}}
{"name":"Ervin Howell","username":"Antonette","email":"Shanna@melissa.tv"}
{"index":{"_index":"adres","_id":"1"}}
{"street":"Kulas Light","suite":"Apt. 556","city":"Gwenborough","zipcode":"92998-3874","geo":{"lat":"-37.3159","lng":"81.1496"}}
{"index":{"_index":"calisan","_id":"2"}}
{"name":"Clementine Bauch","username":"Samantha","email":"Nathan@yesenia.net"}
{"index":{"_index":"adres","_id":"2"}}
{"street":"Douglas Extension","suite":"Suite 847","city":"McKenziehaven","zipcode":"59590-4157","geo":{"lat":"-68.6102","lng":"-47.0653"}}
{"index":{"_index":"calisan","_id":"3"}}
{"name":"Ervin Howell","username":"Antonette","email":"Shanna@melissa.tv"}
{"index":{"_index":"adres","_id":"3"}}
{"street":"Victor Plains","suite":"Suite 879","city":"Wisokyburgh","zipcode":"90566-7771","geo":{"lat":"-43.9509","lng":"-34.4618"}}

curl ifadesinde bir dosyadan okuyarak çalıştırmak daha yerinde olacaktır.

curl -H "Content-type: application/x-ndjson" 
-XPOST "http://localhost:9200/_bulk"
--data-binary @"toplu_data.json"

mget İle Birden Fazla Kayıt Çekmek

Birden fazla ID ile belgelerin çekilmesi için mget kullanılır.

https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-multi-get.html#docs-multi-get
GET /_mget
{
"docs": [
{
"_index": "calisan",
"_id": "1"
},
{
"_index": "calisan",
"_id": "2"
},
{
"_index": "adres",
"_id": "1"
},
{
"_index": "adres",
"_id": "2"
}
]
}

Sadece _source bilgilerini çekmek için

GET /_mget?filter_path=docs._source
{
"docs": [
{
"_index": "calisan",
"_id": "1"
},
{
"_index": "calisan",
"_id": "2"
},
{
"_index": "adres",
"_id": "1"
},
{
"_index": "adres",
"_id": "2"
}
]
}

Tüm komutların gist hali:

_mapping İle Haritalamak

Tüm Endeksin Haritasını Çekmek

GET vtoalarm/_mapping

Bir Alanın Haritasını Çekmek

GET vtoalarm/_mapping/field/Level

Filtered Query

Artık filtered sorguları yok! query ile aynı hizada yer alan filtered sorgularının işlevini artık query içindeki bool sorgularda yaşatıyoruz. Yani filtered artık yok!

QUERY

match ile arama

customer_full_name metin alanında tanımlı analizöre göre değer bilgisi token halinde ters endekslenerek belgeler kayıt edilmiştir. “Eddie Underwood” kelimelerinin bir arada olanları en yüksek puanla (score) sonuç listesinde en yukarıda yer alırken, sadece “Eddie” ve sadece “Underwood” kelimelerini bulduğu sonuçlar daha düşük puanla alt sıralarda yer alır.

match_phrase ile arama

Bu kez tam olarak geçmesi halinde sonuç listesinde görüntülenecektir aranan kelimeler. Yani Eddie Underwood kelimeleri yan yana olmak zorunda ancak bir cümlenin içinde olabilir.

Tek bir sonuç döner çünkü match_phrase ile aramada ancak aynı şekilde dizilmiş değere sahip belge sonuç listesinde yer alacaktır.

Örnek 2:

GET kibana_sample_data_ecommerce/_search?filter_path=hits.total.value,hits.**._source
{
"_source": "products.product_name",
"query": {
"match_phrase": {
"products.product_name": "Midnight Blue"
}
}
}
İlk sonucun iki ürününde de aranan sözcük (phrase) eşleştiği (match) için en yüksek puanla en üstte yer alıyor

match_all İle Her Şeyi, match_none İle Hiçbir Belgeyi Eşleştirmek

match_all
match_none

TERM Seviyesi Sorgular

Yapılandırılmış verilerdeki kesin değerlere dayalı belgeleri bulmak için terim düzeyinde sorguları kullanabilirsiniz. Yapılandırılmış veri örnekleri arasında tarih aralıkları, IP adresleri, fiyatlar veya ürün kimlikleri (id) bulunur. Tam metin sorguların (full text search) aksine, terim düzeyindeki sorgularda arama terimlerini analiz etmez. Bunun yerine, terim düzeyindeki sorgular, bir alanda depolanan terimlerle tam olarak eşleşir.

Uygulama ile gösterelim. Level alanımız metin tipinde (text) ve bu alan içinde terim seviyesinde süzgeçten geçirdiğimizde sonuçların gelmediğini görürüz çünkü terim seviyesi sorgular, metin tanımlı alanlar için KULLANILAMAZ!

Önce Level alanının metin tipinde olduğunu görelim:

GET alarm_yeni/_mapping/field/Level

# {
# "alarm_yeni" : {
# "mappings" : {
# "Level" : {
# "full_name" : "Level",
# "mapping" : {
# "Level" : {
# "type" : "text",
# "fields" : {
# "keyword" : {
# "type" : "keyword",
# "ignore_above" : 256
# }
# },
# "fielddata" : true
# }
# }
# }
# }
# }
# }

Şimdi sorgumuz ve sonucun gelmediğini görelim:

GET alarm_yeni/_search
{
"size": 1,
"query": {
"bool": {
"filter": [
{
"term": {
"Level": "ALARM_MAJOR"
}
}
]
}
}
}
# Bu sorguya dönecek sonuçları Level alanına göre gruplandırmak için cardinality kümeleme tipini de uygulayabiliriz ve sonuçların içinde Level alanında kaç farklı değerin geldiğini kova (bucket) içinde görebiliriz:GET alarm_yeni/_search
{
"size": 1,
"query": {
"bool": {
"filter": [
{
"term": {
"Level": "ALARM_MAJOR"
}
}
]
}
},
"aggs": {
"cardi_level": {
"cardinality": {
"field": "Level"
}
}
}
}
# Sonuç gelmeyeceği için kümeleme işi de boş dönecektir. Çünkü Level alanı METİN TİPİNDE olduğu için TERM seviyesi sorgu işe yaramayacak!# {
# "took" : 2,
# "timed_out" : false,
# "_shards" : {...},
# "hits" : {
# "total" : {
# "value" : 0, <------- Hiç sonuç dönmedi
# "relation" : "eq"
# },
# "max_score" : null,
# "hits" : [ ] <------- Dönmedi diyoruz yahu
# },
# "aggregations" : {
# "cardi_level" : {
# "value" : 0 <------- Olmayan sonuçları nasıl kümelesin!
# }
# }
# }

Şimdi terim seviyesi sorguyu olması gerektiği gibi ID, rakam, anahtar kelime tipinde bir alana yapalım. Bunun için günlük seviyesi (log level) veya bilgisayar adı (hostname) alanları yani hep aynı değerleri alacak, metin alanı olmayan alanları anahtar kelime (keyword) ile işaretleyerek yeni bir endeks oluşturup kayıtları bu endekse gönderelim (_reindex başlığında örneği mevcut). Sonra sorgumuzu tekrar yeni endekste çalıştıralım:

# kümeleme sadece cardinality olsaydı kaç farklı değerin olduğunu 
# rakamla göreceğiz
#
# Ancak terms ile kümeleme(aggs) yaparsak farklı değerlerin distinct
# halini yani tüm farklı değerleri görebileceğiz
GET alarm_yeni_2/_search
{
"size": 1,
"query": {
"bool": {
"filter": [
{ "term": { "Level": "ALARM_MAJOR" } }
]
}
},
"aggs": {
"card_level": {
"cardinality": { "field": "Level" }
},
"terms_level": {
"terms": {
"field": "Level",
"size": 10
}
}
}
}
# Sonuçta 1 tane Level değeri küme içinde gelecek o da: ALARM_MAJOR

2602 Belgenin Level değeri ALARM_MAJOR ve sadece ALARM_MAJOR getirildiği için kümeleme içinde 1 tane Level alanı değeri gelecek.

term Sorguları

Önce sku alanının metin (text) tipinde olmadığından emin olmak için alan temelli mapping bilgisini çekelim:

GET kibana_sample_data_ecommerce/_mapping/field/sku

Tipinin date, id, keyword, rakam olduğunu görelim:

{
"kibana_sample_data_ecommerce" : {
"mappings" : {
"sku" : {
"full_name" : "sku",
"mapping" : {
"sku" : {
"type" : "keyword"
}
}
}
}
}
}

Şimdi belirli bir değer için sku alanını sorgulayalım:

GET kibana_sample_data_ecommerce/_search
{
"query": {
"term": {
"sku": {
"value": "ZO0549605496"
}
}
}
}

Sonuçta iki belgenin döndüğünü göreceğiz:

terms Sorguları

Şimdi ise Level alanına birden fazla değer için sorgu yazalım:

GET alarm_yeni_2/_search
{
"size": 1,
"query": {
"bool": {
"filter": [
{
"terms": {
"Level": [
"ALARM_CRITICAL",
"ALARM_MAJOR"
]
}
}
]
}
},
"aggs": {
"card_level": {
"cardinality": {
"field": "Level"
}
},
"terms_level": {
"terms": {
"field": "Level",
"size": 10
}
}
}
}

Zaten sorgumuz içinde sadece iki değeri süzerek sonuçları dönüyoruz. Dönen sonuçların içinde kümeleme yapınca doğal olarak bu iki değer (ALARM_MAJOR ve ALARM_CRITICAL) gözükecek.

null_value Boş değerli alanların endekslenmesi ve aranabilir olması

Boş bir değer (null_value) dizine eklenemez veya aranamaz. Bir alan null olarak ayarlandığında (veya boş bir dizi veya bir boş değerler dizisi), o alanın hiç değeri yokmuş gibi değerlendirilir.

null_value parametresi, null değerleri belirtilen değerle değiştirmenize izin verir, böylece dizine eklenebilir (endekslenebilir) ve aranabilir.

_source içindeki bu değişimden etkilenmez sadece endeksleme ve arama yaparken değiştirmek için kullanacağımız değeri kullanabiliriz

null_value değeri ile alanın değeri aynı veri türünde olmalıdır. Örneğin, text türünde bir alanın null_value değeri sayı olmamalı veya long türünde bir alanın null_value değerinin metin (string) olmaması gibi.

Bir örnek daha:

# Önce varsa örneğimizdeki endeksi bir silelim
DELETE null_value_endeksi
# Şimdi kod alanının null gelmesi halinde aranabilir
# ve endekslenebilir olması için null_value parametresiyle
# mapping'i değiştirelim
PUT null_value_endeksi
{
"mappings": {
"properties": {
"adi": { "type": "text" },
"kod": {
"type" : "keyword",
"null_value": "CEMO"
}
}
}
}
# Bir kayıt ekleyelim ve kod alanı boş olsunPUT null_value_endeksi/_doc/1
{
"adi":"Cem",
"kod": null
}
# Sadece _source'a bakalım ve kod alanının değerinin değişmediğini görelimGET null_value_endeksi/_search?filter_path=**._source# kod alanı daima null gelir çünkü
# null_value ile _source'ta yer alan belgenin içindeki alanın
# değeri değişmez sadece endeksleme ve arama yapılabilir
# hale getirilir

#{
# "hits" : {
# "hits" : [
# {
# "_source" : {
# "adi" : "Cem",
# "kod" : null
# }
# }
# ]
# }
#}
GET null_value_endeksi/_search
{
"query": {
"term": {
"kod": {
"value": "CEMO"
}
}
}
}
# kod Hâlâ null ancak sorgumuzda kod alanı CEMO olanı getir dedik
# yani null_value da belirttiğimiz değeri kullanarak sorgulayabildik

#{
# "took" : 0,
# "timed_out" : false,
# "_shards" : {
# "total" : 1,
# "successful" : 1,
# "skipped" : 0,
# "failed" : 0
# },
# "hits" : {
# "total" : {
# "value" : 1,
# "relation" : "eq"
# },
# "max_score" : 0.2876821,
# "hits" : [
# {
# "_index" : "null_value_endeksi",
# "_type" : "_doc",
# "_id" : "1",
# "_score" : 0.2876821,
# "_source" : {
# "adi" : "Cem",
# "kod" : null
# }
# }
# ]
# }
#}

exists İle bol olmayan alanların belgelerini çekmek

null_value parametreli mapping belgeleri exists ile arayabiliriz

DELETE null_value_endeksiPUT null_value_endeksi
{
"mappings": {
"properties": {
"adi": {
"type": "text"
},
"soyadi": {
"type": "text"
},
"tcno": {
"type": "keyword"
},
"kod": {
"type": "keyword",
"null_value": "NULL-KOD_ALAN_DEGERI"
}
}
}
}
GET null_value_endeksi/_mapping# kod Alanı boş gelirse NULL-KOD_ALAN_DEGERI değeriyle endeksler
# ve kod alanını NULL-KOD_ALAN_DEGERI değeriyle arayabiliriz
#{
# "null_value_endeksi" : {
# "mappings" : {
# "properties" : {
# "adi" : {
# "type" : "text"
# },
# "kod" : {
# "type" : "keyword",
# "null_value" : "NULL-KOD_ALAN_DEGERI"
# },
# "soyadi" : {
# "type" : "text"
# },
# "tcno" : {
# "type" : "keyword"
# }
# }
# }
# }
#}
2 Belgeyi kaydedelimGET null_value_endeksi/_search
{
"query": {"match_all": {}}
}
# kod alanı daima null gelir çünkü
# null_value ile _source'ta yer alan belgenin içindeki alanın değeri değişmez
# sadece endeksleme ve arama yapılabilir hale getirilir
#
#...
# "hits" : [
# {
# "_index" : "null_value_endeksi",
# "_type" : "_doc",
# "_id" : "1",
# "_score" : 1.0,
# "_source" : {
# "adi" : "Cem",
# "kod" : null
# }
# },
# {
# "_index" : "null_value_endeksi",
# "_type" : "_doc",
# "_id" : "2",
# "_score" : 1.0,
# "_source" : {
# "adi" : "Cengo",
# "kod" : "CG007",
# "tcno" : "11111111111"
# }
# }
# ]
#...
# Şimdi exists ile tcno alanında değer olan belgeleri getirelimGET null_value_endeksi/_search?filter_path=hits.hits._source
{
"query": {
"exists": {
"field": "tcno"
}
}
}
# Sadece Cengo belgesi gelecek çünkü Cem için tcno alanı boş (null)
# Ancak null_value parametresi verilmeden map edildiği için
# exists bize Cem belgesini dönmeyecekti
#...
# "hits" : [
# {
# "_source" : {
# "adi" : "Cengo",
# "kod" : "CG007",
# "tcno" : "11111111111"
# }
# }
# ]
#...
# exists ile bu kez null_value parametreli kod alanını arayalımGET null_value_endeksi/_search?filter_path=hits.hits._source
{
"query": {
"exists": {
"field": "kod"
}
}
}
# kod Alanı null gelse bile null_value ile endekslendiği için
# exist bize Cem belgesini de dönecek
#...
# "hits" : [
# {
# "_source" : {
# "adi" : "Cem",
# "kod" : null
# }
# },
# {
# "_source" : {
# "adi" : "Cengo",
# "kod" : "CG007",
# "tcno" : "11111111111"
# }
# }
# ]
#...

must_not İle şartın (condition) tersini (değilini) sorgulayalım

exists ve must_not ile tcno alanının değeri boş (null) olmayan yani değeri olan belgeleri arayalım:

GET null_value_endeksi/_search
{
"query": {
"bool": {
"must_not": [
{
"exists": {
"field": "tcno"
}
}
]
}
}
}
# Sonuçta tcno alanı boş değer alabildiği için must_not+exists bize
# Cem belgesini dönecektir:
#...
# "hits" : [
# {
# "_index" : "null_value_endeksi",
# "_type" : "_doc",
# "_id" : "1",
# "_score" : 0.0,
# "_source" : {
# "adi" : "Cem",
# "kod" : null
# }
# }
# ]
#...

Küme İşlemleri (aggregation)

Metrik kümeleme işlemleri bize toplam, ortalama, en küçük, en büyük gibi istatistiki bilgileri verecektir. Birazdan metrik kümelemeyi göreceğiz ancak önce bir alanın aldığı farklı değerleri görelim (SQL’de distinct gibi)

Terms Aggregation

x İsimli alanda kaç farklı değer var? (aggs-<y>-terms-field:<x>)

Kayıtlarımız içinde JSON nesnesi ve içinde severity adlı bir alan mevcut. Bir hatanın ayrıntılarını Detail nesnesi, hatanın şiddetini severity alanı tutuyor. Metin tipinde değerleri olan alanlar için fielddata tanımlanması gerekiyor. Birazdan daha ayrıntılı anlatılacak ama bir alıntıyla hafızamıza yazalım:

Metin alanları varsayılan olarak aranabilir (searchable) ancak varsayılan olarak toplamalar (aggregations), sıralama veya script ile erişilebilir durumda değillerdir. Metin alanındaki bir komut dosyasından (script ile ) değerleri sıralamayı, toplamayı veya bunlara erişmeyi denerseniz, şu istisnayı görürsünüz:

Fielddata is disabled on text fields by default. Set `fielddata=true` on
[`your_field_name`] in order to load fielddata in memory by uninverting the
inverted index. Note that this can however use significant memory.

Çözüm fielddata ile bu metin alanlarını işaretlemek. Şimdi tekrar bir alanda kaç farklı değerin olduğunu bulmaya devam edelim:

PUT stoalarm/_mapping
{
"properties": {
"Detail.severity": {
"type": "text",
"fielddata": true
}
}
}

Şimdi sorgumuzu ve cevabını görelim:

GET stoalarm/_search?size=0&filter_path=aggregations
{
"aggs": {
"distinct_gibi_birsey": {
"terms": {
"field": "Detail.severity"
}
}
}
}
# Metadata olmaksızın aggregations'ı görelim
# {
# "aggregations" : {
# "distinct_gibi_birsey" : {
# "doc_count_error_upper_bound" : 0,
# "sum_other_doc_count" : 0,
# "buckets" : [
# {
# "key" : "normal",
# "doc_count" : 10946
# },
# {
# "key" : "cleared",
# "doc_count" : 10263
# },
# {
# "key" : "major",
# "doc_count" : 20
# }
# ]
# }
# }
# }
# GET stoalarm/_search?filter_path=aggregations.**.buckets.key
# Sadece terimleri görelim
#
# {
# "aggregations" : {
# "distinct_gibi_birsey" : {
# "buckets" : [
# {
# "key" : "normal"
# },
# {
# "key" : "cleared"
# },
# {
# "key" : "major"
# }
# ]
# }
# }
# }

Biraz ileri gideceğiz ama ileride tekrar değindiğimizde perçinlenecek. Aşağıdaki GET isteği; bir sorgu (QUERY) ve bir kümeleme (AGGREGATION) den oluşuyor. Kümeleme ise iki parçadan oluşuyor:

  • Level alanında geçen farklı değerleri getirem terms aggregation
  • Sonuçlar içinde filters ile ALARM_MAJOR ve ALARM_CRITICAL ile eşleşen Level alanı değerlerinin belge sayısını getirir

Sorguda Detail içindeki nfType alanı AMF değeri içerenleri getir diyoruz ve sadece iki sonucu görüntülüyoruz. Dikkat edin ALARM_NORMAL ve ALARM_MAJOR değeri olan iki sonuç geldi. Birazdan kümeleme kısmında ise filters ile ALARM_MAJOR ve ALARM_CRITICAL için istatistik işlemler yapabileceğiz ama sadece 7 belgenin ALARM_CRITICAL ve 767 belgenin ALARM_MAJOR içerdiğini göreceğiz.

Yani; aggregations içindeki filter ile sonuç kümesini !!!süzmüyor!!!, query ile süzülen sonuçların içinde belirtilen alanın (field) beklenen değeri içeren belge sayılarını görüyoruz.

Tire (hypen, dash) olunca neden kümelenmiyor?

Verimiz Detail içinde bir GUID değer içeren nfInstanceId isimli alan olsun. Bu alanda veri tireli tutuluyor olsun:

“nfInstanceId” : “49a4534c-90bb-44bf-81b7–7750556739aa”,

Önce başarısız olacak sorgumuzu yapalım ve sonuçları görelim:

GET stoalarm/_search?filter_path=aggregations.**.buckets.key
{
"aggs": {
"distinct_gibi_birsey": {
"terms": {
"field": "Detail.nfInstanceId"
}
}
}
}

Sonuçlar, standart analizörün “tireler arasındaki her şeyi” metin gibi algılamasındandır. Standart analizör zannediyorki bu tireler aslında kelimeleri bölüyor: use-case, computer-aided, sugar-free gibi.

{
"aggregations" : {
"distinct_gibi_birsey" : {
"buckets" : [
{ "key" : "44bf"
}, { "key" : "49a4534c"
}, { "key" : "7750556739aa"
}, { "key" : "81b7"
}, { "key" : "90bb"
}, { "key" : "3fd52bef206c"
}, { "key" : "4a0e"
}, { "key" : "6db2ef70"
}, { "key" : "82b5"
}, { "key" : "ad34"
}
]
}
}
}

Bakın standard analizör bizim değeri nasıl anlıyor?

POST _analyze
{
"analyzer": "standard",
"text": "49a4534c-90bb-44bf-81b7-7750556739aa"
}
# Tire karakterleriyle kelimelerin ayrıldığını düşünüyor...
# Ayrıca her kelime diye ayrıştırdığı token
# hem alpha hem nümerik veri içeriyor diye ALPHANUM tipini atıyor
#{
# "tokens" : [
# {
# "token" : "49a4534c",
# "start_offset" : 0,
# "end_offset" : 8,
# "type" : "<ALPHANUM>",
# "position" : 0
# },
# {
# "token" : "90bb",
# "start_offset" : 9,
# "end_offset" : 13,
# "type" : "<ALPHANUM>",
# "position" : 1
# },
# {
# "token" : "44bf",
# "start_offset" : 14,
# "end_offset" : 18,
# "type" : "<ALPHANUM>",
# "position" : 2
# },
# {
# "token" : "81b7",
# "start_offset" : 19,
# "end_offset" : 23,
# "type" : "<ALPHANUM>",
# "position" : 3
# },
# {
# "token" : "7750556739aa",
# "start_offset" : 24,
# "end_offset" : 36,
# "type" : "<ALPHANUM>",
# "position" : 4
# }
# ]
#}

Şimdide whitespace analizörüyle incelemeden önce dinamik haritalanması için bir kayıt girelim :

Dinamik haritalama ve Standard Analizör Kullanıldığını Görelim

PUT test_alarm/_doc/1
{
"Detail": {
"alarmId": "68c569d6-bfb3-420d-8b8c-0657af4883f4"
}
}
GET test_alarm/_mapping
# Dinamik haritalandı ve işte sonuç:
#{
# "test_alarm" : {...............
# "mappings" : { :
# "properties" : { :
# "Detail" : {............:
# "properties" : { :
# "alarmId" : {.......: test_alarm/Detail.alarmId
# "type" : "text",
# "fields" : {
# "keyword" : {
# "type" : "keyword",
# "ignore_above" : 256
# }

Şimdi standard analizör ile yukarıdaki örnekte tireden dolayı yaşadıklarımızı bir daha görelim.

Önce fielddata: true olmadığı için şu hatayı alacağız:

Düzeltmek için inverted index ile endekslenen bir alan olmaktan çıkaralım alarmId alanını:

PUT test_alarm/_mapping
{
"properties": {
"Detail.alarmId": {
"type": "text",
"fielddata": true,
### "analyzer": "whitespace" > ancak index yaratılırken ayarlanır!!
}
}
}

İşte tirelerin arasındaki her bir token sanki kelime gibi algılanıyor:

Tamam olmayacaktı çünkü dinamik haritalama standard analyzer kullanıyordu. Şimdi elle haritalayarak veri girelim çünkü daha önceden yaratılmış bir endeks için analyzer güncellenemez !

Elle haritalama ve Analizör olarak whitespace Kullanalım

Analizör biraz teferreuatlı ve elasticsearch’ün nasıl çalıştığına giren bir başlık ama burada kısaca “ayrıştırma sürecinin yönetilmesi” diyerek devam edebiliriz.

whitespace Analizörü tireli metnimizi nasıl parçalıyor ona bir bakalım:

POST _analyze
{
"analyzer": "whitespace",
"text": "49a4534c-90bb-44bf-81b7-7750556739aa"
}
# Sonuçta tireler değil boşluk karakterleri ile kelimeler bölünüyor
#{
# "tokens" : [
# {
# "token" : "49a4534c-90bb-44bf-81b7-7750556739aa",
# "start_offset" : 0,
# "end_offset" : 36,
# "type" : "word",
# "position" : 0
# }
# ]
#}

Artık indeksimizi oluştururken analizörü de verelim:

PUT test_alarm
{
"mappings": {
"properties": {
"Detail": {
"properties": {
"alarmId": {
"type": "text",
"analyzer": "whitespace",
"fielddata": true,
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
}
GET test_alarm/_mapping# Elle ayarlarını yapıyoruz ama bu kez analyzer olarak whitespace
# ataması yapıyoruz
#
#{
# "test_alarm" : {
# "mappings" : {
# "properties" : {
# "Detail" : {
# "properties" : {
# "alarmId" : {
# "type" : "text",
# "fields" : {
# "keyword" : {
# "type" : "keyword",
# "ignore_above" : 256
# }
# },
# "analyzer" : "whitespace",
# "fielddata" : true
# }

Endeksimiz hazır ve bir kayıt ekleyelim:

PUT test_alarm/_doc/1
{
"Detail": {
"alarmId": "68c569d6-bfb3-420d-8b8c-0657af4883f4"
}
}

Artık alarmId alanında kümelemeyi çalıştırabiliriz:

GET test_alarm/_search?filter_path=aggregations.**.buckets
{
"aggs": {
"distinct_gibi_birsey": {
"terms": {
"field": "Detail.alarmId"
}
}
}
}

Çıktımızda tire karakterlerini (hyphen) değil artık boşluk (whitespace) karakterini ayrıştırmak için kullandığımızı göreceğiz:

{
"aggregations" : {
"distinct_gibi_birsey" : {
"buckets" : [
{
"key" : "68c569d6-bfb3-420d-8b8c-0657af4883f4",
"doc_count" : 1
}
]
}
}
}

Tekrar hatırlayalım: Analizörü whitespace olarak değiştirmemiz gerekiyor ancak verinin olduğu bir endeks üstünde bunu gerçekleştiremeyiz! Bu yüzden yeni bir endeks yaratıp _reindex ile veriyi aktarabiliriz.

_reindex İle Eski Endeksten Yenisine Veri Aktarmak

Verilerimizin kaynağından sadece Detail.alarmId alanının değerini taşımak istiyoruz.

Sonuçlarda tüm _source yerine belirli alanların olmasını istiyorsak ya querystring olarak URL içinde belirtiyor ya da isteğin gövdesine _source alanına dizinin elemanları olarak yazıyoruz:

GET stoalarm/_search?size=100
{
"_source": [ "Detail.alarmId", "Level" ]
}

Tüm kayıtları çekmek zorunda değilsek size ile istediğimiz sayıda, query arama sonuçlarımızı kaynaktan çekebiliriz.

POST _reindex
{
"source": {
"index": "stoalarm",
"size": 10,
"_source": [ "Detail.alarmId" ],
"query": {
"match_all": {}
}
},
"dest": {
"index": "test_alarm"
}
}

Bir örnek daha:

Önce ID olan alanları veya sürekli aynı değerleri alacak alanları keyword olarak tanımlayalım ve bu kez yeni endeks adımız alarm_yeni_2 olsun

PUT alarm_yeni_2
{
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"Detail": {
"properties": {
"alarmClearedTime": {
"type": "date"
},
"alarmId": {
"type": "keyword"
},
"alarmNature": {
"type": "text"
},
"alarmRaisedTime": {
"type": "date"
},
"alarmType": {
"type": "keyword"
},
"cause": {
"type": "text"
},
"dateTime": {
"type": "date"
},
"eventType": {
"type": "text"
},
"info": {
"type": "text"
},
"nfInstanceId": {
"type": "keyword"
},
"nfType": {
"type": "keyword"
},
"parameters": {
"type": "text"
},
"perceivedSeverity": {
"type": "text"
},
"probableCause": {
"type": "text"
},
"serviceInstanceId": {
"type": "keyword"
},
"serviceName": {
"type": "text"
},
"serviceinstanceid": {
"type": "keyword"
},
"severity": {
"type": "text",
"fielddata": true
},
"snssai": {
"properties": {
"sd": {
"type": "text"
},
"sst": {
"type": "long"
}
}
}
}
},
"File": {
"type": "text"
},
"ID": {
"type": "text"
},
"Level": {
"type": "keyword"
},
"Log_Time": {
"type": "text"
},
"Messages": {
"type": "text"
},
"hostname": {
"type": "keyword"
}
}
}
}

Ve tekrar _reindex ile önceki endeksin verilerini yeni endekse taşıyalım

POST _reindex
{
"source": {
"index": "alarm_yeni",
"size": 10,
"query": {
"match_all": {}
}
},
"dest": {
"index": "alarm_yeni_2"
}
}

Metrik Kümeleme

Şimdi biraz aggs nasıl yazılıyor, metrik sonuçları nasıl alabiliyoruz görelim:

GET /<endeks adı>/_search[?ekparametreler]
{
"aggs": {
"<küme_adını_ne_isterseniz>": {
"<avg|sum|count|min...>": {
"field": "<hangi alan üstünde uygulansın>"
}
}
}
}

Yapı yukarıdaki gibi. aggS Adına dikkat ederseniz birden fazla tanımladığınız kümeyi alabileceğini ancak bir dizi değil, objenin yeni alanı olarak yazabileceğimizi devamında görüyoruz.

Aşağıdaki sorguda sonuçlardan keyfi olarak 3'ünü (size=3) ve bu 3 sonucun {adi,yasi,cinsiyeti} bilgileriyle birlikte “ortalama kümesinin sonucu” getireceğiz.

GET /ogrenci/_search?size=3&_source=adi,yasi,cinsiyeti
{
"aggs": {
"ortalama-yas": {
"avg": {
"field": "yas"
}
}
}
}

Eğer yaş alanı üstünde sadece avg değil tüm istatistiki sonuçları (avg, min, max, sum) sonuçlarını bir kerede almak istersek:

GET /ogrenci/_search?size=3&_source=adi,yasi,cinsiyeti
{
"aggs": {
"tum-istatistikler-yas": {
"stats": {
"field": "yas"
}
}
}
}

Bu sorguya ek olarak dilerseniz cinsiyeti kadın olan öğrencileri süzerek ortalamayı getirebiliriz:

GET /ogrenci/_search?size=3&_source=adi,yasi,cinsiyeti
{
"aggs": {
"ortalama-yas": {
"avg": {
"field": "yas"
}
}
}
}

Örnek verimiz aşağıdaki gibi olsun ve sorgularımızı çalıştıralım:

Aşağıdaki 5 kayıtlı veri kümesini çalıştırabilirsiniz
POST _bulk
{"index":{"_index":"test","_id":"1"}}
{"index":0,"guid":"ba145b3d-8e78-43eb-9359-3c2b15777888","isActive":true,"balance":"$1,578.32","picture":"http://placehold.it/32x32","age":27,"eyeColor":"brown","name":"Alice Reeves","gender":"female","company":"UTARIAN","email":"alicereeves@utarian.com","phone":"+1 (876) 570-2846","address":"532 Vernon Avenue, Brewster, Minnesota, 9098","about":"Enim ullamco nostrud incididunt laboris exercitation nostrud commodo do id mollit. Cillum labore pariatur magna labore veniam anim. Consectetur id magna ullamco excepteur id mollit excepteur eu sunt Lorem velit tempor consequat id. Minim minim exercitation irure esse.\r\n","registered":"2019-02-22T07:19:54 -03:00","latitude":-37.041271,"longitude":-87.401533,"tags":["ad","occaecat","enim","sint","reprehenderit","aute","incididunt"],"friends":[{"id":0,"name":"Keller Blair"},{"id":1,"name":"Austin Murphy"},{"id":2,"name":"Levine Goodman"}]}
{"index":{"_index":"test","_id":"2"}}
{"index":1,"guid":"bb2653e9-411e-49b4-b6f7-9f60d6042a83","isActive":true,"balance":"$3,670.41","picture":"http://placehold.it/32x32","age":29,"eyeColor":"blue","name":"Riddle Aguilar","gender":"male","company":"FOSSIEL","email":"riddleaguilar@fossiel.com","phone":"+1 (848) 600-3335","address":"948 Battery Avenue, Chilton, Pennsylvania, 9114","about":"Laborum ut commodo consequat occaecat mollit adipisicing sint ipsum. In incididunt amet irure do. Ex voluptate id veniam proident. Labore aliquip aute voluptate ad deserunt. In mollit nostrud ad aute consequat. Occaecat dolore sunt elit deserunt velit labore eiusmod in commodo culpa quis cillum.\r\n","registered":"2015-03-01T07:17:12 -02:00","latitude":9.120384,"longitude":-110.077147,"tags":["excepteur","sunt","enim","magna","pariatur","incididunt","excepteur"],"friends":[{"id":0,"name":"Hudson Frye"},{"id":1,"name":"Bowen Jarvis"},{"id":2,"name":"Ingrid Lloyd"}]}
{"index":{"_index":"test","_id":"3"}}
{"index":2,"guid":"fd436495-28b9-4273-a67b-fb57c9e821e3","isActive":false,"balance":"$3,752.27","picture":"http://placehold.it/32x32","age":40,"eyeColor":"brown","name":"Huber Sanders","gender":"male","company":"AUTOMON","email":"hubersanders@automon.com","phone":"+1 (911) 442-2789","address":"472 Stone Avenue, Remington, West Virginia, 7795","about":"Qui consectetur non pariatur amet. Cupidatat dolore sint nostrud est velit anim ullamco consectetur magna. Deserunt dolor excepteur eiusmod cillum.\r\n","registered":"2019-10-19T01:38:39 -03:00","latitude":11.114887,"longitude":117.706685,"tags":["tempor","pariatur","aute","Lorem","sit","veniam","elit"],"friends":[{"id":0,"name":"James Carter"},{"id":1,"name":"Casandra Suarez"},{"id":2,"name":"Nguyen Kelley"}]}
{"index":{"_index":"test","_id":"4"}}
{"index":3,"guid":"2d954589-cdd4-4bdb-b8c5-d875c76a1c8d","isActive":false,"balance":"$1,561.78","picture":"http://placehold.it/32x32","age":37,"eyeColor":"brown","name":"Mercado Hurst","gender":"male","company":"CUJO","email":"mercadohurst@cujo.com","phone":"+1 (969) 517-2509","address":"377 Junius Street, Chamizal, Maryland, 3699","about":"Id ut cupidatat sunt dolor sint eiusmod ea consectetur. Do cillum magna magna velit non velit ipsum veniam sint ullamco consectetur est. Mollit elit cillum labore non magna duis eiusmod esse minim pariatur ex deserunt.\r\n","registered":"2016-09-01T03:00:38 -03:00","latitude":-2.577622,"longitude":174.042788,"tags":["proident","dolore","excepteur","sit","voluptate","minim","magna"],"friends":[{"id":0,"name":"Mari Bass"},{"id":1,"name":"Andrea Mcintyre"},{"id":2,"name":"Cardenas Mcconnell"}]}
{"index":{"_index":"test","_id":"5"}}
{"index":4,"guid":"423439f1-494e-4bb6-abd2-110f0a44b913","isActive":true,"balance":"$1,219.04","picture":"http://placehold.it/32x32","age":24,"eyeColor":"green","name":"Rosario Strong","gender":"male","company":"NETILITY","email":"rosariostrong@netility.com","phone":"+1 (811) 518-2287","address":"655 McKibbin Street, Konterra, Louisiana, 3997","about":"Do pariatur ipsum deserunt labore. Nostrud commodo in quis incididunt. Velit non tempor tempor exercitation elit adipisicing sit consequat cillum excepteur Lorem eu cillum. Fugiat esse laborum occaecat nostrud irure pariatur consequat nisi eiusmod esse magna voluptate deserunt. Occaecat sunt sit nostrud eu.\r\n","registered":"2018-12-15T11:31:09 -03:00","latitude":20.851648,"longitude":37.538853,"tags":["aliqua","adipisicing","ex","anim","duis","in","fugiat"],"friends":[{"id":0,"name":"Avila Parks"},{"id":1,"name":"Larsen Moran"},{"id":2,"name":"Kennedy Owen"}]}

Tüm kayıtlarda ortalama yaşı bulalım:

GET /test/_search?size=1&_source=age,name
{
"aggs": {
"ortalama-yas": {
"avg": {
"field": "age"
}
}
}
}

Sadece erkeklerin ortalama yaşını bulalım:

Dikkat: query içinde bool->filter çıktısını kümelemeye taşıyor olduğumuzu query ile aggs alanlarının aynı hizada

GET /test/_search?size=1&_source=age,name,gender
{
"query": {
"bool": {
"filter": [
{
"term": {
"gender": "male"
}
}
]
}
},
"aggs": {
"ortalama-yas": {
"avg": {
"field": "age"
}
}
}
}

Tüm istatistik çıktıları görmek için “stats”

Hangi alan üstünde görmek istiyorsak stats ile tüm temel istatistik çıktılarını görebiliriz:

Sadece avg, min, max, sum, count için bakabilmek için

Dizi tipinde bir alanın değerlerinde veya basit bir alanın alabileceği değerlere göre süzmek:

tags Alanında sit, excepteur alanları olan kayıtları süz ve age alanına göre ortalama al. Bunun için terms kullanacağız :

GET /test/_search?size=1&_source=age,name,tags
{
"query": {
"terms": {
"tags": [
"excepteur",
"sit"
]
}
},
"aggs": {
"ortalama-yas": {
"avg": {
"field": "age"
}
}
}
}

fielddata İle Gruplama

Arama (search) ve kümeleme (aggregation veya toplama) yaparken aradaki farkı anlamamız gerekiyor.

Elasticsearch, çok hızlı tam metin aramalarını destekleyen, tersine çevrilmiş dizin adı verilen bir veri yapısı kullanır. Tersine çevrilmiş bir dizin, herhangi bir belgede görünen her benzersiz sözcüğü listeler ve her sözcüğün geçtiği tüm belgeleri tanımlar.

Bir dizin, optimize edilmiş bir belge koleksiyonu olarak düşünülebilir ve her belge, verilerinizi içeren anahtar/değer çiftleri olan bir alan koleksiyonudur. Varsayılan olarak, Elasticsearch her alandaki tüm verileri indeksler ve indekslenmiş her alan, özel, optimize edilmiş bir veri yapısına sahiptir. Örneğin, metin alanları ters çevrilmiş endekslerde saklanır ve sayısal ve coğrafi alanlar BKD ağaçlarında depolanır. Elasticsearch’ü bu kadar hızlı yapan şey, arama sonuçlarını bir araya getirmek ve döndürmek için alan başına veri yapılarını kullanma becerisidir. ***

Okumaya devam ettiğinizde (Google Türkçesi de kifayet eder) göreceksiniz ki adamlar “verinizi siz bizden iyi tanıyorsunuz, dinamik olarak verinizi anlamamız yerine bize verinizin yapısını verirseniz daha başarılır oluruz” diyor.

Nihayetinde, verileriniz ve onu nasıl kullanmak istediğiniz hakkında Elasticsearch’ün yapabileceğinden daha fazla şey biliyorsunuz. Dinamik eşlemeyi kontrol etmek için kurallar tanımlayabilir ve alanların nasıl depolandığını ve dizine eklendiğini tam olarak kontrol etmek için eşlemeleri açıkça tanımlayabilirsiniz.

Arama (search) yaparken (tam metin araması — full text search) belgelerdeki kelimeler hash değerler olarak, tersine çevrilmiş dizin (inverted index) içinden bakılarak bulunuyor. Aşağıdaki gösterimde 3 belge içindeki kelimelerin stopword diye tanımlı “anahtar kelime” kabul edilemeyecekleri hariç endeksleniyor (dizinleniyor). Her kelimenin, hangi belgede ve ne sıklıkla (frekans) geçtiği dizinleniyor/endekleniyor.

Arama yaptığımız kelimelerin geçtiği belgeler bulunup, kelimenin belgede geçiş sıklığına, aranan kelimelerin verildiği sırada mı yoksa farklı yerlerde olduklarına bakılarak bir puanlama (score) yapılarak en yüksek puanlı belgeyi en üstte olacak şekilde sonuç listesi dönüyor Elasticsearch.

Ancak sıralama veya kümeleme/toplama (aggregation) yaparken ise kelimeleri olduğu gibi kullanıyor ve tersine çevrilmiş endeksleri değil!

Farklı amaçlar için aynı alanı farklı şekillerde dizine eklemek genellikle yararlıdır. Örneğin, bir dize alanını hem tam metin araması için bir metin alanı hem de verilerinizi sıralamak veya toplamak için bir anahtar sözcük alanı olarak dizine eklemek isteyebilirsiniz.

Level alanına göre kümeleme yapmak istiyorum.

GET vtoalarm/_search
{
"query": {
"range": {
"@timestamp": {
"gte": "now-10d/d",
"lt": "now/d"
}
}
},
"aggs": {
"Level-Gruplama": {
"terms": {
"field": "Level"
}
}
}
}

Ancak aldığım hata mesajı:

{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [Level] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."
}
],
"type": "search_phase_execution_exception",
"reason": "all shards failed",
"phase": "query",
"grouped": true,
"failed_shards": [
{
"shard": 0,
"index": "vtoalarm",
"node": "PMpVc1cbTMOoJy24Q7EfDg",
"reason": {
"type": "illegal_argument_exception",
"reason": "..yukarıdakiyle aynı..."
}
}
],
"caused_by": {
"type": "illegal_argument_exception",
"reason": "..yukarıdakiyle aynı...",
"caused_by": {
"type": "illegal_argument_exception",
"reason": "..yukarıdakiyle aynı..."
}
}
},
"status": 400
}

Önce Level alanının haritalanmasına bakalım:

GET vtoalarm/_mapping/field/Level

Level alanı:

{
"vtoalarm" : {
"mappings" : {
"Level" : {
"full_name" : "Level",
"mapping" : {
"Level" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}

}
}
}
}
}
}
}

Level alanında fielddata özelliğini aktif etmek gerekecek.

PUT vtoalarm/_mapping
{
"properties": {
"Level": {
"type": "text",
"fielddata": true
}
}
}

Tekrar Level alanının haritasına bakalım:

GET vtoalarm/_mapping/field/Level{
"vtoalarm" : {
"mappings" : {
"Level" : {
"full_name" : "Level",
"mapping" : {
"Level" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword"
}
},
"fielddata" : true
}
}
}
}
}
}

Şimdi tekrar yukarıdaki kümeleme sorgumuzu çalıştırdığımızda:

GET vtoalarm/_search
{“query”:{“range”:{“@timestamp”:{“gte”:”now-10d/d”,”lt”:”now/d”}}},”aggs”:{“Level-Gruplama”:{“terms”:{“field”:”Level”}}}}

Hepsini bir örnekte görelim:

# eyeColor Alanına göre gruplayarak kaç farklı göz rengi olduğunu görelimGET test/_search
{
"size":0,
"aggs": {
"eyeColor_alanina_gore_essiz_degerler": {
"cardinality": {
"field": "eyeColor"
}
}
}
}
# "reason" : "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. Alternatively, set fielddata=true on [eyeColor] in order to load field data by uninverting the inverted index. Note that this can use significant memory."GET test/_mapping/field/eyeColor
# >>> Çıktı:
# {
# "test" : {
# "mappings" : {
# "eyeColor" : {
# "full_name" : "eyeColor",
# "mapping" : {
# "eyeColor" : {
# "type" : "text",
# "fields" : {
# "keyword" : {
# "type" : "keyword",
# "ignore_above" : 256
# metin alanı olduğu için gruplanamıyor
# tekrar **fielddata** ile haritalayalım
PUT test/_mapping
{
"properties": {
"eyeColor": {
"type": "text",
"fielddata": true
}
}
}
# >>> Çıktı:
# { "acknowledged" : true }
GET test/_mapping/field/eyeColor
# >>> Çıktı:
# {
# "test" : {
# "mappings" : {
# "eyeColor" : {
# "full_name" : "eyeColor",
# "mapping" : {
# "eyeColor" : {
# "type" : "text",
# "fields" : {
# "keyword" : {
# "type" : "keyword",
# "ignore_above" : 256
# }
# },
# "fielddata" : true
GET test/_search?filter_path=hits.total,hits.hits._source.eyeColor,aggregations
{
"size":1,
"aggs": {
"eyeColor_alanina_gore_essiz_degerler": {
"cardinality": {
"field": "eyeColor"
}
}
}
}
# 343 Kayıt içinde 3 eyeColor değeri kullanılıyor (brown, green, blue)
#
# {
# "hits" : {
# "total" : {
# "value" : 343,
# "relation" : "eq"
# },
# "hits" : [
# {
# "_source" : {
# "eyeColor" : "brown"
# }
# }
# ]
# },
# "aggregations" : {
# "eyeColor_alanina_gore_essiz_degerler" : {
# "value" : 3
# }
# }
# }

Bir diğer örnek:

# Metin alanları belleğin heap bölümünü işgal ettiği için ön tanımlı olara
# fielddata=false geliyor tüm metin alanları için.
# Bu yüzden istediğimiz alanın mappings ile özelliğini değiştiriyoruz.
PUT test/_mapping
{
"properties": {
"gender": {
"type": "text",
"fielddata": true
}
}
}
GET test/_search
{
"size": 0,
"aggs": {
"gender_aggs": {
"cardinality": {
"field": "gender"
}
}
}
}
# Female ve Male olarak iki farklı değer dönüyor
# {
# "took" : 10,
# "timed_out" : false,
# "_shards" : {
# "total" : 1,
# "successful" : 1,
# "skipped" : 0,
# "failed" : 0
# },
# "hits" : {
# "total" : {
# "value" : 343,
# "relation" : "eq"
# },
# "max_score" : null,
# "hits" : [ ]
# },
# "aggregations" : {
# "gender_aggs" : {
# "value" : 2
# }
# }
# }

size İle 0–10 Arasında Gelecek Sonuçların Sayısı Getirme

Varsayılan olarak 10 kayıt getirir ancak size ile 0–10 arasında “hiç sonuç getirme” veya “10'dan az getir” denilebilir.

Hiç Sonuç Getirme, Verinin Verisini (metadata) Getir

GET vtoalarm/_search
{
"query": {
"range": {
"@timestamp": {
"gte": "now-10d/d",
"lt": "now/d"
}
}
},
"size": 0,
"aggs": {
"Level-Gruplama": {
"terms": {
"field": "Level"
}
}
}
}

Bucket — Kova

# Hangi cinsiyetten kaç kişi kayıtlı diye bakmak istediğimizde
# kova (bucket) ile çözüyor olacağız
# Bu kez AGGS içinde TERMS ile hangi FIELD için yazacağımızı belirliyoruz
GET test/_search
{
"size": 0,
"aggs": {
"gender_kovası": {
"terms": {
"field": "gender"
}
}
}
}
# Erkek: 164, Kadın: 179
# {
# "took" : 19,
# "timed_out" : false,
# "_shards" : {
# "total" : 1,
# "successful" : 1,
# "skipped" : 0,
# "failed" : 0
# },
# "hits" : {
# "total" : {
# "value" : 343,
# "relation" : "eq"
# },
# "max_score" : null,
# "hits" : [ ]
# },
# "aggregations" : {
# "gender_kovası" : {
# "doc_count_error_upper_bound" : 0,
# "sum_other_doc_count" : 0,
# "buckets" : [
# {
# "key" : "female",
# "doc_count" : 179
# },
# {
# "key" : "male",
# "doc_count" : 164
# }
# ]
# }
# }
# }
#

Aralık İçerisindeki Değerler (range)

GET test/_search
{
"size": 0,
"aggs": {
"yas_araliklari": {
"range": {
"field": "age",
"ranges": [
{ "to": 20 },
{ "from": 20, "to": 30 },
{ "from": 30, "to": 40 },
{ "from": 40 }
]
}
}
}
}
# Aralık içeren kova oluşturyoruz
# {
# "took" : 9,
# "timed_out" : false,
# "_shards" : {
# "total" : 1,
# "successful" : 1,
# "skipped" : 0,
# "failed" : 0
# },
# "hits" : {
# "total" : {
# "value" : 343,
# "relation" : "eq"
# },
# "max_score" : null,
# "hits" : [ ]
# },
# "aggregations" : {
# "yas_araliklari" : {
# "buckets" : [
# {
# "key" : "*-20.0",
# "to" : 20.0,
# "doc_count" : 0
# },
# {
# "key" : "20.0-30.0",
# "from" : 20.0,
# "to" : 30.0,
# "doc_count" : 164
# },
# {
# "key" : "30.0-40.0",
# "from" : 30.0,
# "to" : 40.0,
# "doc_count" : 162
# },
# {
# "key" : "40.0-*",
# "from" : 40.0,
# "doc_count" : 17
# }
# ]
# }
# }

Yukarıdaki aralık değerlerini dizi olarak alıyoruz ancak eğer bir nesnenin alanları olarak almak istersek keyed: true diyerek işletiyoruz:

GET test/_search
{
"size": 0,
"aggs": {
"yas_araliklari": {
"range": {
"field": "age",
"keyed": true,
"ranges": [
{ "to": 20 },
{ "from": 20, "to": 30 },
{ "from": 30, "to": 40 },
{ "from": 40 }
]
}
}
}
}
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 343,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"yas_araliklari" : {
"buckets" : {
"*-20.0" : {
"to" : 20.0, "doc_count" : 0 },
"20.0-30.0" : {

"from" : 20.0, "to" : 30.0, "doc_count" : 164
},
"30.0-40.0" : {

"from" : 30.0, "to" : 40.0, "doc_count" : 162
},
"40.0-*" : {
"from" : 40.0, "doc_count" : 17 }
}
}
}
}

Takma adlarla sonuç gruplarını isimlendirmek

GET test/_search
{
"size": 0,
"aggs": {
"yas_araliklari": {
"range": {
"field": "age",
"keyed": true, # keyed:true olmazsa aşağıdaki çalışmaz
"ranges": [
{ "key": "genç", "to": 20 },
{ "key": "çalışan", "from": 20, "to": 30 },
{ "key": "orta-yaş", "from": 30, "to": 40 },
{ "key": "yaşlı", "from": 40 }
]
}
}
}
}

Sonuçta grupların isimleri var:

...,
"aggregations" : {
"yas_araliklari" : {
"buckets" : {
"genç" : { "to" : 20.0, "doc_count" : 0 },
"çalışan" : { "from" : 20.0, "to" : 30.0, "doc_count" : 164 },
"orta-yaş" : { "from" : 30.0, "to" : 40.0, "doc_count" : 162 },
"yaşlı" : { "from" : 40.0, "doc_count" : 17 }
}
}
}...

Gruplamanın üstünden metrik işlemler:

GET test/_search
{
"size": 0,
"aggs": {
"cinsiyet_kovası": {
"terms": {
"field": "gender"
},
"aggs": {
"yas_ortalamasi": {
"avg": {
"field": "age"
}
}
}
}
}
}
# Sonuç olarak
# 179 kadının yaş ortalaması: ~30
# 164 erkeğin yaş ortalaması: ~29
#"aggregations" : {
# "cinsiyet_kovası" : {
# "doc_count_error_upper_bound" : 0,
# "sum_other_doc_count" : 0,
# "buckets" : [
# {
# "key" : "female",
# "doc_count" : 179,
# "yas_ortalamasi" : {
# "value" : 30.1731843575419
# }
# },
# {
# "key" : "male",
# "doc_count" : 164,
# "yas_ortalamasi" : {
# "value" : 29.445121951219512
# }
# }
# ]
# }
#}

iç içe kümeleme (nested aggregations)

Grupladığımız sonuçların yaş ortalamasını bulalım :

GET test/_search
{
"size": 0,
"aggs": {
"cinsiyet_kovası": {
"terms": { "field": "gender" },
"aggs": {
"yas_araliklari": {
"range": {
"field": "age",
"keyed": true,
"ranges": [
{ "key": "genç", "to": 20 },
{ "key": "çalışan", "from": 20, "to": 30 },
{ "key": "orta-yaş", "from": 30, "to": 40 },
{ "key": "yaşlı", "from": 40 }
]
},
"aggs": {
"yas_ortalamasi": {
"avg": { "field": "age" }
}
}
}
}
}
}
}
# "aggregations" : {
# "cinsiyet_kovası" : {
# "doc_count_error_upper_bound" : 0,
# "sum_other_doc_count" : 0,
# "buckets" : [
# {
# "key" : "female",
# "doc_count" : 179,
# "yas_araliklari" : {
# "buckets" : {
# "genç" : {
# "to" : 20.0,
# "doc_count" : 0,
# "yas_ortalamasi" : {
# "value" : null
# }
# },
# "çalışan" : {
# "from" : 20.0,
# "to" : 30.0,
# "doc_count" : 81,
# "yas_ortalamasi" : {
# "value" : 24.506172839506174
# }
# },
# "orta-yaş" : {
# "from" : 30.0,
# "to" : 40.0,
# "doc_count" : 90,
# "yas_ortalamasi" : {
# "value" : 34.4
# }
# },
# "yaşlı" : {
# "from" : 40.0,
# "doc_count" : 8,
# "yas_ortalamasi" : {
# "value" : 40.0
# }
# }
# }
# }
# },
# {
# "key" : "male",
# "doc_count" : 164,
# "yas_araliklari" : {
# "buckets" : {
# "genç" : {
# "to" : 20.0,
# "doc_count" : 0,
# "yas_ortalamasi" : {
# "value" : null
# }
# },
# "çalışan" : {
# "from" : 20.0,
# "to" : 30.0,
# "doc_count" : 83,
# "yas_ortalamasi" : {
# "value" : 23.93975903614458
# }
# },
# "orta-yaş" : {
# "from" : 30.0,
# "to" : 40.0,
# "doc_count" : 72,
# "yas_ortalamasi" : {
# "value" : 34.47222222222222
# }
# },
# "yaşlı" : {
# "from" : 40.0,
# "doc_count" : 9,
# "yas_ortalamasi" : {
# "value" : 40.0
# }
# }
# }
# }
# }
# ]
# }
# }

Kümelerin içinde süzmek (filters)

Aşağıdaki sorguda dikkat edeceğimiz noktalar:

  • aggs ile bir toplama/kümeleme yapıyoruz ve filters ile istediğimiz kadar istediğimiz alanda istediğimiz içeriğe göre grupluyoruz.
  • Çıktılara bakarsanız içinde ALARM_MAJOR ve ALARM_CRITICAL olan olmayan hepsi var. Çünkü query ile arama yapmıyor ve çıktıları dilediğimiz şartlara göre süzmüyoruz. Yani kümeleme içindeki filters bize sonuçları süzmez, sonuçlar içinde (query ile arama yapılan ve filters ile içinde süzülen sonuçlar içinde) kümeleme yapar.
GET stoalarm/_search?_source=Detail.nfInstanceId,_id
{
"size": 1,
"aggs": {
"alarm_major": {
"filters": {
"filters": {
"major": { "match": { "Level": "ALARM_MAJOR" } },
"critical": { "match": { "Level": "ALARM_CRITICAL" } }
}
}
}
}
}
# ALARM_CRITICAL olan 38 kayıt varken
# ALARM_MAJOR olan 86 kayıt var
# },
# "aggregations" : {
# "alarm_major" : {
# "buckets" : {
# "critical" : { "doc_count" : 38 },
# "major" : { "doc_count" : 86 }
# }
# }
# }

Kompozit Kümeleme

Birden fazla alanı içeren bir küme oluşturmak istiyorum. Önce sadece Level alanının olduğu bir kümeleme dizisi oluşturup çıktısına bakalım:

GET vtoalarm/_search
{
"query": {
"range": {
"@timestamp": {
"gte": "now-10d/d",
"lt": "now/d"
}
}
},
"size": 0,
"aggs": {
"kompozit-kumeleme": {
"composite": {
"sources": [
{
"Level-Gruplama": {
"terms": {
"field": "Level"
}
}
}
]
}
}

}
}

Komut Çalıştırma (SCRIPT)

script Kelimesinin Türkçe karşılığı “senaryo, diyalog …”.

  • Bizim yedekleri otomatik alıp, sıkıştıran ve ağda yedekler isimli dizine bu sıkıştırılmış dosyayı attıktan sonra aldığı son yedek dosyasını silen bir SENARYO (script) yazar mısın?

Hakikaten yazdığımız satır satır komutlardan oluşan script dosyası, repliklerden oluşmuş bir senaryo gibi değil midir?

Gelin Elasticsearch üstünde script yazmaya bakalım.

"script": {
"lang": "...",
"source" | "id": "...",
"params": { ... }
}

Varsayılan olarak painless (acısız) diliyle script yazılıyor ama expression (ifade) de yazabiliriz. Painless için başlangıç noktanız bu adres olabilir.

Örnekteki kayıt şöyleydi:

{
"first" : "mark" ,
"last" : "giordano" ,
"goals" : [6,30,15] ,
"assists" : [3,30,24] ,
"gp" : [26,60,63] ,
"born" : "1983/10/03"
}

Sorgumuz şöyle olacak ve birazdan daha iyi anlıyor olacağız:

GET hockey/_search
{
"query": {
"function_score": {
"script_score": {
"script": {
"lang": "painless", <-- Tek satır ifade de olabilirdi
"source": """ <-- kodumuz birden çok satırda olacak
int total = 0; <-- Gol değerlerinin toplamını tutacak

Satırın alanı: golas[6,30,15] ve 6+30+15 sonucumuz olacak
for (int i = 0; i < doc['goals'].length; ++i) {
doc ile satıra erişeceğiz. doc['alanAdı'] ile alanın değerine:
total += doc['goals'][i];
}
return total;
"""
}
}
}
}
}

Sonuçları Bir Kısmını Çekmek (top Komutu Gibi)

Sonuçlar sadece hits altında değil ayrıca kümelemelerin (aggs)içinde (bucket — kova) olabilir. Her iki sonuç kümesinde sayfalama yapabilir (from) ve sonuçların bir kısmını (size) alabilirsiniz .”

Hem URL, hem de sorgunun içinde size ile dönecek sonuç sayısını kısıtlayabiliyoruz. Aşağıdaki çıktıda URL içinde belirtilen kadar döndüğünü, gövdede tanımlı sayı olan 5'in etkisiz kaldığını görüyoruz.

Kümeleme içinde sonuçları sınırlamak için de size kullanılabilinir. Önce sınırlama olmaksızın sonuçları getirelim (aggs içinde size yok!):

Şimdi kümenin içinde dönen sonuç sayısını 1'e indirelim:

from İle Başlangıç Belirlemek

Önce sıralayalım ki _score temelli sonuçlar getireceği için sonuçları sürekli farklı sırada görmeyelim. from ile vereceğimiz değerin çalıştığını görebilmek için sort ile @timestamp alanına göre sıralıyoruz:

from değerini 0'a atadığımızda en üstte 01. saniyeden itibaren başlıyor. Ancak from değerini 1 yaptığımızda en üstteki kayıt 47:16 olacak.

from : 1 Olduğunda 47:16 ilk sıraya geldi:

Sıralama (Sorting)

Sıralamayı

  • hem sorgu sonuçlarında
  • hem de kümeleme çıktılarında (bucket)

yapabiliriz.

Sonuçların Sıralanması (sorting hits)

Sorgu sonuçlarının sıralanması için yapımız şöyle:

{ 
"sort" : [
{ "alan_adı": { "order" : "asc|desc" } },
...
],

"query": { ... },

"aggs": { ... }
}

Bir veya daha fazla alana göre sıralamayı artan veya azalan olacak şekilde yapabiliriz.

Önce sadece tarihe göre sıralayalım ve çıktı olarak sadece tarihi görüntüleyelim (POST alarm_yeni_2/_search?size=100&filter_path=hits.hits._source.@timestamp).

Şimdi hostname alanına göre tersine sıralama yaparak sonuçları görüntüleyelim: POST alarm_yeni_2/_search?size=100&filter_path=hits.hits._source.@timestamp,hits.hits._source.hostname

Sorugumuzu buraya da yazalım:

POST alarm_yeni_2/_search?size=100&filter_path=hits.hits._source.@timestamp,hits.hits._source.hostname
{
"sort": [
{
"hostname" : { "order": "desc" },
"@timestamp" : { "order": "desc" }
}
],
"query": {
"bool": {
"filter": [
{
"range": {
"@timestamp": {
"gte": "2020-01-01 01:01:55",
"lt": "2020-12-28 01:01:55",
"format": "yyyy-MM-dd hh:mm:ss"
}
}
},
{
"terms": {
"Level": [ "ALARM_CRITICAL", "ALARM_MAJOR" ]
}
}
],
"must": [
{ "exists": { "field": "Detail.nfInstanceId" } },
{ "exists": { "field": "Detail.serviceInstanceId" } },
{ "exists": { "field": "Level" } }
]
}
},
"aggs": {
"terms_hostname": {
"terms": {
"field": "hostname",
"order": { "_key": "desc" }
},
"aggs": {
"terms_nfInstanceId": {
"terms": {
"field": "Detail.nfInstanceId",
"order": { "_key": "asc" }
},
"aggs": {
"terms_serviceInstanceId": {
"terms": {
"field": "Detail.serviceInstanceId",
"order": { "_key": "asc" }
},
"aggs": {
"terms_level": {
"terms": {
"field": "Level",
"order": { "_key": "asc" }
}
}
}
}
}
}
}
}
}
}

Kümeleme Sonuçlarının Sıralanması (sorting aggregation results)

aggregations içinde terms_hostname isimli kümeleme sonuçlarını sıralamak için "order": { "_key": "desc" } sıralamayı asc veya desc ile aşağıdaki çıktılardaki gibi değiştiriyoruz. SQL’de ORDER BY hostname DESC der gibi

"aggs": {
"terms_hostname": {
"terms": {
"field": "hostname",
"order": { "_key": "desc" }
},
Cem Topkaya
Cem Topkaya

Written by Cem Topkaya

Evlat, kardeş, ağabey, eş, baba, müzik sever, öğrenmek ister, paylaşmaya can atar, iyi biri olmaya çalışır, hakkı geçenlerden helallik ister vs.

Responses (1)