المستوى: متوسط — يفترض إنك جربت RAG قبل كده، وعارف يعني إيه embeddings و vector database.
لو نظام RAG عندك بيرجع chunks دلاليًا قريبة بس مش بيلاقي الـ chunk اللي فيه اسم المنتج بالظبط، المشكلة مش في الـ embedding model — المشكلة إنك بتعتمد على وسيلة بحث واحدة. الـ Hybrid Search بيدمج BM25 مع Dense Embeddings، وبيرفع Recall@10 من 71% لـ 89% على BEIR benchmarks بدون ما تغير الموديل.
Hybrid Search في RAG: ادمج BM25 مع Embeddings وارفع دقة الإجابة
المشكلة باختصار
عندك RAG بيشتغل على وثائق منتجاتك. المستخدم بيكتب "ZK-87 specs"، الموديل بيرجع 3 chunks عن "specifications of similar models" بدون ما يلاقي الـ chunk اللي فيه ZK-87 بالحرف. السبب: الـ embeddings بتفهم المعنى، لكن بتضيع في الكلمات النادرة زي أكواد المنتجات وأسماء الـ APIs والإصدارات.
BM25 لوحده بيحل المشكلة دي بسهولة — لكنه بيفشل في الأسئلة الدلالية اللي بتسأل عن المعنى بدون تطابق حرفي. الحل مش اختيار واحد، الحل دمج الاتنين.
BM25 vs Dense Embeddings: الفرق بمثال بسيط
تخيل مكتبة فيها 100 ألف كتاب. عندك أمين مكتبة يعرف الفهرس بالظبط — لو طلبت كتاب اسمه "علم النفس المعرفي للطفل"، بيلاقيه في ثواني بناءً على الكلمات. ده BM25.
عندك أمين مكتبة تاني، خبير في المحتوى — لو قلتله "عايز كتاب يشرح إزاي الأطفال بيتعلموا"، بيرشحلك 5 كتب من غير ما اسمها يطابق طلبك. ده Dense Embedding.
الأمين الأول بيفشل لو قلت "كتب تعلم الأطفال" بدل العنوان الحرفي. التاني بيفشل لو طلبت "كتاب ISBN 978-1234". الحل: الاتنين مع بعض، وكل واحد يصوّت في نتائج التاني.
التعريف العلمي: إيه اللي بيحصل فعلاً
BM25 (Best Matching 25) دالة scoring مبنية على TF-IDF مع تطبيع لطول الوثيقة. كل كلمة في الـ query بتاخد score بناءً على تكرارها في الوثيقة (TF) و ندرتها في الـ corpus كله (IDF). الناتج: rank lexical حرفي بيكافئ التطابق المباشر.
Dense Retrieval بيحوّل الـ query والوثائق لـ vectors في فضاء 768 أو 1536 بُعد، وبيستخدم cosine similarity أو dot product. الناتج: rank دلالي بيفهم المرادفات والسياق وبيتجاهل التطابق الحرفي.
Hybrid Search بياخد الترتيبين ويمزجهم بطريقة Reciprocal Rank Fusion (RRF) أو weighted sum. الـ RRF هي الطريقة الأفضل عمليًا لأنها مش محتاجة معايرة scores من نظامين عندهم scales مختلفة تمامًا.
كود Python شغّال في 40 سطر
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
import numpy as np
docs = [
"ZK-87 specifications: 12V DC input, 5A max load, IP67 rated.",
"Configuring electronic devices for outdoor use requires waterproof rating.",
"ZK-87 troubleshooting guide: blink codes and reset procedure.",
"Indoor IoT sensors typically operate at 3.3V and consume less power."
]
# 1) BM25 index
tokenized = [d.lower().split() for d in docs]
bm25 = BM25Okapi(tokenized)
# 2) Dense embeddings
model = SentenceTransformer("intfloat/multilingual-e5-base")
doc_emb = model.encode(docs, normalize_embeddings=True)
def hybrid_search(query, k=3):
bm25_scores = bm25.get_scores(query.lower().split())
bm25_ranks = np.argsort(-bm25_scores)
q_emb = model.encode([query], normalize_embeddings=True)[0]
dense_scores = doc_emb @ q_emb
dense_ranks = np.argsort(-dense_scores)
# Reciprocal Rank Fusion (RRF)
rrf = {}
for rank_list in (bm25_ranks, dense_ranks):
for rank, doc_id in enumerate(rank_list):
rrf[doc_id] = rrf.get(doc_id, 0) + 1 / (60 + rank)
return sorted(rrf.items(), key=lambda x: -x[1])[:k]
print(hybrid_search("ZK-87 specs"))