Advertisement
  1. Code
  2. Redis

فهم سحر مرشحات بلوم مع Node.js و Redis

Scroll to top
Read Time: 16 min

Arabic (العربية/عربي) translation by ansgaradh (you can also view the original English article)

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

مرشحات بلوم هي بنية احتمالية للبيانات أحادية الاتجاه. يمكن أن تكون كلمة "فلتر" مربكة في هذا السياق ؛ يشير الفلتر إلى أنه شيء نشط ، فعل ، ولكن قد يكون من الأسهل التفكير فيه كخزن ، اسم. باستخدام فلتر بلوم بسيط يمكنك القيام بأمرين:

  1. أضف بندًا.
  2. تحقق من عدم إضافة عنصر مسبقًا.

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

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

مع هذه القيود ، لديك عدد من المزايا: حجم ثابت ، تشفير يستند إلى التجزئة ، وعمليات بحث سريعة.

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

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

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

الآن بعد أن عرفت حدود وفلاتر بلوم ، دعنا نلقي نظرة على بعض المواقف التي يمكنك استخدامها.

اقامة

سنستخدم Redis و Node.js لتوضيح مرشحات Bloom. Redis هو وسيلة تخزين لمرشح Bloom الخاص بك؛ انها سريعة ، في الذاكرة ، ولها بعض الأوامر المحددة (GETBIT ، SETBIT ) التي تجعل التنفيذ فعالة. سأفترض أن لديك Node.js و npm و Redis مثبت على نظامك. يجب تشغيل خادم Redis الخاص بك على localhost في المنفذ الافتراضي حتى تعمل الأمثلة الخاصة بنا.

في هذا البرنامج التعليمي ، لن نطبق فلترًا من الألف إلى الياء ؛ بدلاً من ذلك ، سنركز على الاستخدامات العملية باستخدام وحدة مدمجة مسبقًا في npm: bloom-redis . ازهر رديس لديه مجموعة موجزة جدا من الطرق: إضافة ، يحتوي و اضحة .

كما ذكرنا سابقًا ، تحتاج عوامل تصفية Bloom إلى خوارزمية تجزئة لإنشاء معرفات فريدة لعنصر ما. يستخدم bloom-redis خوارزمية MD5 المعروفة ، والتي ، على الرغم من أنها ربما ليست مناسبة تمامًا لمرشح Bloom (بطيء قليلاً ، مبالغة في البتات) ، ستعمل بشكل جيد.

أسماء المستخدمين الفريدة

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

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

لتوضيح ذلك ، لنقم بإنشاء ملقم REST سريع باستخدام Express. أولاً ، قم بإنشاء ملفpackage.json ثم قم بتشغيل الأوامر الطرفية التالية.

npm install bloom-redis --save

npm install express --save

npm install redis --save

يكون حجم الخيارات الافتراضية لـ bloom-redis عند اثنين ميغا بايت. هذا يخطئ على جانب الحذر ، لكنه كبير جدا. يعد إعداد حجم مرشح بلوم أمرًا بالغ الأهمية: كبير جدًا وتهدر الذاكرة ، صغير جدًا ومعدل موجب كاذب سيكون مرتفعًا جدًا. يتم تضمين الرياضيات تشارك في تحديد حجم تماما وخارج نطاق هذا البرنامج التعليمي ، ولكن لحسن الحظ هناك آلة حاسبة حجم مرشح بلوم لإنجاز المهمة دون تكسير كتاب دراسي.

الآن ، قم بإنشاء app.js الخاص بك كما يلي:

1
var
2
  Bloom         =   require('bloom-redis'),
3
  express       =   require('express'),
4
  redis         =   require('redis'),
5
  
6
  app,
7
  client,
8
  filter;
9
10
//setup our Express server

11
app = express();
12
13
//create the connection to Redis

