Categories: Search Engine

ElasticSearch Serisi 02 – C# ile Document Indexing ve Bulk Indexing

 

Yeni bir ElasticSearch serisi ile merhaba arkadaşlar.

Bu serimizde sizlerle, Document Indexing ve Bulk Indexing gibi işlemleri gerçekleştireceğiz. Bir önceki serimiz olan “ElasticSearch Serisi 01 – C# ile Index Oluşturmak” adlı makalede, type bazlı nasıl index oluşturulduğunu inceledik ve bir ElasticSearch projesi oluşturmuştuk. Bu serimizde de aynı proje üzerinden ilerliyor olacağız.

1) Document Indexing

ElasticSearch’e Başlarken (Kurulum, Kibana, Marvel ve Sense) makalesinde de bahsettiğimiz gibi ElasticSearch üzerindeki Document’lar, RDBMS üzerindeki Row’lara denk gelmektedir. Biz ise burada daha önce type’ına göre oluşturmuş olduğumuz index’e, document’ları ekleyeceğiz yani indexleyeceğiz. Indexleme işleminde herhangi bir tokenizer veya analyzer kullanmayacağız. Bu konfigürasyon bilgilerine, gelecek ElasticSearch serilerinde detaylı olarak değineceğiz.

Hatırlarsak index oluşturma gibi işlemleri yürütecek olduğumuz “IElasticContext” interface’ini implemente eden bir “ElasticContext” imiz vardı. Aşağıdaki solution structure’ı üzerinden de hatırlayabiliriz.

 Index’leme işlemini de “ElasticContext” üzerinden gerçekleştireceğiz. Bunun için öncelikle “IElasticContext” interface’ini açalım ve aşağıdaki gibi yeni bir method imzası tanımlayalım.

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;
    }
}

Dikkat edersek “Index<T>” isminde generic olarak yeni bir method tanımladık. Parametrelerinde ise “indexName” ve “T” tipinde bir document almaktadır. Projemizi geliştirirken abstraction’lardan ve generic type’lardan yararlanarak open-closed prensibine göre geliştirdiğimiz için, istediğimiz ölçüde class’larımızı extend edebilmekteyiz.

Interface’e method imzasını eklediğimize göre şimdi “ElasticContext” class’ı içerisinde yeni method’u implemente edebiliriz.

public IndexResponseDTO Index<T>(string indexName, T document) where T : class
{
    var response = _elasticClient.Index(document, i => i
                   .Index(indexName)
                   .Type<T>());

    return new IndexResponseDTO()
    {
        IsValid = response.IsValid,
        StatusMessage = response.DebugInformation,
        Exception = response.OriginalException
    };
}

Index method’u “_elasticClient” üzerindeki “Index” method’unu kullanarak indexing işlemlerini gerçekleştirmektedir. Bu işlemler sırasında parametre olarak “document” ve bir adet function almaktadır. Bu function’ı fluent bir şekilde kullanarak, ilgili document’i hangi index’e atılacağı ve type’ının ne olduğunu söylüyoruz “_elasticClient” a. Bir önceki seriden hatırlarsak “Product” entity’si “Id” property’sine sahipti. NEST kütüphanesi document üzerindeki bu “Id” parametresini otomatik bir şekilde map’leyerek, elastic içerisine indexlemektedir. Eğer “Id” property’si farklı bir isimde olsaydı, ilgili function üzerinden yine fluent bir şekilde “Id()” method’unu çağırarak hangi property’nin “Id” olarak kullanılacağını söyleyebilirdik.

Not: Indexing sırasında spesifik olarak herhangi bir “id” property’si belirtmez isek, ElasticSearch bizim için kendisi 20 karakter uzunluğunda bir Base64-encoded GUID string tipinde bir “id” atamaktadır. Detaylara buradan erişebilirsiniz.

“Index” method’unu “ElasticContext” class’ına ekledikten sonra, class’ın son hali aşağıdaki şekilde olacaktır.

using ElasticSearch.Data.Contracts;
using Nest;

namespace ElasticSearch.Data
{
    public class ElasticContext : IElasticContext
    {
        private readonly ElasticClient _elasticClient;

        public ElasticContext(ElasticClient elasticClient)
        {
            _elasticClient = elasticClient;
        }

        #region IElasticContext Members
        public IndexResponseDTO CreateIndex<T>(string indexName, string aliasName) where T : class
        {
            var createIndexDescriptor = new CreateIndexDescriptor(indexName)
            .Mappings(ms => ms
                            .Map<T>(m => m.AutoMap())
                     )
             .Aliases(a => a.Alias(aliasName));

            var response = _elasticClient.CreateIndex(createIndexDescriptor);

            return new IndexResponseDTO()
            {
                IsValid = response.IsValid,
                StatusMessage = response.DebugInformation,
                Exception = response.OriginalException
            };
        }

