مطالب
روش استفاده از لوسین 4.8 در دات‌نت

مطالب پیشین مرتبط با لوسین را در اینجا می‌توانید پیگیری کنید. آخرین نگارش آن که تا این تاریخ، 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. - بخش چهارم

Parallel.For & Parallel.ForEach

Parallel.For – اجرای یک حلقه for که در آن عملیات تکرار  ممکن است به صورت موازی انجام شود.
var nums = Enumerable.Range( 0, 1000000 ).ToArray();
long total = 0;

// Use type parameter to make subtotal a long, not an int
Parallel.For< long >( 0, nums.Length, () => 0,
                      ( j, loop, subtotal ) =>
                      {
                          subtotal += nums[j];
                          return subtotal;
                      },
                      x => Interlocked.Add( ref total, x ) );
Console.WriteLine( "The total is {0:N0}", total ); 
Interlocked.Add با استفاده از این متد می‌توان دو عدد صحیح را با هم جمع کرد (به صورت thread safe) و نتیجه را در عدد اول ذخیره کرد.
Parallel.ForEach – اجرای یک حلقه foreach که در آن عملیات تکرار ممکن است به صورت موازی انجام شود.
var nums = Enumerable.Range( 0, 1000000 ).ToArray();
long total = 0;
Parallel.ForEach< int, long >( nums, // source collection
                               () => 0, // method to initialize the local variable
                               ( j, loop, subtotal ) => // method invoked by the loop on each iteration
                               {
                                   subtotal += j; //modify local variable
                                   return subtotal; // value to be passed to next iteration
                               },

                               // Method to be executed when each partition has completed.
                               // finalResult is the final value of subtotal for a particular partition.
                               finalResult => Interlocked.Add( ref total, finalResult ) );
Console.WriteLine( "The total from Parallel.ForEach is {0:N0}", total );


IsInfinity

تابع  IsInfinity  جهت ارزیابی یک مقدار اعشاری که به سمت مثبت یا منفی بی نهایت می‌باشد، استفاده می‌شود.
Console.WriteLine("IsInfinity(3.0 / 0) == {0}.", double.IsInfinity(3.0 / 0) ? "true" : "false");
مقدار خروجی مثال بالا true می‌باشد.

dynamic Type

با استفاده از نوع dynamic می توان عملیات چک کردن نوع در زمان کامپایل را پشت سر گذاشت و در عوض این عملیات را به زمان اجرا، موکول داد.

نکته: نوع dynamic همانند نوع object در بسیاری از شرایط، یکسان رفتار می‌کند. اگرچه عملیات‌هایی که شامل عبارت‌هایی از نوع dynamic هستند، یا نوع آن توسط کامپایلر بررسی می‌شوند و یا پذیرفته نمی‌شوند. کامپایلر اطلاعات مربوط به یک پردازش (روند) را یکجا بسته بندی می‌کند و این اطلاعات را بعداً در زمان اجرا ارزیابی می‌کند. به عنوان بخشی از این پردازش، متغیرهایی از نوع dynamic به متغیرهایی از نوع object کامپایل می‌شوند. بنابراین نوع dynamic فقط در زمان کامپایل وجود دارند (نه در زمان اجرا).

var i = 20;
dynamic dynamicVariable = i;
Console.WriteLine( dynamicVariable );

var stringVariable = "Example string.";
dynamicVariable = stringVariable;
Console.WriteLine( dynamicVariable );

var dateTimeVariable = DateTime.Today;
dynamicVariable = dateTimeVariable;
Console.WriteLine( dynamicVariable );

// The expression returns true unless dynamicVariable has the value null.
if ( dynamicVariable is dynamic )
    Console.WriteLine( "dynamicVariable variable is dynamic" );

// dynamic and the as operator.
dynamicVariable = i as dynamic;

// throw RuntimeBinderException if the associated object doesn't have the specified method.
// The code is still compiling successfully.
Console.WriteLine( dynamicVariable.ToNow1 );

همانطور که در مثال بالا مشاهده می‌کنید، شما می‌توانید متغیرهایی از نوع‌های مختلف را به یک شی از نوع dynamic اختصاص دهید. همچنین می‌توانید برای بررسی یک متغیر که از نوع dynamic است یا خیر، از عملگر is استفاده کنید. اگر یک خصوصیت را که وجود ندارد، درخواست کنید (خط آخر مثال بالا)، خطای RuntimeBinderException پرتاب می‌شود.


