use yo_common::{Code, Error, Result, parse_i64};
use yo_kv::{Foreign, Keyspace};
use yo_shape::Metric;
use yo_vector::hnsw::Requested;
use yo_vector::{Collection, Match, Signature};
use super::args::{self, Args};
use super::table::Spec;
use super::vfilter;
use crate::reply::Out;
const NOT_A_VECTOR_SET: &str = "Operation against a key holding the wrong kind of value";
const BAD_VECTOR: &str = "invalid vector specification";
const BAD_COUNT: &str = "COUNT must be a positive integer";
const BAD_EF: &str = "EF must be a positive integer";
const BAD_M: &str = "M must be a positive integer";
const COUNT: usize = 10;
#[derive(Debug)]
pub(super) struct VectorBody {
c: Collection,
asked: Requested,
quant: &'static str,
side: Vec<Side>,
}
#[derive(Debug, Default, Clone)]
struct Side {
norm: f32,
attr: Option<Box<[u8]>>,
}
impl Foreign for VectorBody {
fn type_name(&self) -> &'static str {
"vectorset"
}
fn encoding(&self) -> &'static str {
"rabitq"
}
fn memory_bytes(&self) -> usize {
let attrs: usize = self
.side
.iter()
.map(|s| s.attr.as_ref().map_or(0, |a| a.len()))
.sum();
self.c.memory_bytes() + self.side.capacity() * size_of::<Side>() + attrs
}
fn is_empty(&self) -> bool {
self.c.is_empty()
}
}
impl VectorBody {
fn new(dim: usize, asked: Requested) -> Result<VectorBody> {
let mut c = Collection::new(dim, Metric::Cosine)?;
c.retune(asked.tuning());
Ok(VectorBody {
c,
asked,
quant: "f32",
side: Vec::new(),
})
}
fn embedding(&self, key: &[u8]) -> Option<Vec<f32>> {
let unit = self.c.get(key)?;
let norm = self.norm(key);
Some(unit.iter().map(|x| x * norm).collect())
}
fn norm(&self, key: &[u8]) -> f32 {
match self.c.id(key).and_then(|id| self.side.get(id as usize)) {
Some(s) if s.norm > 0.0 => s.norm,
_ => 1.0,
}
}
fn attr(&self, key: &[u8]) -> Option<&[u8]> {
let id = self.c.id(key)?;
self.side.get(id as usize)?.attr.as_deref()
}
fn side_mut(&mut self, key: &[u8]) -> &mut Side {
let id = self.c.id(key).expect("the element was just written") as usize;
if self.side.len() <= id {
self.side.resize(id + 1, Side::default());
}
&mut self.side[id]
}
fn retag(&mut self, key: &[u8]) {
let tag = self.attr(key).map_or(0, vfilter::tag);
self.c.retag(key, tag);
}
fn attributes(&self) -> usize {
self.side.iter().filter(|s| s.attr.is_some()).count()
}
}
fn similarity(distance: f32) -> f64 {
f64::from(1.0 - distance / 2.0).clamp(0.0, 1.0)
}
pub(super) fn execute(db: &mut Keyspace, spec: &Spec, args: Args<'_>, out: &mut Out) -> Result<()> {
match spec.name {
"vadd" => vadd(db, args, out),
"vsim" => vsim(db, args, out),
"vrem" => vrem(db, args, out),
"vcard" => vcard(db, args, out),
"vdim" => vdim(db, args, out),
"vemb" => vemb(db, args, out),
"vinfo" => vinfo(db, args, out),
"vismember" => vismember(db, args, out),
"vrandmember" => vrandmember(db, args, out),
"vlinks" => vlinks(db, args, out),
"vsetattr" => vsetattr(db, args, out),
"vgetattr" => vgetattr(db, args, out),
other => unreachable!("{other} is not a vector set command"),
}
}
fn vadd(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
if args.opt(2).is_some_and(|a| args::is(a, b"reduce")) {
return Err(Error::new(
Code::Unsupported,
"REDUCE is not supported. The vector is stored at the dimension it arrives at, and a projection that quietly changed what VDIM says would be worse than saying so",
));
}
let (v, next) = vector(args, 2)?;
let element = args.opt(next).ok_or_else(args::syntax)?;
let opts = Add::parse(args, next + 1)?;
let body = open(db, args.get(1), v.len(), opts.asked)?;
let new = body.c.put(element, &v)?;
body.quant = opts.quant;
let norm = norm(&v);
let side = body.side_mut(element);
side.norm = norm;
if let Some(attr) = opts.attr {
side.attr = Some(attr.into());
}
body.retag(element);
out.int(i64::from(new));
Ok(())
}
fn vsim(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
let (query, next) = if args::is(args.get(2), b"ele") {
(Query::Element(args.opt(3).ok_or_else(args::syntax)?), 4)
} else {
let (v, next) = vector(args, 2)?;
(Query::Vector(v), next)
};
let opts = Sim::parse(args, next)?;
let Some(body) = read(db, args.get(1))? else {
out.array(0);
return Ok(());
};
let mut hits = match query {
Query::Element(e) => {
let Some(q) = body.c.get(e) else {
out.array(0);
return Ok(());
};
let q = q.to_vec();
search(body, &q, opts.effort, Some(e), &opts)?
}
Query::Vector(v) => search(body, &v, opts.effort, None, &opts)?,
};
hits.truncate(opts.count);
answer(body, &hits, &opts, out);
Ok(())
}
fn vrem(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
let Some(body) = write(db, args.get(1))? else {
out.int(0);
return Ok(());
};
let element = args.get(2);
let id = body.c.id(element);
let gone = body.c.remove(element);
if let Some(id) = id.filter(|_| gone)
&& let Some(side) = body.side.get_mut(id as usize)
{
*side = Side::default();
}
out.int(i64::from(gone));
db.reap_foreign(args.get(1));
Ok(())
}
fn vcard(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
let n = read(db, args.get(1))?.map_or(0, |b| b.c.len());
out.uint(n as u64);
Ok(())
}
fn vdim(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
match read(db, args.get(1))? {
Some(body) => out.uint(body.c.dim() as u64),
None => return Err(Error::new(Code::NotFound, "key does not exist")),
}
Ok(())
}
fn vemb(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
let raw = match args.len() {
3 => false,
4 if args::is(args.get(3), b"raw") => true,
_ => return Err(args::syntax()),
};
let Some(body) = read(db, args.get(1))? else {
out.nil_array();
return Ok(());
};
let element = args.get(2);
let Some(v) = body.embedding(element) else {
out.nil_array();
return Ok(());
};
if raw {
let unit = body.c.get(element).expect("the element is there");
let mut bytes = Vec::with_capacity(unit.len() * 4);
for x in unit {
bytes.extend_from_slice(&x.to_le_bytes());
}
out.array(3);
out.bulk(b"f32");
out.bulk(&bytes);
out.double(f64::from(body.norm(element)));
return Ok(());
}
out.array(v.len());
for x in v {
out.double(f64::from(x));
}
Ok(())
}
fn vinfo(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
let Some(body) = read(db, args.get(1))? else {
out.nil();
return Ok(());
};
let t = body.c.tuning();
let fields: [(&[u8], u64); 8] = [
(b"vector-dim", body.c.dim() as u64),
(b"size", body.c.len() as u64),
(b"attributes-count", body.attributes() as u64),
(b"hnsw-m", body.asked.m as u64),
(b"ef-construction", body.asked.ef_construction as u64),
(b"ef-runtime", body.asked.ef_runtime as u64),
(b"partitions", body.c.partitions() as u64),
(b"code-bytes", body.c.code_bytes() as u64),
];
out.map(fields.len() + 4);
out.bulk(b"index-type");
out.bulk(b"partition");
out.bulk(b"quant-type");
out.bulk(body.quant.as_bytes());
for (name, value) in fields {
out.bulk(name);
out.uint(value);
}
out.bulk(b"probe");
out.uint(t.probe as u64);
out.bulk(b"rerank");
out.uint(t.rerank as u64);
Ok(())
}
fn vismember(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
let there = read(db, args.get(1))?.is_some_and(|b| b.c.contains(args.get(2)));
out.int(i64::from(there));
Ok(())
}
fn vrandmember(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
let count = match args.len() {
2 => None,
3 => Some(args.int(2)?),
_ => return Err(args::syntax()),
};
let len = read(db, args.get(1))?.map_or(0, |b| b.c.len());
let Some(count) = count else {
if len == 0 {
out.nil();
return Ok(());
}
let pick = (db.random() % len as u64) as usize;
let body = read(db, args.get(1))?.expect("the set is still there");
out.bulk(body.c.key_at(pick).expect("the draw was under the length"));
return Ok(());
};
if len == 0 {
out.array(0);
return Ok(());
}
let picks = draws(db, count, len);
let body = read(db, args.get(1))?.expect("the set is still there");
out.array(picks.len());
for at in picks {
out.bulk(body.c.key_at(at).expect("the draw was under the length"));
}
Ok(())
}
fn vlinks(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
let withscores = match args.len() {
3 => false,
4 if args::is(args.get(3), b"withscores") => true,
_ => return Err(args::syntax()),
};
let Some(body) = read(db, args.get(1))? else {
out.nil_array();
return Ok(());
};
let element = args.get(2);
let Some(q) = body.c.get(element) else {
out.nil_array();
return Ok(());
};
let q = q.to_vec();
let near = body.c.search(&q, COUNT + 1, Some(element))?;
out.array(1);
write_hits(&near, withscores, out);
Ok(())
}
fn vsetattr(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
let Some(body) = write(db, args.get(1))? else {
out.int(0);
return Ok(());
};
let element = args.get(2);
if !body.c.contains(element) {
out.int(0);
return Ok(());
}
let value = args.get(3);
let side = body.side_mut(element);
side.attr = if value.is_empty() {
None
} else {
Some(value.into())
};
body.retag(element);
out.int(1);
Ok(())
}
fn vgetattr(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
match read(db, args.get(1))?.and_then(|b| b.attr(args.get(2))) {
Some(attr) => out.bulk(attr),
None => out.nil(),
}
Ok(())
}
enum Query<'a> {
Element(&'a [u8]),
Vector(Vec<f32>),
}
struct Add<'a> {
asked: Requested,
quant: &'static str,
attr: Option<&'a [u8]>,
}
impl<'a> Add<'a> {
fn parse(args: Args<'a>, from: usize) -> Result<Add<'a>> {
let mut got = Add {
asked: Requested::default(),
quant: "f32",
attr: None,
};
let mut i = from;
while i < args.len() {
let arg = args.get(i);
let rest = args.len() - i;
if args::is(arg, b"cas") {
i += 1;
} else if args::is(arg, b"noquant") {
got.quant = "f32";
i += 1;
} else if args::is(arg, b"bin") {
got.quant = "bin";
i += 1;
} else if args::is(arg, b"q8") {
got.quant = "int8";
i += 1;
} else if args::is(arg, b"ef") && rest >= 2 {
got.asked.ef_construction = positive(args.get(i + 1), BAD_EF)?;
i += 2;
} else if args::is(arg, b"m") && rest >= 2 {
got.asked.m = positive(args.get(i + 1), BAD_M)?;
i += 2;
} else if args::is(arg, b"setattr") && rest >= 2 {
got.attr = Some(args.get(i + 1));
i += 2;
} else {
return Err(args::syntax());
}
}
Ok(got)
}
}
struct Sim {
count: usize,
effort: usize,
withscores: bool,
withattribs: bool,
truth: bool,
filter: Option<vfilter::Filter>,
}
impl Sim {
fn parse(args: Args<'_>, from: usize) -> Result<Sim> {
let mut got = Sim {
count: COUNT,
effort: COUNT,
withscores: false,
withattribs: false,
truth: false,
filter: None,
};
let mut ef = None;
let mut i = from;
while i < args.len() {
let arg = args.get(i);
let rest = args.len() - i;
if args::is(arg, b"withscores") {
got.withscores = true;
i += 1;
} else if args::is(arg, b"withattribs") {
got.withattribs = true;
i += 1;
} else if args::is(arg, b"truth") {
got.truth = true;
i += 1;
} else if args::is(arg, b"nothread") {
i += 1;
} else if args::is(arg, b"count") && rest >= 2 {
got.count = positive(args.get(i + 1), BAD_COUNT)?;
i += 2;
} else if args::is(arg, b"ef") && rest >= 2 {
ef = Some(positive(args.get(i + 1), BAD_EF)?);
i += 2;
} else if args::is(arg, b"filter") && rest >= 2 {
got.filter = Some(vfilter::Filter::parse(args.get(i + 1))?);
i += 2;
} else if args::is(arg, b"filter-ef") && rest >= 2 {
let asked = match parse_i64(args.get(i + 1)) {
Some(n) => usize::try_from(n).unwrap_or(0),
None => return Err(Error::new(Code::Invalid, BAD_EF)),
};
ef = Some(ef.unwrap_or(0).max(asked));
i += 2;
} else {
return Err(args::syntax());
}
}
got.effort = got.count.max(ef.unwrap_or(0).min(1 << 16));
Ok(got)
}
}
fn search(
body: &VectorBody,
q: &[f32],
k: usize,
skip: Option<&[u8]>,
opts: &Sim,
) -> Result<Vec<Match>> {
let Some(expr) = &opts.filter else {
return if opts.truth {
body.c.search_exact(q, k, skip)
} else {
body.c.search(q, k, skip)
};
};
let want = Filtered {
expr,
want: expr.signature(),
side: &body.side,
};
if opts.truth {
body.c.search_exact_where(q, k, skip, &want)
} else {
body.c.search_where(q, k, skip, &want)
}
}
struct Filtered<'a> {
expr: &'a vfilter::Filter,
want: Signature,
side: &'a [Side],
}
impl yo_vector::Filter for Filtered<'_> {
fn allows(&self, tag: u64) -> bool {
Signature::from_bits(tag).covers(self.want)
}
fn exact(&self, id: u64) -> bool {
let attr = self.side.get(id as usize).and_then(|s| s.attr.as_deref());
self.expr.matches(attr)
}
}
fn answer(body: &VectorBody, hits: &[Match], opts: &Sim, out: &mut Out) {
if !opts.withscores && !opts.withattribs {
out.array(hits.len());
for hit in hits {
out.bulk(&hit.key);
}
return;
}
let extras = usize::from(opts.withscores) + usize::from(opts.withattribs);
if out.proto().is_resp3() {
out.map(hits.len());
for hit in hits {
out.bulk(&hit.key);
if extras > 1 {
out.array(extras);
}
if opts.withscores {
out.double(similarity(hit.distance));
}
if opts.withattribs {
attribute(body, &hit.key, out);
}
}
return;
}
out.array(hits.len() * (1 + extras));
for hit in hits {
out.bulk(&hit.key);
if opts.withscores {
out.double(similarity(hit.distance));
}
if opts.withattribs {
attribute(body, &hit.key, out);
}
}
}
fn attribute(body: &VectorBody, key: &[u8], out: &mut Out) {
match body.attr(key) {
Some(attr) => out.bulk(attr),
None => out.nil(),
}
}
fn write_hits(hits: &[Match], withscores: bool, out: &mut Out) {
out.array(hits.len() * (1 + usize::from(withscores)));
for hit in hits {
out.bulk(&hit.key);
if withscores {
out.double(similarity(hit.distance));
}
}
}
fn vector(args: Args<'_>, at: usize) -> Result<(Vec<f32>, usize)> {
let spec = args.opt(at).ok_or_else(args::syntax)?;
if args::is(spec, b"fp32") {
let blob = args.opt(at + 1).ok_or_else(args::syntax)?;
if blob.is_empty() || blob.len() % 4 != 0 {
return Err(Error::new(Code::Invalid, BAD_VECTOR));
}
let v = blob
.as_chunks::<4>()
.0
.iter()
.map(|b| f32::from_le_bytes(*b))
.collect();
return Ok((v, at + 2));
}
if args::is(spec, b"values") {
let n = positive(args.opt(at + 1).ok_or_else(args::syntax)?, BAD_VECTOR)?;
if args.len() < at + 2 + n {
return Err(args::syntax());
}
let mut v = Vec::with_capacity(n);
for j in 0..n {
let x = args.float(at + 2 + j)?;
#[allow(clippy::cast_possible_truncation)]
v.push(x as f32);
}
return Ok((v, at + 2 + n));
}
Err(Error::new(Code::Invalid, BAD_VECTOR))
}
fn norm(v: &[f32]) -> f32 {
let sum = v.iter().map(|x| f64::from(*x) * f64::from(*x)).sum::<f64>();
#[allow(clippy::cast_possible_truncation)]
let norm = sum.sqrt() as f32;
norm
}
fn draws(db: &mut Keyspace, count: i64, len: usize) -> Vec<usize> {
let Ok(want) = usize::try_from(count) else {
let repeats = usize::try_from(count.unsigned_abs()).unwrap_or(usize::MAX);
return (0..repeats.min(1 << 20))
.map(|_| (db.random() % len as u64) as usize)
.collect();
};
let mut all: Vec<usize> = (0..len).collect();
for i in (1..len).rev() {
all.swap(i, (db.random() % (i as u64 + 1)) as usize);
}
all.truncate(want);
all
}
fn open<'d>(
db: &'d mut Keyspace,
key: &[u8],
dim: usize,
asked: Requested,
) -> Result<&'d mut VectorBody> {
if db.kind_of(key).is_none() {
db.put_foreign(key, Box::new(VectorBody::new(dim, asked)?));
}
match write(db, key)? {
Some(body) => {
if body.c.dim() != dim {
return Err(Error::fmt(
Code::Invalid,
format_args!(
"Vector dimension mismatch - got {dim} but set has {}",
body.c.dim()
),
));
}
Ok(body)
}
None => unreachable!("the vector set was just created"),
}
}
fn write<'d>(db: &'d mut Keyspace, key: &[u8]) -> Result<Option<&'d mut VectorBody>> {
match db.foreign_mut(key)? {
Some(body) => match body.downcast_mut::<VectorBody>() {
Some(body) => Ok(Some(body)),
None => Err(Error::new(Code::WrongType, NOT_A_VECTOR_SET)),
},
None => Ok(None),
}
}
fn read<'d>(db: &'d mut Keyspace, key: &[u8]) -> Result<Option<&'d VectorBody>> {
match db.foreign(key)? {
Some(body) => match body.downcast_ref::<VectorBody>() {
Some(body) => Ok(Some(body)),
None => Err(Error::new(Code::WrongType, NOT_A_VECTOR_SET)),
},
None => Ok(None),
}
}
fn positive(arg: &[u8], msg: &'static str) -> Result<usize> {
match parse_i64(arg) {
Some(n) if n > 0 => Ok(usize::try_from(n).unwrap_or(usize::MAX)),
_ => Err(Error::new(Code::Invalid, msg)),
}
}