نگاهی به انواع دیکشنریها در داتنت
NuoDB 1.1 برای توسعهدهندگان داتنت
الگوی Decorator در داتنت
TypeInfo در داتنت فریمورک 4.5
مطالب پیشین مرتبط با لوسین را در اینجا میتوانید پیگیری کنید. آخرین نگارش آن که تا این تاریخ، 4.8 بتا است، با داتنت(Core) سازگار است و روش برپایی آغازین آن ... تغییرات قابل توجهی داشتهاست که خلاصهی آنها را در این مطلب بررسی خواهیم کرد.
1) بستههای جدید مورد نیاز
برای کار با لوسین جدید، نیاز است حداقل سهبستهی زیر را نصب کنیم تا به امکانات پایهای و کوئری گیریهای پیشرفتهی آن دسترسی داشته باشیم:
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00016"/> <PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016"/> <PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016"/>
2) تهیه نگاشتهای لازم
فرض کنید شیء اصلی ما چنین ساختاری را دارد:
public class WhatsNewItemModel { public required int Id { set; get; } public required string OriginalTitle { set; get; } }
مرحلهی بعد کار با لوسین، تبدیل اشیاء سفارشی خود به شیء Document لوسین و برعکس است. به همین جهت به دو مپر برای این کارها نیاز است:
الف) نگاشتگر یک شیء سفارشی، به شیء Document
public static class LuceneDocumentMapper { public static Document MapToLuceneDocument(this WhatsNewItemModel post) { ArgumentNullException.ThrowIfNull(post); return [ new TextField(nameof(WhatsNewItemModel.OriginalTitle), post.OriginalTitle, Field.Store.YES), // Document StringField instances are sort of keywords, they are not analyzed, they indexed as is (in its original case). new StringField(nameof(WhatsNewItemModel.Id), post.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), ]; } }
در اینجا یک متدالحاقی را تهیه کردهایم تا شیءای از نوع WhatsNewItemModel ما را به یک شیء Document لوسین، تبدیل کند.
چند نکته در اینجا حائز اهمیت هستند:
- در نگارش جدید لوسین، با اشیاء TextField و StringField جدید سروکار داریم و شیء قدیمی Field نگارشهای قبلی لوسین، منسوخ شده درنظر گرفته میشود.
- زمانی از شیء TextField استفاده میکنیم که قرار است توسط لوسین، تحلیل شده و در جستجوهای پیچیده استفاده شود.
- اگر فقط قرار است، مقداری را در این ایندکس ذخیره کنیم و قصد تحلیل آنها را نداریم و حداکثر یک کوئری سادهی یافتن اصل آنها، مدنظر ما است، باید از اشیاء StringField برای معرفی و نگاشت آنها استفاده کنیم (شبیه به کار با واژههای کلیدی).
- پرچم Field.Store.YES به این معنا است که اصل محتوای تحلیل شده نیز در ایندکس لوسین، درج شود. اگر این پرچم را به NO تنظیم کنیم، فقط تحلیل آن صورت گرفته و نتیجهی آن ذخیره میشود، که برای جستجوها مفید است؛ اما مقدار این فیلد دیگر قابل بازیابی نخواهد بود.
ب) نگاشتگر یک شیء Document لوسین، به یک شیء سفارشی
در زمان کوئری گرفتن از لوسین، خروجی نهایی یک شیء Document آن است که باید به شیء سفارشی مدنظر ما نگاشت شود:
public static class LuceneDocumentMapper { public static LuceneSearchResult MapToLuceneSearchResult(this Document document) { ArgumentNullException.ThrowIfNull(document); return new LuceneSearchResult { Id = document.Get(nameof(WhatsNewItemModel.Id), CultureInfo.InvariantCulture).ToInt(), OriginalTitle = document.Get(nameof(WhatsNewItemModel.OriginalTitle), CultureInfo.InvariantCulture) }; } }
نمونهای از این نگاشت را در متد الحاقی فوق مشاهده میکنید که توسط متد Get شیء Document قابل انجام است. بدیهی است خروجی این متد، یک رشتهاست و در صورت نیاز باید توسط ما کار تبدیلات ثانویه آنها انجام شود.
3) نیاز به یک تحلیلگر مناسب
لوسین برای تولید ایندکسهای جستجوی تمام متنی خود، از یک سری Analyzer استفاده میکنید که اگر سری پیشین مطالب مرتبط را مطالعه کنید، به نمونهی StandardAnalyzer آن خواهید رسید که هنوز هم معتبر و قابل استفادهاست و یا میتوان همانند سایت جاری، از یک LowerCaseHtmlStripAnalyzer استفاده کرد که این کارها را همزمان انجام میدهد:
الف) از یک لیست PersianStopwords.List برای حذف واژههای کم اهمیت زبان فارسی استفاده میکند. برای مثال ما نمیخواهیم که واژهی «ما» را با اهمیت شمرده و ایندکس کند و امثال آن.
ب) LowerCaseFilter را به متون دریافتی اعمال میکند. این کار در پشت صحنهی StandardAnalyzer توکار لوسین هم اعمال میشود. اگر با این موضوع آشنا نباشید، ممکن است در حین کوئری گرفتن، به نتیجهای نرسید! چون متن ارسالی به لوسین را ابتدا باید lower-case کنید و سپس آنرا کوئری بگیرید.
ج) HTMLStripCharFilter توکار لوسین هم به آن اعمال شدهاست. از این جهت که متن مقالات ما به همراه تگهای HTML ای هم هستند. این فیلتر کار حذف کردن آنها را در حین تحلیل، انجام میدهد و دیگر نیازی نیست تا ما خودمان متن ارسالی به لوسین را تمیز کنیم.
نکتهی مهم: این تحلیلگر ویژه، فقط باید به فیلدهایی از نوع TextField اعمال شود. اگر آنرا به StringField ها اعمال کنیم، دیگر قادر به کوئری گرفتن از آنها نخواهیم بود! چون تحلیلگر StringFieldها باید از نوع توکار KeywordAnalyzer ثبت و معرفی شود. این نوع فیلدها، حالت واژههای کلیدی را دارند (به همان صورتی که هست ثبت میشوند) و قرارنیست که توسط لوسین تحلیل ویژهای شوند. به همین جهت برای رسیدن به یک تحلیلگر ترکیبی که بتواند این دو نوع فیلد را با هم پوشش دهد و کار معرفی چندین نوع تحلیلگر را یکجا انجام دهد، نیاز به یک PerFieldAnalyzerWrapper جدید داریم:
_keywordAnalyzer = new KeywordAnalyzer(); _lowerCaseHtmlStripAnalyzer = new LowerCaseHtmlStripAnalyzer(LuceneVersion); _analyzer = new PerFieldAnalyzerWrapper(_lowerCaseHtmlStripAnalyzer, new Dictionary<string, Analyzer> { { nameof(WhatsNewItemModel.Id), _keywordAnalyzer } });
PerFieldAnalyzerWrapper در حقیقت برای تمام فیلدهایی که در قسمت دیکشنری فوق، ذکر نشدهاند، از LowerCaseHtmlStripAnalyzer استفاده میکند. برای مابقی موارد از KeywordAnalyzer کمک خواهد گرفت.
4) روش صحیح راه اندازی reader و writer های ایندکس لوسین جدید
کار با لوسین به حدی سریع است که از کیفیت آن شگفت زده خواهید شد! اما ... بهشرطی که بدانید دقیقا به چه صورتی باید نویسنده و خوانندهی ایندکسهای آنرا مدیریت کنید. اکثر مثالهایی را که بر روی اینترنت پیدا میکنید، به همراه متدهایی هستند که مدام در حال گشودن و dispose این نویسندهها و خوانندههای ایندکس هستند که ... این مثالها، روش کار صحیح با لوسین نیستند! و به شدت آنرا کند میکنند.
نکتهی مهمی که این مثالها به آن توجهی نکردهاند، «thread-safe» بودن نویسنده و خوانندهی ایندکس لوسین است. یعنی میتوان یک نمونه از اینها را در ابتدای کار برنامه ایجاد کرد و تا آخر کار برنامه، بدون نیاز به نمونه سازی مجدد و باز و بسته کردن آنها، بارها مورد استفادهی مجدد قرار داد و هیچ تداخلی هم ندارند و از قسمتهای مختلف برنامه هم قابل دسترسی هستند.
به همین جهت باید یک سرویس مرکزی را برای اینکار تدارک دید که طول عمر آن، حتما Singleton باشد تا بتواند نویسنده و خوانندهی ایندکس لوسین را فقط یکبار نمونه سازی و ایجاد کرده و تا پایان کار برنامه، زنده نگه دارد (کدهای کامل این کلاس را در اینجا میتوانید مطالعه کنید):
public class FullTextSearchService : IFullTextSearchService { private const LuceneVersion LuceneVersion = Lucene.Net.Util.LuceneVersion.LUCENE_48; private readonly Analyzer _analyzer; private readonly IAppFoldersService _appFoldersService; private readonly FSDirectory _fsDirectory; // IndexWriter instances are completely thread safe, meaning multiple threads can call any of its methods, concurrently. private readonly IndexWriter _indexWriter; private readonly KeywordAnalyzer _keywordAnalyzer; private readonly ILogger<FullTextSearchService> _logger; private readonly LowerCaseHtmlStripAnalyzer _lowerCaseHtmlStripAnalyzer; // Safely shares IndexSearcher instances across multiple threads, while periodically reopening. private readonly SearcherManager _searcherManager; private bool _isDisposed; public FullTextSearchService(IAppFoldersService appFoldersService, ILogger<FullTextSearchService> logger) { _appFoldersService = appFoldersService ?? throw new ArgumentNullException(nameof(appFoldersService)); _logger = logger; _keywordAnalyzer = new KeywordAnalyzer(); _lowerCaseHtmlStripAnalyzer = new LowerCaseHtmlStripAnalyzer(LuceneVersion); _analyzer = new PerFieldAnalyzerWrapper(_lowerCaseHtmlStripAnalyzer, new Dictionary<string, Analyzer> { // Document StringField instances are sort of keywords, they are not analyzed, they indexed as is (in its original case). // But StandardAnalyzer applies lower case filter to a query. // We can fix this by using KeywordAnalyzer with our query parser. { nameof(WhatsNewItemModel.Id), _keywordAnalyzer }, { nameof(WhatsNewItemModel.DocumentTypeIdHash), _keywordAnalyzer }, { nameof(WhatsNewItemModel.DocumentContentHash), _keywordAnalyzer } }); _fsDirectory = FSDirectory.Open(_appFoldersService.LuceneIndexFolderPath); _indexWriter = new IndexWriter(_fsDirectory, new IndexWriterConfig(LuceneVersion, _analyzer)); _searcherManager = new SearcherManager(_indexWriter, applyAllDeletes: true, searcherFactory: null); }
این سرویس، یک سرویس Singleton است که نحوهی آغاز و شروع به کار با اشیاء لوسین را در سازندهی آن مشاهده میکنید.
توضیحات:
الف) در اینجا، روش نمونه سازی PerFieldAnalyzerWrapper را که پیشتر در مورد آن بحث شد، مشاهده میکنید.
ب) سپس یک IndexWriter، نمونه سازی میشود که از تحلیلگر ترکیبی ما استفاده میکند.
ج) در ادامه یک SearcherManager جدید را مشاهده میکنید که با IndexWriter برنامه هماهنگ است و هر زمانیکه سندی به لوسین اضافه میشود، قادر به کوئری گرفتن از آن هم خواهیم بود.
نکتهی مهم: طول عمر تمام این موارد، با طول عمر کلاس سرویس جاری، یکی است. یعنی تنها یکبار در طول عمر برنامه نمونه سازی شده و تا پایان کار آن، زنده نگه داشته میشوند.
5) روش افزودن یک سند به ایندکس لوسین و سپس به روز رسانی آن
اکنون با استفاده از نگاشتگرهایی که در ابتدای بحث تهیه کردیم و همچنین شیء IndexWriter فوق، به صورت زیر میتوان یک شیء سفارشی خود را به ایندکس لوسین اضافه کنیم:
_indexWriter.AddDocument(post.MapToLuceneDocument()); _indexWriter.Flush(triggerMerge: true, applyAllDeletes: true); _indexWriter.Commit();
و یا اگر خواستیم سند موجودی را به روز کنیم، روش کار به شکل زیر است:
_indexWriter.UpdateDocument(new Term(nameof(WhatsNewItemModel.Id), post.Id.ToString()), post.MapToLuceneDocument());
new Term، در حقیقت یک کوئری جدید را سبب میشود که توسط آن سندی یافت شده، در پشت صحنه حذف میشود و سپس سند جدیدی بجای آن درج خواهد شد. در اینجا باید دقت داشت که چون Id ثبت شده از نوع StringField است، نباید حالت lower-case آنرا جستجو کرد و باید دقیقا به همان نحوی که ثبت شده، جستجو شود.
6) روش کار با searcherManager جدید لوسین
همانطور که عنوان شد، لوسین جدید به همراه یک searcherManager هم هست که کار آن، ارائهی thread-safe دسترسی به خوانندهی ایندکس لوسین است. نحوهی عمومی کار با آن را در ادامه مشاهده میکنید:
private TResult DoSearch<TResult>(Func<IndexSearcher, TResult> action, TResult defaultValue) { _searcherManager.MaybeRefreshBlocking(); var indexSearcher = _searcherManager.Acquire(); try { return action(indexSearcher); } catch (FileNotFoundException) { // It's not indexed yet. return defaultValue; } finally { _searcherManager.Release(indexSearcher); } }
با استفاده از searcherManager، در طول مدت زمان کوتاهی، بر روی ایندکس قفلگذاری شده و یک indexSearcher امن، در اختیار متدهای استفاده کنندهی از آن قرار میگیرند و در پایان کار، این قفل رها میشود.
برای مثال یک نمونه روش استفاده از این indexSearcher امن، به صورت زیر است:
public int GetNumberOfDocuments() => DoSearch(indexSearcher => indexSearcher.IndexReader.NumDocs, defaultValue: 0);
مابقی مثالهای آنرا میتوانید در کلاس FullTextSearchService مشاهده کنید که به همراه یافتن «مطالب مشابه»، جستجوهای صفحه بندی شده، جستجوهای مرتب شدهی بر اساس یک فیلد، امکان دسترسی به تمام اسناد ذخیره شدهی در ایندکس لوسین و امثال آن است که کلیات آن با قبل تفاوتی نکردهاست و مطالب و نکات آنرا پیشتر در مقالات سری لوسین بررسی کردهایم. تنها تفاوت مهمی که در اینجا وجود دارد، نحوهی برپایی و راه اندازی تحلیلگر، خواننده و نویسندهی ایندکس آن است که در این مطلب بررسی شدند؛ وگرنه کلیات جستجوی پیشرفتهی آن، مانند قبل است و تفاوت خاصی نکردهاست.
- آخرین نگارش NET 5 SDK. را از اینجا دریافت و نصب کنید (حتما SDK باشد و نه runtime).
- اگر میخواهید با ویژوال استودیو کار کنید، نیاز است حداقل نگارش 16.7 یا بالاتر را نصب کرده باشید. به همین جهت پیش از ادامهی بحث، از منوی Help، گزینهی Check For Updates را انتخاب کرده و حتما آخرین به روز رسانیهای موجود را نصب کنید. بنابراین برای کار با C# 9.0 توسط VS، حتما باید آخرین نگارش 2019 آن، به همراه تمام به روز رسانیهای ممکن بر روی سیستم شما نصب باشند؛ در غیراینصورت امکان کار با آنرا حداقل توسط VS نخواهید داشت.
- در زمان نگارش این مطلب چون هنوز نگارش نهایی NET 5 SDK. ارائه نشدهاست، نیاز است به منوی Tools در ویژوال استودیو مراجعه کرده و با انتخاب «Preview Features» آن، گزینهی «Use previews of the .NET Core SDK» را فعال کنید. پس از آن، یکبار هم نیاز است VS را بسته و راه اندازی مجدد نمائید.
پس از نصب پیشنیازهای لازم، اکنون فایل csproj را به صورت زیر ویرایش کنید:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net5.0</TargetFramework> <LangVersion>9.0</LangVersion> </PropertyGroup> </Project>
تنظیمات مورد نیاز C# 9.0 در پروژههای کتابخانهای
در این نوع پروژهها، علاوه بر نصب پیشنیازهای یاد شده، نیاز است TargetFramework را به حداقل netstandard2.1 تنظیم کرد (و یا حتی net5.0 هم در اینجا کار میکند):
<TargetFramework>netstandard2.1</TargetFramework> <LangVersion>9</LangVersion>
تنظیمات مورد نیاز C# 9.0 در VSCode
برای کار با VSCode، تنها کافی است آخرین نگارش SDK و آخرین نگارش افزونهی #C آنرا نصب کنید و TargetFramework و LangVersion را همانطور که عنوان شد، تنظیم نمائید. این روش سادهترین و کم حجمترین روش کار با C# 9.0 است.
تنظیمات مورد نیاز C# 9.0 در Rider
اگر میخواهید برای کار با C# 9.0 از محصولات Jetbrains استفاده کنید، نیاز است حداقل نگارش 2020.3 آنها را نصب کنید که در این زمان هنوز در مرحلهی پیشنمایش (ReSharper 2020.3 EAP or Rider 2020.3 EAP) به سر میبرند.
من از چه روشی استفاده میکنم؟!
VS کامل بر روی سیستم من نصب نیست؛ هیچ نگارشی از آن! عمدهی کارهای من توسط VS Code و افزونهی #C آن انجام میشوند و هر از چندگاهی، یکبار هم توسط Rider چون به همراه ReSharper توکار است، کنترل کیفیت میشوند (در حد بررسی گزارشهای ReSharper آن). البته افزونهی Roslynator برای VS Code هم موجود است و بسیاری از قابلیتهای ReSharper را در VSCode نیز مهیا میکند.
public static void PrintSum(int a, int b) { Console.WriteLine("Sum of a {0} b {1} is {2}", a, b, a + b); }
Console::WriteLine(string, object, object, object)
روشی برای نمایان ساختن تخصیصهای حافظهی نهان در ویژوال استودیو
اگر از ReSharper استفاده میکنید، افزونهی «Heap Allocations Viewer» آن و یا اگر از VS 2015 و Roslyn استفاده کنید، افزونهی «Roslyn Clr Heap Allocation Analyzer» آن، سبب نمایان شدن allocationهای مخفی میشوند. برای مثال قطعه کد فوق یک چنین نمایشی را پیدا میکند:
در اینجا در ذیل هر سه موردی که عملیات boxing allocation رخ داده، یک خط قرمز کشیده است. یکی از روشهایی که میتواند boxing allocation فوق را حذف کند، بکار گیری متد ToString بر روی مقادیر int است:
همانطور که مشاهده میکنید، اینبار دیگر خبری از خطوط قرمز، ذیل پارامترهای متد Console.WriteLine نیست. باید دقت داشت که ToString نیز سبب تخصیص حافظه میشود، اما اینبار دیگر int32 آن بر روی heap ذخیره نمیگردد. به عبارتی هر دو حالت سبب تخصیص حافظهی یک رشتهی جدید میشوند؛ اما در حالت اول علاوه بر این شیء جدید، شیء int32 نیز بر روی heap ذخیره میگردد.
تشخیص تخصیص اشیاء مخفی با افزونههای Heap Allocations Viewer
نمونهی دیگر پر کاربرد این نوع بهینه سازیها را در مثال ذیل میتوان مشاهده کرد:
public static void PrintA(int a) { Console.WriteLine("a is " + a); }
اینبار یک خط زرد رنگ ظاهر شده به همراه یک خط قرمز رنگ. خط قرمز رنگ را پیشتر بررسی کردیم و علت وجودی آن Boxing allocation ایی است که رخ میدهد. خط زرد رنگ در ذیل + ظاهر شدهاست و عنوان میکند که عملیات جمع زدن رشتهها، سبب تخصیص حافظهی یک شیء جدید میشود. رشتهها در دات نت immutable هستند. به همین جهت هر تغییری در آنها، سبب تخصیص یک شیء جدید میشود. بنابراین در همین مثال ساده، دو تخصیص حافظهی مخفی وجود دارند. مورد جمع زدن را با بکارگیری string.Format و مشکل boxing را با ToString میتوان برطرف کرد:
public static void PrintA(int a) { Console.WriteLine("a is {0}", a.ToString()); }
منابع دیگری که سبب تخصیصهای حافظهی مخفی میشوند
تا اینجا دو مورد از منابع متداول تخصیصهای حافظهی مخفی را بررسی کردیم. اما این لیست شامل موارد ذیل نیز میشود:
1) فراخوانی متدهایی با پارامترهایی از نوع param همیشه سبب تخصیص حافظهای جهت تشکیل یک آرایهی در برگیرندهی پارامترهای ارسالی میشود.
2) متدهایی که پارامتر از نوع IEnumerable دارند:
public static int Sum(IEnumerable<int> list) { var sum = 0; foreach (var number in list) { sum += number; } return sum; }
برای حل این مشکل فقط کافی است IEnumerable را با List تعویض کنید.
3) کار با LINQ نیز سبب تخصیصهای حافظهی قابل توجهی است. برای مثال در کد پایهی Roslyn، برای رسیدن به حداکثر کارآیی، بسیاری از الگوریتمها را با روشهای غیر LINQ پیاده سازی کردهاند. البته برای تیمی مانند Roslyn رسیدن به یک چنین کارآیی جهت رقابت با سایر محصولات مشابه ضروری بودهاست و گرنه در بسیاری از کارهای متداول، استفاده از LINQ به خوانایی هر چه بیشتر کدها کمک شایانی میکند.
برای مطالعهی بیشتر
Roslyn code base – performance lessons - part 2
Unusual Ways of Boosting Up App Performance. Boxing and Collections
On performance in .NET