# پایپ‌لاین بازیابی دو مرحله‌ای: Vector Search + Reranking ## مقدمه در سیستم‌های RAG (Retrieval-Augmented Generation)، کیفیت پاسخ مدل زبانی مستقیماً به کیفیت داده‌هایی بستگی دارد که به عنوان context به آن داده می‌شود. اگر داده‌های نامرتبط یا کم‌ارتباط به مدل برسند، پاسخ نهایی نیز ضعیف خواهد بود. برای حل این مشکل، ما از یک **پایپ‌لاین بازیابی دو مرحله‌ای** استفاده می‌کنیم: 1. **مرحله اول - Vector Search**: بازیابی N کاندید اولیه از وکتور دیتابیس بر اساس فاصله معنایی (Semantic Similarity) 2. **مرحله دوم - Reranking**: ارزیابی دقیق‌تر کاندیدها توسط یک مدل Reranker و انتخاب بهترین‌ها ## چرا فقط Vector Search کافی نیست؟ ### محدودیت‌های جستجوی وکتوری جستجوی وکتوری (Vector Search) بر اساس **فاصله بردارها** (مثلاً Cosine Similarity) کار می‌کند. یعنی سوال کاربر و اسناد موجود در دیتابیس هر دو به بردار تبدیل می‌شوند و نزدیک‌ترین بردارها به عنوان نتیجه برگردانده می‌شوند. اما این روش محدودیت‌هایی دارد: | محدودیت | توضیح | |---------|-------| | **دقت متوسط** | Embedding model ها معنای کلی متن را می‌گیرند، نه ارتباط دقیق سوال-جواب را | | **حساسیت به نحوه نوشتن** | دو جمله با معنای یکسان اما ساختار متفاوت ممکن است فاصله بیشتری داشته باشند | | **عدم درک سوال-جواب** | مدل embedding فقط شباهت معنایی می‌سنجد، نه اینکه آیا یک سند واقعاً پاسخ سوال را دارد | | **چالش‌های چند‌زبانه** | در متون فارسی/عربی/انگلیسی مخلوط، embedding ممکن است دقت کمتری داشته باشد | ### مثال عملی فرض کنید کاربر می‌پرسد: **"حکم روزه مسافر چیست؟"** Vector Search ممکن است این نتایج را برگرداند: 1. سندی درباره **احکام روزه مسافر** (مرتبط) 2. سندی درباره **احکام نماز مسافر** (شبیه اما نامرتبط) 3. سندی درباره **فضیلت روزه** (کلمه روزه دارد اما پاسخ سوال نیست) 4. سندی درباره **احکام روزه بیمار** (مشابه اما متفاوت) Reranker می‌تواند تشخیص دهد که سند ۱ دقیقاً به سوال پاسخ می‌دهد و بقیه را در اولویت پایین‌تر قرار دهد. ## معماری پایپ‌لاین ### نمای کلی ```mermaid graph TD A["سوال کاربر"] --> B["rag_injection_hook"] B --> C["build_rag_prompt"] subgraph "مرحله ۱: Vector Search" C --> D["Embedding سوال کاربر"] D --> E["جستجو در Qdrant"] E --> F["N=7 کاندید اولیه"] end subgraph "مرحله ۲: Reranking" F --> G["ارسال به Jina Reranker API"] G --> H["ارزیابی ارتباط هر کاندید با سوال"] H --> I["مرتب‌سازی بر اساس relevance_score"] I --> J["انتخاب top_n=3 نتیجه برتر"] end J --> K["ساخت context با منبع و امتیاز"] K --> L["تزریق به prompt کاربر"] L --> M["ارسال به LLM"] M --> N["پاسخ نهایی"] ``` ### جریان داده ``` سوال کاربر │ ▼ ┌─────────────────────────────┐ │ Pre-Hook: rag_injection_hook│ ← src/utils/hooks.py │ ورودی کاربر را دریافت و │ │ به build_rag_prompt پاس │ │ می‌دهد │ └─────────────┬───────────────┘ │ ▼ ┌─────────────────────────────┐ │ مرحله ۱: Vector Search │ ← src/utils/search_knowledge.py │ ───────────────────────── │ │ • Embedding سوال با │ │ EmbeddingFactory │ │ • جستجو در Qdrant │ │ • دریافت 7 کاندید اولیه │ └─────────────┬───────────────┘ │ ▼ ┌─────────────────────────────┐ │ مرحله ۲: Reranking │ ← src/utils/reranker.py │ ───────────────────────── │ │ • ارسال سوال + 7 کاندید │ │ به Jina Reranker API │ │ • دریافت relevance_score │ │ برای هر کاندید │ │ • انتخاب 3 کاندید برتر │ └─────────────┬───────────────┘ │ ▼ ┌─────────────────────────────┐ │ ساخت Context نهایی │ │ ───────────────────────── │ │ • افزودن منبع و امتیاز │ │ به هر سند │ │ • ترکیب با سوال کاربر │ │ • تزریق به RunInput │ └─────────────┬───────────────┘ │ ▼ Agent (LLM) ``` ## پیاده‌سازی ### مرحله ۱: نقطه ورود - Pre-Hook فایل: `src/utils/hooks.py` ```python @observe(name="rag_injection_hook") def rag_injection_hook(run_input: RunInput, **kwargs): """ Intercepts the user input and injects RAG context. """ original_input = run_input.input_content run_input.input_content = build_rag_prompt(original_input) ``` این hook قبل از اجرای Agent فراخوانی می‌شود. ورودی خام کاربر را می‌گیرد و آن را از پایپ‌لاین RAG عبور می‌دهد. نتیجه (prompt غنی‌شده با context) جایگزین ورودی اصلی می‌شود. **نکته**: تغییرات روی `run_input` به صورت in-place اعمال می‌شوند. نیازی به return نیست. ### مرحله ۲: Vector Search - بازیابی کاندیدهای اولیه فایل: `src/utils/search_knowledge.py` ```python def build_rag_prompt(user_question: str, embedder_model_name: str = None) -> str: global knowledge_base # Lazy initialization if knowledge_base is None: embed_factory = EmbeddingFactory() embedder = embed_factory.get_embedder(embedder_model_name) vector_db = get_qdrant_store(embedder=embedder) knowledge_base = Knowledge(vector_db=vector_db) # جستجوی اولیه: دریافت 7 کاندید نزدیک‌ترین initial_results = knowledge_base.search(query=user_question, max_results=7) ``` #### چرا `max_results=7`؟ عدد 7 یک تعادل بین دو نیاز است: - **پوشش کافی**: اگر تعداد کاندیدها خیلی کم باشد (مثلاً 3)، ممکن است بهترین نتیجه در بین آن‌ها نباشد - **سرعت و هزینه**: اگر تعداد خیلی زیاد باشد (مثلاً 50)، هزینه و زمان Reranking بالا می‌رود با 7 کاندید اولیه، Reranker فضای کافی برای انتخاب 3 نتیجه واقعاً مرتبط دارد. #### Lazy Initialization Knowledge Base فقط یک‌بار مقداردهی می‌شود (در اولین درخواست) و سپس در یک متغیر global نگهداری می‌شود. این کار از ایجاد اتصال مجدد به Qdrant در هر درخواست جلوگیری می‌کند. ### مرحله ۳: Reranking - فیلتر هوشمند نتایج فایل: `src/utils/reranker.py` ```python def rerank_documents(query: str, documents: List[Any], top_n: int = 3) -> List[Any]: # 1. استخراج محتوای متنی اسناد doc_contents = [doc.content for doc in documents] # 2. ارسال به Jina Reranker API payload = { "model": "jina-reranker-v3", "query": query, "documents": doc_contents, "top_n": top_n } response = requests.post(url, headers=headers, json=payload) results = response.json()["results"] # 3. بازسازی لیست اسناد با امتیاز جدید reranked_docs = [] for result in results: original_index = result["index"] relevance_score = result["relevance_score"] doc = documents[original_index] doc.meta_data["rerank_score"] = relevance_score reranked_docs.append(doc) return reranked_docs ``` #### مدل Reranker: `jina-reranker-v3` از مدل **Jina Reranker v3** استفاده می‌کنیم. دلایل انتخاب این مدل: | ویژگی | توضیح | |-------|-------| | **پشتیبانی چند‌زبانه** | عملکرد مناسب روی متون فارسی، عربی و انگلیسی | | **درک سوال-جواب** | بر خلاف embedding، ارتباط بین سوال و پاسخ را می‌فهمد (Cross-Encoder) | | **سرعت** | برای تعداد کم اسناد (7 کاندید) بسیار سریع است | | **API ساده** | فقط یک HTTP POST call نیاز دارد | #### تفاوت Reranker با Embedding | | Embedding (Bi-Encoder) | Reranker (Cross-Encoder) | |--|----------------------|------------------------| | **ورودی** | هر متن جداگانه encode می‌شود | سوال و سند با هم encode می‌شوند | | **خروجی** | بردار (vector) | امتیاز ارتباط (relevance score) | | **سرعت** | سریع (مناسب جستجوی میلیون‌ها سند) | کندتر (مناسب ده‌ها سند) | | **دقت** | متوسط | بالا | | **کاربرد** | مرحله اول: غربال‌گری سریع | مرحله دوم: انتخاب دقیق | به زبان ساده: - **Embedding** مثل خواندن سریع عنوان کتاب‌ها در کتابخانه و انتخاب چند کتاب مرتبط است - **Reranker** مثل ورق زدن آن چند کتاب و انتخاب بهترین‌ها بر اساس محتوای واقعی #### نحوه کار Jina API Jina یک لیست از اسناد و یک سوال دریافت می‌کند. برای هر سند، یک `relevance_score` (بین 0 تا 1) و `index` (شماره سند در لیست اصلی) برمی‌گرداند: ```json { "results": [ { "index": 2, "relevance_score": 0.92 }, { "index": 0, "relevance_score": 0.78 }, { "index": 5, "relevance_score": 0.65 } ] } ``` سپس ما با استفاده از `index`، سند اصلی را از لیست اولیه پیدا کرده و `relevance_score` را در `meta_data` آن ذخیره می‌کنیم. ### مرحله ۴: ساخت Context نهایی پس از reranking، اسناد برتر به همراه اطلاعات منبع و امتیاز ارتباط به یک context تبدیل می‌شوند: ```python context_parts = [] for doc in relevant_docs: meta = getattr(doc, "meta_data", {}) or {} source = meta.get('source', 'Unknown') score = meta.get('rerank_score', 0) content = f"[Source: {source} | Relevance: {score:.2f}]\n{doc.content}" context_parts.append(content) context_str = "\n\n".join(context_parts) ``` هر سند در context نهایی شامل: - **Source**: منبع سند (مثلاً نام کتاب یا فایل) - **Relevance**: امتیاز ارتباط از Reranker (0 تا 1) - **Content**: محتوای اصلی سند ### مرحله ۵: Prompt نهایی ```python final_prompt = ( "Here is the context from the database:\n" "---------------------\n" f"{context_str}\n" "---------------------\n" f"User Question: {user_question}" ) ``` این prompt به LLM داده می‌شود. مدل با دیدن context مرتبط و سوال کاربر، پاسخ مناسب تولید می‌کند. ## مدیریت خطا و Fallback ### سطح ۱: خطا در Reranker اگر Jina API در دسترس نباشد یا خطا دهد، سیستم به ترتیب اصلی Vector Search بازمی‌گردد: ```python except Exception as e: print(f"❌ Reranking failed: {e}. Falling back to vector search order.") return documents[:top_n] ``` ### سطح ۲: عدم وجود API Key اگر `JINA_API_KEY` تنظیم نشده باشد، بدون reranking ادامه می‌دهد: ```python api_key = os.getenv("JINA_API_KEY") if not api_key: print("⚠️ JINA_API_KEY not found. Returning original order.") return documents[:top_n] ``` ### سطح ۳: خطا در Knowledge Base اگر کل Knowledge Base در دسترس نباشد، به مدل اطلاع داده می‌شود که پاسخ ندهد: ```python except Exception as e: context_str = "Knowledge base temporarily unavailable. dont answer the question ..." ``` ### خلاصه زنجیره Fallback ``` Reranking موفق → 3 سند با بالاترین ارتباط │ (خطا) ▼ Fallback → 3 سند اول از Vector Search (بدون reranking) │ (خطا) ▼ Fallback → پیام عدم دسترسی به Knowledge Base ``` ## تنظیمات و پارامترها ### متغیرهای محیطی | متغیر | توضیح | الزامی | |--------|--------|--------| | `JINA_API_KEY` | کلید API برای Jina Reranker | بله (بدون آن فقط Vector Search) | | `QDRANT_URL` | آدرس سرور Qdrant | بله | ### پارامترهای قابل تنظیم | پارامتر | مقدار فعلی | محل تعریف | توضیح | |----------|-----------|-----------|-------| | `max_results` | 7 | `search_knowledge.py` | تعداد کاندیدهای اولیه از Vector Search | | `top_n` | 3 | `search_knowledge.py` → `rerank_documents()` | تعداد نتایج نهایی پس از Reranking | | `model` | `jina-reranker-v3` | `reranker.py` | مدل Reranker مورد استفاده | ### توصیه برای تنظیم پارامترها - نسبت `max_results` به `top_n` باید حداقل 2:1 باشد تا Reranker فضای کافی برای انتخاب داشته باشد - افزایش `max_results` دقت را بالا می‌برد اما سرعت و هزینه API را افزایش می‌دهد - `top_n=3` برای اکثر سناریوها مناسب است؛ تعداد بیشتر ممکن است context را شلوغ کند ## فایل‌های مرتبط | فایل | مسئولیت | |------|---------| | `src/utils/hooks.py` | Pre-Hook برای تزریق RAG به ورودی Agent | | `src/utils/search_knowledge.py` | پایپ‌لاین اصلی RAG: جستجو، reranking و ساخت prompt | | `src/utils/reranker.py` | ارتباط با Jina Reranker API و مرتب‌سازی نتایج | | `src/knowledge/embedding_factory.py` | ساخت و مدیریت مدل‌های Embedding | | `src/knowledge/vector_store.py` | اتصال به Qdrant و مدیریت collection | ## نتیجه‌گیری پایپ‌لاین دو مرحله‌ای **Vector Search + Reranking** بهترین تعادل بین سرعت و دقت را فراهم می‌کند: - **مرحله اول (Vector Search)** سریع است و از بین میلیون‌ها سند، تعداد محدودی کاندید مرتبط انتخاب می‌کند - **مرحله دوم (Reranking)** دقیق است و با درک عمیق‌تر ارتباط سوال-جواب، بهترین اسناد را فیلتر می‌کند این ترکیب باعث می‌شود context ارسالی به LLM با کیفیت بالا باشد و در نتیجه پاسخ‌های دقیق‌تر و مرتبط‌تری تولید شود.