# موتور جست‌وجو

پیاده‌سازی: `backend/src/modules/search/` و `backend/src/common/persian.ts`
رابط کاربر: `frontend/features/search/search-experience.tsx`

جست‌وجو مهم‌ترین قابلیت این سامانه است، چون کاربر معمولاً نمی‌داند محتوای مورد
نظرش خبر است، پرونده است، سازمان است یا مجموعه‌داده. بنابراین **همه‌ی موجودیت‌ها
در یک نمایه‌ی واحد** (`search_documents`) قرار می‌گیرند.

## ۱. نرمال‌سازی فارسی

هر متن — چه هنگام نمایه‌سازی و چه هنگام پرس‌وجو — از `normalizePersian()` عبور می‌کند:

| مسئله | راه‌حل |
|---|---|
| `ي` عربی در برابر `ی` فارسی | نگاشت به `ی` |
| `ك` عربی در برابر `ک` فارسی | نگاشت به `ک` |
| `ة`, `ۀ` | نگاشت به `ه` |
| `أ`, `إ`, `آ`, `ٱ` | نگاشت به `ا` |
| ارقام فارسی `۰-۹` و عربی `٠-٩` | تبدیل به لاتین |
| اعراب و کشیده (`ـ`) | حذف |
| نیم‌فاصله (ZWNJ) و نویسه‌های جهت‌دهی | تبدیل به فاصله |
| نقطه‌گذاری | تبدیل به فاصله |
| فاصله‌های چندگانه | یکسان‌سازی |

نتیجه: «كتابخانهٔ مركزي» و «کتابخانه مرکزی» یک رشته‌ی نرمال یکسان تولید می‌کنند.

## ۲. توکن‌سازی و ایست‌واژه

`tokenize()` متن نرمال‌شده را می‌شکند، توکن‌های تک‌حرفی را حذف می‌کند و ایست‌واژه‌های
پرتکرار فارسی (`و`, `در`, `به`, `از`, `که`, `با`, `را`, …) را کنار می‌گذارد.

## ۳. تحمل غلط تایپی (Typo Tolerance)

بر پایه‌ی فاصله‌ی لِوِنشتاین، با آستانه‌ی وابسته به طول توکن:

| طول توکن | حداکثر فاصله مجاز |
|---|---|
| ≤ ۳ حرف | ۰ |
| ۴ تا ۶ حرف | ۱ |
| بیش از ۶ حرف | ۲ |

### درسی که در همین پروژه گرفته شد

نسخه‌ی اول از `String.includes` روی کل متن نرمال‌شده استفاده می‌کرد. نتیجه:
جست‌وجوی «اب» با «خیابان» تطبیق می‌خورد و هر پرس‌وجو ۴۰ سند برمی‌گرداند.
اصلاح: تطبیق در **سطح کلمه** (`wordHit`) انجام می‌شود و تطبیق زیررشته‌ای فقط برای
توکن‌های ۴ حرف به بالا مجاز است. الگوی `includes` روی کل متن دیگر استفاده نمی‌شود.

## ۴. امتیازدهی

```
scoreDocument(doc, tokens) → { total, textScore }

textScore  = تطبیق در عنوان × وزن بالا
           + تطبیق در خلاصه × وزن متوسط
           + تطبیق در متن × وزن پایه
           + تطبیق دقیق کل عبارت × پاداش

total      = textScore
           + امتیاز تازگی (بر پایه published_at)
           + امتیاز محبوبیت (بازدید / گفت‌وگو / ذخیره)
```

**قاعده‌ی حیاتی**: فیلتر آستانه پیش از افزودن تازگی و محبوبیت اعمال می‌شود.
سندی که `textScore <= 0` دارد اصلاً وارد نتایج نمی‌شود. در غیر این صورت، امتیاز
تازگی باعث می‌شد همه‌ی اسناد در هر پرس‌وجو برگردند.

## ۵. حالت ساده

```
GET /api/search?q=قطعی آب منطقه دو
GET /api/search/autocomplete?q=قطع
GET /api/search/trending
```

- تکمیل خودکار زنده در نوار جست‌وجوی سربرگ و در Command Palette (`Ctrl/⌘+K`)
- پیشنهاد «شاید منظورتان این بود» وقتی نتیجه‌ای نیست
- عبارت‌های پرجست‌وجو (trending) از داده‌ی واقعی `search_queries`

## ۶. حالت پیشرفته

| پارامتر | معنا |
|---|---|
| `exactPhrase` | نتیجه باید دقیقاً شامل این عبارت باشد |
| `allWords` | همه‌ی کلمات باید حاضر باشند (AND) |
| `anyWords` | دست‌کم یکی (OR) |
| `noneWords` | هیچ‌کدام نباید حاضر باشد (NOT) |
| `types` | فیلتر نوع: `article, case, organization, business, shop, dataset, guild` |
| `from`, `to` | بازه‌ی تاریخ |
| `organizationId`, `categoryKey`, `topicKey`, `contentType` | فیلترهای ساختاری |
| `hasImage`, `hasVideo`, `hasDocument` | وجود پیوست |
| `sort` | `relevance / newest / oldest / most_viewed / most_discussed / most_saved` |

وضعیت کامل جست‌وجو در URL نگه داشته می‌شود، پس نتیجه قابل اشتراک و بوکمارک است.
شمارنده‌ی هر نوع (`typeCounts`) کنار فیلترها نمایش داده می‌شود تا کاربر بداند
پیش از کلیک، چند نتیجه در انتظارش است.

## ۷. مترادف

جدول `search_synonyms` نگاشت‌های دامنه‌ای را نگه می‌دارد؛ برای نمونه:
`آبفا ↔ آب و فاضلاب`، `قطعی برق ↔ خاموشی`، `پسماند ↔ زباله`.
همین مجموعه در پیکربندی OpenSearch (`datacenter/search/persian-analyzer.json`)
نیز تعریف شده تا در صورت مهاجرت، رفتار یکسان بماند.

## ۸. جست‌وجوی ذخیره‌شده و اعلان

`POST /api/search/saved` پرس‌وجو را با همه‌ی فیلترها ذخیره می‌کند. کارگر
`saved-search-notify` محتوای تازه را با این پرس‌وجوها تطبیق می‌دهد و اعلان می‌سازد.

## ۹. حلقه‌ی بازخورد محتوایی

هر پرس‌وجو در `search_queries` با `result_count` ثبت می‌شود. کنسول مدیریت
(`/admin` → نمای کلی) پرتکرارترین **جست‌وجوهای بی‌نتیجه** را نشان می‌دهد. این فهرست
مستقیماً شکاف محتوایی تحریریه را آشکار می‌کند.
