كانت ذاكرتنا تنفجر: كيف أنقذنا ‘مرشح بلوم’ (Bloom Filter) من جحيم التحقق من العناصر المكررة؟

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

وحدة من أهم المتطلبات كانت حساب عدد “المستخدمين الفريدين” (Unique Users) اللي بيعملوا حدث معين كل يوم. يعني لو المستخدم “أبو العبد” ضغط على زر 10 مرات، بنحسبه مرة وحدة بس. بسيطة، صح؟ هيك فكرنا في البداية. قلنا بنجيب `HashSet` أو `Set` في الذاكرة، وكل ما يجينا ID مستخدم، بنحطه فيها. الـ `Set` بطبيعتها ما بتقبل التكرار، وآخر اليوم بنشوف حجمها وبيكون عنا العدد المطلوب.

أول أسبوع، كانت الأمور تمام. ثاني أسبوع، بدأت الخوادم “تتنهد” شوي. مع نهاية الشهر الأول، صارت القصة كابوس. الذاكرة (RAM) كانت توصل 95%، والنظام كله يصير بطيء ومتجمد، وتوصلنا تنبيهات الساعة 3 الفجر إنه الخادم “وقع”. ليش؟ لأنه ملايين المستخدمين يعني ملايين الـ IDs اللي لازم نخزنها في الذاكرة كل يوم. حجم الـ `Set` صار بالجيجابايت، والذاكرة ببساطة “انفجرت”.

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

ما هو مرشح بلوم (Bloom Filter)؟ الساحر الذي لا يخطئ إلا قليلاً

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

خصائص مرشح بلوم الأساسية هي:

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

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

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

كيف يعمل هذا السحر من الداخل؟

آلية العمل عبقرية في بساطتها. مرشح بلوم يتكون من شيئين أساسيين:

  1. مصفوفة بتات (Bit Array): تخيلها مصفوفة طويلة جداً من الأصفار والواحدات، في البداية كلها أصفار. حجم هذه المصفوفة (خلينا نسميه `m`) يحدد دقة المرشح.
  2. عدد من دوال التجزئة (Hash Functions): مجموعة من `k` دوال تجزئة مختلفة ومستقلة عن بعضها. هذه الدوال تأخذ أي عنصر (نص، رقم، إلخ) وتحوله إلى رقم يمثل موقعاً (index) في مصفوفة البتات.

إضافة عنصر (Add)

عندما تريد إضافة عنصر جديد (مثلاً، ID المستخدم “user123”) إلى المرشح:

  1. تُمرر العنصر على كل دالة من دوال التجزئة الـ `k`.
  2. كل دالة تُرجع لك موقعاً (index) مختلفاً في مصفوفة البتات.
  3. تذهب إلى هذه المواقع الـ `k` في المصفوفة وتُغير قيمتها من `0` إلى `1`.

وهيك بنكون “علّمنا” على الأماكن اللي بتخص هاد العنصر.

التحقق من وجود عنصر (Contains)

عندما تريد التحقق مما إذا كان العنصر “user456” موجوداً:

  1. تُمرر العنصر على نفس دوال التجزئة الـ `k`.
  2. تذهب إلى المواقع الـ `k` التي أعطتك إياها الدوال.
  3. تفحص القيم في هذه المواقع:
    • إذا كان واحد منهم على الأقل قيمته `0`، فأنت متأكد 100% أن هذا العنصر لم تتم إضافته من قبل. مستحيل! لأنه لو تمت إضافته، لكانت كل هذه البتات قد تحولت إلى `1`. (هذا هو مبدأ “لا توجد سلبيات خاطئة”).
    • إذا كانت كل البتات في هذه المواقع قيمتها `1`، فهنا نقول أن العنصر “على الأغلب” موجود. ليش على الأغلب؟ لأنه من الممكن أن تكون هذه البتات قد تحولت إلى `1` بسبب عناصر أخرى مختلفة تماماً تصادف أن دوال التجزئة الخاصة بها أشارت إلى نفس هذه المواقع. (هذا هو مصدر “الإيجابيات الخاطئة”).

مثال بالكود (Python)

