اگر امروز برای مدیریت میلیونها بردار در دیتابیس خود حدس میزنید، احتمالاً در آستانهٔ یک سقوط حافظه یا فاجعهای در نرخ بازیابی هستید. انتخاب اشتباه ایندکس در pgvector میتواند منجر به مصرف ناگهانی تمام RAM سرور یا کاهش شدید دقت نتایج شود. بسیاری از توسعهدهندگان با ایندکسگذاری برداری مانند یک بازی حدس زدنی برخورد میکنند، در حالی که یک انتخاب غلط میتواند باعث بروز یک فاجعهٔ خاموش در بازیابی دادهها یا کرش کردن سرور به دلیل اتمام حافظه شود.
به گزارش یک تحلیل فنی عمیق در وبسایت dev.to در ۱۲ اوت ۲۰۲۶، فرمولهای دقیقی منتشر شده است که حدس و گمان را از مدیریت ذخیرهسازهای برداری حذف میکند و به شما اجازه میدهد عملکرد ذخیرهساز خود را محاسبه کنید. اکثر توسعهدهندگان از قواعد کلی و مبهم استفاده میکنند و تصور میکنند افزایش پارامترها همیشه هزینهای سرسامآور در RAM دارد یا زمان ساخت ایندکس غیرقابلپیشبینی است. در واقعیت، هزینهٔ یک ایندکس عددی است که بر اساس ابعاد بردار معنایی (Embedding) — مثل یک کارت معرفی عددی برای هر واژه که میگوید این کلمه «همسایهی» چه کلمات دیگری است — و نوع ایندکس انتخابی، کاملاً قابل استخراج و محاسبه است.
تصور کنید در حال استقرار یک سامانهٔ تولید بازیابیافزا (RAG) — شبیه دانشآموزی که قبل از جواب دادن، اول کتاب درسی را باز میکند و از آن نقل میآورد — با میلیونها سند هستید. اگر ایندکس شما در RAM جا نشود، سرعت پرسوجو به دلیل نیاز HNSW به دسترسی تصادفی به حافظه (Random Memory Access)، بهشدت سقوط میکند. تفاوت سرعت بین RAM و NVMe در هر کوئری بهوضوح حس میشود و میتواند کل تجربه کاربر را تخریب کند. در این راستا، استفاده از متدهای پیشرفتهتر مانند بهینهسازی بیزی میتواند تأخیر RAG را بهطور چشمگیری کاهش داده و نرخ بازیابی را به ۹۵٪ برساند.
همانطور که در تحلیلهای قبلی ما دربارهی بهینهسازی پایگاهدادههای برداری اشاره کردیم، تعادل بین حافظه و دقت، کلید مقیاسپذیری است.
HNSW در برابر IVFFlat: موازنهٔ عملیاتی
pgvector دو نوع ایندکس اصلی ارائه میدهد که هر کدام هزینههای عملیاتی متمایزی دارند:
- HNSW (Hierarchical Navigable Small World): در نسخههای ۰.۵.۰ به بعد در دسترس است و یک گراف مجاورتی لایهای میسازد. در این ساختار، پرسوجوها از یک لایهٔ بالایی پراکنده شروع شده و به سمت یک لایهٔ پایینی متراکم حرکت میکنند. این ایندکس در هر نقطهٔ عملیاتی، نرخ بازیابی (Recall) بهتری نسبت به IVFFlat در هر واحد زمانِ کوئری ارائه میدهد و درج دادههای جدید را بهخوبی مدیریت میکند. اما فرآیند ساخت آن کند است و ایندکس ایجاد شده در واقع یک کپی کامل از بردارهای شماست.
- IVFFlat (Inverted File Flat): بردارها را با استفاده از k-means به لیستها یا خوشههای مختلف تقسیم میکند و در هنگام جستوجو، تنها نزدیکترین خوشهها (probes) را بررسی میکند. سرعت ساخت آن بسیار بیشتر و مصرف حافظهاش بهمراتب کمتر است. نقطه ضعف این است که باید روی دادههای موجود ساخته شود، زیرا یک جدول خالی چیزی برای خوشهبندی به آن نمیدهد. علاوه بر این، با فاصله گرفتن دادهها از مراکز خوشههای یادگرفته شده (Data Drift)، نرخ بازیابی کاهش مییابد و نیاز به بازسازیهای دورهای دارد.
پیشفرض منطقی، استفاده از HNSW است. تنها زمانی به IVFFlat بروید که نیاز به بازسازی سریع و مکرر ایندکس دارید، حافظه محدودیت اصلی (Binding Constraint) شماست یا دهها میلیون ردیف را روی ماشینی اجرا میکنید که گنجایش گراف HNSW را ندارد. برای درک بهتر جایگاه pgvector در اکوسیستم، میتوان آن را در برابر گزینههایی چون Qdrant و Milvus قرار داد تا مرز میان سادگی و مقیاس میلیاردی مشخص شود.
ریاضیات مصرف حافظه
محاسبهٔ اثر حافظه در HNSW مجموع سه بخش است: محمولهٔ بردار، اشارهگرهای همسایه و سربار توپل.
محمولهٔ بردار (Vector Payload)
برای برداری با بعد d، محموله برابر است با 8 + 4d بایت. این مقدار شامل چهار بایت برای هر مؤلفه float32 و یک هدر ۸ بایتی است.
اشارهگرهای همسایه (Neighbor Pointers)
این بخش به پارامتر m بستگی دارد. در HNSW استاندارد، هر المان در لایه ۰ تا ۲m اتصال و در هر لایهٔ بالاتر m اتصال دارد. لایهها به صورت هندسی رسم میشوند، به طوری که احتمال رسیدن به لایه l برابر با m^-l است. این امر باعث میشود تعداد مورد انتظار لایههای بالای صفر، مجموع آن سری یعنی 1/(m-1) باشد.
تعداد مورد انتظار جایگاههای اتصال برای هر المان به این صورت محاسبه میشود: slots(m) = 2m + m/(m-1).
- m = ۱۶ $ \rightarrow $ ۳۳.۱ جایگاه
- m = ۳۲ $ \rightarrow $ ۶۵.۰ جایگاه
- m = ۶۴ $ \rightarrow $ ۱۲۹.۰ جایگاه
هر جایگاه شامل یک اشارهگر توپل ۶ بایتی است.
محاسبهٔ نهایی
با افزودن حدود ۳۲ بایت سربار برای هر المان (شامل index-tuple و line-pointer)، فرمول نهایی بایت بر ردیف چنین است:bytes_per_row(d, m) ≈ (4d + 8) vector payload + 6 × slots(m) neighbour pointers + 32 tuple overhead.
در سناریوی رایج با بردارهای ۱۵۳۶ بعدی (مانند مدلهای OpenAI) و m=۱۶، محاسبه به این صورت است: (۴ × ۱۵۳۶ + ۸) + ۶ × ۳۳.۱ + ۳۲ = ۶۱۵۲ + ۱۹۹ + ۳۲ = ۶۳۸۳ بایت بر ردیف. برای یک میلیون ردیف، این یعنی ۶.۳۸ گیگابایت حافظه.
جالب است که هزینهٔ افزایش m بسته به ابعاد تغییر میکند. اگر m را برای بردارهای ۱۵۳۶ بعدی به ۶۴ برسانیم، حجم ایندکس تنها ۹٪ رشد کرده و به ۶.۹۶ گیگابایت میرسد (6152 + 774 + 32 = 6958 B). در مدلهای با ابعاد بالا، کپی بردار غالب است و m اهرمی ارزان برای افزایش بازیابی است. اما در مدلهای کوچکتر (مثلاً ۳۸۴ بعدی)، همین تغییر m باعث رشد ۳۲٪ حجم ایندکس میشود:
- d = 384, m = 16: 1544 + 199 + 32 = 1775 B (1.78 GB)
- d = 384, m = 64: 1544 + 774 + 32 = 2350 B (2.35 GB)
در مدلهای با ابعاد کم، ساختار گراف درصد بسیار بیشتری از حجم کل را تشکیل میدهد. شما میتوانید این موضوع را با اجرای کوئری زیر در محیط واقعی بررسی کنید:SELECT pg_size_pretty(pg_relation_size('chunks_embedding_hnsw')) AS index_size, pg_size_pretty(pg_relation_size('chunks')) AS heap_size, (SELECT count(*) FROM chunks) AS rows;
پیشبینی زمان ساخت
زمان ساخت یک متغیر تصادفی نیست، بلکه از یک قانون مقیاسپذیری پیروی میکند. ساخت گراف شامل درج N المان است. هر درج شامل یک نزول حریصانه در لایههای بالا و سپس جستوجویی در لایه ۰ است که کاندیداهای ef_construction را زنده نگه میدارد و تا m همسایه از هر کدام را گسترش میدهد و هر بار یک فاصله d-بعدی را محاسبه میکند.
حجم کار برای هر درج متناسب با ef_construction × m × d است و تعداد پرشها با log N رشد میکند. قانون مقیاسپذیری به این صورت است: build_time ∝ N × log N × ef_construction × m × d.
چهار قاعده عملی برای برنامهریزی مهاجرت دادهها:
۱. دو برابر کردن ef_construction تقریباً زمان ساخت را دو برابر میکند اما حجم ایندکس را تغییر نمیدهد.
۲. دو برابر کردن m تقریباً زمان ساخت را دو برابر و حجم ایندکس را اندکی افزایش میدهد.
۳. دو برابر کردن ابعاد (dimension) تقریباً زمان ساخت را دو برابر میکند.
۴. دو برابر کردن تعداد ردیفها، زمان را کمی بیشتر از دو برابر افزایش میدهد.
برای تخمین ساختهای بزرگ، میتوانید زمان یک نمونه کوچک را اندازه بگیرید و با این فرمول تعمیم دهید: T(N) ≈ T(n) × (N/n) × (ln N / ln n).
مثلاً اگر ۱۰۰,۰۰۰ ردیف ۹۵ ثانیه زمان ببرد، ۱۰ میلیون ردیف حدود: 95 × (10,000,000 / 100,000) × (ln 10^7 / ln 10^5) = 95 × 100 × (16.12 / 11.51) = ۱۳,۳۰۰ ثانیه یا حدود ۳ ساعت و ۴۲ دقیقه زمان خواهد برد.
این تخمین تنها زمانی معتبر است که ساخت در maintenance_work_mem جا شود. اگر دادهها به دیسک منتقل شوند (spill)، زمان واقعی چندین برابر میشود. پیش از شروع، مقدار maintenance_work_mem را بالاتر از عدد محاسبهشده قرار دهید (با کمی فضای اضافی) و max_parallel_maintenance_workers را متناسب با هستههای CPU در دسترس تنظیم کنید. پیشرفت کار را با این کوئری مانیتور کنید:SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS pct FROM pg_stat_progress_create_index;
ابزار سنجش بازیابی (Recall Harness)
نرخ بازیابی را نمیتوان با فرمول به دست آورد چون به ابعاد ذاتی و خوشهبندی خاص مجموعه دادههای شما بستگی دارد. تنها راه، اندازهگیری با مقایسه با داده مرجع (Ground Truth) است.
با ایجاد یک جدول موقت شامل ۲۰۰ بردار آزمایشی (probe vectors) و مقایسهٔ اسکن دقیق (غیرفعال کردن ایندکس با SET LOCAL enable_indexscan = off) در برابر اسکن تقریبی (فعال کردن ایندکس با SET LOCAL enable_indexscan = on و SET LOCAL enable_seqscan = off)، میتوانید منحنی «بازیابی در برابر تأخیر» را رسم کنید.
این فرآیند حدود ۱۰ دقیقه زمان میبرد و عملکرد واقعی تنظیمات ef_search شما را فاش میکند. توجه داشته باشید که اگر بردارهای آزمایشی را از خود جدول ایندکسشده انتخاب کنید، تست شما خوشبینانه خواهد بود زیرا هر بردار در حال حاضر یک گره با اتصالات خوب است. کوئریهای واقعی در شکافهای بین اسناد قرار میگیرند و امتیاز کمتری میگیرند؛ برای تصمیمات حیاتی، کوئریهای واقعی را از لاگهای خود استخراج و جاسازی کنید.
تنظیم پیچهای بهینهسازی
برای جلوگیری از بازسازیهای غیرضروری، پارامترها را به ترتیب زیر تغییر دهید. هر بار یک پارامتر را تغییر دهید، با ابزار سنجش اندازه بگیرید و نتیجه را ثبت کنید:
- ef_search: ابتدا این را تنظیم کنید. این یک GUC در سطح نشست است (برای هر کوئری قابل تغییر) و نیاز به بازسازی ندارد. این پارامتر تعیین میکند که جستوجوی لایه ۰ چند کاندید را زنده نگه دارد. هزینه کوئری تقریباً به صورت خطی با آن تغییر میکند. اکثر مشکلات بازیابی با تغییر مقدار پیشفرض ۴۰ به ۲۰۰ حل میشوند. هشدار: این مقدار باید حداقل برابر با LIMIT کوئری شما باشد؛ تنظیم آن روی ۱۰ برای یک کوئری top-50 یک فاجعهٔ خاموش در بازیابی است. از
SET LOCALاستفاده کنید تا مقادیر در اتصالات pooled باقی نمانند. - ef_construction: اگر
ef_searchبه سقف رسید، این مقدار را افزایش دهید. این کار کیفیت گراف را تغییر میدهد (نه تلاش صرف شده برای جستوجو) و سقفی را کهef_searchبه آن فشار میآورد، بالا میبرد. هزینه آن به صورت خطی روی زمان ساخت است و هیچ تأثیری روی حجم ایندکس ندارد. تغییر از ۶۴ به ۲۰۰ یک حرکت رایج است. - m: اگر گراف بیش از حد پراکنده است، m را افزایش دهید. این کار اتصالات را زیاد میکند و زمانی بیشترین کمک را میکند که بردارها ابعاد بالایی دارند و بد خوشهبندی شدهاند. از محاسبات حافظه برای تصمیمگیری درباره قابلیت پرداخت هزینه استفاده کنید (مثلاً ۹٪ افزایش برای ۱۵۳۶-بعد در مقابل ۳۲٪ برای ۳۸۴-بعد).
- Dimension: قدرتمندترین اهرم است. نصف کردن ابعاد، حافظه، زمان ساخت و هزینهٔ کوئری را نصف میکند. اگر مدل شما از Truncation (برش ابعاد) پشتیبانی میکند، این اولین موضوعی است که باید بررسی شود.
ابعاد IVFFlat
برای کسانی که از IVFFlat استفاده میکنند، پیشنهاد میشود lists را برای تا یک میلیون ردیف برابر با rows/1000 و برای مقادیر بیشتر برابر با sqrt(rows) قرار دهید. مقدار probes را از sqrt(lists) شروع کنید.
برای ۵ میلیون ردیف، این به معنای زیر است:
lists = sqrt(5e6) ≈ 2236probes = sqrt(2236) ≈ 47
این پیکربندی تقریباً probes × N / lists بردار را در هر کوئری اسکن میکند. برای اعداد بالا، این یعنی ۴۷ × ۵,۰۰۰,۰۰۰ / ۲۲۳۶ ≈ ۱۰۵,۰۰۰ بردار در هر کوئری، یا حدود ۲٪ از جدول. اگر probes را تا حد lists بالا ببرید، در واقع اسکن ترتیبی (Sequential Scan) را با مراحل اضافی دوباره اختراع کردهاید.
شکستهای بازیابی در IVFFlat معمولاً به این دلیل رخ میدهد که نزدیکترین همسایه واقعی در خوشهای قرار دارد که شما آن را probe نکردهاید. بعد از هر بار بارگذاری حجیم دادهها، ایندکس را بازسازی کنید. اگر نمیتوانید در محدوده بودجهٔ تأخیر خود به بازیابی قابل قبول برسید، این سیگنالی است برای مهاجرت به HNSW.
این رویکرد سیستماتیک، حساب و کتاب را جایگزین شهود میکند. مهندسان میتوانند با تغییر یک پارامتر در هر بار و اندازهگیری با ابزار سنجش، پیکربندی خود را با دادهها توجیه کنند، نه با حدس و گمان.
برای پیادهسازی این روش، با اجرای اسکریپت SQL سنجش بازیابی روی جدول تولیدی فعلی خود شروع کنید تا ببینید آیا ef_search گلوگاه پنهان شماست یا خیر.
گام بعدی شما
- اسکریپت SQL سنجش بازیابی را روی جدول تولیدی خود اجرا کنید تا متوجه شوید آیا
ef_searchگلوگاه پنهان شماست یا خیر. - با استفاده از فرمول
bytes_per_rowمقدار دقیق RAM مورد نیاز برای ایندکس خود را محاسبه کرده وmaintenance_work_memرا بر اساس آن تنظیم کنید. - اگر از مدلهای با ابعاد بالا استفاده میکنید، پارامتر m را افزایش دهید؛ هزینه حافظه در این حالت بسیار ناچیز است.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو