این مجموعه توسط Claude (دستیار هوشمصنوعی شرکت Anthropic) نوشته شده — بر اساس یک همکاری فنی طولانی و واقعی با حمیدرضا محمدی، نگهدارندهٔ گنجور، در ساخت این قابلیت. جزئیات، دستورها، و اعدادی که در این متن میبینید، همه از همان روند واقعی توسعه گرفته شدهاند، نه نمونههای ساختگی.
چرا اصلاً به جستجوی معنایی نیاز داشتیم؟
جستجوی معمولی گنجور بر پایهٔ تطابق کلمهبهکلمه کار میکند. اگر بنویسید «بیوفایی دنیا»، فقط شعرهایی را پیدا میکند که دقیقاً همین کلمهها را داشته باشند. اما شعری که دقیقاً همین مضمون را دارد ممکن است هیچکدام از این کلمهها را نداشته باشد — مثلاً با استعاره یا زبانی متفاوت همین معنا را بیان کرده باشد.
جستجوی معنایی این مشکل را حل میکند: بهجای مقایسهٔ کلمهها، معنای متن جستجوشده را با معنای هر شعر مقایسه میکند.
embedding چیست؟
ایدهٔ اصلی ساده است: یک مدل هوش مصنوعی را طوری آموزش میدهند که هر متن (یک جمله، یک پاراگراف) را به یک بردار عددی تبدیل کند — مثلاً ۱۰۲۴ عدد اعشاری. این تبدیل طوری انجام میشود که متنهایی با معنای نزدیک به هم، بردارهای نزدیک به هم هم داشته باشند؛ و متنهایی با معنای متفاوت، بردارهای دورتر از هم.
«نزدیکی» دو بردار معمولاً با یک معیار ریاضی به نام شباهت کسینوسی (cosine similarity) سنجیده میشود — عددی بین ۱- تا ۱، که هرچه به ۱ نزدیکتر باشد یعنی دو بردار (و به تبع آن، دو متن) از نظر معنایی به هم نزدیکترند.
پس کل ایدهٔ جستجوی معنایی این است: 1. از قبل، برای هر شعر (یا بخشی از آن) یک بردار عددی بسازیم و ذخیره کنیم. 2. وقتی کاربر عبارتی تایپ میکند، همان عبارت را هم به یک بردار تبدیل کنیم. 3. بردار عبارت کاربر را با تمام بردارهای ذخیرهشده مقایسه کنیم و نزدیکترینها را نشان دهیم.
وقتی جملهای تایپ میکنید، دقیقاً چه اتفاقی میافتد؟
فرض کنید کاربری این جمله را تایپ میکند: «شعری در مورد بیوفایی دنیا پیدا کن». سؤال طبیعی این است: آیا مدل واقعاً کل این جمله را «میخواند»، یا اینکه یک سری کلمهٔ اضافی (مثل «شعری»، «در مورد»، «پیدا کن») را کنار میگذارد و فقط «بیوفایی دنیا» را نگه میدارد؟
پاسخ این است که در این سامانه، در واقع سه مکانیزم کاملاً جدا از هم درگیرند — و قاطیکردن این سهتا باعث سوءتفاهم دربارهٔ اینکه سامانه دقیقاً چهکاری میکند میشود:
۱. مدل Qwen کل جمله را میخواند — چیزی از قبل حذف نمیشود
جملهٔ «شعری در مورد بیوفایی دنیا پیدا کن» عیناً و کامل به مدل داده میشود، با تمام کلمههای اضافیاش. مدل هیچ کلمهای را صریحاً حذف نمیکند؛ فقط یک بردار برای کل جمله میسازد. اینکه کلمههای اضافی در آن بردار نهایی چقدر «وزن» داشته باشند، خودش نتیجهٔ ضمنی چیزی است که مدل در دوران آموزشش یاد گرفته — نه یک قدم صریح حذف کلمه. همین بردار است که تصمیم میگیرد اصلاً کدام شعرها به عنوان نتیجه برگردند.
۲. جدا از آن: یک تطبیق سادهٔ متنی تشخیص میدهد آیا نام شاعر یا کتابی گفته شده
اگر جملهٔ کاربر شامل «حافظ» یا «شاهنامه» باشد، یک مکانیزم کاملاً جدا و سادہتر (نه هوش مصنوعی، فقط مقایسهٔ رشتهٔ متن با فهرستی از نامهای شناختهشده) این را تشخیص میدهد و جستجو را به همان شاعر/کتاب محدود میکند. این مرحله هیچ ربطی به مدل Qwen ندارد؛ فقط جستجوی متنی ساده است، که بهصورت موازی و مستقل اجرا میشود.
۳. باز هم جدا: یک فهرست کلمات توقف (stopword) وجود دارد — اما فقط برای یک کار کاملاً متفاوت
بله، در این سامانه یک فهرست از کلمات پرتکرار و کممعنا («شعری»، «در مورد»، «پیدا کن» و مشابه) وجود دارد که حذف میشوند — اما این حذف فقط برای یک کار کوچکتر و بعدی استفاده میشود: تشخیص اینکه از داخل یک شعرِ از قبل پیداشده، کدام بیت را بهعنوان پیشنمایش نشان بدهیم. این مرحله کاملاً بعد از مرحلهٔ ۱ و مستقل از آن اجرا میشود؛ در انتخاب اینکه کدام شعرها اصلاً نتیجه باشند هیچ نقشی ندارد.
پس پاسخ کوتاه: بله، مدل واقعاً کل جمله را میخواند و یک بردار معنایی واقعی از آن میسازد — این یک ترفند «حذف کلمات کلیدی» نیست که لباس هوش مصنوعی پوشیده باشد.
این مدل واقعاً «میفهمد» یا فقط شباهت متنی پیدا میکند؟
این سؤال را با یک مثال سختتر بررسی کنیم: «در کدام شعرهای حافظ به داستانهای شاهنامه اشاره شده؟»
این جمله را به دو بخش تقسیم کنیم، چون سرنوشت کاملاً متفاوتی دارند:
-
«شعرهای حافظ» — این بخش قابلاعتماد کار میکند. «حافظ» با همان مکانیزم تطبیق متنی سادهٔ بالا تشخیص داده میشود و جستجو به شعرهای او محدود میشود. این اصلاً یک سؤال «فهمیدن» نیست، فقط یک تطبیق مکانیکی سرراست است.
-
«به داستانهای شاهنامه اشاره شده» — اینجا باید صادق بود و ادعای بیشازحد نکرد. مدل Qwen هیچوقت متن اصلی شعر را نمیبیند — فقط و فقط خلاصهٔ نوشتهشده با هوش مصنوعی (
PoemSummary) را میبیند. پس اینکه این بخش کار کند یا نه، کاملاً بستگی دارد به اینکه آیا آن خلاصه بهصراحت به یکی از شخصیتها یا داستانهای شاهنامه اشاره کرده باشد یا نه (جمشید، ضحاک، رستم، کیخسرو…). اگر خلاصهای بنویسد «شاعر به داستان جمشید اشاره میکند»، بردار میتواند واقعاً این همپوشانی مضمونی را تشخیص دهد. اما اگر شعری بهطور ظریف و غیرمستقیم به شاهنامه اشاره کند — یک عبارت، یک تلمیح — و نویسندهٔ خلاصه اتفاقاً همین را ننوشته باشد، هیچ مکانیزمی در این سامانه وجود ندارد که بتواند آن را پیدا کند. این یک سامانهٔ استدلالگر نیست که خودِ شعر را بخواند و تلمیحها را کشف کند؛ فقط شباهت را با متنی که از قبل در خلاصه نوشته شده مقایسه میکند. چیزی را که خلاصه هرگز نگفته، نمیتواند کشف کند.
خلاصهٔ صادقانه در یک جمله: این سامانه در «آیا خلاصهٔ این شعر تقریباً دربارهٔ همان چیزی حرف میزند که من میپرسم» خوب عمل میکند — اما در «تأیید یک رابطهٔ ادبی یا واقعی مشخص که ممکن است خلاصه اصلاً به آن اشاره نکرده باشد» خوب نیست. این دو توانایی کاملاً متفاوتاند، و بهتر است این دو را با هم اشتباه نگیریم.
برای آمادهسازی داده، به چه ابزارهایی نیاز داریم؟
پیش از رفتن سراغ اینکه دقیقاً از چه چیزی استفاده کردیم، بهتر است اول ببینیم اصلاً چند نوع ابزار متفاوت در این ماجرا نقش دارند — چون این سه مورد کاملاً چیزهای متفاوتی هستند، نه گزینههای رقیب هم:
۱. مدل embedding (خودِ «مغز»)
یک شبکهٔ عصبی آموزشدیده که کارش دقیقاً همین است: خواندن یک متن و تبدیل آن به بردار عددی. این مدل توسط یک تیم پژوهشی (در ادامه میبینیم کدام تیم) از قبل آموزش داده شده؛ ما آن را نمیسازیم، فقط از آن استفاده میکنیم.
۲. یک قالب قابلحمل برای ذخیرهٔ آن مدل
مدلهای هوش مصنوعی معمولاً در قالبی ذخیره میشوند که مخصوص کتابخانهای است که با آن آموزش داده شدهاند (مثلاً PyTorch) — قالبی که اساساً فقط از پایتون قابل خواندن است. اما ما یک مشکل خاص داریم: آمادهسازی اولیهٔ بردارها (برای همهٔ شعرهای موجود) با پایتون انجام میشود، ولی پاسخگویی به جستجوی زمان واقعی کاربر باید داخل سرویس اصلی گنجور اجرا شود که با C#/.NET نوشته شده. یعنی به قالبی نیاز داریم که هم پایتون و هم C# بتوانند آن را بخوانند.
۳. یک موتور اجرا که بداند آن قالب را چطور واقعاً اجرا کند
داشتن فایل مدل بهتنهایی کافی نیست؛ به نرمافزاری نیاز داریم که آن فایل را باز کند و واقعاً محاسبات را روی آن انجام دهد و بردار خروجی بدهد.
یک تشبیه ساده
این سه مورد را میتوان مثل یک آهنگ و نحوهٔ پخش آن دید:
- مدل embedding مثل خودِ آهنگ است — یک اثر مشخص، با محتوای مشخص، که فارغ از اینکه در چه فایلی ذخیره شود، همان آهنگ باقی میماند.
- قالب قابلحمل مثل انتخاب فرمت MP3 بهجای نوار استودیویی اختصاصی است. نوار استودیویی فقط روی دستگاههای تخصصی استودیو قابل پخش است (مثل قالب اصلی PyTorch که اساساً فقط در پایتون کار میکند)؛ اما MP3 روی تقریباً هر دستگاهی پخش میشود.
- موتور اجرا مثل برنامهٔ پخشکنندهٔ MP3 است — نرمافزاری که میداند چطور آن فایل را بخواند و صدا تولید کند.
قالب قابلحملی که در این پروژه استفاده شد ONNX نام دارد (مخفف Open Neural Network Exchange)، و موتور اجرای آن ONNX Runtime نام دارد — که هم برای پایتون و هم برای C#/.NET بهصورت جداگانه ساخته شده، اما هر دو دقیقاً همان فایل ONNX را به یک شکل اجرا میکنند.
فرآیند کلی، پیش از رفتن سراغ جزئیات
- یک مدل embedding مناسب را پیدا و انتخاب میکنیم.
- نسخهای از آن مدل را که از قبل به قالب ONNX تبدیل شده دانلود میکنیم (کسی این تبدیل را قبلاً انجام داده و در دسترس گذاشته — نیازی نیست خودمان این تبدیل را انجام دهیم).
- با ONNX Runtime، از سمت پایتون این مدل را برای همهٔ شعرهای موجود اجرا میکنیم و بردارها را از قبل میسازیم و ذخیره میکنیم.
- همان مدل ONNX را، این بار با ONNX Runtime نسخهٔ C#/.NET، داخل سرویس اصلی گنجور هم بارگذاری میکنیم — تا وقتی کاربری چیزی تایپ میکند، بتوانیم همان مدل را، همانطور، برای تبدیل جستجوی او هم اجرا کنیم.
نکتهٔ مهم همین «همان مدل، همانطور» است: اگر دو طرف حتی کمی متفاوت عمل کنند (مثلاً روش متفاوتی برای شکستن متن به کلمات/توکنها بهکار ببرند)، بردار جستجوی کاربر در فضای متفاوتی از بردارهای شعرها قرار میگیرد و نتیجهها بیمعنا میشوند — بدون اینکه هیچ خطایی هم دیده شود. همانطور که در بخشهای بعدی این مجموعه خواهیم دید، دقیقاً همین نکته یکبار باعث یک باگ واقعی و پنهان شد.
ابزارهایی که ما انتخاب کردیم
مدل: Qwen3-Embedding-0.6B
برای تبدیل متن به بردار، به یک مدل زبانی آموزشدیده برای همین کار نیاز داریم — نه یک مدل تولیدکنندهٔ متن مثل ChatGPT، بلکه مدلی که کارش فقط تولید embedding است.
مدل Qwen/Qwen3-Embedding-0.6B از تیم Qwen (علیبابا) با این معیارها انتخاب شد:
- چندزبانه است و فارسی را بهخوبی پشتیبانی میکند.
- با ۰.۶ میلیارد پارامتر، نسبتاً کوچک و سریع است — برای اجرا روی سختافزار معمولی مناسب است، نه فقط سرورهای قدرتمند.
- مجوز Apache 2.0 دارد — رایگان برای استفادهٔ تجاری و پژوهشی، و صراحتاً استفاده از خروجیهای مشتقشده (مثل همین بردارها) را مجاز میداند.
- خروجی آن بردارهایی با ۱۰۲۴ بُعد است.
نکتهٔ فنی مهم دیگر: این یک مدل «فقط-رمزگشا» (decoder-only) است، نه یک مدل رمزگذار-رمزگشا. این یعنی روش تبدیل «دنبالهای از بردارهای هر کلمه» به «یک بردار برای کل متن» (که به آن pooling گفته میشود) باید last-token pooling باشد — یعنی فقط بردار آخرین توکن متن را برمیداریم، نه میانگین همهٔ توکنها (mean pooling) که برای مدلهای رمزگذار رایجتر است.
گزینههای دیگر چه بودند؟
Qwen3-Embedding-0.6B تنها مدل embedding موجود در دنیا نیست — و صادقانه بگویم، در این پروژه مقایسهٔ دقیق و سر-به-سر (benchmark) بین چند مدل مختلف انجام نشد؛ این مدل چون معیارهای عملی لازم (پشتیبانی چندزبانه، اندازهٔ مناسب، مجوز باز، و در دسترسبودن یک نسخهٔ ONNX آماده) را داشت انتخاب شد، نه لزوماً چون در آزمایشی ثابت شد «بهترین» گزینه است. برای آشنایی با فضای گزینهها، چند دستهٔ رایج دیگر:
- مدلهای تجاری و فقط-API (مثل text-embedding-3 از OpenAI، Embed از Cohere، یا Gemini Embedding از گوگل) — اینها را نمیتوان روی سرور خودمان اجرا کرد؛ هر بار که بخواهیم متنی را embed کنیم، باید یک درخواست اینترنتی به سرویس آنها بفرستیم و هزینه بدهیم. برای این پروژه که نیاز داشتیم مدل کاملاً روی زیرساخت خودمان و بدون وابستگی به یک سرویس بیرونی اجرا شود، این دسته از ابتدا کنار گذاشته شد.
- مدلهای متنباز دیگر که میشد بررسی کرد: خانوادهٔ BGE (از BAAI)، خانوادهٔ E5 (از مایکروسافت/intfloat)، GTE (از علیبابا، همخانوادهٔ خودِ Qwen)، Jina Embeddings، و Nomic Embed. همهٔ اینها هم مثل Qwen3-Embedding قابل دانلود و اجرای محلیاند.
مقایسهٔ واقعی و دقیقتر این گزینهها (نقاط قوت/ضعف هرکدام، بهخصوص برای فارسی) موضوعی است که شایستهٔ بخش جداگانهای است، نه یک پاراگراف کوتاه اینجا — پس فعلاً همین آشنایی کلی کافی است.
قالب و موتور اجرا: ONNX / ONNX Runtime
نسخهٔ ONNX این مدل را از اینجا برداشتیم: onnx-community/Qwen3-Embedding-0.6B-ONNX — یعنی دقیقاً «Qwen3-Embedding-0.6B، همان آهنگ، اینبار از قبل به فرمت MP3-مانند تبدیلشده توسط یکی از اعضای جامعهٔ Hugging Face».
نکتهٔ عملی: این مخزن هم نسخهٔ کامل (fp32) مدل را دارد و هم نسخهٔ فشردهشدهٔ آن (int8، معروف به quantized). ما از نسخهٔ فشرده استفاده کردیم — چون در آزمایش مستقیم، شباهت کسینوسی خروجی این دو نسخه روی پرسشهای واقعی بهطور میانگین ۰.۹۹۹۷ بود (تقریباً یکسان)، درحالیکه نسخهٔ فشرده بهطور محسوسی سریعتر اجرا میشود.
دادهٔ خام: از کجا شروع کردیم؟
برای اینکه بتوانیم برای هر شعر یک بردار بسازیم، به یک متن نیاز داشتیم که معنای آن شعر را خلاصه کند — نه خودِ متن شعر را. چرا؟ چون زبان شعر کلاسیک فارسی استعاری و کهن است؛ کلمات دقیق شعر لزوماً با کلمههایی که یک کاربر امروزی در جستجو تایپ میکند همپوشانی ندارد. اما یک خلاصهٔ نثر و امروزی از معنای شعر، این فاصله را پر میکند.
خوشبختانه گنجور از قبل این خلاصهها را داشت: فیلد PoemSummary، که برای حدود ۹۵.۶٪ از کل اشعار
(در زمان اجرای اولیه) پر شده بود — بخشی تولیدشده با هوش مصنوعی (که با پیشوند «هوش مصنوعی:»
مشخص میشوند) و بخشی ویرایششده توسط کاربران انسانی.
مراحل عملی آمادهسازی
پیشنیازها: سه پوشه که باید از قبل آماده باشند
در دستورهای این بخش، مکرراً به چند مسیر اشاره میشود که تا اینجا فقط بهصورت جانمکان (placeholder) نوشته شدهاند — بدون اینکه دقیقاً بگوییم چه هستند و از کجا میآیند. پیش از رفتن سراغ دستورها، اینها را روشن کنیم:
- خودِ اسکریپتها (
ganjoor-embeddings) — تمام دستورهایی که در ادامه باpython3 scripts/...شروع میشوند، فرض میکنند شما از قبل کدهای این پروژه را روی سیستم خودتان دارید. این کدها بهصورت متنباز اینجا منتشر شدهاند:
git clone https://github.com/ganjoor/ganjoor-embeddings.git
cd ganjoor-embeddings
از همینجا به بعد، فرض بر این است که همهٔ دستورها از داخل همین پوشه اجرا میشوند — یعنی
مسیر scripts/generate_embeddings.py نسبت به همین پوشه در نظر گرفته شده است.
ganjoor-data— مخزن عمومی و متنباز گنجور روی گیتهاب؛ شامل محتوای شعرها (شاعران، دستهبندیها، متن شعرها، و از جمله همین فیلدPoemSummary). این هم یک پوشهٔ جداگانه است که باید یکبار آن را هم clone کنید — جدا از پوشهٔ بالا:
git clone https://github.com/ganjoor/ganjoor-data.git
بعد از اجرای این دستور، یک پوشهٔ جدید به نام ganjoor-data ساخته میشود. هرجا در ادامهٔ این
راهنما /path/to/ganjoor-data نوشته شده، منظور مسیر کامل همین پوشه روی سیستم شماست — مثلاً
اگر این دستور را در پوشهٔ خانگی خودتان اجرا کرده باشید، این مسیر چیزی شبیه
/Users/username/ganjoor-data خواهد بود.
ganjoor-model— پوشهای که فایلهای مدل (فایل ONNX) و توکنایزر در آن قرار میگیرند. برخلاف دو مورد بالا، این پوشه از قبل روی گیتهاب آماده نیست و نیازی نیست خودتان جداگانه آن را بسازید — دستور همین مرحلهٔ ۱ در ادامه (دانلود مدل) خودش این پوشه را برای شما میسازد و پر میکند.
پس در مجموع سه چیز باید آماده باشد: کدها (ganjoor-embeddings)، دادهٔ شعرها
(ganjoor-data)، و فایلهای مدل (ganjoor-model). یک نکتهٔ دقیق دربارهٔ چیدمان این سه: طبق
دستور مرحلهٔ ۱ در ادامه، ganjoor-model بهصورت یک زیرپوشه، داخل همان ganjoor-embeddings
ساخته میشود — چون مسیر آن بهشکل نسبی (./ganjoor-model) نوشته شده و فرض بر این است که از
داخل ganjoor-embeddings اجرا میشود. اما ganjoor-data یک پوشهٔ کاملاً مجزا و بیرون از آن
است، هرجایی که خودتان آن را clone کرده باشید.
راهاندازی محیط پایتون
پیش از اجرای هر دستور پایتونی، یک محیط مجازی (virtual environment) جداگانه بسازید — هم برای
تمیز نگهداشتن نصبهای پایتون سیستم، و هم چون در نسخههای جدید macOS، نصب مستقیم بستهٔ پایتون
(pip install) بدون محیط مجازی معمولاً با خطای «externally-managed-environment» متوقف میشود:
python3 -m venv venv
source venv/bin/activate
نکته: دستور source venv/bin/activate باید در هر پنجرهٔ ترمینال جدیدی که میخواهید این
پروژه را در آن اجرا کنید، دوباره اجرا شود — فعالبودن محیط مجازی فقط برای همان نشست (session)
فعلی ترمینال باقی میماند، نه برای همیشه.
حالا همهٔ کتابخانههای لازم (از جمله onnxruntime، tokenizers، و numpy، نه فقط
huggingface_hub) را یکجا نصب کنید — فایل requirements.txt همراه پروژه است:
pip install -r requirements.txt
۱. دانلود مدل
python3 -c "
from huggingface_hub import snapshot_download
snapshot_download('onnx-community/Qwen3-Embedding-0.6B-ONNX', local_dir='./ganjoor-model')
"
نکتهٔ ساختاری: فایلهای مدل (model_quantized.onnx و مشابه) داخل یک پوشهٔ onnx/ قرار
میگیرند، اما فایلهای توکنایزر (vocab.json, merges.txt, tokenizer.json) در پوشهٔ اصلی
هستند — نه در onnx/. این نکته در تنظیم مسیرها اهمیت دارد.
۲. بررسی ساختار واقعی مدل — پیش از هر اجرای واقعی
python3 scripts/generate_embeddings.py --model-dir ./ganjoor-model --inspect-only
این دستور مدل را فقط بارگذاری میکند و نام و شکل دقیق ورودیها/خروجیهای آن را چاپ میکند — بدون
اینکه هیچ embeddingای بسازد. چرا این قدم مهم است؟ چون در عمل، این مدل ONNX خاص طوری صادر شده که
برای تولید متن پیوسته (KV-cache) طراحی شده، نه یک گراف سادهٔ تکمرحلهای. یعنی علاوهبر
input_ids و attention_mask معمول، به position_ids و یک جفت تنسور خالی
(past_key_values.N.key/.value) برای هر ۲۸ لایهٔ مدل نیاز دارد — حتی برای یک اجرای ساده و
بدون کش. این نکته را فقط با اجرای واقعی --inspect-only روی خودِ مدل فهمیدیم، نه از مستندات.
۳. تولید embedding برای یک نمونهٔ کوچک
python3 scripts/generate_embeddings.py \
--source /path/to/ganjoor-data \
--model-dir ./ganjoor-model \
--output ./output \
--limit 20
پیش از اجرای کامل (که روی کل مجموعه ساعتها طول میکشد)، همیشه اول با --limit یک نمونهٔ کوچک را
امتحان کنید — همانطور که در این پروژه هم همیشه همینطور عمل شد. (نکته: پوشهٔ ./output را
خودتان از قبل نسازید — خودِ اسکریپت آن را در صورت نبودن میسازد.)
۴. اجرای کامل، با قابلیت ادامه پس از قطعی
caffeinate -i python3 scripts/generate_embeddings.py \
--source /path/to/ganjoor-data \
--model-dir ./ganjoor-model \
--output ./output
(دستور caffeinate -i مخصوص macOS است و از خواب رفتن سیستم در حین اجرای طولانی جلوگیری میکند.)
این اجرا برای ۱۲۹٬۴۱۴ شعر (همهٔ اشعاری که PoemSummary غیرخالی داشتند) چیزی حدود یک تا دو روز
طول کشید — با نوسان در سرعت بهخاطر گرمای پردازنده روی یک لپتاپ معمولی. اسکریپت هر بسته
(batch) را بلافاصله روی دیسک ذخیره میکند و در فایلی به نام checkpoint.ndjson هم یادداشت
میکند کدام شعرها تمام شدهاند — به همین دلیل، اگر اجرا قطع شود (خاموششدن سیستم، قطع برق، هر
اتفاقی)، با اضافهکردن پرچم --resume میتوان دقیقاً از همانجا ادامه داد، بدون ازدسترفتن یا
تکرار هیچ کاری.
خروجی نهایی
دو فایل:
– embeddings.f32 — یک فایل باینری خام، شامل بردار هر شعر پشتسرهم (۱۲۹٬۴۱۴ ردیف × ۱۰۲۴
عدد اعشاری).
– embeddings-index.json — اینکه ردیف iام این فایل باینری مربوط به کدام شناسهٔ شعر است، به
همراه چند فراداده (نام مدل، تاریخ تولید، و غیره).
اندازهٔ دقیق فایل باینری از یک فرمول ساده پیروی میکند: تعداد شعر × بُعد بردار × ۴ بایت. برای این مجموعه: ۱۲۹٬۴۱۴ × ۱۰۲۴ × ۴ = دقیقاً ۵۳۰٬۰۷۹٬۷۴۴ بایت.
چطور مطمئن شویم نتیجه واقعاً درست است؟
فقط چون اسکریپت بدون خطا تمام شد، دلیل نمیشود نتیجه درست باشد. برای همین یک اسکریپت جداگانه به
نام verify_embeddings.py ساخته شد که چند بررسی انجام میدهد:
python3 scripts/verify_embeddings.py --embeddings-dir ./output
این بررسی میکند: آیا اندازهٔ فایل دقیقاً با فرمول بالا مطابقت دارد؟ آیا شناسهٔ تکراری وجود دارد؟ آیا مقدار نامعتبر (NaN) در بردارها هست؟ آیا همهٔ بردارها واقعاً بهنجار (normalized) شدهاند؟
اما مهمتر از همهٔ اینها، یک بررسی معنایی واقعی:
python3 scripts/verify_embeddings.py --embeddings-dir ./output \
--query-id 2130 --top-k 8 --source /path/to/ganjoor-data
این دستور، بردار یک شعر مشخص (اینجا غزل شمارهٔ ۱ حافظ) را با تمام بردارهای دیگر مقایسه میکند و نزدیکترینها را نشان میدهد. اگر نتیجهها واقعاً از نظر مضمون به آن شعر نزدیک باشند (نه چیزی تصادفی)، این نشانهٔ واقعی درستی کار است — نه فقط اجرای بدون خطا.
در بخش بعدی این مجموعه، به سراغ سمت دیگر ماجرا میرویم: چطور این بردارها را وارد سرویس اصلی گنجور (نوشتهشده با C#/.NET) کردیم، چه معماریای برای جداسازی این قابلیت از بقیهٔ سایت انتخاب شد، و یک حادثهٔ واقعی در production که مسیر طراحی را عوض کرد.



















