ذاكرتي كانت تنفجر مع كل عملية تحقق: كيف أنقذني ‘فلتر بلوم’ من جحيم التحقق المكلف؟

ليلة لا تُنسى: عندما كادت “أسماء المستخدمين” أن تقضي على مشروعي

أذكرها جيدًا، كانت ليلة من ليالي الشتاء الباردة في مكتبي الصغير. كنت أعمل على إطلاق منصة اجتماعية جديدة، وكان الحماس يملأني. كل شيء كان يسير على ما يرام، إلى أن بدأنا مرحلة الاختبارات تحت ضغط عالٍ. كانت الفكرة بسيطة: عند تسجيل مستخدم جديد، يجب التأكد من أن اسم المستخدم الذي اختاره غير مستخدم من قبل. “شغلة بسيطة”، قلت في نفسي.

ولكن مع محاكاة ملايين المستخدمين، بدأت الكارثة. مع كل طلب تسجيل، كان النظام يرسل استعلامًا (query) إلى قاعدة البيانات ليتحقق من وجود الاسم. SELECT 1 FROM users WHERE username = ?. هذا الاستعلام، مع أنه سريع في العادة، أصبح عنق الزجاجة القاتل. الخوادم بدأت تئن تحت الضغط، والمعالجات وصلت إلى 100%، وقاعدة البيانات أصبحت بطيئة كسلحفاة مُنهكة.

قلت “بسيطة، الحل في الكاش (Caching)!”. قمت بتحميل كل أسماء المستخدمين في الذاكرة (RAM) داخل Redis. في البداية، بدا الأمر رائعًا. التحقق أصبح فوريًا. ولكن مع نمو قاعدة المستخدمين الوهمية إلى 10 ملايين، ثم 20 مليونًا، بدأت الذاكرة نفسها “تنفجر”. استهلكنا كل جيجابايت متاحة، والخوادم بدأت تنهار مرة أخرى. كنت في جحيم حقيقي: إما أن أقتل قاعدة البيانات بالاستعلامات، أو أُفلس الشركة بتكاليف الذاكرة. جلست على الكرسي، واضعًا يدي على رأسي، وأنا أقول لنفسي: “لا بد من وجود حل ثالث، حل أذكى”. وهنا، لمعت في ذهني فكرة قديمة كنت قد قرأتها في كتاب خوارزميات… كانت تُدعى “فلتر بلوم”.

ما هو “فلتر بلوم” (Bloom Filter) يا جماعة الخير؟

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

الآن، ماذا لو أعطاك مدير النادي ورقة صغيرة جدًا وقال لك: “هذه الورقة ستساعدك. لن تحتوي على أسماء، بل علامات فقط”. هذه الورقة هي “فلتر بلوم”.

فلتر بلوم هو هيكل بيانات احتمالي (Probabilistic Data Structure)، وهو مصمم ليخبرك بأحد أمرين بسرعة فائقة وباستخدام ذاكرة قليلة جدًا:

  • “هذا العنصر غير موجود بالتأكيد في المجموعة.” (إذا قال لك الفلتر “لا”، فهو صادق 100%).
  • “هذا العنصر قد يكون موجودًا في المجموعة.” (إذا قال لك “نعم”، فهناك احتمال صغير أنه مخطئ).

لاحظوا الكلمتين السحريتين: “غير موجود بالتأكيد” و “قد يكون موجودًا”. هذا هو جوهر فلتر بلوم. إنه لا يعطي إجابة إيجابية مؤكدة أبدًا، ولكنه يعطي إجابة سلبية مؤكدة. وهذا، يا أصدقائي، هو مفتاح القوة كلها.

كيف يعمل هذا السحر؟ تعالوا نفصّص الموضوع

يعتمد فلتر بلوم على مكونين رئيسيين: مصفوفة من البتات (bits) ومجموعة من دوال التجزئة (hash functions).

1. البنية الأساسية: مصفوفة البتات (Bit Array)

تخيل أنها سلسلة طويلة جدًا من الأصفار، مثل هذا:

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

هذه المصفوفة صغيرة جدًا في الذاكرة. فكل 8 خانات منها تستهلك 1 بايت فقط. مليون خانة تستهلك حوالي 125 كيلوبايت فقط. هذا هو سر كفاءته في استخدام الذاكرة.

