#database #tree #data-structures #trie #zerocopy


a radix tree data structure for in memory or zero copy on disk storage

6 releases

0.2.5 Sep 30, 2022
0.2.4 Sep 29, 2022

#977 in Data structures

Download history 3/week @ 2022-11-27 5/week @ 2022-12-04 19/week @ 2022-12-11 14/week @ 2022-12-18 2/week @ 2022-12-25 162/week @ 2023-01-01 390/week @ 2023-01-08 158/week @ 2023-01-15 58/week @ 2023-01-22 305/week @ 2023-01-29 228/week @ 2023-02-05 364/week @ 2023-02-12 292/week @ 2023-02-19 326/week @ 2023-02-26 603/week @ 2023-03-05 531/week @ 2023-03-12

1,758 downloads per month



RadixDB   Latest Version Docs Badge Build status

An efficient radix tree that can be used as an in-memory collection like BTreeMap, or as a primitive database using a custom storage backend.

Detailed documentation at https://docs.rs/radixdb/.


A radix tree data structure

A radix tree is a map data structure that has fast and memory efficient storage of keys that have common prefixes. It can be used as a set by using a unit value.

This radix tree is using blobs as keys and values. A common use case is to use UTF-8 strings as keys.

The lookup performance of this radix tree is roughly in the same range as a [std::collections::BTreeMap].

Where it shines is when you have long keys that frequently have a common prefix, like e.g. namespaced identifiers. In that case it will use much less memory than storing the keys as a whole.

It will also be extremely fast in selecting a subtree given a prefix, like for example for autocompletion.

Elements in a radix tree as lexicographically sorted.

Basic usage

Basic usage is not that different from using a std collection such as BTreeMap.


# use radixdb::*;
let mut dict = RadixTree::default();
dict.insert("dog", "Hund");
dict.insert("cat", "Katze");
for (k, v) in dict.iter() {
  println!("{:?} {:?}", k, v);

Bulk operations

RadixTree supports bulk operations. These are modeled somewhat after database relational operations. Bulk operations take a combiner function to specify how to handle collisions.

Bulk operations are much faster than adding elements one by one, especially if the two radix trees are disjoint.


# use radixdb::*;
// use the radixtree macro to create a set of strings. This works because &str implements AsRef<[u8]>.
let mut collections = radixtree! {
// create another set that is disjoint
let formatting = radixtree! {
// in place union of the two sets
let mut all = collections;
all.outer_combine_with(&formatting, |a, b| {});
// find identifiers, e.g. for autocompletion
for (k, _) in all.scan_prefix("std::c") {
  println!("{:?}", k);

Using a custom blob storage

You can provide a custom store for a radix tree, which can be either a contiguous slice of memory, a file on disk, or a custom storage backend. Custom storage backends are enabled using the custom-storage feature.

The storage is usually fallible, e.g. when reading from a disk or network. When using a fallible storage, every interaction with a radix tree can fail. Therefore there is a fallible version of all methods.


# use radixdb::*;
# use store::BlobStore;
# fn test() -> anyhow::Result<()> {
// build a small tree as above
let mut dict = RadixTree::default();
dict.insert("dog", "Hund");
dict.insert("cat", "Katze");
// attach it to a store - here we use a memstore, but typically you would use a file store
let store = store::MemStore::default();
let mut dict = dict.try_attached(store.clone())?;
// the store is fallible, so we have to use the try_... variants for interacting with it
for r in dict.try_iter() {
  if let Ok((k, v)) = r {
    println!("{:?} {:?}", k, v);
// add another entry. This is now in memory
dict.try_insert("rabbit", "Hase")?;
// persist all changes to the store
// sync the store to disk
# Ok(())
# }

Data format

Radix trees can be very quickly serialized and deserialized. Using a custom store, they can also be traversed and queried without deserializing at all. This is useful to query a very large tree that does not fit into memory.

Serialized format

Each tree node consists of prefix, optional value, and optional children. Either value or children must be defined for a valid node. An empty prefix is only allowed for the root node. Prefix and value can be stored either directly or indirectly via an id. Children is so far only stored as id.

Prefix, value and children are each serialized as a header byte followed by up to 127 value bytes.

Header byte format

The highest bit of the header byte is used to distinguish between inline value (0) and id value (1). The lower 7 bits are the length.

An id with a length of 0 is used as a special value for None. This is only valid for value and children. Likewise, data with the length of 0 is used to store the empty array.

Since the longest possible length is 127, data longer than 127 bytes must always be stored indirectly.

Prefix value

When a prefix is stored indirectly, an extra byte is used to store the first byte of the prefix.


Simplest possible node

{ "" => "" }

00    | prefix: 0 byte long literal ""
  00  | value: 0 byte long literal ""
    80| children: None

Small leaf node

Small values are stored inline

{ "hello" => "world" }

0568656c6c6f                | prefix: 5 byte long literal "hello"
            06776f726c6421  | value: 6 byte long literal "world!"
                          80| children: None

Large leaf node

Large values above 127 bytes must be stored indirectly. The first byte of the prefix is prepended to the id to simplify searching. For the value, just the id is stored.

In this case the id is 8 bytes long, but the details depend on the store. E.g. a store that uses content addressing might use a 32 byte cryptographic hash.

{ [b'a'; 256] => [b'b'; 256] }

89610000000000000001                    | prefix: first char b"a", 8 byte long id
                    880000000000000002  | value: 8 byte long id
                                      80| no children

Branch node

For a branch node, childfen are always stored indirectly. A record size byte is added to simplify indexing. The record size is set to

{ "aa" => "", "ab" => "" }

8161                      | prefix: 1 byte long literal "a"
    80                    | value: None
      89040000000000000001| children: constant record size 4u8, 8 byte long id

The child id refers to a block of data that contains the 2 children. Both children have a size of 4 bytes.

0161            | prefix: 1 byte long literal "a"
    00          | value: 0 byte long literal ""
      80        | children: None
        0162    | prefix: 1 byte long literal "b"
            00  | value: 0 byte long literal ""
              80| children: None


~88K SLoC