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

بتذكرها زي كأنه امبارح. كنا في عز إطلاق ميزة جديدة في نظامنا، وفجأة، بدأت التنبيهات تصرخ زي المجنونة. فتحت لوحة المراقبة (Dashboard) وشفت مؤشر استخدام المعالج لقاعدة البيانات في السقف، أحمر قاني، قريب يوصل 100%. أول إشي خطر ببالي: “يا ساتر، شو المصيبة اللي عملناها؟”.

اجتمع الفريق كله، كل واحد فينا صار يدور في الكود ويحلل السجلات (logs). السيرفرات بتعاني، والمستخدمون بدؤوا يشتكون من بطء شديد في النظام. بعد ساعات من التوتر والبحث المكثف، واحد من زملائي صرخ: “يا جماعة الخير، لقيتها!”. المشكلة ما كانت في كود معقد أو استعلام ثقيل على بيانات موجودة، المصيبة كانت أكبر وأغبى من هيك بكثير.

كانت المشكلة في “الأشباح”. نعم، أشباح! كانت نسبة هائلة من الاستعلامات اللي بتوصل لقاعدة البيانات بتسأل عن بيانات مش موجودة أصلاً. تخيل معي، كأنك بترسل جيش كامل ليبحث عن كنز في مكان، وهو أصلاً ما في كنز. هذا بالضبط ما كان يحدث، استنزاف هائل للموارد على “اللاشيء”. وهنا، تذكرت حلاً قديماً جديداً، حلاً ذكياً واحتمالياً… تذكرت “مرشح بلوم”.

ما هي الأشباح التي كانت تطارد قاعدة بياناتنا؟

لفهم المشكلة بعمق، دعنا نبتعد عن المصطلحات المعقدة. تخيل أن قاعدة البيانات الخاصة بك هي مكتبة ضخمة جداً تحتوي على ملايين الكتب. الآن، تخيل أن هناك طابوراً طويلاً من الناس، 90% منهم يسألون أمين المكتبة عن كتب لم تُطبع ولم تُكتب أصلاً! سيقضي أمين المكتبة المسكين كل وقته وهو يركض بين الرفوف ويبحث، ليعود في كل مرة ويقول: “آسف، الكتاب غير موجود”.

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

السيناريو القاتل: البحث عن “اللا شيء”

هذه المشكلة، التي أسميها “استعلامات الأشباح”، شائعة جداً في العديد من التطبيقات:

  • التحقق من توفر اسم المستخدم: عند تسجيل مستخدم جديد، يجب عليك التحقق من أن اسم المستخدم الذي اختاره غير مستخدم بالفعل.
  • قوائم الحظر (Blocklists): التحقق مما إذا كان عنوان IP أو بريد إلكتروني أو رابط URL موجوداً في قائمة ضخمة من العناصر المحظورة.
  • أنظمة التخزين المؤقت (Caching): قبل جلب بيانات من مصدر بطيء (مثل قاعدة بيانات أو API خارجية)، تتحقق أولاً مما إذا كانت موجودة في الكاش. إذا لم تكن موجودة، فهذا “Cache Miss” هو استعلام شبح بالنسبة للكاش.

في كل هذه الحالات، البحث عن عنصر غير موجود هو عملية مكلفة جداً بلا طائل.

المنقذ الذي لم نتوقعه: مرشح بلوم (Bloom Filter)

مرشح بلوم ليس قاعدة بيانات، وليس بنية بيانات تخزن العناصر نفسها. الأفضل أن تفكر فيه كـ “حارس أمن” ذكي جداً ولكنه “احتمالي” يقف على باب قاعدة البيانات. وظيفته بسيطة: الإجابة على سؤال واحد فقط: “هل *من المحتمل* أن يكون هذا العنصر موجوداً بالداخل؟”.

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

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

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

كيف يعمل هذا “الحارس” الذكي؟

يعتمد مرشح بلوم على مكونين رئيسيين:

  1. مصفوفة بتات (Bit Array): تخيل شريطاً طويلاً من خانات الـ 0، مثل `[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]`.
  2. عدة دوال هاش (Hash Functions): وهي دوال رياضية تأخذ أي مُدخل (مثل كلمة “omar”) وتحوله إلى رقم فريد يمثل موقعاً على مصفوفة البتات.

عند إضافة عنصر (مثلاً، “user1”):

  • نمرر العنصر على كل دالة هاش (لنقل 3 دوال).
  • الدالة الأولى تعطينا الموقع 2، والثانية الموقع 5، والثالثة الموقع 8.
  • نذهب إلى مصفوفة البتات ونغير قيمة الخانات في هذه المواقع إلى 1. فتصبح المصفوفة: `[0, 0, 1, 0, 0, 1, 0, 0, 1, 0]`.

