Categories: .NETSearch Engine

Lucene.Net Search Engine Kütüphanesi Kullanımı

Merhaba arkadaşlar, uzun bir aradan sonra karşınıza güzel bir kütüphane incelemesi ile geliyorum.

Günümüzde yaygınlaşan teknoloji ile artık ciddi anlamda büyük boyutlu veriler ile çalışmaktayız. Zaman zaman bir e-ticaret projesindeki ürünlerin çokluğunu düşünün veya bir sosyal platform sitesini. Özünde geliştirmiş olduğumuz uygulamalarda hepimiz search işlemi yapma ihtiyacı duymaktayız.

Eğer geliştirmiş olduğumuz uygulamada verimizin boyutu ciddi anlamda büyük değilse bunu bir şekilde filtreli sql sorguları ile halledebiliyoruz. Veri boyutumuz dahada büyümeye başladığında ise performans çalışmaları adı altında dahada filtrelenerek git gide kompleks sql sorgularına ve bir zaman sonra bir çıkmaza doğru gidiyoruz. 🙂

Şu sorguyu bir düşünün:

SELECT * FROM Product WHERE Name LIKE '%blabla%'

Eğer veri boyutumuz ciddi anlamda büyük ise (örneğin 20.000+ veya daha az yada çok) istediğimiz kadar Name alanına index versekte veya farklı WHERE kriterleride taksak, günümüz hız teknolojisinde response time olarak geri kalacağız. Özellikle küreselleşen rekabet ortamında herkes daha iyisini ve daha hızlısını yapmaya çalışıyorken.

İşte bu gibi durumlarda imdadımıza çok güzel bir search engine kütüphanesi olan Lucene gibi yapılar koşuyor.

Lucene Nedir?

En saf hali ile Apache Lucene projesi kapsamında olan full-text search engine diyebiliriz.

Günümüz teknolojisinde gayet popülerliğini koruyan search engine’ler arasındaki Elastic Search ve Solr‘nin alt yapısını oluşturmaktadır. Lucene temelinde Java ile geliştirilmiştir ve .NET versiyonu ile de .NET kullanıcılarınında kullanımına sunulmuştur.

Çalışma Mantığı İse:

Indexlenecek olan veri gönderilir ve Lucene tarafından dosya sistemi üzerinde indexlenir. Arama işlemi yapacağımız zaman ise indexlemiş olduğu dosya sistemi üzerinde arama yapar ve istenildiği kadar alan üzerinde de indexlemeye olanak sağlamaktadır.

RDBMS‘lerdeki full-text search işlemlerinden daha performanslı bir sonuç göstermektedir. (Yapacak olduğumuz örnek uygulama üzerinde de inceliyor olacağız.)

Çok büyük veriler üzerinde full-text search işlemi yapacaksak isek, gerçekten harika bir kütüphanedir. Diğer yandan indexlemek istediğimiz içeriğin path’ini vererek, indexleme işlemini Lucene’e yaptırabiliyoruz.

Visual Studio‘mu açıp hemen LuceneSearchEngine isminde yeni bir konsol uygulaması oluşturalım. Oluşturmuş olduğumuz konsol uygulamasına NuGet Package Manager üzerinden Lucene.Net‘i bulup projeye dahil edelim.

Referansları dahil ettikten sonra örneğimizde kullanacağımız Product class’ını, Entities klasörü içerisinde oluşturuyorum.

namespace LuceneSearchEngine.Entities
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

Şimdi sıra geldi search engine ile ilgili gerekli business’ı barındırıcak olan LuceneEngine class’ını oluşturmaya. İlk kısımda sizlere en basit haliyle ilgili entity’lerimizi, Lucene ile nasıl indexleyebiliriz’i göstermek istiyorum. Dilerseniz incelemeye başlayalım.

using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using Lucene.Net.Store;
using LuceneSearchEngine.Entities;
using System.Collections.Generic;
using System.Linq;

namespace LuceneSearchEngine
{
    public class LuceneEngine
    {
        private Analyzer _Analyzer;
        private Directory _Directory;
        private IndexWriter _IndexWriter;
        private IndexSearcher _IndexSearcher;
        private Document _Document;
        private QueryParser _QueryParser;
        private Query _Query;
        private string _IndexPath = @"C:\LuceneIndex";

        public LuceneEngine()
        {
            _Analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
            _Directory = FSDirectory.Open(_IndexPath);
        }

