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

Yeni bir ElasticSearch seri ile tekrar merhaba arkadaşlar.

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.

private static void BulkIndex(ElasticContext elasticContext, string indexName)
{
    var response = elasticContext.BulkIndex(indexName, new List<Product>()
            {
                new Product()
                {
                    Id = 1,
                    Name = "Iphone 6s Plus",
                    Description = "64gb Iphone 6s Plus, Renk: Space Gray",
                    Price = 5000
                },
                new Product()
                {
                    Id = 2,
                    Name = "Sony Xperia Z Ultra",
                    Description = "Full HD ekran, Renk: Siyah",
                    Price = 1200
                },
                new Product()
                {
                    Id = 3,
                    Name = "Samsung Galaxy S7 Edge",
                    Description = "Amelod ekran, Renk: Beyaz",
                    Price = 2800
                },
                new Product()
                {
                    Id = 4,
                    Name = "Samsung Galaxy S7",
                    Description = "Amelod ekran, Renk: Beyaz",
                    Price = 2500
                },
                new Product()
                {
                    Id = 5,
                    Name = "Iphone 6s",
                    Description = "64gb Iphone 6s, Renk: Space Gray",
                    Price = 3800
                },
                new Product()
                {
                    Id = 6,
                    Name = "Iphone 6s",
                    Description = "128gb Iphone 6s, Renk: Space Gray",
                    Price = 3950
                },
            });

    Console.WriteLine(response.StatusMessage);
}

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

GET product_search/product/_search

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

{
  "took": 25,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 6,
    "max_score": 1,
    "hits": [
      {
        "_index": "product_search_201607242334",
        "_type": "product",
        "_id": "5",
        "_score": 1,
        "_source": {
          "id": 5,
          "name": "Iphone 6s",
          "description": "64gb Iphone 6s, Renk: Space Gray",
          "price": 3800
        }
      },
      {
        "_index": "product_search_201607242334",
        "_type": "product",
        "_id": "2",
        "_score": 1,
        "_source": {
          "id": 2,
          "name": "Sony Xperia Z Ultra",
          "description": "Full HD ekran, Renk: Siyah",
          "price": 1200
        }
      },
      {
        "_index": "product_search_201607242334",
        "_type": "product",
        "_id": "4",
        "_score": 1,
        "_source": {
          "id": 4,
          "name": "Samsung Galaxy S7",
          "description": "Amelod ekran, Renk: Beyaz",
          "price": 2500
        }
      },
      {
        "_index": "product_search_201607242334",
        "_type": "product",
        "_id": "6",
        "_score": 1,
        "_source": {
          "id": 6,
          "name": "Iphone 6s",
          "description": "128gb Iphone 6s, Renk: Space Gray",
          "price": 3950
        }
      },
      {
        "_index": "product_search_201607242334",
        "_type": "product",
        "_id": "1",
        "_score": 1,
        "_source": {
          "id": 1,
          "name": "Iphone 6s Plus",
          "description": "64gb Iphone 6s Plus, Renk: Space Gray",
          "price": 5000
        }
      },
      {
        "_index": "product_search_201607242334",
        "_type": "product",
        "_id": "3",
        "_score": 1,
        "_source": {
          "id": 3,
          "name": "Samsung Galaxy S7 Edge",
          "description": "Amelod ekran, Renk: Beyaz",
          "price": 2800
        }
      }
    ]
  }
}

“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.

GET product_search/product/_search
{
  "query": {
    "term": {
      "name": {
        "value": "iphone"
      }
    }
  }
}

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

{index adı veya alias}/{type}/_search

ş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.

{
  "took": 6,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": 0.8784157,
    "hits": [
      {
        "_index": "product_search_201607242334",
        "_type": "product",
        "_id": "6",
        "_score": 0.8784157,
        "_source": {
          "id": 6,
          "name": "Iphone 6s",
          "description": "128gb Iphone 6s, Renk: Space Gray",
          "price": 3950
        }
      },
      {
        "_index": "product_search_201607242334",
        "_type": "product",
        "_id": "5",
        "_score": 0.19178301,
        "_source": {
          "id": 5,
          "name": "Iphone 6s",
          "description": "64gb Iphone 6s, Renk: Space Gray",
          "price": 3800
        }
      },
      {
        "_index": "product_search_201607242334",
        "_type": "product",
        "_id": "1",
        "_score": 0.15342641,
        "_source": {
          "id": 1,
          "name": "Iphone 6s Plus",
          "description": "64gb Iphone 6s Plus, Renk: Space Gray",
          "price": 5000
        }
      }
    ]
  }
}

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.

using System.Collections.Generic;

namespace ElasticSearch.Data.Contracts
{
    public class SearchResponseDTO<T> : IndexResponseDTO
    {
        public IEnumerable<T> Documents { get; set; }
    }
}

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.

using Nest;
using System.Collections.Generic;

