پست

پایگاه داده‌های Vector در مصاحبه‌های طراحی سیستم

بررسی Embedding، جستجوی شباهت، الگوریتم‌های Index مثل HNSW و IVF، فیلترینگ، به‌روزرسانی و نحوه استفاده در مصاحبه

پایگاه داده‌های Vector در مصاحبه‌های طراحی سیستم

اگر در چند سال گذشته به اخبار فناوری توجه کرده باشید، «Embedding» را همه‌جا دیده‌اید. موتورهای جستجویی که واقعاً منظور شما را می‌فهمند. سیستم‌های توصیه‌گری که محتوای عجیب‌ترین حد مرتبط را نشان می‌دهند. چت‌باتی که می‌تواند از مجموعه‌های عظیم سند اطلاعات بازیابی کند. همه‌ی این‌ها به یک اصل زیربنایی تکیه دارند: پیدا کردن سریع چیزهایی که شبیه چیزهای دیگر هستند.

این مفهوم در واقع جدید نیست. پایگاه داده‌های Vector و تکنیک‌های مرتبط با آن‌ها مدت‌هاست در سیستم‌های توصیه‌گر وجود دارند. اما با ظهور تکنیک‌های جدید یادگیری ماشین، قدرت پایگاه داده‌های Vector چند برابر شده و مجموعه‌ی تازه و جذابی از کاربردهای زیرساختی را ممکن کرده است.

پایگاه داده‌های سنتی در جستجوی دقیق عالی‌اند. «کاربر با شناسه‌ی 12345 را بده.» «همه‌ی سفارش‌های ثبت‌شده در اول ژانویه را پیدا کن.» اما اگر از پایگاه داده‌ی سنتی بپرسید «اسنادی شبیه این سند را پیدا کن»، به مشکل می‌خورید. اینجاست که پایگاه داده‌های Vector وارد می‌شوند.

این Deep Dive توضیح می‌دهد پایگاه داده‌های Vector چه هستند، زیر کاپوت چطور کار می‌کنند، و مهم‌تر از همه، چطور در مصاحبه‌ی طراحی سیستم مؤثر از آن‌ها استفاده کنیم. درباره‌ی الگوریتم‌های Index که جستجوی شباهت را سریع می‌کنند عمیق می‌شویم، ولی عملی هم هستیم: کی واقعاً به پایگاه داده‌ی Vector اختصاصی نیاز دارید و کی یک افزونه‌ی ساده روی پایگاه داده‌ی فعلی‌تان کار را انجام می‌دهد.

اگر جزئیات این مطلب شما را می‌ترساند، به بخش کاربردها در انتها بروید و از آنجا به عقب برگردید. بیشتر مصاحبه‌های طراحی سیستم به پایگاه داده‌های Vector نمی‌رسند. آن‌هایی هم که می‌رسند، اغلب کمتر به این اهمیت می‌دهند که درون آن‌ها را بدانید و بیشتر به این که بدانید کجا و چطور استفاده‌شان کنید.

Vector اصلاً چیست؟

پیش از صحبت درباره‌ی پایگاه داده‌هایی که Vector ذخیره می‌کنند، باید بفهمیم دقیقاً چه چیزی را ذخیره می‌کنیم.

Vector (یا Embedding) فقط آرایه‌ای از اعداد است که چیزی را نمایش می‌دهد. آن «چیز» می‌تواند یک کلمه، جمله، تصویر، کاربر، محصول یا هر چیزی باشد که بتوانید به مدل یادگیری ماشین بدهید. جادو در این است که چیزهای شبیه هم Vectorهای شبیه هم می‌گیرند.

1
2
3
4
5
6
// Two sentences that mean similar things
"The cat sat on the mat"   → [0.12, -0.34, 0.78, ..., 0.45]  // 1536 numbers
"A feline rested on a rug" → [0.11, -0.32, 0.79, ..., 0.44]  // very similar!

// A sentence with different meaning
"The stock market crashed" → [-0.89, 0.12, -0.45, ..., 0.23] // very different

Embedding معمولی بین ۱۲۸ تا ۱۵۳۶ بُعد دارد (مدل text-embedding-3-large از OpenAI ۳۰۷۲ بُعد دارد). هر بُعد جنبه‌ای از معنا را می‌گیرد، هرچند بُعدهای منفرد معمولاً برای انسان قابل تفسیر نیستند. مهم این است که روابط هندسی بین Vectorها، روابط معنایی بین چیزهایی را که نمایش می‌دهند بازتاب می‌دهند.

در اینجا درباره‌ی «شباهت» کمی کلی‌گویی می‌کنیم. شبیه یعنی همان رنگ؟ یا گزیده‌هایی از یک کتاب؟ یا مفهوم مشابه؟ این در واقع به مدل Embedding بستگی دارد.

بسیاری از اپلیکیشن‌ها از مدل Embedding از پیش آموزش‌دیده استفاده می‌کنند. برای متن ممکن است چیزی مثل API امبدینگ OpenAI، Sentence Transformers یا BERT باشد. برای تصویر، مدل‌هایی مثل CLIP یا ResNet. این مدل‌ها روی وظایف متنوع آموزش دیده‌اند، طوری که مفهوم شباهتی که برایتان مهم است احتمالاً توسط مدل Embedding گرفته می‌شود. آن را مثل یک شباهت «معنایی» بسیار مبهم در نظر بگیرید. برای شما مدل Embedding فقط یک تابع گران روی GPU است: داده می‌رود تو، Vector با طول ثابت می‌آید بیرون.

اما «شباهت» می‌تواند برای اپلیکیشن بسیار دقیق‌تر باشد. کاربرد بسیار رایج در سیستم‌های توصیه‌گر پیدا کردن آیتم‌هایی است که احتمالاً با هم خریده می‌شوند. پوشک و شیشه‌ی شیر فقط به‌طور مبهم شبیه‌اند از این نظر که هر دو به نوزاد مربوط‌اند، ولی عمیقاً شبیه‌اند از این نظر که چیزهایی هستند که والدین تازه اغلب می‌خرند. در این موارد، مدل ML سفارشی می‌تواند Embeddingهایی بسازد که مشخصاً این مفهوم «شباهت» را هدف می‌گیرند.

معیارهای شباهت

وقتی Vector دارید، به راهی برای سنجش شباهت دو Vector نیاز دارید. چند رویکرد رایج هست:

  • فاصله‌ی اقلیدسی (L2): فاصله‌ی خط مستقیم بین دو نقطه در فضا. اگر قضیه‌ی فیثاغورس دبیرستان را به یاد داشته باشید، اساساً همین است که به صدها بُعد گسترش یافته. فاصله‌ی کمتر یعنی شباهت بیشتر. این معیار هم به جهت و هم به اندازه (Magnitude) اهمیت می‌دهد.
  • شباهت کسینوسی (Cosine Similarity): زاویه‌ی بین دو Vector را بدون توجه به اندازه‌شان می‌سنجد. دو Vector در یک جهت، شباهت کسینوسی ۱ دارند، Vectorهای عمود ۰ و Vectorهای مخالف ۱-. وقتی Embeddingها را Normalize کرده باشید (که بیشتر مدل‌های Embedding انجام می‌دهند) عالی است.
  • ضرب داخلی (Dot Product): مشابه شباهت کسینوسی است ولی برای اندازه Normalize نمی‌کند. اساساً شباهت کسینوسی ضرب در طول هر دو Vector است. بعضی سیستم‌ها از آن استفاده می‌کنند چون محاسبه‌اش کمی سریع‌تر است.
  • فاصله‌ی همینگ (Hamming): تعداد موقعیت‌هایی را که دو Vector باینری در آن‌ها فرق دارند می‌شمارد. بسیار سریع است (فقط XOR بیت‌ها و شمارش)، به همین دلیل در بعضی روش‌های Index مثل Locality Sensitive Hashing محبوب است.

