Tutorial Elasticsearch untuk .NET Developers

Diterbitkan: 2022-03-11

Haruskah pengembang .NET menggunakan Elasticsearch dalam proyek mereka? Meskipun Elasticsearch dibuat di Java, saya yakin ini menawarkan banyak alasan mengapa Elasticsearch layak dicoba untuk pencarian teks lengkap untuk proyek apa pun.

Elasticsearch, sebagai teknologi, telah berkembang pesat selama beberapa tahun terakhir. Tidak hanya membuat pencarian teks lengkap terasa seperti keajaiban, ia menawarkan fitur canggih lainnya, seperti pelengkapan otomatis teks, saluran agregasi, dan banyak lagi.

Jika pemikiran untuk memperkenalkan layanan berbasis Java ke ekosistem .NET Anda yang rapi membuat Anda tidak nyaman, maka jangan khawatir, karena setelah Anda menginstal dan mengonfigurasi Elasticsearch, Anda akan menghabiskan sebagian besar waktu Anda dengan salah satu paket .NET paling keren di luar di sana: NEST.

Pada artikel ini, Anda akan mempelajari bagaimana Anda dapat menggunakan solusi mesin pencari yang luar biasa, Elasticsearch, dalam proyek .NET Anda.

Menginstal dan Mengonfigurasi

Menginstal Elasticsearch sendiri ke lingkungan pengembangan Anda harus mengunduh Elasticsearch dan, opsional, Kibana.

Saat membuka ritsleting, file bat seperti ini berguna:

 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

