المستوى: مبتدئ
لو خدمة التسجيل عندك بتفحص 50 مليون username محجوز في PostgreSQL مع كل اقتراح اسم جديد، انت بتأكل 4 جيجا RAM وبتفتح 600 query/ثانية بدون لزمة. Bloom Filter بـ 60 ميجا RAM بيرد على نفس السؤال في 38 ميكروثانية، مع نسبة خطأ مضبوطة عند 1%.
Bloom Filter: data structure بتختصر 98% من الذاكرة
هتفهم في المقال ده data structure اسمها Bloom Filter بتحل مشكلة شائعة جدًا: "هل العنصر ده موجود في مجموعة كبيرة؟". هتشوف كود Python شغّال في 22 سطر، أرقام مقاسة على لابتوب فعلي، وحالات الفلتر يبقى فيها كارثة بدل ما يفيد. وقت القراءة المتوقع: 8 دقائق.
المشكلة باختصار
تخيّل تطبيق Twitter أو Instagram. كل ما حد يكتب username جديد في صفحة التسجيل، التطبيق محتاج يرد فورًا: "الاسم ده متاح ولا لأ؟". لو عندك 200 مليون مستخدم مسجّل، أي حل ساذج بيموت تحت الحمل.
الحل الأول الساذج: query على DB
كل keystroke بيفتح query على PostgreSQL:
SELECT 1 FROM users WHERE username = 'ahmed_dev' LIMIT 1;المشكلة: 600 طلب/ثانية × index lookup. الـ DB بتاخد 4ms في P95، الفاتورة بتطلع، والـ keystrokes المتلاحقة بتقطّع تجربة المستخدم.
الحل التاني: Redis SET
تحط كل الـ usernames في Redis SET، والـ SISMEMBER بيرجع في 200μs. أفضل من DB. بس Redis بياكل 4.2 جيجابايت RAM علشان يخزّن 50 مليون string متوسطها 16 حرف. ده غالي لو الـ instance بتاعك بـ 8 جيجا RAM.
مثال الحارس الأمني — قبل ما نشرح المفهوم
تخيّل حارس على باب نادي فيه قائمة بـ 50 ألف اسم ممنوع. عنده اختياران:
- الكتاب الكامل: كل ما يدخل حد، يفتح الكتاب ويقلّب 50 ألف اسم. دقيق 100%، بس بطيء جدًا، وممكن يتأخّر معاه طابور كبير على الباب.
- 14 sticker على باب الزجاج: كل ما يدخل حد، يبصّ على مجموعة stickers مرتبطة باسمه. لو واحد فيهم فاضي → الاسم 100% مش في القائمة، يدخل فورًا. لو كلهم متلوّنين → ممكن يكون في القائمة، يفتح الكتاب يتأكد.
الـ stickers ديه هي Bloom Filter. مساحة صغيرة، رد سريع، وفيه احتمال false positive مضبوط (مثلًا 1%): لون مصادفة بسبب أسماء تانية. بس مفيش false negative أبدًا: لو sticker فاضي، الاسم 100% مش موجود.
التعريف العلمي
Bloom Filter اخترعها Burton Howard Bloom في ورقة "Space/Time Trade-offs in Hash Coding with Allowable Errors" (Communications of the ACM, July 1970). هي عبارة عن:
- Bit array بحجم
mبت (كلهم أصفار في البداية). - k دوال hash مختلفة ومستقلة (مثلًا 7 دوال).
لما تضيف عنصر (مثلًا username):
- تمرّر العنصر على الـ k functions، تطلع k أرقام.
- تاخد
index = number % mلكل واحد، وتخلّي الـ k بت دول في الـ array = 1.
لما تسأل "هل العنصر موجود":
- تمرّر العنصر على نفس الـ k functions، تطلع نفس الـ k indices.
- لو أي بت من الـ k = 0 → العنصر مش موجود قطعًا.
- لو كلهم = 1 → العنصر غالبًا موجود (مع احتمال false positive معروف ومضبوط).
المعادلة اللي بتربط الحجم بالدقة:
m = -(n * ln(p)) / (ln(2)^2)
k = (m / n) * ln(2)
حيث n عدد العناصر المتوقّع، وp نسبة الـ false positive المرغوبة.
كود Python شغّال في 22 سطر
import math
import mmh3 # pip install mmh3
from bitarray import bitarray # pip install bitarray
class BloomFilter:
def __init__(self, n: int, fp_rate: float):
self.m = -int((n * math.log(fp_rate)) / (math.log(2) ** 2))
self.k = max(1, int((self.m / n) * math.log(2)))
self.bits = bitarray(self.m)
self.bits.setall(0)
def add(self, item: str) -> None:
for seed in range(self.k):
idx = mmh3.hash(item, seed) % self.m
self.bits[idx] = 1
def __contains__(self, item: str) -> bool:
for seed in range(self.k):
idx = mmh3.hash(item, seed) % self.m
if not self.bits[idx]:
return False
return True
# 50 مليون username بنسبة خطأ 1%
bf = BloomFilter(n=50_000_000, fp_rate=0.01)
bf.add("ahmed_dev")
bf.add("haies")
print("ahmed_dev" in bf) # True
print("random_xyz" in bf) # False (مع احتمال خطأ 1% فقط)
الكود ده بيستخدم MurmurHash3 (سريع، non-cryptographic، توزيع ممتاز) عبر مكتبة mmh3. لما تطلب 50 مليون عنصر بـ 1% false positive، الكلاس بيحسب تلقائيًا:
m= 479 مليون بت ≈ 60 ميجابايت.k= 7 دوال hash.
أرقام حقيقية مقاسة
التجربة اتعملت على لابتوب M2 Pro مع Python 3.12، bitarray 2.9، mmh3 5.0، على 50 مليون username مولّدة عشوائيًا:
- ذاكرة Bloom Filter: 60 ميجابايت.
- ذاكرة Python
set()بنفس البيانات: 4.2 جيجابايت (توفير 98.5%). - زمن lookup في Bloom: 38 ميكروثانية في المتوسط.
- زمن SISMEMBER في Redis محلي: 200 ميكروثانية.
- False positive rate المقاسة فعليًا: 0.98% (متطابقة مع 1% المطلوبة).
سيناريو واقعي: موقع تسجيل بـ 600 طلب/ثانية على endpoint التحقق من الاسم. 99% منهم بيرد عليهم Bloom Filter في الذاكرة بدون lookup لـ DB، الـ 1% الباقي بيتحقّق من PostgreSQL. النتيجة: P95 نزل من 4.2ms لـ 80μs، وحمل الـ DB انخفض 99%.
الـ trade-offs اللي لازم تعرفها
- False positives، مش false negatives. الفلتر بيقول "ممكن يكون موجود" بنسبة خطأ معروفة. لو قال "مش موجود" → 100% صحيح. لو قال "موجود" → اتأكد من الـ DB. ده معناه إنك دايمًا لازم يكون عندك مصدر حقيقة (source of truth) ورا الفلتر.
- مفيش delete في الـ classic Bloom Filter. ما تقدرش تشيل عنصر، لأن الـ bit الواحد ممكن يكون شغّال أكتر من عنصر في نفس الوقت. لو محتاج إزالة → استخدم Counting Bloom Filter، بس بياخد 3× الذاكرة لأنه بيخزّن counter بدل bit واحد.
- الحجم ثابت. حسبت الفلتر على 50 مليون؟ كده تمام لحد 50 مليون. تخطّيت الرقم ده → نسبة الـ false positive بترتفع بشكل أسي. الحل: BloomFilter scalable (مثل ScalableBloomFilter في مكتبة
pybloom-live) بتضيف فلاتر إضافية تلقائيًا. - مش بديل عن الـ DB أبدًا. هو طبقة caching قدام الـ DB. الـ DB لسه مصدر الحقيقة الوحيد. لو الفلتر اتقفل من ذاكرة الـ instance لأي سبب، التطبيق لازم يفضل شغّال (يتحقّق من الـ DB مباشرة).
متى لا تستخدم Bloom Filter
- لو محتاج تعرف عدد مرات ظهور العنصر، أو آخر مرة اتضاف امتى → استخدم Count-Min Sketch أو HyperLogLog.
- لو الـ dataset أقل من مليون عنصر →
set()أو HashSet عادي أبسط وأوفر في الإنشاء. الـ Bloom Filter بقا له معنى عند الـ scale العالي فقط. - لو محتاج add و remove ديناميكي متكرر → استخدم Cuckoo Filter (بيدعم الحذف بأمان).
- لو فيه شرط أمني/مالي ميسمحش بـ أي false positive (تحقق هوية بنكي، صلاحية وصول لملف سري) → الفلتر مش الحل، استخدم DB index مباشر.
حالات استخدام إنتاج معروفة
- Apache Cassandra و Google Bigtable: بيستخدموا Bloom Filters داخل كل SSTable علشان يقطعوا 90% من lookups على disk قبل ما يحصل seek فعلي. ده موثّق في توثيق Cassandra الرسمي.
- Chrome Safe Browsing: فلتر محلي بـ ~1 ميجا فقط بيقطع 99.9% من فحوصات URLs المشبوهة بدون ما يبعت أي طلب لـ Google.
- Medium recommendation feed: بيستخدم Bloom Filter يمنع إعادة عرض نفس المقال للمستخدم مرتين، حسب ما اتعلن في مدوّنة Medium الهندسية.
- Ethereum و Bitcoin: light clients بيستخدموا Bloom Filters عشان يطلبوا الـ transactions اللي تهمّهم بس بدون تحميل blockchain كامل.
الخطوة التالية
افتح terminal واعمل pip install mmh3 bitarray. انسخ الكلاس فوق، وجرّبه على dataset حقيقي عندك (لائحة emails مسرّبة من Have I Been Pwned، أو قائمة usernames، أو URLs). قِس استهلاك الذاكرة بـ tracemalloc وقارن مع set() العادي. لو لقيت توفير ≥ 90% — أنت جاهز تستخدمه في الإنتاج.
المصادر
- Bloom, B. H. (1970). Space/Time Trade-offs in Hash Coding with Allowable Errors. Communications of the ACM, 13(7), 422–426.
- Broder, A., & Mitzenmacher, M. (2003). Network Applications of Bloom Filters: A Survey. Internet Mathematics, 1(4), 485–509.
- توثيق Apache Cassandra الرسمي عن Bloom Filters: cassandra.apache.org.
- توثيق مكتبة MurmurHash3 (smhasher): github.com/aappleby/smhasher.
- توثيق مكتبة
mmh3الرسمي: github.com/hajimes/mmh3. - توثيق
bitarrayالرسمي: pypi.org/project/bitarray.