تعرض الصفحة الرئيسية للفيديو لدينا الآن مجموعة متجددة من 24 توصية عند كل تحديث، وذلك بفضل الانتقال إلى أسلوب "أخذ العينات بالخزان الموزون" (weighted reservoir sampling - A-Res). لقد قضى هذا التغيير على مشكلة "الشبكة الثابتة للأبد" التي كانت تعيب خلاصة المحتوى (feed) السابقة المعتمدة على ترتيب الدرجات.

لماذا أفسل النهج القديم تجربة المستخدم

نقوم كل ساعتين بسحب أحدث الفيديوهات الرائجة من ثماني مناطق ونضع النتائج في جدول SQLite. كان الحل البديهي هو ترتيب المجموعة بأكملها — بناءً على درجة الصلة (relevance score) — واختيار أفضل 24 فيديو. تضمن هذه الطريقة ظهور العناصر ذات الدرجات الأعلى، ولكنها تضمن أيضًا بقاءها في مكانها. يمكن لمقطع فيديو واحد واسع الانتشار أن يهيمن على المركز الأول لأيام، مما يجعل المستخدمين يشاهدون نفس الشبكة في كل مرة يقومون فيها بإعادة التحميل.

وبالإضافة إلى تجربة المستخدم الرتيبة، فإن ترتيب الجدول بأكمله مع كل طلب يجبر SQLite على تحميل هيكل مؤقت كبير في الذاكرة، وهو أمر غير فعال ومبدد للموارد.

ما الذي يوفره لنا أخذ العينات بالخزان الموزون

يحل أسلوب أخذ العينات بالخزان الموزون مشكلتين في آن واحد:

  1. التجدد مع الانحياز – تكون فرصة اختيار كل فيديو متناسبة مع وزنه (على سبيل المثال، درجة الصلة). الفيديو الذي يمتلك ضعف الوزن يكون أكثر عرضة للظهور بمرتين، ولكن لا يوجد عنصر مضمون الحصول على مكان.
  2. كفاءة الذاكرة – يحتفظ الخوارزم بـ "خزان" (reservoir) ثابت الحجم مكون من k من العناصر (k = 24 بالنسبة لخلاصتنا). يقوم بمعالجة التدفق في تمريرة واحدة، لذا تظل الذاكرة O(k) بغض النظر عن عدد المرشحين الذين يصلون.

الفكرة الأساسية بسيطة. أثناء مسح الصفوف الواردة، نقوم بتعيين مفتاح عشوائي لكل عنصر يتضمن وزنه، ثم نحتفظ بالعناصر k ذات المفاتيح الأعلى.

الخوارزمية في الممارسة العملية

لكل فيديو نقوم بما يلي:

  1. سحب رقم عشوائي منتظم u ∈ (0, 1).
  2. حساب المفتاح k = u^(1/weight).
    (كلما زاد الوزن، زاد المفتاح المتوقع.)
  3. إدراج العنصر في "كومة صغرى" (min-heap) تخزن أعلى k مفاتيح حالية.
    إذا تجاوز حجم الكومة k، نقوم بالتخلص من أصغر مفتاح.

عندما ينتهي التدفق، تحتوي الكومة على الفيديوهات الـ 24 التي سنعرضها.

التعامل مع حدود الأرقام العشرية (floating-point)

عندما يكون وزن الفيديو كبيرًا جدًا، تنهار القيمة u^(1/weight) لتصبح قيمة لا يمكن تمييزها عن 1.0 في الحسابات ذات الدقة المزدوجة (double-precision)، مما يتسبب في مشاركة العديد من العناصر الثقيلة لنفس المفتاح وفقدان العشوائية. الحل هو العمل في المجال اللوغاريتمي (log space):

  • حساب log_key = log(u) / weight بدلاً من تعبير القوة.

نظرًا لأن اللوغاريتمات تحول الأس إلى ضرب، يتم الحفاظ على ترتيب المفاتيح مع تجنب الانهيار في الدقة. لا تكلف هذه العملية أي جهد إضافي يُذكر.

قائمة التحقق من التنفيذ

  • هيكل البيانات: "كومة صغرى" (min-heap) أو (priority queue) بحجم k تحتفظ بأكبر المفاتيح بكفاءة (O(log k) لكل عملية إدراج).
  • النوع الرقمي: استخدم أرقامًا عشرية بنظام 64 بت لـ u والمفاتيح القائمة على اللوغاريتمات؛ فهي توفر دقة كافية لنطاقات الأوزان النموذجية.
  • التحقق الإحصائي: قم بإجراء اختبار "مونت كارلو" (Monte-Carlo) سريع على مجموعة عينات صغيرة. إذا ظهرت العناصر ذات الوزن 10 بمعدل عشر مرات تقريبًا مقارنة بالعناصر ذات الوزن 1، فإن التنفيذ صحيح.
  • التدفق بمرور واحد (Single-pass streaming): لا يحتاج الخوارزم لمعرفة العدد الإجمالي للمرشحين مسبقًا، وهو ما يتناسب طبيعيًا مع مؤشر SQLite (cursor) الخاص بنا الذي يعطي الصفوف واحدًا تلو الآخر.

الخلاصة

يتيح أسلوب أخذ العينات بالخزان الموزون لخدمة توصية الفيديوهات الحفاظ على تجدد صفحتها الرئيسية، واحترام درجات الصلة، والبقاء خفيفًا على الذاكرة — كل ذلك من خلال تمريرة واحدة على عشرات الآلاف من المرشحين. ومن خلال نقل حساب المفتاح إلى المجال اللوغاريتمي واستخدام "كومة صغرى"، تظل الطريقة دقيقة وعالية الأداء في آن واحد.