در مصاحبه‌ی طراحی سیستم از نوع زیرساخت، انتخاب معیار شباهت معمولاً اصلاً مطرح نمی‌شود. می‌توانید بگویید «از شباهت کسینوسی یا معیار شباهت مناسب دیگری استفاده می‌کنیم» و از بیشتر نامزدها جلو بیفتید. در مصاحبه‌ی طراحی سیستم ML، بسته به مدل Embedding و کاربردتان، انتخاب ممکن است مهم‌تر باشد. اگر مدل Embedding شما روی وظیفه‌ی خاصی آموزش دیده، همان را به کار می‌برید (مثلاً کمینه کردن ضرب داخلی بین دو آیتمی که با هم خریده می‌شوند، معیار فاصله‌ی شما می‌شود).

مسئله‌ی نزدیک‌ترین همسایه

وقتی مجموعه‌ای Vector و معیار شباهت داریم، می‌توانیم سؤال بپرسیم. این سؤال‌ها معمولاً بر حسب «Query Vector» بیان می‌شوند که می‌توانید آن را مثل عبارت جستجویتان در نظر بگیرید. در بیشتر اپلیکیشن‌ها، Query Vector همان Embedding آیتمی است که می‌خواهید چیزهای شبیه آن را پیدا کنید.

مسئله‌ی K-Nearest Neighbors (KNN) این است: با داشتن Query Vector، K شبیه‌ترین Vector را از مجموعه‌تان بده. یا با داشتن یک آیتم، K شبیه‌ترین آیتم را در مجموعه پیدا کن.

رویکرد ساده‌ی KNN ساده است. Query را با هر Vector در پایگاه داده مقایسه کنید، امتیازهای شباهت را حساب کنید، مرتب کنید و K تای برتر را برگردانید. این KNN دقیق است و عملاً ( O(n) ) است که n تعداد Vectorهاست (K معمولاً بسیار کوچک است، پس نادیده‌اش می‌گیریم).

1
2
3
4
5
6
7
8
9
10
11
12
# Python Pseudocode
def exact_knn(query_vector, all_vectors, k):
    heap = []  # min-heap by similarity

    for vector in all_vectors:
        similarity = compute_similarity(query_vector, vector)
        if len(heap) < k:
            heapq.heappush(heap, (similarity, vector.id))
        elif similarity > heap[0][0]:
            heapq.heapreplace(heap, (similarity, vector.id))

    return sorted(heap, reverse=True)

برای یک میلیون Vector با ۱۵۳۶ بُعد، این به چند میلیارد عملیات اعشاری برای هر Query می‌رسد (متن اصلی حدود ۶ میلیارد گفته؛ با شمردن هر ضرب-جمع به‌عنوان دو عملیات، حدود ۳ میلیارد می‌شود). این می‌تواند گران باشد و برای بسیاری از اپلیکیشن‌ها بیش از حد کند است.

خوانندگان دقیق ممکن است متوجه شوند کد من را می‌توان بهینه کرد. روی CPU می‌توان از دستورهای SIMD (یک دستور، چند داده) برای محاسبه‌ی امتیاز شباهت چند Vector هم‌زمان (۲ تا ۸ بسته به CPU) استفاده کرد. روی GPU عملیات برداری می‌توانند امتیاز هزاران Vector را هم‌زمان محاسبه کنند. پس اگر KNN دقیق لازم باشد، راه‌هایی برای سریع‌تر کردنش هست! اما بیشتر اپلیکیشن‌ها می‌توانند مقداری نادقیقی را برای سرعت بسیار بیشتر تحمل کنند.

اگر لازم نباشد دقیقاً نزدیک‌ترین همسایه‌ها را پیدا کنیم چه؟ اگر راضی باشیم بیشتر وقت‌ها Vectorهایی را پیدا کنیم که «احتمالاً» نزدیک‌ترین همسایه‌اند چه؟ این جستجوی Approximate Nearest Neighbor یا ANN است و پایه‌ی هر پایگاه داده‌ی Vector کاربردی است. با ANN دقت را با سرعت معاوضه می‌کنیم. همیشه دقیق‌ترین همسایه‌ها را پیدا نمی‌کنیم ولی تعداد زیادی همسایه‌ی نزدیک را سریع پیدا می‌کنیم.

معیار کلیدی کیفیت ANN Recall است: از K همسایه‌ی نزدیک واقعی، چه کسری را واقعاً پیدا کردیم؟ Recall برابر ۰٫۹۵ یعنی ۹۵٪ همسایه‌های نزدیک واقعی را پیدا کرده‌ایم. برای بیشتر اپلیکیشن‌ها همین کاملاً کافی است.

این زیربنای همه‌ی پایگاه داده‌های Vector است. پایگاه داده‌های Vector اجازه می‌دهند بین این سه چیز Trade-off نرم داشته باشید:

  • Recall: نتایج ما چقدر دقیق‌اند؟
  • Latency: چقدر سریع می‌توانیم نتایج را برگردانیم؟
  • حافظه: Index چقدر فضا می‌گیرد (به‌خصوص RAM)؟

پایگاه داده‌های Vector چطور کار می‌کنند؟

پس Vector داریم، معیار شباهت داریم و می‌دانیم جستجوی Brute-force مقیاس نمی‌گیرد. پایگاه داده‌های Vector واقعاً چطور این را سریع می‌کنند؟ به ساختارهای داده‌ی هوشمندی برمی‌گردد که اجازه می‌دهند بیشتر مقایسه‌ها را رد کنیم. الگوریتم‌های Index پشت جستجوی نزدیک‌ترین همسایه‌ی تقریبی را بررسی می‌کنیم و بعد دغدغه‌های عملی را می‌بینیم: فیلترینگ، به‌روزرسانی و مقیاس‌دهی به میلیاردها Vector.

استراتژی‌های Index

کلید کار پایگاه داده‌ی Vector محاسبه‌ی Index روی Vectorهای ذخیره‌شده است. این Indexها بازیابی را سریع‌تر می‌کنند و اغلب مستلزم Trade-off هایی هستند که بالا گفتیم؛ معمولاً Recall را برای کاهش بزرگ Latency فدا می‌کنند. همچنین برای درج، به‌روزرسانی و حذف Vectorها پیچیدگی می‌آورند که بعداً می‌گوییم. اول استراتژی‌های اصلی Index را ببینیم.

استراتژی‌های Index زیادی برای جستجوی نزدیک‌ترین همسایه‌ی تقریبی هست. استدلال درباره‌ی Trade-offهای بین آن‌ها بدون اجرای آزمایش روی داده‌ی خودتان سخت است و کم پیش می‌آید که بتوانید با نگاه به یک اپلیکیشن اثبات کنید استراتژی‌ای از دیگری برتر است. در واقع یکی از مزیت‌های اصلی استفاده از پایگاه داده‌ی Vector همین است: اجازه می‌دهد بدون بازنویسی اپلیکیشن بین استراتژی‌های Index جابه‌جا شوید و پارامترها را تنظیم کنید.