2. مفتاح السر: دوال التجزئة (Hash Functions)

نحتاج إلى عدد معين (لنفترض 3 دوال هاش) من الدوال التي تأخذ أي مُدخل (مثل اسم المستخدم “abu_omar”) وتحوله إلى رقم يمثل موقعًا (index) في مصفوفة البتات. من المهم أن تكون هذه الدوال مستقلة عن بعضها لتقليل التصادمات.

3. عملية الإضافة (Adding an item)

عندما نريد إضافة عنصر جديد (مثل اسم المستخدم “sami”) إلى الفلتر، نقوم بالآتي:

  1. نُمرر “sami” على دوال الهاش الثلاث.
  2. لنفترض أن النتائج كانت:
    • hash1("sami") -> 2
    • hash2("sami") -> 8
    • hash3("sami") -> 13
  3. نذهب إلى المواقع 2، 8، و 13 في مصفوفة البتات ونغير قيمتها من 0 إلى 1.

فتصبح المصفوفة هكذا:

[0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0]

4. عملية التحقق (Checking for an item)

هنا يظهر الجمال الحقيقي. عندما يأتي مستخدم جديد يريد التسجيل باسم “sami”، نقوم بالتحقق:

  1. نُمرر “sami” على نفس دوال الهاش الثلاث، ونحصل على نفس المواقع: 2، 8، و 13.
  2. نتحقق من القيم في هذه المواقع بالمصفوفة.
  3. هل القيمة في الموقع 2 هي 1؟ نعم.
  4. هل القيمة في الموقع 8 هي 1؟ نعم.
  5. هل القيمة في الموقع 13 هي 1؟ نعم.

بما أن كل البتات كانت 1، فالفلتر يقول: “هذا الاسم قد يكون موجودًا“. في حالتي، هذا يعني أنني يجب أن أقوم الآن بالتحقق الفعلي في قاعدة البيانات للتأكد 100%. ولكن لاحظوا، لقد تجنبت التحقق في قاعدة البيانات لكل الأسماء التي لم تكن موجودة أصلاً!

الآن، لنفترض أن شخصًا آخر يريد التسجيل باسم “omar”.

  1. نمرر “omar” على دوال الهاش:
    • hash1("omar") -> 4
    • hash2("omar") -> 8
    • hash3("omar") -> 11
  2. نتحقق من القيم:
    • الموقع 4: قيمته 0.

بمجرد أن نجد بتًا واحدًا قيمته 0، نتوقف فورًا. الفلتر يقول: “الاسم ‘omar’ غير موجود بالتأكيد“. لماذا؟ لأنه لو كان موجودًا، لكانت كل بتاته قد تحولت إلى 1 عند إضافته. وبهذا، أكون قد وفرت على نفسي استعلامًا مكلفًا لقاعدة البيانات بيقين 100%.

الإيجابيات الكاذبة (False Positives): العدو اللدود الذي يمكن ترويضه

ماذا لو أردنا إضافة اسم “layla”، وبالصدفة البحتة، أعطتنا دوال الهاش نفس المواقع التي أعطتها أسماء أخرى (مثلاً، موقع من “sami”، وموقع من “ahmad”، وموقع من “fatima”)؟

hash1("layla") -> 2  (نفس نتيجة sami)
hash2("layla") -> 5  (نتيجة من اسم آخر)
hash3("layla") -> 9  (نتيجة من اسم ثالث)

وعند التحقق من اسم “layla” لاحقًا، قد نجد أن كل بتاته تساوي 1، ليس لأنه تمت إضافته، ولكن بسبب تصادمات من عناصر أخرى. هذا ما يسمى بـ “الإيجابية الكاذبة” (False Positive). الفلتر يخبرنا أن العنصر قد يكون موجودًا، ولكنه في الحقيقة غير موجود.

نصيحة من أبو عمر: نسبة الإيجابيات الكاذبة يمكن التحكم بها. كلما زدت حجم مصفوفة البتات (m) وأحسنت اختيار عدد دوال الهاش (k)، كلما قلت هذه النسبة. هناك معادلات رياضية لتحديد الحجم والعدد الأمثل بناءً على عدد العناصر المتوقع ونسبة الخطأ المقبولة (مثلاً 1%). لا تخف من الرياضيات، فالمكتبات الجاهزة تقوم بهذا العمل عنك.

