أنظمة البحث والتوصية والتحليلات

محركات البحث والتوصيات وخطوط التحليلات

4 دقيقة للقراءة

البحث والتوصية والتحليلات هي ثلاث ركائز تُشغّل كل منتج حديث. في مقابلات تصميم الأنظمة، سيُطلب منك تصميم أنظمة تجمع الثلاثة معًا. يغطي هذا الدرس البنية الداخلية التي تحتاج لشرحها بوضوح على السبورة.

البنية الداخلية للبحث النصي الكامل

يحوّل البحث النصي الكامل النص غير المهيكل إلى بنية محسّنة للاسترجاع السريع. يمر المستند عبر أربع مراحل في طريق الدخول والخروج:

خط البحث — وقت الفهرسة ووقت الاستعلام

لاحظ أن التقسيم إلى رموز يجري مرتين: على المستند الداخل وعلى الاستعلام الخارج. وإن اختلف الاثنان، لا يطابق شيء.

كتابة القوائمبحث عن المصطلحاتمجموعة المرشحينمستند"Trail Running Shoes"وقت الفهرسة1. المقسّمتصغير، تقسيم، إسقاط كلمات التوق…بنية البيانات2. الفهرس المقلوبمصطلح ← [(مستند، تردد المصطلح)]استعلام"trail running shoes"وقت الاستعلام3. معالجة الاستعلامنفس المحلل، ثم تقاطع أو اتحاد ق…الصلة4. الترتيبBM25 يرتب ما طابق

وضع الفشل الجدير بالذكر في المقابلة: يجب أن يكون المحلل واحدًا في المسارين. افهرس "running" مُجذَّرة إلى run ثم ابحث عن الرمز الخام running، فتحصل على صفر نتائج من فهرس سليم تمامًا.

المرحلة 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 (اختصار "Best Matching"، وهو الخامس والعشرون في سلسلة مرقّمة من أنظمة الترجيح) خرج من أعمال الاسترجاع الاحتمالي لـ Stephen Robertson وKaren Spärck Jones وزملائهما، ويأخذ اسمه الأكمل — Okapi BM25 — من نظام الاسترجاع Okapi الذي بُني في City University London في الثمانينيات والتسعينيات. والمرجع الحديث المعياري هو دراسة Robertson وZaragoza لعام 2009.1 وهو يحسّن على 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: مراجعة أحذية المسارات50302
D2: دليل أفضل أحذية الجري200054
D3: مقارنة أحذية الجري على المسارات80233

مع 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

تتطلب أنظمة التحليلات بنية مختلفة جوهريًا عن الأنظمة التعاملية.

الخاصيةOLTPOLAP
الغرضتسجيل المعاملاتتحليل الاتجاهات
نمط الاستعلامبحث نقطي، كتابات صغيرةمسح كامل للجدول، تجميعات
نموذج البياناتمطبّع (3NF)غير مطبّع (مخطط نجمي)
هدف الكمون< 10 مللي ثانيةثوانٍ إلى دقائق
حداثة البياناتفوريدقائق إلى ساعات
أمثلةPostgreSQL، MySQLBigQuery، 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

الجانبETLELT
خطوة التحويلقبل التحميل في المستودعبعد التحميل في المستودع
الأدواتAirflow، Informatica، سكربتات مخصصةdbt، BigQuery SQL، Snowflake
الأفضل لـالأنظمة القديمة، التحويلات المعقدةمستودعات السحابة بحوسبة رخيصة
حداثة البياناتدفعات (كل ساعة/يوم)شبه فوري ممكن

المعالجة التدفقية للتحليلات الفورية

عندما تتطلب التحليلات حداثة أقل من دقيقة، تحل المعالجة التدفقية محل ETL الدفعي:

مصدر الأحداث → Kafka → معالج تدفقي → مخزن التحليلات
  (النقرات)   (الذاكرة المؤقتة)  (Flink/Spark)    (ClickHouse)
                        التجميع في
                        نوافذ منزلقة
                        (1دقيقة، 5دقائق، 1ساعة)

بنية Elasticsearch

