use crate::Tuning;
pub const M: usize = 16;
pub const EF_CONSTRUCTION: usize = 200;
pub const EF_RUNTIME: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Requested {
pub m: usize,
pub ef_construction: usize,
pub ef_runtime: usize,
pub initial_cap: Option<usize>,
}
impl Default for Requested {
fn default() -> Requested {
Requested {
m: M,
ef_construction: EF_CONSTRUCTION,
ef_runtime: EF_RUNTIME,
initial_cap: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Compat {
#[default]
Permissive,
Strict,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Plan {
Partitions {
tuning: Tuning,
capacity: usize,
},
Graph,
}
impl Requested {
#[must_use]
pub fn plan(&self, compat: Compat) -> Plan {
match compat {
Compat::Strict => Plan::Graph,
Compat::Permissive => Plan::Partitions {
tuning: self.tuning(),
capacity: self.capacity(),
},
}
}
#[must_use]
pub fn tuning(&self) -> Tuning {
let base = Tuning::default();
Tuning {
posting: scale(base.posting, self.ef_construction, EF_CONSTRUCTION).clamp(32, 4096),
probe: scale(base.probe, self.ef_runtime, EF_RUNTIME).clamp(1, 256),
rerank: scale(base.rerank, self.ef_runtime, EF_RUNTIME).clamp(1, 64),
..base
}
}
#[must_use]
pub fn capacity(&self) -> usize {
match self.initial_cap {
Some(n) if n > 0 => n.div_ceil(self.tuning().posting),
_ => 0,
}
}
}
fn scale(base: usize, asked: usize, default: usize) -> usize {
let asked = asked.min(1 << 24);
(base * asked + default / 2) / default
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Bits, Partitions, Vectors};
use yo_common::Rng;
#[test]
fn the_defaults_a_client_did_not_set_are_the_defaults_it_would_have_got() {
let tuning = Requested::default().tuning();
let base = Tuning::default();
assert_eq!(tuning.posting, base.posting);
assert_eq!(tuning.probe, base.probe);
assert_eq!(tuning.rerank, base.rerank);
assert_eq!(tuning.sweep, base.sweep);
assert_eq!(tuning.widen, base.widen);
assert_eq!(Requested::default().capacity(), 0);
}
#[test]
fn m_is_echoed_and_changes_nothing() {
let base = Requested::default();
let plenty = Requested { m: 512, ..base };
assert_eq!(plenty.tuning().posting, base.tuning().posting);
assert_eq!(plenty.tuning().probe, base.tuning().probe);
assert_eq!(plenty.m, 512);
}
#[test]
fn ef_construction_moves_the_posting_size_and_nothing_else() {
let base = Requested::default();
let harder = Requested {
ef_construction: EF_CONSTRUCTION * 2,
..base
};
assert_eq!(harder.tuning().posting, base.tuning().posting * 2);
assert_eq!(harder.tuning().probe, base.tuning().probe);
}
#[test]
fn ef_runtime_moves_the_probe_and_the_rerank_and_nothing_else() {
let base = Requested::default();
let wider = Requested {
ef_runtime: EF_RUNTIME * 4,
..base
};
assert_eq!(wider.tuning().probe, base.tuning().probe * 4);
assert_eq!(wider.tuning().rerank, base.tuning().rerank * 4);
assert_eq!(wider.tuning().posting, base.tuning().posting);
}
#[test]
fn a_number_off_the_network_cannot_ask_for_something_absurd() {
let silly = Requested {
m: usize::MAX,
ef_construction: usize::MAX,
ef_runtime: usize::MAX,
initial_cap: Some(usize::MAX),
};
let tuning = silly.tuning();
assert_eq!(tuning.posting, 4096);
assert_eq!(tuning.probe, 256);
assert_eq!(tuning.rerank, 64);
let none = Requested {
ef_construction: 0,
ef_runtime: 0,
..Requested::default()
};
assert_eq!(none.tuning().posting, 32);
assert_eq!(none.tuning().probe, 1);
assert_eq!(none.tuning().rerank, 1);
}
#[test]
fn initial_cap_asks_for_the_postings_the_vectors_will_need() {
let asked = Requested {
initial_cap: Some(100_000),
..Requested::default()
};
assert_eq!(asked.capacity(), 391);
let bigger = Requested {
ef_construction: EF_CONSTRUCTION * 2,
..asked
};
assert_eq!(bigger.capacity(), 196);
}
#[test]
fn strict_refuses_rather_than_serving_something_else() {
let asked = Requested::default();
assert_eq!(asked.plan(Compat::Strict), Plan::Graph);
assert_eq!(
asked.plan(Compat::Permissive),
Plan::Partitions {
tuning: asked.tuning(),
capacity: 0,
}
);
assert_eq!(Compat::default(), Compat::Permissive);
}
struct Store(Vec<Vec<f32>>);
impl Vectors for Store {
fn get(&self, id: u64, into: &mut [f32]) -> bool {
match self.0.get(id as usize) {
Some(v) => {
into.copy_from_slice(v);
true
}
None => false,
}
}
}
fn corpus(dim: usize, n: usize, clusters: usize, seed: u64) -> Store {
let mut rng = Rng::new(seed);
let centres: Vec<Vec<f32>> = (0..clusters).map(|_| draw(dim, &mut rng)).collect();
Store(
(0..n)
.map(|i| {
let off = draw(dim, &mut rng);
let mut v: Vec<f32> = centres[i % clusters]
.iter()
.zip(&off)
.map(|(c, o)| c + o * 0.7)
.collect();
unit(&mut v);
v
})
.collect(),
)
}
fn draw(dim: usize, rng: &mut Rng) -> Vec<f32> {
let mut v: Vec<f32> = (0..dim)
.map(|i| {
let u = (rng.next_u64() >> 40) as f32 / (1u32 << 24) as f32;
let heavy = if i < dim / 16 { 6.0 } else { 1.0 };
(u * 2.0 - 1.0) * heavy
})
.collect();
unit(&mut v);
v
}
fn unit(v: &mut [f32]) {
let len = v.iter().map(|c| c * c).sum::<f32>().sqrt();
for c in v {
*c /= len;
}
}
fn truth(q: &[f32], store: &Store, k: usize) -> Vec<u64> {
let mut all: Vec<(u64, f32)> = store
.0
.iter()
.enumerate()
.map(|(i, v)| {
(
i as u64,
q.iter().zip(v).map(|(a, b)| (a - b) * (a - b)).sum::<f32>(),
)
})
.collect();
all.sort_by(|a, b| a.1.total_cmp(&b.1));
all[..k].iter().map(|(i, _)| *i).collect()
}
fn recall(dim: usize, asked: Requested, seed: u64) -> f32 {
let store = corpus(dim, 4000, 24, seed);
let mut ix = Partitions::new(dim, Bits::One, 7, asked.tuning());
for (id, v) in store.0.iter().enumerate() {
ix.insert(id as u64, v);
if id % 128 == 0 {
ix.maintain(&store, 4096);
}
}
ix.maintain(&store, 1 << 20);
let queries = corpus(dim, 30, 24, seed ^ 0x5eed);
let mut hits = 0usize;
for q in &queries.0 {
let want = truth(q, &store, 10);
let got: Vec<u64> = ix.search(q, 10, &store).into_iter().map(|h| h.id).collect();
hits += want.iter().filter(|id| got.contains(id)).count();
}
hits as f32 / (queries.0.len() * 10) as f32
}
#[test]
fn an_ef_runtime_client_gets_what_it_turned_the_knob_for() {
let dim = 64;
let low = recall(
dim,
Requested {
ef_runtime: 1,
..Requested::default()
},
0x1379,
);
let high = recall(
dim,
Requested {
ef_runtime: EF_RUNTIME * 8,
..Requested::default()
},
0x1379,
);
assert!(
high > low + 0.05,
"raising EF_RUNTIME went from {low} to {high}, which is not a knob doing anything"
);
assert!(high >= 0.95, "the top of the range only reached {high}");
}
#[test]
fn an_ef_construction_client_gets_what_it_turned_the_knob_for() {
let dim = 64;
let coarse = recall(
dim,
Requested {
ef_construction: EF_CONSTRUCTION * 8,
ef_runtime: 2,
..Requested::default()
},
0x2468,
);
let fine = recall(
dim,
Requested {
ef_construction: EF_CONSTRUCTION / 4,
ef_runtime: 2,
..Requested::default()
},
0x2468,
);
assert!(
fine > coarse + 0.05,
"smaller postings for the same probe went from {coarse} to {fine}"
);
}
}