معمولاً یک مجموعه‌ی ارزیابی (Query ها با نتایج خوب شناخته‌شده) دارید و Trade-off بین Recall و Latency را می‌سنجید تا ببینید برای کاربردتان چه چیزی جواب می‌دهد.

آنچه در ادامه می‌آید توضیح چند الگوریتم است. به یاد داشته باشید بیشتر مصاحبه‌ها به‌ندرت به این سطح جزئیات می‌رسند. مهم این نیست که بتوانید این الگوریتم‌ها را پیاده کنید، بلکه شهود نحوه‌ی کارشان را داشته باشید.

HNSW (Hierarchical Navigable Small World)

HNSW محبوب‌ترین الگوریتم در پایگاه داده‌های Vector Production است. اگر فقط یک استراتژی Index را به یاد بسپارید، همین باشد.

شهودش شبیه Skip List است. در Linked List معمولی، پیدا کردن عنصر نیاز به اسکن هر گره دارد، یعنی ( O(n) ). Skip List با افزودن لایه‌های «باند سریع» (Express Lane) بالای لیست پایه این را حل می‌کند. لایه‌ی پایین همه‌ی عناصر را دارد، ولی لایه‌های بالاتر بیشتر عناصر را رد می‌کنند و فقط زیرمجموعه‌ی تصادفی را نگه می‌دارند. برای جستجو، از باند سریع بالا شروع می‌کنید و جلو می‌روید تا از هدف رد نشوید، بعد یک لایه پایین می‌آیید و ادامه می‌دهید. این جستجوی ( O(\log n) ) در Linked List می‌دهد.

HNSW همین ایده را روی جستجوی نزدیک‌ترین همسایه مبتنی بر گراف اعمال می‌کند. یک گراف چندلایه می‌سازد که هر گره یک Vector است. اما این در عمل یعنی چه؟

اینطور فکر کنید: وقتی Vector را در Index مربوط به HNSW درج می‌کنید، به گره تبدیل می‌شود. بعد Index شبیه‌ترین Vectorها را به Vector جدید پیدا می‌کند و بین آن‌ها یال می‌سازد. پس اگر Embedding «Taylor Swift» را درج کنید، به Embeddingهای نزدیکی مثل «Beyoncé» و «Ed Sheeran» وصل می‌شود، نه به این خاطر که کسی دستی آن‌ها را وصل کرده، بلکه چون Vectorهایشان در فضای Embedding از نظر هندسی نزدیک‌اند.

نتیجه گرافی است که می‌توانید با دنبال کردن یال‌ها از هر Vector به Vectorهای شبیه «قدم بزنید». و مهم‌تر، اگر دو Vector شبیه هم باشند، احتمالاً مسیر کوتاهی از میان گراف بین آن‌ها هست.

همان ایده‌ی Skip List اینجا هم دیده می‌شود. لایه‌ی پایین (لایه‌ی ۰) همه‌ی Vectorها را دارد که هرکدام به نزدیک‌ترین همسایه‌هایشان وصل‌اند. اما جستجو در این گراف متراکم برای مجموعه‌داده‌های بزرگ هنوز کند است. پس HNSW لایه‌های «باند سریع» بالای آن اضافه می‌کند. هر Vector احتمالی دارد که به لایه‌های بالاتر «ارتقا» یابد. نتیجه یک سلسله‌مراتب است: لایه‌ی ۰ میلیون‌ها گره دارد که متراکم به همسایه‌ها وصل‌اند، لایه‌ی ۱ شاید ده‌ها هزار، لایه‌ی ۲ شاید صدها و همین‌طور ادامه. Vectorهای لایه‌های بالاتر نقش اتصال «دوربرد» را دارند که به شما اجازه می‌دهند سریع از فضا عبور کنید.

جستجو در HNSW از بالا شروع می‌شود و به پایین می‌رود:

  1. از لایه‌ی بالا با نقطه‌ی ورود تصادفی شروع کنید.
  2. جستجوی حریصانه (Greedy): به همسایه‌ای بروید که به Query Vector نزدیک‌تر است.
  3. تکرار کنید تا در این لایه نتوانید نزدیک‌تر شوید.
  4. به لایه‌ی بعدی (که گره‌های بیشتری دارد) پایین بروید و جستجوی حریصانه را ادامه دهید.
  5. در لایه‌ی ۰ جستجوی محلی دقیق‌تری انجام دهید تا K همسایه‌ی نزدیک را پیدا کنید.

لایه‌های بالا اجازه می‌دهند سریع به منطقه‌ی درست از فضا «زوم» کنید. وقتی به لایه‌ی ۰ می‌رسید، از قبل در همسایگی درست هستید و فقط باید محلی جستجو کنید. این پیچیدگی جستجوی ( O(\log n) ) با Recall عالی می‌دهد. HNSW به‌طور مداوم به Recall بالای ۹۵٪ با Latency کم می‌رسد، که دلیل انتخاب پیش‌فرض شدنش است.

اما رایگان نیست. Indexهای HNSW پرمصرف از نظر حافظه‌اند. باید ساختار گراف (همه‌ی آن یال‌ها) را علاوه بر خود Vectorها ذخیره کنید، تقریباً ۲ برابر حافظه‌ی Vectorهای خام. ساخت Index کند است چون این ساختار گراف پیچیده را می‌سازید. و درج نسبتاً گران است، چون هر Vector جدید باید جایش را در گراف پیدا کند و در هر لایه اتصال بسازد. به این برمی‌گردیم.

وقتی مصاحبه‌کننده می‌پرسد «پایگاه داده‌ی Vector چطور سریع آیتم‌های مشابه را پیدا می‌کند؟»، معمولاً جواب HNSW است. می‌توانید این‌طور توضیح دهید: «یک گراف چندلایه می‌سازد که Vectorها گره‌هایش هستند. جستجو از لایه‌ی بالای خلوت شروع می‌شود و حریصانه به سمت هدف حرکت می‌کند و هرچه نزدیک‌تر می‌شود به لایه‌های متراکم‌تر پایین می‌آید. مثل Skip List است ولی برای فضای با بُعد بالا.»

IVF (Inverted File Index)

IVF رویکرد متفاوتی دارد. به‌جای ساخت گراف، Vectorها را با خوشه‌بندی k-means به Clusterها تقسیم می‌کند. هر Cluster یک Centroid (نقطه‌ی مرکزی) دارد و Vectorها به نزدیک‌ترین Centroid تخصیص داده می‌شوند.

هنگام Query، اول نزدیک‌ترین Centroidها را به Query Vector پیدا می‌کنید و فقط داخل آن Clusterها جستجو می‌کنید. اگر ۱۰۰۰ Cluster دارید و ۱۰ تا را جستجو کنید، ۹۹٪ مقایسه‌ها را حذف کرده‌اید.

شاید با خودتان فکر کنید: چرا فقط Cluster «درست» را جستجو نکنم؟ مشکل، مثل Geospatial Indexing، لبه‌هاست. اگر Query ما نزدیک لبه‌ی Cluster باشد، باید Clusterهای مجاور را هم بگیریم تا چیزی را از دست ندهیم. در فضای دو‌بُعدی جای زیادی برای قرار گرفتن روی «لبه» هست. در فضای ۱۵۳۶ بُعدی، لبه‌ها خیلی بیشترند!

