Hybrid_Search چیست؟ آموزش ترکیب BM25 و Vector Search برای افزایش دقت RAG

در این راهنمای فنی، Hybrid_Search را با ترکیب BM25 و Vector_Search پیاده‌سازی می‌کنیم و با RRF، نرمال‌سازی امتیاز، ارزیابی Retrieval و اتصال به RAG دقت بازیابی اسناد را افزایش می‌دهیم.

Share
Hybrid_Search چیست؟ آموزش ترکیب BM25 و Vector Search برای افزایش دقت RAG

Hybrid_Search یا جست‌وجوی ترکیبی روشی برای بازیابی اطلاعات است که نتایج جست‌وجوی کلمه‌محور را با نتایج جست‌وجوی معنایی ترکیب می‌کند.

در بیشتر پیاده‌سازی‌ها، Hybrid_Search از دو Retriever اصلی تشکیل می‌شود:

  • جست‌وجوی Lexical یا واژگانی با الگوریتم‌هایی مانند BM25
  • جست‌وجوی Semantic یا معنایی با Embedding و Vector_Sarch

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

هیچ‌کدام به‌تنهایی برای همه Queryها مناسب نیستند.

برای مثال، کاربر می‌پرسد:

چرا درخواست‌های API بعد از مدتی پاسخ نمی‌دهند؟

در سند ممکن است نوشته شده باشد:

برای جلوگیری از انتظار نامحدود، مقدار Request Timeout را تنظیم کنید.

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

اما اگر کاربر جست‌وجو کند:

ERR_RATE_LIMIT_429

جست‌وجوی BM25 معمولاً بهتر از مدل Embedding عمل می‌کند؛ زیرا تطبیق دقیق کد خطا اهمیت بیشتری از شباهت معنایی دارد.

Hybrid Search تلاش می‌کند مزایای هر دو روش را در یک رتبه‌بندی نهایی ترکیب کند.

Hybrid_Search چیست؟

در Hybrid_Search یک Query هم‌زمان به چند سیستم Retrieval ارسال می‌شود. هر سیستم فهرستی از اسناد مرتبط تولید می‌کند و سپس یک Fusion Algorithm نتایج را ترکیب و مرتب می‌کند.

معماری ساده:

User Query
   ├── BM25 / Lexical Retrieval
   │       ↓
   │   Ranked List A
   │
   └── Embedding / Vector Retrieval
           ↓
       Ranked List B
           ↓
       Rank Fusion
           ↓
     Final Ranked List

فهرست نهایی می‌تواند مستقیماً به کاربر نمایش داده شود یا به‌عنوان Context وارد یک سیستم RAG شود.

Elasticsearch، Hybrid_Search را ترکیب دقت جست‌وجوی Lexical با انعطاف جست‌وجوی Semantic معرفی می‌کند و استفاده از Reciprocal Rank Fusion یا RRF را یکی از روش‌های اصلی ترکیب نتایج می‌داند. مستندات Hybrid Search در Elasticsearch

چرا Vector_Search به‌تنهایی کافی نیست؟

Vector Search مفهوم متن را با تبدیل Query و Document به بردار عددی مقایسه می‌کند. این روش برای مترادف‌ها، بازنویسی و پرسش‌های طبیعی مناسب است؛ اما در بعضی Queryها ضعف دارد.

شناسه‌ها و کدهای دقیق

payment_failed_1042
gpt-model-v3.2
SKU-IR-9871

مدل Embedding ممکن است این رشته‌ها را به‌خوبی از یکدیگر متمایز نکند.

نام‌های خاص

نام محصول، شرکت، تابع، متغیر یا Endpoint ممکن است در داده آموزشی مدل Embedding وجود نداشته باشد.

نسخه‌ها و اعداد

تفاوت میان این دو عبارت می‌تواند مهم باشد:

Python 3.12
Python 3.13

اما Vector_Search ممکن است آن‌ها را بسیار مشابه ارزیابی کند.

عبارت‌های کوتاه

Queryهای بسیار کوتاه مانند موارد زیر Context معنایی کافی ندارند:

billing
timeout
embedding

اصطلاح جدید

اگر اصطلاحی پس از آموزش Embedding Model ایجاد شده باشد، نمایش برداری آن ممکن است کیفیت مناسبی نداشته باشد.

چرا BM25 به‌تنهایی کافی نیست؟

BM25 براساس حضور و تکرار کلمات کار می‌کند و معنای جمله را مستقیماً درک نمی‌کند.

فرض کنید Query این باشد:

چطور هزینه استفاده از مدل را کمتر کنم؟

اما سند با این عنوان ذخیره شده باشد:

روش‌های بهینه‌سازی مصرف Token در API هوش مصنوعی

از نظر معنایی این دو مرتبط‌اند، اما ممکن است کلمات مشترک کمی داشته باشند.

مشکلات BM25:

  • مترادف‌ها را ذاتاً نمی‌شناسد
  • بازنویسی معنایی را درک نمی‌کند
  • نسبت به تفاوت شکل نوشتاری حساس است
  • Query محاوره‌ای را به متن رسمی وصل نمی‌کند
  • ارتباط مفهومی بدون واژه مشترک را از دست می‌دهد
  • برای جست‌وجوی چندزبانه محدود است

Hybrid_Search برای پوشش ضعف متقابل این دو Retriever ساخته می‌شود.

Sparse Retrieval چیست؟

Sparse Retrieval اسناد و Queryها را به نمایش‌هایی تبدیل می‌کند که بیشتر ابعاد آن‌ها صفر است. هر بعد معمولاً به یک Term یا ویژگی واژگانی مربوط می‌شود.

روش‌های رایج Sparse Retrieval:

  • TF-IDF
  • BM25
  • BM25F
  • Learned Sparse Retrieval
  • SPLADE
  • ELSER

در BM25، نمای Sparse براساس کلمات موجود در Corpus ساخته می‌شود. در روش‌های Learned Sparse، یک مدل می‌تواند Termهای مرتبط را نیز وزن‌دهی کند.

Dense Retrieval چیست؟

Dense Retrieval متن را با یک Embedding Model به برداری متراکم تبدیل می‌کند:

[
f(text) \rightarrow \mathbb{R}^{d}
]

برای مثال:

[
f(\text{Query}) =
[0.12, -0.08, 0.44, \ldots]
]

[
f(\text{Document}) =
[0.10, -0.11, 0.41, \ldots]
]

سپس شباهت Query و Document با معیارهایی مانند Cosine Similarity یا Dot Product محاسبه می‌شود.

Cosine Similarity:

[
\text{cosine}(q,d) =
\frac{q \cdot d}
{|q||d|}
]

هرچه مقدار شباهت بیشتر باشد، Document از نظر معنایی به Query نزدیک‌تر در نظر گرفته می‌شود.

BM25 چیست؟

BM25 مخفف Best Matching 25 است و یکی از رایج‌ترین الگوریتم‌های رتبه‌بندی در Information Retrieval محسوب می‌شود.

