rustis/commands/cuckoo_commands.rs
1use crate::{
2 client::{PreparedCommand, prepare_command},
3 resp::{BulkString, Response, Value, cmd, serialize_flag},
4};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// A group of Redis commands related to [`Cuckoo filters`](https://redis.io/docs/stack/bloom/)
9///
10/// # See Also
11/// [Cuckoo Filter Commands](https://redis.io/commands/?group=cf)
12pub trait CuckooCommands<'a>: Sized {
13 /// Adds an item to the cuckoo filter, creating the filter if it does not exist.
14 ///
15 /// Cuckoo filters can contain the same item multiple times, and consider each insert as separate.
16 /// You can use [`cf_addnx`](CuckooCommands::cf_addnx) to only add the item if it does not exist yet.
17 /// Keep in mind that deleting an element inserted using [`cf_addnx`](CuckooCommands::cf_addnx) may cause false-negative errors.
18 ///
19 /// # Arguments
20 /// * `key` - The name of the filter
21 /// * `item` - The item to add
22 ///
23 /// # See Also
24 /// * [<https://redis.io/commands/cf.add/>](https://redis.io/commands/cf.add/)
25 #[must_use]
26 fn cf_add(self, key: impl Serialize, item: impl Serialize) -> PreparedCommand<'a, Self, ()> {
27 prepare_command(self, cmd("CF.ADD").arg(key).arg(item))
28 }
29
30 /// Adds an item to a cuckoo filter if the item did not exist previously.
31 ///
32 /// See documentation on [`cf_add`](CuckooCommands::cf_add) for more information on this command.
33 ///
34 /// This command is equivalent to a [`cf_exists`](CuckooCommands::cf_exists) + [`cf_add`](CuckooCommands::cf_add) command.
35 /// It does not insert an element into the filter if its fingerprint already exists in order to use the available capacity more efficiently.
36 /// However, deleting elements can introduce `false negative` error rate!
37 ///
38 /// Note that this command is slower than [`cf_add`](CuckooCommands::cf_add) because it first checks whether the item exists.
39 ///
40 /// # Arguments
41 /// * `key` - The name of the filter
42 /// * `item` - The item to add
43 ///
44 /// # Return
45 /// * `true` - if the item did not exist in the filter.
46 /// * `false` - if the item already existed.
47 ///
48 /// # See Also
49 /// * [<https://redis.io/commands/cf.addnx/>](https://redis.io/commands/cf.addnx/)
50 #[must_use]
51 fn cf_addnx(
52 self,
53 key: impl Serialize,
54 item: impl Serialize,
55 ) -> PreparedCommand<'a, Self, bool> {
56 prepare_command(self, cmd("CF.ADDNX").arg(key).arg(item))
57 }
58
59 /// Returns the number of times an item may be in the filter.
60 ///
61 /// Because this is a probabilistic data structure, this may not necessarily be accurate.
62 ///
63 /// If you just want to know if an item exists in the filter,
64 /// use [`cf_exists`](CuckooCommands::cf_exists) because it is more efficient for that purpose.
65 ///
66 /// # Arguments
67 /// * `key` - The name of the filter
68 /// * `item` - The item to count
69 ///
70 /// # Return
71 /// the count of possible matching copies of the item in the filter.
72 ///
73 /// # See Also
74 /// * [<https://redis.io/commands/cf.count/>](https://redis.io/commands/cf.count/)
75 #[must_use]
76 fn cf_count(
77 self,
78 key: impl Serialize,
79 item: impl Serialize,
80 ) -> PreparedCommand<'a, Self, usize> {
81 prepare_command(self, cmd("CF.COUNT").arg(key).arg(item))
82 }
83
84 /// Deletes an item once from the filter.
85 ///
86 /// If the item exists only once, it will be removed from the filter.
87 /// If the item was added multiple times, it will still be present.
88 ///
89 /// # Danger !
90 /// Deleting elements that are not in the filter may delete a different item, resulting in false negatives!
91 ///
92 /// # Arguments
93 /// * `key` - The name of the filter
94 /// * `item` - The item to delete from the filter
95 ///
96 /// # Complexity
97 /// O(n), where n is the number of `sub-filters`. Both alternative locations are checked on all `sub-filters`.
98 ///
99 /// # Return
100 /// * `true` - the item has been deleted from the filter.
101 /// * `false` - if the item was not found.
102 ///
103 /// # See Also
104 /// * [<https://redis.io/commands/cf.del/>](https://redis.io/commands/cf.del/)
105 #[must_use]
106 fn cf_del(self, key: impl Serialize, item: impl Serialize) -> PreparedCommand<'a, Self, bool> {
107 prepare_command(self, cmd("CF.DEL").arg(key).arg(item))
108 }
109
110 /// Check if an `item` exists in a Cuckoo Filter `key`
111 ///
112 /// # Arguments
113 /// * `key` - The name of the filter
114 /// * `item` - The item to check for
115 ///
116 /// # Return
117 /// * `true` - the item may exist in the filter
118 /// * `false` - if the item does not exist in the filter.
119 ///
120 /// # See Also
121 /// * [<https://redis.io/commands/cf.exists/>](https://redis.io/commands/cf.exists/)
122 #[must_use]
123 fn cf_exists(
124 self,
125 key: impl Serialize,
126 item: impl Serialize,
127 ) -> PreparedCommand<'a, Self, bool> {
128 prepare_command(self, cmd("CF.EXISTS").arg(key).arg(item))
129 }
130
131 /// Return information about `key`
132 ///
133 /// # Arguments
134 /// * `key` - Name of the key to get info about
135 ///
136 /// # Return
137 /// An instance of [`CfInfoResult`](CfInfoResult)
138 ///
139 /// # See Also
140 /// * [<https://redis.io/commands/cf.info/>](https://redis.io/commands/cf.info/)
141 #[must_use]
142 fn cf_info(self, key: impl Serialize) -> PreparedCommand<'a, Self, CfInfoResult> {
143 prepare_command(self, cmd("CF.INFO").arg(key))
144 }
145
146 /// Adds one or more items to a cuckoo filter, allowing the filter to be created with a custom capacity if it does not exist yet.
147 ///
148 /// These commands offers more flexibility over the [`cf_add`](CuckooCommands::cf_add) command, at the cost of more verbosity.
149 ///
150 /// # Arguments
151 /// * `key` - The name of the filter
152 /// * `options` - see [`CfInsertOptions`](CfInsertOptions)
153 /// * `items` - One or more items to add.
154 ///
155 /// # See Also
156 /// * [<https://redis.io/commands/cf.insert/>](https://redis.io/commands/cf.insert/)
157 #[must_use]
158 fn cf_insert(
159 self,
160 key: impl Serialize,
161 options: CfInsertOptions,
162 item: impl Serialize,
163 ) -> PreparedCommand<'a, Self, Vec<bool>> {
164 prepare_command(
165 self,
166 cmd("CF.INSERT")
167 .arg(key)
168 .arg(options)
169 .arg("ITEMS")
170 .arg(item),
171 )
172 }
173
174 /// Adds one or more items to a cuckoo filter, allowing the filter to be created with a custom capacity if it does not exist yet.
175 ///
176 /// This command is equivalent to a [`cf_exists`](CuckooCommands::cf_exists) + [`cf_add`](CuckooCommands::cf_add) command.
177 /// It does not insert an element into the filter if its fingerprint already exists and therefore better utilizes the available capacity.
178 /// However, if you delete elements it might introduce `false negative` error rate!
179 ///
180 /// These commands offers more flexibility over the [`cf_add`](CuckooCommands::cf_add) and [`cf_addnx`](CuckooCommands::cf_addnx) commands,
181 /// at the cost of more verbosity.
182 ///
183 /// # Complexity
184 /// `O(n + i)`, where n is the number of `sub-filters` and `i` is `maxIterations`.
185 /// Adding items requires up to 2 memory accesses per `sub-filter`.
186 /// But as the filter fills up, both locations for an item might be full.
187 /// The filter attempts to `Cuckoo` swap items up to maxIterations times.
188 ///
189 /// # Arguments
190 /// * `key` - The name of the filter
191 /// * `options` - see [`CfInsertOptions`](CfInsertOptions)
192 /// * `items` - One or more items to add.
193 ///
194 /// # Return
195 /// A collection of integers corresponding to the items specified. Possible values for each element are:
196 /// * `>0` - if the item was successfully inserted
197 /// * `0` - if the item already existed and [`cf_insertnx`](CuckooCommands::cf_insertnx) is used.
198 /// * `<0` - if an error occurred
199 ///
200 /// # See Also
201 /// * [<https://redis.io/commands/cf.insert/>](https://redis.io/commands/cf.insert/)
202 #[must_use]
203 fn cf_insertnx<R: Response>(
204 self,
205 key: impl Serialize,
206 options: CfInsertOptions,
207 item: impl Serialize,
208 ) -> PreparedCommand<'a, Self, R> {
209 prepare_command(
210 self,
211 cmd("CF.INSERTNX")
212 .arg(key)
213 .arg(options)
214 .arg("ITEMS")
215 .arg(item),
216 )
217 }
218
219 /// Restores a filter previously saved using [`cf_scandump`](CuckooCommands::cf_scandump).
220 ///
221 /// See the [`cf_scandump`](CuckooCommands::cf_scandump) command for example usage.
222 ///
223 /// This command overwrites any bloom filter stored under `key`.
224 /// Make sure that the bloom filter is not be changed between invocations.
225 ///
226 /// # Arguments
227 /// * `key` - Name of the key to restore
228 /// * `iterator` - Iterator value associated with `data` (returned by [`cf_scandump`](CuckooCommands::cf_scandump))
229 /// * `data` - Current data chunk (returned by [`cf_scandump`](CuckooCommands::cf_scandump))
230 ///
231 /// # See Also
232 /// [<https://redis.io/commands/cf.loadchunk/>](https://redis.io/commands/cf.loadchunk/)
233 #[must_use]
234 fn cf_loadchunk(
235 self,
236 key: impl Serialize,
237 iterator: i64,
238 data: impl Serialize,
239 ) -> PreparedCommand<'a, Self, ()> {
240 prepare_command(self, cmd("CF.LOADCHUNK").arg(key).arg(iterator).arg(data))
241 }
242
243 /// Check if one or more `items` exists in a Cuckoo Filter `key`
244 ///
245 /// # Arguments
246 /// * `key` - The name of the filter
247 /// * `items` - One or more items to check for
248 ///
249 /// # Return
250 /// Collection reply of boolean - for each item where `true` value means the corresponding item
251 /// may exist in the filter, and a `false` value means it does not exist in the filter.
252 ///
253 /// # See Also
254 /// [<https://redis.io/commands/cf.mexists/>](https://redis.io/commands/cf.mexists/)
255 #[must_use]
256 fn cf_mexists<R: Response>(
257 self,
258 key: impl Serialize,
259 items: impl Serialize,
260 ) -> PreparedCommand<'a, Self, R> {
261 prepare_command(self, cmd("CF.MEXISTS").arg(key).arg(items))
262 }
263
264 /// Create a Cuckoo Filter as `key` with a single sub-filter for the initial amount of `capacity` for items.
265 /// Because of how Cuckoo Filters work, the filter is likely to declare itself full before `capacity` is reached
266 /// and therefore fill rate will likely never reach 100%.
267 /// The fill rate can be improved by using a larger `bucketsize` at the cost of a higher error rate.
268 /// When the filter self-declare itself `full`, it will auto-expand by generating additional sub-filters at the cost of reduced performance and increased error rate.
269 /// The new sub-filter is created with size of the previous sub-filter multiplied by `expansion`.
270 /// Like bucket size, additional sub-filters grow the error rate linearly.
271 /// The size of the new sub-filter is the size of the last sub-filter multiplied by `expansion`.
272 ///
273 /// The minimal false positive error rate is 2/255 ≈ 0.78% when bucket size of 1 is used.
274 /// Larger buckets increase the error rate linearly (for example, a bucket size of 3 yields a 2.35% error rate) but improve the fill rate of the filter.
275 ///
276 /// `maxiterations` dictates the number of attempts to find a slot for the incoming fingerprint.
277 /// Once the filter gets full, high `maxIterations` value will slow down insertions.
278 ///
279 /// Unused capacity in prior sub-filters is automatically used when possible. The filter can grow up to 32 times.
280 ///
281 /// # Arguments
282 /// * `key` - The key under which the filter is found
283 /// * `capacity` - Estimated capacity for the filter.
284 /// Capacity is rounded to the next 2^n number.
285 /// The filter will likely not fill up to 100% of it's capacity.
286 /// Make sure to reserve extra capacity if you want to avoid expansions.
287 /// * `options` - See [`CfReserveOptions`](CfReserveOptions)
288 ///
289 /// # See Also
290 /// [<https://redis.io/commands/cf.reserve/>](https://redis.io/commands/cf.reserve/)
291 #[must_use]
292 fn cf_reserve(
293 self,
294 key: impl Serialize,
295 capacity: usize,
296 options: CfReserveOptions,
297 ) -> PreparedCommand<'a, Self, ()> {
298 prepare_command(self, cmd("CF.RESERVE").arg(key).arg(capacity).arg(options))
299 }
300
301 /// Begins an incremental save of the cuckoo filter.
302 /// This is useful for large cuckoo filters which cannot fit into the normal [`dump`](crate::commands::GenericCommands::dump)
303 /// and [`restore`](crate::commands::GenericCommands::restore) model.
304 ///
305 /// # Arguments
306 /// * `key` - Name of the filter
307 /// * `iterator` - Iterator value; either 0 or the iterator from a previous invocation of this command.\
308 /// The first time this command is called, the value of `iterator` should be 0.
309 ///
310 /// # Return
311 /// This command returns successive `(iterator, data)` pairs until `(0, vec![])` to indicate completion.
312 ///
313 /// # See Also
314 /// [<https://redis.io/commands/cf.scandump/>](https://redis.io/commands/cf.scandump/)
315 #[must_use]
316 fn cf_scandump(
317 self,
318 key: impl Serialize,
319 iterator: i64,
320 ) -> PreparedCommand<'a, Self, CfScanDumpResult> {
321 prepare_command(self, cmd("CF.SCANDUMP").arg(key).arg(iterator))
322 }
323}
324
325/// Result for the [`cf_info`](CuckooCommands::cf_info) command.
326#[derive(Debug, Deserialize)]
327pub struct CfInfoResult {
328 /// Size
329 #[serde(rename = "Size")]
330 pub size: usize,
331 /// Number of buckets
332 #[serde(rename = "Number of buckets")]
333 pub num_buckets: usize,
334 /// Number of filters
335 #[serde(rename = "Number of filters")]
336 pub num_filters: usize,
337 /// Number of items inserted
338 #[serde(rename = "Number of items inserted")]
339 pub num_items_inserted: usize,
340 /// Number of items deleted
341 #[serde(rename = "Number of items deleted")]
342 pub num_items_deleted: usize,
343 /// Bucket size
344 #[serde(rename = "Bucket size")]
345 pub bucket_size: usize,
346 /// Expansion rate
347 #[serde(rename = "Expansion rate")]
348 pub expansion_rate: usize,
349 /// Max iteration
350 #[serde(rename = "Max iterations")]
351 pub max_iteration: usize,
352 /// Additional information
353 #[serde(flatten)]
354 pub additional_info: HashMap<String, Value>,
355}
356
357/// Options for the [`cf_insert`](CuckooCommands::cf_insert) command.
358#[derive(Default, Serialize)]
359#[serde(rename_all(serialize = "UPPERCASE"))]
360pub struct CfInsertOptions {
361 #[serde(skip_serializing_if = "Option::is_none")]
362 capacity: Option<u32>,
363 #[serde(
364 skip_serializing_if = "std::ops::Not::not",
365 serialize_with = "serialize_flag"
366 )]
367 nocreate: bool,
368}
369
370impl CfInsertOptions {
371 /// Specifies the desired capacity of the new filter, if this filter does not exist yet.
372 ///
373 /// If the filter already exists, then this parameter is ignored.
374 /// If the filter does not exist yet and this parameter is not specified,
375 /// then the filter is created with the module-level default capacity which is `1024`.
376 /// See [`cf_reserve`](CuckooCommands::cf_reserve) for more information on cuckoo filter capacities.
377 #[must_use]
378 pub fn capacity(mut self, capacity: u32) -> Self {
379 self.capacity = Some(capacity);
380 self
381 }
382
383 /// If specified, prevents automatic filter creation if the filter does not exist.
384 ///
385 /// Instead, an error is returned if the filter does not already exist.
386 /// This option is mutually exclusive with [`capacity`](CfInsertOptions::capacity).
387 #[must_use]
388 pub fn nocreate(mut self) -> Self {
389 self.nocreate = true;
390 self
391 }
392}
393
394/// Options for the [`cf_reserve`](CuckooCommands::cf_reserve) command.
395#[derive(Default, Serialize)]
396#[serde(rename_all(serialize = "UPPERCASE"))]
397pub struct CfReserveOptions {
398 #[serde(skip_serializing_if = "Option::is_none")]
399 bucketsize: Option<u32>,
400 #[serde(skip_serializing_if = "Option::is_none")]
401 maxiterations: Option<u32>,
402 #[serde(skip_serializing_if = "Option::is_none")]
403 expansion: Option<u32>,
404}
405
406impl CfReserveOptions {
407 /// Number of items in each bucket.
408 ///
409 /// A higher bucket size value improves the fill rate but also causes a higher error rate and slightly slower performance.
410 /// The default value is 2.
411 #[must_use]
412 pub fn bucketsize(mut self, bucket_size: u32) -> Self {
413 self.bucketsize = Some(bucket_size);
414 self
415 }
416
417 /// Number of attempts to swap items between buckets before declaring filter as full and creating an additional filter.
418 ///
419 /// A low value is better for performance and a higher number is better for filter fill rate.
420 /// The default value is 20.
421 pub fn maxiterations(mut self, max_iterations: u32) -> Self {
422 self.maxiterations = Some(max_iterations);
423 self
424 }
425
426 /// When a new filter is created, its size is the size of the current filter multiplied by `expansion`.
427 /// Expansion is rounded to the next `2^n` number.
428 /// The default value is 1.
429 #[must_use]
430 pub fn expansion(mut self, expansion: u32) -> Self {
431 self.expansion = Some(expansion);
432 self
433 }
434}
435
436/// Result for the [`cf_scandump`](CuckooCommands::cf_scandump) command.
437#[derive(Debug, Deserialize)]
438pub struct CfScanDumpResult {
439 pub iterator: i64,
440 pub data: BulkString,
441}