الكلام النظري حلو، بس خلينا نشوف الكود. هذه نسخة مبسطة جداً لمفهوم مرشح بلوم باستخدام Python ومكتبة `mmh3` لدوال التجزئة (لأنها سريعة ومناسبة جداً لهذه المهمة).


# First, you need to install the mmh3 library
# pip install mmh3 bitarray

import mmh3
from bitarray import bitarray

class BloomFilter:
    def __init__(self, size, hash_count):
        """
        size: The size of the bit array (m)
        hash_count: The number of hash functions to use (k)
        """
        self.size = size
        self.hash_count = hash_count
        self.bit_array = bitarray(size)
        self.bit_array.setall(0)

    def add(self, item):
        """Adds an item to the filter."""
        for i in range(self.hash_count):
            # We use a different seed for each hash function to simulate k functions
            digest = mmh3.hash(item, i) % self.size
            self.bit_array[digest] = 1

    def __contains__(self, item):
        """
        Checks if an item is in the filter.
        This allows using the 'in' keyword (e.g., 'if item in bloom_filter:')
        """
        for i in range(self.hash_count):
            digest = mmh3.hash(item, i) % self.size
            if self.bit_array[digest] == 0:
                # If any bit is 0, the item is definitely not in the set
                return False
        # If all bits are 1, the item is *probably* in the set
        return True

# --- Example Usage ---

# Let's solve our original problem
# We expect about 1,000,000 unique users (n)
# We want a false positive rate of 1% (p=0.01)

# Using online calculators, we can find optimal values for size (m) and hash_count (k)
# For n=1,000,000 and p=0.01:
# m (size) ≈ 9,585,059 bits (around 1.2 MB)
# k (hash_count) ≈ 7

# Compare this 1.2 MB to the GBs we were using with a HashSet!

n = 1000000  # Number of items
p = 0.01     # False positive probability

# Let's use the calculated values
m = 9585059
k = 7

user_filter = BloomFilter(m, k)

# Let's add some users that we have seen
seen_users = ["user123", "abu_omar", "user789", "guest_abc"]
for user in seen_users:
    user_filter.add(user)

# Now, let's check for users
# 1. Check for a user we know is in the set
print(f"Is 'abu_omar' in the filter? {'abu_omar' in user_filter}") # Should be True

# 2. Check for a user we know is NOT in the set
print(f"Is 'new_user_xyz' in the filter? {'new_user_xyz' in user_filter}") # Should be False

# 3. Check for a potential false positive (hard to predict, but possible)
# A false positive would be a random string that hashes to all-1 bits
print(f"Is 'random_string_12345' in the filter? {'random_string_12345' in user_filter}") # Probably False

نصيحة من أبو عمر: لا تخترع العجلة! في المشاريع الحقيقية، استخدم مكتبات جاهزة ومُختبرة لمرشح بلوم. هذه المكتبات غالباً ما تكون محسّنة للأداء وتعتني بتفاصيل كثيرة مثل اختيار دوال التجزئة وحساب الحجم الأمثل تلقائياً. مثال في Python مكتبة `pybloom_live`.

متى ولماذا نستخدم مرشح بلوم؟

القاعدة الذهبية هي: استخدم مرشح بلوم عندما يكون توفير الموارد (الذاكرة، وقت المعالجة، I/O) أهم من الدقة المطلقة بنسبة 100%، وعندما يكون ثمن “الإيجابية الخاطئة” مقبولاً.

أمثلة من واقع الحياة العملية:

  • قواعد البيانات الموزعة: قواعد بيانات مثل Google Bigtable و Apache Cassandra تستخدم مرشحات بلوم قبل قراءة أي بيانات من القرص. عند البحث عن مفتاح (key)، يتم التحقق من مرشح بلوم في الذاكرة أولاً. إذا قال “غير موجود”، يتم تجنب عملية القراءة البطيئة من القرص تماماً. هذا يحسن أداء القراءة بشكل هائل.
  • أنظمة التوصية (Recommendation Engines): لمنع عرض نفس المقال أو المنتج على المستخدم مرتين، يمكن تخزين كل ما شاهده المستخدم في مرشح بلوم. التحقق منه سريع جداً ولا يستهلك ذاكرة كبيرة على جهاز المستخدم أو الخادم.
  • التحقق من تفرد اسم المستخدم: عندما يسجل مستخدم جديد، يمكن التحقق من الاسم المقترح مقابل مرشح بلوم يحتوي على كل الأسماء المستخدمة. إذا قال “غير موجود”، فالاسم متاح. إذا قال “موجود”، هنا فقط نقوم بالاستعلام الفعلي من قاعدة البيانات للتأكد 100%.
  • شبكات توصيل المحتوى (CDNs): لتحديد ما إذا كان ملف معين موجود في الذاكرة المؤقتة (cache) لخادم معين دون الحاجة لفحص قائمة الملفات الكاملة.