Setelah memulai kedua layanan, Anda selalu dapat memeriksa server Kibana lokal (biasanya tersedia di http://localhost:5601), bermain-main dengan indeks dan tipe, dan mencari menggunakan JSON murni, seperti yang dijelaskan secara ekstensif di sini.

Langkah pertama

Menjadi pengembang yang menyeluruh dan baik, dengan dukungan dan pemahaman penuh dari manajemen, Anda memulai dengan menambahkan proyek pengujian unit dan menulis Layanan Pencarian dengan setidaknya cakupan kode 90%.

Langkah pertama adalah dengan jelas mengonfigurasi file app.config untuk menyediakan semacam string koneksi untuk server Elasticsearch.

Hal keren tentang Elasticsearch adalah sepenuhnya gratis. Tapi, saya tetap menyarankan menggunakan layanan Elastic Cloud yang disediakan oleh Elastic.co. Layanan yang di-host membuat semua pemeliharaan dan konfigurasi cukup mudah. Terlebih lagi, Anda memiliki dua minggu uji coba gratis, yang seharusnya lebih dari cukup untuk mencoba semua contoh di sini!

Karena di sini kita menjalankan secara lokal, kunci konfigurasi seperti ini harus dilakukan:

 <add key="Search-Uri" value="http://localhost:9200" />

Instalasi Elasticsearch berjalan pada port 9200 secara default, tetapi Anda dapat mengubahnya jika Anda mau.

Paket ElasticClient dan NEST

ElasticClient adalah orang kecil yang baik yang akan melakukan sebagian besar pekerjaan untuk kita, dan ia datang dengan paket NEST.

Mari kita instal dulu paketnya.

Untuk mengkonfigurasi klien, sesuatu seperti ini dapat digunakan:

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

Pengindeksan dan Pemetaan

Untuk dapat mencari sesuatu, kita harus menyimpan beberapa data ke dalam ES. Istilah yang digunakan adalah "pengindeksan."

Istilah "mapping" digunakan untuk memetakan data kita di database ke objek yang akan diserialkan dan disimpan di Elasticsearch. Kami akan menggunakan Entity Framework (EF) dalam tutorial ini.

Umumnya, saat menggunakan Elasticsearch, Anda mungkin mencari solusi mesin pencari di seluruh situs. Anda akan menggunakan semacam umpan atau intisari, atau pencarian seperti Google yang mengembalikan semua hasil dari berbagai entitas, seperti pengguna, entri blog, produk, kategori, acara, dll.

Ini mungkin tidak hanya menjadi satu tabel atau entitas dalam database Anda, tetapi Anda ingin menggabungkan beragam data dan mungkin mengekstrak atau memperoleh beberapa properti umum seperti judul, deskripsi, tanggal, penulis/pemilik, foto, dan sebagainya. Hal lain adalah, Anda mungkin tidak akan melakukannya dalam satu kueri, tetapi jika Anda menggunakan ORM, Anda harus menulis kueri terpisah untuk setiap entri blog, pengguna, produk, kategori, acara, atau yang lainnya.

Saya menyusun proyek saya dengan membuat indeks untuk setiap jenis "besar", misalnya, posting blog atau produk. Beberapa jenis Elasticsearch kemudian dapat ditambahkan untuk jenis yang lebih spesifik yang akan berada di bawah indeks yang sama. Misalnya, jika sebuah artikel dapat berupa cerita, artikel video, atau podcast, artikel itu akan tetap berada dalam indeks “artikel”, tetapi kami akan memiliki keempat jenis tersebut di dalam indeks tersebut. Namun, itu masih cenderung menjadi kueri yang sama di database.

Ingatlah bahwa Anda memerlukan setidaknya satu jenis untuk setiap indeks—mungkin jenis yang memiliki nama yang sama dengan indeks.

Untuk memetakan entitas Anda, Anda perlu membuat beberapa kelas tambahan. Saya biasanya menggunakan kelas DocumentSearchItemBase , dari mana setiap kelas khusus akan mewarisi BlogPostSearchItem , ProductSearchItem , dan seterusnya.

Saya suka memiliki ekspresi mapper di dalam kelas-kelas itu. Saya selalu dapat memodifikasi ekspresi jika diperlukan di jalan.

Dalam salah satu proyek saya yang paling awal dengan Elasticsearch, saya menulis kelas SearchService yang cukup besar dengan pemetaan dan pengindeksan yang dilakukan dengan pernyataan kasus sakelar yang bagus dan panjang: Untuk setiap jenis entitas yang ingin saya masukkan ke Elasticsearch, ada sakelar dan kueri dengan pemetaan yang melakukan itu.

Namun, selama proses itu, saya belajar bahwa itu bukan cara terbaik, setidaknya bukan untuk saya.

Solusi yang lebih elegan adalah memiliki semacam kelas IndexDefinition cerdas dan kelas definisi indeks khusus untuk setiap indeks. Dengan cara ini, kelas IndexDefinition dasar saya dapat menyimpan daftar semua indeks yang tersedia dan beberapa metode pembantu seperti penganalisis yang diperlukan dan laporan status, sementara kelas khusus indeks turunan menangani kueri database dan memetakan data untuk setiap indeks secara khusus. Ini berguna terutama ketika Anda harus menambahkan entitas tambahan ke ES nanti. Itu datang untuk menambahkan kelas SomeIndexDefinition lain yang mewarisi dari IndexDefinition dan mengharuskan Anda untuk hanya menerapkan beberapa metode yang menanyakan data yang Anda inginkan dalam indeks Anda.

Elasticsearch Bicara

Inti dari semua yang dapat Anda lakukan dengan Elasticsearch adalah bahasa kuerinya. Idealnya, semua yang Anda butuhkan untuk dapat berkomunikasi dengan Elasticsearch adalah mengetahui cara membuat objek kueri.

Di balik layar, Elasticsearch memaparkan fungsinya sebagai API berbasis JSON melalui HTTP.

Meskipun API itu sendiri dan struktur objek kueri cukup intuitif, menangani banyak skenario kehidupan nyata masih bisa merepotkan.

Umumnya, permintaan pencarian ke Elasticsearch memerlukan informasi berikut:

  • Indeks mana dan tipe apa yang dicari

  • Informasi pagination (berapa banyak item yang harus dilewati, dan berapa banyak item yang harus dikembalikan)

  • Pemilihan tipe konkret (saat melakukan agregasi, seperti yang akan kita lakukan di sini)

  • Permintaan itu sendiri

  • Sorot definisi (Elasticsearch dapat secara otomatis menyorot hit jika kita menginginkannya)

Misalnya, Anda mungkin ingin menerapkan fitur pencarian di mana hanya beberapa pengguna yang dapat melihat konten premium di situs Anda, atau Anda mungkin ingin beberapa konten hanya dapat dilihat oleh “teman” penulisnya, dan seterusnya.

Mampu membangun objek kueri adalah inti dari solusi untuk masalah ini, dan itu benar-benar dapat menjadi masalah ketika mencoba untuk menutupi banyak skenario.

Dari semua hal di atas, yang paling penting dan paling sulit untuk disiapkan adalah, tentu saja, segmen kueri—dan di sini, kami akan berfokus terutama pada hal itu.

Kueri adalah konstruksi rekursif yang digabungkan dari BoolQuery dan kueri lainnya, seperti MatchPhraseQuery , TermsQuery , DateRangeQuery , dan ExistsQuery . Itu sudah cukup untuk memenuhi persyaratan dasar apa pun, dan seharusnya bagus untuk permulaan.

Kueri MultiMatch cukup penting karena memungkinkan kita untuk menentukan bidang di mana kita ingin melakukan pencarian dan mengubah hasil sedikit lebih banyak—yang akan kita kembalikan nanti.

MatchPhraseQuery dapat memfilter hasil menurut apa yang akan menjadi kunci asing dalam database SQL konvensional atau nilai statis seperti enum—misalnya, saat mencocokkan hasil dengan penulis tertentu ( AuthorId ), atau mencocokkan semua artikel publik ( ContentPrivacy=Public ).

TermsQuery akan diterjemahkan sebagai "dalam" ke dalam bahasa SQL konvensional. Misalnya, ia dapat mengembalikan semua artikel yang ditulis oleh salah satu teman pengguna atau mendapatkan produk secara eksklusif dari sekumpulan pedagang tetap. Seperti halnya SQL, seseorang tidak boleh menggunakan ini secara berlebihan dan menempatkan 10.000 anggota dalam array ini karena akan memiliki dampak kinerja, tetapi umumnya menangani jumlah yang wajar dengan cukup baik.

DateRangeQuery diri sendiri.

ExistsQuery menarik: Ini memungkinkan Anda untuk mengabaikan atau mengembalikan dokumen yang tidak memiliki bidang tertentu.

Ini, bila digabungkan dengan BoolQuery , memungkinkan Anda untuk menentukan logika pemfilteran yang kompleks.

Pikirkan situs blog, misalnya, di mana posting blog dapat memiliki bidang AvailableFrom yang menunjukkan kapan mereka harus terlihat.

Jika kami menerapkan filter seperti AvailableFrom <= Now , maka kami tidak akan mendapatkan dokumen yang tidak memiliki bidang tertentu sama sekali (kami menggabungkan data, dan beberapa dokumen mungkin tidak menetapkan bidang itu). Untuk memecahkan masalah, Anda akan menggabungkan ExistsQuery dengan DateRangeQuery dan membungkusnya dalam BoolQuery dengan syarat bahwa setidaknya satu elemen di BoolQuery terpenuhi. Sesuatu seperti ini:

 BoolQuery Should (at least one of the following conditions should be fulfilled) DateRangeQuery with AvailableFrom condition Negated ExistsQuery for field AvailableFrom

Meniadakan kueri bukanlah pekerjaan langsung yang mudah. Tetapi dengan bantuan BoolQuery , tetap saja dimungkinkan:

 BoolQuery MustNot ExistsQuery

Otomatisasi dan Pengujian

Untuk mempermudah, metode yang disarankan adalah menulis tes sambil jalan.

Dengan cara ini, Anda akan dapat bereksperimen dengan lebih efisien dan—bahkan yang lebih penting—Anda akan memastikan bahwa setiap perubahan baru yang Anda perkenalkan (seperti filter yang lebih kompleks) tidak akan merusak fungsi yang ada. Saya secara eksplisit tidak ingin mengatakan "pengujian unit", karena saya bukan penggemar mengejek sesuatu seperti mesin Elasticsearch — tiruan itu hampir tidak akan pernah menjadi perkiraan realistis tentang bagaimana ES benar-benar berperilaku — oleh karena itu, ini bisa menjadi tes integrasi, jika Anda adalah penggemar terminologi.

Contoh dunia nyata

Setelah semua dasar dilakukan dengan pengindeksan, pemetaan, dan pemfilteran, sekarang kita siap untuk bagian yang paling menarik: mengutak-atik parameter pencarian untuk menghasilkan hasil yang lebih baik.

Dalam proyek terakhir saya, saya menggunakan Elasticsearch untuk menyediakan umpan pengguna: semua konten dikumpulkan ke satu tempat yang dipesan berdasarkan tanggal pembuatan dan pencarian teks lengkap dengan beberapa opsi. Umpan itu sendiri cukup mudah; cukup pastikan bahwa ada bidang tanggal di suatu tempat di data Anda dan pesan berdasarkan bidang itu.

Pencarian, di sisi lain, tidak akan bekerja dengan sangat baik di luar kotak. Itu karena, secara alami, Elasticsearch tidak dapat mengetahui hal-hal penting apa yang ada dalam data Anda. Katakanlah kita memiliki beberapa data yang (di antara bidang lainnya) memiliki bidang Title , Tags (array), dan Body . Bidang isi dapat berupa konten HTML (untuk membuat segalanya sedikit lebih realistis).

Kesalahan Ejaan

Persyaratan: Pencarian kami harus mengembalikan hasil bahkan jika terjadi kesalahan ejaan atau jika akhir kata berbeda. Misalnya, jika ada artikel dengan judul “Hal Luar Biasa yang Dapat Anda Lakukan dengan Sendok Kayu”, ketika saya mencari “benda” atau “kayu”, saya tetap ingin mendapatkan kecocokan.

Untuk mengatasi hal ini, kita harus berkenalan dengan analyzer, tokenizers, char filter, dan token filter. Itu adalah transformasi yang diterapkan pada saat pengindeksan.

  • Penganalisa perlu didefinisikan. Ini dapat didefinisikan per indeks.

  • Penganalisis dapat diterapkan ke beberapa bidang dalam dokumen kami. Ini dapat dilakukan dengan menggunakan atribut atau API yang lancar. Dalam contoh kita, kita menggunakan atribut.

  • Penganalisis adalah kombinasi dari filter, filter char, dan tokenizer.

Untuk memenuhi persyaratan (partial word match), kita akan membuat “autocomplete” analyzer, yang terdiri dari:

  • Filter stopword bahasa Inggris: filter yang menghapus semua kata umum dalam bahasa Inggris, seperti “dan” atau “the.”

  • Filter trim: menghilangkan ruang putih di sekitar setiap token

  • Filter huruf kecil: mengubah semua karakter menjadi huruf kecil. Ini tidak berarti bahwa ketika kami mengambil data kami, itu akan dikonversi ke huruf kecil, tetapi mengaktifkan pencarian case-invarian.

  • Tokenizer edge-n-gram: tokenizer ini memungkinkan kita untuk memiliki kecocokan sebagian. Misalnya, jika kita memiliki kalimat "Nenek saya memiliki kursi kayu", ketika mencari istilah "kayu", kita masih ingin mendapatkan pukulan pada kalimat itu. Apa yang dilakukan edge-n-gram, adalah menyimpan "woo", "wood", "woode", dan "wooden" sehingga sebagian kata yang cocok dengan setidaknya tiga huruf ditemukan. Parameter MinGram dan MaxGram menentukan jumlah minimum dan maksimum karakter yang akan disimpan. Dalam kasus kami, kami akan memiliki minimal tiga dan maksimal 15 huruf.

Di bagian berikut, semua itu diikat menjadi satu:

 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_") ) );

Dan, ketika kita ingin menggunakan penganalisis ini, kita hanya perlu membubuhi keterangan bidang yang kita inginkan seperti ini:

 public class SearchItemDocumentBase { ... [Text(Analyzer = "autocomplete", Name = nameof(Title))] public string Title { get; set; } ... }

Sekarang, mari kita lihat beberapa contoh yang menunjukkan persyaratan yang cukup umum di hampir semua aplikasi dengan banyak konten.

Membersihkan HTML

Persyaratan: Beberapa bidang kami mungkin memiliki teks HTML di dalamnya.

Tentu, Anda tidak ingin mencari "bagian" untuk mengembalikan sesuatu seperti "<bagian>...</bagian>" atau "tubuh" mengembalikan elemen HTML "<body>." Untuk menghindarinya, selama pengindeksan, kami akan menghapus HTML dan hanya meninggalkan konten di dalamnya.

Untungnya, Anda bukan yang pertama dengan masalah itu. Elasticsearch hadir dengan filter char yang berguna untuk itu:

 analysis.Analyzers(a => a .Custom("html_stripper", cc => cc .Filters("eng_stopwords", "trim", "lowercase") .CharFilters("html_strip") .Tokenizer("autocomplete") )

Dan untuk menerapkannya:

 [Text(Analyzer = "html_stripper", Name = nameof(HtmlText))] public string HtmlText { get; set; }

Bidang Penting

Persyaratan: Kecocokan dalam judul harus lebih penting daripada kecocokan dalam konten.

Untungnya, Elasticsearch menawarkan strategi untuk meningkatkan hasil jika kecocokan terjadi di satu bidang atau bidang lainnya. Ini dilakukan dalam konstruksi permintaan pencarian dengan menggunakan opsi boost :

 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)

Seperti yang Anda lihat, kueri MultiMatch sangat berguna dalam situasi seperti ini, dan situasi seperti ini sama sekali tidak jarang! Seringkali, beberapa bidang lebih penting dan beberapa tidak—mekanisme ini memungkinkan kita untuk memperhitungkannya.

Tidak selalu mudah untuk segera menetapkan nilai peningkatan. Anda harus bermain dengan ini sedikit untuk mendapatkan hasil yang diinginkan.

Memprioritaskan Artikel

Persyaratan: Beberapa artikel lebih penting daripada yang lain. Entah penulisnya lebih penting, atau artikel itu sendiri memiliki lebih banyak suka/bagikan/upvotes/dll. Artikel yang lebih penting harus berperingkat lebih tinggi.

Elasticsearch memungkinkan kami untuk mengimplementasikan fungsi penilaian kami, dan kami menyederhanakannya sedemikian rupa sehingga kami mendefinisikan bidang "Kepentingan", yang merupakan nilai ganda—dalam kasus kami, lebih besar dari 1. Anda dapat menentukan fungsi/faktor kepentingan Anda sendiri dan menerapkannya demikian pula. Anda dapat menentukan beberapa mode peningkatan dan penilaian—mana saja yang paling cocok untuk Anda. Yang ini bekerja untuk kami dengan baik:

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

Setiap film memiliki peringkat, dan kami menyimpulkan peringkat aktor berdasarkan peringkat rata-rata untuk film yang mereka perankan (bukan metode yang sangat ilmiah). Kami menskalakan peringkat itu ke nilai ganda dalam interval [0,1].

Kecocokan Kata Lengkap

Persyaratan: Pencocokan kata lengkap harus berperingkat lebih tinggi.

Saat ini, kami mendapatkan hasil yang cukup baik untuk penelusuran kami, tetapi Anda mungkin memperhatikan bahwa beberapa hasil yang berisi kecocokan sebagian mungkin berperingkat lebih tinggi daripada kecocokan persis. Untuk mengatasinya, kami menambahkan bidang tambahan dalam dokumen kami bernama "Kata Kunci" yang tidak menggunakan penganalisis pelengkapan otomatis, tetapi menggunakan tokenizer kata kunci dan memberikan faktor pendorong untuk mendorong hasil pencocokan tepat lebih tinggi.

Bidang ini hanya akan cocok jika kata persisnya cocok. Ini tidak akan cocok dengan "kayu" untuk "kayu" seperti penganalisis pelengkapan otomatis.

Bungkus

Artikel ini seharusnya memberi Anda gambaran umum tentang cara mengatur Elasticsearch di proyek .NET Anda, dan dengan sedikit usaha, menyediakan fungsionalitas pencarian-di mana-mana yang bagus.

Kurva pembelajaran bisa sedikit curam, tetapi itu sepadan, terutama ketika Anda mengubahnya dengan benar dan mulai mendapatkan hasil pencarian yang bagus.

Selalu ingat untuk menambahkan kasus uji menyeluruh dengan hasil yang diharapkan untuk memastikan bahwa Anda tidak terlalu mengacaukan parameter saat memperkenalkan perubahan dan bermain-main.

Kode lengkap untuk artikel ini tersedia di GitHub, dan menggunakan data yang diambil dari database TMDB untuk menunjukkan bagaimana hasil pencarian meningkat di setiap langkah.