نسخه ساده‌شده امتیاز BM25 برای Document (D) و Query (Q):

[
score(D,Q)=
\sum_{q_i \in Q}
IDF(q_i)
\cdot
\frac{
f(q_i,D)(k_1+1)
}{
f(q_i,D)+k_1
\left(
1-b+b\frac{|D|}{avgdl}
\right)
}
]

در این فرمول:

  • (f(q_i,D)) تعداد تکرار Term در Document است
  • (|D|) طول Document است
  • (avgdl) میانگین طول اسناد Corpus است
  • (k_1) میزان اشباع Term Frequency را کنترل می‌کند
  • (b) میزان نرمال‌سازی طول Document را مشخص می‌کند
  • (IDF) اهمیت Term در کل Corpus را نشان می‌دهد

مفهوم IDF

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

فرم متداول IDF:

[
IDF(q_i)=
\log
\left(
1+
\frac{
N-n(q_i)+0.5
}{
n(q_i)+0.5
}
\right)
]

که در آن:

  • (N) تعداد کل اسناد است
  • (n(q_i)) تعداد اسنادی است که Term در آن‌ها دیده می‌شود

بنابراین یک شناسه نادر مانند ERR_429_BILLING می‌تواند وزن بیشتری از یک کلمه پرتکرار مانند «API» داشته باشد.

پارامترهای مهم BM25

پارامتر k1

این پارامتر اثر تکرار Term را کنترل می‌کند.

مقادیر رایج برای شروع:

1.2 تا 2.0

اگر k1 بزرگ‌تر باشد، تکرار بیشتر کلمه می‌تواند اثر بیشتری روی امتیاز داشته باشد.

پارامتر b

این پارامتر میزان اثر طول Document را تعیین می‌کند.

b = 0

یعنی طول Document نادیده گرفته شود.

b = 1

یعنی نرمال‌سازی طول به‌طور کامل اعمال شود.

مقدار متداول:

b = 0.75

این اعداد نقطه شروع هستند و باید براساس Evaluation Set تنظیم شوند.

تفاوت BM25 و Vector_Search

معیارBM25Vector Search
نوع جست‌وجوواژگانیمعنایی
نیاز به Embedding Modelندارددارد
تطبیق کلمه دقیقبسیار مناسبمتغیر
مترادف و بازنویسیضعیفمناسب
شناسه و کد خطامناسبضعیف‌تر
Query طبیعیمحدودمناسب
قابل‌توضیح بودنبیشترکمتر
هزینه Indexingکمتربیشتر
جست‌وجوی چندزبانهمحدودوابسته به مدل
سازگاری با داده جدیدسریعنیازمند Embedding
عملکرد روی اصطلاحات نادرمناسبمتغیر

معماری Hybrid_Search در RAG

در یک سیستم RAG، Hybrid Search در مرحله Retrieval قرار می‌گیرد:

Documents
   ↓
Cleaning and Normalization
   ↓
Chunking
   ├── BM25 Index
   └── Embedding Generation
           ↓
       Vector Index

User Query
   ├── Lexical Search
   └── Semantic Search
           ↓
       Rank Fusion
           ↓
       Optional Reranker
           ↓
       Context Selection
           ↓
           LLM
           ↓
       Final Answer

هر مرحله می‌تواند روی کیفیت پاسخ نهایی اثر بگذارد. Hybrid Search نمی‌تواند Chunking ضعیف، داده ناقص یا Prompt نامناسب را کاملاً جبران کند.

مرحله Indexing

برای هر Chunk بهتر است اطلاعات زیر ذخیره شوند:

{
  "id": "doc-42-chunk-3",
  "document_id": "doc-42",
  "title": "مدیریت خطای API",
  "content": "در پاسخ به خطای 429 باید Retry-After بررسی شود...",
  "section": "Rate Limit",
  "url": "/docs/api-errors",
  "language": "fa",
  "updated_at": "2026-07-01",
  "dense_vector": []
}

BM25 روی فیلدهای متنی Index می‌شود و Dense Vector در Vector Index قرار می‌گیرد.

شناسه Chunk باید در هر دو Index یکسان باشد تا نتایج به‌درستی ادغام شوند.

Chunk بسیار کوچک:

  • Context ناکافی
  • پاسخ ناقص
  • افزایش تعداد نتایج مشابه
  • وابستگی زیاد به Chunkهای مجاور

Chunk بسیار بزرگ:

  • کاهش تمرکز Embedding
  • افزایش Token مصرفی
  • کاهش دقت Retrieval
  • ادغام چند موضوع در یک بردار
  • سخت‌ترشدن رتبه‌بندی

برای متن‌های فنی می‌توانید از این نقطه شروع کنید:

Chunk Size: 300 تا 800 Token
Overlap: 50 تا 150 Token

این مقادیر عمومی هستند و باید براساس ساختار سند و Evaluation تنظیم شوند.

روش بهتر، Semantic یا Structure-aware Chunking است:

  • براساس عنوان
  • براساس Section
  • براساس پاراگراف
  • براساس تابع یا Class در کد
  • براساس Endpoint در مستندات API
  • براساس سؤال و جواب
  • براساس Topic Boundary

نرمال‌سازی متن فارسی

در جست‌وجوی فارسی، تفاوت Unicode می‌تواند BM25 را دچار مشکل کند.

نمونه‌های رایج:

ی / ي
ک / ك
هٔ / ه
فاصله / نیم‌فاصله
اعداد فارسی / انگلیسی

تابع ساده نرمال‌سازی:

import re
import unicodedata


PERSIAN_DIGITS = "۰۱۲۳۴۵۶۷۸۹"
ENGLISH_DIGITS = "0123456789"

DIGIT_MAP = str.maketrans(
    PERSIAN_DIGITS,
    ENGLISH_DIGITS,
)


def normalize_persian(text: str) -> str:
    text = unicodedata.normalize("NFKC", text)

    replacements = {
        "ي": "ی",
        "ك": "ک",
        "ۀ": "ه",
        "ة": "ه",
        "\u200c": " ",
        "\u200f": " ",
        "\u200e": " ",
    }

    for source, target in replacements.items():
        text = text.replace(source, target)

    text = text.translate(DIGIT_MAP)
    text = text.lower()
    text = re.sub(r"\s+", " ", text)

    return text.strip()

برای BM25 ممکن است بخواهید نیم‌فاصله را به فاصله تبدیل کنید تا واژه‌هایی مانند این‌ها مشابه پردازش شوند:

می‌شود
می شود
مي‌شود

اما متن اصلی را برای نمایش و ارسال به LLM نگه دارید. نسخه نرمال‌شده فقط برای Indexing و Search استفاده شود.

Tokenization فارسی برای BM25

ساده‌ترین Tokenizer:

import re


TOKEN_PATTERN = re.compile(
    r"[A-Za-z0-9_\-./:]+|[\u0600-\u06FF]+"
)


def tokenize(text: str) -> list[str]:
    normalized = normalize_persian(text)
    return TOKEN_PATTERN.findall(normalized)