        public IndexResponseDTO Index<T>(string indexName, T document) where T : class
        {
            var response = _elasticClient.Index(document, i => i
                           .Index(indexName)
                           .Type<T>());

            return new IndexResponseDTO()
            {
                IsValid = response.IsValid,
                StatusMessage = response.DebugInformation,
                Exception = response.OriginalException
            };
        }
        #endregion
    }
}

Index method’u şuan hazır durumda. Dilerseniz “ElasticSearch.DataTransfer” namespace’i altında bulunan, console uygulamasından hemen test edebiliriz. Bir önceki seride hatırlarsak, console üzerine index oluşturma ile ilgili test kodlarını yazmıştık. Main method’u üzerindeki index oluşturma ile ilgili method’ları sub method’lara bölelim ve yeni işlemimiz olan document indexleme kodlarını yazalım.

Bu işlemler sonucunda ise “Program.cs” class’ı aşağıdaki gibi olacaktır.

using System;
using ElasticSearch.Core.Common;
using ElasticSearch.Data;
using ElasticSearch.Data.Entities;
using Nest;
using System.Configuration;

namespace ElasticSearch.DataTransfer
{
    class Program
    {
        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);

            Console.ReadKey();
        }

        private static void Index(ElasticContext elasticContext, string indexName)
        {
            var response = elasticContext.Index(indexName, new Product()
            {
                Id = 5,
                Name = "Iphone 6s Plus",
                Description = "64gb Iphone 6s Plus, Renk: Space Gray",
                Price = 4000
            });

            Console.WriteLine(response.StatusMessage);
        }

        private static void CreateIndex(ElasticContext elasticContext, string indexName)
        {
            indexName = string.Concat(indexName, "_", DateTime.Now.ToString("yyyyMMddHHss"));
            string aliasName = ConfigurationManager.AppSettings["ElasticSearchIndexName"];

            var response = elasticContext.CreateIndex<Product>(indexName, aliasName);

            Console.WriteLine(response.StatusMessage);
        }
    }
}

Burada dikkat edersek “CreateIndex” method’unu sub method olarak ayırdık ve içerisinde ihtiyaç duyabileceği değişkenleri tanımladık. Global olarak kullanacak olduğumuz değişkenler ise, Main method’u içerisinde kaldı. Bir diğer method’umuz olan “Index” method’unda ise, elasticContext üzerine yeni implemente ettiğimiz “Index” method’unu çağırıyoruz ve parametre olarak “indexName” ve yeni bir Product nesnesi veriyoruz. Bu işlem sonucunda ilgili Product nesnesi elastic üzerine NEST vasıtasıyla indexlenecek ve sonucu console üzerine yazılacaktır.

Dilerseniz console uygulamasını başlatalım ve sonucuna bir bakalım.

Indexleme işlemimizin başarılı bir şekilde gerçekleştiğini “Valid NEST response built from a successful low call” cümlesinden anlayabiliyoruz. Product nesnesini Sense üzerinden kontrol ettiğimizde ise:

“1” numaralı Id ile indexlendiğini görebilmekteyiz. Indexleme işlemi şimdilik bu kadardır.

“ElasticContext” artık hem verilen tipe göre yeni bir index oluşturabilme yetisine, hemde ilgili document’ı istenilen index üzerine indexleyebilme yetisine sahip olmuştur.

Not: Sense üzerinden “GET product_search/product/_search” komutu ile product’ları listeletebildiğimiz gibi “GET product_search/product/{id}” komutu ile de, istenilen “id” li ürün girildiğinde o ürüne ait detaylar çekilebilmektedir.

1) Bulk Document Indexing

Tıpkı Document Indexing özelliğini eklediğimiz gibi şimdide, Bulk Indexing yapabilmesi için ilgili method imzasını tanımlayıp implemente etmeye başlayacağız.

“IElasticContext” interface’ini aşağıdaki gibi genişletelim.

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;
    }
}

Eklemiş olduğumuz “BulkIndex” generic method imzası ile ilgili bulking işlemlerini gerçekleştiriyor olacağız. Bu sefer artık T tipinde bir document yerine, List of T almaktadır. “ElasticContext” class’ına gelelim ve yeni eklemiş olduğumuz method imzasını, aşağıdaki gibi implemente etmeye başlayalım.

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

namespace ElasticSearch.Data
{
    public class ElasticContext : IElasticContext
    {
        private readonly ElasticClient _elasticClient;

        public ElasticContext(ElasticClient elasticClient)
        {
            _elasticClient = elasticClient;
        }

