`isActive`، نه `categoryId`، نه `brandId`، نه `createdAt`) و `ProductVariant`
ایندکسی روی `productId` ندارد. با ۱۶ هزار رکورد، صفحهبندی بدون ایندکس همان
کندی را میدهد؛ پس ایندکس **بخشی از این کار است**، نه یک بهبود جداگانه.
## ۲. چرا کار از «اضافه کردن take/skip» بزرگتر است
هفت مصرفکننده روی فرض «کل کاتالوگ در حافظه است» نوشته شدهاند. سهتا بحرانیاند
— هر سه کل لیست را میگیرند تا **یک چیز** پیدا کنند:
| جا | الان چه میکند | با ۱۶ هزار رکورد |
|---|---|---|
| `useProductDetail` | کل کاتالوگ را میگیرد تا **یک محصول** را `find` کند | بازکردن هر محصول = دانلود کل کاتالوگ |
| `cartStore.init` | کل کاتالوگ برای همگامسازی **۲-۳ قلم سبد** | هر بار باز شدن اپ |
| `configStore.init` | کل کاتالوگ فقط برای **لیست دستهها** | صفحه خانه |
| `SearchView` | متن/دسته/برند/بازه وزن را روی کلاینت فیلتر میکند | فیلتر روی دادهی ناقص = نتیجه غلط |
| `Filters.vue` | گزینههای فیلتر را از کل لیست مشتق میکند | گزینهها ناقص میشوند |
| `FavoritesView` | علاقهمندیها را با `find` در کل لیست پیدا میکند | علاقهمندیِ خارج از صفحه دیده نمیشود |
| `useOrderItemProduct` | عنوان محصولِ آیتم سفارش را از کل لیست میخواند | سفارش قدیمی بیعنوان میشود |
**نتیجه:** اگر فقط `take`/`skip` به مسیر اضافه شود، جستجو و فیلتر **بیصدا غلط**
میشوند (روی ۲۰ رکورد اول فیلتر میکنند) و این بدتر از کندی است. پس هر
مصرفکننده باید مسیر مخصوص خودش را بگیرد.
## ۳. طراحی پیشنهادی
### (الف) چهار مسیر، بهجای یک مسیر
| مسیر | برای چه | چرا جدا |
|---|---|---|
| `GET /catalog/products?page=&limit=&search=&categoryId=&brandId=&minGram=&maxGram=&sort=` | لیست و جستجو | **فیلتر و صفحهبندی سمت سرور**؛ تنها راه درست بودن نتیجه |
| `GET /catalog/products/:id` | صفحه محصول | یک رکورد، نه ۱۶ هزار |
| `GET /catalog/facets` | گزینههای فیلتر (دستهها، برندها، بازه وزن) | کوچک، `distinct`/`aggregate`؛ جایگزین مشتقکردن از کل لیست |
| `POST /catalog/products/by-ids` | سبد، علاقهمندی، آیتمهای سفارش | چند شناسه مشخص در یک درخواست |
شکل پاسخ لیست: `{ rows, total, page, limit }`. **عمداً** از `TableService`
استفاده نمیکنم: آن برای قرارداد XTable پنل ادمین است و شکل پاسخش
(`{rows, query:{...}}`) برای اپ بیمورد پیچیده است.
- بدون `page`/`limit` ⇒ **رفتار امروز** (کل لیست). پس هیچ مصرفکنندهای نمیشکند.
- با `page`/`limit` ⇒ پاسخ صفحهبندیشده.
- در لاگ، درخواست بدون `page` روی کاتالوگ بزرگ **هشدار** میدهد تا بفهمیم چه
کسی هنوز قدیمی است.
### (ج) کش سرور — بازطراحی
کش فعلی **یک** خروجی را نگه میدارد (`value: {at, data}`). با پارامترها این
نادرست میشود: صفحهٔ ۲ جواب صفحهٔ ۱ را میگیرد.
پیشنهاد: کلید کش = رشتهی پارامترهای نرمالشده، با سقف تعداد کلید (مثلاً ۵۰،
LRU) تا حافظه بینهایت رشد نکند. باطلسازی موجود (middleware پریزما) **همه** را
با هم پاک میکند — که درست است، چون هر نوشتنی میتواند هر صفحهای را عوض کند.
⚠️ سقف کلید لازم است: بدون آن، هر ترکیب فیلترِ کاربران یک ردیف کش میسازد و
حافظهی سرور بهتدریج پر میشود.
### (د) ایندکسها (مهاجرت)
```
Product: @@index([isActive, createdAt]) ← لیست و مرتبسازی پیشفرض
@@index([categoryId])
@@index([brandId])
ProductVariant: @@index([productId]) ← الان نیست؛ هر include یک اسکن
ProductImage: @@index([productId])
```
جستجوی متنی: `contains` فارسی روی ۱۶ هزار ردیف با ایندکس معمولی سریع نمیشود.
پیشنهاد **فعلی**: همان `contains` با ایندکس بالا (برای ۱۶ هزار قابل قبول است).
اگر کند بود، مرحلهی بعد `pg_trgm` است — ولی **اول اندازهگیری، بعد اضافه کردن**.
### (ه) سمت اپ
| کار | تغییر |
|---|---|
| `catalogStore` | از «یک آرایهی کامل» به «صفحات + `total`»؛ `load()` صفحه میگیرد، `loadMore()` صفحه بعد |
| `SearchView` / `NewestView` | `IntersectionObserver` بهجای زیاد کردن `visibleCount`، **درخواست صفحه بعد** میزند؛ فیلتر/جستجو با debounce به سرور میرود و از صفحه ۱ شروع میکند |
| ۴ | `SearchView` / `NewestView` / rails خانه → صفحهبندی واقعی | ✅ **انجام شد 2026-10-17** — هر دو اپ؛ facets + `listProductsPaged`؛ multi-brand با `brandId=a,b` |
| ۵ | حذف خوانش کل کاتالوگ و هشدار لاگ | ⏳ `catalog.load` هنوز یک صفحهٔ ۱۰۰تایی میگیرد برای callerهای باقیمانده؛ هشدار لاگ بعد از پاکسازی |