عند التحقق من وجود عنصر (مثلاً، “ghost_user”):

  • نمرر العنصر على نفس دوال الهاش الثلاث.
  • لنفترض أنها أعطتنا المواقع 1، و5، و9.
  • نذهب لفحص الخانات في هذه المواقع:
    • الخانة 1 قيمتها 0.
    • الخانة 5 قيمتها 1.
    • الخانة 9 قيمتها 0.
  • بما أننا وجدنا خانة واحدة على الأقل قيمتها 0، فإننا نجزم بشكل قاطع 100% أن “ghost_user” لم تتم إضافته من قبل. وبهذا، نرفض الاستعلام ونمنعه من الوصول لقاعدة البيانات. وهذا هو سحر مرشح بلوم!

شرح بسيط للإيجابية الكاذبة (False Positive): قد يأتي عنصر جديد، وبالصدفة البحتة، تكون مواقع الهاش الخاصة به كلها قد تم تحويلها إلى 1 بواسطة عناصر أخرى. في هذه الحالة، سيقول المرشح “ربما يكون موجوداً” بالخطأ. لكن باختيار حجم مصفوفة مناسب وعدد دوال هاش جيد، يمكننا جعل هذا الاحتمال ضئيلاً جداً (مثلاً 0.1%).

تطبيق الحل عمليًا: من النظرية إلى الكود

بعد أن شرحت الفكرة للفريق، تحمسنا جميعاً. قررنا وضع مرشح بلوم في الذاكرة (In-memory) أمام مسار الكود الذي يتحقق من وجود العناصر. قبل أن يرسل الكود استعلام `SELECT` إلى قاعدة البيانات، يسأل مرشح بلوم أولاً.

مثال بسيط باستخدام بايثون

لست بحاجة لإعادة اختراع العجلة. هناك مكتبات جاهزة ومُحسَّنة يمكنك استخدامها. هنا مثال باستخدام مكتبة `pybloom_live` في بايثون لتوضيح الفكرة:


# First, install the library: pip install pybloom_live
from pybloom_live import BloomFilter

# Create a Bloom Filter
# capacity: a rough estimate of how many items you'll add
# error_rate: the acceptable false positive rate (e.g., 1%)
# Let's say we expect 1 million usernames and want an error rate of 0.01
bloom = BloomFilter(capacity=1000000, error_rate=0.01)

# --- Phase 1: Populate the filter with existing usernames from DB ---
# This is typically done once at startup or periodically
existing_users = ["ahmad", "fatima", "omar", "sara"]
for user in existing_users:
    bloom.add(user)

print(f"Filter populated. Size in bytes: {bloom.num_bytes}")

# --- Phase 2: Use the filter in our application logic ---

def is_username_available(username):
    # Step 1: Check the Bloom Filter first (very fast, in-memory)
    if username in bloom:
        # The filter says the username *might* exist.
        # This could be a true positive or a rare false positive.
        # So, we MUST confirm with the database (the expensive check).
        print(f"Bloom filter says '{username}' MIGHT exist. Checking DB...")
        # In a real app, you would query the database here.
        # return db.users.exists(username)
        # For our example, we'll just check our list.
        if username in existing_users:
            return False # It really exists
        else:
            # This was a false positive!
            print(f"  -> It was a false positive! '{username}' is actually available.")
            return True
    else:
        # The filter says the username DEFINITELY does not exist.
        # We can be 100% sure and avoid a DB query. This is the big win!
        print(f"Bloom filter says '{username}' DEFINITELY does not exist. No DB check needed!")
        return True

# --- Let's test it ---
print("nTesting known users:")
print(f"Is 'omar' available? {is_username_available('omar')}") # Should check DB
print(f"Is 'sara' available? {is_username_available('sara')}") # Should check DB

print("nTesting new (non-existent) users:")
print(f"Is 'ghost_user1' available? {is_username_available('ghost_user1')}") # Should be blocked by filter
print(f"Is 'another_ghost' available? {is_username_available('another_ghost')}") # Should be blocked by filter

نصيحة أبو عمر: كيف تختار حجم المرشح وعدد دوال الهاش؟

هذا هو السؤال الأهم. الأمر كله عبارة عن موازنة. لا تختر الأرقام بشكل عشوائي. المعادلة بسيطة:

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

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

  1. السعة المتوقعة (n): كم عدد العناصر التي تتوقع إضافتها للمرشح؟ (مثلاً، مليون اسم مستخدم).
  2. معدل الخطأ المقبول (p): ما هي نسبة الإيجابيات الكاذبة التي يمكنك تحملها؟ (مثلاً، 1% أو 0.1%).

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

