Hybrid_Search چیست؟ آموزش ترکیب BM25 و Vector Search برای افزایش دقت RAG
در این راهنمای فنی، Hybrid_Search را با ترکیب BM25 و Vector_Search پیادهسازی میکنیم و با RRF، نرمالسازی امتیاز، ارزیابی Retrieval و اتصال به 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
| معیار | BM25 | Vector 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 یکسان باشد تا نتایج بهدرستی ادغام شوند.
Chunking مناسب برای Hybrid Search
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برای BM25numpyبرای 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 خواهد بود.
اجرای Vector Search
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:
- Chunk کوچک را برای Retrieval Index کنید.
- شناسه Parent Section را ذخیره کنید.
- پس از انتخاب Chunk، بخش بزرگتر Parent را واکشی کنید.
- 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 تنظیم کنید.
ارزیابی Hybrid Search
بدون 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@5 | Recall@10 | MRR | nDCG@10 |
|---|---|---|---|---|
| BM25 | 0.68 | 0.77 | 0.71 | 0.70 |
| Vector | 0.74 | 0.82 | 0.73 | 0.75 |
| Hybrid RRF | 0.82 | 0.89 | 0.81 | 0.84 |
| Hybrid + Reranker | 0.84 | 0.91 | 0.87 | 0.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
k1b- 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 تنظیم شدهاند؟
Vector Search
- 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
مقالات مرتبط
- RAG چیست؟ آموزش Retrieval-Augmented Generation
- جستوجوی معنایی یا Semantic Search چیست؟
- پایگاه داده برداری یا Vector Database چیست؟
- Embedding چیست؟ راهنمای کامل بردارسازی متن
- Chunking چیست؟ آموزش تقسیم اسناد برای RAG
- آموزش ساخت RAG با LangChain و LlamaIndex
- آموزش ارزیابی مدلهای هوش مصنوعی و Evals
- Context Window چیست؟
- آموزش کامل LlamaIndex