پارامتر nprobe تعیین می‌کند چند Cluster را جستجو کنید. nprobe بالاتر یعنی Recall بهتر ولی Queryهای کندتر. این یک دستگیره‌ی خوب برای تنظیم Trade-off بین Recall و Latency می‌دهد.

این Probeها به‌راحتی موازی می‌شوند، پس برای یک Query تنها لازم نیست با افزایش nprobe ضربه‌ی Latency بخورید. ولی در سیستم‌های واقعی تحت بار، آن‌قدر درخواست دارید که محاسبات را اشباع کنید. یعنی ضربه‌ی Latency برای درخواست متوسط.

IVF ساختنش از HNSW سریع‌تر است و درج را ملایم‌تر مدیریت می‌کند (فقط Vector جدید را به یک Cluster تخصیص می‌دهید). حافظه‌ی کمتری مصرف می‌کند چون فقط تخصیص Cluster را ذخیره می‌کنید، نه گراف کامل. عیبش معمولاً Recall کمتر برای همان Latency است، به‌خصوص اگر داده‌ی شما طبیعتاً Cluster نباشد.

Locality Sensitive Hashing (LSH)

LSH رویکرد اساساً متفاوتی دارد: به‌جای ساخت گراف یا خوشه‌بندی، از Hash Functionهایی استفاده می‌کند که طوری طراحی شده‌اند که Vectorهای شبیه با احتمال بالا در همان Bucket قرار گیرند. Hash Functionهای معمولی سعی می‌کنند از Collision جلوگیری کنند. Hash Functionهای LSH طوری طراحی شده‌اند که برای آیتم‌های شبیه Collision ایجاد کنند.

رایج‌ترین روش برای شباهت کسینوسی از ابرصفحه‌های تصادفی (Random Hyperplane) استفاده می‌کند (ابرصفحه فقط اسم شیکی برای صفحه در فضای با بُعد بالاست). تصور کنید خط تصادفی از میان فضای Vector می‌کشید. هر Vector یا «بالای» آن خط است یا «پایین» آن؛ این یک بیت از Hash شماست. این را مثلاً با ۸ ابرصفحه‌ی تصادفی انجام دهید و Hash ۸ بیتی می‌گیرید. Vectorهایی که به هم نزدیک‌اند احتمالاً در همان سمت بیشتر ابرصفحه‌ها قرار می‌گیرند، پس Hash شبیه (یا یکسان) خواهند داشت.

برای قوی‌تر کردن این، از چند جدول Hash با ابرصفحه‌های تصادفی مختلف استفاده می‌کنیم. یک جدول تنها ممکن است Vectorهای شبیهی را که اتفاقاً دو سمت یک ابرصفحه افتاده‌اند از دست بدهد. اما با چند جدول، احتمال اینکه Vectorهای شبیه دست‌کم یک Bucket مشترک داشته باشند به‌شدت بالا می‌رود.

هنگام Query، Query Vector را در همه‌ی جدول‌ها Hash می‌کنید، همه‌ی کاندیداها را از Bucketهای مطابق جمع می‌کنید و بعد فاصله‌ی دقیق را فقط روی این مجموعه‌ی کاندیدا (که امیدواریم کوچک باشد) حساب می‌کنید. Trade-off روشن است: جدول و بیت بیشتر یعنی Recall بهتر ولی حافظه‌ی بیشتر و Query کندتر.

LSH پیش از غالب شدن HNSW محبوب بود. پیاده‌سازی‌اش ساده است، با بُعد بالا خوب کنار می‌آید و تضمین‌های نظری خوبی دارد. اما در عمل HNSW معمولاً برای همان Latency به Recall بهتری می‌رسد. LSH هنوز وقتی مفید است که نیاز دارید:

  • ساخت Index بسیار سریع (فقط Hash محاسبه می‌شود).
  • داده‌ی Streaming که Vectorها مدام می‌رسند.
  • شباهت بر پایه‌ی فاصله‌ی همینگ (LSH در اینجا طبیعی است).

Annoy

مثل LSH، ایده‌ی بریدن فضای Vector با ابرصفحه‌های تصادفی را می‌توان به ساختارهای درختی گسترش داد. Annoy (مخفف Approximate Nearest Neighbors Oh Yeah، از Spotify) جنگلی از درخت‌های Random Projection می‌سازد. ایده به‌طرز زیبایی ساده است: فضای Vector را به‌صورت بازگشتی با ابرصفحه‌های تصادفی تقسیم کنید تا هر گره‌ی برگ تعداد کمی Vector داشته باشد.

برای ساخت یک درخت، دو Vector تصادفی انتخاب می‌کنید و ابرصفحه‌ای هم‌فاصله بین آن‌ها می‌کشید. همه‌ی Vectorهای یک طرف به زیردرخت چپ و همه‌ی Vectorهای طرف دیگر به زیردرخت راست می‌روند. بازگشتی تکرار کنید تا هر برگ به‌اندازه‌ی کافی Vector کم داشته باشد (مثلاً ۱۰۰). نتیجه درخت دودویی است که Vectorهای نزدیک معمولاً در همان برگ یا برگ‌های مجاور قرار می‌گیرند.

برای جستجو، درخت را به سمت برگی که با Query Vector جور است پیمایش می‌کنید. اما یک درخت تنها می‌تواند اشتباه کند؛ نزدیک‌ترین همسایه‌ی واقعی شما ممکن است در یکی از تقسیم‌های اولیه به طرف دیگر افتاده باشد. پس Annoy جنگلی از درخت‌های متعدد (معمولاً ۱۰ تا ۱۰۰) می‌سازد که هرکدام تقسیم‌های تصادفی متفاوتی دارند. هنگام Query همه‌ی درخت‌ها را جستجو می‌کنید، برگ‌های کاندیدا را جمع می‌کنید و فاصله‌ی دقیق را روی اجتماع کاندیداها حساب می‌کنید.

اگر با رویکردهای کلاسیک ML آشنا باشید، شباهت قوی با الگوریتم Random Forest اینجا هست! (Random Forest مجموعه‌ای از درخت‌های تصمیم است که با تغییرهای تصادفی ساخته می‌شوند و برای کاهش Overfitting میانگین‌گیری می‌شوند [web:22].)

ویژگی کشنده‌ی Annoy حافظه‌نگاشتی (Memory Mapping) است. کل Index به‌صورت یک فایل ذخیره می‌شود که می‌توان آن را mmap کرد. mmap فراخوان سطح پایین سیستمی است که فایل را در حافظه نگاشت می‌کند و در مقایسه با جایگزین، یعنی هماهنگ کردن خواندن و نوشتن فایل از User Space، بسیار کارآمد است. این یعنی:

  • چند پروسس می‌توانند بدون کپی کردن یک Index را به اشتراک بگذارند.
  • می‌توانید با Indexهای بزرگ‌تر از RAM کار کنید (سیستم‌عامل آنچه را لازم دارید به حافظه می‌آورد).
  • بارگذاری Index آنی است (فقط mmap، بدون Deserialization).

عیب این است که Indexهای Annoy تغییرناپذیرند (Immutable). وقتی ساخته شد، نمی‌توانید Vector اضافه یا حذف کنید و باید کل Index را دوباره بسازید. این آن را برای مجموعه‌داده‌های ایستا (مثل کاتالوگ موسیقی Spotify که به‌صورت دسته‌ای به‌روز می‌شود) عالی ولی برای اپلیکیشن‌های بلادرنگ که Vectorها مدام می‌رسند نامناسب می‌کند.