این Tokenizer رشته‌های فنی مانند موارد زیر را حفظ می‌کند:

ERR_429
/api/v1/chat/completions
gpt-model-v2
Python-3.13

حذف بی‌هدف علامت‌ها ممکن است شناسه‌های فنی را تخریب کند.

Stopword حذف کنیم یا نه؟

در جست‌وجوی سنتی، کلمات پرتکرار حذف می‌شوند؛ اما حذف گسترده Stopword همیشه مناسب نیست.

عبارت‌های زیر را مقایسه کنید:

مدل با Streaming
مدل بدون Streaming

اگر «با» و «بدون» به‌عنوان Stopword حذف شوند، معنی Query تغییر می‌کند.

پیشنهاد عملی:

  • ابتدا بدون Stopword Removal یک Baseline بسازید.
  • فقط Termهای واقعاً بی‌اثر را براساس Corpus حذف کنید.
  • فهرست Stopword را Version کنید.
  • تغییر آن را با معیار Retrieval ارزیابی کنید.
  • کلمات منفی‌ساز را حذف نکنید.

تولید Embedding از طریق API درواره

برای Dense Retrieval به Embedding Model نیاز داریم. می‌توانید از مدل‌های Embedding موجود در پنل درواره استفاده کنید.

Base URL:

https://api.darvareh.ir/v1

نمونه Python:

import os
from openai import OpenAI


client = OpenAI(
    api_key=os.environ["DARVAREH_API_KEY"],
    base_url="https://api.darvareh.ir/v1",
)


def create_embeddings(
    texts: list[str],
) -> list[list[float]]:
    response = client.embeddings.create(
        model=os.environ["DARVAREH_EMBEDDING_MODEL_ID"],
        input=texts,
    )

    sorted_items = sorted(
        response.data,
        key=lambda item: item.index,
    )

    return [
        item.embedding
        for item in sorted_items
    ]

شناسه مدل را از فهرست مدل‌های فعال درواره بردارید. طول ورودی، Batch Size و ابعاد Embedding براساس مدل متفاوت است.

برای مشاهده مدل‌ها و دریافت API Key به درواره مراجعه کنید.

پیاده‌سازی Hybrid_Search با Python

در این پروژه از ابزارهای زیر استفاده می‌کنیم:

  • rank-bm25 برای BM25
  • numpy برای Vector Similarity
  • API درواره برای Embedding
  • RRF برای ادغام رتبه‌ها

نصب وابستگی‌ها:

pip install openai numpy rank-bm25

تعریف Corpus

documents = [
    {
        "id": "doc-1",
        "title": "مدیریت Timeout",
        "content": (
            "Timeout حداکثر زمان انتظار برای پاسخ سرویس "
            "خارجی را مشخص می‌کند. برای APIهای هوش مصنوعی "
            "باید زمان اتصال و زمان خواندن پاسخ جداگانه "
            "تنظیم شوند."
        ),
    },
    {
        "id": "doc-2",
        "title": "مدیریت خطای 429",
        "content": (
            "کد وضعیت HTTP 429 نشان می‌دهد تعداد درخواست‌ها "
            "از محدودیت تعیین‌شده بیشتر شده است. مقدار "
            "Retry-After و سیاست Backoff باید بررسی شوند."
        ),
    },
    {
        "id": "doc-3",
        "title": "کاهش هزینه Token",
        "content": (
            "برای کاهش هزینه API می‌توان تاریخچه مکالمه را "
            "خلاصه کرد، سقف خروجی تعیین کرد و پاسخ‌های "
            "تکراری را Cache کرد."
        ),
    },
    {
        "id": "doc-4",
        "title": "Streaming پاسخ",
        "content": (
            "در Streaming، توکن‌های خروجی به‌تدریج برای "
            "کاربر ارسال می‌شوند. در برنامه‌های وب معمولاً "
            "از Server-Sent Events استفاده می‌شود."
        ),
    },
    {
        "id": "doc-5",
        "title": "مدیریت Context",
        "content": (
            "برای جلوگیری از عبور تاریخچه گفتگو از Context "
            "Window باید پیام‌های قدیمی خلاصه یا براساس "
            "اهمیت انتخاب شوند."
        ),
    },
]

ساخت BM25 Index

from rank_bm25 import BM25Okapi


def document_text(document: dict) -> str:
    return (
        f"{document['title']} "
        f"{document['content']}"
    )


tokenized_corpus = [
    tokenize(document_text(document))
    for document in documents
]


bm25 = BM25Okapi(
    tokenized_corpus,
    k1=1.5,
    b=0.75,
)

اجرای BM25_Search

import numpy as np


def bm25_search(
    query: str,
    limit: int = 10,
) -> list[dict]:
    query_tokens = tokenize(query)
    scores = bm25.get_scores(query_tokens)

    ranked_indices = np.argsort(scores)[::-1]

    results = []

    for index in ranked_indices[:limit]:
        score = float(scores[index])

        if score <= 0:
            continue

        results.append({
            "id": documents[index]["id"],
            "score": score,
            "rank": len(results) + 1,
            "document": documents[index],
        })

    return results

ساخت Dense Vector Index

برای دیتاست کوچک، بردارها را در حافظه نگه می‌داریم. در Production باید از Vector Database یا موتور جست‌وجوی مناسب استفاده شود.

document_texts = [
    document_text(document)
    for document in documents
]


document_vectors = np.asarray(
    create_embeddings(document_texts),
    dtype=np.float32,
)

نرمال‌سازی بردارها:

def l2_normalize(
    matrix: np.ndarray,
) -> np.ndarray:
    norms = np.linalg.norm(
        matrix,
        axis=1,
        keepdims=True,
    )

    norms = np.maximum(norms, 1e-12)

    return matrix / norms


normalized_document_vectors = l2_normalize(
    document_vectors,
)

اگر بردارها Normalize شوند، Dot Product معادل Cosine Similarity خواهد بود.

def vector_search(
    query: str,
    limit: int = 10,
) -> list[dict]:
    query_vector = np.asarray(
        create_embeddings([query])[0],
        dtype=np.float32,
    )

    query_vector = query_vector / max(
        np.linalg.norm(query_vector),
        1e-12,
    )

    scores = (
        normalized_document_vectors
        @ query_vector
    )

    ranked_indices = np.argsort(scores)[::-1]

    results = []

    for index in ranked_indices[:limit]:
        results.append({
            "id": documents[index]["id"],
            "score": float(scores[index]),
            "rank": len(results) + 1,
            "document": documents[index],
        })

    return results

مشکل جمع مستقیم Scoreها

امتیاز BM25 و Cosine Similarity روی مقیاس یکسانی نیستند.

مثلاً:

BM25 Score: 12.8
Cosine Similarity: 0.73

اگر مستقیم جمع کنیم:

12.8 + 0.73

BM25 تقریباً همیشه بر نتیجه مسلط می‌شود.

