retriever

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.

Callie, a golden retriever puppy. (Image of Callie, a golden retriever puppy, by Wikimedia Commons user MichaelMcPhee.)

Features:

Retriever does not have:

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, adopteddate: Option>, breed: HashSet, parents: HashSet>, }

// Some convenience functions for describing puppies impl Puppy { fn new(name: &str, rescueddate: Date) -> Puppy { Puppy { name: String::from(name), rescueddate, adopted_date: None, breed: HashSet::default(), parents: HashSet::default(), } }

fn withadopteddate(mut self, adopteddate: Date) -> Puppy { self.adopteddate = Some(adopted_date); self }

fn with_breeds(mut self, breeds: &[&str]) -> Puppy { self.breed.extend(breeds.iter().map(|breed| String::from(*breed))); self }

fn with_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. // We choose the year the puppy was rescued as the chunk key, // and the name of the puppy as the item key. // 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 for Puppy { fn chunkkey(&self) -> Cow { Cow::Owned(self.rescueddate.year()) }

fn item_key(&self) -> Cow { Cow::Borrowed(&self.name) } }

// Let's create a storage of puppies. let mut storage : Storage = Storage::new();

// Add some example puppies to work with storage.add( Puppy::new("Lucky", Utc.ymd(2019, 3, 27)) .withadopteddate(Utc.ymd(2019, 9, 13)) .with_breeds(&["beagle"]) );

storage.add( Puppy::new("Spot", Utc.ymd(2019, 1, 9)) .withbreeds(&["labrador", "dalmation"]) // See below for correct spelling. .withparent(2010, "Yeller") );

storage.add( Puppy::new("JoJo", Utc.ymd(2018, 9, 2)) .withadopteddate(Utc.ymd(2019, 5, 1)) .withbreeds(&["labrador","shepherd"]) .withparent(2010, "Yeller") );

storage.add( Puppy::new("Yeller", Utc.ymd(2010, 8, 30)) .withadopteddate(Utc.ymd(2013, 12, 24)) .with_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()); ```

Comparison to other databases (SQL, MongoDB, etc)

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.

Comparison to ECS (entity-component-system) frameworks

Retriever can be used as a serviceable 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.

Getting started:

  1. Create a rust struct or enum that represents a data item that you want to store.
  2. Choose a chunk key and item key for each instance of your record.
  3. Implement the Record trait for your choice of record, chunk key, and item key types.
  4. Create a new empty Storage object using Storage::new().
  5. Use Storage::add(), Storage::iter(), Storage::query(), Storage::modify(), and Storage::remove() to implement CRUD operations on your storage.
  6. If you want, create some secondary indexes using SecondaryIndex::new(). Define secondary indexes by writing a single closure that maps records into zero or more secondary keys.
  7. If you want, create some reductions using Reduction::new(). Define reductions by writing two closures: (1) A map from the record to a summary, and (2) a fold of several summaries into a single summary. Use Reduction::reduce() to reduce an entire storage to a single summary, or Reduction::reduce_chunk() to reduce a single chunk to a single summary.

More about how to choose a good chunk key:

About Cow

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 generic parameter like ChunkKey, ItemKey, or IndexKey, these keys should also be borrowed forms.

These are good:

This will work for the most part but it's weird:

License

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)

Contributing

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.

How to Help

At this stage, any bug reports or questions about unclear documentation are highly valued. Please be patient if I'm not able to respond immediately. I'm also interested in any suggestions that would help further simplify the code base.

To Do: (I want these features, but they aren't yet implemented)

License: ISC OR GPL-3.0-or-later