Annoy پیش از غالب شدن HNSW راه‌حل اصلی بسیاری از شرکت‌ها بود. هنوز در سیستم‌های Production که مجموعه‌داده ایستاست و حافظه‌نگاشتی ارزشمند است می‌بینیدش.

خوب، هنوز با من هستید؟ حالا در Indexing Vector متخصص شده‌اید. برای خلاصه: جستجوی Vector راهی برای بازیابی آیتم‌های «شبیه» از پایگاه داده باز می‌کند و می‌توانیم با الگوریتم‌هایی مثل HNSW، IVF و LSH آن را کارآمد (هرچند اندکی نادقیق) کنیم. اما وقتی می‌خواهیم آیتم‌هایی را بازیابی کنیم که صرفاً شبیه نیستند، بلکه با Query مشخصی هم‌خوانی دارند چه؟

اپلیکیشن‌های واقعی به‌ندرت می‌گویند «۱۰ آیتم شبیه‌تر را پیدا کن» بدون قید. معمولاً می‌خواهید «۱۰ آیتم شبیه‌تر که موجودی دارند» یا «در بازه‌ی قیمتی کاربرند» یا «امسال منتشر شده‌اند» را پیدا کنید.

به این Filtered Vector Search می‌گویند و از چیزی که به نظر می‌رسد دشوارتر است. دو گزینه دارید:

  • Post-filtering: N شبیه‌ترین Vector را پیدا کنید (که N بسیار بزرگ‌تر از K است)، بعد فیلتر کنید تا K نتیجه بماند. مشکل این است که اگر فیلتر سخت‌گیر باشد، ممکن است K نتیجه پیدا نکنید. می‌توانید N را بالا ببرید ولی کار بیشتری می‌کنید.
  • Pre-filtering: اول فیلتر کنید، بعد فقط داخل مجموعه‌ی فیلترشده جستجو کنید. مشکل این است که ممکن است نتوانید از ساختارهای Index پیشرفته‌تان روی زیرمجموعه‌ی دلخواه از داده استفاده کنید.

کدام بهتر است کاملاً به داده بستگی دارد، که اظهارنظر کلی درباره‌ی بهترین گزینه برای یک اپلیکیشن را بسیار سخت می‌کند. پس معمولاً امن است به مصاحبه‌کننده یادآوری کنید «این به داده بستگی زیادی دارد، احتمالاً یک Benchmark با داده‌ی واقعی می‌سازم و می‌بینم کدام بهتر جواب می‌دهد».

با این حال، وقتی می‌توانیم اظهارنظر کلی کنیم (مثلاً اگر فقط برای زیرمجموعه‌ی بسیار کوچکی از داده‌تان جستجوی Vector می‌کنید)، Pre-filtering احتمالاً بهتر است و مصاحبه‌کننده‌ها دوست دارند چنین Edge Caseهایی را برای سنجش شهود شما پیدا کنند. مشابه همان مشکلاتی است که در مسائلی مثل جستجوی پست فیسبوک حل کردیم، با پیچ‌وتاب چندبُعدی.

سخت بودن چیزی به این معنا نیست که پایگاه‌های داده‌ی زیربنایی سعی نکنند بار سنگین را از دوش شما بردارند. بیشتر پایگاه‌های داده‌ی Vector از رویکردهای ترکیبی استفاده می‌کنند. بعضی چند Index برای ترکیب‌های رایج فیلتر نگه می‌دارند. بعضی فیلترینگ را مستقیم در پیمایش Index ادغام می‌کنند. ببینیم سه سیستم محبوب چطور با این مدیریت می‌کنند:

  • pgvector در Postgres به برنامه‌ریز Query در Postgres تکیه می‌کند. Query معمولی SQL می‌نویسید که هم WHERE دارد و هم ORDER BY بر اساس فاصله‌ی Vector. Planner تصمیم می‌گیرد از Index برداری، Index از نوع B-tree روی ستون فیلتر، یا ترکیبی استفاده کند. برای فیلترهای بسیار گزینشی، اغلب Index برداری را کاملاً رد می‌کند و روی ردیف‌های فیلترشده شباهت Brute-force انجام می‌دهد، که در واقع سریع‌تر است. نکته این است که pgvector «پیمایش HNSW فیلترشده‌ی واقعی» ندارد؛ یا این یا آن، پس وقتی گزینشی بودن فیلتر در محدوده‌ی میانی نامناسب باشد، می‌توانید Planهای غیربهینه بگیرید.
  • Elasticsearch یکپارچگی محکم‌تری دارد. جستجوی kNN آن پارامتر filter را پشتیبانی می‌کند که حین پیمایش Index اعمال می‌شود، نه بعد از آن. زیر کاپوت، ترکیبی از HNSW و تولید کاندیدای فیلترشده استفاده می‌کند. هنگام جستجو، ES اول Vectorهای کاندیدا را از گراف HNSW شناسایی می‌کند، بعد فیلتر شما را اعمال می‌کند، و بعد تا وقتی نتایج فیلترشده‌ی کافی داشته باشد به کاوش ادامه می‌دهد. یعنی فیلترهای بسیار سخت‌گیر جستجو را کند می‌کنند (برای یافتن K نتیجه‌ی منطبق باید بخش بیشتری از گراف را پیمایش کنید)، ولی اگر K نتیجه وجود داشته باشد تضمین می‌شود. ES جستجوی ترکیبی را هم به‌صورت بومی پشتیبانی می‌کند؛ می‌توانید امتیازدهی کلمه‌کلیدی BM25 را با شباهت Vector ترکیب کنید.
  • پایگاه داده‌های Vector ساخته‌شده برای این کار مثل Pinecone فیلتر فراداده را قابلیت درجه‌ی یک می‌دانند. هر Vector می‌تواند فراداده‌ی دلخواه (تا ۴۰ کیلوبایت) داشته باشد و فیلترها حین جستجوی ANN اعمال می‌شوند، نه قبل یا بعد از آن. Pinecone ساختارهای Index تخصصی می‌سازد تا این را پشتیبانی کند، اساساً با نگه داشتن Inverted Index روی فیلدهای فراداده کنار Index برداری. وقتی با فیلتر Query می‌زنید، نتایج فیلتر فراداده را با جستجوی Vector در یک عملیات واحد اشتراک می‌گیرد. Pinecone همچنین اجازه می‌دهد پارامتر «تلاش فیلتر» را تنظیم کنید تا بین دقت فیلتر و Latency تعادل ایجاد کنید.

خلاصه اینکه گزینه‌ها هستند.

در نهایت، برای پیچیدگی بیشتر، دسته‌ای از جستجوها هست که می‌خواهیم هم‌زمان هم جستجوی متن کامل و هم جستجوی Vector انجام دهیم، که اغلب «Hybrid Search» نامیده می‌شود. Queryی مثل «کفش دویدن قرمز» ممکن است برای «قرمز» و «کفش دویدن» از تطابق کلمه‌کلیدی و برای پیدا کردن محصولات مرتبط معنایی از شباهت Vector استفاده کند. این اغلب نتایج بهتری از هر رویکرد تنها می‌دهد. با اجرای موازی هر دو جستجو و ادغام نتایج، یا با استراتژی‌های ادغام هوشمندانه می‌توان به آن رسید.

