Retriever is an embedded, in-memory, document-oriented data store for rust applications. It stores ordinary rust data types in a similar manner as a NoSQL database.
Retriever is ideal when you need to index a collection by multiple properties, you need a variety of relations between elements in a collection, or or you need to maintain summary statistics about a collection.
(Image of Callie, a golden retriever puppy, by Wikimedia Commons user MichaelMcPhee.)
Storage::raw()
for an example.```rust use retriever::prelude::; use std::borrow::Cow; use chrono::prelude::; // Using rust's Chrono crate to handle date/time // (just for this example, you don't need it) use std::collections::HashSet;
// This example is going to be about a puppy rescue agency
struct Puppy {
name: String,
rescueddate: Date
// Some convenience functions for describing puppies in source code
impl Puppy {
fn new(name: &str, rescueddate: Date
fn adopted(mut self, adopteddate: Date
fn breeds(mut self, breeds: &[&str]) -> Puppy { self.breed.extend(breeds.iter().map(|breed| String::from(*breed))); self }
fn parent(mut self, year: i32, name: &str) -> Puppy { self.parents.insert(ID.chunk(year).item(String::from(name))); self } }
// We need to implement Record for our Puppy type.
// Because of this design, we can never have two puppies with same name
// rescued in the same year. They would have the same Id.
impl Record
fn item_key(&self) -> Cow
// Let's create a storage of puppies.
let mut storage : Storage
storage.add( Puppy::new("Lucky", Utc.ymd(2019, 3, 27)) .adopted(Utc.ymd(2019, 9, 13)) .breeds(&["beagle"]) );
// Add some example puppies to work with storage.add( Puppy::new("Spot", Utc.ymd(2019, 1, 9)) .breeds(&["labrador", "dalmation"]) .parent(2010, "Yeller") );
storage.add( Puppy::new("JoJo", Utc.ymd(2018, 9, 2)) .adopted(Utc.ymd(2019, 5, 1)) .breeds(&["labrador","shepherd"]) .parent(2010, "Yeller") );
storage.add( Puppy::new("Yeller", Utc.ymd(2010, 8, 30)) .adopted(Utc.ymd(2013, 12, 24)) .breeds(&["labrador"]) );
// Get all puppies rescued in 2019: let q = Chunks([2019]); let mut rescued2019 : Vec<_> = storage.query(&q) .map(|puppy: &Puppy| &puppy.name).collect(); rescued2019.sort(); // can't depend on iteration order! asserteq!(vec!["Lucky","Spot"], rescued2019);
// Get all puppies rescued in the last 3 years: let q = Chunks(2017..=2019); let mut rescuedrecently : Vec<_> = storage.query(&q) .map(|puppy: &Puppy| &puppy.name).collect(); rescuedrecently.sort(); asserteq!(vec!["JoJo","Lucky","Spot"], rescuedrecently);
// Get all puppies rescued in march: let q = Everything.filter(|puppy: &Puppy| puppy.rescueddate.month() == 3); let mut rescuedinmarch : Vec<_> = storage.query(&q) .map(|puppy| &puppy.name).collect(); rescuedinmarch.sort(); asserteq!(vec!["Lucky"], rescuedinmarch);
// Fix spelling of "dalmatian" on all puppies: let q = Everything.filter(|puppy : &Puppy| puppy.breed.contains("dalmation")); storage.modify(&q, |mut editor| { let puppy = editor.getmut(); puppy.breed.remove("dalmation"); puppy.breed.insert(String::from("dalmatian")); }); asserteq!(0, storage.iter().filter(|x| x.breed.contains("dalmation")).count()); assert_eq!(1, storage.iter().filter(|x| x.breed.contains("dalmatian")).count());
// Set up an index of puppies by their parent. // In SecondaryIndexes, we always return a collection of secondary keys. // (In this case, a HashSet containing the Ids of the parents.) let mut by_parents = SecondaryIndex::new(&storage, |puppy: &Puppy| Cow::Borrowed(&puppy.parents));
// Use an index to search for all children of Yeller: let yellerid = ID.chunk(2010).item(String::from("Yeller")); let q = Everything.matching(&mut byparents, Cow::Borrowed(&yellerid)); let mut childrenofyeller : Vec<_> = storage.query(&q) .map(|puppy: &Puppy| &puppy.name).collect(); childrenofyeller.sort(); asserteq!(vec!["JoJo","Spot"], childrenofyeller);
// Remove puppies who have been adopted more than five years ago. let q = Chunks(0..2014).filter(|puppy: &Puppy| puppy.adopteddate.map(|date| date.year() <= 2014).unwrapor(false)); assert!(storage.get(&yellerid).issome()); storage.remove(&q, std::mem::drop); assert!(storage.get(&yellerid).isnone()); ```
Unlike most databases, retriever stores your data as a plain old rust data type inside heap memory. (Specifically, each chunk has a Vec that stores all of the data for that chunk.) It doesn't support access over a network from multiple clients.
Like a traditional database, retriever has a flexible indexing and query system and can model many-to-many relationships between records.
Retriever can be used as a servicable component store, because records that share the same keys are easy to cross-reference with each other. But Retriever is not designed specifically for game projects, and it tries to balance programmer comfort with reliability and performance.
ECSs use low-cardinality indexes to do an enormous amount of work very quickly. Retriever uses high-cardinality indexes to avoid as much work as possible.
If you know you need to use Data Oriented Design then you might consider an ECS like specs or legion.
Clone + Debug + Eq + Hash + Ord
. See ValidKey
.Storage::new()
.Storage::add()
, Storage::iter()
, Storage::query()
, Storage::modify()
, and
Storage::remove()
to implement CRUD operations on your storage.SecondaryIndex::new()
. Define
secondary indexes by writing a single closure that maps records into zero or more secondary
keys.Reduction::new()
. Define reductions by writing
two closures: (1) A map from the record type to a summary type, and (2) a fold
of several summary objects into a single summary.
Use Reduction::reduce()
to reduce your entire storage to a single summary object, or
Reduction::reduce_chunk()
to reduce a single chunk to a single summary object.Reduction
on only part of your storage, then that part must be defined
as a single chunk. In the future, I want to implement convolutional reductions that map onto
zero or more chunks.Retriever makes heavy use of Cow
to represent various kinds of index keys. Using Cow
allows retriever to bridge a wide
range of use cases.
A Cow<T>
is usually either Cow::Owned(T)
or Cow::Borrowed(&T)
. The generic parameter refers
to the borrowed form, so Cow<str>
is either Cow::Owned<String>
or Cow::Borrowed<&str>
.
Whenever you see a ChunkKey
, ItemKey
, or IndexKey
, these keys follow the same convention.
These are good:
Record<i64,str>
Record<i64,&'static str>
Record<i64,Arc<String>>
This will work for the most part but it's weird:
Record<i64,String>
Retriever is licensed under your choice of either the ISC license (a permissive license) or the AGPL v3.0 or later (a strong copyleft license).
The photograph of the puppy is by Wikimedia Commons user MichaelMcPhee. Creative Commons Attribution 3.0 Unported. (Source)
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in retriever by you, shall be licensed as ISC OR AGPL-3.0-or-later, without any additional terms or conditions.
At this stage, bug reports and questions about any unclear documentation are highly valuable. I consider it appropriate to open a ticket just for technical support. I'm also interested in any suggestions that would help further simplify the codebase.
License: ISC OR GPL-3.0-or-later