يا جماعة الخير، السلام عليكم ورحمة الله وبركاته. معكم أخوكم أبو عمر.
قبل كم سنة، كنا شغالين على نظام كبير فيه تسجيل مستخدمين بالملايين. كل ما يجي مستخدم جديد يسجل اسم، لازم نتأكد إنه هاد الاسم مش محجوز. الوضع الطبيعي، صح؟ لكن اللي ما كان طبيعي هو حجم الكارثة اللي كنا بنعيشها خلف الكواليس. كنت أذكر سهرات طويلة في المكتب، وأنا والشباب بنحلل أداء قاعدة البيانات اللي كانت “بتصرخ” وتطلب النجدة. المؤشرات كلها باللون الأحمر، والمعالج (CPU) واصل للسما، وزمن الاستجابة بطيء لدرجة إنه المستخدم ممكن يروح يعمل فنجان قهوة ويرجع ولسه الصفحة بتحمّل.
كانت المشكلة بسيطة في ظاهرها، ومعقدة في أثرها: كل عملية تسجيل جديدة كانت تعني استعلامًا لقاعدة البيانات من نوع SELECT للتأكد من عدم وجود اسم المستخدم. ومع ملايين المستخدمين وعشرات الآلاف من محاولات التسجيل اليومية، كانت قاعدة البيانات تقضي معظم وقتها تجيب على سؤال واحد: “هل فلان موجود؟”. ٩٩٪ من الإجابات كانت “لأ، مش موجود”، بس عشان نوصل لهالـ “لأ”، كنا بنستهلك موارد هائلة. قعدت مع حالي ليلة، صافن في الشاشة والبيانات اللي بتنهار، وبحكي لحالي: “يا زلمة، أكيد في طريقة أذكى من هيك! مش معقول كل هالتكنولوجيا وفي الآخر بنغرق في شبر مي!”. وهنا، لمعت في بالي فكرة من أيام الجامعة، خوارزمية احتمالية كانوا يحكوا عنها… اسمها ‘مرشح بلوم’.
ما هو الجحيم الذي كنا نعيشه؟ (المشكلة بالتفصيل)
لفهم حجم المشكلة، دعونا نحلل ما كان يحدث تقنياً. كان لدينا جدول ضخم في قاعدة البيانات اسمه users يحتوي على ملايين الأسماء. كلما حاول مستخدم جديد التسجيل بالاسم “user123″، كان نظامنا ينفذ استعلامًا بسيطًا ولكنه مكلف:
SELECT COUNT(*) FROM users WHERE username = 'user123';
هذا الاستعلام، على بساطته، يجبر قاعدة البيانات على البحث في فهرس (index) ضخم. عندما يكون عدد الطلبات بالآلاف في الدقيقة الواحدة، فإنك تضع عبئًا هائلاً على I/O (عمليات القراءة والكتابة على القرص الصلب) وعلى وحدة المعالجة المركزية. كانت النتيجة الحتمية هي تباطؤ النظام بأكمله، وليس فقط عملية التسجيل.
المشكلة الأكبر كانت أن معظم هذه الاستعلامات كانت “ضائعة”. فغالبية المستخدمين يختارون أسماء فريدة من نوعها، مما يعني أن قاعدة البيانات كانت تبذل مجهودًا كبيرًا لتبحث عن شيء، وفي النهاية تقول “لم أجده”. كنا ندفع ثمنًا باهظًا مقابل معلومة سلبية.
ظهور المُنقذ: ما هو ‘مرشح بلوم’ (Bloom Filter)؟
مرشح بلوم ليس قاعدة بيانات ولا بنية بيانات تقليدية. هو “هيكل بيانات احتمالي” (Probabilistic Data Structure) فائق الكفاءة من حيث المساحة، ومصمم للإجابة على سؤال واحد فقط: “هل هذا العنصر عضو محتمل في هذه المجموعة؟”.
لاحظوا كلمة “محتمل”. هنا يكمن سحره ومقايضته.
- إذا قال مرشح بلوم: “هذا العنصر غير موجود بالتأكيد“، فهو صادق 100%.
- إذا قال مرشح بلوم: “هذا العنصر قد يكون موجودًا“، فهناك احتمال صغير أنه مخطئ (وهو ما يسمى بالإيجابية الكاذبة أو False Positive).
بالنسبة لمشكلتنا، كان هذا الحل مثاليًا. كنا نريد طريقة سريعة جدًا لتصفية (فلترة) الأسماء التي من المؤكد أنها غير مستخدمة، وبالتالي تجنب الذهاب إلى قاعدة البيانات دون داعٍ.
كيف يعمل هذا الساحر تحت الغطاء؟
الفكرة عبقرية في بساطتها وتعتمد على مكونين رئيسيين:
- مصفوفة بتات (Bit Array): تخيل أنها شريط طويل جدًا من الأصفار، بحجم معين (m).
- عدة دوال تجزئة (Hash Functions): مجموعة من دوال الهاش المستقلة (k دوال)، وظيفتها تحويل أي مُدخل (مثل اسم المستخدم) إلى رقم ضمن حجم مصفوفة البتات.
عند إضافة عنصر جديد (مثلاً، اسم المستخدم ‘omar’):
- نمرر الاسم ‘omar’ على كل دالة من دوال التجزئة (k دوال).
- كل دالة ستعطينا رقمًا (موقعًا) مختلفًا في مصفوفة البتات.
- نذهب إلى تلك المواقع في المصفوفة ونغير قيمة البت من 0 إلى 1.
عند التحقق من وجود عنصر (مثلاً، ‘ahmad’):
- نمرر الاسم ‘ahmad’ على نفس دوال التجزئة (k دوال).
- ننظر إلى قيم البتات في المواقع التي أعطتنا إياها الدوال.
- إذا كانت كل البتات في تلك المواقع تساوي 1، فإننا نقول: “العنصر قد يكون موجودًا”.
- إذا كان بت واحد على الأقل يساوي 0، فإننا نقول بيقين تام: “العنصر غير موجود بالتأكيد“. لماذا؟ لأنه لو كان موجودًا، لكانت كل بتاته قد تحولت إلى 1 عند إضافته.
لنترجم الحكي لكود: مثال عملي بلغة Python
دعونا نرى كيف يمكن بناء مرشح بلوم بسيط باستخدام Python لتوضيح الفكرة. سنحتاج إلى مكتبة للهاش مثل mmh3 ومكتبة للتعامل مع البتات مثل bitarray.
# أولاً، قم بتثبيت المكتبات اللازمة
# pip install mmh3 bitarray
import mmh3
from bitarray import bitarray
class BloomFilter:
def __init__(self, size, hash_count):
self.size = size
self.hash_count = hash_count
# إنشاء مصفوفة البتات، كلها أصفار في البداية
self.bit_array = bitarray(size)
self.bit_array.setall(0)
def add(self, item):
"""
إضافة عنصر إلى المرشح
"""
for i in range(self.hash_count):
# نستخدم 'i' كـ "seed" للحصول على دالة هاش مختلفة في كل مرة
digest = mmh3.hash(item, i) % self.size
self.bit_array[digest] = 1
print(f"تمت إضافة '{item}' إلى المرشح.")
def check(self, item):
"""
التحقق من وجود عنصر في المرشح
"""
for i in range(self.hash_count):
digest = mmh3.hash(item, i) % self.size
if self.bit_array[digest] == 0:
# إذا وجدنا صفرًا واحدًا، فهذا يعني أن العنصر غير موجود بالتأكيد
return False
# إذا كانت كل البتات تساوي 1، فالعنصر قد يكون موجودًا
return True
# --- مثال على الاستخدام ---
# لنجهز مرشح بلوم يتسع لـ 100 عنصر مع نسبة خطأ مقبولة
# باستخدام حاسبة مرشح بلوم على الإنترنت، حجم 1000 بت و 7 دوال هاش هو خيار جيد
bf = BloomFilter(size=1000, hash_count=7)
# إضافة أسماء مستخدمين موجودة بالفعل في قاعدة بياناتنا
bf.add("abu_omar")
bf.add("palestine_dev")
bf.add("pythonista")
# الآن، لنتحقق من أسماء جديدة
new_user_1 = "hacker"
if bf.check(new_user_1):
print(f"الاسم '{new_user_1}' قد يكون مستخدماً. (يجب التحقق من قاعدة البيانات)")
# هنا تذهب وتنفذ استعلام SELECT
else:
print(f"الاسم '{new_user_1}' غير مستخدم بالتأكيد. (لا داعي للتحقق من قاعدة البيانات)")
# مثال على إيجابية كاذبة (نادر الحدوث مع الإعدادات الجيدة)
new_user_2 = "some_random_name"
# لنفترض أن هذا الاسم غير موجود، ولكن بالصدفة كل بتاته كانت 1
if bf.check(new_user_2):
# قد يطبع هذا السطر حتى لو كان الاسم غير موجود
print(f"الاسم '{new_user_2}' قد يكون مستخدماً. (يجب التحقق من قاعدة البيانات)")
else:
print(f"الاسم '{new_user_2}' غير مستخدم بالتأكيد.")
في نظامنا الحقيقي، قمنا بتحميل كل أسماء المستخدمين الموجودة في قاعدة البيانات إلى مرشح بلوم مُخزن في الذاكرة (Memory/Redis). عند محاولة تسجيل مستخدم جديد، كنا نتحقق أولاً من مرشح بلوم. إذا قال “غير موجود”، نسمح بالتسجيل مباشرة. إذا قال “قد يكون موجودًا”، عندها فقط كنا نذهب ونتحقق من قاعدة البيانات. هذه الخطوة البسيطة قللت عدد الاستعلامات إلى قاعدة البيانات بنسبة تجاوزت 95%!
المقايضة الكبرى: الإيجابيات الكاذبة (The False Positive)
كما ذكرت، مرشح بلوم ليس مثاليًا. لديه ما يسمى بـ “معدل الإيجابية الكاذبة” (False Positive Rate). هذا يعني أنه في بعض الأحيان، قد يخبرك أن عنصرًا ما “قد يكون موجودًا” بينما هو في الحقيقة غير موجود. هذا يحدث عندما تكون كل البتات الخاصة بعنصر جديد قد تم تحويلها إلى 1 عن طريق الصدفة بسبب عناصر أخرى تمت إضافتها مسبقًا.
هل هذه مشكلة؟ في حالتنا، لا على الإطلاق! أسوأ ما يمكن أن يحدث هو أننا سنقوم بإجراء استعلام غير ضروري لقاعدة البيانات في حالات نادرة. هذا أفضل بكثير من إجراء استعلام في كل مرة! لقد قبلنا بكل سرور هذه المقايضة: توفير هائل في الموارد مقابل نسبة ضئيلة جدًا من الاستعلامات الإضافية.
نصيحة أبو عمر: مفتاح النجاح مع مرشح بلوم هو فهم المقايضة. أنت تضحي باليقين المطلق مقابل سرعة ومساحة تخزين لا مثيل لهما. إذا كان تطبيقك لا يتحمل أي نسبة خطأ (مثل نظام مالي دقيق)، فهذا ليس الحل المناسب. أما إذا كنت تستطيع تحمل القليل من “الضوضاء” لتخفيف الحمل الهائل، فهو سلاحك السري.
نصائح أبو عمر الذهبية (خلاصة الخبرة)
- متى تستخدمه؟ مثالي جدًا لسيناريوهات “التحقق قبل الفعل”. مثلاً: التحقق من أن اسم المستخدم غير محجوز، التحقق من أن رابطًا لم تتم زيارته من قبل، بناء قائمة سوداء (blacklist) لعناوين IP أو الإيميلات الخبيثة، أو منع تكرار عرض نفس الإعلان للمستخدم.
- متى تتجنبه؟ عندما لا تستطيع تحمل الإيجابيات الكاذبة. وأيضًا، مرشح بلوم القياسي لا يدعم حذف العناصر. إذا حذفت عنصرًا من قاعدة البيانات، لا يمكنك “إزالة” بصمته من المرشح (لأنك لو قمت بتصفير بتاته، قد تؤثر على عناصر أخرى تشترك معه في نفس البتات). هناك أنواع متقدمة مثل (Counting Bloom Filter) تدعم الحذف، لكنها أكثر تعقيدًا.
- كيف تختار الحجم المناسب؟ يعتمد معدل الخطأ على ثلاثة عوامل: حجم مصفوفة البتات (m)، العدد المتوقع للعناصر (n)، وعدد دوال التجزئة (k). لا تخمن! استخدم حاسبات متوفرة على الإنترنت (ابحث عن “Bloom Filter Calculator”). أدخل عدد العناصر التي تتوقعها ومعدل الخطأ الذي تقبله (مثلاً 0.01%)، وستعطيك أفضل قيم لـ m و k.
- التكامل العملي: أفضل مكان لوضع مرشح بلوم هو في طبقة ذاكرة تخزين مؤقت سريعة مثل Redis، والتي لديها دعم مدمج له. هذا يجعله متاحًا لكل خوادم التطبيق لديك ويبقى متزامنًا.
الخلاصة: لا تخف من “الاحتمالات”
في عالم البرمجة، غالبًا ما نبحث عن اليقين والدقة المطلقة. لكن قصة نجاحنا مع مرشح بلوم علمتني درسًا مهمًا: أحيانًا، الحل الأكثر فعالية يكمن في تقبل القليل من عدم اليقين. لقد أنقذنا هذا “المرشح الاحتمالي” من انهيار حتمي، وحوّل نظامنا من سلحفاة مرهقة إلى غزال سريع.
لذا في المرة القادمة التي تواجه فيها مشكلة “هل هذا الشيء موجود؟” على نطاق واسع، تذكر قصة أبو عمر، ولا تتردد في استدعاء هذا البطل المتواضع. قد يكون القليل من “الاحتمال” هو كل ما تحتاجه لحل مشكلة “مؤكدة”. 😉