14
client = redis.createClient();
15
16
17
filter = new Bloom.BloomFilter({ 
18
  client    : client, //make sure the Bloom module uses our newly created connection to Redis

19
  key       : 'username-bloom-filter', //the Redis key

20
  
21
  //calculated size of the Bloom filter.

22
  //This is where your size / probability trade-offs are made

23
  //http://hur.st/bloomfilter?n=100000&p=1.0E-6

24
  size      : 2875518, // ~350kb

25
  numHashes : 20
26
});
27
28
app.get('/check', function(req,res,next) {
29
  //check to make sure the query string has 'username'

30
  if (typeof req.query.username === 'undefined') {
31
    //skip this route, go to the next one - will result in a 404 / not found

32
    next('route');
33
  } else {
34
   filter.contains(
35
     req.query.username, // the username from the query string

36
     function(err, result) {
37
       if (err) { 
38
        next(err); //if an error is encountered, send it to the client

39
        } else {
40
          res.send({ 
41
            username : req.query.username, 
42
            //if the result is false, then we know the item has *not* been used

43
            //if the result is true, then we can assume that the item has been used

44
            status : result ? 'used' : 'free' 
45
          });
46
        }
47
      }
48
    );
49
  }
50
});
51
52
53
app.get('/save',function(req,res,next) {
54
  if (typeof req.query.username === 'undefined') {
55
    next('route');
56
  } else {
57
    //first, we need to make sure that it's not yet in the filter

58
    filter.contains(req.query.username, function(err, result) {
59
      if (err) { next(err); } else {
60
        if (result) {
61
          //true result means it already exists, so tell the user

62
          res.send({ username : req.query.username, status : 'not-created' });
63
        } else {
64
          //we'll add the username passed in the query string to the filter

65
          filter.add(
66
            req.query.username, 
67
            function(err) {
68
              //The callback arguments to `add` provides no useful information, so we'll just check to make sure that no error was passed

69
              if (err) { next(err); } else {
70
                res.send({ 
71
                  username : req.query.username, status : 'created' 
72
                });
73
              }
74
            }
75
          );
76
        }
77
      }
78
    });
79
  }
80
});
81
82
app.listen(8010);

لتشغيل هذا الخادم: عقدة app.js . انتقل إلى متصفحك وأشره إلى: https: // localhost: 8010 / check؟ username = kyle . يجب أن تكون الإجابة: {"اسم المستخدم": "kyle" ، "status": "free"} .

الآن ، دعنا نحفظ اسم المستخدم هذا من خلال توجيه المتصفح على http: // localhost: 8010 / save؟ username = kyle . ستكون الإجابة:{"اسم المستخدم": "kyle" ، "status": "created"} . إذا رجعت إلى العنوان http: // localhost: 8010 / check؟ username = kyle ، فستكون الإجابة {"اسم المستخدم": "kyle" و "status": "used"} . وبالمثل ، عند الرجوع إلىhttp: // localhost: 8010 / save؟ username = kyleسيؤدي إلى {"اسم المستخدم": "kyle" و "status": "not-created"} .

من الجهاز ، يمكنك رؤية حجم الفلتر: redis-cli strlen username-bloom-filter .

الآن ، مع عنصر واحد ، يجب أن تظهر 338622 .

والآن ، حاول إضافة المزيد من أسماء المستخدمين باستخدام المسار / save . جرب ما تشاء.

