برنامج تعليمي Elasticsearch لمطوري .NET
نشرت: 2022-03-11هل يجب على مطور .NET استخدام Elasticsearch في مشاريعهم؟ على الرغم من أن Elasticsearch مبنية على Java ، إلا أنني أعتقد أنها تقدم العديد من الأسباب التي تجعل Elasticsearch تستحق التصوير للبحث عن النص الكامل لأي مشروع.
لقد قطعت Elasticsearch ، كتقنية ، شوطًا طويلاً خلال السنوات القليلة الماضية. فهو لا يجعل البحث عن نص كامل يبدو وكأنه سحر فحسب ، بل إنه يوفر ميزات متطورة أخرى ، مثل الإكمال التلقائي للنص وخطوط التجميع والمزيد.
إذا كان التفكير في تقديم خدمة قائمة على Java إلى نظام NET. هناك: عش.
في هذه المقالة ، ستتعلم كيف يمكنك استخدام حل محرك البحث المذهل ، Elasticsearch ، في مشاريع .NET الخاصة بك.
التثبيت والتكوين
يأتي تثبيت Elasticsearch نفسه في بيئة التطوير الخاصة بك إلى تنزيل Elasticsearch واختيارياً ، Kibana.
عند فك ضغطه ، يكون ملف الخفافيش مثل هذا مفيدًا:
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بعد بدء كلتا الخدمتين ، يمكنك دائمًا التحقق من خادم Kibana المحلي (متوفر عادة على http: // localhost: 5601) ، والتلاعب بالفهارس والأنواع ، والبحث باستخدام JSON الخالص ، كما هو موضح على نطاق واسع هنا.
الخطوة الأولى
نظرًا لكونك مطورًا شاملاً وجيدًا ، مع دعم وفهم كامل من الإدارة ، فأنت تبدأ بإضافة مشروع اختبار وحدة وكتابة SearchService بتغطية أكواد بنسبة 90 ٪ على الأقل.
تتمثل الخطوة الأولى بوضوح في تكوين ملف app.config لتوفير سلسلة من نوع الاتصال لخادم Elasticsearch.
الشيء الرائع في Elasticsearch هو أنه مجاني تمامًا. لكني ما زلت أنصح باستخدام خدمة Elastic Cloud التي تقدمها Elastic.co. تجعل الخدمة المستضافة جميع عمليات الصيانة والتكوين سهلة إلى حد ما. أكثر من ذلك ، لديك أسبوعان من الإصدار التجريبي المجاني ، والذي يجب أن يكون أكثر من كافٍ لتجربة جميع الأمثلة هنا!
نظرًا لأننا نعمل محليًا هنا ، يجب أن يعمل مفتاح التكوين مثل هذا:
<add key="Search-Uri" value="http://localhost:9200" />يتم تشغيل تثبيت Elasticsearch على المنفذ 9200 افتراضيًا ، ولكن يمكنك تغييره إذا أردت.
ElasticClient وحزمة NEST
ElasticClient هو زميل صغير لطيف سيقوم بمعظم العمل لنا ، وهو يأتي مع حزمة NEST.
دعونا أولا نثبت الحزمة.
لتكوين العميل ، يمكن استخدام شيء مثل هذا:
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);الفهرسة ورسم الخرائط
لكي نتمكن من البحث عن شيء ما ، يجب علينا تخزين بعض البيانات في ES. المصطلح المستخدم هو "الفهرسة".
يُستخدم مصطلح "الخرائط" لتعيين بياناتنا في قاعدة البيانات إلى الكائنات التي سيتم إجراء تسلسل لها وتخزينها في Elasticsearch. سنستخدم Entity Framework (EF) في هذا البرنامج التعليمي.
بشكل عام ، عند استخدام Elasticsearch ، ربما تبحث عن حل محرك بحث على مستوى الموقع. ستستخدم إما نوعًا من الخلاصة أو الملخص ، أو البحث الشبيه بـ Google الذي يعرض جميع النتائج من الكيانات المختلفة ، مثل المستخدمين ، وإدخالات المدونة ، والمنتجات ، والفئات ، والأحداث ، وما إلى ذلك.
ربما لن تكون هذه مجرد جدول أو كيان واحد في قاعدة البيانات الخاصة بك ، ولكن بدلاً من ذلك ، ستحتاج إلى تجميع بيانات متنوعة وربما استخراج أو اشتقاق بعض الخصائص المشتركة مثل العنوان والوصف والتاريخ والمؤلف / المالك والصورة وما إلى ذلك. شيء آخر هو أنك ربما لن تفعل ذلك في استعلام واحد ، ولكن إذا كنت تستخدم ORM ، فسيتعين عليك كتابة استعلام منفصل لكل من إدخالات المدونة أو المستخدمين أو المنتجات أو الفئات أو الأحداث أو أي شيء آخر.
لقد قمت بتنظيم مشاريعي عن طريق إنشاء فهرس لكل نوع "كبير" ، على سبيل المثال ، منشور مدونة أو منتج. يمكن بعد ذلك إضافة بعض أنواع Elasticsearch لأنواع أكثر تحديدًا تندرج تحت نفس الفهرس. على سبيل المثال ، إذا كان من الممكن أن تكون المقالة قصة أو مقالة فيديو أو بودكاست ، فستظل موجودة في فهرس "المقالة" ، ولكن سيكون لدينا هذه الأنواع الأربعة داخل هذا الفهرس. ومع ذلك ، لا يزال من المحتمل أن يكون نفس الاستعلام في قاعدة البيانات.
ضع في اعتبارك أنك تحتاج إلى نوع واحد على الأقل لكل فهرس - ربما يكون نوعًا له نفس اسم الفهرس.
لتعيين الكيانات الخاصة بك ، ستحتاج إلى إنشاء بعض الفئات الإضافية. عادةً ما أستخدم فئة DocumentSearchItemBase ، والتي من خلالها ترث كل فئة من الفئات المتخصصة BlogPostSearchItem و ProductSearchItem وما إلى ذلك.
أحب أن يكون لدي تعبيرات رسام الخرائط داخل تلك الفئات. يمكنني دائمًا تعديل التعبيرات إذا لزم الأمر على الطريق.
في أحد مشاريعي المبكرة مع Elasticsearch ، كتبت فئة SearchService كبيرة إلى حد ما مع التعيينات والفهرسة التي تم إجراؤها باستخدام عبارات حالة التبديل لطيفة وطويلة: بالنسبة لكل نوع كيان أريد طرحه في Elasticsearch ، كان هناك مفتاح واستعلام مع التعيين الذي فعل ذلك.
ومع ذلك ، طوال العملية ، تعلمت أنها ليست أفضل طريقة ، على الأقل ليس بالنسبة لي.
الحل الأكثر أناقة هو الحصول على نوع من فئة تعريف الفهرس الذكية IndexDefinition تعريف فهرس محددة لكل فهرس. بهذه الطريقة ، يمكن لفئة IndexDefinition الأساسية الخاصة بي تخزين قائمة بجميع الفهارس المتاحة وبعض الطرق المساعدة مثل المحللات المطلوبة وتقارير الحالة ، بينما تتعامل الفئات الخاصة بالفهرس المشتقة مع الاستعلام عن قاعدة البيانات وتعيين البيانات لكل فهرس على وجه التحديد. هذا مفيد خاصة عندما تضطر إلى إضافة كيان إضافي إلى ES في وقت لاحق. يتعلق الأمر بإضافة فئة SomeIndexDefinition أخرى ترث من IndexDefinition وتتطلب منك فقط تنفيذ بعض الطرق التي تستعلم عن البيانات التي تريدها في فهرسك.
الكلام المرن
إن جوهر كل ما يمكنك فعله باستخدام Elasticsearch هو لغة الاستعلام الخاصة به. من الناحية المثالية ، كل ما تحتاجه لتكون قادرًا على التواصل مع Elasticsearch هو معرفة كيفية إنشاء كائن استعلام.
وراء الكواليس ، تكشف Elasticsearch وظائفها باعتبارها واجهة برمجة تطبيقات تستند إلى JSON عبر HTTP.
على الرغم من أن واجهة برمجة التطبيقات نفسها وهيكل كائن الاستعلام بديهي إلى حد ما ، إلا أن التعامل مع العديد من سيناريوهات الحياة الواقعية لا يزال يمثل مشكلة.
بشكل عام ، يتطلب طلب البحث إلى Elasticsearch المعلومات التالية:
ما الفهرس والأنواع التي يتم البحث فيها
معلومات ترقيم الصفحات (عدد العناصر التي يجب تخطيها ، وعدد العناصر المراد إرجاعها)
اختيار نوع ملموس (عند إجراء تجميع ، مثلما نحن على وشك القيام بذلك هنا)
الاستعلام نفسه
تحديد التحديد (يمكن لـ Elasticsearch تحديد النتائج تلقائيًا إذا أردنا ذلك)
على سبيل المثال ، قد ترغب في تنفيذ ميزة بحث حيث يمكن لبعض المستخدمين فقط رؤية المحتوى المتميز على موقعك ، أو قد ترغب في أن يكون بعض المحتوى مرئيًا فقط "لأصدقاء" مؤلفيه ، وما إلى ذلك.
تعد القدرة على إنشاء كائن الاستعلام في صميم الحلول لهذه المشكلات ، ويمكن أن تكون مشكلة بالفعل عند محاولة تغطية الكثير من السيناريوهات.
مما سبق ، فإن الجزء الأكثر أهمية والأصعب في الإعداد هو ، بطبيعة الحال ، مقطع الاستعلام - وهنا ، سنركز بشكل أساسي على ذلك.
الاستعلامات عبارة عن بنيات متكررة مجمعة من BoolQuery واستعلامات أخرى ، مثل MatchPhraseQuery و TermsQuery و DateRangeQuery و ExistsQuery . كانت هذه كافية لتلبية أي متطلبات أساسية ، وينبغي أن تكون جيدة كبداية.
يعد استعلام MultiMatch مهمًا جدًا لأنه يتيح لنا تحديد الحقول التي نريد إجراء البحث فيها وتعديل النتائج أكثر قليلاً - والتي سنعود إليها لاحقًا.
يمكن MatchPhraseQuery تصفية النتائج حسب ما يمكن أن يكون مفتاحًا خارجيًا في قواعد بيانات SQL التقليدية أو القيم الثابتة مثل التعداد - على سبيل المثال ، عند مطابقة النتائج حسب مؤلف معين ( AuthorId ) ، أو مطابقة جميع المقالات العامة ( ContentPrivacy=Public ).
TermsQuery ستتم ترجمة "in" إلى لغة SQL تقليدية. على سبيل المثال ، يمكنه إرجاع جميع المقالات التي كتبها أحد أصدقاء المستخدم أو الحصول على منتجات حصريًا من مجموعة ثابتة من التجار. كما هو الحال مع SQL ، لا ينبغي للمرء أن يفرط في استخدام هذا وأن يضع 10000 عضو في هذه المجموعة لأنه سيكون لها تأثير على الأداء ، لكنها عمومًا تتعامل مع المبالغ المعقولة بشكل جيد إلى حد ما.
DateRangeQuery يقوم بالتوثيق الذاتي.
يعد ExistsQuery مثيرًا للاهتمام: فهو يمكّنك من تجاهل أو إرجاع المستندات التي لا تحتوي على حقل معين.
تسمح لك هذه ، عند دمجها مع BoolQuery ، بتعريف منطق التصفية المعقد.
فكر في موقع مدونة ، على سبيل المثال ، حيث يمكن أن تحتوي منشورات المدونة على حقل AvailableFrom من الحقل الذي يشير إلى متى يجب أن تصبح مرئية.
إذا طبقنا عامل تصفية مثل AvailableFrom <= Now ، فلن نحصل على المستندات التي لا تحتوي على هذا الحقل المحدد على الإطلاق (نقوم بتجميع البيانات ، وقد لا يتم تحديد هذا الحقل في بعض المستندات). لحل المشكلة ، يمكنك دمج ExistsQuery مع DateRangeQuery ولفها داخل BoolQuery بشرط استيفاء عنصر واحد على الأقل في BoolQuery . شيء من هذا القبيل:
BoolQuery Should (at least one of the following conditions should be fulfilled) DateRangeQuery with AvailableFrom condition Negated ExistsQuery for field AvailableFrom إن رفض الاستعلامات ليس مهمة سهلة ومباشرة. ولكن بمساعدة BoolQuery ، من الممكن مع ذلك:

BoolQuery MustNot ExistsQueryالأتمتة والاختبار
لتسهيل الأمور ، فإن الطريقة الموصى بها هي بالتأكيد كتابة الاختبارات كما تذهب.
بهذه الطريقة ، ستكون قادرًا على إجراء التجربة بشكل أكثر كفاءة - والأهم من ذلك - سوف تتأكد من أن أي تغييرات جديدة تدخلها (مثل المرشحات الأكثر تعقيدًا) لن تؤدي إلى تعطيل الوظائف الحالية. لم أرغب صراحة في قول "اختبارات الوحدة" ، بما أنني لست من محبي السخرية من شيء مثل محرك Elasticsearch - لن تكون المحاكاة تقريبًا تقريبًا واقعيًا لكيفية تصرف ES حقًا - وبالتالي ، يمكن أن تكون اختبارات التكامل ، إذا أنت من محبي المصطلحات.
أمثلة من العالم الحقيقي
بعد الانتهاء من جميع الأعمال الأساسية من خلال الفهرسة ورسم الخرائط والتصفية ، أصبحنا الآن جاهزين للجزء الأكثر إثارة للاهتمام: تعديل معلمات البحث لتحقيق نتائج أفضل.
في مشروعي الأخير ، استخدمت Elasticsearch لتوفير موجز المستخدم: تم تجميع كل المحتوى في مكان واحد مرتبة حسب تاريخ الإنشاء والبحث عن النص الكامل مع بعض الخيارات. الخلاصة نفسها واضحة تمامًا ؛ فقط تأكد من وجود حقل تاريخ في مكان ما في بياناتك وقم بالترتيب حسب هذا الحقل.
البحث ، من ناحية أخرى ، لن يعمل بشكل مثير للدهشة خارج الصندوق. هذا لأنه ، بطبيعة الحال ، لا يستطيع Elasticsearch معرفة الأشياء المهمة في بياناتك. لنفترض أن لدينا بعض البيانات التي (من بين الحقول الأخرى) لها حقول Title Tags (المصفوفة) Body . يمكن أن يكون مجال الجسم عبارة عن محتوى بتنسيق HTML (لجعل الأمور أكثر واقعية قليلاً).
اخطاء املائية
المطلب: يجب أن يعرض بحثنا النتائج حتى في حالة حدوث أخطاء إملائية أو إذا كانت نهاية الكلمة مختلفة. على سبيل المثال ، إذا كان هناك مقال بعنوان "أشياء رائعة يمكنك فعلها بملعقة خشبية ،" عندما أبحث عن "شيء" أو "خشب" ، فما زلت أرغب في الحصول على تطابق.
للتعامل مع هذا ، سيتعين علينا أن نكون على دراية بالمحللين ، والمميزات ، ومرشحات شار ، ومرشحات الرموز. هذه هي التحولات التي يتم تطبيقها في وقت الفهرسة.
المحللون بحاجة إلى تعريف. يمكن تحديد هذا لكل فهرس.
يمكن تطبيق أدوات التحليل على بعض الحقول في مستنداتنا. يمكن القيام بذلك باستخدام السمات أو واجهة برمجة التطبيقات بطلاقة. في مثالنا ، نستخدم السمات.
المحللون عبارة عن مجموعة من المرشحات ومرشحات الحرف والمكونات المميزة.
للوفاء بالمتطلب (مطابقة جزئية للكلمات) ، سننشئ محلل "الإكمال التلقائي" ، والذي يتكون من:
عامل تصفية كلمات الإيقاف باللغة الإنجليزية: الفلتر الذي يزيل جميع الكلمات الشائعة في اللغة الإنجليزية ، مثل "و" أو "."
مرشح القطع: يزيل المسافة البيضاء حول كل رمز مميز
مرشح الأحرف الصغيرة: يحول جميع الأحرف إلى أحرف صغيرة. هذا لا يعني أنه عند إحضار بياناتنا ، سيتم تحويلها إلى أحرف صغيرة ، ولكن بدلاً من ذلك نتيح البحث الثابت عن حالة الأحرف.
رمز Edge-n-gram المميز: يمكّننا هذا الرمز المميز من الحصول على مطابقات جزئية. على سبيل المثال ، إذا كانت لدينا جملة "جدتي لها كرسي خشبي" عند البحث عن مصطلح "خشب" ، فإننا لا نزال نرغب في الحصول على نتيجة لهذه الجملة. ما يفعله edge-n-gram هو تخزين "woo" و "wood" و "woode" و "woode" بحيث يتم العثور على أي تطابق جزئي للكلمات مع ثلاثة أحرف على الأقل. تحدد المعلمات MinGram و MaxGram الحد الأدنى والحد الأقصى لعدد الأحرف المراد تخزينها. في حالتنا ، سيكون لدينا ثلاثة أحرف كحد أدنى و 15 حرفًا كحد أقصى.
في القسم التالي ، كل هؤلاء مرتبطون ببعضهم البعض:
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_") ) );وعندما نريد استخدام هذا المحلل ، يجب علينا فقط إضافة تعليق توضيحي للحقول التي نريدها مثل هذا:
public class SearchItemDocumentBase { ... [Text(Analyzer = "autocomplete", Name = nameof(Title))] public string Title { get; set; } ... }الآن ، دعنا نلقي نظرة على بعض الأمثلة التي توضح المتطلبات الشائعة جدًا في أي تطبيق تقريبًا يحتوي على الكثير من المحتوى.
تنظيف HTML
المطلب: قد تحتوي بعض حقولنا على نص HTML بداخلها.
بطبيعة الحال ، لن ترغب في البحث عن "section" لإرجاع شيء مثل "<section>… </section>" أو "body" لإرجاع عنصر HTML "<body>". لتجنب ذلك ، أثناء الفهرسة ، سنزيل HTML ونترك المحتوى فقط بالداخل.
لحسن الحظ ، لست أول من يعاني من هذه المشكلة. يأتي Elasticsearch مع مرشح Char مفيد لذلك:
analysis.Analyzers(a => a .Custom("html_stripper", cc => cc .Filters("eng_stopwords", "trim", "lowercase") .CharFilters("html_strip") .Tokenizer("autocomplete") )ولتطبيقه:
[Text(Analyzer = "html_stripper", Name = nameof(HtmlText))] public string HtmlText { get; set; }مجالات مهمة
المطلب: يجب أن تكون التطابقات في العنوان أكثر أهمية من التطابقات داخل المحتوى.
لحسن الحظ ، تقدم Elasticsearch استراتيجيات لتعزيز النتائج إذا حدثت المباراة في مجال أو آخر. يتم ذلك ضمن بناء استعلام البحث باستخدام خيار 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) كما ترى ، فإن استعلام MultiMatch مفيد جدًا في مثل هذه المواقف ، ومواقف مثل هذه ليست نادرة على الإطلاق! غالبًا ما تكون بعض الحقول أكثر أهمية والبعض الآخر ليس كذلك - هذه الآلية تمكننا من أخذ ذلك في الاعتبار.
ليس من السهل دائمًا تعيين قيم التعزيز على الفور. ستحتاج إلى اللعب بهذا قليلاً للحصول على النتائج المرجوة.
المقالات ذات الأولوية
المطلب: بعض المقالات أكثر أهمية من غيرها. إما أن يكون المؤلف أكثر أهمية ، أو أن المقالة نفسها بها المزيد من الإعجابات / المشاركات / الأصوات المؤيدة / إلخ. المقالات الأكثر أهمية يجب أن تحتل مرتبة أعلى.
يسمح لنا Elasticsearch بتنفيذ وظيفة التسجيل لدينا ، ونقوم بتبسيطها بطريقة نحدد فيها حقل "الأهمية" ، وهو عبارة عن قيمة مزدوجة - في حالتنا ، أكبر من 1. يمكنك تحديد وظيفة / عامل الأهمية الخاص بك وتطبيقه بصورة مماثلة. يمكنك تحديد أوضاع التعزيز والتسجيل المتعددة - أيهما يناسبك بشكل أفضل. هذا واحد يعمل لدينا بشكل جيد:
.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) ) )كل فيلم له تصنيف ، واستنتجنا تصنيف الممثل من خلال متوسط تقييمات الأفلام التي تم تصويرها فيها (ليست طريقة علمية للغاية). قمنا بتحجيم هذا التصنيف إلى قيمة مزدوجة في الفاصل الزمني [0،1].
تطابقات كاملة الكلمات
المطلب: يجب أن تحتل المطابقات ذات الكلمات الكاملة مرتبة أعلى.
في الوقت الحالي ، نحصل على نتائج جيدة إلى حد ما لعمليات البحث التي نجريها ، ولكن قد تلاحظ أن بعض النتائج التي تحتوي على مطابقات جزئية قد تأتي في مرتبة أعلى من المطابقات التامة. للتعامل مع ذلك ، أضفنا حقلاً إضافيًا في وثيقتنا باسم "الكلمات الرئيسية" والذي لا يستخدم محلل الإكمال التلقائي ، ولكنه يستخدم بدلاً من ذلك رمزًا مميزًا للكلمة الرئيسية ويوفر عاملاً معززًا لدفع نتائج المطابقة التامة إلى أعلى.
لن يتطابق هذا الحقل إلا إذا تطابقت الكلمة تمامًا. لن يتطابق مع "الخشب" مع "الخشب" مثل محلل الإكمال التلقائي.
يتم إحتوائه
يجب أن تعطيك هذه المقالة نظرة عامة حول كيفية إعداد Elasticsearch في مشروع .NET الخاص بك ، وبقليل من الجهد ، قم بتوفير وظيفة بحث جيدة في كل مكان.
يمكن أن يكون منحنى التعلم حادًا بعض الشيء ، لكنه يستحق ذلك ، خاصة عندما تقوم بتعديله بشكل صحيح والبدء في الحصول على نتائج بحث رائعة.
تذكر دائمًا إضافة حالات اختبار شاملة مع النتائج المتوقعة للتأكد من أنك لا تفسد المعلمات كثيرًا عند إدخال التغييرات والتلاعب.
الكود الكامل لهذه المقالة متاح على GitHub ، ويستخدم البيانات التي تم سحبها من قاعدة بيانات TMDB لإظهار كيف تتحسن نتائج البحث مع كل خطوة.