درج، به‌روزرسانی و نگهداری Index

پایگاه داده‌های Vector معمولاً برای بارهای کاری سنگین خواندنی بهینه شده‌اند. نوشتن پیچیده‌تر است، به‌خصوص با Indexهای پیشرفته مثل HNSW.

درج می‌تواند بلادرنگ کار کند، ولی اغلب از نظر محاسباتی گران است. افزودن Vector جدید به گراف HNSW یعنی پیدا کردن جایش و به‌روز کردن اتصال‌ها. و اگر درج‌های زیاد انجام دهید، ساختار گراف می‌تواند به‌مرور افت کند (Vectorهای جدید ممکن است به اندازه‌ی Vectorهایی که هنگام ساخت اولیه بودند خوب وصل نباشند). به همین شکل، با IVF ممکن است با گذشت زمان به بازسازی Index نیاز پیدا کنید چون Clusterها جابه‌جا می‌شوند و تکامل می‌یابند، وگرنه عملکرد افت می‌کند.

بسیاری از سیستم‌ها این را با نگه داشتن یک Index «داغ» (Hot) کوچک برای درج‌های اخیر و یک Index «سرد» (Cold) بزرگ‌تر برای داده‌ی قدیمی‌تر مدیریت می‌کنند. Queryها هر دو را جستجو می‌کنند و نتایج را ادغام می‌کنند. به‌صورت دوره‌ای Index داغ با بازسازی کامل در Index سرد ادغام می‌شود. Index «داغ» لازم نیست اصلاً Index کامل باشد؛ می‌تواند فقط لیست ساده‌ای از ورودی‌هایی باشد که Index نشده‌اند و کامل جستجویشان می‌کنیم.

این الگو در جاهایی بیش از پایگاه داده‌های Vector مفید است. مثلاً اگر سیستم شما باید حذف آیتم‌ها را مدیریت کند ولی این حذف‌ها به‌ندرت رخ می‌دهند، شاید ساده‌تر باشد Index کوچکی از آیتم‌های حذف‌شده نگه دارید که پیش از برگرداندن نتایج به کاربر بررسی کنید. آن‌وقت زمان دارید همه‌ی پاکسازی لازم را انجام دهید تا مطمئن شوید آن آیتم دیگر در Cacheها، پیش‌محاسبه‌ها و مانند آن وجود ندارد.

به‌روزرسانی‌ها معمولاً به‌صورت حذف + درج پیاده می‌شوند. بیشتر سیستم‌ها از حذف نرم (Soft Delete) استفاده می‌کنند (علامت‌گذاری Vectorها به‌عنوان حذف‌شده) به‌جای اینکه واقعاً از Index برشان دارند. یعنی Vectorهای حذف‌شده هنوز فضا اشغال می‌کنند و تا بازسازی یا Compact کردن Index، Queryها را کند می‌کنند.

بازسازی Index می‌تواند کند باشد. ساخت Index HNSW روی میلیون‌ها Vector می‌تواند ساعت‌ها طول بکشد. باید برایش برنامه بریزید. چند استراتژی:

  • بازسازی چرخشی (Rolling) که Index جدید را کنار قدیمی می‌سازید و بعد جابه‌جا می‌کنید.
  • Indexهای پارتیشن‌بندی‌شده که می‌توانید هر بار یک پارتیشن را بازسازی کنید.
  • بازایندکس پس‌زمینه که Queryها را بلاک نمی‌کند.

اگر Embeddingهای شما مدام تغییر می‌کنند (شاید مدل ML را به‌روز می‌کنید، یا داده‌ی زیربنایی سریع تغییر می‌کند)، باید با دقت به استراتژی به‌روزرسانی فکر کنید. به‌روزرسانی دسته‌ای با بازسازی دوره‌ای اغلب از به‌روزرسانی بلادرنگ عملی‌تر است.

نگهداری گران Index یکی از تفاوت‌های اصلی پایگاه داده‌های Vector با پایگاه داده‌های سنتی است. پایگاه داده‌های سنتی معمولاً می‌توانند به‌روزرسانی را بلادرنگ مدیریت کنند، چون برای تحمل نوشتن زیاد طراحی شده‌اند. پایگاه داده‌های Vector اغلب این‌طور نیستند. در مصاحبه‌ها این یعنی عمدی‌تر درباره‌ی استراتژی بازسازی فکر و صحبت می‌کنید، از جمله چیزهایی مثل Index «داغ» جانبی که گفتیم.

گزینه‌های پایگاه داده‌ی Vector

وقتی نوبت به انتخاب پایگاه داده‌ی Vector می‌رسد، گزینه‌های زیادی هست. بیشتر اینکه این حوزه خیلی سریع حرکت می‌کند و راه‌حل‌هایی که چند سال پیش محبوب بودند دارند کهنه می‌شوند.

پس توصیه‌ی عملی: ساده شروع کنید. برخلاف شهود، احتمالاً به پایگاه داده‌ی Vector تخصصی نیاز ندارید. افزونه‌های پایگاه داده‌هایی که از قبل استفاده می‌کنید میلیون‌ها Vector را به‌خوبی تحمل می‌کنند و از سربار عملیاتی یک سیستم دیگر دوری می‌کنید. فقط وقتی سراغ پایگاه داده‌ی Vector اختصاصی بروید که مقیاس یا ویژگی‌ها ایجاب کنند.

افزونه‌های Vector برای پایگاه داده‌های سنتی (از اینجا شروع کنید)

اگر دارید قابلیتی را به معماری موجود اضافه می‌کنید، احتمالاً نباید طراحی‌تان را با پیچیدگی اضافه زیاد خراب کنید. افزونه‌های پایگاه داده‌هایی که از قبل توصیه کرده‌ایم، مثل Postgres و Elasticsearch، اغلب نقطه‌ی شروع خوبی هستند.

  • pgvector اولین چیزی است که اگر از قبل روی PostgreSQL هستید باید امتحان کنید. هم HNSW و هم IVF را پشتیبانی می‌کند و میلیون‌ها Vector را بدون زحمت مدیریت می‌کند. مزیت واقعی همه‌ی چیزهای دیگری است که رایگان می‌گیرید: تراکنش‌های ACID، ابزارهای آشنا و امکان Join کردن نتایج Vector با داده‌ی رابطه‌ای. می‌خواهید محصولات شبیه را پیدا کنید که موجودی هم دارند و در منطقه‌ی کاربرند؟ یک Query.
  • Elasticsearch kNN اگر از قبل Elasticsearch برای جستجو دارید عالی است. افزودن جستجوی Vector ساده است و جستجوی ترکیبی (کلمه‌کلیدی + Vector) عالی به‌صورت آماده می‌گیرید. عیبش این است که ES خودش از نظر عملیاتی پیچیده است، ولی اگر به هر حال اجرایش می‌کنید، افزودن Vector افزایشی است.
  • Redis Vector Search وقتی مناسب است که نیازمندی‌های بلادرنگ و Latency کم دارید. Redis از قبل بخش رایج بیشتر معماری‌هاست و افزونه‌ی Vector ساده است. گزینه‌های Index ساده‌تر از پایگاه داده‌های Vector اختصاصی است، ولی برای بسیاری از کاربردها همین خوب است.
  • S3 Vector سرویس نسبتاً جدید AWS است که اجازه می‌دهد Vectorها را در S3 ذخیره و با API مربوط به S3 Query کنید. اگر از قبل S3 را برای کارهای دیگر استفاده می‌کنید و نمی‌خواهید پیچیدگی پایگاه داده‌ی Vector اختصاصی اضافه کنید، گزینه‌ی خوبی است.