        public void AddToIndex(IEnumerable values)
        {
            using (_IndexWriter = new IndexWriter(_Directory, _Analyzer, IndexWriter.MaxFieldLength.UNLIMITED))
            {
                foreach (var loopEntity in values)
                {
                    _Document = new Document();

                    foreach (var loopProperty in loopEntity.GetType().GetProperties())
                    {
                        // Dilersek indexlenmesini istemediğimiz bir alan varsa, kontrol ederek Field.Index.NO vermemiz yeterli olacaktır.
                        _Document.Add(new Field(loopProperty.Name, loopProperty.GetValue(loopEntity).ToString(), Field.Store.YES, Field.Index.ANALYZED));
                    }
                    
                    _IndexWriter.AddDocument(_Document);
                    _IndexWriter.Optimize();
                    _IndexWriter.Commit();
                }
            }
        }
    }
}

AddToIndex metot’u ile parametre olarak IEnumerable olarak Product entity’lerimizi alıyoruz. IndexWriter objesine ise parametrelerinde hangi path’e indexleyeceği bilgisini Directory objesi ile veriyoruz ve ardından constructor’da oluşturmuş olduğumuz analyzer tipini veriyoruz. Son olarak limitsiz field uzunluğunu ilgili enum üzerinden set ediyoruz.

Constructor’da initialize ettiğimiz analyzer ve filtrelerini inceleyelim.

ANALYZER AÇIKLAMA
StandardAnalyzer İngilizce duraksatma kelimelerini kullanarak StandardTokenizer filtreleri ile StandardFilter, LowerCaseFilter ve StopFilter’ı kapsamaktadır.
KeywordAnalyzer İsminden de anlaşılacağı üzere gelen token’ı yani keyword’ü tamamını herhangi bir parçaya ayırmadan kullanmaya yarar. Örneğin belli ürün isimlerinde aramada veya posta kodları aramalarında.
WhitespaceAnalyzer WhitespaceTokenizer’ı kullanır ve text’i boşluklarına ayrır.
StopAnalyzer LetterTokenizer filtreleri ile LowerCaseFilter ve StopFilter’ı içerir.
SimpleAnalyzer LetterTokenizer ve LowerCaseFilter’ı içermektedir.
FILTERS AÇIKLAMA
StandardFilter Token’ı normalleştirir. Yani kelimelerdeki kısaltmalar veya belirteçleri kaldırır. (C.E.O = CEO)
LowerCaseFilter Token’ı küçük harflere çevirir.
StopFilter Token üzerindeki kelimeyi duraksatıcı noktalama işaretlerini kaldırır.

Ben örneğimizi StandardAnalyzer kullanarak gerçekleştirdim. Şimdide gelelim LuceneEngine class’ımızın geriye kalan search metot’unun implementasyonuna.

        public List Search(string field, string keyword)
        {
            // Üzerinde arama yapmak istediğimiz field için bir query oluşturuyoruz.
            _QueryParser = new QueryParser(Lucene.Net.Util.Version.LUCENE_30, field, _Analyzer);
            _Query = _QueryParser.Parse(keyword);

            using (_IndexSearcher = new IndexSearcher(_Directory, true))
            {
                List products = new List();
                var result = _IndexSearcher.Search(_Query, 10);

                foreach (var loopDoc in result.ScoreDocs.OrderBy(s => s.Score))
                {
                    _Document = _IndexSearcher.Doc(loopDoc.Doc);

                    products.Add(new Product() { Id = System.Convert.ToInt32(_Document.Get("Id")), Name = _Document.Get("Name") });
                }

                return products;
            }
        }

Indexlemiş olduğumuz doküman üzerinde arama yapabilmek için bir QueryParser oluşturuyoruz ve parametre olarak arama yapacak olduğu field‘ı ve arama yapma işleminde de kullanacağı analyzer tipini veriyoruz.

NOT: Dokümanı indexlerken kullanmış olduğunuz analyzer tipini, search işleminde de kullanmanız gerekmektedir doğru scoring alabilmek ve sonuçları elde edebilmek adına. Örneğin indexlerken kullanmış olduğunuz analyzer’in birinde StandardFilter olduğunu düşünün. Hatırlarsanız noktalama işaretlerini ve belirli kısaltmaları kaldırıyordu. (C.E.O = CEO) Arama işleminde ise StandardFilter olmadığını varsayarsak ve sizin search için keyword’ünüz eğer C.E.O’ise o dokümanı bulamazsınız.

QueryParser’ı initialize ettikten sonra ise Parse metotu ile Query‘i elde ediyoruz. Daha sonra IndexSearcher sınıfı üzerinden tekrardan initialize ederken constructor üzerinden index işlemi için kullanmış olduğumuz path’i belirtiyoruz ve Search metot’una oluşan Query’i verip, kaç adet sonuç almak istediğimizi belirterek ilgili dokümanlara ulaşabiliyoruz. En uygun sonuçları bize Lucene bir scoring değerleri oluşturarak sıralayıp veriyor.

