İçeriğe geç →

ElasticSearch Serisi 03 – C# ile Genişletilebilir Temel Search ve Filter Yapısı

Yeni bir ElasticSearch seri ile tekrar merhaba arkadaşlar.

elasticsearch-nest

Bir önceki seriden hatırlarsak oluşturmuş olduğumuz index içerisine, hem tek olarak hem de bulk olarak product’lar eklemiştik. Bu noktaya kadar artık her şeyimiz mevcut. Bir adet “product_search” alias’ına sahip indeximiz ve içerisinde de bir kaç ürün var. Geriye artık yavaş yavaş ElasticSearch’ün asıl amacı olan search işlemlerine girmenin zamanı geldi.

Bu makale içerisinde ElasticSearch üzerinde temel olarak nasıl search işlemlerini gerçekleştirebiliriz, filtre nedir ve nasıl kullanılır gibi maddeleri ele alıyor olacağız.

Öncelikle bir önceki seride implemente etmiş olduğumuz “BulkIndex” method’unu kullanarak bir kaç adet daha product ekleyelim. Eklemiş olduğumuz product’lar üzerinden search ve filter işlemlerini gerçekleştireceğiz. “ElasticSearch.DataTransfer” projesi içerisinde çağırmış olduğumuz “BulkIndex” method’unu aşağıdaki gibi değiştirip, çalıştıralım.

Bu işlemin ardından Sense üzerinden aşağıdaki query’i execute edelim ve response’a bir bakalım.

Query sonucundaki response aşağıdaki gibi olacaktır.

“BulkIndex” method’u ile eklemiş olduğumuz product’ların, geldiğini görebilmekteyiz. Yeterli sayıda product elde ettiğimize göre artık temel olarak search işlemlerine başlayabiliriz.

1) Query DSL & Basic Search

Query yapısını anlatım kısmında ilk önce Sense üzerinden işlemlerimizi pratik bir şekilde gerçekleştireceğiz ve ardından C# tarafında bunu nasıl implemente edebileceğimize bir bakacağız. Bu işlemlerin öncesinde biraz Query DSL yapısına bir göz atalım.

ElasticSearch query’leri tanımlayabilmek için, JSON temelli bir Query DSL (Domain Specific Language) yapısı kullanır. Bu yapı ise iki maddeye dayanır:

  • Leaf Query: Belirli bir alan üzerindeki belirli bir değerlere bakılmak istenildiğinde kullanılır.
  • Compound Query: Leaf Query’leri veya Compound Query’leri sarmalayarak, beklenen doğrultusunda birden fazla sorguları birleştirmek için kullanılır.

Query structure’ını daha iyi kavrayabilmek için dilerseniz basic bir search query’si yazalım.

İlk olarak URI terminolojisine baktığımızda HTTP verb’lerinden “GET” verb’ünü yazdıktan sonra

şeklinde bir URI path’i oluşturuyoruz ve devamındaki JSON objesine baktığımızda ise, içerisinde bir adet “query” context’i bulunmakta. Bu query context’i içerisinde kullanılacak olan herhangi bir Leaf Query veya Compound Query, ElasticSearch’ün kendi referans sitesinde de bahsettiği gibi şu soruya cevap verir “How well does this document match this query clause?” ve ayrıca eşleşenler arasında bir skorlama(scoring) işlemi gerçekleştirir. Yukarıdaki query context içerisinde kullanmış olduğumuz “term” query, leaf query’e bir örnektir. Burada “product” type’ının “name” property’si üzerinde bir contain işlemi yapmaktadır. Farklı ihtiyaçlara yönelik diğer query örneklerine ise buraya tıklayarak, sağ bölümde bulunan “Query DSL” tree’si altından erişebilirsiniz.

JSON objesi üzerine query contextden farklı olarak kullanabileceğimiz önemli bir kaç alana daha bakmak gerekirse:

  • size: geriye dönecek olan result sayısı (default 10)
  • from: result içerisindeki offset (default 0)
  • fields: geriye dönmesini istediğiniz field’lar
  • sort: neye göre sıralama yapılacağı
  • facets: data içerisindeki belirli bir field(lar) özelinde özet bilgileri getirmektedir (örneğin bir e-ticaret sitesinde bir ürün aradığınızda, genelde sol menüde o ürün özelinde hangi renkten kaç adet, hangi markadan kaç adet mevcut olduğu bilgileri)
  • filter: makalenin ilerleyen bölümlerinde detaylı olarak ele alacağız ama özetle filtreler query’leri daha fazla özelleştirebilmek için kullanılır

Terminolojiye baktıktan sonra şimdi yazmış olduğumuz basic search query’sini, Sense üzerinden execute edelim ve response’a bir bakalım.