پایگاه داده‌های Vector اختصاصی (وقتی به مقیاس نیاز دارید)

طراحی‌هایی هست که با مقیاس عظیم سروکار دارید و مسئله مشخصاً حول بازیابی یا توصیه می‌چرخد. این زمان‌هایی است که باید پایگاه داده‌ی Vector اختصاصی را در نظر بگیرید. قاعده‌ی سرانگشتی خوب: اگر با بیش از ۱۰۰ میلیون Vector سروکار دارید، پایگاه داده‌ی Vector اختصاصی را بسنجید.

  • Pinecone کاملاً مدیریت‌شده و Serverless است. هیچ زیرساختی اجرا نمی‌کنید؛ فقط API صدا می‌زنید. عملیات‌کردنش آسان‌ترین است که ارزش زیادی دارد. Trade-off هزینه و کنترل کمتر است.
  • Weaviate متن‌باز است و جستجوی ترکیبی خوب و API از نوع GraphQL دارد. تعادل منطقی بین ساخت دستی و مدیریت‌شده‌ی کامل است. از بعضی جایگزین‌ها آسان‌تر عملیاتی می‌شود ولی هنوز کنترل می‌دهد.
  • Milvus متن‌باز است و برای مقیاس جدی ساخته شده. می‌تواند میلیاردها Vector را مدیریت کند. روی دیگر سکه پیچیدگی عملیاتی است؛ سیستم توزیع‌شده‌ای با چند نوع Node اجرا می‌کنید.
  • Qdrant متن‌باز است، با Rust نوشته شده و پشتیبانی فیلترینگ ویژه‌ای خوبی دارد. اگر Queryهای فیلترشده‌ی پیچیده برای کاربردتان مرکزی است، ارزش بررسی دارد.
  • Chroma سبک است و برای Prototype عالی. در فضای LLM/RAG به‌طور فزاینده‌ای محبوب است چون شروع کار با آن آسان است.

استفاده از پایگاه داده‌های Vector در مصاحبه

سناریوهای رایج مصاحبه

پایگاه داده‌های Vector در تعدادی از سؤال‌های طراحی سیستم ظاهر می‌شوند، تقریباً منحصراً در حوزه‌های مجاور AI/ML. به‌ندرت در مصاحبه‌ای مثل این غافلگیر می‌شوید؛ می‌دانید یا برای تیمی نزدیک به AI/ML مصاحبه می‌دهید یا کل شرکت روی این متمرکز است. در این صورت چند الگوی سؤال هست که عملاً پایگاه داده‌ی Vector را الزامی می‌کنند:

  • جستجوی معنایی: «سیستم جستجوی سند طراحی کن» یا «ابزار جستجوی کد طراحی کن». کاربران Query زبان طبیعی می‌نویسند و شما اسناد مرتبط را پیدا می‌کنید. Embedding + جستجوی Vector کلاسیک.
  • توصیه‌گری: «سیستم توصیه‌ی محصول طراحی کن» یا «Feed توصیه‌ی محتوا طراحی کن». آیتم‌های شبیه به چیزی که کاربر با آن تعامل داشته پیدا کن. اغلب با Collaborative Filtering ترکیب می‌شود.
  • شباهت تصویر/ویدیو: «جستجوی معکوس تصویر طراحی کن» یا «قابلیت ویدیوهای مشابه طراحی کن». همان الگو: رسانه را Embed کنید و دنبال Embeddingهای شبیه بگردید.
  • سیستم‌های RAG: «سیستم پرسش‌وپاسخ پایگاه دانش طراحی کن» یا هر چیزی که شامل LLM با داده‌ی سفارشی باشد. جستجوی Vector اسناد مرتبط را بازیابی می‌کند و LLM پاسخ را ترکیب می‌کند.
  • حذف تکراری (Deduplication): «سیستم تشخیص تقریباً-تکراری طراحی کن». می‌تواند تشخیص سرقت ادبی، پیدا کردن تیکت‌های پشتیبانی مشابه یا شناسایی آگهی‌های تکراری باشد. آیتم‌ها را Embed کنید و آیتم‌هایی را که در آستانه‌ی شباهت مشخصی هستند پیدا کنید.
  • تشخیص ناهنجاری: «سیستم تشخیص تقلب طراحی کن». تراکنش‌ها را Embed کنید و تراکنش‌هایی را پیدا کنید که با الگوهای عادی فرق دارند.

اگر سابقه‌ی ML ندارید، باز هم ممکن است خواندن طراحی سطح بالای تحلیل‌های ML System Design برای بعضی از این سیستم‌ها برایتان مفید باشد. این به شما شهود می‌دهد پایگاه داده‌های Vector کجا قرار می‌گیرند.

الگوهای معماری

چند روش رایج برای وصل کردن جستجوی Vector در سیستم هست:

  • الگوی ۱، پایگاه داده‌ی Vector به‌عنوان سرویس جدا: رایج‌ترین است. اپلیکیشن یک Embedding تولید یا بازیابی می‌کند، آن را به سرویس Vector می‌فرستد، شناسه‌ی آیتم‌های شبیه را می‌گیرد، بعد جزئیات کامل آیتم را از پایگاه داده‌ی اصلی می‌خواند. جداسازی تمیز دغدغه‌ها.
  • الگوی ۲، Hybrid Search: Query هم به Index کلمه‌کلیدی (مثل Elasticsearch) و هم Index برداری می‌رود. نتایج با تابع رتبه‌بندی ادغام می‌شوند. برای اپلیکیشن‌های جستجو که هم تطابق دقیق و هم شباهت معنایی مهم است خوب است.
  • الگوی ۳، بازیابی دومرحله‌ای: جستجوی Vector مجموعه‌ی کاندیدای بزرگی (شاید ۱۰۰۰ تای برتر) برمی‌گرداند، بعد مدل پیچیده‌تر (ولی کندتر) آن‌ها را دوباره رتبه‌بندی (Rerank) می‌کند تا نتایج نهایی را انتخاب کند. در سیستم‌های توصیه‌گر رایج است چون Reranker می‌تواند از ویژگی‌هایی استفاده کند که Embedding نمی‌گیرد.

یادتان باشد مگر اینکه سؤال مشخصاً روی این وظیفه متمرکز باشد، احتمال بیشتری دارد با افزودن پیچیدگی غیرضروری در دنیایی از مسائل جدید و چالش‌برانگیز فرو بروید تا اینکه مصاحبه‌کننده را با عمقتان تحت تأثیر قرار دهید. برای بیشتر مسائل به الگوی ۱ پایبند باشید!

تصمیم‌های طراحی کلیدی برای بحث