ExpandoObject

ExpandoObject  این امکان را فراهم می‌آورد که  در زمان اجرا، اعضای یک شیء به صورت پویا، اضافه و حذف شوند (همانند DataTableها).
dynamic sampleObject = new ExpandoObject();
sampleObject.FirstName = "Vahid";
sampleObject.LastName = "Mohammad Taheri";
sampleObject.Age = "28";
sampleObject.TestRemoveProperty = DateTime.Now;
sampleObject.AsString = new Action( () => Console.WriteLine( "{0} {1} is {2} years old.",
                                                                sampleObject.FirstName,
                                                                sampleObject.LastName,
                                                                sampleObject.Age ) );
sampleObject.AsString();
همانطور که در مثال بالا مشاهده می‌کنید، یک شیء با 4 خصوصیت و یک متد را ایجاد کردیم. حال برای حذف یکی از خصوصیت‌ها از روش زیر استفاده می‌کنیم.
( (IDictionary< String, Object >)sampleObject ).Remove( "TestRemoveProperty" );
و در صورت استفاده از خصوصیت حذف شده، خطای  RuntimeBinderException  پرتاب می‌شود.
اشتراک‌ها
معرفی Scala برای توسعه‌دهندگان #C
سایت زبان برنامه‌نویسی Scala
Scala is a general purpose programming language designed to express common programming patterns in a concise, elegant, and type-safe way. It smoothly integrates features of object-oriented and functional languages, enabling Java and other programmers to be more productive. Code sizes are typically reduced by a factor of two to three when compared to an equivalent Java application. 

معرفی Scala برای توسعه‌دهندگان #C
اشتراک‌ها
بانک اطلاعاتی LokiJS

LokiJS is an in-memory database which prioritises performance over everything. LokiJS supports field indexing for faster document access and performs really well (near 500,000 ops/s) on those. Its built-in DynamicView class also enables to utilize indexes on data subsets for even faster performance.  

بانک اطلاعاتی LokiJS
اشتراک‌ها
روشی برای تبدیل برنامه‌های React به PWA

You’ve built a React web app and would love to bring it to iOS and Android. That means you have to learn React Native first, right? As it turns out, there’s an easier way to deploy to mobile. With Capacitor, a new native runtime for web apps, you can deploy any React-based web app as a PWA, iOS, or Android app - all from the same codebase. 

روشی برای تبدیل برنامه‌های React به PWA
نظرات مطالب
React 16x - قسمت 16 - مسیریابی - بخش 2 - پارامترهای مسیریابی
با سلام؛ به نظر میرسه اگر کامپوننتی رو داخل یک کامپوننت دیگر رند کنیم، در حالیکه در کامپوننت فرزند دکمه‌ای داشته باشیم که بعد پردازش بخواد به یک صفحه دیگه‌ای منتقل بشه (redirect کنه)، props دیگه اینجا خالیه و دستور this.props.history.push("") کار نمیکنه و خطای Cannot read property 'push' of undefined رو نشون خواهد داد. 
اگر جائیکه کامپوننت فرزند رو رندر میکنیم بخوایم props والد رو که شامل شی history هست رو بهش پاس بدیم:
<MyComponent{...this.props} />
اینبار خطای The prop `to` is marked as required in `Redirect`, but its value is `undefined`. رو از react ریافت خواهیم کرد.
در نهایت باید بجای دستور  this.props.history.push("") از  return <Redirect to={"/search/" + this.state.name} />
استفاده کنیم . آیا این امکان وجود دارد اگه نخوایم سراغ query string بریم و  بجای استفاده از روش فوق، از همان متد push در شیء history  استفاده کرد؟
اشتراک‌ها
پیشنهاد ساختار بهینه‌ی پروژه‌های React در سال 2024
React Folder Structure in 5 Steps [2024]

Organizing large React applications into folders and files is a topic that often sparks strong opinions. I found it challenging to write about this, as there isn't a definitive "correct" approach. However, I frequently get asked how I structure my React projects, from small to large, and I'm happy to share my approach.
پیشنهاد ساختار بهینه‌ی پروژه‌های React در سال 2024