.NET Geliştiricileri için Elasticsearch Eğitimi
Yayınlanan: 2022-03-11Bir .NET geliştiricisi projelerinde Elasticsearch'ü kullanmalı mı? Elasticsearch, Java üzerine kurulmuş olsa da, herhangi bir proje için tam metin arama için Elasticsearch'ün neden denenmeye değer olduğuna dair birçok neden sunduğuna inanıyorum.
Elasticsearch, bir teknoloji olarak, son birkaç yılda çok yol kat etti. Tam metin aramayı sihir gibi hissettirmekle kalmaz, otomatik metin tamamlama, toplama ardışık düzenleri ve daha fazlası gibi diğer gelişmiş özellikler de sunar.
Düzgün .NET ekosisteminize Java tabanlı bir hizmet sunma düşüncesi sizi rahatsız ediyorsa endişelenmeyin, Elasticsearch'ü kurup yapılandırdıktan sonra zamanınızın çoğunu en havalı .NET paketlerinden biriyle geçireceksiniz. orada: NEST.
Bu makalede, harika arama motoru çözümü Elasticsearch'ü .NET projelerinizde nasıl kullanabileceğinizi öğreneceksiniz.
Yükleme ve Yapılandırma
Elasticsearch'ün kendisini geliştirme ortamınıza yüklemek, Elasticsearch'ü ve isteğe bağlı olarak Kibana'yı indirmeye bağlıdır.
Açıldığında, bunun gibi bir yarasa dosyası işe yarar:
cd "D:\elastic\elasticsearch-5.2.2\bin" start elasticsearch.bat cd "D:\elastic\kibana-5.0.0-windows-x86\bin" start kibana.bat exit
Her iki hizmeti de başlattıktan sonra, her zaman yerel Kibana sunucusunu kontrol edebilirsiniz (genellikle http://localhost:5601 adresinde bulunur), dizinler ve türlerle oynayabilir ve burada kapsamlı olarak açıklandığı gibi saf JSON kullanarak arama yapabilirsiniz.
İlk adım
Yönetimden gelen tam destek ve anlayışla eksiksiz ve iyi bir geliştirici olarak, bir birim test projesi ekleyerek ve en az %90 kod kapsamına sahip bir Arama Hizmeti yazarak başlarsınız.
İlk adım, app.config
dosyasını Elasticsearch sunucusu için bir tür bağlantı dizesi sağlayacak şekilde açıkça yapılandırmaktır.
Elasticsearch ile ilgili harika olan şey, tamamen ücretsiz olmasıdır. Ancak yine de Elastic.co tarafından sağlanan Elastic Cloud hizmetini kullanmanızı tavsiye ederim. Barındırılan hizmet, tüm bakım ve yapılandırmayı oldukça kolaylaştırır. Dahası, burada tüm örnekleri denemek için fazlasıyla yeterli olan iki haftalık ücretsiz deneme hakkınız var!
Burada yerel olarak çalıştığımız için, bunun gibi bir yapılandırma anahtarı şunları yapmalıdır:
<add key="Search-Uri" value="http://localhost:9200" />
Elasticsearch kurulumu varsayılan olarak 9200 numaralı bağlantı noktasında çalışır, ancak isterseniz bunu değiştirebilirsiniz.
ElasticClient ve NEST Paketi
ElasticClient, işin çoğunu bizim için yapacak olan hoş bir küçük arkadaştır ve NEST paketi ile birlikte gelir.
Önce paketi kuralım.
İstemciyi yapılandırmak için şöyle bir şey kullanılabilir:
var node = new Uri(ConfigurationManager.AppSettings["Search-Uri"]); var settings = new ConnectionSettings(node); settings.ThrowExceptions(alwaysThrow: true); // I like exceptions settings.PrettyJson(); // Good for DEBUG var client = new ElasticClient(settings);
Dizin Oluşturma ve Eşleme
Bir şeyi arayabilmek için bazı verileri ES'ye kaydetmemiz gerekir. Kullanılan terim “indeksleme”dir.
“Eşleme” terimi, veri tabanındaki verilerimizi Elasticsearch'te seri hale getirilecek ve saklanacak nesnelere eşlemek için kullanılır. Bu eğitimde Entity Framework (EF) kullanacağız.
Genel olarak, Elasticsearch'ü kullanırken, muhtemelen site çapında bir arama motoru çözümü arıyorsunuz. Kullanıcılar, blog girişleri, ürünler, kategoriler, etkinlikler vb. gibi çeşitli varlıklardan tüm sonuçları döndüren bir tür özet akışı veya özet veya Google benzeri arama kullanacaksınız.
Bunlar muhtemelen veritabanınızdaki tek bir tablo veya varlık olmayacak, bunun yerine çeşitli verileri toplamak ve başlık, açıklama, tarih, yazar/sahip, fotoğraf vb. gibi bazı ortak özellikleri çıkarmak veya türetmek isteyeceksiniz. Başka bir şey, muhtemelen tek bir sorguda yapmayacaksınız, ancak bir ORM kullanıyorsanız, bu blog girişlerinin, kullanıcıların, ürünlerin, kategorilerin, etkinliklerin veya başka bir şeyin her biri için ayrı bir sorgu yazmanız gerekecek.
Projelerimi, örneğin blog yazısı veya ürün gibi her "büyük" tür için bir dizin oluşturarak yapılandırdım. Bazı Elasticsearch türleri daha sonra aynı dizine girecek daha spesifik türler için eklenebilir. Örneğin, bir makale bir hikaye, video makalesi veya podcast olabilirse, yine de “makale” dizininde olurdu, ancak bu dört tür bu dizinde olurdu. Ancak, yine de veritabanında aynı sorgu olması muhtemeldir.
Her dizin için en az bir türe ihtiyacınız olduğunu unutmayın - muhtemelen dizinle aynı ada sahip bir tür.
Varlıklarınızı eşlemek için bazı ek sınıflar oluşturmak isteyeceksiniz. Genellikle, özel sınıfların her birinin BlogPostSearchItem
, ProductSearchItem
vb. öğelerini devralacağı DocumentSearchItemBase
sınıfını kullanırım.
Bu sınıflarda eşleyici ifadelerine sahip olmayı seviyorum. Yolda gerekirse ifadeleri her zaman değiştirebilirim.
Elasticsearch ile ilk projelerimden birinde, Nice ve uzun geçiş durumu ifadeleriyle yapılan eşlemeler ve indeksleme ile oldukça büyük bir SearchService sınıfı yazdım: Elasticsearch'e atmak istediğim her varlık türü için, eşlemeli bir anahtar ve sorgu vardı. bunu yaptı.
Ancak süreç boyunca bunun en iyi yol olmadığını, en azından benim için olmadığını öğrendim.
Daha zarif bir çözüm, bir tür akıllı IndexDefinition
sınıfına ve her dizin için belirli bir dizin tanımlama sınıfına sahip olmaktır. Bu şekilde, temel IndexDefinition
sınıfım mevcut tüm dizinlerin bir listesini ve gerekli analizörler ve durum raporları gibi bazı yardımcı yöntemleri saklayabilirken, türetilmiş dizine özgü sınıflar, veritabanını sorgulamayı ve her bir dizin için verileri özel olarak eşlemeyi yönetir. Bu, özellikle bir süre sonra ES'ye ek bir varlık eklemeniz gerektiğinde kullanışlıdır. Bu, IndexDefinition
miras alan ve dizininizde olmasını istediğiniz verileri sorgulayan birkaç yöntem uygulamanızı gerektiren başka bir SomeIndexDefinition
sınıfı eklemekle ilgilidir.
Elasticsearch Speak
Elasticsearch ile yapabileceğiniz her şeyin temelinde onun sorgulama dili vardır. İdeal olarak, Elasticsearch ile iletişim kurabilmeniz için ihtiyacınız olan tek şey, bir sorgu nesnesinin nasıl oluşturulacağını bilmektir.
Perde arkasında, Elasticsearch, HTTP üzerinden JSON tabanlı bir API olarak işlevlerini ortaya koyuyor.
API'nin kendisi ve sorgu nesnesinin yapısı oldukça sezgisel olsa da, birçok gerçek hayat senaryosu ile uğraşmak hala güç olabilir.
Genellikle, Elasticsearch'e yapılan bir arama isteği aşağıdaki bilgileri gerektirir:
Hangi dizin ve hangi türler aranır
Sayfalandırma bilgileri (kaç öğe atlanacak ve kaç öğe döndürülecek)
Somut bir tür seçimi (burada yapacağımız gibi bir toplama yaparken)
Sorgunun kendisi
Vurgu tanımı (Elasticsearch, istersek isabetleri otomatik olarak vurgulayabilir)
Örneğin, sitenizdeki premium içeriği yalnızca bazı kullanıcıların görebileceği bir arama özelliği uygulamak isteyebilirsiniz veya bazı içeriğin yalnızca yazarlarının "arkadaşları" tarafından görünmesini vb. isteyebilirsiniz.
Sorgu nesnesini oluşturabilmek, bu sorunlara yönelik çözümlerin özünde yer alır ve birçok senaryoyu kapsamaya çalışırken gerçekten sorun olabilir.
Yukarıdakilerin hepsinden, kurulumu en önemli ve en zor olanı, doğal olarak, sorgu segmentidir ve burada esas olarak buna odaklanacağız.
Sorgular, BoolQuery ile MatchPhraseQuery
, TermsQuery
, DateRangeQuery
ve ExistsQuery
gibi diğer sorguların bir araya BoolQuery
özyinelemeli yapılardır. Bunlar herhangi bir temel gereksinimi karşılamak için yeterliydi ve başlangıç için iyi olmalı.
Bir MultiMatch
sorgusu, üzerinde arama yapmak istediğimiz alanları belirlememize ve sonuçları biraz daha düzeltmemize olanak sağladığı için oldukça önemlidir - buna daha sonra geri döneceğiz.
Bir MatchPhraseQuery
, sonuçları, geleneksel SQL veritabanlarında yabancı anahtarın ne olacağına veya numaralandırmalar gibi statik değerlere göre filtreleyebilir; örneğin, sonuçları belirli bir yazara göre eşleştirirken ( AuthorId
) veya tüm genel makaleleri eşleştirirken ( ContentPrivacy=Public
).
TermsQuery
, geleneksel SQL diline "in" olarak çevrilecektir. Örneğin, kullanıcının bir arkadaşının yazdığı tüm makaleleri iade edebilir veya yalnızca belirli bir satıcı kümesinden ürün alabilir. SQL'de olduğu gibi, bunu aşırı kullanmamalı ve performansa etkisi olacağından bu diziye 10.000 üye koymamalıdır, ancak genellikle makul miktarları oldukça iyi işler.
DateRangeQuery
kendi kendini belgeliyor.
ExistsQuery
ilginçtir: Belirli bir alanı olmayan belgeleri yok saymanıza veya geri göndermenize olanak tanır.
Bunlar, BoolQuery
ile birleştirildiğinde, karmaşık filtreleme mantığı tanımlamanıza izin verir.
Örneğin, blog gönderilerinin ne zaman görünür hale gelmeleri gerektiğini belirten bir AvailableFrom
alanına sahip olabileceği bir blog sitesi düşünün.
AvailableFrom <= Now
gibi bir filtre uygularsak, o zaman belirli bir alana sahip olmayan belgeleri alamayız (verileri topluyoruz ve bazı belgelerde bu alan tanımlı olmayabilir). Sorunu çözmek için ExistsQuery
ile DateRangeQuery
birleştirir ve BoolQuery
en az bir öğenin yerine getirilmesi BoolQuery
içine sararsınız. Bunun gibi bir şey:

BoolQuery Should (at least one of the following conditions should be fulfilled) DateRangeQuery with AvailableFrom condition Negated ExistsQuery for field AvailableFrom
Sorguları reddetmek, bu kadar basit, kullanıma hazır bir iş değildir. Ancak BoolQuery
yardımıyla yine de mümkündür:
BoolQuery MustNot ExistsQuery
Otomasyon ve Test
İşleri kolaylaştırmak için önerilen yöntem kesinlikle ilerledikçe testler yazmaktır.
Bu şekilde, daha verimli denemeler yapabileceksiniz ve daha da önemlisi, yaptığınız yeni değişikliklerin (daha karmaşık filtreler gibi) mevcut işlevselliği bozmamasını sağlayacaksınız. Açıkça "birim testleri" demek istemedim, çünkü Elasticsearch'ün motoru gibi bir şeyle alay etmenin hayranı değilim—sahte neredeyse hiçbir zaman ES'nin gerçekte nasıl davrandığına dair gerçekçi bir yaklaşım olmayacak—bu nedenle, bu entegrasyon testleri olabilir, eğer sen bir terminoloji hayranısın.
Gerçek Dünya Örnekleri
Dizin oluşturma, haritalama ve filtreleme ile ilgili tüm temel çalışmaları yaptıktan sonra, artık en ilginç kısma hazırız: daha iyi sonuçlar elde etmek için arama parametrelerinde ince ayar yapmak.
Son projemde, bir kullanıcı beslemesi sağlamak için Elasticsearch'ü kullandım: tüm içerik, oluşturma tarihine göre sıralanmış tek bir yerde toplandı ve bazı seçeneklerle tam metin araması. Feed'in kendisi oldukça basittir; sadece verilerinizin bir yerinde bir tarih alanı olduğundan emin olun ve o alana göre sıralayın.
Öte yandan arama, kutunun dışında şaşırtıcı derecede iyi çalışmayacaktır. Bunun nedeni, doğal olarak Elasticsearch'ün verilerinizdeki önemli şeylerin ne olduğunu bilememesidir. Diyelim ki (diğer alanların yanı sıra) Title
, Tags
(dizi) ve Body
alanlarına sahip bazı verilerimiz var. Gövde alanı HTML içeriği olabilir (işleri biraz daha gerçekçi hale getirmek için).
Yazım Hataları
Gereklilik: Yazım hataları meydana gelse veya kelime sonları farklı olsa bile aramamız sonuçları döndürmelidir. Örneğin, “Tahta Kaşıkla Yapabileceğiniz Muhteşem Şeyler” başlıklı bir makale olsa, “şey” veya “tahta” arattığımda yine de bir eşleşme almak isterim.
Bununla başa çıkmak için analizörler, belirteçler, karakter filtreleri ve belirteç filtreleri hakkında bilgi sahibi olmamız gerekecek. Bunlar indeksleme sırasında uygulanan dönüşümlerdir.
Analizörlerin tanımlanması gerekir. Bu, indeks başına tanımlanabilir.
Belgelerimizdeki bazı alanlara analizörler uygulanabilir. Bu, öznitelikler veya akıcı API kullanılarak yapılabilir. Örneğimizde öznitelikleri kullanıyoruz.
Analizörler, filtreler, karakter filtreleri ve belirteçlerin bir kombinasyonudur.
Gereksinimi (kısmi kelime eşleşmesi) yerine getirmek için, aşağıdakilerden oluşan "otomatik tamamlama" çözümleyicisini oluşturacağız:
İngilizce durma sözcükleri filtresi: İngilizce'de "ve" veya "the" gibi tüm yaygın sözcükleri kaldıran filtre.
Kırpma filtresi: her simgenin etrafındaki beyaz boşluğu kaldırır
Küçük harf filtresi: tüm karakterleri küçük harfe dönüştürür. Bu, verilerimizi getirdiğimizde küçük harfe dönüştürüleceği anlamına gelmez, bunun yerine büyük/küçük harf değişmez aramayı etkinleştirir.
Edge-n-gram belirteci: Bu belirteç, kısmi eşleşmelere sahip olmamızı sağlar. Örneğin, “tahta” terimini ararken “Büyükannemin tahta sandalyesi var” cümlesine sahipsek, yine de o cümleye bir isabet almak isteriz. Edge-n-gram'ın yaptığı şey, "woo", "woode", "woode" ve "wooden" kelimelerini saklamaktır, böylece en az üç harfle herhangi bir kısmi kelime eşleşmesi bulunur. MinGram ve MaxGram parametreleri, saklanacak minimum ve maksimum karakter sayısını tanımlar. Bizim durumumuzda en az üç ve en fazla 15 harf olacak.
Aşağıdaki bölümde, bunların tümü birbirine bağlanmıştır:
analysis.Analyzers(a => a .Custom("autocomplete", cc => cc .Filters("eng_stopwords", "trim", "lowercase") .Tokenizer("autocomplete") ) .Tokenizers(tdesc => tdesc .EdgeNGram("autocomplete", e => e .MinGram(3) .MaxGram(15) .TokenChars(TokenChar.Letter, TokenChar.Digit) ) ) .TokenFilters(f => f .Stop("eng_stopwords", lang => lang .StopWords("_english_") ) );
Ve bu analizörü kullanmak istediğimizde, istediğimiz alanlara şu şekilde açıklama yapmalıyız:
public class SearchItemDocumentBase { ... [Text(Analyzer = "autocomplete", Name = nameof(Title))] public string Title { get; set; } ... }
Şimdi, çok sayıda içeriğe sahip hemen hemen her uygulamada oldukça yaygın gereksinimleri gösteren birkaç örneğe bakalım.
HTML'yi temizleme
Gereklilik: Bazı alanlarımızın içinde HTML metni olabilir.
Doğal olarak, “bölüm” aramasının “<section>…</section>” gibi bir şey döndürmesini veya “body” aramasının “<body>” HTML öğesini döndürmesini istemezsiniz. Bunu önlemek için, indeksleme sırasında HTML'yi çıkaracağız ve sadece içeriği içeride bırakacağız.
Neyse ki, bu sorunu yaşayan ilk kişi siz değilsiniz. Elasticsearch bunun için kullanışlı bir karakter filtresiyle birlikte gelir:
analysis.Analyzers(a => a .Custom("html_stripper", cc => cc .Filters("eng_stopwords", "trim", "lowercase") .CharFilters("html_strip") .Tokenizer("autocomplete") )
Ve uygulamak için:
[Text(Analyzer = "html_stripper", Name = nameof(HtmlText))] public string HtmlText { get; set; }
Önemli Alanlar
Gereklilik: Başlıktaki eşleşmeler, içerikteki eşleşmelerden daha önemli olmalıdır.
Neyse ki Elasticsearch, eşleşme bir alanda veya diğerinde gerçekleşirse sonuçları artırmak için stratejiler sunar. Bu, boost
seçeneği kullanılarak arama sorgusu yapısı içinde yapılır:
const int titleBoost = 15; .Query(qx => qx.MultiMatch(m => m .Query(searchRequest.Query.ToLower()) .Fields(ff => ff .Field(f => f.Title, boost: titleBoost) .Field(f => f.Summary) ... ) .Type(TextQueryType.BestFields) ) && filteringQuery)
Gördüğünüz gibi, MultiMatch
sorgusu bu gibi durumlarda çok kullanışlıdır ve bunun gibi durumlar o kadar da nadir değildir! Çoğu zaman, bazı alanlar daha önemlidir ve bazıları değildir - bu mekanizma, bunu hesaba katmamızı sağlar.
Yükseltme değerlerini hemen ayarlamak her zaman kolay değildir. İstenilen sonuçları elde etmek için bununla biraz oynamanız gerekecek.
Makalelere Öncelik Verme
Gereklilik: Bazı makaleler diğerlerinden daha önemlidir. Ya yazar daha önemlidir ya da makalenin kendisi daha çok beğeni/paylaşım/artı oy/vb. Daha önemli makaleler daha üst sıralarda yer almalıdır.
Elasticsearch, puanlama fonksiyonumuzu uygulamamıza izin veriyor ve bunu, bizim durumumuzda 1'den büyük olan çift değerli bir “Önem” alanı tanımladığımız şekilde basitleştiriyoruz. Kendi önem fonksiyonunuzu/faktörünüzü tanımlayabilir ve uygulayabilirsiniz. benzer şekilde. Hangisi size en uygunsa, birden fazla hızlandırma ve puanlama modu tanımlayabilirsiniz. Bu bizim için iyi çalıştı:
.Query(q => q .FunctionScore(fsc => fsc .BoostMode(FunctionBoostMode.Multiply) .ScoreMode(FunctionScoreMode.Sum) .Functions(f => f .FieldValueFactor(b => b .Field(nameof(SearchItemDocumentBase.Rating)) .Missing(0.7) .Modifier(FieldValueFactorModifier.None) ) ) .Query(qx => qx.MultiMatch(m => m .Query(searchRequest.Query.ToLower()) .Fields(ff => ff ... ) .Type(TextQueryType.BestFields) ) && filteringQuery) ) )
Her filmin bir derecelendirmesi vardır ve oyuncu derecelendirmesini, yayınlandıkları filmlerin derecelendirmelerinin ortalamasına göre çıkardık (pek bilimsel bir yöntem değil). Bu derecelendirmeyi [0,1] aralığında bir çift değere ölçeklendirdik.
Tam Kelime Eşleşmeleri
Gereklilik: Tam kelime eşleşmeleri daha üst sıralarda yer almalıdır.
Şimdiye kadar, aramalarımız için oldukça iyi sonuçlar alıyoruz, ancak kısmi eşleşmeler içeren bazı sonuçların tam eşleşmelerden daha yüksek sıralanabileceğini fark etmiş olabilirsiniz. Bununla başa çıkmak için, belgemize otomatik tamamlama çözümleyicisi kullanmayan, bunun yerine bir anahtar kelime belirteci kullanan ve tam eşleme sonuçlarını daha yükseğe çıkarmak için bir destek faktörü sağlayan "Anahtar Kelimeler" adlı ek bir alan ekledik.
Bu alan yalnızca tam kelime eşleşirse eşleşir. Otomatik tamamlama analizörünün yaptığı gibi "ahşap" ile "ahşap" eşleşmeyecektir.
Sarmak
Bu makale size .NET projenizde Elasticsearch'ü nasıl kuracağınıza dair bir genel bakış vermiş olmalı ve biraz çaba sarf ederek güzel bir her yerde arama işlevselliği sağlamalıdır.
Öğrenme eğrisi biraz dik olabilir, ancak özellikle tam olarak ince ayar yaptığınızda ve harika arama sonuçları almaya başladığınızda buna değer.
Değişiklikleri uygularken ve etrafta dolaşırken parametreleri çok fazla karıştırmadığınızdan emin olmak için her zaman beklenen sonuçlara sahip kapsamlı test senaryoları eklemeyi unutmayın.
Bu makalenin tam kodu GitHub'da mevcuttur ve arama sonuçlarının her adımda nasıl geliştiğini göstermek için TMDB veritabanından alınan verileri kullanır.