يا جماعة الخير، السلام عليكم ورحمة الله.
خليني أحكي لكم قصة صارت معي ومع فريقي قبل كم سنة. كنا شغالين على منصة تجارة إلكترونية كبيرة، وكان فيها نظام كوبونات وعروض ترويجية معقد شوي. من ضمن الميزات، كان لازم نتأكد إن كل اسم مستخدم جديد هو فريد من نوعه، وإنه أي كوبون خصم يتم إدخاله لم يتم استخدامه من قبل. في البداية، الأمور كانت “عال العال” والسيستم ماشي زي الحلاوة.
لكن مع الوقت، كبرت المنصة، وصار عنا ملايين المستخدمين وملايين الكوبونات المستخدمة. وهنا بلشت المصايب. قاعدة البيانات صارت تصرخ وتستغيث مع كل عملية تسجيل مستخدم جديد أو استخدام كوبون. كل عملية تحقق بسيطة مثل “هل اسم المستخدم ‘ahmad123’ موجود؟” كانت تعني استعلام SELECT على جدول فيه ملايين السجلات. 99% من هذه الاستعلامات كانت ترجع بنتيجة “لا، غير موجود”، لكنها كانت تستهلك موارد السيرفر وتخنق قاعدة البيانات خنق.
وصلنا لمرحلة إنه السيرفرات صارت “تلعلع” من الضغط، والمستخدمين يشتكوا من بطء التسجيل. قعدنا كفريق، حطينا إيدينا على خدنا، والكل صافن. جربنا كل الحلول التقليدية: عملنا indexing للجداول، وحسّنا الاستعلامات، وعملنا caching… تحسنت الأمور شوي، بس المشكلة الأساسية بعدها موجودة: إحنا بنسأل قاعدة البيانات سؤال مكلف ملايين المرات، مع إنه جوابه في أغلب الأحيان هو “لأ”.
في ليلة من الليالي وأنا بقلّب في دفاتري القديمة وملاحظات الجامعة، لمعت في بالي فكرة من محاضرة عن هياكل البيانات المتقدمة. اسم غريب رن في أذني: “فلاتر بلوم” أو Bloom Filters. تذكرت إنه الدكتور وقتها وصفها بأنها “حارس بوابة ذكي وكسول”. ذكي لأنه بيمنع معظم الزوار غير المرغوب فيهم، وكسول لأنه ما بيعرف كل التفاصيل. قلت لحالي: “يا ولد، يمكن هذا هو الحل اللي بندور عليه!”.
ما هي فلاتر بلوم (Bloom Filters)؟ مش سحر، بس قريب!
ببساطة شديدة، فلتر بلوم هو هيكل بيانات احتمالي (Probabilistic Data Structure) مصمم ليجيب على سؤال واحد بسرعة فائقة: “هل هذا العنصر ربما ينتمي إلى مجموعة؟”.
لاحظوا كلمة “ربما” اللي حطيت تحتها خط. هاي هي مفتاح فهم فلاتر بلوم. إجابتها ليست قطعية 100%، لكنها قوية بشكل مذهل. الفلتر يمكن أن يجيب بإحدى إجابتين:
- “بالتأكيد لا” (Definitely No): إذا قال الفلتر إن العنصر غير موجود، فهو 100% غير موجود. ما في مجال للخطأ هنا.
- “ربما نعم” (Possibly Yes): إذا قال الفلتر إن العنصر موجود، فهو غالباً موجود، ولكن هناك احتمال صغير (يمكن التحكم به) أنه يكون مخطئ. وهذا ما نسميه “الإيجابية الكاذبة” أو “False Positive”.
الفكرة العبقرية هنا هي أننا نتخلص من الغالبية العظمى من الاستعلامات (حالات الـ “بالتأكيد لا”) بضربة واحدة، ونترك لقاعدة البيانات الحالات القليلة التي تحتاج إلى تدقيق فعلي (حالات الـ “ربما نعم”).
كيف تعمل هذه “الخزعبلات” التقنية؟ (آلية العمل بالتفصيل)
لنفهم كيف تعمل، تخيلوا معنا ثلاثة مكونات رئيسية.
1. المصفوفة السحرية (The Bit Array)
تخيل عندنا شريط طويل جداً من خانات الذاكرة، كل خانة فيها إما 0 أو 1. هذا الشريط بنسميه مصفوفة البتات (Bit Array). في البداية، كل الخانات بتكون قيمتها 0.
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... ]
2. الطباخون الماهرون (The Hash Functions)
عندنا مجموعة من “الطباخين” أو دوال التجزئة (Hash Functions). وظيفة كل دالة هي أنها تأخذ أي عنصر (مثل اسم مستخدم “abu_omar”) وتحوله إلى رقم فريد يمثل موقعاً (index) على شريط البتات الطويل تبعنا. نستخدم عادةً عدة دوال (مثلاً 3 دوال) لزيادة الدقة.
3. عملية الإضافة (Adding an Element)
لنفترض أننا نريد إضافة اسم المستخدم “khalid” إلى الفلتر. ماذا نفعل؟
- نمرر “khalid” على دوال التجزئة الثلاثة.
- الدالة الأولى تعطينا الرقم 2.
- الدالة الثانية تعطينا الرقم 5.
- الدالة الثالثة تعطينا الرقم 9.
- بكل بساطة، نذهب إلى المواقع 2، 5، و9 في مصفوفة البتات ونغير قيمتها من 0 إلى 1.
فتصبح المصفوقة هكذا:
[0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, ... ]
لاحظ أننا لم نخزن اسم “khalid” نفسه! فقط علمنا بعض الأماكن في الذاكرة.
4. عملية التحقق (Checking for an Element)
هنا يظهر الجمال الحقيقي. لنفترض أن مستخدماً جديداً يريد التسجيل باسم “omar”.
- السيناريو الأول: التحقق من اسم “omar” (غير موجود)
- نمرر “omar” على نفس دوال التجزئة.
- الدوال تعطينا الأرقام: 1، 4، و8.
- نذهب ونتفحص القيم في المواقع 1، 4، و8. نجدها كلها 0.
- النتيجة: بما أن أحد المواقع على الأقل قيمته 0، فالفلتر يجيب بثقة 100%: “omar بالتأكيد غير موجود”. انتهى. لا داعي لسؤال قاعدة البيانات.
- السيناريو الثاني: التحقق من اسم “khalid” (موجود)
- نمرر “khalid” على دوال التجزئة.
- الدوال تعطينا نفس الأرقام: 2، 5، و9.
- نتفحص القيم في المواقع 2، 5، و9. نجدها كلها 1.
- النتيجة: بما أن كل المواقع المطلوبة قيمتها 1، الفلتر يجيب: “khalid ربما موجود”. في هذه الحالة، نذهب ونتأكد من قاعدة البيانات.
الجانب المظلم: احتمالية الخطأ (False Positives) وكيف نتعامل معها
قد تسأل: “طيب يا أبو عمر، شو قصة الإيجابية الكاذبة هاي؟”.
لنفترض أننا أضفنا “khalid” (الذي حجز المواقع 2, 5, 9) ثم أضفنا “samir” (الذي حجز المواقع 3, 7, 10). ثم أتينا لنتحقق من اسم جديد وليكن “hassan”. بالصدفة البحتة، دوال التجزئة لـ “hassan” أعطتنا المواقع 2، 7، و10. عندما يفحص الفلتر هذه المواقع، سيجدها كلها 1 (الموقع 2 من “khalid”، والمواقع 7 و10 من “samir”). سيقول الفلتر إن “hassan” ربما موجود، مع أننا لم نضفه أبداً! هذا هو الـ False Positive.
هل هذا سيء؟ ليس بالضرورة! الهدف ليس الدقة المطلقة، بل تخفيف الحمل. الفلتر سيجعلنا نقوم باستعلام إضافي غير ضروري لقاعدة البيانات في حالة “hassan”. لكن مقابل كل حالة “false positive” واحدة، يكون الفلتر قد منع آلاف الاستعلامات الصحيحة عن عناصر غير موجودة فعلاً.
يمكننا التحكم في معدل الخطأ هذا عبر تغيير حجم مصفوفة البتات (m) وعدد دوال التجزئة (k). كلما زاد حجم المصفوفة، قل احتمال تصادم المواقع وقل معدل الخطأ.
مثال عملي: منقذنا في مشروع “التحقق من اسم المستخدم”
لنعد إلى قصتنا. هذا ما فعلناه بالضبط لحل مشكلة التحقق من اسم المستخدم.
قبل فلاتر بلوم: الجحيم بعينه
كان الكود ببساطة يقوم بالتالي (مثال بلغة بايثون للتوضيح):
def is_username_taken_slow(username):
# مكلف جداً: يضرب قاعدة البيانات في كل مرة!
# SELECT COUNT(*) FROM users WHERE username = '...'
count = db.users.count_documents({'username': username})
return count > 0
بعد فلاتر بلوم: يا سلام سلّم!
أولاً، قمنا بإنشاء فلتر بلوم عند بدء تشغيل التطبيق وملأناه بكل أسماء المستخدمين الموجودة في قاعدة البيانات. ثم قمنا بتعديل دالة التحقق.
# pip install pybloom-live
from pybloom_live import BloomFilter
# عدد المستخدمين المتوقع ومليون، ومعدل خطأ مقبول 0.1%
# سيتم حساب حجم المصفوفة وعدد الدوال تلقائياً
user_filter = BloomFilter(capacity=1000000, error_rate=0.001)
# عند بدء تشغيل الخادم، نملأ الفلتر مرة واحدة
all_users = db.users.find({}, {'username': 1})
for user in all_users:
user_filter.add(user['username'])
# دالة التحقق الجديدة والسريعة
def is_username_taken_fast(username):
# الخطوة 1: تحقق سريع جداً في الذاكرة
if username not in user_filter:
# إذا قال الفلتر "لأ"، فهو "لأ" بالتأكيد. ارجع فوراً.
return False
# الخطوة 2: الفلتر قال "ربما". الآن فقط نتأكد من قاعدة البيانات.
# هذا يحدث فقط لأسماء المستخدمين الموجودة فعلاً + نسبة صغيرة من الإيجابيات الكاذبة
count = db.users.count_documents({'username': username})
return count > 0
# عند تسجيل مستخدم جديد بنجاح
def register_new_user(username, ...):
# ... منطق تسجيل المستخدم في قاعدة البيانات ...
db.users.insert_one({'username': username, ...})
# لا تنسَ إضافة الاسم الجديد إلى الفلتر في الذاكرة!
user_filter.add(username)
النتيجة كانت مذهلة. أكثر من 99% من محاولات التحقق لأسماء مستخدمين غير موجودة تم صدها في الذاكرة خلال أجزاء من الملي ثانية، دون أن تلمس قاعدة البيانات. الضغط على قاعدة البيانات انخفض بشكل دراماتيكي، وعادت المنصة لتعمل بسرعة وكفاءة.
نصائح أبو عمر الذهبية لاستخدام فلاتر بلوم
- نصيحة 1: اختر الحجم المناسب. لا تبخل في حجم الفلتر (الذاكرة). قبل أن تبدأ، قدّر عدد العناصر التي ستخزنها (n) وحدد معدل الخطأ (p) الذي يمكنك تحمله. استخدم حاسبات فلاتر بلوم الموجودة على الإنترنت لتعرف الحجم الأمثل للمصفوفة (m) وعدد دوال التجزئة (k).
- نصيحة 2: لا يمكن الحذف! فلاتر بلوم القياسية لا تدعم حذف العناصر. لماذا؟ لأنك إذا حذفت بت (bit) معين، قد يكون هذا البت مشتركاً مع عنصر آخر، وبالتالي تكون قد “أفسدت” بيانات عنصر آخر دون قصد. إذا كنت تحتاج للحذف، ابحث عن أنواع متقدمة مثل “Counting Bloom Filters”.
- نصيحة 3: متى تستخدمها؟ هي مثالية لأي سيناريو تحتاج فيه إلى التحقق من عضوية عنصر في مجموعة كبيرة جداً، ولا يهمك وجود نسبة خطأ صغيرة جداً. أمثلة:
- التحقق من أن المستخدم لم يرَ هذا الخبر/المنتج من قبل.
- حجب المواقع الضارة أو عناوين IP المعروفة.
- تجنب تكرار معالجة البيانات في أنظمة البيانات الضخمة.
- نصيحة 4: متى لا تستخدمها؟ لا تستخدمها عندما تكون الإيجابيات الكاذبة غير مقبولة على الإطلاق، أو عندما تحتاج إلى قائمة بالعناصر نفسها وليس فقط التحقق من وجودها. هي للتحقق، وليست للتخزين.
يا جماعة، الهندسة البرمجية ليست مجرد كتابة كود، بل هي فن حل المشاكل بأبسط وأذكى الطرق الممكنة. فلاتر بلوم هي مثال حي على هذا الفن: فكرة بسيطة، موفرة للموارد، وفعالة بشكل لا يصدق لحل مشكلة حقيقية ومؤلمة. لا تخافوا من تجربة هياكل البيانات والخوارزميات التي تبدو “غريبة”، ففي كثير من الأحيان، يكون الحل لمشكلتك الكبيرة مختبئاً في فكرة بسيطة وجميلة مثل هذه. 😉