use core::ops::Bound;
use yo_common::{Code, Error, Result};
use yo_kv::{Cursor, Elements, Full};
use yo_shape::Metric;
use yo_vector::{Match, Signature};
use crate::head::{DEPTH_MAX, Kind};
use crate::index::{self, IndexKind, Key, PathIndex};
use crate::path::{Step, Steps};
use crate::vector::{self, VectorIndex};
use crate::{Builder, Keys, Value};
#[derive(Debug)]
pub struct Docs {
rows: Elements<()>,
keys: Keys,
build: Builder,
indexes: Vec<PathIndex>,
taken: Vec<Vec<u8>>,
vectors: Vec<VectorIndex>,
drawn: Vec<Vec<f32>>,
}
impl Default for Docs {
fn default() -> Docs {
Docs::new()
}
}
impl Docs {
#[must_use]
pub fn new() -> Docs {
Docs {
rows: Elements::tailed(0, 0),
keys: Keys::new(),
build: Builder::new(),
indexes: Vec::new(),
taken: Vec::new(),
vectors: Vec::new(),
drawn: Vec::new(),
}
}
#[must_use]
pub fn with_capacity(n: usize, each: usize) -> Docs {
Docs {
rows: Elements::tailed(n, n.saturating_mul(each)),
keys: Keys::new(),
build: Builder::with_capacity(each),
indexes: Vec::new(),
taken: Vec::new(),
vectors: Vec::new(),
drawn: Vec::new(),
}
}
pub fn put(&mut self, id: &[u8], value: Value<'_>) -> Result<bool> {
self.write(id, value, None)
}
pub fn put_bytes(&mut self, id: &[u8], doc: &[u8]) -> Result<bool> {
let value = Value::new(doc)
.ok_or_else(|| Error::new(Code::Corrupt, "the document is not a readable value"))?;
self.write(id, value, Some(doc))
}
fn write(&mut self, id: &[u8], value: Value<'_>, raw: Option<&[u8]>) -> Result<bool> {
let Docs {
rows,
keys,
build,
indexes,
taken,
vectors,
drawn,
} = self;
taken.resize(indexes.len(), Vec::new());
for (slot, index) in taken.iter_mut().zip(indexes.iter()) {
slot.clear();
let Some(at) = value.path_bytes(index.path())? else {
continue;
};
if index.keys_at(at, slot).is_err() {
return Err(Error::fmt(
Code::Full,
format_args!(
"a value at {} is longer than {} bytes and cannot be indexed",
String::from_utf8_lossy(index.path()),
index::KEY_MAX
),
));
}
}
drawn.resize(vectors.len(), Vec::new());
for (slot, index) in drawn.iter_mut().zip(vectors.iter()) {
slot.clear();
let Some(at) = value.path_bytes(index.path())? else {
continue;
};
vector::coordinates(at, index.dim(), index.path(), slot)?;
}
let tag = if vectors.is_empty() {
0
} else {
vector::tag_of(
indexes
.iter()
.map(PathIndex::path)
.zip(taken.iter().map(Vec::as_slice)),
)
};
unindex(rows, keys, indexes, id);
build.clear();
let fresh = if intern_into(keys, build, value, 0)? {
store(rows, id, build.finish()?)?
} else if let Some(raw) = raw {
store(rows, id, raw)?
} else {
build.clear();
build.embed(&value)?;
store(rows, id, build.finish()?)?
};
for (slot, index) in taken.iter().zip(indexes.iter_mut()) {
let mut filed = Ok(());
index::each_key(slot, |key| {
if filed.is_ok() {
filed = index.add(key, id);
}
});
filed?;
}
for (slot, index) in drawn.iter().zip(vectors.iter_mut()) {
if slot.is_empty() {
index.collection_mut().remove(id);
} else {
index.collection_mut().put_tagged(id, slot, tag)?;
}
}
Ok(fresh)
}
pub fn create_index(&mut self, path: &str) -> Result<()> {
self.create_index_bytes(path.as_bytes(), IndexKind::Equality)
}
pub fn create_ordered_index(&mut self, path: &str) -> Result<()> {
self.create_index_bytes(path.as_bytes(), IndexKind::Ordered)
}
pub fn create_array_index(&mut self, path: &str) -> Result<()> {
self.create_index_bytes(path.as_bytes(), IndexKind::Array)
}
pub fn create_text_index(&mut self, path: &str) -> Result<()> {
self.create_index_bytes(path.as_bytes(), IndexKind::Text)
}
pub fn create_index_bytes(&mut self, path: &[u8], kind: IndexKind) -> Result<()> {
for step in Steps::new(path) {
step?;
}
let old = match self.indexes.iter().position(|i| i.path() == path) {
Some(at) if self.indexes[at].kind() == kind => return Ok(()),
Some(at)
if kind == IndexKind::Equality && self.indexes[at].kind() == IndexKind::Ordered =>
{
return Ok(());
}
found => found,
};
let mut index = PathIndex::new(path, kind);
let mut list = Vec::new();
for (id, bytes) in self.rows.pairs() {
let Some(value) = Value::new(bytes) else {
continue;
};
let doc = Doc {
value,
keys: &self.keys,
};
let Some(at) = doc.path_bytes(path)? else {
continue;
};
list.clear();
if index.keys_at(at.value(), &mut list).is_err() {
return Err(Error::fmt(
Code::Full,
format_args!(
"a value at {} in {} is longer than {} bytes and cannot be indexed",
String::from_utf8_lossy(path),
String::from_utf8_lossy(id),
index::KEY_MAX
),
));
}
let mut filed = Ok(());
index::each_key(&list, |key| {
if filed.is_ok() {
filed = index.add(key, id);
}
});
filed?;
}
match old {
Some(at) => self.indexes[at] = index,
None => {
self.indexes.push(index);
self.taken.push(Vec::new());
}
}
self.retag();
Ok(())
}
pub fn drop_index(&mut self, path: &str) -> bool {
self.drop_index_bytes(path.as_bytes())
}
pub fn drop_index_bytes(&mut self, path: &[u8]) -> bool {
let Some(at) = self.indexes.iter().position(|i| i.path() == path) else {
return false;
};
self.indexes.remove(at);
self.taken.truncate(self.indexes.len());
self.retag();
true
}
#[must_use]
pub fn indexes(&self) -> &[PathIndex] {
&self.indexes
}
#[must_use]
pub fn index(&self, path: &str) -> Option<&PathIndex> {
self.indexes.iter().find(|i| i.path() == path.as_bytes())
}
pub fn create_vector_index(&mut self, path: &str, dim: usize) -> Result<()> {
self.create_vector_index_bytes(path.as_bytes(), dim, Metric::Cosine)
}
pub fn create_vector_index_with(
&mut self,
path: &str,
dim: usize,
metric: Metric,
) -> Result<()> {
self.create_vector_index_bytes(path.as_bytes(), dim, metric)
}
pub fn create_vector_index_bytes(
&mut self,
path: &[u8],
dim: usize,
metric: Metric,
) -> Result<()> {
for step in Steps::new(path) {
step?;
}
let old = match self.vectors.iter().position(|v| v.path() == path) {
Some(at) if self.vectors[at].dim() == dim && self.vectors[at].metric() == metric => {
return Ok(());
}
found => found,
};
let mut index = VectorIndex::new(path, dim, metric)?;
let mut list = Vec::new();
let mut v = Vec::new();
for (id, bytes) in self.rows.pairs() {
let Some(value) = Value::new(bytes) else {
continue;
};
let doc = Doc {
value,
keys: &self.keys,
};
let Some(at) = doc.path_bytes(path)? else {
continue;
};
vector::coordinates(at.value(), dim, path, &mut v)?;
let tag = tag_for(&doc, &self.indexes, &mut list);
index.collection_mut().put_tagged(id, &v, tag)?;
}
match old {
Some(at) => self.vectors[at] = index,
None => {
self.vectors.push(index);
self.drawn.push(Vec::new());
}
}
Ok(())
}
pub fn drop_vector_index(&mut self, path: &str) -> bool {
self.drop_vector_index_bytes(path.as_bytes())
}
pub fn drop_vector_index_bytes(&mut self, path: &[u8]) -> bool {
let Some(at) = self.vectors.iter().position(|v| v.path() == path) else {
return false;
};
self.vectors.remove(at);
self.drawn.truncate(self.vectors.len());
true
}
#[must_use]
pub fn vector_indexes(&self) -> &[VectorIndex] {
&self.vectors
}
#[must_use]
pub fn vector_index(&self, path: &str) -> Option<&VectorIndex> {
self.vectors.iter().find(|v| v.path() == path.as_bytes())
}
#[must_use]
pub fn embedding(&self, path: &str, id: &[u8]) -> Option<&[f32]> {
self.vector_index(path)?.collection().get(id)
}
pub fn nearest(
&self,
path: &str,
q: &[f32],
k: usize,
f: impl FnMut(&[u8], Doc<'_>, f32),
) -> Result<usize> {
let hits = self.vector(path)?.collection().search(q, k, None)?;
Ok(self.answer(&hits, f))
}
pub fn nearest_where(
&self,
path: &str,
q: &[f32],
k: usize,
want: &[(&str, Key)],
f: impl FnMut(&[u8], Doc<'_>, f32),
) -> Result<usize> {
let filter = self.wanted(want)?;
let hits = self
.vector(path)?
.collection()
.search_where(q, k, None, &filter)?;
Ok(self.answer(&hits, f))
}
pub fn nearest_to(
&self,
path: &str,
id: &[u8],
k: usize,
f: impl FnMut(&[u8], Doc<'_>, f32),
) -> Result<usize> {
let index = self.vector(path)?;
let Some(q) = index.collection().get(id) else {
return Ok(0);
};
let hits = index.collection().search(q, k, Some(id))?;
Ok(self.answer(&hits, f))
}
fn vector(&self, path: &str) -> Result<&VectorIndex> {
self.vector_index(path).ok_or_else(|| {
Error::fmt(
Code::Invalid,
format_args!("there is no vector index on {path}, so this would be a scan"),
)
})
}
fn wanted(&self, want: &[(&str, Key)]) -> Result<Signature> {
let mut sig = Signature::default();
for (path, key) in want {
if self.index(path).is_none() {
return Err(Error::fmt(
Code::Invalid,
format_args!("there is no index on {path}, so a search cannot filter on it"),
));
}
sig.insert(path, key.as_bytes());
}
Ok(sig)
}
fn answer(&self, hits: &[Match], mut f: impl FnMut(&[u8], Doc<'_>, f32)) -> usize {
let mut n = 0usize;
for hit in hits {
if let Some(doc) = self.get(&hit.key) {
f(&hit.key, doc, hit.distance);
n += 1;
}
}
n
}
fn retag(&mut self) {
let Docs {
rows,
keys,
indexes,
vectors,
..
} = self;
if vectors.is_empty() {
return;
}
let mut list = Vec::new();
for (id, bytes) in rows.pairs() {
let Some(value) = Value::new(bytes) else {
continue;
};
let doc = Doc { value, keys };
let tag = tag_for(&doc, indexes, &mut list);
for index in vectors.iter_mut() {
index.collection_mut().retag(id, tag);
}
}
}
pub fn find(&self, path: &str, key: &Key, mut f: impl FnMut(&[u8], Doc<'_>)) -> Result<usize> {
let index = self.index(path).ok_or_else(|| {
Error::fmt(
Code::Invalid,
format_args!("there is no index on {path}, so this would be a scan"),
)
})?;
let Some(set) = index.get(key) else {
return Ok(0);
};
let mut n = 0usize;
index::each_id(set, |id| {
if let Some(doc) = self.get(id) {
f(id, doc);
n += 1;
}
});
Ok(n)
}
pub fn count(&self, path: &str, key: &Key) -> Result<usize> {
let index = self.index(path).ok_or_else(|| {
Error::fmt(
Code::Invalid,
format_args!("there is no index on {path}, so this would be a scan"),
)
})?;
Ok(index.count(key))
}
pub fn range(
&self,
path: &str,
lo: Bound<&Key>,
hi: Bound<&Key>,
mut f: impl FnMut(&[u8], Doc<'_>),
) -> Result<usize> {
let index = self.ordered(path)?;
let mut n = 0usize;
for (_, set) in index.range(lo, hi) {
index::each_id(set, |id| {
if let Some(doc) = self.get(id) {
f(id, doc);
n += 1;
}
});
}
Ok(n)
}
pub fn range_rev(
&self,
path: &str,
lo: Bound<&Key>,
hi: Bound<&Key>,
mut f: impl FnMut(&[u8], Doc<'_>),
) -> Result<usize> {
let index = self.ordered(path)?;
let mut n = 0usize;
for (_, set) in index.range_rev(lo, hi) {
index::each_id(set, |id| {
if let Some(doc) = self.get(id) {
f(id, doc);
n += 1;
}
});
}
Ok(n)
}
pub fn count_range(&self, path: &str, lo: Bound<&Key>, hi: Bound<&Key>) -> Result<usize> {
Ok(self.ordered(path)?.count_in(lo, hi))
}
fn ordered(&self, path: &str) -> Result<&PathIndex> {
match self.index(path) {
Some(index) if index.kind() == IndexKind::Ordered => Ok(index),
Some(_) => Err(Error::fmt(
Code::Invalid,
format_args!("the index on {path} answers equality and not ranges"),
)),
None => Err(Error::fmt(
Code::Invalid,
format_args!("there is no index on {path}, so this would be a scan"),
)),
}
}
#[must_use]
pub fn get(&self, id: &[u8]) -> Option<Doc<'_>> {
let value = Value::new(self.rows.tail(id)?)?;
Some(Doc {
value,
keys: &self.keys,
})
}
#[must_use]
pub fn bytes(&self, id: &[u8]) -> Option<&[u8]> {
self.rows.tail(id)
}
#[must_use]
pub fn contains(&self, id: &[u8]) -> bool {
self.rows.contains(id)
}
pub fn remove(&mut self, id: &[u8]) -> bool {
let Docs {
rows,
keys,
indexes,
vectors,
..
} = self;
unindex(rows, keys, indexes, id);
for index in vectors.iter_mut() {
index.collection_mut().remove(id);
}
rows.remove(id).is_some()
}
#[must_use]
pub fn len(&self) -> usize {
self.rows.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
#[must_use]
pub fn keys(&self) -> &Keys {
&self.keys
}
pub fn iter(&self) -> impl Iterator<Item = (&[u8], Doc<'_>)> {
let keys = &self.keys;
self.rows.pairs().filter_map(move |(id, bytes)| {
let value = Value::new(bytes)?;
Some((id, Doc { value, keys }))
})
}
pub fn scan<F>(&self, cursor: Cursor, count: usize, mut f: F) -> Cursor
where
F: FnMut(&[u8], Doc<'_>),
{
let keys = &self.keys;
self.rows.scan_pairs(cursor, count, |id, bytes| {
if let Some(value) = Value::new(bytes) {
f(id, Doc { value, keys });
}
})
}
pub fn clear(&mut self) {
self.rows.clear();
self.build.clear();
for index in &mut self.indexes {
index.clear();
}
for index in &mut self.vectors {
index.clear();
}
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.rows.memory_bytes()
+ self.keys.memory_bytes()
+ self
.indexes
.iter()
.map(PathIndex::memory_bytes)
.sum::<usize>()
+ self
.vectors
.iter()
.map(VectorIndex::memory_bytes)
.sum::<usize>()
}
}
fn tag_for(doc: &Doc<'_>, indexes: &[PathIndex], list: &mut Vec<u8>) -> u64 {
let mut sig = Signature::default();
for index in indexes {
let Ok(Some(at)) = doc.path_bytes(index.path()) else {
continue;
};
list.clear();
let _ = index.keys_at(at.value(), list);
vector::add_keys(&mut sig, index.path(), list);
}
sig.bits()
}
fn unindex(rows: &Elements<()>, keys: &Keys, indexes: &mut [PathIndex], id: &[u8]) {
if indexes.is_empty() {
return;
}
let Some(bytes) = rows.tail(id) else {
return;
};
let Some(value) = Value::new(bytes) else {
return;
};
let doc = Doc { value, keys };
let mut list = Vec::new();
for index in indexes {
let Ok(Some(at)) = doc.path_bytes(index.path()) else {
continue;
};
list.clear();
let _ = index.keys_at(at.value(), &mut list);
index::each_key(&list, |key| index.take(key, id));
}
}
fn store(rows: &mut Elements<()>, id: &[u8], bytes: &[u8]) -> Result<bool> {
match rows.set_tailed(id, bytes, ()) {
Ok((_, fresh)) => Ok(fresh),
Err(Full::Name) => Err(Error::fmt(
Code::Full,
format_args!("a document id is at most {} bytes", yo_kv::NAME_MAX),
)),
Err(Full::Rows) => Err(Error::fmt(
Code::Full,
format_args!("a collection holds at most {} documents", yo_kv::MAX_ROWS),
)),
}
}
fn intern_into(keys: &mut Keys, b: &mut Builder, value: Value<'_>, depth: usize) -> Result<bool> {
let corrupt = || Error::new(Code::Corrupt, "the document is not readable at that point");
match value.kind() {
Kind::Null => b.null()?,
Kind::Bool => b.bool(value.as_bool().ok_or_else(corrupt)?)?,
Kind::Int => b.int(value.as_int().ok_or_else(corrupt)?)?,
Kind::Float => b.float(value.as_float().ok_or_else(corrupt)?)?,
Kind::Text => b.text_bytes(value.text_bytes().ok_or_else(corrupt)?)?,
Kind::Array => {
b.begin_array()?;
for i in 0..value.len() {
let child = value.at(i).ok_or_else(corrupt)?;
if !intern_into(keys, b, child, depth + 1)? {
return Ok(false);
}
}
b.end_array()?;
}
Kind::Object => {
if value.is_interned() {
return Err(Error::new(
Code::Invalid,
"this document's keys are ids from another collection's key table",
));
}
b.begin_object_interned()?;
for i in 0..value.len() {
let name = value.key_at(i).ok_or_else(corrupt)?;
let Some(id) = keys.intern(name) else {
return Ok(false);
};
b.key_id(id)?;
let child = value.at(i).ok_or_else(corrupt)?;
if !intern_into(keys, b, child, depth + 1)? {
return Ok(false);
}
}
b.end_object()?;
}
}
debug_assert!(depth <= DEPTH_MAX, "the builder caps the depth");
Ok(true)
}
#[derive(Clone, Copy)]
pub struct Doc<'a> {
value: Value<'a>,
keys: &'a Keys,
}
impl<'a> Doc<'a> {
#[must_use]
pub fn new(value: Value<'a>, keys: &'a Keys) -> Doc<'a> {
Doc { value, keys }
}
#[must_use]
pub fn value(&self) -> Value<'a> {
self.value
}
#[must_use]
pub fn keys(&self) -> &'a Keys {
self.keys
}
#[must_use]
pub fn kind(&self) -> Kind {
self.value.kind()
}
#[must_use]
pub fn is_null(&self) -> bool {
self.value.is_null()
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
self.value.as_bool()
}
#[must_use]
pub fn as_int(&self) -> Option<i64> {
self.value.as_int()
}
#[must_use]
pub fn as_float(&self) -> Option<f64> {
self.value.as_float()
}
#[must_use]
pub fn as_text(&self) -> Option<&'a str> {
self.value.as_text()
}
#[must_use]
pub fn text_bytes(&self) -> Option<&'a [u8]> {
self.value.text_bytes()
}
#[must_use]
pub fn len(&self) -> usize {
self.value.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.value.is_empty()
}
#[must_use]
pub fn get(&self, key: &[u8]) -> Option<Doc<'a>> {
let value = if self.value.is_interned() {
self.value.get_id(self.keys.id(key)?)?
} else {
self.value.get(key)?
};
Some(Doc {
value,
keys: self.keys,
})
}
#[must_use]
pub fn at(&self, i: usize) -> Option<Doc<'a>> {
Some(Doc {
value: self.value.at(i)?,
keys: self.keys,
})
}
#[must_use]
pub fn key_at(&self, i: usize) -> Option<&'a [u8]> {
if self.value.is_interned() {
self.keys.name(self.value.key_id_at(i)?)
} else {
self.value.key_at(i)
}
}
#[must_use]
pub fn members(&self) -> DocMembers<'a> {
DocMembers { d: *self, i: 0 }
}
#[must_use]
pub fn iter(&self) -> DocElems<'a> {
DocElems { d: *self, i: 0 }
}
pub fn path(&self, path: &str) -> Result<Option<Doc<'a>>> {
self.path_bytes(path.as_bytes())
}
pub fn path_bytes(&self, path: &[u8]) -> Result<Option<Doc<'a>>> {
let mut at = *self;
for step in Steps::new(path) {
let next = match step? {
Step::Key(k) => at.get(k),
Step::Index(_) if at.kind() != Kind::Array => None,
Step::Index(i) => {
let n = at.len();
let i = if i < 0 {
match n.checked_sub(i.unsigned_abs() as usize) {
Some(i) => i,
None => return Ok(None),
}
} else {
i as usize
};
at.at(i)
}
};
let Some(next) = next else {
return Ok(None);
};
at = next;
}
Ok(Some(at))
}
}
impl core::fmt::Debug for Doc<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self.kind() {
Kind::Object => {
let mut m = f.debug_map();
for (k, v) in self.members() {
m.entry(&String::from_utf8_lossy(k), &v);
}
m.finish()
}
Kind::Array => f.debug_list().entries(self.iter()).finish(),
_ => self.value.fmt(f),
}
}
}
#[derive(Clone)]
pub struct DocMembers<'a> {
d: Doc<'a>,
i: usize,
}
impl<'a> Iterator for DocMembers<'a> {
type Item = (&'a [u8], Doc<'a>);
fn next(&mut self) -> Option<(&'a [u8], Doc<'a>)> {
let key = self.d.key_at(self.i)?;
let val = self.d.at(self.i)?;
self.i += 1;
Some((key, val))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let left = self.d.len().saturating_sub(self.i);
(left, Some(left))
}
}
#[derive(Clone)]
pub struct DocElems<'a> {
d: Doc<'a>,
i: usize,
}
impl<'a> Iterator for DocElems<'a> {
type Item = Doc<'a>;
fn next(&mut self) -> Option<Doc<'a>> {
let out = self.d.at(self.i)?;
self.i += 1;
Some(out)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let left = self.d.len().saturating_sub(self.i);
(left, Some(left))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn order(id: i64, status: &str, lines: usize) -> Vec<u8> {
let mut b = Builder::new();
b.begin_object().expect("open");
b.key(b"id").expect("key");
b.int(id).expect("value");
b.key(b"customer").expect("key");
b.int(id * 7).expect("value");
b.key(b"status").expect("key");
b.text(status).expect("value");
b.key(b"lines").expect("key");
b.begin_array().expect("open");
for i in 0..lines {
b.begin_object().expect("open");
b.key(b"sku").expect("key");
b.text(&format!("sku-{i}")).expect("value");
b.key(b"qty").expect("key");
b.int(i as i64 + 1).expect("value");
b.end_object().expect("close");
}
b.end_array().expect("close");
b.end_object().expect("close");
b.finish().expect("finished").to_vec()
}
#[test]
fn a_document_reads_back_the_way_it_went_in() {
let mut docs = Docs::new();
assert!(
docs.put_bytes(b"order:1", &order(1, "open", 3))
.expect("put")
);
assert!(
!docs
.put_bytes(b"order:1", &order(1, "shut", 3))
.expect("put")
);
assert_eq!(docs.len(), 1);
let d = docs.get(b"order:1").expect("stored");
assert_eq!(d.get(b"id").and_then(|v| v.as_int()), Some(1));
assert_eq!(d.get(b"status").and_then(|v| v.as_text()), Some("shut"));
assert_eq!(d.get(b"lines").map(|v| v.len()), Some(3));
assert_eq!(
d.path("$.lines[1].sku")
.expect("a path")
.and_then(|v| v.as_text()),
Some("sku-1")
);
assert_eq!(
d.path("$.lines[-1].qty")
.expect("a path")
.and_then(|v| v.as_int()),
Some(3)
);
assert!(d.get(b"missing").is_none());
}
#[test]
fn the_keys_are_interned_and_the_names_come_back() {
let mut docs = Docs::new();
docs.put_bytes(b"order:1", &order(1, "open", 2))
.expect("put");
let names: Vec<String> = docs
.keys()
.iter()
.map(|(n, _)| String::from_utf8_lossy(n).into_owned())
.collect();
names.iter().for_each(|n| assert!(!n.is_empty()));
assert_eq!(
docs.keys().len(),
6,
"id customer status lines sku qty: {names:?}"
);
let d = docs.get(b"order:1").expect("stored");
assert!(d.value().is_interned());
let mut got: Vec<&[u8]> = d.members().map(|(k, _)| k).collect();
got.sort_unstable();
assert_eq!(got, [&b"customer"[..], b"id", b"lines", b"status"]);
let line = d.path("$.lines[0]").expect("a path").expect("there");
assert!(line.value().is_interned());
let mut inner: Vec<&[u8]> = line.members().map(|(k, _)| k).collect();
inner.sort_unstable();
assert_eq!(inner, [&b"qty"[..], b"sku"]);
}
fn shrinkage(shape: impl Fn(i64) -> Vec<u8>) -> f64 {
let mut docs = Docs::new();
let mut plain = 0usize;
for i in 0..256i64 {
let bytes = shape(i);
plain += bytes.len();
docs.put_bytes(format!("d:{i}").as_bytes(), &bytes)
.expect("put");
}
let stored: usize = (0..256i64)
.map(|i| {
docs.bytes(format!("d:{i}").as_bytes())
.expect("stored")
.len()
})
.sum();
stored as f64 / plain as f64
}
#[test]
fn interning_makes_a_collection_of_the_same_shape_smaller() {
let names = shrinkage(|i| {
let mut b = Builder::new();
b.begin_object().expect("open");
for f in 0..20 {
b.key(format!("some_field_name_{f:02}").as_bytes())
.expect("key");
b.int(i + f).expect("value");
}
b.end_object().expect("close");
b.finish().expect("finished").to_vec()
});
assert!(names < 0.60, "a document of names kept {names}");
let orders = shrinkage(|i| order(i, "open", 2));
assert!(orders < 0.80, "an order collection kept {orders}");
}
#[test]
fn a_document_whose_keys_are_already_ids_is_refused() {
let mut b = Builder::new();
b.begin_object_interned().expect("open");
b.key_id(0).expect("key");
b.int(1).expect("value");
b.end_object().expect("close");
let bytes = b.finish().expect("finished").to_vec();
let mut docs = Docs::new();
let err = docs.put_bytes(b"x", &bytes).expect_err("refused");
assert_eq!(err.code(), Code::Invalid);
}
#[test]
fn a_document_that_is_not_readable_is_refused() {
let mut docs = Docs::new();
let err = docs.put_bytes(b"x", &[2, 0, 0, 0]).expect_err("refused");
assert_eq!(err.code(), Code::Corrupt);
assert!(docs.is_empty());
}
#[test]
fn a_removal_leaves_every_other_document_where_it_was() {
let mut docs = Docs::new();
for i in 0..64i64 {
docs.put_bytes(format!("order:{i}").as_bytes(), &order(i, "open", 1))
.expect("put");
}
for i in (0..64i64).step_by(3) {
assert!(docs.remove(format!("order:{i}").as_bytes()));
}
assert_eq!(docs.len(), 64 - 22);
for i in 0..64i64 {
let id = format!("order:{i}");
match docs.get(id.as_bytes()) {
Some(d) => {
assert!(i % 3 != 0, "{id} was removed");
assert_eq!(d.get(b"id").and_then(|v| v.as_int()), Some(i));
}
None => assert!(i % 3 == 0, "{id} was not removed"),
}
}
assert_eq!(docs.keys().len(), 6, "a removal does not un-intern a name");
}
#[test]
fn a_walk_sees_every_document_once() {
let mut docs = Docs::new();
for i in 0..200i64 {
docs.put_bytes(format!("order:{i}").as_bytes(), &order(i, "open", 1))
.expect("put");
}
let mut seen: Vec<i64> = docs
.iter()
.map(|(_, d)| d.get(b"id").and_then(|v| v.as_int()).expect("an id"))
.collect();
seen.sort_unstable();
assert_eq!(seen, (0..200).collect::<Vec<i64>>());
let mut scanned = Vec::new();
let mut cursor = Cursor::START;
loop {
cursor = docs.scan(cursor, 16, |id, _| scanned.push(id.to_vec()));
if cursor.is_end() {
break;
}
}
scanned.sort_unstable();
scanned.dedup();
assert_eq!(scanned.len(), 200);
}
#[test]
fn an_empty_collection_answers_nothing_rather_than_failing() {
let docs = Docs::new();
assert!(docs.is_empty());
assert!(docs.get(b"nothing").is_none());
assert!(docs.bytes(b"nothing").is_none());
assert!(!docs.contains(b"nothing"));
assert_eq!(docs.iter().count(), 0);
}
#[test]
fn a_document_prints_with_its_names_back_on() {
let mut docs = Docs::new();
docs.put_bytes(b"order:1", &order(1, "open", 1))
.expect("put");
let text = format!("{:?}", docs.get(b"order:1").expect("stored"));
assert!(text.contains("\"status\": \"open\""), "{text}");
assert!(text.contains("\"sku\": \"sku-0\""), "{text}");
}
fn found(docs: &Docs, path: &str, key: &Key) -> Vec<String> {
let mut out = Vec::new();
let n = docs
.find(path, key, |id, d| {
assert!(!d.is_empty(), "the document came back whole");
out.push(String::from_utf8_lossy(id).into_owned());
})
.expect("indexed");
assert_eq!(n, out.len(), "the count is what the callback saw");
out.sort();
out
}
#[test]
fn an_index_declared_after_the_documents_finds_them() {
let mut docs = Docs::new();
for i in 0..64i64 {
let status = if i % 4 == 0 { "shut" } else { "open" };
docs.put_bytes(format!("order:{i}").as_bytes(), &order(i, status, 1))
.expect("put");
}
docs.create_index("$.status").expect("indexed");
assert_eq!(docs.index("$.status").expect("there").len(), 2);
assert_eq!(docs.count("$.status", &Key::text("shut")).expect("i"), 16);
assert_eq!(docs.count("$.status", &Key::text("open")).expect("i"), 48);
assert_eq!(found(&docs, "$.status", &Key::text("shut")).len(), 16);
assert!(found(&docs, "$.status", &Key::text("gone")).is_empty());
docs.put_bytes(b"order:64", &order(64, "shut", 1))
.expect("put");
assert_eq!(docs.count("$.status", &Key::text("shut")).expect("i"), 17);
}
#[test]
fn an_overwrite_moves_a_document_from_one_key_to_the_other() {
let mut docs = Docs::new();
docs.create_index("$.status").expect("indexed");
docs.put_bytes(b"order:1", &order(1, "open", 1))
.expect("put");
assert_eq!(found(&docs, "$.status", &Key::text("open")), ["order:1"]);
docs.put_bytes(b"order:1", &order(1, "shut", 1))
.expect("put");
assert!(
found(&docs, "$.status", &Key::text("open")).is_empty(),
"the old key kept it"
);
assert_eq!(found(&docs, "$.status", &Key::text("shut")), ["order:1"]);
assert_eq!(docs.index("$.status").expect("there").postings(), 1);
}
#[test]
fn a_removal_takes_a_document_out_of_every_index() {
let mut docs = Docs::new();
docs.create_index("$.status").expect("indexed");
docs.create_index("$.customer").expect("indexed");
for i in 0..8i64 {
docs.put_bytes(format!("order:{i}").as_bytes(), &order(i, "open", 1))
.expect("put");
}
assert!(docs.remove(b"order:3"));
assert_eq!(found(&docs, "$.status", &Key::text("open")).len(), 7);
assert_eq!(docs.count("$.customer", &Key::int(21)).expect("i"), 0);
assert_eq!(docs.count("$.customer", &Key::int(28)).expect("i"), 1);
for index in docs.indexes() {
assert_eq!(index.postings(), 7);
}
assert!(!docs.remove(b"order:3"), "it is already gone");
assert_eq!(docs.index("$.status").expect("there").postings(), 7);
}
#[test]
fn a_path_that_names_a_container_or_nothing_is_simply_not_filed() {
let mut docs = Docs::new();
docs.create_index("$.lines").expect("indexed");
docs.create_index("$.shipped").expect("indexed");
docs.create_index("$.lines[0].qty").expect("indexed");
for i in 0..4i64 {
docs.put_bytes(format!("order:{i}").as_bytes(), &order(i, "open", 2))
.expect("put");
}
assert_eq!(docs.len(), 4);
assert!(
docs.index("$.lines").expect("there").is_empty(),
"an array has no equality key"
);
assert!(
docs.index("$.shipped").expect("there").is_empty(),
"no document has that path"
);
assert_eq!(
docs.count("$.lines[0].qty", &Key::int(1)).expect("i"),
4,
"a path through an array reaches a scalar"
);
}
#[test]
fn a_value_too_long_to_index_fails_the_write_and_stores_nothing() {
let mut b = Builder::new();
b.begin_object().expect("open");
b.key(b"status").expect("key");
b.text(&"x".repeat(crate::KEY_MAX)).expect("value");
b.end_object().expect("close");
let huge = b.finish().expect("finished").to_vec();
let mut docs = Docs::new();
docs.create_index("$.status").expect("indexed");
let err = docs.put_bytes(b"order:1", &huge).expect_err("refused");
assert_eq!(err.code(), Code::Full);
assert!(
docs.is_empty(),
"a write that cannot be indexed leaves nothing behind"
);
assert!(docs.drop_index("$.status"));
docs.put_bytes(b"order:1", &huge).expect("put");
assert_eq!(docs.len(), 1);
}
#[test]
fn a_query_on_a_path_with_no_index_says_so_rather_than_scanning() {
let mut docs = Docs::new();
docs.put_bytes(b"order:1", &order(1, "open", 1))
.expect("put");
let err = docs
.find("$.status", &Key::text("open"), |_, _| ())
.expect_err("refused");
assert_eq!(err.code(), Code::Invalid);
assert_eq!(
docs.count("$.status", &Key::text("open"))
.expect_err("refused")
.code(),
Code::Invalid
);
assert!(docs.index("$.status").is_none());
assert!(!docs.drop_index("$.status"));
}
#[test]
fn declaring_the_same_index_twice_leaves_the_first_one_alone() {
let mut docs = Docs::new();
docs.create_index("$.status").expect("indexed");
docs.put_bytes(b"order:1", &order(1, "open", 1))
.expect("put");
docs.create_index("$.status").expect("indexed again");
assert_eq!(docs.indexes().len(), 1);
assert_eq!(
docs.index("$.status").expect("there").postings(),
1,
"a redeclaration did not double file anything"
);
assert!(docs.create_index("$.[").is_err(), "the path has to parse");
}
#[test]
fn clearing_a_collection_empties_its_indexes_and_keeps_them() {
let mut docs = Docs::new();
docs.create_index("$.status").expect("indexed");
for i in 0..8i64 {
docs.put_bytes(format!("order:{i}").as_bytes(), &order(i, "open", 1))
.expect("put");
}
docs.clear();
assert!(docs.is_empty());
assert!(docs.index("$.status").expect("still declared").is_empty());
assert_eq!(docs.count("$.status", &Key::text("open")).expect("i"), 0);
docs.put_bytes(b"order:9", &order(9, "open", 1))
.expect("put");
assert_eq!(found(&docs, "$.status", &Key::text("open")), ["order:9"]);
}
#[test]
fn two_indexes_intersect_as_the_sets_they_are() {
let mut docs = Docs::new();
docs.create_index("$.status").expect("indexed");
docs.create_index("$.customer").expect("indexed");
for i in 0..32i64 {
let status = if i % 2 == 0 { "open" } else { "shut" };
docs.put_bytes(format!("order:{i}").as_bytes(), &order(i % 4, status, 1))
.expect("put");
}
let open = Key::text("open");
let customer = Key::int(14);
let small = docs.count("$.customer", &customer).expect("indexed");
let large = docs.count("$.status", &open).expect("indexed");
assert_eq!((small, large), (8, 16));
let small = docs.index("$.customer").expect("there").get(&customer);
let large = docs.index("$.status").expect("there").get(&open);
let (Some(small), Some(large)) = (small, large) else {
panic!("both keys are filed");
};
let mut both = Vec::new();
index::each_id(small, |id| {
if large.contains(id) {
both.push(String::from_utf8_lossy(id).into_owned());
}
});
both.sort();
assert_eq!(
both,
[
"order:10", "order:14", "order:18", "order:2", "order:22", "order:26", "order:30",
"order:6"
]
);
}
fn ranged(docs: &Docs, lo: Bound<&Key>, hi: Bound<&Key>) -> Vec<i64> {
let mut out = Vec::new();
let n = docs
.range("$.customer", lo, hi, |_, d| {
out.push(d.get(b"customer").and_then(|v| v.as_int()).expect("there"));
})
.expect("ordered");
assert_eq!(n, out.len());
let mut back = Vec::new();
docs.range_rev("$.customer", lo, hi, |_, d| {
back.push(d.get(b"customer").and_then(|v| v.as_int()).expect("there"));
})
.expect("ordered");
back.reverse();
assert_eq!(out, back, "backwards is forwards read the other way");
assert_eq!(
docs.count_range("$.customer", lo, hi).expect("ordered"),
out.len()
);
out
}
#[test]
fn an_ordered_index_answers_a_range_in_order() {
let mut docs = Docs::new();
docs.create_ordered_index("$.customer").expect("ordered");
for i in 0..64i64 {
docs.put_bytes(format!("order:{i}").as_bytes(), &order(i, "open", 1))
.expect("put");
}
assert_eq!(
ranged(&docs, Bound::Unbounded, Bound::Unbounded),
(0..64i64).map(|i| i * 7).collect::<Vec<i64>>()
);
let (lo, hi) = (Key::int(70), Key::int(105));
assert_eq!(
ranged(&docs, Bound::Included(&lo), Bound::Included(&hi)),
[70, 77, 84, 91, 98, 105]
);
assert_eq!(
ranged(&docs, Bound::Excluded(&lo), Bound::Excluded(&hi)),
[77, 84, 91, 98]
);
assert_eq!(
ranged(
&docs,
Bound::Included(&Key::int(71)),
Bound::Excluded(&Key::int(90))
),
[77, 84]
);
assert!(ranged(&docs, Bound::Included(&Key::int(442)), Bound::Unbounded).is_empty());
assert_eq!(docs.count("$.customer", &Key::int(70)).expect("i"), 1);
assert_eq!(
docs.index("$.customer").expect("there").kind(),
IndexKind::Ordered
);
}
#[test]
fn a_range_stays_right_through_writes_and_removals() {
let mut docs = Docs::new();
for i in 0..128i64 {
docs.put_bytes(format!("order:{i}").as_bytes(), &order(i, "open", 1))
.expect("put");
}
docs.create_ordered_index("$.customer").expect("ordered");
assert_eq!(ranged(&docs, Bound::Unbounded, Bound::Unbounded).len(), 128);
for i in (0..128i64).step_by(2) {
assert!(docs.remove(format!("order:{i}").as_bytes()));
}
assert_eq!(
ranged(&docs, Bound::Unbounded, Bound::Unbounded),
(0..128i64)
.filter(|i| i % 2 == 1)
.map(|i| i * 7)
.collect::<Vec<i64>>()
);
docs.put_bytes(b"order:1", &order(200, "open", 1))
.expect("put");
let after = ranged(&docs, Bound::Unbounded, Bound::Unbounded);
assert_eq!(after.first(), Some(&21), "seven is gone");
assert_eq!(after.last(), Some(&1400), "and it came back at the top");
}
#[test]
fn an_equality_index_refuses_a_range_rather_than_answering_nothing() {
let mut docs = Docs::new();
docs.create_index("$.customer").expect("indexed");
docs.put_bytes(b"order:1", &order(1, "open", 1))
.expect("put");
let err = docs
.range("$.customer", Bound::Unbounded, Bound::Unbounded, |_, _| ())
.expect_err("refused");
assert_eq!(err.code(), Code::Invalid);
assert!(err.to_string().contains("equality"), "{err}");
assert_eq!(
docs.range("$.status", Bound::Unbounded, Bound::Unbounded, |_, _| ())
.expect_err("refused")
.code(),
Code::Invalid
);
}
#[test]
fn asking_for_an_order_on_an_equality_index_upgrades_it() {
let mut docs = Docs::new();
docs.create_index("$.customer").expect("indexed");
for i in 0..8i64 {
docs.put_bytes(format!("order:{i}").as_bytes(), &order(i, "open", 1))
.expect("put");
}
assert_eq!(
docs.index("$.customer").expect("there").kind(),
IndexKind::Equality
);
docs.create_ordered_index("$.customer").expect("upgraded");
assert_eq!(docs.indexes().len(), 1, "it replaced rather than added");
assert_eq!(ranged(&docs, Bound::Unbounded, Bound::Unbounded).len(), 8);
docs.create_index("$.customer").expect("already there");
assert_eq!(
docs.index("$.customer").expect("there").kind(),
IndexKind::Ordered
);
assert_eq!(docs.indexes().len(), 1);
}
fn tagged(title: &str, tags: &[&str]) -> Vec<u8> {
let mut b = Builder::new();
b.begin_object().expect("open");
b.key(b"title").expect("key");
b.text(title).expect("value");
b.key(b"tags").expect("key");
b.begin_array().expect("open");
for tag in tags {
b.text(tag).expect("value");
}
b.end_array().expect("close");
b.end_object().expect("close");
b.finish().expect("finished").to_vec()
}
#[test]
fn an_array_index_files_a_document_under_every_element() {
let mut docs = Docs::new();
docs.create_array_index("$.tags").expect("indexed");
docs.put_bytes(b"a", &tagged("one", &["red", "blue"]))
.expect("put");
docs.put_bytes(b"b", &tagged("two", &["blue", "green"]))
.expect("put");
docs.put_bytes(b"c", &tagged("three", &[])).expect("put");
assert_eq!(found(&docs, "$.tags", &Key::text("red")), ["a"]);
assert_eq!(found(&docs, "$.tags", &Key::text("blue")), ["a", "b"]);
assert_eq!(found(&docs, "$.tags", &Key::text("green")), ["b"]);
assert!(found(&docs, "$.tags", &Key::text("puce")).is_empty());
assert_eq!(
docs.index("$.tags").expect("there").len(),
3,
"three distinct tags over two documents"
);
}
#[test]
fn an_array_index_takes_every_element_back_out_again() {
let mut docs = Docs::new();
docs.create_array_index("$.tags").expect("indexed");
docs.put_bytes(b"a", &tagged("one", &["red", "blue"]))
.expect("put");
docs.put_bytes(b"b", &tagged("two", &["blue"]))
.expect("put");
docs.put_bytes(b"a", &tagged("one", &["blue", "green"]))
.expect("put");
assert!(found(&docs, "$.tags", &Key::text("red")).is_empty());
assert_eq!(found(&docs, "$.tags", &Key::text("blue")), ["a", "b"]);
assert_eq!(found(&docs, "$.tags", &Key::text("green")), ["a"]);
assert!(docs.remove(b"a"));
assert_eq!(found(&docs, "$.tags", &Key::text("blue")), ["b"]);
assert!(found(&docs, "$.tags", &Key::text("green")).is_empty());
assert_eq!(
docs.index("$.tags").expect("there").len(),
1,
"a tag nobody has left is not a key any more"
);
}
#[test]
fn an_array_index_treats_one_value_as_a_list_of_one() {
let mut docs = Docs::new();
docs.create_array_index("$.status").expect("indexed");
docs.put_bytes(b"order:1", &order(1, "open", 1))
.expect("put");
assert_eq!(found(&docs, "$.status", &Key::text("open")), ["order:1"]);
}
#[test]
fn the_same_element_twice_is_one_posting() {
let mut docs = Docs::new();
docs.create_array_index("$.tags").expect("indexed");
docs.put_bytes(b"a", &tagged("one", &["red", "red", "red"]))
.expect("put");
assert_eq!(found(&docs, "$.tags", &Key::text("red")), ["a"]);
assert_eq!(docs.index("$.tags").expect("there").postings(), 1);
assert!(docs.remove(b"a"));
assert_eq!(docs.index("$.tags").expect("there").postings(), 0);
assert!(docs.index("$.tags").expect("there").is_empty());
}
#[test]
fn a_text_index_files_a_document_under_every_word() {
let mut docs = Docs::new();
docs.create_text_index("$.title").expect("indexed");
docs.put_bytes(b"a", &tagged("A red bicycle", &[]))
.expect("put");
docs.put_bytes(b"b", &tagged("The red car, and a bicycle!", &[]))
.expect("put");
assert_eq!(found(&docs, "$.title", &word("bicycle")), ["a", "b"]);
assert_eq!(found(&docs, "$.title", &word("car")), ["b"]);
assert_eq!(
found(&docs, "$.title", &word("RED")),
["a", "b"],
"a search folds case the same way the write did"
);
assert!(found(&docs, "$.title", &word("lorry")).is_empty());
}
#[test]
fn a_text_index_follows_the_words_through_a_rewrite() {
let mut docs = Docs::new();
docs.create_text_index("$.title").expect("indexed");
docs.put_bytes(b"a", &tagged("a red bicycle", &[]))
.expect("put");
docs.put_bytes(b"a", &tagged("a blue bicycle", &[]))
.expect("put");
assert!(found(&docs, "$.title", &word("red")).is_empty());
assert_eq!(found(&docs, "$.title", &word("blue")), ["a"]);
assert_eq!(found(&docs, "$.title", &word("bicycle")), ["a"]);
assert!(docs.remove(b"a"));
assert!(docs.index("$.title").expect("there").is_empty());
}
#[test]
fn a_text_index_declared_after_the_documents_finds_them() {
let mut docs = Docs::new();
for i in 0..16i64 {
let title = if i % 2 == 0 {
"a red one"
} else {
"a blue one"
};
docs.put_bytes(format!("t:{i}").as_bytes(), &tagged(title, &[]))
.expect("put");
}
docs.create_text_index("$.title").expect("indexed");
assert_eq!(docs.count("$.title", &word("red")).expect("i"), 8);
assert_eq!(docs.count("$.title", &word("one")).expect("i"), 16);
assert_eq!(
docs.index("$.title").expect("there").len(),
4,
"a, red, blue and one"
);
}
#[test]
fn changing_what_an_index_is_asked_rebuilds_it() {
let mut docs = Docs::new();
docs.create_index("$.tags").expect("indexed");
docs.put_bytes(b"a", &tagged("one", &["red", "blue"]))
.expect("put");
assert!(
found(&docs, "$.tags", &Key::text("red")).is_empty(),
"an equality index over an array files nothing"
);
docs.create_array_index("$.tags").expect("rebuilt");
assert_eq!(docs.indexes().len(), 1, "it replaced rather than added");
assert_eq!(found(&docs, "$.tags", &Key::text("red")), ["a"]);
docs.create_array_index("$.tags").expect("already there");
assert_eq!(docs.indexes().len(), 1);
}
fn word(w: &str) -> Key {
Key::word(w).expect("one word")
}
#[test]
fn a_collection_whose_key_table_is_full_stores_the_rest_with_names() {
let mut docs = Docs::new();
for i in 0..crate::KEYS_MAX {
let name = format!("filler{i}");
assert!(docs.keys.intern(name.as_bytes()).is_some());
}
assert!(docs.keys().is_full());
docs.put_bytes(b"order:1", &order(1, "open", 1))
.expect("put");
let d = docs.get(b"order:1").expect("stored");
assert!(!d.value().is_interned(), "there were no ids left to use");
assert_eq!(d.get(b"status").and_then(|v| v.as_text()), Some("open"));
assert_eq!(
d.path("$.lines[0].sku")
.expect("a path")
.and_then(|v| v.as_text()),
Some("sku-0")
);
}
fn item(lang: &str, v: &[f32]) -> Vec<u8> {
let mut b = Builder::new();
b.begin_object().expect("open");
b.key(b"lang").expect("key");
b.text(lang).expect("value");
b.key(b"embedding").expect("key");
b.begin_array().expect("open");
for x in v {
b.float(f64::from(*x)).expect("value");
}
b.end_array().expect("close");
b.end_object().expect("close");
b.finish().expect("finished").to_vec()
}
fn bare(lang: &str) -> Vec<u8> {
let mut b = Builder::new();
b.begin_object().expect("open");
b.key(b"lang").expect("key");
b.text(lang).expect("value");
b.end_object().expect("close");
b.finish().expect("finished").to_vec()
}
fn spread(n: u64) -> [f32; 8] {
let mut s = n.wrapping_mul(0x9e37_79b9_7f4a_7c15) | 1;
let mut v = [0.0f32; 8];
for x in &mut v {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
*x = (s >> 40) as f32 / 4096.0 - 1.0;
}
v
}
#[test]
fn a_document_and_its_embedding_are_one_write() {
let mut docs = Docs::new();
docs.create_vector_index("$.embedding", 3).expect("index");
for (id, v) in [
("a", [1.0, 0.0, 0.0]),
("b", [0.0, 1.0, 0.0]),
("c", [0.0, 0.0, 1.0]),
] {
docs.put_bytes(id.as_bytes(), &item("en", &v)).expect("put");
}
assert_eq!(docs.vector_index("$.embedding").expect("declared").len(), 3);
let mut got = Vec::new();
let n = docs
.nearest("$.embedding", &[0.9, 0.1, 0.0], 3, |id, doc, d| {
let lang = doc
.get(b"lang")
.and_then(|v| v.as_text())
.map(str::to_owned);
got.push((id.to_vec(), lang, d));
})
.expect("nearest");
assert_eq!(n, 3);
assert_eq!(got[0].0, b"a".to_vec());
assert_eq!(got[0].1.as_deref(), Some("en"));
assert!(got[0].2 <= got[1].2 && got[1].2 <= got[2].2);
assert!(
docs.nearest("$.lang", &[1.0, 0.0, 0.0], 1, |_, _, _| {})
.is_err()
);
}
#[test]
fn an_embedding_of_the_wrong_shape_fails_the_write_and_stores_nothing() {
let mut docs = Docs::new();
docs.create_vector_index("$.embedding", 3).expect("index");
docs.put_bytes(b"a", &item("en", &[1.0, 0.0, 0.0]))
.expect("put");
for wrong in [vec![1.0, 0.0], vec![1.0, 0.0, 0.0, 0.0]] {
assert!(docs.put_bytes(b"b", &item("en", &wrong)).is_err());
}
assert!(docs.get(b"b").is_none(), "the write left nothing behind");
assert_eq!(docs.len(), 1);
assert_eq!(docs.vector_index("$.embedding").expect("declared").len(), 1);
}
#[test]
fn a_document_with_nothing_at_the_path_is_not_in_the_index() {
let mut docs = Docs::new();
docs.create_vector_index("$.embedding", 3).expect("index");
docs.put_bytes(b"a", &item("en", &[1.0, 0.0, 0.0]))
.expect("put");
docs.put_bytes(b"b", &bare("en")).expect("put");
assert_eq!(docs.len(), 2);
assert_eq!(docs.vector_index("$.embedding").expect("declared").len(), 1);
docs.put_bytes(b"a", &bare("en")).expect("put");
assert!(
docs.vector_index("$.embedding")
.expect("declared")
.is_empty()
);
assert!(docs.get(b"a").is_some());
docs.put_bytes(b"a", &item("en", &[1.0, 0.0, 0.0]))
.expect("put");
assert!(docs.remove(b"a"));
assert!(
docs.vector_index("$.embedding")
.expect("declared")
.is_empty()
);
}
#[test]
fn a_filter_finds_the_nearest_match_and_not_the_nearest_that_matches() {
let mut docs = Docs::new();
docs.create_index("$.lang").expect("index");
docs.create_vector_index("$.embedding", 8).expect("index");
for n in 0..400u64 {
let id = format!("en:{n}");
docs.put_bytes(id.as_bytes(), &item("en", &spread(n)))
.expect("put");
}
docs.put_bytes(b"fr", &item("fr", &spread(9_999)))
.expect("put");
let q = spread(3);
let mut top = Vec::new();
docs.nearest("$.embedding", &q, 20, |id, _, _| top.push(id.to_vec()))
.expect("nearest");
assert_eq!(top[0], b"en:3".to_vec());
assert!(
!top.iter().any(|id| id == b"fr"),
"searching and then filtering would have answered nothing"
);
let french = [("$.lang", Key::text("fr"))];
let mut found = Vec::new();
docs.nearest_where("$.embedding", &q, 5, &french, |id, _, _| {
found.push(id.to_vec())
})
.expect("nearest");
assert_eq!(found, [b"fr".to_vec()]);
let nothing = [("$.topic", Key::text("finance"))];
assert!(
docs.nearest_where("$.embedding", &q, 5, ¬hing, |_, _, _| {})
.is_err()
);
}
#[test]
fn declaring_either_index_last_gives_the_same_answers() {
let q = spread(11);
let french = [("$.lang", Key::text("fr"))];
let mut late = Docs::new();
late.create_vector_index("$.embedding", 8).expect("index");
for n in 0..200u64 {
let lang = if n % 50 == 0 { "fr" } else { "en" };
let id = format!("{n}");
late.put_bytes(id.as_bytes(), &item(lang, &spread(n)))
.expect("put");
}
late.create_index("$.lang").expect("index");
let mut early = Docs::new();
early.create_index("$.lang").expect("index");
for n in 0..200u64 {
let lang = if n % 50 == 0 { "fr" } else { "en" };
let id = format!("{n}");
early
.put_bytes(id.as_bytes(), &item(lang, &spread(n)))
.expect("put");
}
early.create_vector_index("$.embedding", 8).expect("index");
let mut a = Vec::new();
late.nearest_where("$.embedding", &q, 4, &french, |id, _, _| {
a.push(id.to_vec())
})
.expect("nearest");
let mut b = Vec::new();
early
.nearest_where("$.embedding", &q, 4, &french, |id, _, _| {
b.push(id.to_vec())
})
.expect("nearest");
assert_eq!(a.len(), 4, "there are four French documents to find");
assert_eq!(a, b);
assert!(late.drop_index("$.lang"));
late.create_index("$.lang").expect("index");
let mut again = Vec::new();
late.nearest_where("$.embedding", &q, 4, &french, |id, _, _| {
again.push(id.to_vec());
})
.expect("nearest");
assert_eq!(again, a);
}
#[test]
fn nearest_to_leaves_the_document_itself_out() {
let mut docs = Docs::new();
docs.create_vector_index("$.embedding", 3).expect("index");
for (id, v) in [
("a", [1.0, 0.0, 0.0]),
("b", [0.9, 0.1, 0.0]),
("c", [0.0, 0.0, 1.0]),
] {
docs.put_bytes(id.as_bytes(), &item("en", &v)).expect("put");
}
let mut like = Vec::new();
docs.nearest_to("$.embedding", b"a", 2, |id, _, _| like.push(id.to_vec()))
.expect("nearest");
assert_eq!(like, [b"b".to_vec(), b"c".to_vec()]);
docs.put_bytes(b"d", &bare("en")).expect("put");
let mut none = 0;
assert_eq!(
docs.nearest_to("$.embedding", b"d", 2, |_, _, _| none += 1)
.expect("nearest"),
0
);
}
#[test]
fn declaring_the_same_vector_index_again_rebuilds_nothing() {
let mut docs = Docs::new();
for n in 0..8u64 {
let id = format!("{n}");
docs.put_bytes(id.as_bytes(), &item("en", &spread(n)))
.expect("put");
}
docs.create_vector_index("$.embedding", 8).expect("index");
assert_eq!(docs.vector_index("$.embedding").expect("declared").len(), 8);
docs.create_vector_index("$.embedding", 8).expect("again");
assert_eq!(docs.vector_indexes().len(), 1);
assert!(docs.create_vector_index("$.embedding", 4).is_err());
let still = docs.vector_index("$.embedding").expect("still declared");
assert_eq!(still.dim(), 8);
assert_eq!(still.len(), 8);
assert!(docs.drop_vector_index("$.embedding"));
assert!(!docs.drop_vector_index("$.embedding"));
assert!(docs.vector_indexes().is_empty());
assert_eq!(docs.len(), 8, "the documents are untouched");
}
#[test]
fn clearing_a_collection_empties_the_vector_index_and_keeps_it_declared() {
let mut docs = Docs::new();
docs.create_vector_index("$.embedding", 3).expect("index");
docs.put_bytes(b"a", &item("en", &[1.0, 0.0, 0.0]))
.expect("put");
let full = docs.memory_bytes();
docs.clear();
assert!(docs.is_empty());
assert!(
docs.vector_index("$.embedding")
.expect("declared")
.is_empty()
);
assert!(docs.memory_bytes() < full);
docs.put_bytes(b"b", &item("en", &[0.0, 1.0, 0.0]))
.expect("put");
let mut got = Vec::new();
docs.nearest("$.embedding", &[0.0, 1.0, 0.0], 1, |id, _, _| {
got.push(id.to_vec())
})
.expect("nearest");
assert_eq!(got, [b"b".to_vec()]);
}
}