Search metotunuda oluşturduğumuza göre LuceneEngine sınıfımızın son hali aşağıdaki gibi olacaktır.

using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using Lucene.Net.Store;
using LuceneSearchEngine.Entities;
using System.Collections.Generic;
using System.Linq;

namespace LuceneSearchEngine
{
    public class LuceneEngine
    {
        private Analyzer _Analyzer;
        private Directory _Directory;
        private IndexWriter _IndexWriter;
        private IndexSearcher _IndexSearcher;
        private Document _Document;
        private QueryParser _QueryParser;
        private Query _Query;
        private string _IndexPath = @"C:\LuceneIndex";

        public LuceneEngine()
        {
            _Analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
            _Directory = FSDirectory.Open(_IndexPath);
        }

        public void AddToIndex(IEnumerable values)
        {
            using (_IndexWriter = new IndexWriter(_Directory, _Analyzer, IndexWriter.MaxFieldLength.UNLIMITED))
            {
                foreach (var loopEntity in values)
                {
                    _Document = new Document();

                    foreach (var loopProperty in loopEntity.GetType().GetProperties())
                    {
                        // Dilersek indexlenmesini istemediğimiz bir alan varsa, kontrol ederek Field.Index.NO vermemiz yeterli olacaktır.
                        _Document.Add(new Field(loopProperty.Name, loopProperty.GetValue(loopEntity).ToString(), Field.Store.YES, Field.Index.ANALYZED));

                        _IndexWriter.AddDocument(_Document);
                        _IndexWriter.Optimize();
                        _IndexWriter.Commit();
                    }
                }
            }
        }

        public List Search(string field, string keyword)
        {
            // Üzerinde arama yapmak istediğimiz field için bir query oluşturuyoruz.
            _QueryParser = new QueryParser(Lucene.Net.Util.Version.LUCENE_30, field, _Analyzer);
            _Query = _QueryParser.Parse(keyword);

            using (_IndexSearcher = new IndexSearcher(_Directory, true))
            {
                List products = new List();
                var result = _IndexSearcher.Search(_Query, 10);

                foreach (var loopDoc in result.ScoreDocs.OrderBy(s => s.Score))
                {
                    _Document = _IndexSearcher.Doc(loopDoc.Doc);

                    products.Add(new Product() { Id = System.Convert.ToInt32(_Document.Get("Id")), Name = _Document.Get("Name") });
                }

                return products;
            }
        }
    }
}

Oluşturmuş olduğumuz engine’imizi şimdi kullanımına bir bakalım.

using System.Collections.Generic;
using LuceneSearchEngine.Entities;

namespace LuceneSearchEngine
{
    class Program
    {
        static void Main(string[] args)
        {
            LuceneEngine engine = new LuceneEngine();
            List products = new List();

            products.Add(new Product() { Id = 1, Name = "Apple Iphone 6" });
            products.Add(new Product() { Id = 2, Name = "MacBook Air" });
            products.Add(new Product() { Id = 3, Name = "Sony Xperia Z Ultra" });
            products.Add(new Product() { Id = 4, Name = "Samsung Ultra HD Tv" });
            products.Add(new Product() { Id = 5, Name = "Asus Zenphone 6" });
            products.Add(new Product() { Id = 6, Name = "Sony Xperia Z 3" });
            products.Add(new Product() { Id = 7, Name = "Sony Playstation 3" });

            // Oluşturmuş olduğumuz test dokümanlarını ilgili indexleme metotumuza gönderelim ve indexlesin. Bu metotu sadece bir kere çalıştırmamız yeterlidir, daha sonrasında ise kapatabilirsiniz.
            engine.AddToIndex(products);

            // Oluşturmuş olduğumuz index üzerinden Name field'ına bakarak ve içerisinde "Sony" geçen en uygun kelimeleri scoring işlemi yaparak bize getirecektir.
            var result = engine.Search("Name", "Sony");
        }
    }
}

Örnek gereği büyük veriler üzerinde çalışamadım ama eğer sizler büyük boyutlu verilere sahipseniz kesinlikle tavsiye edebileceğim bir kütüphanedir Lucene search engine olarak.

Harika özelliklerinin aksine kötü olarak nitelendirebileceğim tek yanı ise distributed (dağıtık) olarak indexlemeye izin vermemektedir ve arama yapamamaktadır. Fakat bu tarz işler için geliştirilmiş olan Katta isimli teknoloji ile indexler dağıtık olarak kullanılabilir ve search işlemi de yapılabilmektedir.

