Categories: .NETSearch Engine

ElasticSearch Serisi 01 – C# ile Index Oluşturmak

Merhaba arkadaşlar.

Hatırlarsak bir önceki makalem olan “ElasticSearch’e Başlarken (Kurulum, Kibana, Marvel ve Sense)” ile aslında ElasticSearch dünyasına bir adım atmıştık. Bu makale kapsamında ise C# üzerinde ElasticSearch için kullanacak olduğumuz NEST kütüphanesini tanıyacağız ve hemen ardından bir index oluşturacağız.

Dilerseniz öncelikle biraz NEST kütüphanesinden bahsedelim.

NEST: ElasticSearch’e bağlantı kurabilmemizi, indexleme ve sorgulama gibi işlemleri yapabilmemizi sağlayan ve aynı zamanda strongly typed ve fluent request’ler oluşturabilmemize imkan tanıyan bir kütüphanedir.

NEST hakkında detaylı bilgilere erişmek isterseniz buradan inceleyebilirsiniz. ElasticSearch makale serilerimiz boyunca NEST kütüphanesini kullanıyor ve tanıyor olacağız.

Kodlamaya başlayabilmek için “ElasticSearch” isminde bir Blank Solution oluşturuyorum. ElasticSearch makale serisi boyunca bu solution altından ilerlemeyi düşünüyorum. Oluşturmuş olduğumuz solution’a, “ElasticSearch.Core.Common” ismine sahip bir Class Library ekleyelim. Bu Library içerisinde ihtiyaç duyacağımız ortak noktaları toplayacağız. Bu işlemin ardından Nuget Package Manager’a girerek, “ElasticSearch.Core.Common” projesine aşağıdaki NEST kütüphanesini kuralım.

Kurulum işlemlerinin ardından, “ElasticHelper” isminde bir class ekleyelim ve içerisini aşağıdaki gibi kodlayalım.

using System;
using System.Configuration;
using Nest;

namespace ElasticSearch.Core.Common
{
    public class ElasticHelper
    {
        private static readonly Lazy<ElasticHelper> _Instance = new Lazy<ElasticHelper>(() => new ElasticHelper());

        private ElasticHelper()
        {

        }

        public static ElasticHelper Instance
        {
            get
            {
                return _Instance.Value;
            }
        }

        #region Public Methods
        public ConnectionSettings GetConnectionSettings()
        {
            var connectionSettings = new ConnectionSettings(new Uri(ConfigurationManager.AppSettings["ElasticSearchURI"]));

            return connectionSettings;
        }
        #endregion
    }
}

ElasticHelper sınıfı içerisinde, ihtiyaç duyabileğimiz yardımcı niteliğindeki method’larımızı kodlayacağız.  Burada Lazy class’ından faydalanarak ilgili helper’ımızın tek bir instance’a sahip olmasını sağladık. “GetConnectionSettings” method’unda ise, ElasticSearch’ün Client objesini yaratabilmemiz için ihtiyaç duyduğu “ConnectionSettings” tipindeki instance’ını yaratıp geriye dönüyoruz. Instance yaratma işlemi sırasında ihtiyaç duyduğu Elastic URI’ını ise, “System.Configuration” namespace’i altında bulunan “ConfigurationManager” aracılığı ile “AppSettings” üzerinden veriyoruz.

Şimdi domain entity’lerini tanımlamaya giriş yapabiliriz. Bunun için bir Entity katmanı oluşturacağız. “ElasticSearch.Data.Entities” isminde bir Class Library ekleyelim. Bu Library içerisinde bahsettiğimiz gibi elastic içerisine index’leyecek olduğumuz domin entity’leri yer alacak. Library’i ekleme işleminden sonra “Product” isminde ilk entity’mizi oluşturalım. Bu entity aşağıdaki gibi property’lere sahip olacak.

namespace ElasticSearch.Data.Entities
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public decimal Price { get; set; }
    }
}

Burada dikkat etmemiz gereken bazı önemli noktalar bulunmakta. Elastic üzerinde verilerimizi store ederken, ilgili dokümanın yanı entity’nin her property’sini index’lememeliyiz. ElasticSearch sadece search işlemlerimizi hızlı bir şekilde yapabilmemizi sağlayan bir araçtır, veritabanı değildir. Evet veritabanı olarak da kullanabilirsiniz fakat bu işlem sonucunda getireceği olumsuz etkilerle yüzleşmekte pek de hoş olmayacaktır. Örnek vermek gerekirse, gün geçtikçe doküman boyutlarınız artacak, search işlemleri nispeten daha yavaş çalışmaya başlayacak (ki işin içine aggregation’lar vb. girdikçe) ve verilerinizi elastic’e aktarım süreleriniz daha da uzayacaktır. Büyük scale’e sahip ortamlarda işler git gide daha da zor olacaktır.