حتی مقیاس BM25 در Queryهای مختلف ثابت نیست. یک Query ممکن است حداکثر Score برابر ۵ و Query دیگر حداکثر Score برابر ۳۰ داشته باشد.

بنابراین ترکیب خام زیر اشتباه است:

final_score = bm25_score + vector_score

Qdrant نیز در مستندات Hybrid Query توضیح می‌دهد که ترکیب خطی امتیازهای Dense و Sparse بدون نرمال‌سازی قابل‌اعتماد نیست؛ زیرا مقیاس آن‌ها متفاوت و وابسته به Query است. مستندات Hybrid Query در Qdrant

روش‌های Fusion

روش‌های متداول ادغام نتایج:

  • Reciprocal Rank Fusion
  • Weighted RRF
  • Min-Max Normalization
  • Z-Score Normalization
  • Distribution-Based Score Fusion
  • Weighted Linear Combination
  • Learned-to-Rank
  • Reranking

RRF چیست؟

RRF یا Reciprocal Rank Fusion براساس رتبه Document در هر فهرست کار می‌کند و امتیاز خام Retrieverها را نادیده می‌گیرد.

فرمول RRF:

[
RRF(d)=
\sum_{r \in R}
\frac{w_r}{k+rank_r(d)}
]

در این رابطه:

  • (d) یک Document است
  • (R) مجموعه Retrieverهاست
  • (rank_r(d)) رتبه Document در Retriever است
  • (w_r) وزن Retriever است
  • (k) ثابت RRF است

اگر سندی در هر دو فهرست رتبه بالایی داشته باشد، امتیاز نهایی بیشتری دریافت می‌کند.

RRF به هماهنگ‌کردن Scoreهای BM25 و Vector Search نیاز ندارد. به همین دلیل نقطه شروع مناسبی برای Hybrid Search است. Elasticsearch نیز RRF را روشی معرفی می‌کند که می‌تواند مجموعه نتایج با سیگنال‌های Relevance متفاوت را بدون وابستگی به مقیاس Score ترکیب کند. مستندات RRF در Elasticsearch

مثال ساده RRF

نتایج BM25:

1. Document A
2. Document B
3. Document C

نتایج Vector Search:

1. Document C
2. Document A
3. Document D

با فرض (k=60):

A = 1/(60+1) + 1/(60+2)
C = 1/(60+3) + 1/(60+1)
B = 1/(60+2)
D = 1/(60+3)

A و C به دلیل حضور در هر دو فهرست، امتیاز بیشتری می‌گیرند.

پیاده‌سازی RRF با Python

from collections import defaultdict


def reciprocal_rank_fusion(
    result_lists: list[list[dict]],
    limit: int = 10,
    k: int = 60,
    weights: list[float] | None = None,
) -> list[dict]:
    if weights is None:
        weights = [1.0] * len(result_lists)

    if len(weights) != len(result_lists):
        raise ValueError(
            "weights length must match result_lists"
        )

    fused_scores = defaultdict(float)
    documents_by_id = {}

    for results, weight in zip(
        result_lists,
        weights,
        strict=True,
    ):
        for rank, result in enumerate(
            results,
            start=1,
        ):
            document_id = result["id"]

            fused_scores[document_id] += (
                weight / (k + rank)
            )

            documents_by_id[document_id] = (
                result["document"]
            )

    ranked_ids = sorted(
        fused_scores,
        key=fused_scores.get,
        reverse=True,
    )

    return [
        {
            "id": document_id,
            "score": fused_scores[document_id],
            "rank": rank,
            "document": documents_by_id[
                document_id
            ],
        }
        for rank, document_id in enumerate(
            ranked_ids[:limit],
            start=1,
        )
    ]

تابع کامل Hybrid_Search

def hybrid_search(
    query: str,
    lexical_limit: int = 20,
    vector_limit: int = 20,
    final_limit: int = 5,
    bm25_weight: float = 1.0,
    vector_weight: float = 1.0,
) -> list[dict]:
    lexical_results = bm25_search(
        query=query,
        limit=lexical_limit,
    )

    semantic_results = vector_search(
        query=query,
        limit=vector_limit,
    )

    return reciprocal_rank_fusion(
        result_lists=[
            lexical_results,
            semantic_results,
        ],
        weights=[
            bm25_weight,
            vector_weight,
        ],
        limit=final_limit,
        k=60,
    )

استفاده:

results = hybrid_search(
    query="چطور مصرف توکن درخواست‌ها را کم کنم؟",
    lexical_limit=20,
    vector_limit=20,
    final_limit=3,
)

for result in results:
    print(
        result["rank"],
        result["document"]["title"],
        result["score"],
    )

Weighted RRF

همیشه قدرت BM25 و Dense Retrieval یکسان نیست.

برای Queryهای طبیعی ممکن است Dense Retrieval بهتر باشد:

weights=[
    1.0,  # BM25
    1.5,  # Vector
]

برای Corpus دارای کد، شناسه و نام محصول، BM25 ممکن است مهم‌تر باشد:

weights=[
    1.5,  # BM25
    1.0,  # Vector
]

وزن را با حدس انتخاب نکنید. یک Evaluation Set بسازید، بخشی را برای تنظیم وزن و بخشی را برای Validation نگه دارید.

Qdrant نیز برای Weighted RRF پیشنهاد می‌کند وزن‌ها روی Queryهای دارای Document مرتبط تنظیم و روی بخش جداگانه‌ای اعتبارسنجی شوند. راهنمای RRF در Qdrant

ترکیب خطی با نرمال‌سازی

روش دیگر، نرمال‌کردن Scoreها و سپس ترکیب آن‌هاست:

[
FinalScore(d)=
\alpha S_{dense}(d)
+
(1-\alpha)S_{sparse}(d)
]

که در آن:

  • (S_{dense}) امتیاز نرمال‌شده Dense است
  • (S_{sparse}) امتیاز نرمال‌شده BM25 است
  • (\alpha) وزن Dense Retrieval است

Min-Max Normalization:

[
\hat{x}=
\frac{x-x_{min}}
{x_{max}-x_{min}}
]

پیاده‌سازی:

def min_max_normalize(
    values: dict[str, float],
) -> dict[str, float]:
    if not values:
        return {}

    minimum = min(values.values())
    maximum = max(values.values())

    if maximum == minimum:
        return {
            key: 1.0
            for key in values
        }

    return {
        key: (value - minimum)
        / (maximum - minimum)
        for key, value in values.items()
    }

مشکل Min-Max این است که به Outlier و تعداد Candidateها حساس است. یک نتیجه با Score بسیار بالا می‌تواند بقیه امتیازها را فشرده کند.

برای شروع، RRF معمولاً ساده‌تر و مقاوم‌تر است. Elastic نیز پیشنهاد می‌کند ابتدا با RRF شروع و زمانی به ترکیب خطی بروید که Evaluation Set و نیاز به کنترل دقیق وزن‌ها داشته باشید. مقایسه روش‌های Fusion در Elastic

DBSF چیست؟