Result’a baktığımızda term query tüm “name” field’ları içerisinde “iphone” kelimesi geçen product’ları getirmiş ve bir scoring işlemi gerçekleştirerek, en uygun olanına göre sırlama işlemini yapmıştır.

Dilerseniz uygulamış olduğumuz bu basic search işlemini, diğer makale serilerinde de olduğu gibi ElasticSearch projesi içerisine implemente edelim. Bunun için ilk olarak “ElasticSearch.Data.Contracts” projesine giderek, içerisinde aşağıdaki gibi yeni bir DTO tanımlayalım.

SearchResponseDTO, “IndexResponseDTO” class’ından türeyerek içerisinde bir adet IEnumerable tipinde “Documents” barındırıyor. Bu dokümanlar search işlemi sonucunda, geriye dönecek olan dokümanlardır. Şimdi search method’unu “IElasticContext” interface’i üzerinde tanımlayabiliriz.

Eklemiş olduğumuz “Search” method’u parametre olarak NEST’e ait olan ISearchRequest interface’ini almaktadır ve geriye biraz önce eklemiş olduğumuz “SearchResponseDTO” yu dönmektedir. “ElasticContext” üzerine yeni method’u implemente edebiliriz.

Search method’unda tek yaptığımız, dışarıdan aldığımız “searchRequest” parametresini “_elasticClient” üzerindeki “Search” method’una geçmektir. Bu işlemin sonucunda ise NEST, verilen kriterler doğrultusunda search işlemini gerçekleştirerek resoponse’u dönmektedir. “SearchResponseDTO” yu ise “IndexResponseDTO” dan türettiğimiz için “IsValid”, “StatusMessage” ve “Exception” parametrelerini tekrar yazmanın önüne geçmiş olduk ve ilgili property’leri response’a göre doldurarak, geriye dönüyoruz.

Şimdi geldik asıl search logic’in işleyeceği kısma. Yani “Search” method’una parametre olarak geçtiğimiz “searchRequest” objesinin hazırlanmasına. Burada öyle bir design yapalım ki istediğimiz query’leri, filter’ları kolayca eklemeye ve geliştirilmeye açık bir yapı olsun. Uzun uzaya giden method overload’ları gibi anti-pattern‘lere gitmeden, esnek bir yapı kuracağız. Logic işlemlerine başlamak için öncelikle “ElasticSearch” solution’ına “ElasticSearch.Business.Contracts” isminde yeni bir class library ekleyelim ve içerisinde “IElasticSearchEngine” isminde bir interface tanımlayalım.

“IElasticSearchEngine” interface’inin içerisine tanımlamış olduğumuz “Execute” method’u içerisinde search işlemlerimizi gerçekleştireceğiz. Engine katmanı eklememizin temel sebebi ise, bu tarz logic içeren işlemleri burada toplamaktır. “ElasticSearch” solution’ına “ElasticSearch.Business” isminde bir class library daha ekleyelim. Burada ise interface’leri implemente edeceğimiz asıl concrete engine’ler yer alacaktır. Library’nin eklenmesinden sonra “ElasticSearch.Business.Contracts”, “ElasticSearch.Data.Contracts” ve “ElasticSearch.Data” library’lerini referans olarak ekleyelim ve Nuget Manager üzerinden “NEST” kütüphanesini de kuralım.

“ElasticSearch.Business” library’si içerisine “Business Engines” isminde bir folder ekleyip, içerisine “ElasticSearchEngine” isminde bir class tanımlıyorum. Eklemiş olduğumuz class’a “IElasticSearchEngine” interface’ini şimdilik boş olacak şekilde implemente edelim.

İçerisinde private olarak tanımlamış olduğumuz field’ları constructor aracılığı ile daha sonra inject edeceğiz. Yukarıdaki bölümde hatırlarsak, uzun uzaya giden method overload’ları gibi anti-pattern’lerden kaçınacağımızı ve kolayca geliştirmeye açık bir design yapacağımızdan bahsetmiştik. “ElasticSearchEngine” i bir Builder ile Fluently bir şekilde oluşturmaya ne dersiniz? Yani düşünelim ki şu şekilde bir kullanımı olsa:

Ne kadar da akıcı duruyor değil mi? E haydi ozaman implementasyon işlemlerine başlayalım.

“ElasticSearch.Business” projesi içerisine “ElasticSearchBuilder” isminde bir class ekleyelim. Bu class içerisinde Builder pattern’ini, search logic’ine yönelik olarak implemente edeceğiz. Builder pattern’i özetle kompleks class’ların nesne üretim süreçlerini, client’dan gizleyen bir pattern’dir ve GOF desenleri arasından Creational grubunda yer almaktadır.

“ElasticSearchBuilder” içerisinde “ElasticSearchEngine” içerisinde de tanımlamış olduğumuz field’ları, internal olarak tanımladık. Aynı assembly içerisinden erişilmesi bizim için yeterli olacaktır. Çünkü bu parametrelere daha sonra “ElasticSearchEngine” içerisinden erişeceğiz. Constructor’da ise, “indexName” ve “elasticContext” parametrelerini zorunlu olduğu için costructor aracılığı ile inject ediyoruz ve ardından “QueryContainer” ı initialize ediyoruz. Bunun sebebi ise NEST’in elastic client’ı query container aracılığı ile query’leri almaktadır ve bizde Builder içerisinde Fluently bir yapı kuracağımız için, ilk başta “QueryContainer” ı initialize ediyoruz.

Diğer parametreler elastic için must olmadıklarından dolayı “SetSize”, “SetFrom” ve “AddTermQuery” olarak farklı method’lara böldük ve hepsi ilgili işlemlerini yapıp geriye yine kendi context’ini dönmektedir. Son olarak “Build” method’u ise artık Fluently olarak eklememiz bittikten sonra çağıracağımız son method olacaktır. Burada yeni bir “ElasticSearchEngine” initialize edip, constructor aracılığı ile kendisini yani “ElasticSearchBuilder” ı parametreleri ile beraber inject etmektedir.

Şimdi “ElasticSearchEngine” class’ına geri dönelim ve artık constructor aracılığı ile bir “ElasticSearchBuilder” alabilecek bir hale getirelim, ardından ilgili field’ları builder’a göre set’leyelim.

Constructor aracılığı ile bir adet “ElasticSearchBuilder” aldık ve yukarıdaki field’ları setledik. Bu işlemin ardından ise “Execute” method’unda “_elasticContext” üzerindeki “Search” method’unu çağırdık ve ilgili parametreleri verdik. Eğer response sonucu “IsValid” ise, response üzerinden gelen dokümanları List of T tipinde geriye dönüyoruz.

Implementasyonumuz şuan bitmiş durumdadır. Hemen “ElasticSearch.DataTransfer” projesi altındaki “Program.cs” class’ını aşağıdaki gibi düzenleyelim ve bir test yapalım.

“SearchProduct” method’unda parametre olarak “indexName” ve “elasticContext” i gönderiyoruz ve “ElasticSearchBuilder” aracılığı ile, doldurmuş olduğumuz parametrelere göre bir “ElasticSearchEngine” initialize ediyoruz. Bu işlemin sonucunda “Execute” method’una istemiş olduğumuz tipi verip, search işlemini başlatıyoruz. Console ekranındaki sonuç ise aşağıdaki gibi olacaktır.

search-result

2) Filter Kullanımı

Her ne kadar query’ler ile istediğimiz düzeyde işlem yapabiliyor olsak da, bazı durumlarda query’leri filter kullanarak daha fazla özelleştirmeye ihtiyaç duyabiliriz. Filter’ları tıpkı SQL’de olduğu gibi “WHERE” statement’ına benzetebiliriz. DSL üzerinde iki farklı filter bulunmaktadır. Bunlar:

  • Filtered: Sorgu execute edilirken filtreleme işlemini yapmaktadır bir nevi prefiltering
  • Filter: Sorgu execute edildikten sonra filtreleme işlemini gerçekleştirmektedir yani postfiltering

Filtered query Filter’a göre daha performanslı çalışmaktadır. Filter executing işleminden sonra filtreleme işlemi gerçekleştirdiği için, CPU’ya olan maliyeti daha fazladır. Tabi bu filtre’yi kullanmak, CPU’ya olan maliyetine göre değil istenilen sonucun doğrultusunda olmalıdır. Çünkü atmış olduğunuz query’deki Aggregation‘ların ve Hits‘lerin filtrelemeden etkilenmemesini isteyebilirsiniz. İşte bu gibi durumlarda Filter yani postfiltering işlemi uygulanmaktadır.

Örneğimiz üzerinden devam etmek gerekirse, aşağıdaki gibi basit bir filtre yazalım.

Yukarıdaki sorguda query context içerisine “filtered” ve “filter” olmak üzere iki filtreyi de ekledik. Filtered kapsamında prefiltering olarak bir adet term filter ekledik ve “name” field’larında “iphone” kelimesi geçenleri getirmesini söyledik. Filter kapsamında ise postfiltering olarak bir adet range filter taktık. Bu filtre ile “3900” değerinden büyük ve eşitleri, “5000” değerinden ise küçük ve eşitleri filtrelemesini söyledik. Bu işlemin sonucunda ise response, Sense üzerinde aşağıdaki gibi olacaktır:

Sonuca baktığımızda “3950” ve “5000” price değerlerine sahip iki adet product’ın geldiğini görüyoruz. Dilerseniz şimdi filter ekleme işlemlerini, geliştirmiş olduğumuz builder yapısına implemente edelim.

