Merhaba,
Bu içeriğimizde Elasticsearch’ün temel düzeyde kullanım mantığını inceleyecek ve biryandan da .NET uygulamaları için tarafımızca geliştireceğimiz gayet kullanışlı, esnek ve özgün bir Elasticsearch servisi inşa edeceğiz. O halde fazla vakit kaybetmeksizin buyurun başlayalım…
Elasticsearch & Kibana Kurulumu (Dockerize)
Kibana Elasticsearch’ü kullanırken tercih edeceğimiz arayüz olacaktır. Nedir bu? diye sorarsanız Elasticsearch Nedir? Temel Kavramları Nelerdir? başlıklı makalemizde yaptığımızı özeti direkt alıntılamakta fayda görmekteyim;
Kibana, Elasticsearch ile etkileşimli görselleştirmeler ve veri analizi sağlayan bir kullanıcı arayüzüdür. Verileri görselleştirme, raporlama, anlık durum panoları oluşturma, etkileşimli analiz yapma vs. gibi yetenekleri mevcuttur.
Evet, şimdi ilk olarak Elasticsearch ile Kibana’nın kurulumundan başlayacağız. Ben kurulum açısından her iki uygulamayı da docker ortamında ayağa kaldıracağımdan dolayı aşağıdaki docker compose dosyasından istifade edeceğim. Tabi ki de sizler istediğiniz farklı şekillerde kurulum gerçekleştirebilir ve içeriğimize eşlik edebilirsiniz.
version: '3.8'
services:
elasticsearch:
image: elasticsearch:8.15.0
environment:
- discovery.type=single-node
- bootstrap.memory_lock=true
- xpack.security.enabled=false
volumes:
- es_data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
kibana:
image: kibana:8.14.3
environment:
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
ports:
- "5601:5601"
volumes:
es_data:
driver: local
Kibana’yı Tanıyalım
Kibana, yukarıdaki satırlardan da anlaşılacağı üzere verisel analizlerden tutun raporlamaya, veri görselleştirmeden anlık durum panoları oluşturmaya kadar birçok kompleks operasyonu yürütebilecek geniş yeteneğe sahip olan bir araçtır. Haliyle bu aracı tanımak bu içeriğin amacının oldukça dışında bir uğraş olacağından dolayı bizler direkt konuya odaklı bir şekilde hareket edecek ve Kibana’nın Elasticsearch veritabanı üzerinden işlem yapmamızı sağlayacak olan modülüne odaklanacağız. Bunun için tek yapılması gereken yukarıdaki docker compose dosyasını çalıştırdıktan sonra tarayıcı üzerinden localhost:5601 adresine gitmek ve soldaki menü üzerinden aşağıdaki görselde olduğu gibi Management kategorisinden Dev Tools menüsüne tıklamaktır.
- [Request Type] : Yapılacak işlemin istek tipine karşılık gelmektedir. GET, POST, PUT ve DELETE olabilir.
- /index_name : İşlem yapılacak olan index’in adına karşılık gelmektedir.
- /_operation : İsteğin türüne göre yapılacak operasyona karşılık gelmektedir. _doc, _search, _update vs. olabilir.
- json_body : İsteğin JSON formatındaki içeriğini ifade eder.
Velhasıl kelam, Kibana üzerinde bu şemaya uygun herhangi bir isteği Elasticsearch’e gönderebilmek için aşağıdaki görselde olduğu gibi yandaki ok ikonuna tıklanması yeterlidir.
Elasticsearch Temel CRUD İşlemleri
Şimdi mümkün mertebe ihtiyaç duyulabilecek operasyonları tek tek başlıklandırarak nasıl uygulanabileceğini incelemeye başlayalım.
- Index Oluşturma
Elasticsearch’de yeni bir index oluşturmak için aşağıdaki gibi PUT isteğinin gönderilmesi yeterlidir;PUT /my_index
Bu istek neticesinde Elasticsearch tarafından aşağıdaki result elde edilecektir.
{ "acknowledged": true, "shards_acknowledged": true, "index": "my_index" } - Mevcut Tüm Index’leri Listeleme
GET /_cat/indices?v
- Index Silme
DELETE /my-index
- Veri Indeksleme
Elasticsearch’de veri indekslerken yapılması gereken aşağıdaki gibi bir istekte bulunmaktır;POST /person/_doc { "name": "Gençay", "surname": "Yıldız", "age": 32 }Elasticsearch’te document eklemek, okumak, güncellemek yahut silmek için endpoint’te kullanılan operasyon
_docanahtarıdır.Bu istek neticesinde
/personindex’i içerisine yeni bir document eklenmiş olacaktır. Ayrıca eğer/personindex’i yoksa, oluşturacak ve ardından veriyi indeksleyecektir. Ve eklenen document’a rastgele bir_iddeğeri verecektir. Eğer ki belirli bir id ile document eklemek istersek bunu aşağıdaki gibi belirtmemiz gerekecektir;POST /person/_doc/5 { "name": "Gençay", "surname": "Yıldız", "age": 32 }Velhasıl, veriyi indeksledikten sonra Elasticsearch aşağıdaki gibi bizlere netice döndürmektedir.
{ "_index": "person", "_id": "5", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 0, "_primary_term": 1 }Görüldüğü üzere burada ilgili verinin hangi index’e, hangi id değeri ile eklendiği vs. bilgileri sunulmaktadır. Nedir bu bilgiler? diye sorarsanız eğer kısaca izah edelim;
- _version, document’in sürüm numarasıdır. Yeni eklenen bir document olduğu için sürümü şimdilik 1’dir. Aynı veri güncellendiği taktirde bu sayı +1 artacaktır.
- _result, yeni bir verinin oluşturulduğunu ifade etmektedir.
- _shards, indeksleme işleminin kaç parçaya dağıtıldığını göstermektedir. Burada; ‘total’, toplam shard sayısına, ‘successful’, başarıyla işlenen shard sayısına ve ‘failed’ ise başarısız olan shard sayısına karşılık gelir.
- _seq_no, document’in dizin numarasıdır. İlk veri olduğu için 0’dır.
- _primary_term, index’te değişiklik oldukça artan değerdir.
- Veri Güncelleme
Veri güncelleme için PUT isteğini aşağıdaki gibi yapmak yeterlidir;PUT /person/_doc/9 { "name": "Hilmi", "surname": "Şuayipoğulları", "age": 48 }Burada verilen id değerine karşın bir değer varsa güncelleme işlemi gerçekleştirilmekte, yoksa eğer ilgili document indekslenmektedir.
- Veri Sorgulama
En basitinden tüm verileri sorgulamak şu şekildedir;GET /person/_search
id’ye göre document sorgulamak istersek eğer bunu da şu şekilde gerçekleştirebiliriz;
GET /person/_doc/9
Sadece belirli alanları döndürmek istersek;
GET /person/_doc/9?_source=name, age
- Wildcard Sorgusu
Veri sorgulama sürecinde belirli bir kalıba uygun taramada bulunabilmek için yıldız(*) ve soru işareti(?) gibi joker karakterlerden istifade etmekteyiz.GET /person/_search { "query": { "wildcard": { "name": { "value": "g??ç*" } } } }Yıldız(*) bir veya daha fazla karaktere karşılık gelirken soru işareti(?) ise sadece bir karakteri ifade etmektedir. Haliyle bu örnekte ‘name’ değerinin ilk harfi ‘g’ olan, ikincisi ve üçüncüsü önemli olmayan, dördüncüsü ‘ç’ olan ve gerisi herhangi bir değer olan verilerin getirilmesini istemiş bulunuyoruz.
- Fuzzy Sorgusu
Özellikle arama süreçlerinde kullanıcılar tarafından yanlış yazımdan kaynaklı kelime hatalarının söz konusu olduğu aramalarda benzer kelimeleri bulmak için fuzzy aramasından istifade edebiliriz.GET /person/_search { "query": { "fuzzy": { "name": { "value": "genay", "fuzziness": "AUTO" } } } }Burada fuzziness değeri ile yanlış yazılma ihtimali olan karakter adedini belirleyebiliriz. ‘AUTO’ değeri ile kelimenin uzunluğuna bağlı olarak otomatik bir davranış sergilenebilir.
- Prefix Sorgusu
Belirli bir ön ek ile başlayan document’leri bulmak için kullanılır.GET /person/_search { "query": { "prefix": { "name": { "value": "g" } } } } - Range Sorgusu
Belirli bir aralıktaki değerleri bulmak için range aramasından istifade edilebilir.GET /person/_search { "query": { "range": { "age": { "gte": 35, "lte": 50 } } } } - Match Sorgusu
Arama sürecinde bir alandaki metni analiz eder, analiz sonucunda elde edilen terimleri kullanarak aramayı gerçekleştirir.GET /news/_search { "query": { "match": { "title": "uçağında aramalar sürüyor: Peçete Bomba var'" } } }Yukarıdaki örnekte esas ‘title’ verisi -Erzurum’a inen Hindistan uçağında aramalar sürüyor: Peçeteye, ‘Bomba var’ yazmış- şeklinde olan bir veriyi elde etmek için match aramasından istifade etmekteyiz.
- Term Sorgusu
Eğer ki tam olarak belirtilen ifadeyi eşleştirmek istiyorsak term aramasından istifade edebiliriz.GET /news/_search { "query": { "term": { "title": { "value": "Erzurum" } } } } - Multi-Match Sorgusu
Birden fazla alanda arama yapma olanağı tanımaktadır.GET /news/_search { "query": { "multi_match": { "query": "Türk aktivist", "fields": [ "title", "content" ] } } }Multi-match sorgusunda ‘query’ alanına verilen değer ‘fields’ alanında bildirilen document alanlarında analiz edilerek sorgulanmaktadır.
- Bool Sorgusu
Bool sorgusu, birden fazla sorguyu bir araya getirip mantıksal ilişki ile birleştirmemizi sağlamakta ve böylece karmaşık sorgu yapıları oluşturmamıza olanak tanımaktadır.GET /news/_search { "query": { "bool": { "must": [ { "match": { "title": "rum'a inen Hindistanlı inek uçağında" } }, { "term": { "date_year": { "value": "2024" } } } ] } } }
Evet, artık Kibana üzerinden Elasticsearch’ün kullanımını hasbel kader de olsa tecrübe etmiş bulunuyoruz. Artık bu aşamadan sonra .NET uygulamaları üzerinde Elasticsearch’ün nasıl kullanılabileceğine odaklanalım ve içeriğimizin ilk satırlarında bahsettiğimiz gibi bu işe odaklı oldukça kullanışlı özgün servisimizi inşa etmeye başlayalım.
NET Uygulamalarında Elasticsearch’ün Kullanımı
Her şeyden önce içerik seyrinde yapacağımız geliştirmeleri hızlıca test edebilmek için bir Asp.NET Core uygulaması oluşturalım ve Elasticsearch çalışmalarını gerçekleştirebilmek için ilk olarak bu projeye Elastic.Clients.Elasticsearch kütüphanesini kurarak başlayalım.
Ardından Elasticsearch sorgulaması sürecinde çalışacağımız veri formatını belirleyebilmek için aşağıdaki gibi bir entity sınfı tasarlayalım;
public class Product : IElasticsearchModal
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
Tabi uygulamada olası diğer veritabanı entity’lerinden Elasticsearch entity’lerini ayırt edebilmek için (bir nevi Marker Pattern mantığında) IElasticsearchModal adında bir interface oluşturalım ve yukarıdaki çalışmada yaptığımız üzere Elasticsearch entity’lerini bu interface’ten implemente ederek gerekli işaretlemeyi sağlayalım.
public interface IElasticsearchModal
{
Guid Id { get; set; }
}
Evet, bu temelden sonra artık Elasticsearch veritabanı işlemlerini gerçekleştirecek olan servisimize odaklanmaya başlayabiliriz. Bunun için oluşturacağımız ‘IElasticsearchService’ isimli interface bize rehber olacaktır ve bu interface’e tanımlayacağımız imzalar bahsi konu olan servisin yeteneklerini özetliyor olacaktır. Şimdi gelin interface’i oluşturalım ve içeriğini aşağıdaki gibi dolduralım.
public interface IElasticsearchService
{
Task<bool> CreateIndexAsync(string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default);
Task<bool> CreateDocumentAsync<T>(T document, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default) where T : IElasticsearchModal;
Task<bool> UpdateDocumentAsync<T>(string documentId, object partialDocument, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default);
Task<bool> DeleteDocumentAsync<T>(string documentId, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default);
Task<long> CountDocumentsAsync<T>(string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default) where T : IElasticsearchModal;
Task<IReadOnlyCollection<T>> GetDocumentsAsync<T>(string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default) where T : IElasticsearchModal;
Task<T> GetDocumentAsync<T>(string documentId, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default) where T : IElasticsearchModal;
Task<IReadOnlyCollection<T>> SearchAsync<T>(Action<SearchRequestDescriptor<T>> searchRequestDescriptor, CancellationToken cancellationToken = default) where T : IElasticsearchModal;
Task<IReadOnlyCollection<T>> MatchQueryAsync<T>(Expression<Func<T, object>> field, string queryKeyword, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal;
Task<IReadOnlyCollection<T>> FuzzyQueryAsync<T>(Expression<Func<T, object>> field, string queryKeyword, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal;
Task<IReadOnlyCollection<T>> WildcardQueryAsync<T>(Expression<Func<T, object>> field, string queryKeyword, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal;
Task<IReadOnlyCollection<T>> BoolQueryAsync<T>(Expression<Func<T, object>> matchField, string matchQueryKeyword, Expression<Func<T, object>> fuzzyField, string fuzzyQueryKeyword, Expression<Func<T, object>> wildcardField, string wildcardQueryKeyword, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal;
Task<IReadOnlyCollection<T>> TermQueryAsync<T>(Expression<Func<T, object>> field, string queryKeyword, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal;
Task<IReadOnlyCollection<T>> ExistsQueryAsync<T>(Expression<Func<T, object>> field, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal;
}
Görüldüğü üzere Elasticsearch üzerinde index oluşturmaktan tutun, document sayısını verene ya da match, fuzzy, wildcard vs. gibi sorguları gerçekleştirene kadar birçok sorumluluğu tarif eden bir arayüz tanımlamış bulunuyoruz. Tabi burada daha fazla yetenek ekleme şansımız olsa da ben içeriğimiz için bu kadarını kâfi görmekte ve bu vaziyette yola devam etmekteyim.
Şimdi burada odaklanmanızı istediğim nokta şudur ki, yukarıda tanımlanmış olan her bir imza esasında Elasticsearch’ün bizlere sunduğu servise bir abstraction sağlamakta ve bu servisi kullanacak developer açısından oldukça kolaylaştırıcı bir arayüz sunmaktadır. Şöyle ki… Şimdi bizler bu interface’i aşağıdaki sınıfta kullandığımızı varsayarsak;
public class ElasticsearchService : IElasticsearchService
{
readonly ElasticsearchClient _elasticsearchClient;
public ElasticsearchService(IConfiguration configuration)
{
ElasticsearchClientSettings elasticsearchClientSettings = new(new Uri(configuration["ElasticsearchSettings:Uri"]));
elasticsearchClientSettings.DefaultIndex(ElasticsearchIndexes.DefaultIndex);
_elasticsearchClient = new(elasticsearchClientSettings);
}
.
.
.
}
Görüldüğü gibi öncelikle Elasticsearch client’a bir bağlantı oluşturmamız gerekecektir. Bunu da Elastic.Clients.Elasticsearch kütüphanesinin bizlere sağlamış olduğu sınıflarla yukarıdaki gibi rahatlıkla gerçekleştirebilmekteyiz. Şimdi, oluşturduğumuz IElasticsearchService interface’inin implementasyonuna odaklanırsak eğer aşağıdaki gibi bir çalışma yapmamız gerekecektir.
public class ElasticsearchService : IElasticsearchService
{
readonly ElasticsearchClient _elasticsearchClient;
public ElasticsearchService(IConfiguration configuration)
{
ElasticsearchClientSettings elasticsearchClientSettings = new(new Uri(configuration["ElasticsearchSettings:Uri"]));
elasticsearchClientSettings.DefaultIndex(ElasticsearchIndexes.DefaultIndex);
_elasticsearchClient = new(elasticsearchClientSettings);
}
public async Task<bool> CreateIndexAsync(string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default)
{
IndexResponse indexResponse = await _elasticsearchClient.IndexAsync(indexName, cancellationToken);
return indexResponse.IsSuccess();
}
public async Task<bool> CreateDocumentAsync<T>(T document, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default) where T : IElasticsearchModal
{
CreateRequest<T> createRequest = new(document, indexName, document.Id);
CreateResponse createResponse = await _elasticsearchClient.CreateAsync(createRequest, cancellationToken);
return createResponse.IsSuccess();
}
public async Task<bool> UpdateDocumentAsync<T>(string documentId, object partialDocument, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default)
{
UpdateRequest<T, object> updateRequest = new(indexName, documentId)
{
Doc = partialDocument
};
UpdateResponse<T> updateResponse = await _elasticsearchClient.UpdateAsync<T, object>(updateRequest, cancellationToken);
return updateResponse.IsSuccess();
}
public async Task<bool> DeleteDocumentAsync<T>(string documentId, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default)
{
DeleteResponse deleteResponse = await _elasticsearchClient.DeleteAsync<T>(indexName, documentId, cancellationToken);
return deleteResponse.IsSuccess();
}
public async Task<long> CountDocumentsAsync<T>(string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default) where T : IElasticsearchModal
{
CountResponse countResponse = await _elasticsearchClient.CountAsync<T>(indexName, cancellationToken);
return countResponse.Count;
}
public async Task<IReadOnlyCollection<T>> SearchAsync<T>(Action<SearchRequestDescriptor<T>> searchRequestDescriptor, CancellationToken cancellationToken = default) where T : IElasticsearchModal
{
SearchResponse<T> searchResponse = await _elasticsearchClient.SearchAsync<T>(searchRequestDescriptor, cancellationToken);
return searchResponse.Documents;
}
public async Task<IReadOnlyCollection<T>> GetDocumentsAsync<T>(string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default) where T : IElasticsearchModal
{
SearchResponse<T> searchResponse = await _elasticsearchClient.SearchAsync<T>(indexName, cancellationToken);
return searchResponse.Documents;
}
public async Task<T> GetDocumentAsync<T>(string documentId, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default) where T : IElasticsearchModal
{
GetResponse<T> getResponse = await _elasticsearchClient.GetAsync<T>(documentId, index => index
.Index(indexName), cancellationToken);
return getResponse.Source;
}
public async Task<IReadOnlyCollection<T>> MatchQueryAsync<T>(Expression<Func<T, object>> field, string queryKeyword, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal
{
SearchResponse<T> searchResponse = await _elasticsearchClient.SearchAsync<T>(index => index
.Index(indexName)
.Query(query => query
.Match(t => t.Field(field)
.Query(queryKeyword)))
.From(from)
.Size(size), cancellationToken);
return searchResponse.Documents;
}
public async Task<IReadOnlyCollection<T>> FuzzyQueryAsync<T>(Expression<Func<T, object>> field, string queryKeyword, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal
{
SearchResponse<T> searchResponse = await _elasticsearchClient.SearchAsync<T>(index => index
.Index(indexName)
.Query(query => query
.Fuzzy(t => t.Field(field)
.Value(queryKeyword)))
.From(from)
.Size(size), cancellationToken);
return searchResponse.Documents;
}
public async Task<IReadOnlyCollection<T>> WildcardQueryAsync<T>(Expression<Func<T, object>> field, string queryKeyword, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal
{
SearchResponse<T> searchResponse = await _elasticsearchClient.SearchAsync<T>(index => index
.Index(indexName)
.Query(query => query
.Wildcard(t => t.Field(field)
.Value(queryKeyword)))
.From(from)
.Size(size), cancellationToken);
return searchResponse.Documents;
}
public async Task<IReadOnlyCollection<T>> BoolQueryAsync<T>(Expression<Func<T, object>> matchField, string matchQueryKeyword, Expression<Func<T, object>> fuzzyField, string fuzzyQueryKeyword, Expression<Func<T, object>> wildcardField, string wildcardQueryKeyword, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal
{
SearchResponse<T> searchResponse = await _elasticsearchClient.SearchAsync<T>(index => index
.Index(indexName)
.Query(query => query
.Bool(t => t.Should(
match => match.Match(t => t.Field(matchField).Query(matchQueryKeyword)),
fuzzy => fuzzy.Fuzzy(p => p.Field(fuzzyField).Value(fuzzyQueryKeyword)),
wildcard => wildcard.Wildcard(p => p.Field(wildcardField).Value(wildcardQueryKeyword))
)))
.From(from)
.Size(size), cancellationToken);
return searchResponse.Documents;
}
public async Task<IReadOnlyCollection<T>> TermQueryAsync<T>(Expression<Func<T, object>> field, string queryKeyword, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal
{
SearchResponse<T> searchResponse = await _elasticsearchClient.SearchAsync<T>(index => index
.Index(indexName)
.Query(query => query
.Term(t => t.Field(field)
.Value(queryKeyword)))
.From(from)
.Size(size), cancellationToken);
return searchResponse.Documents;
}
public async Task<IReadOnlyCollection<T>> ExistsQueryAsync<T>(Expression<Func<T, object>> field, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal
{
SearchResponse<T> searchResponse = await _elasticsearchClient.SearchAsync<T>(index => index
.Index(indexName)
.Query(query => query
.Exists(t => t.Field(field)))
.From(from)
.Size(size), cancellationToken);
return searchResponse.Documents;
}
}
Evet, yukarıda görüleceği üzere ElasticsearchClient üzerinden yapılan sorgulamalar mahiyetlerine bağlı olarak belirli kullanım komplekslikleri barındırmakta ve bu, her ihtiyaç olduğu durumlarda bizlere zorluk yaratabilecek derecelerde olabilmektedir. Dolayısıyla bizler oluşturduğumuz interface sayesinde her bir metoda uygun davranış ve değerleri developer’dan parametre olarak almakta ve buradaki kullanım zorluğunun maliyetini bir kereye mahsus bu concrete servis nesnesi üzerinde ödeyerek, bundan sonrası için kullanımı kolaylaştırmaktayız.
Bunu daha net bir şekilde izah edebilmek için mesela term sorgusunu ele alabiliriz. Şöyle ki, ElasticsearchClient üzerinden bir term sorgusunu yapabilmek için aşağıdaki çalışmaya benzer bir kod geliştirmemiz gerekecektir;
await _elasticsearchClient.SearchAsync<Product>(index => index
.Index("products")
.Query(query => query
.Term(t => t.Field("name")
.Value("şuayip")))
.From(0)
.Size(10));
Şimdi soruyorum, her ihtiyaç olduğunda bu sorguyu tekrar tekrar yazacak babayiğit var mı ulan aranızda? Ne, nerde, nasıl çağrılıyor belli değil 🙂 Ee haliyle yazılımcıysak eğer bi tek tüketici değil bazen de düzeltici olmamız ve bizim bunu daha da kolay kullanılabilir hale getirmemiz gerekiyor değil mi? İşte bu durumda aşağıdaki gibi bir metodu inşa ediyoruz;
public async Task<IReadOnlyCollection<T>> TermQueryAsync<T>(Expression<Func<T, object>> field, string queryKeyword, string indexName = ElasticsearchIndexes.DefaultIndex, CancellationToken cancellationToken = default, int from = 0, int size = 10) where T : IElasticsearchModal
{
SearchResponse<T> searchResponse = await _elasticsearchClient.SearchAsync<T>(index => index
.Index(indexName)
.Query(query => query
.Term(t => t.Field(field)
.Value(queryKeyword)))
.From(from)
.Size(size), cancellationToken);
return searchResponse.Documents;
}
Ve bu metot sayesinde artık term sorgusunun ihtiyacını duyduğumuz her noktada aşağıdaki şekilde çağrım yapmamız yeterli olmaktadır.
var datas = await elasticsearchService.TermQueryAsync<Product>(p => p.Name, queryKeyword: "şuayip", indexName: "products", from: 0, size: 10);
Evet, itiraf edin ki bu daha insancıl bir kod 🙂 İşte bu mantık doğrultusunda interface içerisindeki diğer imzalara karşın davranışlarda artık hangi metoda göre özelleştirilecekse onun fıtratına uygun bir şekilde yapılandırılmış vaziyettedir.
Velhasıl, son olarak inşa ettiğimiz bu yapıyı aşağıdaki gibi minimal api’ler de kullanabilir ve Elasticsearch çalışmasını .NET ortamında başarıyla gerçekleştirebiliriz;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSwaggerGen();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSingleton<IElasticsearchService, ElasticsearchService>();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.MapGet("/products", async (IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
var datas = await elasticsearchService.GetDocumentsAsync<Product>(ElasticsearchIndexes.Products, cancellationToken);
return Results.Ok(datas);
});
app.MapGet("/products/{id}", async (string id, IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
var data = await elasticsearchService.GetDocumentAsync<Product>(id, ElasticsearchIndexes.Products, cancellationToken);
return Results.Ok(data);
});
app.MapGet("/products-details", async (IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
var datas = await elasticsearchService.SearchAsync<Product>(product =>
product.Index(ElasticsearchIndexes.Products)
.From(0)
.Size(10), cancellationToken);
return Results.Ok(datas);
});
app.MapGet("/products-match/{query_keyword}", async (string query_keyword, IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
var datas = await elasticsearchService.MatchQueryAsync<Product>(p => p.Name, query_keyword, ElasticsearchIndexes.Products, cancellationToken);
return Results.Ok(datas);
});
app.MapGet("/products-fuzzy/{query_keyword}", async (string query_keyword, IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
var datas = await elasticsearchService.FuzzyQueryAsync<Product>(p => p.Name, query_keyword, ElasticsearchIndexes.Products, cancellationToken);
return Results.Ok(datas);
});
app.MapGet("/products-wildcard/{query_keyword}", async (string query_keyword, IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
var datas = await elasticsearchService.WildcardQueryAsync<Product>(p => p.Name, query_keyword, ElasticsearchIndexes.Products, cancellationToken);
return Results.Ok(datas);
});
app.MapGet("/products-exists", async (IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
var datas = await elasticsearchService.ExistsQueryAsync<Product>(p => p.Name, ElasticsearchIndexes.Products, cancellationToken);
return Results.Ok(datas);
});
app.MapGet("/products-bool", async (IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
var datas = await elasticsearchService.BoolQueryAsync<Product>(p => p.Name, "Mouse", p => p.Name, "erasr", p => p.Name, "*se*", ElasticsearchIndexes.Products, cancellationToken);
return Results.Ok(datas);
});
app.MapGet("/products-term/{query_keyword}", async (string query_keyword, IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
var datas = await elasticsearchService.TermQueryAsync<Product>(p => p.Name, query_keyword, ElasticsearchIndexes.Products, cancellationToken);
return Results.Ok(datas);
});
app.MapGet("/products-count", async (IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
var count = await elasticsearchService.CountDocumentsAsync<Product>(ElasticsearchIndexes.Products, cancellationToken);
return Results.Ok(count);
});
app.MapPost("/products", async (CreateProductVM createProductVM, IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
Product product = new()
{
Name = createProductVM.Name,
Price = createProductVM.Price,
Quantity = createProductVM.Quantity
};
bool result = await elasticsearchService.CreateDocumentAsync(product, ElasticsearchIndexes.Products, cancellationToken);
return Results.Ok(result);
});
app.MapPut("/products", async (UpdateProductVM updateProductVM, IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
bool result = await elasticsearchService.UpdateDocumentAsync<Product>(updateProductVM.Id, updateProductVM, ElasticsearchIndexes.Products, cancellationToken);
return Results.Ok(result);
});
app.MapDelete("/products/{id}", async (string id, IElasticsearchService elasticsearchService, CancellationToken cancellationToken) =>
{
bool result = await elasticsearchService.DeleteDocumentAsync<Product>(id, ElasticsearchIndexes.Products, cancellationToken);
return Results.Ok(result);
});
app.Run();
Evet, böylece Elasticsearch’ün nasıl kullanıldığına Kibana ve .NET ortamı eşliğinde göz atmış bulunuyor ve Elastic.Clients.Elasticsearch kütüphanesinin getirdiği yeteneklere kendimizce özelleştirilmiş abstraction uygulayarak daha kullanışlı ve özgün bir yapıyı ortaya koymuş oluyoruz. Ve böylece bu uzun ve meşakkatli içeriği taktirlerinize sunarak burada noktalıyorum 🙂
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…
Not : Örnek çalışmaya aşağıdaki github adresinden erişebilirsiniz.
https://github.com/gncyyldz/Elasticsearch.Example