إذا قمت بفحص الحجم مرة أخرى ، قد تلاحظ أن حجمك قد ارتفع قليلاً ، ولكن ليس لكل إضافة.الغريب ، أليس كذلك؟ داخليًا ، يعمل مرشح بلوم على تعيين وحدات البت الفردية (1's / 0) في مواضع مختلفة في السلسلة المحفوظة عند اسم المستخدم -البلوم. ومع ذلك ، هذه ليست متجاورة ، لذلك إذا قمت بتعيين بت في فهرس 0 ثم واحد في الفهرس 10،000 ، سيكون كل شيء بين 0. بالنسبة للاستخدامات العملية ، ليس من المهم في البداية فهم الآليات الدقيقة لكل عملية - فقط اعلم أن هذا أمر طبيعي وأن تخزينك في Redis لن يتجاوز القيمة التي حددتها.

محتوى جديد

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

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

في هذا المثال ، لنقم بإنشاء خادم Express أساسي آخر يعرض المحتوى. في كل مرة تزور فيها المسار / عرض المحتوى / أي اسم مستخدم (مع وجود أي اسم مستخدم لأي قيمة آمنة لعنوان URL) ، سيتم عرض جزء جديد من المحتوى حتى يصبح الموقع خارج المحتوى. في المثال ، يكون المحتوى هو السطر الأول من الكتب العشرة الأولى فيProject Gutenberg .

سنحتاج إلى تثبيت وحدة npm أخرى. من المحطة ، شغل: npm install async - save

ملف app.js الجديد الخاص بك:

1
var
2
  async         =   require('async'),
3
  Bloom         =   require('bloom-redis'),
4
  express       =   require('express'),
5
  redis         =   require('redis'),
6
  
7
  app,
8
  client,
9
  filter,
10
  
11
  // From Project Gutenberg - opening lines of the top 10 public domain ebooks

12
  // https://www.gutenberg.org/browse/scores/top

13
  openingLines = {
14
    'pride-and-prejudice' : 
15
      'It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.',
16
    'alices-adventures-in-wonderland' : 
17
      'Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, \'and what is the use of a book,\' thought Alice \'without pictures or conversations?\'',
18
    'a-christmas-carol' :
19
      'Marley was dead: to begin with.',
20
    'metamorphosis' : 
21
      'One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin.',
22
    'frankenstein'  : 
23
      'You will rejoice to hear that no disaster has accompanied the commencement of an enterprise which you have regarded with such evil forebodings.',
24
    'adventures-of-huckleberry-finn' : 
25
      'YOU don\'t know about me without you have read a book by the name of The Adventures of Tom Sawyer; but that ain\'t no matter.',
26
    'adventures-of-sherlock-holmes' :
27
      'To Sherlock Holmes she is always the woman.',
28
    'narrative-of-the-life-of-frederick-douglass' :
29
      'I was born in Tuckahoe, near Hillsborough, and about twelve miles from Easton, in Talbot county, Maryland.',
30
    'the-prince' :
31
      'All states, all powers, that have held and hold rule over men have been and are either republics or principalities.',
32
    'adventures-of-tom-sawyer' :
33
      'TOM!'
34
  };
35
36
37
app = express();
38
client = redis.createClient();
39
40
filter = new Bloom.BloomFilter({ 
41
  client    : client,
42
  key       : '3content-bloom-filter', //the Redis key

43
  size      : 2875518, // ~350kb

44
  //size      : 1024,

45
  numHashes : 20
46
});
47
48
app.get('/show-content/:user', function(req,res,next) {
49
  //we're going to be looping through the contentIds, checking to see if they are in the filter.

50
  //Since this spends time on each contentId wouldn't be advisable to do over a high number of contentIds

51
  //But, in this case the number of contentIds is small / fixed and our filter.contains function is fast, it is okay.

52
  var
53
    //creates an array of the keys defined in openingLines

54
    contentIds = Object.keys(openingLines),
55
    //getting part of the path from the URI

56
    user = req.params.user,
57
    checkingContentId,
58
    found = false,
59
    done = false;
60
   
61
  //since filter.contains is asynchronous, we're using the async library to do our looping 

62
  async.whilst(
63
    //check function, where our asynchronous loop will end

64
    function () { return (!found && !done); },
65
    function(cb) {
66
      //get the first item from the array of contentIds

67
      checkingContentId = contentIds.shift();
68
      
69
      //false means we're sure that it isn't in the filter

70
      if (!checkingContentId)  {
71
         done = true; // this will be caught by the check function above

72
         cb();
73
      } else {
74
        //concatenate the user (from the URL) with the id of the content

75
        filter.contains(user+checkingContentId, function(err, results) {
76
          if (err) { cb(err); } else {
77
            found = !results;
78
            cb();
79
          }
80
        });
81
      }
82
    },
83
    function(err) {
84
      if (err) { next(err); } else {
85
        if (openingLines[checkingContentId]) {
86
          //before we send the fresh contentId, let's add it to the filter to prevent it from showing again

87
          filter.add(
88
            user+checkingContentId, 
89
            function(err) {
90
              if (err) { next(err); } else {
91
                //send the fresh quote

92
                res.send(openingLines[checkingContentId]);
93
              }
94
            }
95
          );
96
        } else {
97
          res.send('no new content!');
98
        }
99
      }
100
    }
101
  );
102
});
103
104
app.listen(8011);

إذا انتبهت بعناية إلى وقت الذهاب والإياب في أدوات Dev ، فستلاحظ أنه كلما طلبت مسارًا واحدًا باسم مستخدم ، كلما طالت المدة. بينما يستغرق التحقق من عامل التصفية وقتًا ثابتًا ، في هذا المثال ، نتحقق من وجود المزيد من العناصر. تقتصر عوامل تصفية Bloom على ما يمكن أن يخبرك به ، لذلك فأنت تختبر وجود كل عنصر. بالطبع ، في مثالنا هذا بسيط إلى حد ما ، ولكن الاختبار لمئات العناصر سيكون غير فعال.

بيانات تالفة

في هذا المثال ، سنقوم ببناء خادم Express صغير يقوم بعمل أمرين: قبول بيانات جديدة عبر POST ، وعرض البيانات الحالية (مع طلب GET). عندما تكون البيانات الجديدة POST'ed إلى الخادم ، سيتحقق التطبيق من وجودها في الفلتر. إذا لم يكن موجودًا ، سنقوم بإضافته إلى مجموعة في Redis ، وإلا فسنعود إلى null. سيقوم طلب GET بجلبه من Redis وإرساله إلى العميل.

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

إذن ، ماذا نكسب؟ نكتسب سرعة عدم التحقق من المصدر الفعلي في كل مرة. في الحالات التي يكون فيها مصدر البيانات بطيئًا (واجهات برمجة التطبيقات الخارجية ، قواعد بيانات pokey ، وسط ملف ثابت) ، تكون هناك حاجة إلى زيادة السرعة. لشرح السرعة ، دعنا نضيف تأخيرًا واقعيًا يبلغ 150 مللي ثانية في مثالنا. سنستخدم أيضًا console.time /console.timeEnd لتسجيل الاختلافات بين فحص مرشح Bloom وفحص الفلتر غير Bloom.

في هذا المثال ، سنستخدم أيضًا عددًا محدودًا جدًا من البتات: 1024 فقط. سوف تملأ بسرعة.عندما تملأ ، ستظهر المزيد والمزيد من الإيجابيات الخاطئة - سترى زيادة وقت الاستجابة مع تملأ المعدل الإيجابي الخاطئ.

يستخدم هذا الخادم نفس الوحدات النمطية كما كان من قبل ، لذا قم بتعيين ملف app.jsإلى:

1
var
2
  async           =   require('async'),
3
  Bloom           =   require('bloom-redis'),
4
  bodyParser      =   require('body-parser'),
5
  express         =   require('express'),
6
  redis           =   require('redis'),
7
  
8
  app,
9
  client,
10
  filter,
11
  
12
  currentDataKey  = 'current-data',
13
  usedDataKey     = 'used-data';
14
  
15
app = express();
16
client = redis.createClient();
17
18
filter = new Bloom.BloomFilter({ 
19
  client    : client,
20
  key       : 'stale-bloom-filter',
21
  //for illustration purposes, this is a super small filter. It should fill up at around 500 items, so for a production load, you'd need something much larger!

22
  size      : 1024,
23
  numHashes : 20
24
});
25
26
app.post(
27
  '/',
28
  bodyParser.text(),
29
  function(req,res,next) {
30
    var
31
      used;
32
      
33
    console.log('POST -', req.body); //log the current data being posted

34
    console.time('post'); //start measuring the time it takes to complete our filter and conditional verification process

35
    
36
    //async.series is used to manage multiple asynchronous function calls.

37
    async.series([
38
      function(cb) {
39
        filter.contains(req.body, function(err,filterStatus) {
40
          if (err) { cb(err); } else {
41
            used = filterStatus;
42
            cb(err);
43
          }
44
        });
45
      },
46
      function(cb) {
47
        if (used === false) {
48
          //Bloom filters do not have false negatives, so we need no further verification

49
          cb(null);
50
        } else {
51
          //it *may* be in the filter, so we need to do a follow up check

52
          //for the purposes of the tutorial, we'll add a 150ms delay in here since Redis can be fast enough to make it difficult to measure and the delay will simulate a slow database or API call

53
          setTimeout(function() {
54
            console.log('possible false positive');
55
            client.sismember(usedDataKey, req.body, function(err, membership) {
56
              if (err) { cb(err); } else {
57
                //sismember returns 0 if an member is not part of the set and 1 if it is.

58
                //This transforms those results into booleans for consistent logic comparison

59
                used = membership === 0 ? false : true;
60
                cb(err);
61
              }
62
            });
63
          }, 150);
64
        }
65
      },
66
      function(cb) {
67
        if (used === false) {
68
          console.log('Adding to filter');
69
          filter.add(req.body,cb);
70
        } else {
71
          console.log('Skipped filter addition, [false] positive');
72
          cb(null);
73
        }
74
      },
75
      function(cb) {
76
        if (used === false) {
77
          client.multi()
78
            .set(currentDataKey,req.body) //unused data is set for easy access to the 'current-data' key

79
            .sadd(usedDataKey,req.body) //and added to a set for easy verification later

80
            .exec(cb); 
81
        } else {
82
          cb(null);
83
        }
84
      }
85
      ],
86
      function(err, cb) {
87
        if (err) { next(err); } else {
88
          console.timeEnd('post'); //logs the amount of time since the console.time call above

89
          res.send({ saved : !used }); //returns if the item was saved, true for fresh data, false for stale data.

90
        }
91
      }
92
    );
93
});
94
95
app.get('/',function(req,res,next) {
96
  //just return the fresh data

97
  client.get(currentDataKey, function(err,data) {
98
    if (err) { next(err); } else {
99
      res.send(data);
100
    }
101
  });
102
});
103
104
app.listen(8012);

بما أن POSTing إلى خادم يمكن أن تكون خادعة مع متصفح ، دعونا نستخدم curlللاختبار.

curl --data “your data goes here" --header "Content-Type: text/plain" http://localhost:8012/

يمكن استخدام برنامج نصي باش سريع لإظهار مدى ملاءمة الفلتر بأكمله:

1
#!/bin/bash

2
for i in `seq 1 500`;
3
do

4
  curl --data “data $i" --header "Content-Type: text/plain" http://localhost:8012/

5
done   

النظر في ملء أو مرشح كامل مثير للاهتمام. بما أن هذا الجهاز صغير ، يمكنك مشاهدته بسهولة مع redis-cli . من خلال تشغيلredis-cli تحصل على فلتر قديم من المحطة بين إضافة عناصر ، سترى زيادة البايت الفردي. سيكون عامل تصفية كامل \ xff لكل بايت. في هذه المرحلة ، سيعود الفلتر دائمًا إلى وضع إيجابي.

استنتاج

لا تعتبر مرشحات Bloom حلًا لكل دواء ، ولكن في الحالة المناسبة ، يمكن لمرشح Bloom توفير تكملة سريعة وفعالة لهياكل البيانات الأخرى.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.