(به توصیه چند تا از دوستان و به کمک chatGPT این پست رو که قبلا اینجا منتشر کرده بودم فارسی کردم و اینجا گذاشتم.)
یک کانفیگ Analyzer تستشده در محیط اجرای واقعی برای کاتالوگ محصول فارسی
اگر تا به حال برای یک کاتالوگ محصول فارسی، مثل دیتای دیجی کالا که اینجا کرال کردیم، سیستم جستجو ساخته اید و حس کرده اید نتایجی که بر می گردونه تصادفی یا غیرقابلپیشبینی هستند، تنها نیستید.
Analyzer پیشفرض Elasticsearch برای زبانهای لاتین طراحی شده است و اگر بدون تغییر روی متن فارسی بشود، تجربه جستجویی خواهد ساخت که هم کاربر را ناامید میکند و هم روی نرخ کانورژن خوبی ایجاد نخواهد کرد.
در این پست، یک کانفیگ analyzer که در محیط production و روی میلیونها کوئری فارسی استفاده و بهینه شده را به اشتراک میگذارم. اما مهمتر از خود کد، توضیح میدهم چرا هر تصمیم گرفته شده است، تا بتوانید آن را هوشمندانه با دادههای خودتان تطبیق بدهید.
ریشه مشکل: متن فارسی با استاندارد یگانه ای نوشته نمی شود!
قبل از هر تغییری بای داده را درست شناخت. محتوای فارسی از منابع مختلفی میآید:
اطلاعات وارد شده توسط فروشنده ها، کاتالوگهایی که به سیستم اضاف میشوند، سیستمهای قدیمی، متن تولیدشده توسط کاربر و ...
این دادهها معمولا با هم سازگاری ندارند. یک کلمه واحد ممکن است بنا به دلایل زیر، در چندین شکل متفاوت ظاهر شود:
تفاوت کیبورد عربی و فارسی
وجود یا عدم وجود اِعراب
استفاده یا عدم استفاده از نیمفاصله (ZWNJ)
استفاده از اعداد فارسی، عربی یا لاتین
اگر این تفاوتها قبل از توکنسازی (tokenization) نرمالسازی نشوند، اسناد و کوئریهایی که باید با هم match شوند، هرگز یکدیگر را پیدا نمیکنند.
تقریباً تمام اجزای یک analyzer درست برای فارسی، برای حل همین مشکل طراحی میشوند.
Character filterها قبل از توکنسازی اجرا میشوند. برای فارسی، بخش عمده کار واقعی همینجاست.
نگاشت حروف عربی به فارسی
"arabic_char_mapper": { "type": "mapping", "mappings": [ "ك => ک", "أ => ا", "إ => ا", "ي => ی", "ى => ی", "ئ => ی", "ة => ه", "ؤ => و", "آ => ا" ] }
فارسی و عربی الفبای مشترکی دارند، اما برخی حروف حتی اگر ظاهرا شبیه به هم دیده بشوند از نظر یونیکد متفاوتاند. این نگاشت این مشکل را حل می کند و اگر انجام نشود، ممکن است کوئری کاربر با کیبورد فارسی نوشته شود ولی عنوان محصولی که از با کیبورد عربی نوشته شده باشد و با اینکه از دید ما یکسان هستند با هم match نشوند.
حذف اِعراب
در فارسی روزمره معمولاً از اِعراب استفاده نمیشود، اما در دادههای قدیمی یا داده های که به سیستم inject میشود ممکن است اعراب وجود داشته باشد. وجود اِعراب فقط تنوع بیدلیل ایجاد میکند. بنابراین باید حذف شود.
مثال:
کِتاب = کتاب
"persian_vowel_remover": { "type": "mapping", "mappings": [ "\\u064B => ", "\\u064C => ", "\\u064D => ", "\\u064E => ", "\\u064F => ", "\\u0650 => ", "\\u0651 => ", "\\u0652 => ", "\\u0653 => ", "\\u0670 => " ] }
نرمالسازی اعداد
در متن فارسی سه سیستم عددی داریم:
اعداد عربی-هندی
اعداد فارسی
اعداد لاتین
مثلاً:
۱۲۸ گیگابایت
باید با
128 گیگابایت
match شود. ولی اگر نرمالسازی انجام نشود، توکنهای این دو متن متفاوت خواهند بود و match اتفاق نمیافتد.
"persian_digit_mapper": { "type": "mapping", "mappings": [ "٠ => 0", "١ => 1", "٢ => 2", "٣ => 3", "٤ => 4", "٥ => 5", "٦ => 6", "٧ => 7", "٨ => 8", "٩ => 9", "۰ => 0", "۱ => 1", "۲ => 2", "۳ => 3", "۴ => 4", "۵ => 5", "۶ => 6", "۷ => 7", "۸ => 8", "۹ => 9" ] }
حذف نیمفاصله و کاراکترهای نامرئی
کاراکترهای نامرئی یونیکد مثل ZWNJ در منابع مختلف به شکلهای متفاوت استفاده میشوند.
کاربر آنها را نمیبیند، اما برای موتور جستجو تفاوت دارند. حذف این کاراکترها برای یک جستجوی موثر، کاملاً ضروری است.
"persian_zwnj_mapper": { "type": "mapping", "mappings": [ "\\u200B => ", "\\u200C => ", "\\u00A0 => ", "\\u2009 => ", "\\u200A => ", "\\u2002 => " ] }
حذف علائم نگارشی
"punctuation_mapper": { "type": "mapping", "mappings": [ ": => ", ", => ", "! => ", "? => ", "؟ => ", ". => ", "- => ", "، => " ] }
هم علائم لاتین و هم علائم فارسی (مثل «،» و «؟») باید در نظر گرفته شوند. بسیاری از تیمها این مورد را نادیده میگیرند و نتیجه آن، توکنسازی ناسازگار و mismatchهای غیرضروری است.
جدا کردن پسوندها
"persian_suffix_stripper": { "type": "pattern_replace", "pattern": "([\u0600-\u06FF\\w]+)(های|ها)", "replacement": "$1 $2" }
این فیلتر، پسوندهای «ها» و «های» را جدا میکند تا بعداً بتوان آنها را به عنوان stopword حذف کرد. این مرحله stemming واقعی نیست؛ بلکه یک راهحل عملی برای کاتالوگهای محصول است. در واقع در کاتالوگ های مربوط به فروشگاه ها یا لیست محصولات، stemming کامل ضروری نیست. اما اگر با متن فارسی به صورت کلی، و نه کاتالوگ های فروشگاه ها کار می کنید، بهتر است stemming را آفلاین (مثلاً با Hazm) انجام دهید و نتیجه را با stemmer_override وارد کنید.
بعد از توکنسازی، token filterها روی استریم توکنها اعمال میشوند.
جدا کردن متن و عدد
"text_digit_splitter": { "type": "pattern_replace", "pattern": "([\u0600-\u06FFa-zA-Z\\s])(\\d+)|(\\d+)([\u0600-\u06FFa-zA-Z\\s])", "replacement": "$1$3 $2$4" }
در عنوان محصول زیاد میبینیم:
Samsung32اینچ
اگر بخش های مخلف این محصول جدا نشود، به یک توکن غیرقابلجستجو تبدیل میشود.
این فیلتر باعث میشود هم بخش متنی و هم عددی قابل match باشند.
Stopwordها
"persian_stopwords": { "type": "stop", "stopwords": ["و", "از", "در", "با", "برای", "ها", "های"] }, "digikala_stopwords": { "type": "stop", "stopwords": ["طرح", "کد", "مدل"] }
دو لیست جدا داریم:
Stopwordهای عمومی فارسی برای حذف کلمات عملکردی کماهمیت
Stopwordهای دامنهای (Domain-specific) برای حذف کلماتی که تقریباً در همه عناوین محصول تکرار میشوند (مثل «مدل» و «کد»)
جدا بودن این دو لیست، نگهداری را سادهتر و امنتر میکند.
نکته:
هفت stopword فارسی ای که اینجا لیست شده حداقل ممکن است. اگر داده عمومیتر دارید، اضافه کردن کلماتی مثل «که»، «را»، «به»، «این»، «یک» احتمالاً به بهبود ranking کمک میکند.
اما قبل از deploy حتماً اثر آن بر recall را بررسی کنید.
مترادفها (Synonyms)
"synonyms_filter": { "type": "synonym", "synonyms": ["بسته,پک"] }
با یک لیست کوچک شروع کنید. و کم کم و بر اساس آنچه در لاگهای سیستم می بینید لیست مترادف ها را گسترش بدهید.
حدس نزنید. مترادفها را از رفتار واقعی کاربران (کوئری و کلیک) استخراج کنید.
نکته عملیاتی:
اگر مترادفها در زمان ایندکس اعمال شوند، برای تغییر آنها باید reindex انجام دهید.
اگر لیست مترادفها زیاد تغییر میکند، بهتر است در زمان کوئری اعمال شوند.
در اغلب سیستمهای production، استفاده از synonym در زمان ایندکس توصیه نمیشود مگر اینکه لیست ثابت و پایدار باشد.
"my_bm25_persian": { "type": "BM25", "k1": 1.2, "b": 0.3 }
این بخش معمولاً نادیده گرفته میشود، اما در جستجوی محصول تأثیرگذار است. در فرمول BM25 پارامتر b میزان نرمالسازی طول سند را کنترل میکند و مقدار پیشفرض 0.75 است. در عناوین محصول که کوتاه و تقریباً همطول هستند، این مقدار معمولاً ایدهآل نیست. مقدار 0.3 باعث میشود اختلاف طول تأثیر کمتری روی امتیاز داشته باشد.
پارامتر k1 میزان اشباع تکرار کلمه را کنترل میکند. مقدار 1.2 معمولاً برای عنوان محصول مناسب و پایدار است.
اما در نهایت، این مقادیر را باید با داده و لاگ واقعی اعتبارسنجی کنید، نه بر اساس حدس.
"my_persian_analyzer": { "tokenizer": "standard", "char_filter": [ "arabic_char_mapper", "persian_vowel_remover", "persian_digit_mapper", "persian_zwnj_mapper", "punctuation_mapper", "persian_suffix_stripper" ], "filter": [ "lowercase", "text_digit_splitter", "persian_stopwords", "digikala_stopwords", "synonyms_filter" ] }
در نهایت باید این character filter و token filterها را در یک آنالایزر کنار هم قرار بدهید و استفاده کنید. ترتیب اجرای مراحل مهم است. ابتدا نرمالسازی کاراکتری انجام میشود تا متن قبل از tokenization تمیز و یکنواخت شود.
سپس token filterها روی جریان نرمالشده اعمال میشوند.
فیلتر lowercase قبل از stopword اجرا میشود تا حذف stopwordها بدون حساسیت به حروف انجام شود.
این پیکربندی چه مشکلاتی را حل میکند؟
نرمالسازی کاراکتری در سطح یونیکد
یکسانسازی اعداد
حذف کاراکترهای نامرئی
بهبود ranking واژگانی (lexical) برای عناوین محصول فارسی
چه چیزهایی را حل نمیکند؟
stemming واقعی
درک نیت کاربر (query intent)
رتبهبندی معنایی (semantic ranking)
جایگزینی کامل
این پیکربندی، یک پایهی lexical قوی میسازد. اینکه روی آن چه چیزی بسازید ،در سرچ کلاسیک از آن استفاده کنید یا مثلا در هیبرید رنکینگ و وکتور سرچ از آن استفاده کنید، کاملاً به مقیاس، منابع و جاهطلبی شما بستگی دارد.