DBSF یا Distribution-Based Score Fusion ابتدا توزیع Score هر Retriever را جداگانه نرمال و سپس نتایج را ترکیب می‌کند.

Qdrant برای هر مجموعه نتیجه، میانگین و انحراف معیار را محاسبه می‌کند و Scoreها را براساس محدوده سه انحراف معیار نرمال می‌کند:

[
\hat{s}=
\frac{s-(\mu-3\sigma)}
{6\sigma}
]

DBSF زمانی مفید است که مقدار Scoreهای Retrieverها اطلاعات معناداری داشته باشد. در مقابل، RRF فقط ترتیب را در نظر می‌گیرد.

انتخاب میان RRF، DBSF و Linear Fusion باید با Evaluation انجام شود.

Dynamic Query Routing

می‌توان وزن Retrieverها را براساس نوع Query تغییر داد.

Queryهای Identifier-heavy

ERR_API_429
SKU-2026-IR
/api/v1/embeddings

وزن بیشتر برای BM25.

Queryهای طبیعی

چطور پاسخ‌های مشابه مدل را ذخیره کنم تا هزینه کمتر شود؟

وزن بیشتر برای Dense Retrieval.

Classifier ساده:

import re


IDENTIFIER_PATTERN = re.compile(
    r"""
    (
        [A-Z]{2,}[A-Z0-9_\-]+
        |
        /[A-Za-z0-9_\-./]+
        |
        \b\d{3,}\b
        |
        [A-Za-z]+[_\-][A-Za-z0-9_\-]+
    )
    """,
    re.VERBOSE,
)


def retrieval_weights(
    query: str,
) -> tuple[float, float]:
    if IDENTIFIER_PATTERN.search(query):
        return 1.7, 1.0

    if len(tokenize(query)) <= 2:
        return 1.4, 1.0

    return 1.0, 1.3

استفاده:

bm25_weight, vector_weight = (
    retrieval_weights(query)
)

results = hybrid_search(
    query=query,
    bm25_weight=bm25_weight,
    vector_weight=vector_weight,
)

این Ruleها باید با داده واقعی ارزیابی شوند. در سیستم بزرگ‌تر می‌توان Query Classifier آموزش داد.

افزودن Metadata Filter

فیلتر باید پیش از Fusion یا در هر دو Retriever اعمال شود.

نمونه Filter:

{
  "language": "fa",
  "product": "api",
  "document_type": "documentation",
  "version": "v2"
}

اگر BM25 روی اسناد فارسی نسخه ۲ جست‌وجو کند اما Vector_Search روی کل Corpus اجرا شود، نتایج ناسازگار خواهند بود.

فیلترهای رایج:

  • زبان
  • Tenant
  • محصول
  • نسخه
  • تاریخ
  • نوع سند
  • دسته‌بندی
  • وضعیت انتشار
  • منبع
  • سطح دسترسی محصول

Parent-Child Retrieval

گاهی Chunk مناسب Embedding برای ارسال به LLM بیش‌ازحد کوتاه است.

راهکار Parent-Child:

  1. Chunk کوچک را برای Retrieval Index کنید.
  2. شناسه Parent Section را ذخیره کنید.
  3. پس از انتخاب Chunk، بخش بزرگ‌تر Parent را واکشی کنید.
  4. Parent را به LLM ارسال کنید.

ساختار:

{
  "chunk_id": "chunk-42",
  "parent_id": "section-8",
  "content": "بخش کوتاه مناسب Retrieval"
}

پس از Retrieval:

chunk-42
   ↓
parent_id = section-8
   ↓
بخش کامل برای Context

این روش می‌تواند Recall دقیق Chunk کوچک را با Context کامل Parent ترکیب کند.

حذف نتایج تکراری

Overlap میان Chunkها ممکن است چند نتیجه تقریباً یکسان تولید کند.

روش‌های Deduplication:

  • حذف Chunkهای دارای document_id یکسان
  • محدودکردن تعداد Chunk از هر Document
  • مقایسه هم‌پوشانی متن
  • Maximal Marginal Relevance
  • انتخاب Parent Section
  • خوشه‌بندی Candidateها

نمونه ساده:

def limit_per_document(
    results: list[dict],
    max_per_document: int = 2,
) -> list[dict]:
    counts = {}
    filtered = []

    for result in results:
        document_id = result["document"].get(
            "document_id",
            result["id"],
        )

        count = counts.get(document_id, 0)

        if count >= max_per_document:
            continue

        counts[document_id] = count + 1
        filtered.append(result)

    return filtered

افزودن Reranker

Hybrid_Search مرحله Candidate Generation را بهبود می‌دهد. برای رتبه‌بندی دقیق‌تر می‌توان Top Candidateها را به Reranker داد.

Pipeline:

BM25 Top 50
      +
Vector Top 50
      ↓
RRF Top 30
      ↓
Cross-Encoder Reranker
      ↓
Final Top 5

Retriever باید Recall بالا داشته باشد. Reranker سپس Precision نتایج بالایی را افزایش می‌دهد.

Reranker نسبت به Vector Search سنگین‌تر است؛ بنابراین نباید روی کل Corpus اجرا شود.

این موضوع را می‌توان در مقاله مستقلی به‌صورت کامل بررسی کرد.

اتصال Hybrid_Search به LLM

پس از Retrieval، Context را بسازید:

def build_context(
    results: list[dict],
) -> str:
    sections = []

    for index, result in enumerate(
        results,
        start=1,
    ):
        document = result["document"]

        sections.append(
            "\n".join([
                f"[Source {index}]",
                f"Title: {document['title']}",
                f"Content: {document['content']}",
            ])
        )

    return "\n\n".join(sections)

فراخوانی مدل:

def answer_with_rag(
    query: str,
) -> str:
    results = hybrid_search(
        query=query,
        lexical_limit=30,
        vector_limit=30,
        final_limit=5,
    )

    context = build_context(results)

    response = client.chat.completions.create(
        model=os.environ["DARVAREH_CHAT_MODEL_ID"],
        messages=[
            {
                "role": "system",
                "content": (
                    "به پرسش فقط براساس Context پاسخ بده. "
                    "اگر پاسخ در Context وجود ندارد، "
                    "صریحاً اعلام کن اطلاعات کافی نیست. "
                    "شماره Sourceهای استفاده‌شده را ذکر کن."
                ),
            },
            {
                "role": "user",
                "content": (
                    f"Question:\n{query}\n\n"
                    f"Context:\n{context}"
                ),
            },
        ],
        temperature=0,
    )

    return response.choices[0].message.content

برای شروع استفاده از مدل‌های متن و Embedding از طریق یک API استاندارد، می‌توانید در درواره ثبت‌نام کنید.

پیاده‌سازی Hybrid_Search با Qdrant

Qdrant امکان ذخیره چند Named Vector و اجرای Queryهای چندمرحله‌ای را فراهم می‌کند. یک Document می‌تواند هم Dense Vector و هم Sparse Vector داشته باشد.

مفهوم کلی:

{
  "id": 42,
  "vector": {
    "dense": [0.1, 0.2, 0.3],
    "sparse": {
      "indices": [12, 98, 104],
      "values": [0.8, 1.4, 0.6]
    }
  },
  "payload": {
    "title": "مدیریت Token",
    "content": "..."
  }
}

Query ترکیبی:

{
  "prefetch": [
    {
      "query": {
        "indices": [12, 104],
        "values": [0.9, 0.7]
      },
      "using": "sparse",
      "limit": 30
    },
    {
      "query": [0.11, 0.24, 0.37],
      "using": "dense",
      "limit": 30
    }
  ],
  "query": {
    "rrf": {
      "k": 60
    }
  },
  "limit": 10
}

Qdrant از prefetch برای اجرای Retrieverهای مختلف و از Fusion برای ترکیب نتایج استفاده می‌کند. راهنمای رسمی Hybrid Search در Qdrant

Syntax دقیق براساس نسخه Qdrant و Client ممکن است تغییر کند؛ بنابراین مستندات نسخه نصب‌شده را بررسی کنید.

Hybrid_Search در Elasticsearch

در Elasticsearch می‌توان جست‌وجوی Keyword و Semantic را در یک Request اجرا کرد و با RRF ترکیب کرد.

ساختار مفهومی:

GET documents/_search
{
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "multi_match": {
                "query": "کاهش هزینه API",
                "fields": [
                  "title^3",
                  "content"
                ]
              }
            }
          }
        },
        {
          "knn": {
            "field": "content_vector",
            "query_vector": [0.1, 0.2, 0.3],
            "k": 30,
            "num_candidates": 100
          }
        }
      ],
      "rank_window_size": 50,
      "rank_constant": 60
    }
  },
  "size": 10
}

در این مثال:

  • عنوان سه برابر Boost شده است.
  • BM25 روی title و content اجرا می‌شود.
  • kNN روی content_vector اجرا می‌شود.
  • RRF رتبه‌ها را ترکیب می‌کند.
  • فقط ۱۰ نتیجه نهایی بازگردانده می‌شود.

Syntax نهایی باید با نسخه Elasticsearch شما تطبیق داده شود.

تنظیم Boost فیلدها

همه فیلدها اهمیت یکسانی ندارند.

مثال:

title^4
heading^2
content^1
tags^3

اگر Query دقیقاً در عنوان دیده شود، احتمالاً سند مرتبط‌تر است.

اما Boost زیاد عنوان ممکن است اسنادی با عنوان مشابه و محتوای نامرتبط را بالا بیاورد. Boostها را براساس Evaluation تنظیم کنید.

بدون Evaluation نمی‌توان گفت Hybrid_Search بهتر شده است.

دیتاست ارزیابی:

{
  "query": "چطور هزینه توکن را کم کنم؟",
  "relevant_document_ids": [
    "doc-3",
    "doc-8"
  ]
}

بهتر است Relevance چندسطحی باشد:

{
  "query": "مدیریت خطای 429",
  "judgments": {
    "doc-2": 3,
    "doc-9": 2,
    "doc-11": 1
  }
}

سطوح:

3 = کاملاً مرتبط
2 = مرتبط
1 = تا حدی مرتبط
0 = نامرتبط

Recall@K

Recall@K بررسی می‌کند چه درصدی از اسناد مرتبط در K نتیجه اول بازیابی شده‌اند:

[
Recall@K =
\frac{
RelevantDocumentsRetrieved@K
}{
TotalRelevantDocuments
}
]

اگر سه سند مرتبط وجود داشته باشد و دو مورد در Top 5 بازیابی شوند:

[
Recall@5 = \frac{2}{3}
]

برای RAG، Recall اهمیت زیادی دارد؛ زیرا اگر سند درست بازیابی نشود، LLM به آن دسترسی نخواهد داشت.

Precision@K

[
Precision@K =
\frac{
RelevantDocumentsRetrieved@K
}{
K
}
]

اگر از پنج نتیجه، سه مورد مرتبط باشند:

[
Precision@5 = \frac{3}{5}
]

MRR

Mean Reciprocal Rank رتبه اولین نتیجه مرتبط را اندازه‌گیری می‌کند:

[
RR =
\frac{1}
{rank_{first\ relevant}}
]

اگر اولین سند مرتبط در رتبه دوم باشد:

[
RR = \frac{1}{2}
]

MRR میانگین Reciprocal Rank روی همه Queryهاست.

nDCG

nDCG زمانی مفید است که Relevance چندسطحی باشد. این معیار هم مرتبط‌بودن و هم جایگاه نتایج را در نظر می‌گیرد.

سند کاملاً مرتبط در رتبه اول ارزش بیشتری از سند تاحدی مرتبط در رتبه پنجم دارد.

مقایسه Retrieverها

جدول ارزیابی:

روشRecall@5Recall@10MRRnDCG@10
BM250.680.770.710.70
Vector0.740.820.730.75
Hybrid RRF0.820.890.810.84
Hybrid + Reranker0.840.910.870.89

اعداد بالا صرفاً نمونه‌اند. ارزیابی واقعی باید روی Corpus و Queryهای محصول شما انجام شود.

کد ساده Recall@K

def recall_at_k(
    retrieved_ids: list[str],
    relevant_ids: set[str],
    k: int,
) -> float:
    if not relevant_ids:
        return 0.0

    retrieved_at_k = set(
        retrieved_ids[:k]
    )

    relevant_retrieved = (
        retrieved_at_k & relevant_ids
    )

    return (
        len(relevant_retrieved)
        / len(relevant_ids)
    )

کد MRR

def reciprocal_rank(
    retrieved_ids: list[str],
    relevant_ids: set[str],
) -> float:
    for rank, document_id in enumerate(
        retrieved_ids,
        start=1,
    ):
        if document_id in relevant_ids:
            return 1.0 / rank

    return 0.0

ساخت Evaluation Runner

def evaluate_retriever(
    evaluation_set: list[dict],
    search_function,
    k: int = 10,
) -> dict:
    recalls = []
    reciprocal_ranks = []

    for sample in evaluation_set:
        results = search_function(
            sample["query"]
        )

        retrieved_ids = [
            result["id"]
            for result in results
        ]

        relevant_ids = set(
            sample["relevant_document_ids"]
        )

        recalls.append(
            recall_at_k(
                retrieved_ids,
                relevant_ids,
                k,
            )
        )

        reciprocal_ranks.append(
            reciprocal_rank(
                retrieved_ids,
                relevant_ids,
            )
        )

    return {
        f"recall@{k}": sum(recalls)
        / max(len(recalls), 1),
        "mrr": sum(reciprocal_ranks)
        / max(len(reciprocal_ranks), 1),
    }

چه پارامترهایی را تنظیم کنیم؟

BM25

  • k1
  • b
  • Field Boost
  • Analyzer
  • Tokenizer
  • Stopwords
  • Synonyms

Vector Retrieval

  • Embedding Model
  • Distance Metric
  • Top K
  • Candidate Count
  • Chunk Size
  • Vector Dimension
  • Query Prefix
  • Document Prefix

