أنظمة البحث والتوصية والتحليلات
محركات البحث والتوصيات وخطوط التحليلات
البحث والتوصية والتحليلات هي ثلاث ركائز تُشغّل كل منتج حديث. في مقابلات تصميم الأنظمة، سيُطلب منك تصميم أنظمة تجمع الثلاثة معًا. يغطي هذا الدرس البنية الداخلية التي تحتاج لشرحها بوضوح على السبورة.
البنية الداخلية للبحث النصي الكامل
يحوّل البحث النصي الكامل النص غير المهيكل إلى بنية محسّنة للاسترجاع السريع. يمر خط المعالجة عبر خمس مراحل:
مستند → Tokenizer → فهرس مقلوب → معالجة الاستعلام → ترتيب (BM25)
↓ ↓
"running" مصطلح → [doc1:2, doc3:1]
"shoes" (قوائم النشر مع
"trail" ترددات المصطلحات)
المرحلة 1: التقسيم إلى رموز (Tokenization)
يقسم التقسيم إلى رموز النص الخام إلى مصطلحات قابلة للبحث. يُجري المقسّم الإنتاجي تحويلات متعددة:
| الخطوة | المدخل | المخرج |
|---|---|---|
| تصغير الأحرف | "Running Shoes" |
"running shoes" |
| التقسيم | "running shoes" |
["running", "shoes"] |
| إزالة كلمات التوقف | ["the", "running", "shoes"] |
["running", "shoes"] |
| التجذير | ["running", "shoes"] |
["run", "shoe"] |
المرحلة 2: الفهرس المقلوب (Inverted Index)
يربط الفهرس المقلوب كل مصطلح بقائمة المستندات التي تحتويه. يخزّن كل إدخال في قائمة النشر معرّف المستند وتردد المصطلح (عدد مرات ظهور المصطلح في ذلك المستند).
الفهرس المقلوب:
─────────────────────────────────────────
المصطلح → قائمة النشر
─────────────────────────────────────────
"run" → [(doc1, tf=3), (doc4, tf=1)]
"shoe" → [(doc1, tf=2), (doc2, tf=5)]
"trail" → [(doc3, tf=1), (doc4, tf=2)]
─────────────────────────────────────────
يمكّن الفهرس المقلوب من البحث بتعقيد O(1) لكل مصطلح. استعلامات المصطلحات المتعددة تتقاطع أو تتحد قوائم النشر حسب العامل المنطقي (AND مقابل OR).
المرحلة 3: تسجيل TF-IDF
TF-IDF (تردد المصطلح - تردد المستند العكسي) يُوزّن المصطلحات حسب مدى إفادتها:
- TF(t, d) = عدد مرات ظهور المصطلح t في المستند d / إجمالي المصطلحات في d
- IDF(t) = log(N / df(t))، حيث N = إجمالي المستندات، df(t) = المستندات التي تحتوي t
المصطلح الذي يظهر في كل مستند (مثل "the") يحصل على IDF منخفض. المصطلح النادر (مثل "kubernetes") يحصل على IDF مرتفع.
المرحلة 4: تسجيل BM25
BM25 (أفضل مطابقة 25)، طوّره Robertson وZaragoza، يحسّن على TF-IDF بتحسينين رئيسيين: تشبّع تردد المصطلح وتطبيع طول المستند.
درجة BM25 لاستعلام Q يحتوي مصطلحات q1, q2, ..., qn مقابل المستند D هي:
BM25(D, Q) = مجموع على qi من:
IDF(qi) * [ f(qi, D) * (k1 + 1) ]
─────────────────────────────────────
f(qi, D) + k1 * (1 - b + b * |D| / avgdl)
حيث:
f(qi, D) = تردد المصطلح qi في المستند D
|D| = طول المستند D (بالمصطلحات)
avgdl = متوسط طول المستند عبر المجموعة
k1 = 1.2 = معامل تشبّع تردد المصطلح
b = 0.75 = معامل تطبيع طول المستند
لماذا k1 = 1.2؟ يتحكم في مدى سرعة تشبّع تردد المصطلح. مع k1 = 1.2، المصطلح الذي يظهر 3 مرات يسجّل أكثر بقليل فقط من المصطلح الذي يظهر مرتين. هذا يمنع حشو الكلمات المفتاحية من السيطرة على الصلة.
لماذا b = 0.75؟ يتحكم في مقدار عقوبة طول المستند على الدرجة. عند b = 0.75، المستندات الأطول تُعاقب (المصطلح الذي يظهر مرة في مستند من 1000 كلمة يسجّل أقل من مستند من 100 كلمة)، لكن ليس بقسوة b = 1.0.
مثال عملي على BM25
لنفترض أن لدينا 3 مستندات والاستعلام Q = "trail running shoes":
| المستند | الطول (مصطلحات) | تردد "trail" | تردد "running" | تردد "shoes" |
|---|---|---|---|---|
| D1: مراجعة أحذية المسارات | 50 | 3 | 0 | 2 |
| D2: دليل أفضل أحذية الجري | 200 | 0 | 5 | 4 |
| D3: مقارنة أحذية الجري على المسارات | 80 | 2 | 3 | 3 |
مع avgdl = 110 وk1 = 1.2 وb = 0.75 وN = 3:
- D3 يسجّل الأعلى لأنه يحتوي على جميع مصطلحات الاستعلام الثلاثة وله طول معتدل
- D2 لديه ترددات عالية لـ "running" و"shoes" لكن صفر لـ "trail"، وطوله (200) يعاقبه
- D1 لديه "trail" لكن يفتقر لـ "running"
interface PostingEntry {
docId: string;
termFrequency: number;
}
interface BM25Config {
k1: number; // 1.2 — تشبّع تردد المصطلح
b: number; // 0.75 — تطبيع طول المستند
}
function computeBM25(
queryTerms: string[],
docId: string,
index: Map<string, PostingEntry[]>,
docLengths: Map<string, number>,
avgDocLength: number,
totalDocs: number,
config: BM25Config = { k1: 1.2, b: 0.75 }
): number {
let score = 0;
const docLength = docLengths.get(docId) ?? 0;
for (const term of queryTerms) {
const postings = index.get(term) ?? [];
const df = postings.length; // تردد المستند
if (df === 0) continue;
const entry = postings.find(p => p.docId === docId);
const tf = entry?.termFrequency ?? 0;
if (tf === 0) continue;
// مكوّن IDF
const idf = Math.log(
(totalDocs - df + 0.5) / (df + 0.5) + 1
);
// البسط والمقام في BM25
const numerator = tf * (config.k1 + 1);
const denominator =
tf + config.k1 * (1 - config.b + config.b * docLength / avgDocLength);
score += idf * (numerator / denominator);
}
return score;
}
الإكمال التلقائي والاقتراحات الفورية
يجب أن يعيد الإكمال التلقائي النتائج خلال 100 مللي ثانية ليبدو فوريًا. يوجد نهجان رئيسيان:
الإكمال التلقائي المبني على Trie
يخزّن Trie كل بادئة لكل مصطلح قابل للبحث. عندما يكتب المستخدم "ru"، يتنقل النظام إلى عقدة "r" -> "u" ويعيد جميع الإكمالات مرتبة حسب الشعبية.
(الجذر)
/ \
r s
/ \
u h
/ \ \
n s o
| | |
[run:450] [rush:30] [shoe:800]
|
[running:320]
البحث بالبادئة مع ترتيب الشعبية
للأنظمة واسعة النطاق، الفهرس المرتب للمصطلحات مع تصفية البادئة أكثر عملية من trie في الذاكرة:
interface AutocompleteEntry {
term: string;
score: number; // وزن الشعبية أو الحداثة
}
class AutocompleteService {
private entries: AutocompleteEntry[];
constructor(entries: AutocompleteEntry[]) {
// الترتيب حسب المصطلح للبحث الثنائي بالبادئة
this.entries = entries.sort((a, b) => a.term.localeCompare(b.term));
}
suggest(prefix: string, limit: number = 10): AutocompleteEntry[] {
const lowerPrefix = prefix.toLowerCase();
// تصفية الإدخالات المطابقة للبادئة
const matches = this.entries.filter(
entry => entry.term.startsWith(lowerPrefix)
);
// الترتيب حسب درجة الشعبية تنازليًا
return matches
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
}
في الإنتاج، يوفر Elasticsearch مقترح completion مدعوم بـ FST (محوّل الحالة المحدود) يحقق عمليات بحث بالبادئة بأقل من مللي ثانية.
أنظمة التوصية
تتنبأ أنظمة التوصية بما يريده المستخدمون قبل أن يبحثوا. ثلاث مقاربات تشكل أساس كل محرك توصية.
التصفية التعاونية (Collaborative Filtering)
تعمل التصفية التعاونية على مبدأ أن المستخدمين الذين اتفقوا في الماضي سيتفقون في المستقبل. لا تتطلب معرفة بمحتوى العنصر.
CF المبنية على المستخدم: إيجاد مستخدمين مشابهين للمستخدم المستهدف، وتوصية العناصر التي أعجبت هؤلاء المستخدمين المشابهين.
CF المبنية على العنصر: إيجاد عناصر مشابهة للعناصر التي أعجبت المستخدم المستهدف، وتوصية تلك العناصر المشابهة.
مصفوفة تقييم المستخدم-العنصر:
────────────────────────────────────
عنصر A عنصر B عنصر C عنصر D
مستخدم 1 5 3 ? 1
مستخدم 2 4 ? 2 1
مستخدم 3 ? 3 5 4
مستخدم 4 5 4 ? ?
────────────────────────────────────
CF المبنية على المستخدم للمستخدم 1، العنصر C:
1. إيجاد مستخدمين مشابهين للمستخدم 1 (بتشابه الجيب تمام على التقييمات)
2. المستخدم 4 الأكثر تشابهًا (كلاهما قيّم A=5، B مرتفع)
3. لكن المستخدم 4 لم يقيّم C أيضًا
4. المستخدم 3 قيّم C=5، وهو مشابه إلى حد ما
5. التنبؤ أن المستخدم 1 سيقيّم C ≈ 4
تشابه الجيب تمام بين متجهي تقييم مستخدمين:
function cosineSimilarity(a: number[], b: number[]): number {
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
return denominator === 0 ? 0 : dotProduct / denominator;
}
التصفية المبنية على المحتوى (Content-Based Filtering)
توصي التصفية المبنية على المحتوى بعناصر مشابهة لما أعجب المستخدم سابقًا، بناءً على خصائص العنصر (النوع، العلامة التجارية، نطاق السعر، الوسوم).
متجهات خصائص العناصر:
────────────────────────────────────
أكشن كوميديا خيال علمي المدة
"The Matrix" 0.9 0.1 0.9 0.7
"Die Hard" 0.9 0.3 0.1 0.6
"Inception" 0.8 0.1 0.8 0.8
────────────────────────────────────
المستخدم أعجبه: "The Matrix"، "Inception"
متجه ملف المستخدم: [0.85, 0.1, 0.85, 0.75] (المتوسط)
أكثر عنصر غير مشاهد تشابهًا: "Die Hard" (تشابه الجيب تمام = 0.88)
مشكلة البداية الباردة (Cold Start)
تواجه كلتا المقاربتين صعوبة عندما تكون البيانات شحيحة:
| السيناريو | المشكلة | الحل |
|---|---|---|
| مستخدم جديد | لا تقييمات أو سجل | مبنية على المحتوى من تفضيلات التسجيل |
| عنصر جديد | لم يقيّمه أحد بعد | مبنية على المحتوى من البيانات الوصفية |
| نظام جديد | لا بيانات لمستخدمين أو عناصر | الرجوع للعناصر الأكثر شعبية |
المُوصي الهجين
أنظمة الإنتاج تجمع كلتا المقاربتين. Netflix مثلًا تستخدم التصفية التعاونية للتوصيات الشخصية، والتصفية المبنية على المحتوى لـ "لأنك شاهدت X"، والترتيب حسب الشعبية كبديل للمستخدمين الجدد.
interface Recommendation {
itemId: string;
score: number;
source: "collaborative" | "content" | "popularity";
}
function hybridRecommend(
userId: string,
cfResults: Recommendation[],
cbResults: Recommendation[],
popularItems: Recommendation[],
userHistoryCount: number
): Recommendation[] {
// البداية الباردة: الرجوع للمحتوى + الشعبية
if (userHistoryCount < 5) {
const blended = [
...cbResults.map(r => ({ ...r, score: r.score * 0.6 })),
...popularItems.map(r => ({ ...r, score: r.score * 0.4 })),
];
return deduplicateAndSort(blended);
}
// مستخدم نشط: مزج التعاوني والمبني على المحتوى
const blended = [
...cfResults.map(r => ({ ...r, score: r.score * 0.7 })),
...cbResults.map(r => ({ ...r, score: r.score * 0.3 })),
];
return deduplicateAndSort(blended);
}
function deduplicateAndSort(items: Recommendation[]): Recommendation[] {
const seen = new Map<string, Recommendation>();
for (const item of items) {
const existing = seen.get(item.itemId);
if (!existing || item.score > existing.score) {
seen.set(item.itemId, item);
}
}
return [...seen.values()].sort((a, b) => b.score - a.score);
}
تحليل المصفوفات (Matrix Factorization)
يفكّك تحليل المصفوفات مصفوفة تقييم المستخدم-العنصر المتناثرة إلى مصفوفتين أقل رتبة. استُخدم هذا بشكل مشهور في مسابقة Netflix Prize (2006-2009)، حيث استخدم الفريق الفائز مقاربات مبنية على SVD.
R ≈ U × V^T
R (مستخدمون x عناصر) U (مستخدمون x k) V (عناصر x k)
[5 3 ? 1] [u1] [v1]
[4 ? 2 1] ≈ [u2] × [v2]^T
[? 3 5 4] [u3] [v3]
[5 4 ? ?] [u4] [v4]
k = أبعاد العوامل الكامنة (مثلًا 50)
SVD (تحليل القيمة المفردة) يجد المصفوفتين U وV المثاليتين. ALS (المربعات الصغرى المتناوبة) هو الخوارزمية الشائعة للأنظمة واسعة النطاق لأنه قابل للتوازي: ثبّت U وحُل V، ثم ثبّت V وحُل U، وكرر حتى التقارب.
خطوط التحليلات: OLTP مقابل OLAP
تتطلب أنظمة التحليلات بنية مختلفة جوهريًا عن الأنظمة التعاملية.
| الخاصية | OLTP | OLAP |
|---|---|---|
| الغرض | تسجيل المعاملات | تحليل الاتجاهات |
| نمط الاستعلام | بحث نقطي، كتابات صغيرة | مسح كامل للجدول، تجميعات |
| نموذج البيانات | مطبّع (3NF) | غير مطبّع (مخطط نجمي) |
| هدف الكمون | < 10 مللي ثانية | ثوانٍ إلى دقائق |
| حداثة البيانات | فوري | دقائق إلى ساعات |
| أمثلة | PostgreSQL، MySQL | BigQuery، Redshift، ClickHouse |
تصميم المخطط النجمي (Star Schema)
ينظّم المخطط النجمي البيانات التحليلية حول جدول حقائق مركزي محاط بجداول الأبعاد:
dim_date
┌─────────┐
│date_key │
│day │
│month │
│quarter │
└────┬────┘
│
dim_product fact_sales dim_customer
┌──────────┐ ┌──────────────┐ ┌────────────┐
│product_id│ │sale_id │ │customer_id │
│name │◄───│product_id │────►│name │
│category │ │customer_id │ │segment │
│brand │ │date_key │ │region │
└──────────┘ │quantity │ └────────────┘
│revenue │
│discount │
└──────────────┘
جداول الحقائق تخزّن الأحداث القابلة للقياس (المبيعات، النقرات، مشاهدات الصفحة). جداول الأبعاد تخزّن السمات الوصفية (تفاصيل المنتج، ديموغرافيا العملاء، التواريخ). هذا التخطيط يمكّن من استعلامات التجميع الفعالة مثل "إجمالي الإيرادات حسب فئة المنتج لكل ربع سنة."
ETL مقابل ELT
| الجانب | ETL | ELT |
|---|---|---|
| خطوة التحويل | قبل التحميل في المستودع | بعد التحميل في المستودع |
| الأدوات | Airflow، Informatica، سكربتات مخصصة | dbt، BigQuery SQL، Snowflake |
| الأفضل لـ | الأنظمة القديمة، التحويلات المعقدة | مستودعات السحابة بحوسبة رخيصة |
| حداثة البيانات | دفعات (كل ساعة/يوم) | شبه فوري ممكن |
المعالجة التدفقية للتحليلات الفورية
عندما تتطلب التحليلات حداثة أقل من دقيقة، تحل المعالجة التدفقية محل ETL الدفعي:
مصدر الأحداث → Kafka → معالج تدفقي → مخزن التحليلات
(النقرات) (الذاكرة المؤقتة) (Flink/Spark) (ClickHouse)
↓
التجميع في
نوافذ منزلقة
(1دقيقة، 5دقائق، 1ساعة)
بنية Elasticsearch
Elasticsearch هو محرك البحث الأكثر انتشارًا لبحث التطبيقات. فهم بنيته ضروري لمقابلات تصميم أنظمة البحث.
مجموعة Elasticsearch
┌─────────────────────────────────────────────┐
│ العقدة 1 العقدة 2 │
│ ┌──────────┐ ┌──────────┐ │
│ │ Shard P0 │ │ Shard P1 │ │
│ │ Shard R1 │ │ Shard R0 │ (نسخة) │
│ └──────────┘ └──────────┘ │
│ │
│ العقدة 3 │
│ ┌──────────┐ │
│ │ Shard P2 │ │
│ │ Shard R0 │ (نسخة) │
│ └──────────┘ │
└─────────────────────────────────────────────┘
الشظايا (Shards) توزّع الفهرس عبر العقد. كل شظية هي فهرس Lucene مستقل. فهرس من 5 شظايا يمكنه التعامل مع 5 أضعاف بيانات فهرس من شظية واحدة.
النسخ (Replicas) توفر التكرار وإنتاجية القراءة. شظية مع نسخة واحدة تعني وجود نسختين من البيانات. إذا تعطلت عقدة، تُرقّى النسخة إلى أساسية.
المحللات (Analyzers) تحدد خط أنابيب التقسيم إلى رموز (مرشحات الأحرف، المقسّم، مرشحات الرموز). المحللات المخصصة تعالج الاحتياجات الخاصة باللغة مثل تقسيم CJK أو توسيع المرادفات.
تطبيق المقابلة: بحث وتوصيات المنتجات للتجارة الإلكترونية
عندما يُطلب منك "صمم نظام بحث وتوصية منتجات لمنصة تجارة إلكترونية"، هيكل إجابتك عبر هذه الطبقات الأربع:
┌────────────────────────────────────────────────────┐
│ بوابة API │
├────────────────────────────────────────────────────┤
│ خدمة البحث │ خدمة التوصية │
│ ┌──────────────┐ │ ┌────────────────────────┐ │
│ │ محلل الاستعلام│ │ │ التصفية التعاونية │ │
│ │ Elasticsearch │ │ │ التصفية بالمحتوى │ │
│ │ BM25 + تعزيز │ │ │ المزج الهجين │ │
│ │ الإكمال التلقائي│ │ │ الرجوع للشعبية │ │
│ └──────────────┘ │ └────────────────────────┘ │
├────────────────────────────────────────────────────┤
│ خط التحليلات (Kafka → Flink) │
│ تتبع النقرات، أحداث اختبار A/B، سجلات البحث │
├────────────────────────────────────────────────────┤
│ PostgreSQL │ Elasticsearch │ Redis │ ClickHouse│
│ (الكتالوج) │ (فهرس البحث) │ (الكاش)│(التحليلات)│
└────────────────────────────────────────────────────┘
قرارات التصميم الرئيسية للمناقشة:
- ترتيب البحث: صلة BM25 الأساسية + تعزيز تجاري (ترتيب المبيعات، الهامش، التوفر) + إشارات التخصيص
- الإكمال التلقائي: مقترح completion في Elasticsearch مع وزن الشعبية، يُحدّث من تحليلات سجل البحث
- التوصيات: التصفية التعاونية لـ "العملاء اشتروا أيضًا"، المبنية على المحتوى لـ "منتجات مشابهة"، والشعبية للمستخدمين الجدد
- البداية الباردة: المنتجات الجديدة تبدأ بتوصيات مبنية على المحتوى من بيانات الفئة/العلامة التجارية؛ المستخدمون الجدد يرون العناصر الرائجة حتى يكون لديهم 5+ تفاعلات
- خط التحليلات: Kafka يستقبل أحداث النقر والشراء، Flink يحسب المقاييس الفورية (معدل التحويل، معدل نجاح البحث)، ClickHouse يخزّن التحليلات المجمّعة للوحات المعلومات
التالي: اختبار الوحدة 4 — اختبر فهمك لبنية البحث الداخلية وخوارزميات التوصية وبنية التحليلات. :::