وقتی جستجوی Vector را در مصاحبه معرفی می‌کنید، باید این‌ها را پوشش دهید:

  • نیازمندی‌های Consistency. نتایج جستجوی Vector معمولاً اشکالی ندارد که کمی کهنه باشند. آیتم‌های جدید ممکن است چند ثانیه یا حتی چند دقیقه قابل‌جستجو نباشند. صریحاً بگویید: «جستجوی Vector می‌تواند Eventually Consistent باشد؛ لازم نیست Embedding بلافاصله بعد از درج قابل‌جستجو باشد.»
  • استراتژی به‌روزرسانی. Embeddingها چطور وارد سیستم می‌شوند؟ بلادرنگ همین که آیتم‌ها ساخته می‌شوند؟ Batch Job که هر ساعت اجرا می‌شود؟ به نیازمندی‌های Latency بستگی دارد.
  • استراتژی فیلترینگ. اگر Query شما فیلتر دارد، چطور مدیریتش می‌کنید؟ Pre-filter، Post-filter یا ترکیبی؟ به‌خصوص اگر فیلترها گزینشی باشند مهم است.

بسیار کمتر رایج در مصاحبه‌ی زیرساختی بحث درباره‌ی این‌هاست:

  • انتخاب مدل Embedding. چه مدلی Embeddingها را تولید می‌کند؟ این روی اندازه‌ی بُعد، کیفیت و Latency اثر می‌گذارد. برای متن چیزی مثل «از مدل Sentence Transformer استفاده می‌کنیم» یا «API امبدینگ OpenAI» بگویید. مگر در مصاحبه‌ی ML System Design نیازی نیست عمیق شوید.
  • نوع Index. «برای بهترین عملکرد Query از HNSW استفاده می‌کنیم» معمولاً جواب درست است، مگر محدودیت مشخصی داشته باشید (نوشتن بسیار زیاد، مقیاس بسیار بزرگ، محدودیت حافظه).
  • به‌روزرسانی Embedding. گاهی مدل Embedding را عوض یا تازه می‌کنید. این یعنی بازسازی عظیم و گاهی نیاز به هماهنگی دقیق دارد. اطمینان از اینکه API های شما جزئیات اینکه کدام مدل Embedding را تولید کرده در بر می‌گیرند، تضمین می‌کند با Embedding اشتباه جستجو نمی‌کنید.

اعدادی که باید بدانید

چند عدد تقریبی مفید در مصاحبه:

  • بُعد Embedding: ۱۲۸ تا ۱۵۳۶ معمول است. OpenAI از ۱۵۳۶ استفاده می‌کند و بسیاری از مدل‌های متن‌باز ۳۸۴ یا ۷۶۸. (توجه: همین متن پیشتر text-embedding-3-large را با ۳۰۷۲ بُعد گفته بود، پس بسته به مدل فرق می‌کند.)
  • حافظه‌ی هر Vector: ۴ بایت به ازای هر بُعد برای float32. Vector با ۱۵۳۶ بُعد حدود ۶ کیلوبایت خام است. (بزرگ‌تر از آنچه فکر می‌کنید!)
  • ۱ میلیون Vector با ۱۵۳۶ بُعد: حدود ۶ گیگابایت فقط برای Vectorها، بیشتر با سربار Index (HNSW تقریباً دوبرابر می‌کند).
  • Latency Query: زیر ۱۰ میلی‌ثانیه برای سیستم‌های خوب تنظیم‌شده قابل‌دستیابی است. ۱ تا ۵ میلی‌ثانیه رایج است.
  • هدف Recall: ۹۵٪ یا بیشتر معمولاً قابل‌قبول است. ۹۹٪ یا بیشتر قابل‌دستیابی است ولی از نظر Latency یا حافظه گران‌تر.
  • Throughput: ده‌ها هزار Query در ثانیه برای هر Node با Indexهای درون‌حافظه‌ای واقع‌بینانه است.

مشکلات و محدودیت‌ها

چند چیز که باید در ذهن داشته باشید و احتمالاً در مصاحبه ذکر کنید:

  • پایگاه داده‌های Vector پایگاه داده‌ی تراکنشی نیستند. سعی نکنید از آن‌ها به‌عنوان منبع حقیقت استفاده کنید. در جستجوی شباهت عالی‌اند ولی در هر کار دیگری که پایگاه داده‌ها انجام می‌دهند وحشتناک. داده‌ی معتبر شما جای دیگری زندگی می‌کند؛ پایگاه داده‌ی Vector یک Index است.
  • Embedding Drift نگرانی عملیاتی واقعی است. اگر مدل Embedding را عوض کنید، همه‌ی Embeddingهای قدیمی ناسازگار می‌شوند. یا باید همه‌چیز را دوباره Embed کنید (گران) یا در دوره‌ی انتقال چند Index نگه دارید. ارزش برنامه‌ریزی دارد.
  • Cold Start برای شخصی‌سازی مشکل است. اگر رفتار کاربر را Embed می‌کنید تا کاربران شبیه را پیدا کنید، کاربران جدید رفتاری برای Embed ندارند. به استراتژی‌های Fallback نیاز دارید.
  • بُعد در برابر عملکرد یک Trade-off است. Embeddingهای با بُعد بالاتر ظرافت بیشتری می‌گیرند ولی جستجویشان کندتر است و حافظه‌ی بیشتری می‌خواهند. برای بعضی اپلیکیشن‌ها ۱۲۸ بُعد کاملاً کافی است. فرض نکنید به بزرگ‌ترین Embeddingی که مدلتان تولید می‌کند نیاز دارید.
  • ساخت Index زمان می‌برد. ساخت Index HNSW روی ۱۰ میلیون Vector می‌تواند یک ساعت یا بیشتر طول بکشد. این روی سرعت استقرار تغییرات یا بازیابی از شکست اثر می‌گذارد.
  • جستجوی Vector تطابق دقیق نیست. اگر باید سند دقیقی را با شناسه پیدا کنید، از پایگاه داده‌ی معمولی استفاده کنید. جستجوی Vector چیزهای شبیه را پیدا می‌کند، نه یکسان. گاهی هر دو را می‌خواهید.

جمع‌بندی

پایگاه داده‌های Vector دسته‌ی جدیدی از اپلیکیشن‌ها را ممکن می‌کنند که بر شباهت معنایی ساخته شده‌اند، نه تطابق دقیق. فناوری اصلی جستجوی نزدیک‌ترین همسایه‌ی تقریبی است و HNSW رایج‌ترین الگوریتم در سیستم‌های Production است.

توصیه‌ی عملی این است که ساده شروع کنید. اگر PostgreSQL اجرا می‌کنید اول pgvector را امتحان کنید. فقط وقتی به پایگاه داده‌ی Vector اختصاصی بروید که از چیزی که افزونه‌ها می‌دهند فراتر رفته‌اید. پیچیدگی اجرای یک سیستم دیگر اغلب دست‌کم گرفته می‌شود.

در مصاحبه‌ها پایگاه داده‌های Vector برای جستجو، توصیه‌گری و هر چیزی که AI/ML را شامل می‌شود روزبه‌روز مرتبط‌ترند. بدانید چه مشکلی را حل می‌کنند (پیدا کردن سریع چیزهای شبیه)، چطور حل می‌کنند (Indexهای ANN مثل HNSW)، و کی ابزار درست‌اند (شباهت معنایی، نه تطابق دقیق). با معماری ساده شروع کنید و فقط وقتی نیازمندی‌ها ایجاب کند پیچیدگی اضافه کنید.

این حوزه سریع در حال تکامل است. الگوریتم‌های Index جدید، یکپارچگی محکم‌تر با پایگاه داده‌ها و ابزارهای بهتر مرتب ظاهر می‌شوند. اما اصول Embed کردن داده، سنجش شباهت و Trade-off بین Recall، Latency و حافظه فارغ از اینکه کدام فناوری برنده شود مرتبط خواهد ماند.