يتصدر Elasticsearch باستمرار فئة محركات البحث في تصنيف شعبية DB-Engines، إلى جانب تفرّعه OpenSearch — ولهذا يتعامل المحاورون مع مصطلحاته كأرضية مشتركة. فهم بنيته ضروري لمقابلات تصميم أنظمة البحث.

مجموعة Elasticsearch
┌─────────────────────────────────────────────┐
│  العقدة 1            العقدة 2               │
│  ┌──────────┐       ┌──────────┐           │
│  │ Shard P0 │       │ Shard P1 │           │
│  │ Shard R1 │       │ Shard R0 │  (نسخة)  │
│  └──────────┘       └──────────┘           │
│                                             │
│  العقدة 3                                   │
│  ┌──────────┐                               │
│  │ Shard P2 │                               │
│  │ Shard R0 │  (نسخة)                      │
│  └──────────┘                               │
└─────────────────────────────────────────────┘

الشظايا (Shards) توزّع الفهرس عبر العقد. كل شظية هي فهرس Lucene مستقل. فهرس من 5 شظايا يمكنه التعامل مع 5 أضعاف بيانات فهرس من شظية واحدة.

النسخ (Replicas) توفر التكرار وإنتاجية القراءة. شظية مع نسخة واحدة تعني وجود نسختين من البيانات. إذا تعطلت عقدة، تُرقّى النسخة إلى أساسية.

المحللات (Analyzers) تحدد خط أنابيب التقسيم إلى رموز (مرشحات الأحرف، المقسّم، مرشحات الرموز). المحللات المخصصة تعالج الاحتياجات الخاصة باللغة مثل تقسيم CJK أو توسيع المرادفات.

تطبيق المقابلة: بحث وتوصيات المنتجات للتجارة الإلكترونية

عندما يُطلب منك "صمم نظام بحث وتوصية منتجات لمنصة تجارة إلكترونية"، هيكل إجابتك عبر هذه الطبقات الأربع:

بحث وتوصيات التجارة الإلكترونية — أربع طبقات

المدخل
الخدمة
خط الأحداث
التخزين

السؤال الذي يطرحه المحاورون عن هذا المخطط: أي مخزن هو سجل النظام المرجعي؟ الجواب PostgreSQL: فهرس البحث ومخزن التحليلات كلاهما مشتق، ويجب أن يكون كلاهما قابلًا لإعادة البناء من الكتالوج مع سجل الأحداث. قول هذا دون أن تُسأل يُظهر أنك فكّرت فيما يحدث بعد تلف الفهرس.

قرارات التصميم الرئيسية للمناقشة:

  1. ترتيب البحث: صلة BM25 الأساسية + تعزيز تجاري (ترتيب المبيعات، الهامش، التوفر) + إشارات التخصيص
  2. الإكمال التلقائي: مقترح completion في Elasticsearch مع وزن الشعبية، يُحدّث من تحليلات سجل البحث
  3. التوصيات: التصفية التعاونية لـ "العملاء اشتروا أيضًا"، المبنية على المحتوى لـ "منتجات مشابهة"، والشعبية للمستخدمين الجدد
  4. البداية الباردة: المنتجات الجديدة تبدأ بتوصيات مبنية على المحتوى من بيانات الفئة/العلامة التجارية؛ المستخدمون الجدد يرون العناصر الرائجة حتى يكون لديهم 5+ تفاعلات
  5. خط التحليلات: Kafka يستقبل أحداث النقر والشراء، Flink يحسب المقاييس الفورية (معدل التحويل، معدل نجاح البحث)، ClickHouse يخزّن التحليلات المجمّعة للوحات المعلومات

التالي: اختبار الوحدة 4 — اختبر فهمك لبنية البحث الداخلية وخوارزميات التوصية وبنية التحليلات. :::

Footnotes

  1. Stephen Robertson وHugo Zaragoza، "The Probabilistic Relevance Framework: BM25 and Beyond"، Foundations and Trends in Information Retrieval 3(4)، 2009.

اختبار

اختبار الوحدة 4: أنظمة البحث والتوصية والتحليلات

خذ الاختبار
هل كان هذا الدرس مفيدًا؟

سجّل الدخول للتقييم