نصائح عملية من خبرة أبو عمر

بعد ما استخدمنا هالأداة في مشاريع كثيرة، تعلمت كم شغلة مهمة بحب أشاركها معكم:

1. لا يمكنك حذف عنصر

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

2. المعادلة السحرية: الحجم ودوال التجزئة

أداء مرشح بلوم يعتمد كلياً على اختيار حجم مصفوفة البتات (`m`) وعدد دوال التجزئة (`k`). هناك معادلات رياضية لحساب القيم المثلى بناءً على عدد العناصر المتوقع (`n`) ونسبة الإيجابيات الخاطئة المقبولة (`p`). لا داعي لحفظها، فقط ابحث عن “Bloom filter calculator” على الإنترنت وستجد أدوات جاهزة تساعدك.

3. نمط “التحقق المزدوج” هو الأقوى

أقوى استخدام لمرشح بلوم هو كـ “طبقة أولى” سريعة للفلترة. هذا هو النمط الذي اتبعناه لحل مشكلتنا:

  1. عند وصول ID مستخدم جديد، تحقق منه في مرشح بلوم.
  2. إذا قال المرشح “غير موجود”: ممتاز! هذا مستخدم جديد. أضفه إلى المرشح، وقم بزيادة عداد المستخدمين الفريدين، ثم أضفه إلى قاعدة البيانات الرئيسية (إذا لزم الأمر).
  3. إذا قال المرشح “موجود”: هنا لا نثق به 100%. نقوم بالخطوة الإضافية والمكلفة: التحقق من وجود الـ ID في قاعدة البيانات الفعلية (أو `HashSet` ثانوية أصغر حجماً).
    • إذا كان موجوداً فعلاً في قاعدة البيانات، فهذا ليس مستخدماً جديداً. نتجاهله.
    • إذا لم يكن موجوداً، فهذه “إيجابية خاطئة”. نتعامل معه كمستخدم جديد ونضيفه للمرشح وقاعدة البيانات.

هذا النمط يضمن دقة 100% للنظام ككل، مع الاستفادة من سرعة مرشح بلوم لتصفية الغالبية العظمى من الحالات المتكررة.

الخلاصة: فكر كمهندس، لا كمجرد مبرمج 🚀

مرشح بلوم علمني درساً مهماً: في عالم الأنظمة الكبيرة، الحلول المثالية 100% ليست دائماً الحلول الأفضل. أحياناً، التضحية بجزء صغير جداً من الدقة (بشكل مدروس ومقبول) يمكن أن يعطيك مكاسب هائلة في الأداء واستهلاك الموارد.

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

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

أتمنى تكون المقالة مفيدة، ويعطيكم ألف عافية.

أبو عمر

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

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

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

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

آخر المدونات

نصائح برمجية

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

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

28 أبريل، 2026 قراءة المزيد
​معمارية البرمجيات

كانت نماذج بياناتنا في حرب أهلية: كيف أنقذ نمط CQRS نظامنا من جحيم تضارب القراءة والكتابة؟

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

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

كانت قوائمنا تربك المستخدمين: كيف أنقذنا ‘قانون هيك’ (Hick’s Law) من جحيم شلل الاختيار؟

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

28 أبريل، 2026 قراءة المزيد
برمجة وقواعد بيانات

كانت تقاريرنا تتجمد: كيف أنقذتنا ‘المشاهد المادية’ (Materialized Views) من جحيم الاستعلامات التحليلية؟

هل تعاني من تقارير ولوحات معلومات بطيئة تتجمد عند كل تحديث؟ في هذه المقالة، أسرد لكم قصة حقيقية من تجربتي كمبرمج وكيف كانت "المشاهد المادية"...

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

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

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

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

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

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

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