جستجوی ترکیبی در RAG فارسی: راهنمای عملی
جستجوی ترکیبی در RAG فارسی با PostgreSQL و pgvector: چرا روشهای انگلیسیمحور شکست میخورند، نرمالسازی امن، ادغام رتبهها با RRF، پرهیز از فیلتر زبان و ارزیابی دوزبانه.
جستجوی ترکیبی (hybrid search) در RAG فارسی یعنی دو بازیاب را کنار هم اجرا کنید، یکی جستجوی برداری چگال (dense) و دیگری جستجوی واژگانی (lexical)، و رتبهبندی این دو را با ادغام رتبه معکوس (reciprocal rank fusion یا RRF) یکی کنید. RAG فارسی به هر دو نیاز دارد، چون متن فارسی فرضهای ضمنی روشهای انگلیسیمحور را نقض میکند: یک کلمه ممکن است با حروف عربی یا فارسی ذخیره شده باشد، نیمفاصله جزئی از خود کلمه است، و نه Snowball ریشهیاب فارسی دارد و نه PostgreSQL. خلاصه این راهنما چنین است: متن را هنگام ذخیرهسازی نرمالسازی کنید، بخش واژگانی را بدون ریشهیابی نگه دارید، رتبهها را ادغام کنید نه امتیازها را، هرگز بر اساس زبان فیلتر نکنید، سند را بر اساس ساختار خودش قطعهبندی (chunking) کنید، و پیش از آنکه به هیچیک از اینها اعتماد کنید، همه را با مجموعهای از پرسشهای دوزبانه بیازمایید.
این راهنما حاصل کار روی دو سامانه است. نخست، دستیار همین سایت است که خودم آن را ساختهام و نگهداری میکنم. دوم، کارتز کانترکت است که در آن مهندس هوش مصنوعی بودم؛ دستیار ویرایش (copilot) آن باید بندهای درست را درون قراردادهای فارسی، با شمارهگذاری ماده و بند و تبصره، پیدا میکرد. طراحی بازیابی که در این راهنما شرح داده میشود (چگال بهعلاوه واژگانی، ادغام رتبهها، pgvector) متعلق به همین سایت است؛ از کارتز فقط آنچه را کیساستادیاش آورده است برمیگیرم. این نوشته گزارش محکزنی هیچیک از این دو سامانه نیست.
RAG چیست؟ RAG یا تولید مبتنی بر بازیابی (retrieval-augmented generation) روشی است که در آن مدل زبانی پیش از پاسخدادن، متنهایی را میخواند که یک بازیاب از پیکره اسناد پیدا کرده است. کیفیت پاسخ مستند هرگز از کیفیت همین شواهد بالاتر نمیرود: اگر شاهد درست بازیابی نشود، خطا پیش از نوشتهشدن نخستین کلمه پاسخ رخ داده است. نمودار زیر معماری جستجوی ترکیبی را در دستیار همین سایت نشان میدهد.
چرا روشهای انگلیسیمحور RAG روی متن فارسی شکست میخورند؟
در فارسی، شکست رایج پاسخ غلط نیست، بیپاسخی است: جستجو کلمهای را پیدا نمیکند که کاربر آن را به چشم در سند میبیند، چون دو رشتهای که یکسان به نظر میرسند، در لایه نویسهها با هم فرق دارند.
یک کلمه، دو رشته: «ی» و «ک» عربی و فارسی
فارسی و عربی در خط مشترکاند، نه در همه نقطهکدهای (code point) یونیکد. یای فارسی و یای عربی دو نویسه جدا هستند، و کاف فارسی و کاف عربی هم:
حرف | شکل | نقطهکد | نام در یونیکد |
|---|---|---|---|
یای فارسی |
| U+06CC | ARABIC LETTER FARSI YEH |
یای عربی |
| U+064A | ARABIC LETTER YEH |
کاف فارسی |
| U+06A9 | ARABIC LETTER KEHEH |
کاف عربی |
| U+0643 | ARABIC LETTER KAF |
سند «الزامات چیدمان عربی و فارسی» از کنسرسیوم وب جهانی (W3C) جفت «ی» را صریحاً نام میبرد: این حرف «با دو نویسه متفاوت» بازنمایی میشود. متنی که با یک شکل ذخیره شده باشد، با پرسشی که با شکل دیگر تایپ شده تطبیق نمییابد، هرچند چشم تفاوتی نمیبیند؛ نمودار بالا هر دو جفت را در مرحله نرمالسازی نشان میدهد.
این سایت پیش از ورود هر متن به نمایه بازیابی، چند تبدیل روی آن انجام میدهد؛ از جمله یای عربی و الف مقصوره را به یای فارسی و کاف عربی را به کاف فارسی تبدیل میکند. نشانههای نامرئی جهت متن (bidi) را هم از نسخهای که وارد نمایه میشود حذف میکند و همین تبدیل حروف را روی هر پرسش نیز اجرا میکند.
نیمفاصله بخشی از کلمه است، نه فاصله
«میشود»، «میشود» و «می شود» سه رشته متفاوتاند. در اولی، پیشوند «می» با نیمفاصله (zero-width non-joiner یا ZWNJ، با نقطهکد U+200C) از فعل جدا شده است؛ نویسهای که اتصال حروف را قطع میکند بیآنکه فاصلهای بیفزاید. استاندارد یونیکد این گسستن اتصال پیشفرض حروف را از مواردی میداند که «گاهی در نوشتن فارسی با خط عربی لازم است»، و الزامات W3C تصریح میکند که نویسههای کنترلی «باید هنگام ذخیره و نمایش متن حفظ شوند».
یک خط لوله (pipeline) انگلیسی ممکن است بیآنکه خبری بدهد نیمفاصله را حذف کند؛ آنوقت «میشود» به «میشود» تبدیل میشود. اگر نیمفاصله به فاصله تبدیل شود، یک کلمه به دو توکن تقسیم میشود. نرمالساز هضم (hazm)، جعبهابزار پردازش زبان فارسی، «اصلاح نیمفاصله» (ZWNJ correction) را هم انجام میدهد و «کتاب های» را به «کتابهای» تبدیل میکند. در این سایت، متن ذخیرهشده نیمفاصله را نگه میدارد و فقط تبدیلی که درون مقایسهها به کار میرود، آن را فاصله به حساب میآورد.
سه دسته رقم برای یک عدد
در فارسی، عدد ۱۲۳ را با ارقامی مینویسند که W3C آنها را Eastern Arabic-Indic مینامد (U+06F0 تا U+06F9). در عربی همان عدد با ارقام Arabic-Indic نوشته میشود (U+0660 تا U+0669)، یعنی ۱۲۳، و در متنهای فنی اغلب با ارقام لاتین: 123. یک عدد، سه رشته.
ارقام را فقط برای مقایسه یکسان کنید، نه در متن ذخیرهشده؛ در این سایت، بازنویسی ارقام فارسیِ ذخیرهشده به ارقام لاتین ظاهر رزومه فارسی را عوض میکرد. اما این یکسانسازی باید در هر دو سوی مقایسه انجام شود: اگر فقط ارقام پرسش را تبدیل کنید، پرسشی که با ارقام فارسی نوشته شده دیگر با همان متنی که از آن کپی شده تطبیق نمییابد. نوشته گاردریل پاسخ استریم LLM همین یکسانسازی را روی عددهایی به کار میبرد که مدل تولید میکند.
اعراب کمکاربرد است و یک نشانه ناخواسته تطبیق دقیق را خراب میکند
الزامات W3C صریح است: «در متنی که به خط عربی نوشته میشود، بهکاربردن اعراب برای نشاندادن مصوتها و تشدید صامتها معمول نیست.» پس کافی است متنی کسره داشته باشد، یا پرسشی با تشدید از جایی کپی شده باشد، تا هیچ تطبیق دقیقی با املای بیاعراب همان کلمه برقرار نشود. در پژوهش تبدیل متن به گفتار فارسی برای همراه اول که روی آن کار کردم، همین ویژگی، این بار از سمت تولید گفتار، بخش دشوار کار بود: مصوتهای کوتاه و ساخت اضافه در نوشتار نمیآیند. برای بازیابی، اعراب را درون مقایسهها حذف کنید و در متن ذخیرهشده نگه دارید.
کاربر محاورهای میپرسد، سند رسمی نوشته شده است
تجلی، شمسفرد و کلانتری در معرفی پیکرهای موازی با ۵۰٬۰۰۰ جفت جمله مینویسند: «فارسی یکی از زبانهایی است که میان سبکهای نوشتاری رسمی و غیررسمی آن تفاوتهای فراوانی وجود دارد.» کاربر مینویسد «میتونم»، در حالی که متن رسمی «میتوانم» دارد. برداشت من از پیامد این تفاوت چنین است: تطبیقدهنده واژگانی میان پرسش محاورهای و بند رسمی همپوشانی چندانی نمیبیند؛ پس بخت اصلی با بازیاب چگال است، و مجموعه آزمونی که فقط پرسشهای رسمی و خوشساخت داشته باشد، این شکاف را پنهان میکند.
چکلیست نرمالسازی امن متن فارسی
یای عربی و کاف عربی را هنگام ذخیرهسازی، و فقط در محتوای فارسی، به «ی» و «ک» فارسی تبدیل کنید.
نیمفاصله را در متن ذخیرهشده نگه دارید.
نشانههای bidi را از نسخهای که نمایه و مقایسه میکنید حذف کنید، نه از متنی که نمایش داده میشود.
ارقام را یکسان و اعراب را حذف کنید، اما فقط درون مقایسهها و در هر دو سو.
روی پرسشها دقیقاً همان تابعی را اجرا کنید که روی متنها اجرا میکنید.
هرگز تبدیل حروف فارسی را روی متن عربی اجرا نکنید؛ در عربی، یای عربی (U+064A) و کاف عربی (U+0643) حروف درستاند.
بازیاب همین سایت هنوز به این فهرست نرسیده است: تبدیل حروف فارسی را روی همه زبانها، از جمله عربی، اجرا میکند؛ بخش واژگانیاش ممکن است متنی را که با ارقام فارسی نوشته شده پیدا نکند؛ و اعراب از واژههای پرسش و از همه مقایسههای لایه کنترل پاسخ حذف میشود، اما نه از متن ذخیرهشدهای که بخش واژگانی در آن جستجو میکند. این فهرست را هدف بخوانید.
جستجوی متن کامل فارسی در PostgreSQL: پیکربندی فارسی وجود ندارد
جستجوی متن کامل (full-text search) گزینه بدیهی برای بخش واژگانی جستجوی ترکیبی است، و به گفته مستندات PostgreSQL، این پایگاه داده «برای بسیاری از زبانها پیکربندیهای ازپیشتعریفشده» دارد. فارسی یکی از آنها نیست.
Snowball ریشهیاب عربی دارد، فارسی ندارد
فهرست الگوریتمهای ریشهیابی Snowball عربی را دارد و هیچ مدخلی برای Persian یا Farsi ندارد. PostgreSQL دیکشنریهای ریشهیابی خود را از Snowball میسازد: فهرست زبانها در کد منبع PostgreSQL 18 شامل arabic است و persian در آن نیست، و فهرست دیکشنریها در مستندات دیکشنریهای arabic_stem و simple را نشان میدهد، بیآنکه دیکشنری فارسیای در آن باشد. ریشهیاب عربی با فارسی در خط شریک است، نه در دستور زبان. بیرون از پایگاه داده، هضم ریشهیاب (stemmer) و بنواژهیاب (lemmatizer) فارسی دارد؛ درون آن، چنین ابزاری نیست.
پیکربندی simple چه میکند و چه نمیکند
به گفته مستندات دیکشنریهای PostgreSQL، قالب دیکشنری simple هر توکن را «به حروف کوچک تبدیل» و «با فایلی از واژههای ایست» (stop words) مقایسه میکند. حروف کوچک و بزرگ در خط فارسی معنایی ندارد، و دیکشنری آماده simple فایل واژههای ایست ندارد؛ پس هیچ توکنی کنار گذاشته نمیشود. ریشهیابی هم در کار نیست، و «قرارداد» و «قراردادها» دو واژه (lexeme) جدا میمانند؛ این به recall آسیب میزند، اما رفتار جستجو را پیشبینیپذیر میکند.
پیش از اجرای هر دیکشنری، تجزیهگر (parser) تعیین میکند کلمه چیست، و تعریف «حرف» در آن «با تنظیم locale پایگاه داده، بهطور مشخص lc_ctype، تعیین میشود». پس درباره شیوه توکنسازی متن فارسی در پایگاه دادهتان، بهویژه کلمههای دارای نیمفاصله، پیشفرضی نداشته باشید و با تابع ts_debug از مستندات آزمودن و اشکالزدایی جستجوی متنی از خود پایگاه داده بپرسید:
-- How does this database tokenise Persian? Check; do not assume.
SELECT alias, token, lexemes
FROM ts_debug('simple', 'قراردادها را میتوان فسخ کرد');جستجوی کلیدواژهای این سایت برای ردیفهای فارسی از simple و برای ردیفهای انگلیسی از english استفاده میکند. نمونه جستوجوی معنایی در برابر کلیدواژهای در آزمایشگاه سایت، همین جستجوی کلیدواژهای را روی محتوای سایت و در کنار بازیاب ترکیبی دستیار اجرا میکند.
شباهت سهنویسهای (pg_trgm) برای غلط تایپی
افزونه pg_trgm دنبالههای سهنویسهای (trigram) را مقایسه میکند؛ پس کلمهای که غلط تایپ شده، هنوز دنبالههای سهنویسهای مشترکی با شکل درستش دارد. این افزونه «هنگام استخراج دنبالههای سهنویسهای از یک رشته، نویسههای غیرکلمهای (نویسههایی جز حرف و عدد) را نادیده میگیرد»؛ پس پیش از تکیه بر آن، بررسی کنید که پرسوجوی زیر دنبالههایی از حروف فارسی برگرداند:
SELECT show_trgm('قرارداد');جستجوی کلیدواژهای سایت برای تحمل غلط تایپی یک شاخه word_similarity هم دارد؛ این تابع پرسش کوتاه را با شبیهترین بازه پیوسته یک متن بلند مقایسه میکند.
پوشش کلمههای کامل در بازیاب همین سایت
بازیاب دستیار برای بخش واژگانی خود از جستجوی متن کامل استفاده نمیکند، چون پیکربندی فارسیای وجود ندارد که بتوان به کار گرفت. بهجای آن میشمارد که چند واژه محتوایی از پرسش بهصورت کلمه کامل در متن آمدهاند، با مرز کلمه در عبارت منظم (regular expression). واژههای ایست هر یک از زبانهای سایت را کنار میگذارد، از واژههای بسیار کوتاه میگذرد، نشانههای مصوت خط هندی را جزء همان کلمهای نگه میدارد که به آن تعلق دارند، و ریشهیابی نمیکند. حاصل، نسبت پوششی میان ۰ و ۱ است.
مرز کلمه اهمیت دارد: تطبیق زیررشتهای، is را درون his و this و Isfahan هم پیدا میکند، و آنوقت هر متنی شبیه شاهد به نظر میرسد. کراندار بودن نسبت هم اهمیت دارد، چون آستانه شباهت را میتوان با آن مقایسه کرد. این نسبت فقط وقتی در آستانه امتناع به حساب میآید که دستکم دو واژه محتوایی متفاوت تطبیق یابند.
بردارهای تنک آموختهشده: گزینهای مستند
بازیابی تنک آموختهشده (learned sparse retrieval) مانند جستجوی واژگانی واژهها را تطبیق میدهد، با وزنهایی که مدل میآموزد. در BGE-M3 منظور از واژه، تکههایی است که توکنساز (tokenizer) میسازد؛ مثال کارت مدل BGE-M3 به De و fin و ation وزن میدهد. اندازه بردار هم، به تعبیر مقاله معرفی آن، «برابر با اندازه واژگان» است: ۲۵۰٬۰۰۲ در پیکربندی مدل. بازیاب عملیاتی این سایت از این بردارها استفاده نمیکند: ترکیب چگال و واژگانی را اجرا میکند، و API امبدینگی که فراخوانی میکند وزن تنکی برنمیگرداند. اگر این بردارها را اضافه میکنید، اول بخش دام اندیسگذاری را در ادامه بخوانید.
ادغام رتبهها، نه امتیازها: ادغام رتبه معکوس (RRF)
بازیاب چگال و بازیاب واژگانی فهرستهایی رتبهبندیشده برمیگردانند که امتیازهایشان معنای یکسانی ندارد. ادغام رتبه معکوس امتیازها را کنار میگذارد و جایگاهها را ترکیب میکند.
فرمول RRF و منشأ ثابت k = 60
Cormack، Clarke و Büttcher امتیاز ادغامشده هر سند را مجموع 1/(k + r(d)) در همه رتبهبندیها تعریف میکنند، که در آن r(d) رتبه سند در هر فهرست است. حساسیت این ثابت کمتر از آن است که به نظر میرسد: به نوشته آنها، «k = 60 در یک بررسی مقدماتی تعیین شد و در اعتبارسنجیهای بعدی تغییر نکرد»، و همان بررسی مقدماتی «نشان داد k = 60 نزدیک به بهینه است، اما انتخاب آن تعیینکننده نیست».
بازیاب این سایت k = 60 را به کار میبرد، و هر فهرست بیش از تعدادی که پاسخ به کار خواهد برد نامزد میفرستد؛ به این ترتیب متنی که در میانه یک فهرست و نزدیک صدر فهرست دیگر است، باز هم میتواند بالا بیاید. طرح کلی آن چنین است:
K = 60 # Cormack, Clarke and Büttcher
def fuse(lists, k=K):
"""lists: (weight, passage ids best-first) for each retriever."""
scores = {}
for weight, ranked in lists:
for rank, passage in enumerate(ranked, start=1):
scores[passage] = scores.get(passage, 0.0) + weight / (k + rank)
return scores
def order(scores, locales_of, query_locale):
# The fused score decides. Locale only separates exactly equal
# scores, and the id keeps the order identical on every run.
return sorted(scores, key=lambda p: (-scores[p], query_locale not in locales_of[p], str(p)))
fused = fuse([(1.0, dense_ids), (0.5, lexical_ids)])
passages = order(fused, locales_of, query_locale="fa")[:top_k]چرا جمع امتیازها به تنظیم وزن نیاز دارد
راه دیگر، جمعزدن امتیازهاست؛ همان کاری که مقاله معرفی BGE-M3 با s_rank ← w1·s_dense + w2·s_lex + w3·s_mul انجام میدهد، «که در آن مقادیر w1، w2 و w3 به سناریوی پاییندستی بستگی دارند». نویسندگان برای ترکیب چگال و تنک، روی MIRACL وزن w1 را ۱ و وزن w2 را ۰٫۳ گذاشتهاند، و روی MLDR، محکی برای بازیابی اسناد بلند، w1 را ۰٫۲ و w2 را ۰٫۸.
شباهت کسینوسی، ضرب داخلی بردارهای تنک و نسبت پوشش مقیاسهای متفاوتی دارند، و نسبت تبدیل میان آنها از پیکرهای به پیکره دیگر تغییر میکند. ادغام رتبهای نیاز به این کالیبراسیون را از میان برمیدارد. این یک بدهبستان است، نه برتری بیهزینه: مقاله با جمع وزندار هم نتایج قوی گزارش میکند، و RRF نمیتواند برتری با فاصله زیاد را از برتری با فاصله اندک تشخیص دهد.
چرا نتایج واژگانی نصف وزن میگیرند
در این سایت، نتایج واژگانی برای بالا آوردن متنهایی است که رشتهای دقیق دارند و مدل چگال ممکن است آن را محو کند: نام دامنهای مانند booqai.app، یک سرواژه، یا نام برنامهای مانند SAMPAD (سمپاد). دلیل رایج برای داشتن بخش واژگانی همین است. در نوشته Anthropic درباره Contextual Retrieval، مدل امبدینگ «ممکن است محتوایی درباره کدهای خطا بهطور کلی پیدا کند، اما تطبیق دقیق TS-999 را از دست بدهد».
نتایج واژگانی با نصف وزن سهم میگیرند؛ این وزن متنی را که فهرست چگال هم برگردانده است بالاتر میبرد، بیآنکه همپوشانی واژهها بر رتبهبندی چگال غلبه کند. این انتخاب بهایی واقعی دارد: با k = 60 و تعداد نامزدهایی که هر فهرست در این سایت برمیگرداند، متنی که فقط بخش واژگانی پیدایش کرده، پایینتر از همه نامزدهای چگال قرار میگیرد؛ پس ادغام رتبهها نمیتواند متنی را که مدل چگال بهکلی از دست داده است برگرداند. تنها استثنا متنی است که بیشترین شباهت را دارد و، چنانکه بخش بعد توضیح میدهد، همیشه برگردانده میشود. اگر فراخوانی امبدینگ شکست بخورد، بازیابی فقط با نتایج واژگانی ادامه مییابد و آستانه شباهت همچنان اعمال میشود.
امتیاز RRF معیار اطمینان نیست
امتیاز RRF فقط از جایگاهها ساخته میشود؛ پس حتی پیکرهای که سراسر نویز است، متنی در رتبه اول دارد، با بهترین امتیاز ادغامشده موجود. برای همین، آستانه امتناع این سایت با یک شباهت واقعی مقایسه میشود، نه با امتیاز ادغامشده: برای هر متن، مقدار بزرگتر از میان شباهت کسینوسی آن با پرسش، همانگونه که پرسیده شده است (و در پرسش پیگیری، با پرسش پیوسته به پرسش پیش از آن؛ و اگر پرسش به شکل جایگاهی در یک فهرست بیان شود، موردی که به آن اشاره میکند میان این دو قرار میگیرد، یا اگر آن مورد خوانده نشود، ابتدای پاسخ قبلی)، و پوشش کلمههای کامل، که فقط با تطبیق دستکم دو واژه محتوایی به حساب میآید. این مقایسه پیش از فراخوانی مدل زبانی انجام میشود.
متنی که در میان همه نامزدها بیشترین شباهت را دارد، همیشه در میان متنهای برگرداندهشده است، حتی اگر ادغام رتبهها آن را پایینتر از حد انتخاب قرار داده باشد. زیر آستانه، پرسشی که درباره من نیست بدون فراخوانی مدل رد میشود، و پرسشی که درباره من است بهجای آن رکوردهای خود سایت را دریافت میکند؛ نوشته جلوگیری از توهم هوش مصنوعی در RAG هر دو را شرح میدهد.
هرگز بازیابی را بر اساس زبان فیلتر نکنید
فیلتر زبان در نگاه اول منطقی به نظر میرسد: پرسش فارسی فقط در متنهای فارسی جستجو شود. در پیکرهای دوزبانه، همین فیلتر شواهد را حذف میکند.
بهترین شاهد ممکن است به زبان دیگر باشد
در این سایت، متنهای همه زبانها کنار هم در یک جدول قرار دارند و هر پرسش همه آنها را جستجو میکند. رزومه فارسی سند اصلی است و رزومه انگلیسی برگردانی آزاد از آن؛ پس پرسشی انگلیسی درباره یک سمت شغلی ممکن است بهترین شاهدش را در متن فارسی داشته باشد. کارتز کانترکت همین وضع را در جهت عکس داشت: دستیار تنظیم قرارداد باید نیازهای بیانشده به فارسی را به کتابخانهای از متون انگلیسی تنظیم قرارداد پیوند میداد.
زبان فقط در صورت تساوی کامل امتیازها
زبان در رتبهبندی هنوز یک کاربرد دارد: میان دو متن با امتیاز ادغامشده دقیقاً برابر، متنی که به زبان پرسش موجود است اول میآید و سپس یک شناسه پایدار تصمیم میگیرد؛ همان کلید مرتبسازی در کد بالا. با یک فهرست چگال و نصف وزن برای نتایج واژگانی، تساوی کامل عملاً رخ نمیدهد. اما اگر پرسش در بیش از یک صورت جستجو شود، که در ادامه شرح داده شده است، دو متن ممکن است از فهرستهای چگال این صورتها، که وزن برابر دارند، رتبههای یکسانی بگیرند و تساوی پیش بیاید؛ حتی آنوقت هم زبان فقط وقتی تعیینکننده است که تنها یکی از آن دو متن به زبان پرسش موجود باشد. این عمدی است: زبان هرگز نباید بر امتیاز مقدم شود. زبان یک کار دیگر هم دارد: تعیین اینکه خواننده کدام ترجمه از یک متن را ببیند.
افزودن امتیازی کوچک به متنهای همزبان، رفع تساوی نیست. سهم دو رتبه مجاور از یک فهرست با وزن کامل به اندازه 1/((k + r)(k + r + 1)) با هم فرق دارد؛ این فاصله با پایینتر رفتن جایگاه در فهرست کوچکتر میشود، و مجموع امتیازهای حاصل از دو فهرست ممکن است از این هم به یکدیگر نزدیکتر باشد. امتیاز افزوده ثابت فقط تا وقتی بیخطر است که از همه فاصلههایی که عمق فهرستها و وزنهایتان پدید میآورند کوچکتر بماند، و این مرز با تغییر هر یک از آن دو جابهجا میشود. اگر امتیاز افزوده از این مرز بگذرد، نتایجی که برابر نبودهاند در میانه فهرست جابهجا میشوند، درست میان نامزدهایی که ادغام برای بالا آوردنشان طراحی شده است.
یک متن، ده ترجمه
صفحههای این سایت به حداکثر ده زبان منتشر میشوند، و نسخههای یک متن به زبانهای مختلف ممکن بود چند جایگاه از هشت جایگاهی را بگیرند که به مدل داده میشود. بازیابی اکنون ترجمههای یک متن را یک نامزد به حساب میآورد: امتیازش را از بهترین ترجمهاش میگیرد، یک بار رتبهبندی میشود، و اگر ترجمهای به زبان خواننده داشته باشد، به همان زبان نشان داده میشود. نوشتهها بر اساس بخش گروهبندی میشوند، نه بر اساس قطعه، چون یک بخش بلند در هر زبان به تعداد متفاوتی قطعه تقسیم میشود؛ ترجمهای که بخشهایش با بخشهای نسخه انگلیسی جور درنمیآید، جدا نگه داشته میشود تا بهاشتباه گروهبندی نشود. زبان خواننده تعیین میکند کدام ترجمه نشان داده شود؛ جز در تساوی کامل، هرگز جایگاه متن در رتبهبندی را تعیین نمیکند.
پرسش فارسی روی اسناد انگلیسی
فقط بخش چگال میتواند از مرز زبان بگذرد: پرسش فارسی و متن انگلیسی، جز در اصطلاحهای لاتینی مانند RAG، تقریباً هیچ واژه مشترکی ندارند. ارزیابی خود BGE-M3 هم تکلیف فارسی را روشن نمیکند: محک میانزبانی آن، MKQA، برای پرسشهایی به ۲۵ زبان غیرانگلیسی متنهایی از ویکیپدیای انگلیسی بازیابی میکند؛ عربی در میان این زبانهاست و فارسی نیست. جفتهای فارسی–انگلیسی خودتان را بیازمایید.
پرسشهایی بدون کلیدواژه
پرسش بازدیدکنندهای مانند «اخیراً چه کار کرده؟» هیچ واژهای ندارد که پیکره بتواند با آن تطبیق دهد، و «و قبل از آن؟» اصلاً چیزی در خود ندارد. وقتی پرسش درباره من باشد، همراه با نام من هم جستجو میشود، و پرسش پیگیری در حالی که به پرسش پیش از خود پیوسته است نیز جستجو میشود؛ اگر به شکل جایگاهی در یک فهرست بیان شود، مثلاً «دومی»، موردی که به آن اشاره میکند هم میان این دو قرار میگیرد، یا اگر آن مورد خوانده نشود، ابتدای پاسخ قبلی. همه این صورتها در یک فراخوانی به بردار تبدیل و با رتبه ادغام میشوند. صورتی که نام من به آن افزوده شده، متنها را مرتب میکند اما هرگز در آستانه امتناع به حساب نمیآید، چون نام من بهتنهایی میتواند پرسشی نامرتبط درباره من را به رزومهام نزدیک نشان دهد. پرسشهایی که درباره کل کار مناند به بازیابی تکیه نمیکنند: پاسخ آنها از رکوردهای تاریخداری میآید که سایت از رزومه، کیساستادیها، نوشتهها و خدمات من میسازد، چنانکه نوشته جلوگیری از توهم هوش مصنوعی در RAG شرح میدهد.
قطعهبندی قرارداد فارسی بر اساس ماده، بند و تبصره
قراردادهای فارسی ساختار خاص خود را از ماده، بند و تبصره دارند. در دستیار ویرایش کارتز کانترکت، قرارداد خود کاربر بر اساس همین ساختار بخشبندی میشد و فقط بخشهای مرتبط با هر پرسش برای پاسخ به کار میرفت؛ «پاسخها فقط از بندهای انتخابشده استفاده میکنند». باقی پروژه در کیساستادی کارتز کانترکت آمده است و خود این حوزه در نوشته توهم هوش مصنوعی حقوقی و آنچه باید ساخت.
همین اصل، قطعهبندی این سایت را هم شکل داده است: هر قطعه (chunk) یک رکورد معنایی است. یک سمت شغلی در رزومه هرگز از تاریخهایش جدا نمیشود، هر کیساستادی در مرز بخشهایی که خودش تعریف کرده تقسیم میشود، هر نوشته در مرز سرتیترهایش، و بخش بلند فقط در مرز پاراگراف بریده میشود. عنوان سند همراه هر قطعه میآید، تا قطعهای که بهتنهایی بازیابی شده هنوز بگوید از کجا آمده است.
برای قراردادهای فارسی، این اصل به چند قاعده میرسد؛ اینها توصیههایی کلیاند، نه شرح اینکه کارتز چگونه ساخته شد:
شماره را با متن نگه دارید. «ماده ۵» و متن آن در یک قطعه جای میگیرند.
عنوان ماده را همراه هر قطعهای بیاورید که یک بند یا تبصره از آن ماده را در بر دارد.
هرگز بند را صرفاً بر اساس تعداد توکن قطع نکنید. اگر تبصره از بندش جدا شود، ممکن است بند معنایی متفاوت با منظور واقعیاش پیدا کند.
مسیر ساختاری را ذخیره کنید، یعنی ماده ← بند ← تبصره، تا پاسخ بتواند متخصص را به محل دقیق متن در قرارداد ارجاع دهد.
جستجوی دقیق یا HNSW در pgvector؟
خود pgvector نیمی از پاسخ را میدهد: «pgvector بهطور پیشفرض جستجوی دقیق نزدیکترین همسایه را انجام میدهد که recall کامل دارد.» نمایه تقریبی «بخشی از recall را با سرعت معاوضه میکند».
هزینهای که کمتر به چشم میآید، اثر فیلترهاست: «در نمایههای تقریبی، فیلتر پس از پویش نمایه اعمال میشود. اگر شرطی با ۱۰ درصد ردیفها منطبق باشد، با HNSW و مقدار پیشفرض hnsw.ef_search یعنی ۴۰، بهطور میانگین فقط ۴ ردیف منطبق خواهد بود.» pgvector از نسخه 0.8.0 به بعد میتواند نمایه را بهصورت تکرارشونده پویش کند تا ردیفهای کافی پیدا شود، به شرط آنکه این قابلیت را فعال کنید.
جدول بازیابی این سایت نمایه تقریبی ندارد: جستجوی برداری یک پویش دقیق است. وضعیت انتشار بخشی از شرط WHERE در SQL است، نه فیلتری که پس از LIMIT اعمال شود و بیآنکه کسی متوجه شود مجموعه نتایج را کوچک کند. با پویش دقیق شروع کنید، وقتی تأخیر ایجاب کرد نمایه بیفزایید، و پیش از اعتماد به آن، نتایج برترش (top-k) را روی پرسشهای خودتان با پویش دقیق مقایسه کنید.
دام اندیسگذاری sparsevec در pgvector: از ۰ یا از ۱؟
این بخش فقط برای کسی مهم است که بردارهای تنک آموختهشده را به PostgreSQL اضافه میکند، و اشتباه در آن هیچ پیغام خطایی نمیدهد.
قالب متنی بردار تنک در pgvector به شکل {index1:value1,index2:value2}/dimensions است، «و اندیسها مانند آرایههای SQL از ۱ شروع میشوند». شناسه توکنها (token id) از ۰ شروع میشود. کتابخانه پایتون pgvector اندیس صفرمبنا میگیرد (Indices start at 0) و هنگام نوشتن شکل متنی، خودش یک واحد به آن اضافه میکند.
from pgvector import SparseVector
weights = {0: 0.05, 2: 0.14, 4: 0.04} # tokeniser ids, 0-based
# Client library: pass 0-based ids; it adds 1 when it writes SQL text.
vec = SparseVector(weights, 6)
# Hand-built SQL literal: shift exactly once, here.
literal = "{" + ",".join(f"{i + 1}:{w}" for i, w in weights.items()) + "}/6"
# "{1:0.05,3:0.14,5:0.04}/6"قاعده ساده است: فقط یک بار یک واحد به اندیسها اضافه کنید، همانجا که لیترال SQL را دستی میسازید. ورودی متنی pgvector هر اندیس را به صفرمبنا برمیگرداند و خطای sparsevec index out of bounds را فقط وقتی میدهد که نتیجه کمتر از صفر، یا برابر یا بیشتر از تعداد ابعاد باشد. پس اگر دو بار یک واحد اضافه کنید، فقط برای انتهای واژگان خطا میگیرید، و اگر هیچ واحدی اضافه نکنید، فقط برای توکنی که شناسهاش صفر است؛ هر وزن دیگری، بیسروصدا، به توکن مجاور نسبت داده میشود.
انتخاب مدل امبدینگ فارسی برای RAG
محکها نقطه شروع انتخاباند، نه جایگزین آزمون روی داده خودتان.
MIRACL درباره فارسی و عربی چه میگوید
محک MIRACL بازیابی را در ۱۸ زبان میسنجد که فارسی و عربی هم در میان آنهاست. نویسندگان BGE-M3 در مقالهشان امتیاز nDCG@10 را، که کیفیت رتبهبندی ده نتیجه اول را میسنجد، روی مجموعه توسعه (dev) آن چنین گزارش میکنند (نسخه ACL؛ نویسندگان این نتایج را در تیر ۱۴۰۳ بهروز کردند):
بازیاب | فارسی (fa) | عربی (ar) |
|---|---|---|
BM25 روی توکنهای XLM-RoBERTa | ۲۸٫۷ | ۳۹٫۵ |
mE5-large (E5 چندزبانه) | ۵۹٫۰ | ۷۶٫۰ |
BGE-M3، تنک | ۴۵٫۱ | ۶۷٫۱ |
BGE-M3، چگال | ۶۰٫۹ | ۷۸٫۴ |
BGE-M3، چگال + تنک + چندبرداری | ۶۳٫۴ | ۸۰٫۲ |
در فارسی، بازیابی چگال امتیازی بسیار بالاتر از این BM25 میگیرد، امتیاز بازیابی تنک آموختهشده میان این دو است، و ترکیب هر سه حالت اندکی از چگال بهتر است. این عددها را باید با چهار ملاحظه خواند:
این BM25 روی زیرواژهها اجرا شده است. نویسندگان برای همسنگ کردن با مدل، BM25 را با توکنساز XLM-RoBERTa اجرا کردهاند؛ پیوست مقاله نشان میدهد تحلیلگر (analyzer) کتابخانه Lucene میانگین MIRACL در همه زبانها را از ۳۱٫۹ به ۳۸٫۵ میرساند. خط مبنای BM25 در مقاله خود MIRACL، که هر جا ممکن باشد از تحلیلگر ویژه همان زبان استفاده میکند، در معیار nDCG@10 روی فارسی ۰٫۳۳۳ و روی عربی ۰٫۴۸۱ میگیرد؛ این اعداد در مقیاس ۰ تا ۱ هستند.
MIRACL تکزبانه است. این مجموعه «برای پشتیبانی از وظایف بازیابی تکزبانه طراحی شده است، که در آنها پرسشها و پیکرهها به یک زباناند»؛ پس درباره پرسش فارسی روی اسناد انگلیسی چیزی نمیگوید.
مقایسه فارسی و عربی مقایسهای کنترلشده نیست. پرسشها و پیکرههای ویکیپدیای این دو زبان متفاوتاند؛ صفحه پروژه برای فارسی ۶۳۲ پرسش و برای عربی ۲٬۸۹۶ پرسش در مجموعه توسعه فهرست میکند. پس این فاصله یک نشانه است، نه سنجهای از دشواری.
BGE-M3 روی داده آموزشی MIRACL تنظیم دقیق (fine-tune) شده است، پس امتیازهای آن دروندامنهایاند.
FaMTEB و مدل حکیم
محک FaMTEB، که سال ۲۰۲۵ در Findings of EMNLP منتشر شده، محکی فارسی بر پایه MTEB است با ۶۳ مجموعهداده در هفت وظیفه، از جمله بازیابی.
حکیم (Hakim) یک مدل امبدینگ برای فارسی است که در یک پیشانتشار (preprint) در arXiv معرفی شده است؛ نسخه نخست آن در اردیبهشت ۱۴۰۴ منتشر شد و نسخه ویرایششدهاش در مهر ۱۴۰۴. نویسندگانش از ۸٫۵ درصد بهبود نسبت به رویکردهای موجود روی محک FaMTEB خبر میدهند. دو واقعیت را باید کنار این نتیجه گذاشت: نویسندگان حکیم در شمار نویسندگان FaMTEB هم هستند، و فهرست دادههای مرحله دوم آموزش حکیم MIRACL_fa را در بر دارد. هیچکدام نتیجه را نادرست نمیکند؛ هر دو دلیلیاند برای آزمودن روی داده خودتان.
پیش از انتخاب مدل چه چیزی را بسنجید
پرسشهای خودتان به فارسی و انگلیسی، از جمله عبارتهای محاورهای.
جفتهای میانزبانی: پرسش فارسی که پاسخش در متن انگلیسی است، و برعکس.
شکلهای املایی یک پرسش: «ی» و «ک» عربی یا فارسی، نیمفاصله یا فاصله، و هر سه دسته رقم.
مجوز. کارت مدل BGE-M3 مجوز آن را MIT اعلام میکند.
بیشینه طول ورودی در برابر اندازه قطعههایتان، بر حسب توکن. نویسندگان BGE-M3 از اسناد بلند تا ۸٬۱۹۲ توکن سخن میگویند، و متن فارسی ممکن است برای همان محتوا توکن بیشتری از انگلیسی لازم داشته باشد؛ نوشته مالیات توکن فارسی این اضافه را روی توکنساز خود BGE-M3 کم، و روی توکنسازهای مدلهای تولید متنی که سنجیده، بسیار بیشتر یافته است.
ابعاد بردار. در این سایت ابعاد در طرحواره (schema) پایگاه داده ثابت شده است؛ پس تغییر آن یعنی مهاجرت (migration) و نمایهسازی دوباره همه محتوا.
نمونه کاوشگر بردارهای معنایی در آزمایشگاه سایت، با وجود نامش، عمداً از شبکه عصبی استفاده نمیکند: بردارهای آن از انگرمهای نویسهای ساخته میشوند و کلمهها را بر اساس املا گروهبندی میکنند، نه معنا؛ و این همان شکافی است که مدل چگال باید برای فارسی محاورهای پر کند.
ارزیابی RAG فارسی با مجموعه پرسش طلایی دوزبانه
مجموعه طلایی (golden set) برای RAG فارسی به پرسشهایی به هر دو زبان نیاز دارد، از این سه دسته:
پرسشهای پاسخدار، همراه متنهایی که باید بازیابی شوند.
پرسشهایی که سامانه باید از پاسخ به آنها خودداری کند، چون هیچ شاهدی در پیکره ندارند.
پرسشهایی که بهترین شاهدشان به زبان دیگر است.
روی پرسشهای پاسخدار recall@k را بسنجید، بررسی کنید که هر پرسش بیشاهد زیر آستانه شباهت بماند، و آزمونی بیفزایید که اگر هیچ پاسخی از متنی به زبان دیگر استفاده نکند، شکست بخورد. اگر کسی روزی دوباره فیلتر زبان را اضافه کند، این آزمون همان روز شکست میخورد.
در این سایت، مجموعه طلایی، که به هر ده زبان سایت است و بیشتر پرسشهایش انگلیسی و فارسیاند، در CI از کد واقعی ادغام، گروهبندی، مرتبسازی و انتخاب نتایج برتر (top-k) میگذرد، اما روی پیکرهای درون حافظه و با یک مدل امبدینگ جایگزین ساده از نوع کیسه توکنها (bag-of-tokens)، نه مدل عملیاتی. این مدل جایگزین یک فهرست تنک هم میسازد که محیط عملیاتی ندارد، و مجموعه پرسشهای بیشاهد کوچک است. CI پسرفتهای سازوکار رتبهبندی را آشکار میکند، نه recall دستیاری را که روی سایت کار میکند. فرمانی که فقط بازیابی را اجرا میکند و هرگز مدل زبانی را فرانمیخواند، آستانه امتناع را با مدل امبدینگ مستقر میسنجد؛ اما آزمون recall با آن مدل هنوز اجرا نشده است، پس هیچ رقمی از recall برای این بازیاب منتشر نمیکنم.
چه چیزی از این راهنما به عربی منتقل میشود؟
من بازیابی را برای متن عربی نه طراحی کردهام و نه ارزیابی. بازیاب این سایت محتوای عربی آن را هم نمایه میکند، اما هیچ بخشی از این بازیاب برای عربی ساخته یا آزموده نشده است. این بخش فقط نتیجهای است که طراحی فارسی، روی کاغذ، درباره عربی به دست میدهد.
بیشتر توصیهها به عربی هم منتقل میشود: ادغام رتبهای، نبودن فیلتر زبان، قطعهبندی بر اساس ساختار، حذف اعراب درون مقایسهها (گفته W3C کل خط عربی را در بر میگیرد)، شروع با پویش دقیق و مجموعه طلایی دوزبانه. سه چیز فرق میکند:
PostgreSQL برای عربی ریشهیاب دارد. Snowball ریشهیاب عربی دارد و PostgreSQL دیکشنری
arabic_stem؛ اینکه ریشهیابی کمکی میکند یا نه، کار مجموعه طلایی شماست.تبدیل حروف فارسی نباید اجرا شود. در عربی، یای عربی (U+064A) و کاف عربی (U+0643) حروف درستاند؛ پس پیکره چندزبانه باید هر متن را بر اساس زبان خودش نرمالسازی کند.
ارقام متفاوتاند. ارقام عربی در بازه U+0660 تا U+0669 هستند، نه بازه U+06F0 که فارسی به کار میبرد.
پوشش محکها هم برای عربی بیشتر است: MIRACL و MKQA هر دو عربی را در بر دارند. در جدول MKQA مقاله BGE-M3، پرسشهای عربی روی ویکیپدیای انگلیسی با BGE-M3 چگال در معیار Recall@100 به ۷۱٫۱ میرسند، در برابر ۱۸٫۹ برای BM25 زیرواژهای همان مقاله؛ معیار Recall@100 پرسشی را یافتهشده میشمارد که متنی حاوی پاسخش در میان صد نتیجه اول باشد. فارسی چنین عدد میانزبانیای ندارد.
چکلیست جستجوی ترکیبی برای RAG فارسی
شکل حروف را هنگام ذخیرهسازی و فقط در محتوای فارسی یکسان کنید؛ نشانههای bidi را از نسخه بازیابی حذف کنید.
همان نرمالساز را روی هر پرسش اجرا کنید؛ نیمفاصله را در متن ذخیرهشده نگه دارید.
بخش واژگانی را بدون ریشهیابی نگه دارید؛ برای غلط تایپی شباهت سهنویسهای بیفزایید.
نتایج چگال و واژگانی را با RRF و
k = 60ادغام کنید و به نتایج واژگانی نصف وزن بدهید، با این آگاهی که نصف وزن نمیتواند متنی را که فهرست چگال از دست داده است برگرداند.آستانه امتناع را روی یک شباهت بگذارید، نه روی امتیاز RRF.
هرگز بر اساس زبان فیلتر نکنید؛ زبان فقط در تساوی کامل امتیازها و در انتخاب ترجمهای که از یک متن نشان داده میشود تعیینکننده باشد.
بر اساس ساختار سند قطعهبندی کنید و عنوان سند را همراه هر قطعه بیاورید.
با پویش دقیق شروع کنید و هر نمایه تقریبی را با آن بسنجید.
با مجموعه طلایی به همه زبانهایی که پشتیبانی میکنید، شامل پرسشهای میانزبانی و بیشاهد، ارزیابی کنید.
مدلهای امبدینگ را با پرسشهای فارسی خودتان، از جمله پرسشهای محاورهای، بیازمایید.
اگر بازیابی روی محتوای فارسی یا چندزبانه را برنامهریزی میکنید، فرم سنجش پروژه با پنج پرسش، مسئله شما را به یک معماری اولیه تبدیل میکند.
پرسشهای متداول
آیا BM25 برای جستجوی فارسی کافی است؟
بهتنهایی نه. BM25 نامها، عددها و کدهای دقیق را پیدا میکند، اما عبارتهای هممعنا با واژههای متفاوت (paraphrase) را نمیبیند، و پرسش محاورهای فارسی ممکن است با سند رسمی واژه مشترک اندکی داشته باشد. روی مجموعه فارسی MIRACL، نتایج منتشرشده مدلهای چگال بسیار بالاتر از BM25 است، چه BM25 روی زیرواژهها اجرا شود و چه با تحلیلگرهای زبانی Lucene. از BM25، یا پوشش کلمههای کامل، بهعنوان بخش واژگانی یک جستجوی ترکیبی استفاده کنید.
آیا باید پرسش فارسی را پیش از بازیابی به انگلیسی ترجمه کرد؟
بهطور پیشفرض نه. مدل امبدینگ چندزبانه خودش هر دو زبان را در یک فضای برداری مشترک مینشاند، و ترجمه یک فراخوانی مدل دیگر میافزاید که ممکن است نامها و اصطلاحهای دقیق را خراب کند. محک میانزبانیای که نویسندگان BGE-M3 به کار بردهاند عربی را دارد اما فارسی را ندارد؛ پس هر دو راه را روی جفتهای فارسی–انگلیسی خودتان بیازمایید.
بهترین مدل embedding برای RAG فارسی چیست؟
محکها فقط فهرست نامزدها را کوتاه میکنند و پاسخ نهایی را نمیدهند: نتایج MIRACL مدل BGE-M3 و E5 چندزبانه را روی فارسی پوشش میدهند، FaMTEB شامل ۶۳ مجموعهداده است، و نویسندگان حکیم بهبودی روی FaMTEB گزارش میکنند. فهرست کوتاه خود را، پس از بررسی مجوز و ابعاد بردار، روی پرسشهای محاورهای و میانزبانی خودتان بیازمایید.
آیا RAG فارسی به پایگاه داده برداری (vector database) جداگانه نیاز دارد؟
برای شروع نه. pgvector در همان PostgreSQL که دارید، بهطور پیشفرض جستجوی دقیق نزدیکترین همسایه را با recall کامل انجام میدهد و فیلترهایی مانند وضعیت انتشار را درون همان پرسوجوی SQL نگه میدارد. نمایه تقریبی را فقط وقتی اضافه کنید که تأخیر ایجاب کند.
چرا جستجوی فارسی کلمهای را که در متن هست پیدا نمیکند؟
معمولاً چون رشتهها تفاوتی دارند که چشم نمیبیند: یای عربی یا کاف عربی (U+064A یا U+0643) بهجای «ی» یا «ک» فارسی، نیمفاصلهای که بهجایش فاصله آمده یا از قلم افتاده، دسته رقم دیگر، یا یک نشانه اعراب ناخواسته. متنهای ذخیرهشده و پرسشها را با همان یک تابع نرمالسازی کنید، و در هر دو سوی مقایسه ارقام را یکسان و اعراب را حذف کنید.
منابع
مقاله Cormack، Clarke و Büttcher، کنفرانس SIGIR، سال ۲۰۰۹: Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods
مقاله Chen، Xiao، Zhang، Luo، Lian و Liu، در Findings of ACL، سال ۲۰۲۴: M3-Embedding: Multi-Linguality, Multi-Functionality, Multi-Granularity Text Embeddings Through Self-Knowledge Distillation
کارت مدل BGE-M3، منتشرشده از سوی BAAI
مقاله Zhang و همکاران، مجله TACL، سال ۲۰۲۳: MIRACL: A Multilingual Retrieval Dataset Covering 18 Diverse Languages، نسخه PDF با جدول خطوط مبنا، و صفحه پروژه MIRACL
مقاله Zinvandi و همکاران، در Findings of EMNLP، سال ۲۰۲۵: FaMTEB: Massive Text Embedding Benchmark in Persian Language
پیشانتشار Sarmadi و همکاران، arXiv، سال ۲۰۲۵: Hakim: Farsi Text Embedding Model
پروژه pgvector: README پروژه، کد منبع بردار تنک، و README کتابخانه pgvector-python
مستندات PostgreSQL: مقدمه جستجوی متنی، دیکشنریها، پشتیبانی psql، تجزیهگرها، آزمودن و اشکالزدایی جستجوی متنی، و pg_trgm
کد منبع PostgreSQL در شاخه REL_18_STABLE: فایل snowball_create.pl و فایل pg_ts_dict.dat
سند W3C، یادداشت پیشنویس گروه (Group Draft Note)، مهر ۱۴۰۴: Arabic & Persian Layout Requirements
استاندارد Unicode 18.0: فصل ۲۳، نواحی ویژه و نویسههای قالببندی، و پایگاه داده نویسههای یونیکد
مقاله تجلی، شمسفرد و کلانتری، کارگاه AbjadNLP، سال ۲۰۲۵: Developing an Informal-Formal Persian Corpus: Highlighting the Differences between Two Writing Styles
وبلاگ مهندسی Anthropic، شهریور ۱۴۰۳: Introducing Contextual Retrieval