جستجوی برداری — چطور بهترین نتایج رو پیدا کنیم

قسمت ۶ ۲۰ دقیقه

یه مرور سریع

تو اپیزودهای قبلی یاد گرفتیم که چرا LLM به تنهایی کافی نیست، ایده اصلی RAG چیه، Embedding چطور متن رو به بردار تبدیل می‌کنه، Vector Database چطور این بردارها رو ذخیره می‌کنه، و Chunking چطور متن رو تکه‌تکه می‌کنه. حالا وقتشه بریم سراغ یه سؤال خیلی مهم: وقتی کاربر یه سؤال می‌پرسه، چطور بهترین تکه‌های مرتبط رو پیدا کنیم؟

اینجاست که جستجوی برداری وارد بازی می‌شه. یعنی هنر پیدا کردن نزدیک‌ترین بردارها به بردار سؤال کاربر.

معیارهای فاصله — سه تا ابزار اصلی

وقتی می‌خوای دو بردار رو مقایسه کنی و بفهمی چقدر شبیه هم هستن، سه تا معیار اصلی داری:

۱. Cosine Similarity (شباهت کسینوسی)

فرض کن دو تا فلش داری که از یه نقطه شروع می‌شن. Cosine Similarity زاویه بین این دو فلش رو اندازه می‌گیره. اگه زاویه صفر باشه (یعنی هم‌جهت باشن)، شباهت ۱ هست. اگه ۹۰ درجه باشن، شباهت ۰ هست. اگه ۱۸۰ درجه باشن (مخالف هم)، شباهت -۱ هست.

similarity = cos(θ) = (A · B) / (|A| × |B|)

نکته کلیدی: Cosine Similarity به طول بردار اهمیت نمی‌ده، فقط جهت مهمه. یعنی اگه یه متن کوتاه و یه متن بلند درباره یه موضوع باشن، بازم شباهت بالایی دارن. به همین خاطر محبوب‌ترین معیار تو RAG هست.

۲. Dot Product (ضرب داخلی)

ضرب داخلی هم جهت و هم طول بردارها رو در نظر می‌گیره. فرمولش ساده‌ست:

dot_product = A₁×B₁ + A₂×B₂ + ... + Aₙ×Bₙ

فرق اصلیش با Cosine اینه که اگه بردارها نرمال‌سازی شده باشن (طولشون ۱ باشه)، Dot Product دقیقاً برابر Cosine Similarity می‌شه. خیلی از مدل‌های Embedding مثل OpenAI بردارهای نرمال‌سازی‌شده تولید می‌کنن، پس عملاً فرقی نداره.

اما اگه بردارها نرمال نباشن، Dot Product به متن‌های بلندتر امتیاز بیشتری می‌ده. گاهی این خوبه (مثلاً وقتی می‌خوای مقالات جامع‌تر رو ترجیح بدی)، گاهی بد.

۳. Euclidean Distance (فاصله اقلیدسی)

این همون فاصله‌ای هست که تو ریاضی دبیرستان یاد گرفتی. فاصله خط مستقیم بین دو نقطه:

distance = √((A₁-B₁)² + (A₂-B₂)² + ... + (Aₙ-Bₙ)²)

برعکس دو تای قبلی، اینجا عدد کمتر یعنی شباهت بیشتر. Euclidean هم به جهت و هم به طول حساسه، ولی تو فضای بالابعدی (مثلاً ۱۵۳۶ بعد) بعضی وقت‌ها نتایج غیرمنتظره می‌ده. به همین خاطر کمتر از بقیه استفاده می‌شه.

کدوم رو انتخاب کنم؟

قانون سرانگشتی: اگه مدل Embedding ات بردارهای نرمال‌سازی‌شده تولید می‌کنه (مثل مدل‌های OpenAI)، هر سه‌تا نتیجه تقریباً یکسان می‌دن. ولی اگه شک داری، Cosine Similarity رو انتخاب کن. تقریباً همیشه جواب می‌ده.

مشکل جستجوی دقیق — و چرا به ANN نیاز داریم

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

جستجوی دقیق (Exact Search یا Brute Force) دقیق‌ترین نتیجه رو می‌ده، ولی مقیاس‌پذیر نیست. اینجاست که ANN یا Approximate Nearest Neighbor وارد می‌شه.

