Reverse Maps
This page focuses on the provided reverse-map implementations, the wrapper's structural interface, and implementing a custom reverse map. A reverse map stores the original key associated with each minirun ID and rank.
Multiple entries can have the same base fingerprint. The quotient and base remainder identify a minirun, and the rank identifies one entry within that minirun. The reverse map maintains the following association:
(minirun ID, rank) -> original key
The minirun ID is the quotient and base remainder that the filter stores for an entry. The rank is not a global identifier; it is the entry's position among entries with the same minirun ID.
Adaptation Flow
When a query returns a positive result, the application must first determine whether it is actually a false positive. If it is, the C++ wrapper derives the minirun ID from the query hash and uses it with the query rank to recover the original key:
queryFilter()returns a positive result with the matching hash and rank.- The application confirms that the query key is not in the original set.
adaptOnce()derives the minirun ID and callsReverseMap::getFingerprint()with it and the rank.- The reverse map returns the original key, which the wrapper passes to the C AQF adaptation function along with the false-positive query key.
The C API does not perform this reverse-map lookup. Applications using the C
API must perform the lookup themselves and pass the recovered original key to
aqf_adapt().
Provided Implementations
| Implementation | Location | Storage | Persistence | Intended use |
|---|---|---|---|---|
LocalMap |
reverse_maps/localmap/localmap_db.hpp |
In-memory STL containers | No | Demonstrations and tests |
SplinterDB |
reverse_maps/splinterdb/splinter_reverse_map.hpp |
SplinterDB | Yes, when opened without overwrite | Larger or persistent stores |
LocalMap
LocalMap is a header-only implementation backed by
in-memory STL containers. It requires no external storage dependency and is the
reverse map used by the basic example. It is volatile and intended for
demonstrations and tests rather than persistent storage.
SplinterDB
SplinterDB stores encoded fingerprint/rank keys in a SplinterDB database. Its
configuration uses:
storage_pathas the database file path.cache_size_bytesas the SplinterDB cache size.overwriteto choose whether existing data is removed before initialization.
With overwrite = true, initialization removes and recreates the existing
database. With overwrite = false, initialization attempts to open the
existing database. The repository's demo uses overwrite = true and removes
its database when it exits, so the demo does not demonstrate reopening data
across process restarts. See the building guide for dependencies
and build commands.
ReverseMapConfig
The wrapper passes a ReverseMapConfig to the reverse map during
initialization:
struct ReverseMapConfig {
std::string storage_path;
uint64_t cache_size_bytes;
bool overwrite;
};
The fields are implementation-specific. LocalMap ignores all of them;
persistent implementations may use them to select a database, configure a
cache, and decide whether to remove existing data. A custom implementation
should document its interpretation of each field.
Implementing Your Own Reverse Map
AdaptiveQF<ReverseMap> uses a structural C++ template requirement. There is
no formal base class to inherit from. A compatible type must provide these
methods:
struct ReverseMap {
int init(ReverseMapConfig config, int base_fingerprint_bits);
int insert(uint64_t fingerprint, uint64_t rank, uint64_t key);
void bulkInsert(uint64_t *keys, uint64_t *fingerprints,
uint64_t *ranks, uint64_t numKeys);
int getFingerprint(uint64_t fingerprint, int rank, uint64_t *value);
int close();
};
The method names and argument types must match the operations used by the
wrapper. The implementation must preserve the fingerprint/rank identity from
insertion through lookup. getFingerprint returns the original key through
value; the name reflects the legacy API and does not mean that it returns a
new fingerprint.
Pseudocode Design
The following is deliberately pseudocode, not a complete implementation. It shows the required data flow without prescribing a storage engine:
class MyReverseMap {
int init(ReverseMapConfig config, int base_fingerprint_bits) {
// Open or create storage using config.
// Save base_fingerprint_bits if the key encoding needs it.
return storage.open(config.storage_path, config.overwrite);
}
int insert(uint64_t fingerprint, uint64_t rank, uint64_t key) {
auto storage_key = encode(fingerprint, rank);
return storage.put(storage_key, key) ? 0 : -1;
}
void bulkInsert(uint64_t *keys, uint64_t *fingerprints,
uint64_t *ranks, uint64_t numKeys) {
for (uint64_t i = 0; i < numKeys; ++i) {
insert(fingerprints[i], ranks[i], keys[i]);
}
}
int getFingerprint(uint64_t fingerprint, int rank, uint64_t *value) {
auto storage_key = encode(fingerprint, rank);
if (!storage.get(storage_key, value)) return -1;
return 0;
}
int close() {
return storage.close() ? 0 : -1;
}
};
In a real implementation, decide how to handle duplicate keys, insertion
failure, transactions, reopening, corrupted storage, and concurrent access.
The current bulkInsert signature returns void, so failures from individual
insertions cannot be reported through that method without changing the
interface.
Implementation Checklist
- Choose an encoding for
(fingerprint, rank)that is stable across process restarts. - Store the original key as the value for that encoded identity.
- Use the same encoding in
insert,bulkInsert, andgetFingerprint. - Define the behavior of
storage_path,cache_size_bytes, andoverwrite. - Return
0for successful operations and a negative value for lookup or storage failures where the interface allows a return value. - Instantiate the wrapper as
AdaptiveQF<YourReverseMap>and link the AQF library with any storage-library dependencies. - Test that every inserted fingerprint/rank pair retrieves the expected key after reopening the storage.
The encoding must preserve the full base fingerprint and rank without collisions across process restarts. It must also use the same widths and byte order for insertion and lookup.
See the API Reference for the wrapper surface, the Getting Started tutorial for usage, and the Building guide for the supplied SplinterDB build path.