Katta’nın workflow’u hakkında bilgi edinmek isterseniz burayı ziyaret edebilirsiniz. Bunun dışında bunlarla uğraşmak istemezseniz ve kompleks şekilde sorgular atıp (Scoring işlemleri, Facets’ler, Auto Suggestorler gibi.) distributed bir search engine kullanmak istiyorum derseniz ElasticSearch‘ü önerebilirim bu noktada. Solr‘ıda es geçmemek gerek tabiki.

Aktif olarak şuan ElasticSearch ile çalışmaktayım bulunduğum şirkette. Elastic üzerinde farklı development işlemleri gerçekleştirmekteyim. ElasticSearch de altyapısında Lucene engine’i kullanmaktadır ayrıca distributed olarak indexleme ve arama işlemlerini de desteklemektedir.

Umarım faydalı bir makale olmuştur. Örnek proje ektedir.

Herkese iyi search’ler 🙂

LuceneSearchEngine

Gökhan Gökalp

View Comments

  • Merhaba,

    Bir proje için dokümanlar arasında search yapmamız gerekiyor. Biz de araştırma yaparken dokümanı kayıt esnasında indexleyip DB de dokumanın index'ini tutmaya karar verdik.

    Lucene.net ile verdiğiniz örneği inceledik fakat örnekte görebildiğimiz kadarıyla dokümanların indexlenip bir string olarak dönmüyor. Adım Adım sanırım DB ye aktarıyorsunuz.

    Bize dokümanın indexlemesinde index numaraları dahi lazım değil sadece index kelimelerine ihtiyacımız var.

    Bu işlemi nasıl yapabiliriz?

  • merhabalar,
    Veri kaynağı olarak DB'de bir tablo gösterebilir miyiz? Cevap evet ise bu tabloda değişen kayıtlar ne sıklık ile cache'lenir?
    Teşekkürler.

    • Merhaba index'leyeceğiniz verileri istediğiniz DB'den alabilirsiniz. Ne sıklıkla cache'leneceği sizin sisteminizdeki verilerin değişim sürelerine ve ne sıklıkla güncelleme isteğinize göre değişkenlik gösterir.

      Teşekkürler.

  • Merhaba Gökhan Bey,

    Final kodunuzda AddtoIndex metodunuz indeksi çokluyor. Nesnenin tüm property değeri document nesnesine eklendikten sonra nesnenin son hali IndexWriter'a eklenerek commit edilmesi gerekiyor. Yani commit Add,Document, Optimize, Commit işlemlerinin bir önceki foreach bloğuna taşınması gerekiyor.

    Faydalı bir makale olmuş elinize sağlık.

  • Merhabalar bu makaledeki kodları denedim ve sonuçları ekrana bastırdım fakat bir gariplik var. Mesela SearchIndex.Search(_Query, 10) kısmında 10 yazılmış ve "Sony" kelimesi aratılmış. Tuhaf bir şekilde şöyle bir sonuç alıyorum.

    Sony Xperia Z Ultra
    Sony Xperia Z 3
    Sony Playstation 3
    Sony Xperia Z Ultra
    Sony Xperia Z 3
    Sony Playstation 3
    Sony Xperia Z Ultra

    Yazdırma komutum da aşağıda

    var result = engine.Search("Name", "Sony");
    foreach (var product in result)
    {
    Console.WriteLine(product.Name);
    }

    İki sorum olacak. Birincisi oradaki 10 sayısını neye göre veriyoruz. (Lucene.net kendi sitesinde Top n gibi bir açıklama yapmış fakat tam olarak anlayamadım.)
    İkinci sorum ise neden böyle bir sonuç çıkıyor. Acaba bir yerde yanlışlık yapmış olabilir miyim?

Recent Posts

Securing the Supply Chain of Containerized Applications to Reduce Security Risks (Policy Enforcement-Automated Governance with OPA Gatekeeper and Ratify) – Part 2

{:tr} Makalenin ilk bölümünde, Software Supply Chain güvenliğinin öneminden ve containerized uygulamaların güvenlik risklerini azaltabilmek…

6 months ago

Securing the Supply Chain of Containerized Applications to Reduce Security Risks (Security Scanning, SBOMs, Signing&Verifying Artifacts) – Part 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 months ago

Delegating Identity & Access Management to Azure AD B2C and Integrating with .NET

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

1 year ago

How to Order Events in Microservices by Using Azure Service Bus (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 years ago

Providing Atomicity for Eventual Consistency with Outbox Pattern in .NET Microservices

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

2 years ago

Building Microservices by Using Dapr and .NET with Minimum Effort – 02 (Azure Container Apps)

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

2 years ago