feat(cf): add cf.add command#3481
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces initial CuckooFilter support to KVrocks (task #3351) by adding the CF.RESERVE and CF.ADD commands, along with a bucket-per-key RocksDB storage implementation and accompanying unit tests.
Changes:
- Add a new Redis metadata type (
kRedisCuckooFilter) andCuckooChainMetadataencoding/decoding. - Implement a bucket-based cuckoo filter chain (
redis::CuckooChain) withReserveandAddoperations. - Register new commands (
cf.reserve,cf.add) and add a comprehensive C++ unit test suite.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/cppunit/types/cuckoo_filter_test.cc | Adds unit tests for CF reserve/add behavior and cuckoo filter helper functions. |
| src/types/redis_cuckoo_chain.h | Declares the cuckoo chain DB wrapper and CF defaults. |
| src/types/redis_cuckoo_chain.cc | Implements CF.RESERVE/CF.ADD logic using per-bucket RocksDB keys plus kick-out/expand paths. |
| src/types/cuckoo_filter.h | Adds cuckoo filter hash/fingerprint/alt-hash helpers and bucket-count calculation. |
| src/storage/redis_metadata.h | Introduces the new Redis type and CuckooChainMetadata structure. |
| src/storage/redis_metadata.cc | Implements encode/decode and capacity calculation for CuckooChainMetadata. |
| src/commands/commander.h | Adds a new command category for CuckooFilter commands. |
| src/commands/cmd_cuckoo_filter.cc | Implements and registers cf.reserve and cf.add command handlers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Hi @nagisa-kunhah. Thank you for your contribution. Additionally, please follow our AI policy: https://kvrocks.apache.org/community/contributing#guidelines-for-ai-assisted-contributions. Please let us know which AI tools and models you used, as this will help us better review the code. |
|
@jihuayu Hi, thank you for the review. I have summarized the design and implementation details below to explain the layering, responsibilities, and key storage decisions. 1 Design OverviewThe design follows Kvrocks' existing layered architecture. Conceptually, the change can be divided into four layers:
1.1 Command layerThe command layer introduces RedisBloom-compatible Cuckoo Filter commands, such as 1.2 Type layerThe type layer introduces the new The reason for using a chain abstraction is that Cuckoo Filters are not easy to resize in place: the bucket positions depend on the current bucket count, so resizing a filter would require rebuilding existing data. Instead, the design appends new sub-filters when expansion is needed. This model is inspired by RedisBloom's scalable Cuckoo Filter design.
To keep the type layer separated from the algorithm details, 1.3 Metadata/storage encoding layerAt the metadata/storage encoding layer, the implementation adds The Cuckoo-specific fields describe the filter chain, including the number of sub-filters, base capacity, bucket size, expansion factor, and insertion iteration limit. This keeps the logical filter state compact while leaving the actual bucket contents in subkeys. 1.4 RocksDB persistence layerAt the persistence layer, the implementation reuses Kvrocks' existing metadata/subkey model. The logical key metadata is stored as metadata, and individual buckets are stored as internal subkeys. This follows the same general pattern used by other complex Redis data structures in Kvrocks. The metadata entry and bucket entries are written through RocksDB write batches when they need to be updated together. This keeps the logical filter state and the modified bucket data consistent without introducing a separate persistence path for Cuckoo Filter. 2 Logical StructureA logical Cuckoo Filter is associated with one user key. Internally, it is represented as a chain of sub-filters rather than a single resizable filter. The relationship between the main concepts is:
3 Implementation Details3.1 Data Layout3.1.1 MetadataEach logical Cuckoo Filter key has one The metadata is stored in the Conceptually: The metadata contains the following fields:
This metadata belongs to the logical Cuckoo Filter as a whole. It is not stored per bucket or per sub-filter. 3.1.2 Bucket StorageThe bucket data is stored as internal subkeys under the logical Cuckoo Filter key. Each bucket is identified by both a The bucket subkey is constructed from:
The bucket value is a fixed-size byte array whose length is Conceptually, the bucket layout is: Here, This layout keeps the logical filter metadata separate from the bucket contents, while still reusing Kvrocks' existing internal subkey model. 3.2 Hashing Model3.2.1 Item HashEach item is first converted into a 64-bit hash using The item hash is the base value used to derive both the fingerprint and the candidate bucket positions. Since these values determine where the item is stored and looked up, the hash function is part of the persistent data layout and should remain stable once data has been written. 3.2.2 FingerprintThe fingerprint is generated from the item hash as 3.2.3 Candidate BucketsFor each sub-filter, an item has two candidate buckets. The first bucket is derived directly from the item hash: The second bucket is derived from both the hash and the fingerprint: The constant During kick-out insertion, the original item hash of an evicted fingerprint is no longer available. Instead, the implementation uses the current bucket index and the fingerprint to compute the alternate bucket: This is valid because This lets the kick-out path move a fingerprint between its two candidate buckets using only the current bucket index and the fingerprint. 3.3 Current Write Path3.3.1 CF.RESERVE
The initial metadata records the base capacity, bucket size, maximum insertion iterations, expansion factor, and initializes The implementation does not preallocate all buckets during reserve. Buckets are created lazily when they are first written. This keeps 3.3.2 CF.ADD
For each sub-filter in the chain, the implementation derives the two candidate buckets for the item. It reads these buckets, treats missing buckets as empty buckets, and tries to place the fingerprint into any available slot in either bucket. For insertion, sub-filters are checked from the first one to the latest one, in This is different from RedisBloom, which checks sub-filters from the latest one back to the first one. If a free slot is found, the updated bucket data and the updated metadata are written in the same write batch. This keeps the bucket content and the logical filter state updated atomically. If no free slot is available in the candidate buckets, the implementation falls back to kick-out insertion on the latest sub-filter. The kick-out path relocates existing fingerprints between their candidate buckets and writes all modified buckets together when the insertion succeeds. 3.3.3 ExpansionExpansion is triggered when insertion cannot find a free slot and kick-out insertion also fails. Instead of resizing an existing sub-filter in place, the implementation appends a new sub-filter to the chain. The new sub-filter is represented by increasing The existing buckets are not rebuilt or moved during expansion. This avoids rewriting existing filter data. After expansion, the insertion is retried against the newly added sub-filter. |
|
Thanks for the proposal! These images are beautiful. Hi @git-hulk @torwig @PragmaTwice @aleksraiden @LiuQhahah. |
|
Hi @nagisa-kunhah. Regarding the current proposal, I have the following suggestions: Paging the BucketsI believe that assigning one key per bucket will lead to a massive number of small keys, which is fatal to the system's performance. The overhead of RocksDB internal keys, memtables, index/filters, and compaction will be significantly greater than one or a few bytes. I suggest we introduce a Page abstraction for buckets, where one page contains multiple buckets. For example, a 1KB page could contain 256 buckets. I have performed a rough estimation as follows: Full Page Utilization
Note on "Buckets Needed to Break Even": This represents the minimum number of buckets that must be used within a page for the "paged" approach to become more space-efficient than the "bucket-per-key" approach. This occurs at approximately 5% occupancy. Scenario for Default Capacity = 1024
Based on these findings, I recommend a default page size of 2KB or 4KB. Using MultiGet / Batch Read for Candidate Buckets/PagesOperations like Insertion Order: Prioritize Latest -> OldRedisBloom queries from the newest sub-filter to the oldest. In Kvrocks, if we proceed from old to new, every write operation will first hit the older, fuller filters. This is likely to increase read amplification and the probability of "kick-outs." I suggest maintaining consistency with RedisBloom's "latest -> old" approach. |
|
@jihuayu Thanks for the suggestions. My understanding is that the Page abstraction is a persistent storage-layout unit, not an application-level cache. I plan to replace the current bucket key layout: with a page-based layout: The bucket mapping would be: Each sub-filter would own its own set of pages, so pages are not shared across sub-filters. Also, I would treat For the first version, I would like to use a fixed internal page size, likely Could you confirm whether this matches your expectation, especially:
For the suggestions about |
|
@nagisa-kunhah Your understanding is spot on. There’s no need to rush into code changes just yet, as others might still chime in with feedback. We can wait until everyone is on the same page before you start refactoring.
I agree with you. Storing the page size makes sense; it will definitely make future extensions much easier to handle. |
|
@jihuayu Excuse me, I’ve added the paging implementation and now included CuckooPageSet. The overall architecture is illustrated in the attached diagram. The diff is a bit large — would you recommend splitting it into smaller PRs for easier review? |
|
Hi @nagisa-kunhah The review might take longer since the PR is quite large. By the way, your image is very clear, and I really like it. |


task id: #3351