Kodlamamıza şimdi Resource Access işlemlerini yani indexing işlemlerini gerçekleştirecek olduğumuz Data katmanı ile devam edelim. Bunun için Solution’a “ElasticSearch.Data” olarak bir Class Library daha ekleyelim. Eklemiş olduğumuz Data katmanına yine Nuget Package Manager üzerinden NEST kütüphanesini kuralım. Henüz içerisine hiç bir class eklemeden, “ElasticSearch.Data.Contracts” isminde bir Class Library ekleyelim ve içerisine aşağıdaki gibi “IElasticContext” isminde bir interface tanımlayalım.

namespace ElasticSearch.Data.Contracts
{
    public interface IElasticContext
    {
        IndexResponseDTO CreateIndex<T>(string indexName, string aliasName) where T : class;
    }
}

Bu interface ElasticContext’inin barındıracağı method imzalarını içerecek. Şimdilik sadece “CreateIndex” method’u ile devam edeceğiz. Bu method elastic üzerinde bir adet index yaratmamızı sağlayacak. ElasticSearch’e giriş makalesinde bahsettiğimiz gibi index’ler, verilerimizin tutulduğu bir nevi veritabanıdır. Bu sebeple herhangi bir doküman eklemeden önce bir index yaratmamız gerekmektedir.

Interface’i oluşturduktan sonra geri dönüş tipimiz olan “IndexResponseDTO” objesini de tanımlayalım.

using System;

namespace ElasticSearch.Data.Contracts
{
    public class IndexResponseDTO
    {
        public bool IsValid { get; set; }
        public string StatusMessage { get; set; }
        public Exception Exception { get; set; }
    }
}

IndexResponseDTO ile de oluşturacak olduğumuz index sonuçlarını geri dönüyor olacağız. Şimdi kaldığımız yerden devam edebiliriz ve oluşturmuş olduğumuz “ElasticSearch.Data” Library’sine “ElasticSearch.Data.Contracts” Library’sini Referans olarak gösterip, içerisine “ElasticContext” isminde yeni bir class oluşturalım.

using ElasticSearch.Data.Contracts;
using Nest;

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

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

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

ElasticContext class’ında constructor injection yöntemi ile dışarıdan bir adet “ElasticClient” alıyoruz. “CreateIndex<T>” method’unda ise “CreateIndexDescriptor” yaratarak, içerisinde fluently bir şekilde “Mappings” ve “Aliases” tanımlamalarımızı yapıyoruz. Mappings ile elastic üzerinde verilerimizin nasıl tutulacağını tanımlarken, Aliases sadece index’imizin takma ismidir. Yani sorgulama yaparken daha kısa bir isim üzerinden ilgili index’e erişebilmek içindir.

Not: Aliases’ın bir başka faydası ise, production ortamlarında bir index üzerinde çalışırken güncelleme veya yeniden atma(reindex) gibi ihtiyaçlarımız doğduğunda, production üzerindeki index’i bozmadan farklı bir index ismi ile veri baştan atılır/güncellenir. Daha sonra sadece saniyeler içerisinde ilgili alias eski index üzerinden silinir, yeni atılan index’e verilir. Bu sebeple alias’lar unique olmalıdır. Bu sayede hiçbir risk ve kesinti olmadan index değiştirebilmek mümkün olacaktır.

Daha sonra dışarıdan inject ettiğimiz “_elasticClient” üzerinden “CreateIndex” method’unu çağırarak, tanımlamış olduğumuz “createIndexDescriptor”ü parametre olarak set ediyoruz. Bu işlemlerin ardından ElasticClient bizim için HTTP üzerinden bir REST çağrısı gerçekleştirecek. REST çağrısı sonucunda elde edeceğimiz response’u ise, “IndexResponseDTO” objesine map ederek, geriye dönüyoruz.

