يا جماعة، الساعة كانت ٢ بعد نص الليل، وأنا بفنجان القهوة الثالث اللي برد من كتر ما أنا سارح في شاشات المراقبة. كنا مطلقين منصة جديدة، والمستخدمين بسجلوا بشكل هستيري، وهذا إشي بفرح طبعًا. لكن كان في صوت “أنين” طالع من سيرفرات قاعدة البيانات، صوت أنا بعرفه منيح… صوت الأداء اللي قاعد بموت.
المشكلة كانت بسيطة وواضحة: كل مستخدم جديد بحاول يسجل، لازم نتأكد إن اسم المستخدم اللي اختاره مش مأخوذ. استعلام بسيط زي
SELECT 1 FROM users WHERE username = ?كان يتنفذ آلاف المرات في الدقيقة. مع ملايين المستخدمين، حتى لو الجدول معمول له فهرسة (indexing) صح، الضغط كان هائل. قاعدة البيانات صارت زي موظف الأرشيف اللي لازم يركض لآخر المستودع عشان يتأكد من وجود ملف، ويرجع يحكي “آه موجود” أو “لأ مش موجود”، آلاف المرات في الساعة. كان وضعًا لا يُحتمل.وقتها، لمعت في بالي فكرة كنت قرأت عنها زمان في ورقة بحثية وضحكت… “هياكل البيانات الاحتمالية”. وبالتحديد، “مرشح بلوم” أو الـ Bloom Filter. عرضت الفكرة على الفريق الصبح، والنظرات كانت بين “شو هاي الشعوذة؟” و “إنت متأكد يا أبو عمر؟”. لكني كنت أعرف أن هذا “السحر” هو بالضبط ما نحتاجه. وهذه هي قصتنا معه.
ما هو “مرشح بلوم” (Bloom Filter) وليش هو ساحر؟
قبل ما ندخل في التفاصيل التقنية المعقدة، خليني أبسطلك الفكرة. تخيل إنك حارس أمن على باب حفلة كبيرة جدًا، وعندك قائمة بأسماء كل المدعوين على ورقة. كل شخص بيجي، لازم تبحث عن اسمه في القائمة الطويلة هاي. عملية مرهقة وبطيئة، صح؟
مرشح بلوم هو الحل العبقري لهذا الحارس. بدلًا من قائمة الأسماء، الحارس عنده “لوحة سحرية” فيها مجموعة كبيرة من الأضواء الصغيرة (كلها مطفأة في البداية). لما يجي شخص مدعو اسمه “أحمد”، الحارس ما بيكتب اسمه، لكنه بيعمل عملية سرية على الاسم (بنسميها دالة التجزئة أو Hash Function) بتعطيه أماكن ٣ أضواء عشوائية على اللوحة، فبيروح ويضوّيهم.
الآن، لما يجي شخص جديد ويسأل “هل أنا مدعو؟”، الحارس بيعمل نفس العملية السرية على اسمه، وبيشوف أماكن الأضواء الناتجة. عنده حالتين:
- واحد من الأضواء على الأقل مطفأ: هنا الحارس بيحكيله بكل ثقة “لأ، إنت مش مدعو بالتأكيد ١٠٠٪”. لأنه لو كان مدعو، كانت أضواؤه كلها مضاءة.
- كل الأضواء الناتجة مضاءة: هنا الحارس بيحكيله “مممم… غالبًا إنت مدعو، تفضل ادخل وتحقق من الطاولة جوا”.
لاحظت كلمة “غالبًا”؟ هذا هو سر مرشح بلوم. هو هيكل بيانات احتمالي، يعني إجابته مش دائمًا أكيدة ١٠٠٪. هو عنده خاصيتين ذهبيتين:
- لا يوجد سلبيات كاذبة (No False Negatives): مستحيل يقول عن عنصر “غير موجود” وهو في الحقيقة موجود.
- قد يوجد إيجابيات كاذبة (Can have False Positives): ممكن يقول عن عنصر “موجود” وهو في الحقيقة غير موجود (لأن أضواؤه أضاءت بالصدفة بسبب أسماء أخرى).
وهذا بالضبط ما كنا نحتاجه! لا نريد أن نزعج قاعدة البيانات بسؤال “هل اسم المستخدم ‘user123’ موجود؟” إلا إذا كان هناك احتمال كبير جدًا أنه موجود فعلًا.
خلينا نفصّص الموضوع تقنياً: كيف يعمل مرشح بلوم؟
الآن، لنخلع قبعة القصة ونرتدي قبعة المبرمجين. مرشح بلوم يتكون من شيئين أساسيين:
1. مصفوفة البتات (Bit Array)
هي مجرد مصفوفة كبيرة من الأصفار والواحدات، حجمها m. في البداية، تكون كلها أصفار.
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
2. مجموعة من دوال التجزئة (Hash Functions)
نحتاج إلى k من دوال التجزئة المستقلة. وظيفة كل دالة هي أن تأخذ عنصرًا (مثل اسم مستخدم) وتُرجع رقمًا (فهرسًا) يقع ضمن حجم مصفوفة البتات.
عملية الإضافة (Add)
لإضافة عنصر إلى المرشح (مثلاً، اسم المستخدم “omar”):
- نمرر “omar” على كل دالة من دوال التجزئة الـ
k. - كل دالة ستعطينا فهرسًا مختلفًا في مصفوفة البتات.
- نذهب إلى هذه الفهارس في المصفوفة ونغير قيمتها من
0إلى1.
مثال: لو عنا ٣ دوال تجزئة (k=3) ومصفوفة حجمها ١٦ (m=16)، وإضافة “omar” أعطتنا الفهارس [2, 7, 12]، ستصبح المصفوفة:
[0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0]
عملية التحقق (Check / Might Contain)
للتحقق مما إذا كان عنصر ما (مثلاً، “salim”) موجودًا:
- نمرر “salim” على نفس دوال التجزئة الـ
k. - نحصل على مجموعة جديدة من الفهارس، مثلاً [5, 7, 15].
- نتحقق من قيم البتات في هذه الفهارس في المصفوفة.
- إذا كان أي بت قيمته
0: العنصر غير موجود بالتأكيد. انتهى. - إذا كانت كل البتات قيمتها
1: العنصر قد يكون موجودًا (إيجابية كاذبة محتملة)، أو هو موجود فعلًا.
في مثالنا، البت في الفهرس 7 قيمته 1 (بسبب “omar”)، لكن البتات في 5 و 15 قيمتها 0. إذن، “salim” غير موجود قطعًا.
يلا نكتب كود: تطبيق مرشح بلوم بسيط باستخدام Python
الكلام النظري جميل، لكن “فرجيني الكود يا أبو عمر!”. لنبني مرشح بلوم بسيط جدًا لنفهم المبدأ (نصيحة: في المشاريع الحقيقية، استخدم مكتبات جاهزة ومختبرة!).
import hashlib
import array
class SimpleBloomFilter:
def __init__(self, size, hash_count):
self.size = size
self.hash_count = hash_count
# استخدام array.array 'B' (unsigned char) لتوفير الذاكرة
self.bit_array = array.array('B', [0]) * size
self.num_items = 0
def _get_hashes(self, item):
# استخدام دالتي تجزئة لإنشاء k من التجزئات (تقنية شائعة)
# MD5 و SHA1 فقط كمثال، يمكن استخدام دوال أسرع مثل Murmur3
hasher1 = hashlib.md5()
hasher1.update(item.encode('utf-8'))
hash1 = int(hasher1.hexdigest(), 16)
hasher2 = hashlib.sha1()
hasher2.update(item.encode('utf-8'))
hash2 = int(hasher2.hexdigest(), 16)
hashes = []
for i in range(self.hash_count):
# توليد k من التجزئات بناءً على التجزئتين الأساسيتين
combined_hash = (hash1 + i * hash2) % self.size
hashes.append(combined_hash)
return hashes
def add(self, item):
# حساب احتمالية الإيجابية الكاذبة قبل إضافة العنصر
# يمكن استخدام هذا الرقم للمراقبة
# p = (1 - (1 - 1/self.size)**(self.hash_count * self.num_items))**self.hash_count
for h in self._get_hashes(item):
self.bit_array[h] = 1
self.num_items += 1
def check(self, item):
for h in self._get_hashes(item):
if self.bit_array[h] == 0:
# إذا وجدنا 0 واحد، فهو غير موجود بالتأكيد
return False
# إذا كانت كلها 1، فهو موجود "على الأغلب"
return True
# --- مثال على الاستخدام ---
# إنشاء مرشح بحجم 100 بت و 3 دوال تجزئة
bloom = SimpleBloomFilter(size=100, hash_count=3)
# إضافة أسماء مستخدمين موجودة في قاعدة البيانات
existing_users = ["abu_omar", "developer_gaza", "palestine_coder", "jerusalem_dev"]
for user in existing_users:
bloom.add(user)
# --- الآن مرحلة التحقق ---
# 1. التحقق من مستخدم موجود فعلًا
print(f"هل 'abu_omar' موجود؟ -> {bloom.check('abu_omar')}") # يجب أن يرجع True
# 2. التحقق من مستخدم غير موجود قطعًا
print(f"هل 'new_user_123' موجود؟ -> {bloom.check('new_user_123')}") # سيرجع False على الأغلب
# 3. مثال على إيجابية كاذبة (نادر الحدوث مع معاملات جيدة)
# قد يرجع True بالصدفة، لكن هذا هو الثمن مقابل السرعة الهائلة
print(f"هل 'some_random_string' موجود؟ -> {bloom.check('some_random_string')}")
في قصتنا، قمنا بإنشاء خدمة صغيرة تقوم كل فترة (مثلاً كل ساعة) ببناء مرشح بلوم جديد من كل أسماء المستخدمين في قاعدة البيانات، ووضعه في الذاكرة (Memory/Redis). عندما يأتي طلب تسجيل مستخدم جديد، كنا نسأل مرشح بلوم أولاً. إذا قال “غير موجود”، نسمح للمستخدم بالمرور إلى صفحة التسجيل فورًا. إذا قال “قد يكون موجودًا”، عندها فقط كنا نرسل استعلامًا لقاعدة البيانات للتأكد.
النتيجة؟ أكثر من 99% من محاولات اختيار أسماء المستخدمين الجديدة كانت لأسماء غير مستخدمة فعلًا. وهذا يعني أننا منعنا 99% من استعلامات التحقق من الوصول إلى قاعدة البيانات! انخفض الحمل بشكل دراماتيكي وعادت الشاشات إلى اللون الأخضر المريح. 🎉
وين بنقدر نستخدم هاي الأداة العبقرية؟
مرشح بلوم ليس فقط لأسماء المستخدمين. أي سيناريو يتكرر فيه سؤال “هل هذا الشيء موجود في مجموعة كبيرة جدًا؟” هو مرشح مثالي:
- أنظمة التخزين المؤقت (Caching): لمنع “Cache Penetration”، حيث يتم طلب مفتاح غير موجود بشكل متكرر، مما يرهق قاعدة البيانات. إذا قال مرشح بلوم “غير موجود”، لا تبحث في قاعدة البيانات أبدًا.
- متصفحات الويب: يستخدمه Google Chrome لتحديد المواقع الضارة. المتصفح لديه مرشح بلوم صغير يحوي بصمات المواقع الضارة. قبل زيارة أي موقع، يتم التحقق منه. إذا قال “قد يكون ضارًا”، عندها فقط يتم إجراء فحص كامل ومكلف أكثر.
- قواعد البيانات الموزعة (مثل Cassandra و Bigtable): لتجنب البحث عن مفتاح في عقدة (node) لا تحتويه. كل عقدة لديها مرشح بلوم للمفاتيح التي تخزنها.
- أنظمة التوصية (Recommendation Systems): للتحقق بسرعة مما إذا كان المستخدم قد رأى مقالًا أو منتجًا معينًا من قبل، لتجنب عرضه مرة أخرى.
نصائح من خبرة أبو عمر
- لا تخترع العجلة في بيئة الإنتاج: الكود الذي كتبناه للتوضيح فقط. في مشروع حقيقي، استخدم مكتبات قوية ومُختبرة مثل
pybloom_liveفي Python أو Guava في Java. هذه المكتبات محسّنة للأداء والذاكرة. - الموازنة هي المفتاح: حجم مصفوفة البتات (m) وعدد دوال التجزئة (k) يحددان معدل الإيجابية الكاذبة. كلما زاد حجم المصفوفة، قل هذا المعدل، لكن زاد استهلاك الذاكرة. هناك معادلات وآلات حاسبة على الإنترنت تساعدك في اختيار أفضل القيم بناءً على عدد العناصر المتوقع ومعدل الخطأ المقبول.
- مرشح بلوم لا يخزن البيانات: تذكر، هو يجيب فقط على سؤال “هل هو موجود؟”. لا يمكنك استرجاع قائمة العناصر التي أضفتها منه.
- ماذا عن الحذف؟ مرشح بلوم القياسي لا يدعم حذف العناصر (لأن إطفاء بت واحد قد يؤثر على عناصر أخرى تشترك فيه). إذا احتجت إلى الحذف، فابحث عن أنواع متقدمة مثل “مرشح بلوم العدّاد” (Counting Bloom Filter).
الخلاصة: متى تستخدم مرشح بلوم؟ 🤔
من الآخر، مرشح بلوم هو “حارس البوابة” الذكي الذي تضعه أمام نظامك البطيء والمكلف. استخدمه عندما:
- لديك مجموعة كبيرة جدًا من البيانات.
- الاستعلام للتحقق من وجود عنصر مكلف (سواء وقتًا أو موارد).
- يمكنك تحمل نسبة صغيرة جدًا من “الإيجابيات الكاذبة”.
- لا يمكنك تحمل “السلبيات الكاذبة” إطلاقًا (وهذا ما يضمنه لك).
في المرة القادمة التي تجد فيها نفسك أمام عنق زجاجة بسبب استعلامات التحقق من الوجود، تذكر قصة أبو عمر وفنجان القهوة البارد. قد يكون “مرشح بلوم” هو البطل الصامت الذي ينتظره نظامك. إنه ليس مجرد خوارزمية، بل هو طريقة تفكير مختلفة في حل المشاكل، طريقة تعتمد على “الجيد بما فيه الكفاية” لتحقيق سرعة مذهلة. 🚀