النتائج كانت “إشي بيرفع الراس”!

بعد تطبيق الحل، كانت النتائج فورية ومذهلة. كأننا أزلنا جبلاً عن صدر قاعدة البيانات. لوحة المراقبة التي كانت تصرخ بالأحمر، هدأت وأصبحت تعرض اللون الأخضر المريح. الحمل على المعالج في قاعدة البيانات انخفض من 95% في أوقات الذروة إلى أقل من 15%.

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

الخلاصة: متى تستخدم مرشح بلوم؟ 🤔

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

  • عندما تحتاج إلى التحقق من وجود عنصر بشكل متكرر ضمن مجموعة كبيرة جداً من البيانات.
  • عندما تكون تكلفة التحقق “الحقيقي” (مثل استعلام قاعدة بيانات أو استدعاء API) باهظة جداً.
  • عندما يمكنك تحمل نسبة ضئيلة جداً من الإيجابيات الكاذبة (False Positives). أي عندما لا يسبب خطأ “ربما موجود” كارثة، بل فقط استعلاماً إضافياً غير ضروري في بعض الحالات النادرة.
  • عندما تكون متأكداً أنك لن تحتاج إلى حذف عناصر من المجموعة (مرشح بلوم القياسي لا يدعم الحذف، على الرغم من وجود أنواع أخرى مثل Counting Bloom Filter تدعم ذلك بتكلفة ذاكرة أعلى).

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

أبو عمر

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

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

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

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

آخر المدونات

تجربة المستخدم والابداع البصري

كان تطبيقنا جداراً منيعاً: كيف أنقذتنا ‘إمكانية الوصول’ من جحيم استبعاد المستخدمين؟

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

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

كانت تطبيقاتنا تمطر قاعدة البيانات بالاستعلامات: كيف أنقذنا ‘التحميل الجشع’ (Eager Loading) من جحيم مشكلة N+1؟

في هذه المقالة، أشارككم قصة حقيقية عن كيفية تسبب مشكلة N+1 بكارثة أداء في أحد مشاريعنا، وكيف كان "التحميل الجشع" (Eager Loading) هو طوق النجاة....

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

خدماتنا كانت تتحدث بلغة JSON بطيئة: كيف أنقذنا gRPC من جحيم الاتصال غير الفعال بين الخدمات المصغرة؟

في هذه المقالة، يشارك أبو عمر تجربته الشخصية في الانتقال من REST/JSON إلى gRPC لتحسين أداء الاتصال بين الخدمات المصغرة. استكشف معنا المشاكل الكامنة في...

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

كانت فاتورتنا السحابية لغزاً شهرياً: كيف أنقذتنا ‘علامات تخصيص التكلفة’ من جحيم الإنفاق المجهول؟

هل تواجهون فاتورة سحابية متضخمة وغامضة كل شهر؟ في هذه المقالة، أشارككم تجربتي كـ "أبو عمر" في ترويض وحش التكاليف المجهولة باستخدام أداة بسيطة وقوية:...

19 مايو، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

سيرتي الذاتية في سلة المهملات الرقمية: كيف هزمتُ روبوتات التوظيف (ATS) بهندسة الكلمات المفتاحية

كانت طلباتي الوظيفية تذهب إلى ثقب أسود رقمي دون أي رد. في هذه المقالة، أسرد لكم قصتي مع أنظمة تتبع المتقدمين (ATS) وكيف أنقذتني هندسة...

19 مايو، 2026 قراءة المزيد
التوسع والأداء العالي والأحمال

كانت طلبات المستخدمين تُجمّد تطبيقنا: كيف أنقذتنا ‘طوابير الرسائل’ (Message Queues) من جحيم المعالجة المتزامنة؟

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

19 مايو، 2026 قراءة المزيد
التكنلوجيا المالية Fintech

الصيرفة المفتوحة (Open Banking): كيف أنقذتنا واجهات الـ API من جحيم تجميع بيانات العملاء يدويًا؟

أروي لكم حكايتنا، أنا أبو عمر وفريقي، وكيف انتقلنا من الغرق في بحر من ملفات CSV وبيانات العملاء المبعثرة، إلى عالم من الكفاءة والابتكار بفضل...

19 مايو، 2026 قراءة المزيد
البنية التحتية وإدارة السيرفرات

من خوادم “ندفات الثلج” إلى بنية صخرية: كيف أنقذتنا البنية التحتية ككود (IaC) من جحيم الإعداد اليدوي

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

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