Şuan neredeyse hazır gibiyiz. Elastic’e verilerimizi aktarım işlemlerimizi gerçekleştirmek için ben bir adet Console Application seçiyorum. (On-Demand olarak çalışacağı için) Solution içerisine “ElasticSearch.DataTransfer” isminde bir Console Application ekliyorum ve Referans olarak “ElasticSearch.Core.Common”, “ElasticSearch.Data.Contracts”, “ElasticSearch.Data.Entities” ve “ElasticSearch.Data” projelerini ekliyorum. Hemen Nuget Package Manager’ı açarak, NEST kütüphanesini buraya da yüklüyorum. Artık bu proje içerisinde index, reindex veya doküman indexleme gibi işlemleri gerçekleştiriyor olacağız.

Program.cs içerisini aşağıdaki gibi kodlayalım.

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 = string.Format("{0}_{1}", ConfigurationManager.AppSettings["ElasticSearchIndexName"],
                                         DateTime.Now.ToString("yyyyMMddHHss"));
            string aliasName = ConfigurationManager.AppSettings["ElasticSearchIndexName"];

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

            CreateIndex(elasticContext, indexName, aliasName);

            Console.ReadKey();
        }

        private static void CreateIndex(ElasticContext elasticContext, string indexName, string aliasName)
        {
            var response = elasticContext.CreateIndex<Product>(indexName, aliasName);

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

İlk olarak “Main” method’u içerisine baktığımızda “indexName” ve “aliasName” field’larını AppSettings üzerinden dolduruyoruz. “indexName” oluştururken dikkat edersek, ilk başa index ismi ve alttan çizgi ile ayırdıktan sonra o anki tarih formatını saniyesine kadar alıyoruz. Bunu yapmamızdaki sebep yukarıda alias‘ın öneminden bahsederken mevcut index’i bozmadan farklı bir index’de atabileceğimizden bahsetmiştik. İşte bu noktada en son ne zaman index attığımızı takip edebiliriz. Daha sonrasında ise ElasticHelper’ın “GetConnectionSettings” method’unu parametre olarak kullanıp, bir ElasticClient yaratıyoruz. Yaratmış olduğumuz bu ElasticClient’ı “Data” katmanı içerisinde oluşturmuş olduğumuz ElasticContext’in constructor’ına inject ediyoruz. Artık Context’imiz hazır durumda.

Daha fazla kod kalabalığı yaratmamak için private olarak “CreateIndex” isminde bir method yaratıyoruz ve parametre olarak “ElasticContext, IndexName ve AliasName” objelerini alıyoruz. Daha sonra bu parametreleri kullanarak “elasticContext” üzerindeki “CreateIndex<T>” method’unu çağırarak, “indexName” ve “aliasName” property’lerini set ediyoruz. Bu işlemlerin sonucunda ise elde edecek olduğumuz response içerisindeki “StatusMessage” property’sini Console yardımı ile ekrana yazdırıyoruz.

Test işlemlerine başlamadan önce ilgili değerlerimizi “App.config” içerisine ekleyelim.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
  </startup>
      <appSettings>
        <add key="ElasticSearchURI" value="http://localhost:9200"/>
        <add key="ElasticSearchIndexName" value="product_search"/>
      </appSettings>
</configuration>

Dilerseniz “CreateIndex” method’u içerisindeki “response” alanına bir debug koyalım ve QuickWatch’da  bir bakalım.

Gördüğümüz ilk index’imiz başarılı bir şekilde oluştu. İlgili sorgumuz Valid durumda ve “StatusMessage” içeriğine de bakarsak eğer, “Valid NEST response built from a successful…” şeklinde devam ediyor. Şimdi doğrulamak adına Sense plugin’i üzerinden bir bakalım.

İlk olarak Sense üzerinden Mapping’i bir kontrol edelim. Bunun için aşağıdaki sorguyu kullanalım.

GET product_search/_mapping

Dikkat edersek GET verb’ünden sonra vermiş olduğumuz aliasName’i kullanıyoruz.

GET sorgusunu execute ettiğimizde karşımıza gerçek index’imiz “product_search_201607242334” ismi ile gelmektedir. İçerisine baktığımızda ise “mappings” başlığı altında “product” entity’sinin property’leri yer almaktadır ve elastic tarafından otomatik olarak nasıl tutulacağını yani tiplerini belirlemiştir.

Bir elastic makalesinin daha sonuna geldik. Bir sonraki makalede ise doküman atma gibi işlemlere bir başlangıç yapıyor olacağız.

Şimdilik sağlıcakla kalın.

ElasticSearch-CreateIndex

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