namespace ElasticSearch.Data.Contracts
{
    public interface IElasticContext
    {
        IndexResponseDTO CreateIndex<T>(string indexName, string aliasName) where T : class;
        IndexResponseDTO Index<T>(string indexName, T document) where T : class;
        IndexResponseDTO BulkIndex<T>(string indexName, List<T> document) where T : class;
        SearchResponseDTO<T> Search<T>(ISearchRequest searchRequest) where T : class;
    }
}

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.

public SearchResponseDTO<T> Search<T>(ISearchRequest searchRequest) where T : class
{
    var response = _elasticClient.Search<T>(searchRequest);

    return new SearchResponseDTO<T>()
    {
        IsValid = response.IsValid,
        StatusMessage = response.DebugInformation,
        Exception = response.OriginalException,
        Documents = response.Documents
    };
}

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.

using System.Collections.Generic;

namespace ElasticSearch.Business.Contracts
{
    public interface IElasticSearchEngine
    {
        List<T> Execute<T>() where T : class;
    }
}

“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.

using System.Collections.Generic;
using System.Linq;
using Nest;
using ElasticSearch.Business.Contracts;
using ElasticSearch.Data.Contracts;

namespace ElasticSearch.Business
{
    public class ElasticSearchEngine : IElasticSearchEngine
    {
        private readonly string _indexName;
        private readonly int _size;
        private readonly int _from;
        private readonly IQueryContainer _queryContainer;
        private readonly IElasticContext _elasticContext;

        public ElasticSearchEngine()
        {
        }

        #region IElasticSearchEngine Members
        public List<T> Execute<T>() where T : class
        {
            return null;
        }
        #endregion
    }
}

İç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:

var elasticSearchEngine = new ElasticSearchBuilder(indexName)
        .SetElasticContext(elasticContext)
        .SetSize(5)
        .SetFrom(0)
        .AddTermQuery("iphone", "name")
        .AddBlaBlaQuery()
        .AddBlaBlaFilter()
        .Build()
        .Execute<Product>();

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.

using ElasticSearch.Data.Contracts;
using Nest;

namespace ElasticSearch.Business
{
    public class ElasticSearchBuilder
    {
        internal string IndexName;
        internal int Size;
        internal int From;
        internal IQueryContainer QueryContainer;
        internal IElasticContext ElasticContext;

        public ElasticSearchBuilder(string indexName, IElasticContext elasticContext)
        {
            IndexName = indexName;
            ElasticContext = elasticContext;

            QueryContainer = new QueryContainer();
        }

        public ElasticSearchBuilder SetSize(int size)
        {
            Size = size;

            return this;
        }

        public ElasticSearchBuilder SetFrom(int from)
        {
            From = from;

            return this;
        }

        public ElasticSearchBuilder AddTermQuery(string term, string field)
        {
            QueryContainer.Term = new TermQuery()
            {
                Field = new Field()
                {
                    Name = field

                },
                Value = term
            };

            return this;
        }

        public ElasticSearchEngine Build()
        {
            return new ElasticSearchEngine(this);
        }
    }
}

“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.

using System.Collections.Generic;
using System.Linq;
using Nest;
using ElasticSearch.Business.Contracts;
using ElasticSearch.Data.Contracts;

namespace ElasticSearch.Business
{
    public class ElasticSearchEngine : IElasticSearchEngine
    {
        private readonly string _indexName;
        private readonly int _size;
        private readonly int _from;
        private readonly IQueryContainer _queryContainer;
        private readonly IElasticContext _elasticContext;

        public ElasticSearchEngine(ElasticSearchBuilder elasticSearchBuilder)
        {
            _indexName = elasticSearchBuilder.IndexName;
            _size = elasticSearchBuilder.Size;
            _from = elasticSearchBuilder.From
            _queryContainer = elasticSearchBuilder.QueryContainer;
            _elasticContext = elasticSearchBuilder.ElasticContext;
        }

        #region IElasticSearchEngine Members
        public List<T> Execute<T>() where T : class
        {
            var response = _elasticContext.Search<T>(new SearchRequest(_indexName, typeof(T))
            {
                Size = _size,
                From = _from,
                Query = (QueryContainer)_queryContainer
            });

            if (response.IsValid)
            {
                return response.Documents.ToList();
            }

            return null;
        }
        #endregion
    }
}

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.

static void Main(string[] args)
{
    string indexName = ConfigurationManager.AppSettings["ElasticSearchIndexName"];

    var elasticClient = new ElasticClient(ElasticHelper.Instance.GetConnectionSettings());
    var elasticContext = new ElasticContext(elasticClient);

    //CreateIndex(elasticContext, indexName);

    //Index(elasticContext, indexName);

    //BulkIndex(elasticContext, indexName);

    SearchProduct(indexName, elasticContext);

    Console.ReadKey();
}