بعضی Embedding Modelها انتظار Prefix متفاوت دارند:

query: ...
passage: ...

مستندات مدل را بررسی کنید.

Fusion

  • RRF k
  • Retriever Weight
  • Fusion Window
  • Normalization Method
  • Number of Candidates
  • Dynamic Routing Rule

RAG

  • Final Context Count
  • Context Token Budget
  • Parent Expansion
  • Deduplication
  • Reranker
  • ترتیب Contextها

Grid Search برای وزن RRF

weight_candidates = [
    (0.5, 1.5),
    (0.75, 1.25),
    (1.0, 1.0),
    (1.25, 0.75),
    (1.5, 0.5),
]


for bm25_weight, vector_weight in (
    weight_candidates
):
    def search(query: str):
        return hybrid_search(
            query=query,
            bm25_weight=bm25_weight,
            vector_weight=vector_weight,
            final_limit=10,
        )

    metrics = evaluate_retriever(
        evaluation_set=validation_set,
        search_function=search,
        k=10,
    )

    print({
        "bm25_weight": bm25_weight,
        "vector_weight": vector_weight,
        **metrics,
    })

وزن برتر روی Validation Set را سپس روی Test Set ارزیابی کنید.

Latency در Hybrid_Search

Hybrid_Search دو یا چند Retrieval را اجرا می‌کند و ممکن است Latency را افزایش دهد.

برای کاهش زمان:

  • BM25 و Vector_Search را موازی اجرا کنید.
  • Query Embedding را Cache کنید.
  • Connection Pool استفاده کنید.
  • Candidate Count را بی‌دلیل زیاد نکنید.
  • Metadata Filter را زود اعمال کنید.
  • Indexها را مناسب پیکربندی کنید.
  • Reranker را فقط روی Top Candidateها اجرا کنید.
  • زمان هر مرحله را جدا ثبت کنید.

اجرای موازی با Python:

import asyncio


async def hybrid_search_async(
    query: str,
):
    lexical_task = asyncio.to_thread(
        bm25_search,
        query,
        30,
    )

    vector_task = asyncio.to_thread(
        vector_search,
        query,
        30,
    )

    lexical_results, vector_results = (
        await asyncio.gather(
            lexical_task,
            vector_task,
        )
    )

    return reciprocal_rank_fusion(
        [
            lexical_results,
            vector_results,
        ],
        limit=10,
        k=60,
    )

اگر Embedding API از Client Async پشتیبانی می‌کند، از فراخوانی Native Async استفاده کنید.

Cache کردن Query Embedding

Queryهای پرتکرار را می‌توان Cache کرد:

Key:
embedding:{model}:{normalized_query_hash}

Value:
embedding vector

در Cache Key باید نسخه مدل Embedding لحاظ شود. اگر مدل تغییر کند، بردارهای قدیمی نباید با Index جدید ترکیب شوند.

Versioning در Retrieval

این موارد را نسخه‌بندی کنید:

{
  "corpus_version": "docs-v12",
  "chunker_version": "semantic-v3",
  "embedding_model": "MODEL_ID",
  "embedding_revision": "REVISION",
  "bm25_analyzer": "fa-tech-v2",
  "fusion_method": "weighted-rrf",
  "rrf_k": 60,
  "bm25_weight": 1.0,
  "vector_weight": 1.25,
  "reranker": "RERANKER_ID"
}

بدون Versioning، مقایسه نتایج آزمایش‌ها دشوار می‌شود.

Observability در Hybrid_Search

برای هر Query این اطلاعات را ثبت کنید:

  • Query نرمال‌شده
  • زمان BM25
  • زمان ساخت Embedding
  • زمان Vector Search
  • زمان Fusion
  • زمان Reranking
  • شناسه نتایج هر Retriever
  • رتبه نهایی
  • تعداد Candidate
  • Filterها
  • نسخه Index
  • نسخه Embedding Model
  • Latency کل

نمونه Trace:

{
  "query_id": "q-8421",
  "latency_ms": {
    "bm25": 18,
    "embedding": 64,
    "vector_search": 22,
    "fusion": 1,
    "rerank": 93,
    "total": 198
  },
  "candidates": {
    "bm25": 30,
    "vector": 30,
    "fused": 24,
    "final": 5
  }
}

این داده‌ها برای تشخیص Bottleneck و افت Relevance ضروری‌اند.

اشتباهات رایج Hybrid_Search

جمع مستقیم BM25 و Cosine Score

Scoreها مقیاس یکسان ندارند. از RRF یا روش نرمال‌سازی‌شده استفاده کنید.

استفاده از Embedding Model نامناسب

مدل باید زبان، نوع Query و حوزه اسناد را پوشش دهد.

نادیده‌گرفتن فارسی

نرمال‌سازی Unicode و Tokenization نامناسب می‌تواند مسیر BM25 را تضعیف کند.

Top K بسیار کوچک

اگر هر Retriever فقط سه نتیجه برگرداند، Fusion Candidate کافی ندارد.

Top K بسیار بزرگ

Candidate زیاد Latency و هزینه Reranking را افزایش می‌دهد و ممکن است نویز وارد کند.

تنظیم وزن بدون Evaluation

وزن‌های ظاهراً منطقی ممکن است در داده واقعی نتیجه را بدتر کنند.

Chunking یکسان برای همه اسناد

مستندات API، مقاله، FAQ و کد به Chunking متفاوت نیاز دارند.

Index ناسازگار

Dense و Sparse Index باید به نسخه یکسان Corpus اشاره کنند.

ارزیابی فقط پاسخ نهایی

ابتدا Retrieval را مستقل ارزیابی کنید. در غیر این صورت مشخص نمی‌شود مشکل از Retriever است یا LLM.

استفاده از یک Query Set مصنوعی

Evaluation باید شامل Queryهای واقعی کاربران، غلط املایی، متن محاوره‌ای، شناسه، نسخه و سؤال‌های کوتاه باشد.

چه زمانی Hybrid_Search لازم نیست؟

Hybrid_Search همیشه بهترین انتخاب نیست.

فقط BM25 کافی است اگر:

  • Corpus کوچک و کاملاً ساختاریافته است.
  • Queryها عمدتاً شناسه و کلمه دقیق هستند.
  • مترادف و زبان طبیعی اهمیت کمی دارد.
  • Latency بسیار محدود است.

فقط Vector_Search کافی است اگر:

  • Queryها کاملاً طبیعی هستند.
  • Corpus اصطلاحات دقیق و شناسه زیادی ندارد.
  • ارزیابی نشان می‌دهد BM25 بهبود معناداری ایجاد نمی‌کند.
  • سادگی معماری اولویت دارد.

Hybrid_Search زمانی ارزشمند است که Corpus ترکیبی از متن طبیعی، اصطلاحات فنی، نام محصول، شناسه و نسخه باشد.

معماری پیشنهادی Production