ایده ANN ساده‌ست: به جای چک کردن همه بردارها، یه ساختار داده هوشمند بساز که فقط بردارهای «احتمالاً مرتبط» رو چک کنه. ممکنه ۱-۲ درصد دقت از دست بدی، ولی سرعت ۱۰۰ برابر بیشتر می‌شه.

HNSW — محبوب‌ترین الگوریتم

Hierarchical Navigable Small World یا HNSW فعلاً محبوب‌ترین الگوریتم ANN هست. بذار ساده توضیح بدم.

تصور کن شهرهای ایران رو روی نقشه داری. یه شبکه چندلایه می‌سازی:

  • لایه بالا: فقط شهرهای بزرگ — تهران، اصفهان، مشهد، تبریز
  • لایه وسط: شهرهای متوسط هم اضافه می‌شن — رشت، یزد، کرمان
  • لایه پایین: همه شهرها و روستاها

وقتی دنبال یه روستای خاص می‌گردی، اول تو لایه بالا نزدیک‌ترین شهر بزرگ رو پیدا می‌کنی، بعد تو لایه وسط نزدیک‌ترین شهر متوسط رو، و در نهایت تو لایه پایین دقیقاً روستای مورد نظر رو.

HNSW دقیقاً همین کار رو با بردارها می‌کنه. سرعتش فوق‌العاده‌ست و دقتش هم خیلی بالاست (معمولاً بالای ۹۵٪).

پارامترهای مهم HNSW:

  • M — تعداد اتصالات هر نقطه. بیشتر = دقیق‌تر ولی حافظه بیشتر
  • ef_construction — دقت ساخت ایندکس. بیشتر = ساخت کندتر ولی ایندکس بهتر
  • ef_search — دقت جستجو. بیشتر = جستجوی کندتر ولی دقیق‌تر

IVF — الگوریتم خوشه‌بندی

Inverted File Index یا IVF یه رویکرد دیگه‌ست. اول بردارها رو به گروه‌ها (خوشه‌ها) تقسیم می‌کنه. موقع جستجو، اول نزدیک‌ترین خوشه‌ها رو پیدا می‌کنه و بعد فقط بردارهای اون خوشه‌ها رو بررسی می‌کنه.

مثل اینه که کتابخونه رو به بخش‌ها تقسیم کنی (تاریخ، علم، ادبیات). وقتی دنبال یه کتاب فیزیک می‌گردی، فقط بخش علم رو نگاه می‌کنی.

IVF معمولاً حافظه کمتری از HNSW مصرف می‌کنه، ولی دقتش یه کم پایین‌تره.

پارامتر مهم: nprobe — تعداد خوشه‌هایی که موقع جستجو بررسی می‌شن. بیشتر = دقیق‌تر ولی کندتر.

فیلتر کردن با Metadata

جستجوی برداری خوبه، ولی همیشه کافی نیست. فرض کن یه سیستم پشتیبانی داری که مستندات محصولات مختلف رو داره. کاربر درباره «محصول A» سؤال می‌پرسه، ولی جستجوی برداری ممکنه تکه‌هایی از مستندات «محصول B» رو هم برگردونه چون کلمات مشابهی دارن.

راه‌حل: Metadata Filtering. موقع ذخیره هر تکه، یه سری اطلاعات اضافی (metadata) هم ذخیره می‌کنی:

{
  "text": "برای ریست کردن دستگاه، دکمه پاور رو ۱۰ ثانیه نگه دارید.",
  "metadata": {
    "product": "product-a",
    "category": "troubleshooting",
    "language": "fa",
    "last_updated": "2025-01-15"
  }
}

حالا موقع جستجو می‌تونی بگی: «فقط تکه‌هایی رو برگردون که product برابر product-a باشه و category برابر troubleshooting باشه.»

دو روش فیلتر:

  • Pre-filtering: اول فیلتر می‌کنی، بعد جستجوی برداری. سریع‌تره ولی ممکنه تعداد نتایج کم بشه.
  • Post-filtering: اول جستجوی برداری، بعد فیلتر. نتایج بهتری می‌ده ولی ممکنه کندتر باشه.

اکثر Vector Databaseهای مدرن مثل Pinecone، Weaviate و Qdrant هر دو روش رو پشتیبانی می‌کنن.

جستجوی ترکیبی — بهترین هر دو دنیا

