Skip to main content

Module doc

Module doc 

Source
Expand description

Docs<T>, the typed document handle, and the traits #[derive(Yo)] writes (15 sections 2 and 4).

A document collection is your own struct, stored as your own struct. There is no schema to declare, no JSON text to parse on either side, and no query language: a struct goes in, the same struct comes out, and the fields worth looking documents up by say so with an attribute.

use yo::Yo;

#[derive(Yo, Debug, PartialEq)]
struct Order {
    #[yo(id)]
    id: u64,
    #[yo(index)]
    status: String,
    #[yo(ordered)]
    total: f64,
}

let db = yo::open(yo::MEMORY)?;
let orders = db.docs::<Order>("orders")?;

orders.put(&Order { id: 1, status: "open".to_owned(), total: 12.5 })?;
orders.put(&Order { id: 2, status: "shipped".to_owned(), total: 99.0 })?;

assert_eq!(orders.get(&1)?.unwrap().total, 12.5);
assert_eq!(orders.find(Order::STATUS, "open")?.len(), 1);
assert_eq!(orders.count(Order::STATUS, "shipped")?, 1);

§The query is a constant, not a string

Order::STATUS is a Path the derive wrote, and Order::TOTAL is an Ordered, which is what a #[yo(ordered)] field gets. A field that is not indexed has no constant at all, so asking for one is a name that does not exist rather than a query that quietly turns into a scan. Docs::range takes an Ordered and nothing else, so asking an equality index for a range is a type error at the call site.

// The index on status answers equality, so there is no range to walk.
orders.range(Order::STATUS, "a".."z").unwrap();

The value side is typed too, so comparing a number field against a string is the same kind of mistake and gets the same answer.

orders.find(Order::TOTAL, "twelve").unwrap();

§A range over a string field takes a pair of bounds

orders.range(Order::NAME, "a".."m") does not compile, and the reason is not this crate. Range<&str> only implements RangeBounds<str> when str is sized, which it is not, so the standard library’s own BTreeMap<String, u8>::range("a".."m") is rejected the same way. Writing the two ends out is what works there and it is what works here.

let early = orders.range(Order::NAME, (Bound::Included("a"), Bound::Excluded("m")))?;
assert_eq!(early.len(), 1);

A range over a number field is written the way anyone would write it, because the numbers are sized and 0.0..50.0 is a RangeBounds<f64> already.

§What a field can be

Field is the list, and it is the JSON types rather than the Rust ones, because a document is JSON shaped whatever it was written from. The integers and floats, bool, String, Option<T> for a field that may be absent, Vec<T> for a list, and any other type that derives Yo, which nests.

An integer is stored as an i64, which is the one number type JSON has, so a u64 above i64::MAX is refused on the way in rather than silently rounded through a float.

§An embedding is a field

#[yo(vector = 384)] on a Vec<f32> gives that path a vector index, and the field is still an ordinary field: it is written with the document, it comes back with the document, and there is no second collection to keep in step. The constant the derive writes is a Vector, so Docs::near takes it and Docs::find does not.

use yo::Yo;

#[derive(Yo, Debug)]
struct Note {
    #[yo(id)]
    id: u64,
    #[yo(index)]
    lang: String,
    #[yo(vector = 3)]
    embedding: Vec<f32>,
}

let db = yo::open(yo::MEMORY)?;
let notes = db.docs::<Note>("notes")?;
for (id, lang, v) in [
    (1u64, "en", [1.0, 0.0, 0.0]),
    (2, "fr", [0.9, 0.1, 0.0]),
    (3, "en", [0.0, 0.0, 1.0]),
] {
    notes.put(&Note { id, lang: lang.to_owned(), embedding: v.to_vec() })?;
}

let close = notes.nearest(Note::EMBEDDING, &[1.0, 0.05, 0.0], 2)?;
assert_eq!(close.iter().map(|n| n.id).collect::<Vec<_>>(), [1, 2]);

// The same search, narrowed by another indexed field.
let english = notes
    .near(Note::EMBEDDING, &[1.0, 0.05, 0.0])
    .filter(Note::LANG, "en")
    .take(2)?;
assert_eq!(english.iter().map(|n| n.id).collect::<Vec<_>>(), [1, 3]);

The filter is decided inside the scan and not over the answers, so asking for two English notes gives the two nearest English notes rather than whichever of the nearest few happened to be English. That distinction is the whole reason the two live in one collection, and yo_doc::vector has the rest of it.

Structs§

Builder
A value under construction.
Doc
A value with the key table its keys are interned against.
Docs
A collection of T.
Key
A value as an index looks it up.
Near
A nearest neighbour search being put together, from Docs::near.
Ordered
A path whose index keeps its keys in order, so it answers ranges as well as equality.
Path
A path into a document, what its index can be asked, and the type of the value that lives there.
Vector
A path that holds an embedding, and how wide it is.

Enums§

IndexKind
A float as bytes that sort the way the float does. What an index can be asked, and how many keys a document gets at its path.

Traits§

Asked
How a field’s type is written in a query.
Document
A type that is a whole document: a Field with an id and its indexes.
Field
A type that can be a field of a document.
Indexed
The indexes a type declares.
Query
A value that can be an index key.

Functions§

at
Read one field out of a document, which is what the derive calls per field.
expect_object
Check that what is stored under this collection is an object at all, which is what the derive calls before it reads the fields.