Document Pipeline
├── Parse
├── Normalize
├── Metadata Extraction
├── Chunk
├── BM25 Index
└── Embedding + Vector Index

Query Pipeline
├── Query Normalization
├── Query Classification
├── BM25 Retrieval
├── Dense Retrieval
├── Metadata Filtering
├── RRF Fusion
├── Deduplication
├── Optional Reranking
├── Context Assembly
└── LLM Generation

Evaluation Pipeline
├── Retrieval Dataset
├── Recall@K
├── MRR
├── nDCG
├── Answer Evaluation
└── Regression Report

چک‌لیست پیاده‌سازی Hybrid_Search

داده

  • Corpus پاک‌سازی شده است؟
  • Chunkها شناسه یکتا دارند؟
  • Dense و Sparse Index هم‌نسخه‌اند؟
  • Metadata کامل است؟
  • متن فارسی نرمال شده است؟
  • متن اصلی برای نمایش حفظ شده است؟

BM25

  • Tokenizer اصطلاحات فنی را حفظ می‌کند؟
  • Field Boost تنظیم شده است؟
  • Stopwordها بررسی شده‌اند؟
  • k1 و b با Evaluation تنظیم شده‌اند؟
  • Embedding Model روی زبان فارسی ارزیابی شده است؟
  • Query و Document با قالب مناسب Embed می‌شوند؟
  • Distance Metric درست است؟
  • بردارها به شکل لازم Normalize شده‌اند؟
  • Batch Embedding استفاده می‌شود؟

Fusion

  • Score خام مستقیم جمع نمی‌شود؟
  • RRF به‌عنوان Baseline آزمایش شده است؟
  • Candidate Window کافی است؟
  • وزن‌ها روی Validation تنظیم شده‌اند؟
  • Test Set جدا باقی مانده است؟

RAG

  • نتایج تکراری حذف می‌شوند؟
  • Token Budget رعایت می‌شود؟
  • Context Source دارد؟
  • Retrieval مستقل ارزیابی شده است؟
  • پاسخ نهایی جداگانه ارزیابی می‌شود؟

عملیات

  • Latency هر مرحله ثبت می‌شود؟
  • نسخه Index ذخیره می‌شود؟
  • امکان Reindex وجود دارد؟
  • Queryهای بدون نتیجه بررسی می‌شوند؟
  • Regression Test اجرا می‌شود؟

سؤالات متداول Hybrid_Search

Hybrid_Search چیست؟

Hybrid Search ترکیبی از جست‌وجوی کلمه‌محور مانند BM25 و جست‌وجوی معنایی مبتنی بر Embedding است که نتایج آن‌ها با الگوریتم Fusion ادغام می‌شوند.

BM25 چیست؟

BM25 یک الگوریتم رتبه‌بندی واژگانی است که براساس تکرار کلمه، نادر‌بودن Term و طول Document امتیاز Relevance محاسبه می‌کند.

Vector_Search چیست؟

Vector Search، Query و اسناد را با Embedding به بردار تبدیل و براساس شباهت عددی آن‌ها نتایج مرتبط را پیدا می‌کند.

چرا BM25 و Vector_Search را ترکیب می‌کنیم؟

BM25 برای عبارت دقیق، شناسه و اصطلاح فنی مناسب است؛ Vector Search مفهوم و مترادف را بهتر تشخیص می‌دهد. ترکیب آن‌ها Recall و پایداری Retrieval را افزایش می‌دهد.

RRF چیست؟

RRF یا Reciprocal Rank Fusion الگوریتمی برای ترکیب چند فهرست رتبه‌بندی‌شده است که به‌جای Score خام، جایگاه هر Document را در فهرست‌ها در نظر می‌گیرد.

مقدار مناسب k در RRF چقدر است؟

مقدار ۶۰ نقطه شروع رایجی است، اما مقدار مناسب باید با Evaluation Set انتخاب شود.

Hybrid_Search بهتر است یا Semantic_Search؟

هیچ پاسخ جهانی وجود ندارد. Hybrid_Search در Corpusهای دارای زبان طبیعی، شناسه، اصطلاح تخصصی و عبارت دقیق معمولاً مقاوم‌تر است؛ اما باید با داده واقعی ارزیابی شود.

آیا Hybrid_Search برای RAG مناسب است؟

بله. Hybrid Search می‌تواند اسناد مرتبط‌تری به مرحله تولید پاسخ برساند و یکی از معماری‌های متداول Retrieval در RAG است.

آیا پس از Hybrid_Search به Reranker نیاز داریم؟

همیشه نه. اگر RRF به کیفیت هدف می‌رسد، Reranker ضروری نیست. برای Precision بالاتر می‌توان Top Candidateها را Rerank کرد.

چگونه Hybrid_Search فارسی بسازیم؟

متن فارسی را نرمال کنید، Tokenizer مناسب اصطلاحات فارسی و فنی بسازید، یک Embedding Model مناسب فارسی انتخاب کنید و نتایج BM25 و Vector Search را با RRF ترکیب کنید.

چگونه کیفیت Hybrid_Search را اندازه‌گیری کنیم؟

از معیارهایی مانند Recall@K، Precision@K، MRR و nDCG روی مجموعه‌ای از Queryها و اسناد مرتبط استفاده کنید.

آیا می‌توان از API درواره برای Embedding استفاده کرد؟

بله، می‌توانید مدل‌های Embedding موجود در پنل را با Base URL استاندارد درواره فراخوانی کنید:

https://api.darvareh.ir/v1

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

جمع‌بندی

Hybrid_Search راهکاری عملی برای افزایش کیفیت Retrieval در موتورهای جست‌وجو و سیستم‌های RAG است. این روش، دقت BM25 در تطبیق عبارت‌های دقیق را با توانایی Vector_Search در درک مفهوم ترکیب می‌کند.

یک Pipeline مناسب معمولاً شامل این مراحل است:

BM25 Retrieval
      +
Dense Vector Retrieval
      ↓
RRF Fusion
      ↓
Deduplication
      ↓
Optional Reranking
      ↓
Context Assembly
      ↓
LLM

برای شروع، BM25 و Vector_Search را مستقل پیاده‌سازی و ارزیابی کنید. سپس RRF را به‌عنوان Fusion Baseline اضافه کنید. پس از ساخت Evaluation Set می‌توانید وزن‌ها، Candidate Count، Chunking، Embedding Model و Reranker را تنظیم کنید.

مهم‌ترین اصل این است که Hybrid_Search را صرفاً براساس ظاهر نتایج ارزیابی نکنید. Recall@K، MRR و nDCG را اندازه بگیرید و تغییرات Retrieval را مستقل از پاسخ LLM بررسی کنید.

برای ساخت Dense Retrieval و اتصال سیستم RAG به مدل‌های Embedding و Chat می‌توانید از API درواره استفاده کنید. در درواره ثبت‌نام کنید، مدل مناسب را از پنل انتخاب کنید و درخواست‌ها را از طریق Base URL زیر ارسال کنید:

https://api.darvareh.ir/v1

مقالات مرتبط

Read more