        #region IElasticContext Members
        public IndexResponseDTO CreateIndex<T>(string indexName, string aliasName) where T : class
        {
            var createIndexDescriptor = new CreateIndexDescriptor(indexName)
            .Mappings(ms => ms
                            .Map<T>(m => m.AutoMap())
                     )
             .Aliases(a => a.Alias(aliasName));

            var response = _elasticClient.CreateIndex(createIndexDescriptor);

            return new IndexResponseDTO()
            {
                IsValid = response.IsValid,
                StatusMessage = response.DebugInformation,
                Exception = response.OriginalException
            };
        }

        public IndexResponseDTO Index<T>(string indexName, T document) where T : class
        {
            var response = _elasticClient.Index(document, i => i
                           .Index(indexName)
                           .Type<T>());

            return new IndexResponseDTO()
            {
                IsValid = response.IsValid,
                StatusMessage = response.DebugInformation,
                Exception = response.OriginalException
            };
        }

        public IndexResponseDTO BulkIndex<T>(string indexName, List<T> document) where T : class
        {
            var response = _elasticClient.IndexMany(document, indexName);

            return new IndexResponseDTO()
            {
                IsValid = response.IsValid,
                StatusMessage = response.DebugInformation,
                Exception = response.OriginalException
            };
        }
        #endregion
    }
}

Implemente etmiş olduğumuz “BulkIndex” method’u içerisinde “_elasticClient” üzerindeki “IndexMany” method’unu call etmektedir. Bu method ise bulk işlemler için yazılmış shortcut bir method’dur.

Context’i hazırladıktan sonra şimdi “Program.cs” i aşağıdaki gibi güncelleyelim ve debug işlemini başlatalım.

using System;
using System.Collections.Generic;
using ElasticSearch.Core.Common;
using ElasticSearch.Data;
using ElasticSearch.Data.Entities;
using Nest;
using System.Configuration;

namespace ElasticSearch.DataTransfer
{
    class Program
    {
        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);

            Console.ReadKey();
        }

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

            Console.WriteLine(response.StatusMessage);
        }

        private static void Index(ElasticContext elasticContext, string indexName)
        {
            var response = elasticContext.Index(indexName, new Product()
            {
                Id = 1,
                Name = "Iphone 6s Plus",
                Description = "64gb Iphone 6s Plus, Renk: Space Gray",
                Price = 4000
            });

            Console.WriteLine(response.StatusMessage);
        }

        private static void CreateIndex(ElasticContext elasticContext, string indexName)
        {
            indexName = string.Concat(indexName, "_", DateTime.Now.ToString("yyyyMMddHHss"));
            string aliasName = ConfigurationManager.AppSettings["ElasticSearchIndexName"];

            var response = elasticContext.CreateIndex<Product>(indexName, aliasName);

            Console.WriteLine(response.StatusMessage);
        }
    }
}

“Main” method’u içerisinde diğer method’ları kapattık ve sadece yeni eklemiş olduğumuz “BulkIndex” method’unu çağırıyoruz. Burada 3 farklı ürün’ü, list of Product tipinde “elasticContext” üzerine eklemiş olduğumuz “BulkIndex”method’una gönderiyoruz.

Console uygulamasını çalıştırdıktan sonra Sense üzerinden bir search query’si atalım ve işlem sonucuna bir bakalım.

Response kısmındaki seçili alana baktığımızda eklediğimiz 3 adet product’ın başarılı bir şekilde indexlendiğini görebilmekteyiz. Burada bir noktaya dikkatinizi çekmek istiyorum. Makalenin giriş kısımlarında zaten “1” numaralı Id’ye sahip olan “Iphone 6s Plus” product’ını “4000” price’a sahip olacak şekilde eklemiştik. Fakat bulk işlemimiz sırasında ise, aynı ürünü aynı Id ile sadece price bilgisini “5000” olarak gönderdik. ElasticSearch burada otomatik olarak ilgili Type ve Id‘ye sahip doküman için, Update işlemi gerçekleştirdi.

ElasticSearch burada bizi bir yükten daha kurtarıyor. Eğer ilgili doküman zaten mevcut ise aynı Type ve Id ile, update işlemini kendisi gerçekleştiriyor. Bu sayede update işlemi için herhangi bir efor harcamamız gerekmiyor.

Bir elastic serisinin daha sonuna geldik. Geliştiriyor olduğumuz bu yapının, faydalı olmasını temenni ederim.

Projenin güncel haline, ekten ulaşabilirsiniz.

Takipte kalın.

ElasticSearch-Index-BulkIndex

Gökhan Gökalp

View Comments

Recent Posts

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

8 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

Dapr ve .NET Kullanarak Minimum Efor ile Microservice’ler Geliştirmek – 02 (Azure Container Apps)

{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…

2 yıl ago