مثال عملي بالكود: فلنُنشئ فلتر بلوم بسيط باستخدام بايثون

للتوضيح، سنستخدم مكتبة 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):
        print(f"إضافة العنصر: {item}")
        for i in range(self.hash_count):
            # نستخدم i كـ "seed" للحصول على دوال هاش مختلفة من نفس الدالة
            digest = mmh3.hash(item, i) % self.size
            print(f"  - الهاش رقم {i+1} يشير إلى الموقع: {digest}")
            self.bit_array[digest] = 1

    def check(self, item):
        print(f"\nالتحقق من العنصر: {item}")
        for i in range(self.hash_count):
            digest = mmh3.hash(item, i) % self.size
            print(f"  - الهاش رقم {i+1} يشير إلى الموقع: {digest}")
            if self.bit_array[digest] == 0:
                print(f"==> نتيجة التحقق: {item} غير موجود بالتأكيد.")
                return False  # إجابة سلبية مؤكدة
        
        print(f"==> نتيجة التحقق: {item} قد يكون موجودًا.")
        return True # إجابة إيجابية محتملة

# --- الاستخدام ---

# فلتر يتسع لـ 100 عنصر تقريبًا بنسبة خطأ بسيطة
# الحجم 500 بت، وعدد دوال الهاش 4
bloom = BloomFilter(size=20, hash_count=3)

# إضافة أسماء مستخدمين موجودة بالفعل
bloom.add("abu_omar")
bloom.add("sami")

# --- الاختبار ---

# 1. التحقق من عنصر موجود بالفعل (True Positive)
bloom.check("sami")

# 2. التحقق من عنصر غير موجود بالتأكيد (True Negative)
bloom.check("hacker")

# 3. التحقق من عنصر قد يعطي إيجابية كاذبة (False Positive)
# قد يحدث أو لا يحدث، يعتمد على التصادمات
bloom.check("some_random_user")

متى تستخدم فلتر بلوم؟ (نصائح من خبرة أبو عمر)

فلتر بلوم ليس مجرد حيلة نظرية، بل هو أداة عملية استخدمتها شركات عملاقة مثل Google و Facebook و Medium. إليك بعض السيناريوهات التي يتألق فيها:

  • التحقق من تفرد اسم المستخدم/البريد الإلكتروني: كما في قصتي. تجنب 99% من الاستعلامات لقاعدة البيانات للأسماء المتاحة.
  • أنظمة التوصية (Recommendation Engines): لمنع عرض مقالات أو منتجات شاهدها المستخدم من قبل. إذا قال الفلتر “شاهدها”، يمكنك إما تخطيها أو القيام بتحقق أدق.
  • الأمان والحماية: تستخدمه Google Chrome للتحقق من الروابط الخبيثة. لديهم فلتر بلوم يحتوي على آلاف الروابط الضارة. عندما تزور رابطًا، يتم التحقق منه أولاً عبر الفلتر. إذا قال “قد يكون ضارًا”، يقوم المتصفح بطلب تأكيد كامل من خوادم Google.
  • قواعد البيانات الموزعة: لتقليل عمليات البحث في الأقراص الصلبة. تحتفظ بعض قواعد البيانات مثل Cassandra بفلتر بلوم في الذاكرة لكل ملف بيانات. قبل البحث في الملف على القرص (عملية بطيئة)، يتم استشارة الفلتر أولاً.

القاعدة الذهبية: استخدم فلتر بلوم عندما يكون التعامل مع “الإيجابية الكاذبة” ممكنًا وغير مكلف (عبر خطوة تحقق ثانية)، ولكن “السلبية الكاذبة” (أن يقول الفلتر إن العنصر غير موجود وهو موجود) غير مقبولة على الإطلاق.

محاذير وقيود: متى تتجنب فلتر بلوم؟

رغم قوته، فلتر بلوم ليس حلاً لكل المشاكل. أهم قيد يجب أن تعرفه هو:

لا يمكنك حذف عنصر من فلتر بلوم قياسي!

لماذا؟ لأنك لو حاولت تغيير بت من 1 إلى 0 لحذف عنصر، قد يكون هذا البت مشتركًا مع عنصر آخر، وبالتالي ستتسبب في إفساد عملية التحقق الخاصة به (ستحصل على سلبية كاذبة، وهو أمر ممنوع). هناك أنواع متقدمة مثل “Counting Bloom Filter” تسمح بالحذف، لكنها تستهلك ذاكرة أكبر وأكثر تعقيدًا.

الخلاصة: فلتر بلوم، ليس حلاً سحرياً ولكنه أداة جبارة 💡

في تلك الليلة، كان فلتر بلوم هو المنقذ. قمت ببناء فلتر صغير في الذاكرة يحتوي على كل أسماء المستخدمين. عند كل طلب تسجيل جديد، كنت أتحقق من الفلتر أولاً. في أكثر من 99% من الحالات (عندما يختار المستخدم اسمًا فريدًا)، كان الفلتر يخبرني فورًا “غير موجود بالتأكيد”، وكنت أسمح للمستخدم بالمتابعة دون لمس قاعدة البيانات. فقط في حالات قليلة (عندما يختار اسمًا مستخدمًا أو تحدث إيجابية كاذبة)، كنت أقوم بالاستعلام الفعلي. النتيجة؟ انخفض الحمل على قاعدة البيانات بنسبة 99%، وتوقفت الذاكرة عن الانفجار، وعاد المشروع إلى الحياة.

فلتر بلوم هو مثال رائع على جمال الخوارزميات وكيف يمكن لفكرة بسيطة وذكية أن تحل مشاكل معقدة تتعلق بالأداء العالي. إنه يجسد مبدأ المقايضة (trade-off) في الهندسة: نضحي بقليل من الدقة (احتمال الإيجابيات الكاذبة) لنكسب الكثير في السرعة وكفاءة الذاكرة.

نصيحتي الأخيرة لك: في المرة القادمة التي تواجه فيها مشكلة تتطلب التحقق من وجود عنصر في مجموعة ضخمة، قبل أن تفكر في بناء كاش ضخم أو إرهاق قاعدة بياناتك، اسأل حالك: “هل بقدر أستخدم فلتر بلوم هون؟”. قد تكون الإجابة هي مفتاح الحل الذي تبحث عنه. 😉

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

نصائح برمجية

بياناتي كانت تتغير بشكل غامض: كيف أنقذتنا ‘اللامتغيرية’ (Immutability) من جحيم الآثار الجانبية الخفية؟

في أحد المشاريع، كنا نعاني من أخطاء غامضة حيث تتغير البيانات دون سابق إنذار. أشارككم قصتي وكيف كان مفهوم "اللامتغيرية" (Immutability) هو طوق النجاة الذي...

11 أبريل، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

تصاميمنا كانت جزرًا معزولة: كيف أنقذتنا ‘رموز التصميم’ (Design Tokens) من جحيم عدم الاتساق

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

11 أبريل، 2026 قراءة المزيد
الشبكات والـ APIs

عملياتنا كانت تتكرر بشكل كارثي: كيف أنقذتنا ‘مفاتيح عدم التكرار’ (Idempotency Keys) من جحيم الطلبات المزدوجة؟

أشارككم قصة حقيقية من قلب المعركة البرمجية، يوم كادت الطلبات المزدوجة أن تودي بمشروعنا. سنغوص في مفهوم الـ Idempotency Keys، ونرى كيف يمكن لهذه الأداة...

11 أبريل، 2026 قراءة المزيد
الحوسبة السحابية

سيرفراتنا كانت تلتهم الميزانية وهي خاملة: كيف أنقذتنا ‘الحوسبة بدون خوادم’ (Serverless) من جحيم الفواتير المنتفخة؟

أشارككم قصة حقيقية من قلب المعركة البرمجية، كيف انتقلنا من فواتير سيرفرات باهظة وشبه خاملة إلى بنية تحتية مرنة وفعالة من حيث التكلفة بفضل الحوسبة...

11 أبريل، 2026 قراءة المزيد
البودكاست