چالشهای مقیاسپذیری پایگاهدادههای برداری (Vector DB) در سامانههای بانکی با ۱۰ میلیون توکن اسناد محرمانه
بررسی فنی معماری هیبریدی Qdrant و Milvus با تکنیک HNSW و بهینهسازی تاخیر P99 در بازخوانی برداری اسناد مالیاتی و اعتبارسنجی بدون خروج داده از زیرساخت داخلی سازمان.
علی رضوانیمعمار ارشد هوش مصنوعی ماه
در پروژهای برای یکی از مشتریان بانکی، قرار شد موتور جستوجوی معنایی روی ۱۰ میلیون توکن از اسناد مالیاتی، قراردادها و مستندات اعتبارسنجی بالاترین دقت را داشته باشد — بدون آنکه حتی یک بایت داده از زیرساخت داخلی سازمان خارج شود. این مقاله دقیقاً همان مسیر را بازگو میکند: از شکست اولیه pgvector تا معماری هیبریدی Qdrant و Milvus و بهینهسازی تأخیر P99.

چرا پایگاهداده رابطهای برای این مقیاس کافی نبود
اولین تلاش ما همان چیزی بود که همه جا توصیه میشود: pgvector. برای ۵۰۰ هزار سند اول مشکلی نبود؛ اما با رسیدن به ۱۰ میلیون بردار با ابعاد ۱۵۳۶، سه دردسر جدی ظاهر شد:
- زمان ساخت ایندکس: HNSW روی PostgreSQL با چنین حجمی چندین ساعت build زمان میگرفت و هر rebuild یعنی پنجره آفلاین جستوجو.
- تقسیم حافظه و ردیف: بار جستوجوی برداری، پلنر PostgreSQL را مجبور به hard parse میکرد و به queryهای OLTP همزمان آسیب میزد.
- کنترل دقیق recall: نمیتوانستیم بهازای هر دسته اسناد، پارامترهای ایندکس (efSearch، nlist) را مجزا تنظیم کنیم.
در سامانههای مالی، تأخیرِ میانگین یک معیار فریبنده است؛ آنچه سامانه را میشکند P99 است؛ همان لحظهای که کاربر برای بار صدم صبر میکند.
معماری هیبریدی Qdrant و Milvus
تصمیم گرفتیم بار را بین دو موتور تقسیم کنیم: Qdrant برای بازیابی تعاملی اسناد با کمترین تأخیر، و Milvus برای نوشتن انبوه و ایندکسگذاری دستهای شبانه.
# بارکاری شبکهای دو-مرحلهای: نوشتن دستهای در Milvus
from pymilvus import connections, Collection
connections.connect(host="milvus-0.internal", port="19530")
collection = Collection("banking_docs")
collection.load()
# جستوجوی تعاملی با Qdrant (گره جدا، حافظه اختصاصی)
from qdrant_client import QdrantClient, models
client = QdrantClient(host="qdrant-0.internal", port=6333)
hits = client.query_points(
collection_name="banking_docs",
query=embedding, # بردار پرسوجوی کاربر
limit=20,
search_params=models.SearchParams(hnsw_ef=128),
with_payload=["doc_id", "clause_no"],
).points
نقش لایه کش embedding
پرتکرارترین پرسوجوها — سؤالات حقوقی استانداردی مثل «شرط فسخ قرارداد» — با الگوریتم LRU در Redis کش میشدند:
import { createHash } from "node:crypto";
import { redis } from "@/server/redis";
const KEY_PREFIX = "rag:emb:";
const TTL_SECONDS = 60 * 60 * 24; // ۲۴ ساعت
export async function embedCached(text: string): Promise<number[]> {
const key = KEY_PREFIX + createHash("sha256").update(text).digest("hex");
const hit = await redis.get(key);
if (hit) return JSON.parse(hit) as number[];
const vector = await embed(text);
await redis.set(key, JSON.stringify(vector), { EX: TTL_SECONDS });
return vector;
}
این لایه بهتنهایی حدود ۳۸ درصد از فراخوانیهای مدل زبانی را حذف کرد.
بهینهسازی تأخیر P99
چهار تغییر متوالی اعمال کردیم؛ هر کدام را جداگانه اندازه گرفتیم تا اثرش قابل استناد باشد:
- فشردهسازی بردارها با PQ (Product Quantization) از ۶ کیلوبایت به ۱.۵ کیلوبایت بهازای هر بردار — مصرف حافظه نصف شد.
- تفکیک namespace بهازای هر نوع سند تا ایندکسها کوچک و جستوجوها محدود بمانند.
- پیشگرم کردن (warm-up) ایندکسها بعد از restart با یک job سبک در deploy pipeline.
- فیلتر اولیه سمت Qdrant (تاریخ سند + نوع) قبل از جستوجوی برداری تا فضای جستوجو ۷۰ درصد کوچکتر شود.
| سناریو | تأخیر P50 | تأخیر P99 | Recall@10 |
|---|---|---|---|
| pgvector (قبل) | ۲۱۰ms | ۱.۹s | ۰.۹۱ |
| Qdrant خام | ۳۸ms | ۲۲۰ms | ۰.۸۹ |
| Qdrant + PQ + فیلتر | ۴۱ms | ۹۶ms | ۰.۸۸ |
| Qdrant + PQ + کش | ۱۲ms | ۷۴ms | ۰.۸۸ |
نتیجهگیری
با این معماری، P99 بازیابی از ۱.۹ ثانیه به ۷۴ میلیثانیه رسید در حالی که Recall بالای ۸۸ درصد حفظ شد — و همه چیز داخل شبکه داخلی سازمان ماند. درس اصلی پروژه این بود: پایگاهداده برداری را نباید «افزونه» پایگاهداده اصلی دید؛ آن را یک سامانه مستقل با SLA، پایش و ظرفیتسنجی خودش طراحی کنید.
برچسبها
علی رضوانی
معمار ارشد هوش مصنوعی ماهدرباره نویسنده
بیش از ۹ سال تجربه در معماری سامانههای جستوجوی معنایی، پایگاهدادههای برداری و استقرار مدلهای زبانی در زیرساختهای داخلی سازمانها.
مقالات نویسنده