“ElasticSearchBuilder” class’ını artık filtered ve filter query’ler üzerinden build yapabilecek şekilde güncelleyelim.

Öncelikle constructor’da “QueryContainer” ın “Filtered” property’sini initialize ediyoruz. “AddTermQuery” method’unda ise, direkt olarak “QueryContainer” ın “Query” property’sine set etmek yerine “Filtered” içerisindeki “Query” e set ediyoruz. Sorgumuzda bir adette range filter vardı hatırlarsak. Bunun için ise “AddRangeFilter” isminde bir method tanımlıyoruz ve içerisinde “QueryContainer.Filtered” içerisindeki “Filter” property’sine “NumericRangeQuery” initialize edip, almış olduğumuz parametrelere göre property’lerini set ediyoruz.

Örneğimiz gereği kurmuş olduğumuz bu yapıda bir adet “Filtered Query” ve bir adet “Filter” ekleyebilmekteyiz. Sizler ise bu builder yapısını kullanarak NEST’in interface’leri aracılığı ile istediğiniz Query’leri array olarak toplayıp, birden fazla filter’ları “QueryContainer” a ekleyebilme gibi logic’leri kazandırabilirsiniz.

Şimdi “ElasticSearch.DataTransfer” projesi içerisindeki “Program.cs” class’ını aşağıdaki gibi güncelleyelim.

Tek fark olarak Fluently bir şekilde sadece “AddRangeFilter” method’unu çağırdık. Console uygulamasını çalıştıralım ve sonucu birde ekran üzerinden görelim.

search-filters

Range filter’a uygun product’ların listelendiğini görebilmekteyiz.

Bir makalenin daha sonuna geldik. Bu makale kapsamında hem temel search işlemleri, filtered ve filter query’ler gibi knowhow’ları edindik ve C# tarafında NEST kütüphanesi ile nasıl implemente edilebileceğini gördük. Aynı zamanda Builder pattern’i ve Fluent interface yaklaşımları için de güzel de bir örnek oldu.

Bir sonraki makalede görüşmek dileğiyle.

ElasticSearch-Search-ve-Filter-Kullanimi

Bu makale toplam (1949) kez okunmuştur.

16
0



Kategori: Architectural Search Engine

10 Yorum

  1. mustafa bayer mustafa bayer

    Merhaba, çok güzel bir çalışma üzerinden biraz zaman geçmiş, bazı şeyler çalışmıyor. foreach’den System.NullReferenceException hatası alıyorum biraz uğraştım ama çözemedim. ne tavsiye edersiniz. teşekkürler.

    • Merhaba, eğer NEST paketinin güncel versiyonunu çekti iseniz değişimlerden dolayı hata alıyor olabilirsiniz. Screen gönderebilirseniz yardımcı olmaya veya uygun bir vakitte yeni versiyonlar ile güncellemeye çalışacağım.

  2. Hakan Hakan

    Hocam mükemmel anlatmışsınız.Ufak bir Mvc projesi üzerinden gitmeniz mümkün olur mu en azından jquery ile bağlayıp searrc vs işlemler yapabilmemiz için.Şimdiden çok teşekkürler

    • Merhaba, uygun bir vakitte güzel bir senaryo belirleyip ele almaya çalışacağım. Teşekkürler yorumunuz için.

  3. Halil Halil

    Hocam elasticSearchEngine değeri data olmasına rağmen null geliyor.

    elasticSearchEngine.ForEach(x =>
    Console.WriteLine(“Id: {0} Name: {1} Description: {2} Price: {3}”, x.Id, x.Name, x.Description, x.Price));

  4. Ali Ali

    Hocam böyle bir hata alıyorum;

    Invalid NEST response built from a unsuccessful low level call on POST: /product_search_201607242334/product/_search
    # Audit trail of this API call:
    – BadResponse: Node: http://localhost:9200/ Took: 00:00:00.0803940
    # ServerError: ServerError: 400Type: parsing_exception Reason: “no [query] registered for [filtered]”
    # OriginalException: System.Net.WebException: Uzak sunucu hata döndürdü: (400) Hatalı İstek.
    konum: System.Net.HttpWebRequest.GetResponse()
    konum: Elasticsearch.Net.HttpConnection.Request[TReturn](RequestData requestData) C:\Users\russ\source\elasticsearch-net-2.x\src\Elasticsearch.Net\Connection\HttpConnection.cs içinde: satır 140
    # Request:

    # Response:

        • Merhaba benim örnekte kullandığım elastic versiyonu 2.x serisi. 6 serisinde çok fazla breaking change’ler gerçekleşti. O yüzden hata alıyorsunuz. Yeni makalelerde güncelliyor olacağım yeni elastic versiyonuna göre.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

*