private static void SearchProduct(string indexName, ElasticContext elasticContext)
{
    var elasticSearchEngine = new ElasticSearchBuilder(indexName, elasticContext)
            .SetSize(5)
            .SetFrom(0)
            .AddTermQuery("iphone", "name")
            .Build()
            .Execute<Product>();

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

“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.

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.

GET product_search/product/_search
{
  "from": 0,
  "size": 5,
  "query": {
    "filtered": {
      "query": {
        "term": {
          "name": {
            "value": "iphone"
          }
        }
      },
      "filter": {
        "range": {
          "price": {
            "gte": 3900,
            "lte": 5000
          }
        }
      }
    }
  }
}

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:

{
  "took": 62,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 0.8784157,
    "hits": [
      {
        "_index": "product_search_201607242334",
        "_type": "product",
        "_id": "6",
        "_score": 0.8784157,
        "_source": {
          "id": 6,
          "name": "Iphone 6s",
          "description": "128gb Iphone 6s, Renk: Space Gray",
          "price": 3950
        }
      },
      {
        "_index": "product_search_201607242334",
        "_type": "product",
        "_id": "1",
        "_score": 0.15342641,
        "_source": {
          "id": 1,
          "name": "Iphone 6s Plus",
          "description": "64gb Iphone 6s Plus, Renk: Space Gray",
          "price": 5000
        }
      }
    ]
  }
}

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.

using ElasticSearch.Data.Contracts;
using Nest;

namespace ElasticSearch.Business
{
    public class ElasticSearchBuilder
    {
        internal string IndexName;
        internal int Size;
        internal int From;
        internal IQueryContainer QueryContainer;
        internal IElasticContext ElasticContext;

        public ElasticSearchBuilder(string indexName, IElasticContext elasticContext)
        {
            IndexName = indexName;
            ElasticContext = elasticContext;

            QueryContainer = new QueryContainer();
            QueryContainer.Filtered = new FilteredQuery();
        }

        public ElasticSearchBuilder SetSize(int size)
        {
            Size = size;

            return this;
        }

        public ElasticSearchBuilder SetFrom(int from)
        {
            From = from;

            return this;
        }

        public ElasticSearchBuilder AddTermQuery(string term, string field)
        {
            QueryContainer.Filtered.Query = new TermQuery()
            {
                Field = new Field()
                {
                    Name = field

                },
                Value = term
            };

            return this;
        }

        public ElasticSearchBuilder AddRangeFilter(double gte, double lte, string field)
        {
            QueryContainer.Filtered.Filter = new NumericRangeQuery()
            {
                Field = new Field()
                {
                    Name = field
                },
                GreaterThanOrEqualTo = gte,
                LessThanOrEqualTo = lte
            };

            return this;
        }

        public ElasticSearchEngine Build()
        {
            return new ElasticSearchEngine(this);
        }
    }
}

Ö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.

private static void SearchProduct(string indexName, ElasticContext elasticContext)
{
    var elasticSearchEngine = new ElasticSearchBuilder(indexName, elasticContext)
            .SetSize(5)
            .SetFrom(0)
            .AddTermQuery("iphone", "name")
            .AddRangeFilter(3900, 5000, "price")
            .Build()
            .Execute<Product>();

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

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.

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

Gökhan Gökalp

View Comments

  • 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.

  • 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.

  • 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));

  • 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.

  • Üstad merhaba öncelikle ellerine kollarına sağlık böyle bir makale sunduğun için..
    Makaleyi daha yeni uyguluyordum ve en heycanlı yerinde birkaç hata vermeye başladı ve malesef ilerleyemedim.
    https://ibb.co/NsSGZvY ekran görüntüsündeki gibi bir hata almaktayım nasıl çözülebilir ?

    Kullandığım
    "version" : {
    "number" : "7.0.0-alpha2"

    • Merhaba, teşekkür ederin öncelikle. Hatanın sebebi, ben bu makaleyi yazdığım zaman yani 2.5 yıl önce kullandığım NEST + Elasticsearch versiyonu ile şimdiki arasında major değişiklikler mevcut olmasıdır. Siz yeni NEST yapısına uydurarak ilgili business kodlarını, benim kullanmış olduğum yapı ile tekrar inşa edebilirsiniz.

      Teşekkürler.

      • Selam çözümü ilgili NEST paketlerini güncelleyip, elastic business kodlarını ise, yenilerine göre güncellemek.

Recent Posts

Event-Driven Architecture’larda Conditional Claim-Check Pattern’ı ile Event Boyut Sınırlarının Üstesinden Gelmek

{:en}In today’s technological age, we typically build our application solutions on event-driven architecture in order…

2 ay ago

Containerized Uygulamaların Supply Chain’ini Güvence Altına Alarak Güvenlik Risklerini Azaltma (Güvenlik Taraması, SBOM’lar, Artifact’lerin İmzalanması ve Doğrulanması) – Bölüm 1

{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…

9 ay ago

Identity & Access Management İşlemlerini Azure AD B2C ile .NET Ortamında Gerçekleştirmek

{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…

1 yıl ago

Azure Service Bus Kullanarak Microservice’lerde Event’ler Nasıl Sıralanır (FIFO Consumers)

{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…

2 yıl ago

.NET Microservice’lerinde Outbox Pattern’ı ile Eventual Consistency için Atomicity Sağlama

{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…

2 yıl ago