جستجوی برداری تو درک مفهوم عالیه، ولی یه ضعف داره: گاهی کلمات دقیق رو از دست می‌ده. مثلاً اگه کاربر بپرسه «خطای E-4021 چیه؟»، جستجوی برداری ممکنه مفهوم «خطا» رو بگیره ولی کد دقیق «E-4021» رو نادیده بگیره.

از طرف دیگه، جستجوی کلمه‌کلیدی سنتی (مثل BM25) دقیقاً کلمات رو مچ می‌کنه ولی مفهوم رو نمی‌فهمه.

Hybrid Search هر دو رو با هم ترکیب می‌کنه:

# شبه‌کد جستجوی ترکیبی
vector_results = vector_search(query, top_k=20)
keyword_results = bm25_search(query, top_k=20)

# ترکیب نتایج با Reciprocal Rank Fusion
final_results = rrf_merge(vector_results, keyword_results)
return final_results[:10]

Reciprocal Rank Fusion (RRF) یه روش ساده ولی مؤثر برای ترکیب نتایجه. به هر نتیجه بر اساس رتبه‌اش تو هر لیست امتیاز می‌ده و بعد امتیازها رو جمع می‌کنه.

score(doc) = Σ 1/(k + rank_i)   # k معمولاً ۶۰

مثلاً اگه یه سند تو جستجوی برداری رتبه ۱ باشه و تو BM25 رتبه ۵، امتیازش می‌شه: 1/(60+1) + 1/(60+5) = 0.0164 + 0.0154 = 0.0318

جستجوی ترکیبی معمولاً ۱۰-۲۰ درصد بهتر از جستجوی صرفاً برداری عمل می‌کنه.

نکات عملی برای بهبود کیفیت جستجو

۱. بردار سؤال رو بهتر کن

قبل از جستجو، سؤال کاربر رو یه کم پردازش کن. مثلاً اگه کاربر نوشته «چرا کار نمی‌کنه؟»، می‌تونی از LLM بخوای سؤال رو بازنویسی کنه: «چرا سیستم ورود کاربر با خطا مواجه می‌شود؟»

۲. تعداد نتایج (top_k) رو تنظیم کن

خیلی کم = ممکنه جواب رو از دست بدی. خیلی زیاد = نویز اضافه می‌شه و Context Window پر می‌شه. معمولاً ۵ تا ۱۰ نقطه شروع خوبیه.

۳. آستانه شباهت (Similarity Threshold) بذار

فقط نتایجی رو برگردون که شباهتشون بالای یه حد مشخص باشه. مثلاً اگه Cosine Similarity زیر ۰.۷ بود، اون نتیجه رو نادیده بگیر. این کمک می‌کنه وقتی سؤال کاربر اصلاً ربطی به داده‌هات نداره، جواب بی‌ربط نده.

۴. مدل Embedding مناسب انتخاب کن

همه مدل‌های Embedding یکسان نیستن. بعضی‌ها تو زبان فارسی بهتر عمل می‌کنن، بعضی‌ها تو متون تخصصی. حتماً با داده‌های خودت تست کن و مقایسه کن.

۵. ایندکس رو بهینه کن

پارامترهای HNSW یا IVF رو بر اساس حجم داده‌ات تنظیم کن. برای ۱۰ هزار تکه، تنظیمات پیش‌فرض کافیه. برای ۱۰ میلیون تکه، باید جدی‌تر بهینه‌سازی کنی.

جمع‌بندی

تو این اپیزود یاد گرفتی که:

  • سه معیار اصلی فاصله (Cosine، Dot Product، Euclidean) چی هستن و کی از کدوم استفاده کنی
  • الگوریتم‌های ANN مثل HNSW و IVF چطور سرعت جستجو رو چند برابر می‌کنن
  • Metadata Filtering چطور نتایج رو دقیق‌تر می‌کنه
  • جستجوی ترکیبی (Hybrid Search) چطور بهترین هر دو دنیا رو می‌ده
  • نکات عملی برای بهبود کیفیت جستجو

حالا که بهترین تکه‌ها رو پیدا کردی، سؤال بعدی اینه: چطور اینا رو به LLM بدی که بهترین جواب رو تولید کنه؟ تو اپیزود بعدی درباره پرامپت‌نویسی برای RAG صحبت می‌کنیم — یه مهارت که فرق بین یه RAG معمولی و یه RAG عالی رو مشخص می‌کنه.

نظرات

هنوز نظری ثبت نشده. اولین نفر باشید!

نظر خود را بنویسید