#![feature(avx512_target_feature)]
#![feature(duration_millis_float)]
#![feature(f16)]
#![feature(path_add_extension)]
#![cfg_attr(target_arch = "aarch64", feature(stdarch_neon_f16))]
#![cfg_attr(
any(target_arch = "x86", target_arch = "x86_64"),
feature(stdarch_x86_avx512_f16)
)]
#![cfg_attr(
any(target_arch = "x86", target_arch = "x86_64"),
feature(stdarch_x86_avx512)
)]
use ahash::HashSet;
use ahash::HashSetExt;
use arbitrary_lock::ArbitraryLock;
use cache::new_cv_cache;
use cache::new_node_cache;
use cache::CVCache;
use cache::NodeCache;
use cfg::Cfg;
use cfg::CompressionMode;
use common::nan_to_num;
use common::Id;
use compaction::compact;
use compressor::pq::ProductQuantizer;
use compressor::trunc::TruncCompressor;
use compressor::Compressor;
use compressor::CV;
use dashmap::DashMap;
use dashmap::DashSet;
use itertools::Itertools;
use metric::Metric;
use metric::StdMetric;
use ordered_float::OrderedFloat;
use parking_lot::Mutex;
use parking_lot::RwLock;
use std::cmp::max;
use std::collections::VecDeque;
use std::convert::identity;
use std::iter::zip;
use std::ops::Deref;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::thread::spawn;
use store::in_memory::InMemoryStore;
use store::rocksdb::RocksDBStore;
use store::schema::DbNodeData;
use store::schema::ADD_EDGES;
use store::schema::CFG;
use store::schema::DELETED;
use store::schema::ID_TO_KEY;
use store::schema::KEY_TO_ID;
use store::schema::NODE;
use store::schema::PQ_MODEL;
use store::Store;
use tracing::debug;
use util::AtomUsz;
use vec::VecData;
pub mod cache;
pub mod cfg;
pub mod common;
pub mod compaction;
pub mod compressor;
pub mod metric;
pub mod store;
pub mod util;
pub mod vec;
enum Mode {
Uncompressed(NodeCache),
Compressed(Arc<dyn Compressor>, CVCache),
}
#[derive(Debug)]
enum PointVec {
Uncompressed(Arc<VecData>),
Compressed(Arc<dyn Compressor>, CV),
}
#[derive(Debug)]
struct Point {
id: Id,
vec: PointVec,
metric_type: StdMetric,
metric: Metric,
dist: OrderedFloat<f64>,
}
impl Point {
pub fn dist(&self, other: &Point) -> f64 {
match (&self.vec, &other.vec) {
(PointVec::Uncompressed(a), PointVec::Uncompressed(b)) => (self.metric)(a, b),
(PointVec::Compressed(c, a), PointVec::Compressed(_c, b)) => c.dist(self.metric_type, a, b),
(PointVec::Uncompressed(u), PointVec::Compressed(c, b))
| (PointVec::Compressed(c, b), PointVec::Uncompressed(u)) => {
c.dist(self.metric_type, &c.compress(u), b)
}
}
}
pub fn dist_query(&self, query: &VecData) -> f64 {
match &self.vec {
PointVec::Uncompressed(v) => (self.metric)(v, query),
PointVec::Compressed(c, cv) => c.dist(self.metric_type, cv, &c.compress(query)),
}
}
}
pub struct State {
add_edges: DashMap<Id, Vec<Id>>,
cfg: Cfg,
compaction_check: Mutex<bool>,
compression_transition_check: Mutex<bool>,
count: AtomUsz,
db: Arc<dyn Store>,
deleted: DashSet<Id>,
first_insert_lock: Mutex<bool>,
key_lock: ArbitraryLock<String, Mutex<()>>,
node_write_lock: ArbitraryLock<Id, Mutex<()>>,
metric: Metric,
mode: RwLock<Mode>,
next_id: AtomUsz,
}
#[derive(Clone)]
pub struct CoreNN(Arc<State>);
impl Deref for CoreNN {
type Target = State;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl CoreNN {
pub fn internal_db(&self) -> &Arc<dyn Store> {
&self.db
}
pub fn cfg(&self) -> &Cfg {
&self.cfg
}
fn get_nodes(&self, ids: &[Id]) -> Vec<Option<Arc<DbNodeData>>> {
match &*self.mode.read() {
Mode::Uncompressed(node_cache) => node_cache.multi_get(ids),
Mode::Compressed(..) => self
.db
.read_ents(&NODE, ids.iter())
.into_iter()
.map(|n| n.map(|n| Arc::new(n)))
.collect_vec(),
}
}
fn get_points<'a>(
&'a self,
ids: &'a [Id],
query: Option<&'a VecData>,
) -> impl Iterator<Item = Option<Point>> + 'a {
let vecs = match &*self.mode.read() {
Mode::Uncompressed(node_cache) => node_cache
.multi_get(ids)
.into_iter()
.map(|raw| {
let raw = raw?;
Some(PointVec::Uncompressed(raw.vector.clone()))
})
.collect_vec(),
Mode::Compressed(compressor, cache) => cache
.multi_get(ids)
.into_iter()
.map(|cv| Some(PointVec::Compressed(compressor.clone(), cv?)))
.collect_vec(),
};
zip(ids, vecs).map(move |(&id, vec)| {
let vec = vec?;
let mut node = Point {
id,
vec,
metric: self.metric,
metric_type: self.cfg.metric,
dist: OrderedFloat(f64::INFINITY),
};
if let Some(q) = query {
node.dist.0 = node.dist_query(q);
}
Some(node)
})
}
fn get_point(&self, id: Id, query: Option<&VecData>) -> Option<Point> {
self.get_points(&[id], query).exactly_one().ok().unwrap()
}
fn prune_candidates(&self, node: &VecData, candidate_ids: &[Id]) -> Vec<Id> {
let max_edges = self.cfg.max_edges;
let dist_thresh = self.cfg.distance_threshold;
let mut candidates = self
.get_points(candidate_ids, Some(node))
.filter_map(|n| n)
.sorted_unstable_by_key(|s| s.dist)
.collect::<VecDeque<_>>();
let mut new_neighbors = Vec::new();
while let Some(p_star) = candidates.pop_front() {
new_neighbors.push(p_star.id);
if new_neighbors.len() == max_edges {
break;
}
candidates.retain(|s| {
let cand_dist_to_node = s.dist.0;
let cand_dist_to_pick = p_star.dist(s);
cand_dist_to_node <= cand_dist_to_pick * dist_thresh
});
}
new_neighbors
}
fn search(&self, query: &VecData, k: usize, search_list_cap: usize) -> (Vec<Point>, DashSet<Id>) {
assert!(
search_list_cap >= k,
"search list capacity must be greater than or equal to k"
);
let mut search_list = Vec::<Point>::new();
let seen = DashSet::new();
let mut expanded = HashSet::new();
let Some(entry) = self.get_point(0, Some(query)) else {
return Default::default();
};
search_list.push(entry);
seen.insert(0);
loop {
let to_expand = search_list
.extract_if(.., |p| expanded.insert(p.id))
.take(self.cfg.beam_width)
.collect_vec();
if to_expand.is_empty() {
break;
};
let fetched = self.get_nodes(&to_expand.iter().map(|p| p.id).collect_vec());
let mut to_add = Vec::<Point>::new();
let mut neighbor_ids = Vec::<Id>::new();
for (mut point, node) in zip(to_expand, fetched) {
let Some(node) = node else {
continue;
};
for &neighbor in node.neighbors.iter() {
if !seen.insert(neighbor) {
continue;
}
neighbor_ids.push(neighbor);
}
if let Some(add) = self.add_edges.get(&point.id) {
for &neighbor in add.iter() {
if !seen.insert(neighbor) {
continue;
}
neighbor_ids.push(neighbor);
}
};
point.dist.0 = (self.metric)(&node.vector, query);
to_add.push(point);
}
for p in self.get_points(&neighbor_ids, Some(query)) {
if let Some(p) = p {
to_add.push(p);
}
}
for point in to_add {
if self.deleted.contains(&point.id) && expanded.contains(&point.id) {
continue;
}
let pos = search_list
.binary_search_by_key(&point.dist, |s| s.dist)
.map_or_else(identity, identity);
search_list.insert(pos, point);
}
search_list.truncate(search_list_cap);
}
seen.retain(|id| !self.deleted.contains(id));
search_list.truncate(k);
(search_list, seen)
}
fn new(
dir: Option<impl AsRef<Path>>,
init_cfg: Option<Cfg>,
create_if_missing: bool,
error_if_exists: bool,
) -> CoreNN {
let db: Arc<dyn Store> = match dir {
Some(dir) => Arc::new(RocksDBStore::open(dir, create_if_missing, error_if_exists)),
None => Arc::new(InMemoryStore::new()),
};
debug!("opened database");
let is_empty = NODE.iter(&db).next().is_none();
let cfg = if let Some(cfg) = init_cfg {
CFG.put(&db, (), &cfg);
cfg
} else {
CFG.read(&db, ()).unwrap()
};
debug!(
beam_width = cfg.beam_width,
max_edges = cfg.max_edges,
max_add_edges = cfg.max_add_edges,
metric = ?cfg.metric,
query_search_list_cap = cfg.query_search_list_cap,
update_search_list_cap = cfg.update_search_list_cap,
"loaded config"
);
let deleted = DashSet::new();
DELETED.iter(&db).for_each(|(id, _)| {
deleted.insert(id);
});
debug!(count = deleted.len(), "loaded deleted");
let add_edges = DashMap::new();
let mut next_id = 1;
let mut count = 0;
ADD_EDGES.iter(&db).for_each(|(id, add)| {
add_edges.insert(id, add);
next_id = next_id.max(id + 1);
count += 1;
});
let metric = cfg.metric.get_fn();
debug!(next_id, count, "loaded state");
let mode = if count > cfg.compression_threshold {
let compressor: Option<Arc<dyn Compressor>> = match cfg.compression_mode {
CompressionMode::PQ => PQ_MODEL.read(&db, ()).map(|pq| {
let compressor: Arc<dyn Compressor> = Arc::new(pq);
compressor
}),
CompressionMode::Trunc => Some(Arc::new(TruncCompressor::new(cfg.trunc_dims))),
};
match compressor {
Some(c) => Mode::Compressed(c.clone(), new_cv_cache(db.clone(), c)),
None => Mode::Uncompressed(new_node_cache(db.clone())),
}
} else {
Mode::Uncompressed(new_node_cache(db.clone()))
};
CoreNN(Arc::new(State {
add_edges,
cfg,
compaction_check: Mutex::new(false),
compression_transition_check: Mutex::new(matches!(mode, Mode::Compressed(..))),
count: count.into(),
db,
deleted,
first_insert_lock: Mutex::new(is_empty),
key_lock: ArbitraryLock::new(),
metric,
mode: RwLock::new(mode),
next_id: next_id.into(),
node_write_lock: ArbitraryLock::new(),
}))
}
pub fn create(dir: impl AsRef<Path>, cfg: Cfg) -> CoreNN {
Self::new(Some(dir), Some(cfg), true, true)
}
pub fn open(dir: impl AsRef<Path>) -> CoreNN {
Self::new(Some(dir), None, false, false)
}
pub fn new_in_memory(cfg: Cfg) -> CoreNN {
Self::new(None::<PathBuf>, Some(cfg), false, true)
}
pub fn query<D>(&self, query: &[D], k: usize) -> Vec<(String, f64)>
where
D: num::Float,
VecData: From<Vec<D>>,
{
let query = VecData::from(nan_to_num(query));
self.query_vec(query, k)
}
pub fn query_vec(&self, query: VecData, k: usize) -> Vec<(String, f64)> {
let res = self
.search(&query, k, max(k, self.cfg.query_search_list_cap))
.0;
let keys = self.db.read_ents(&ID_TO_KEY, res.iter().map(|r| r.id));
zip(keys, res)
.filter_map(|(k, r)| k.map(|k| (k, r.dist.0)))
.collect()
}
fn maybe_compact(&self) {
if self.deleted.len() < self.cfg.compaction_threshold_deletes {
return;
};
let mut is_compacting = self.compaction_check.lock();
if *is_compacting {
return;
};
*is_compacting = true;
drop(is_compacting);
let corenn = self.clone();
spawn(move || {
compact(&corenn);
let mut is_compacting = corenn.compaction_check.lock();
*is_compacting = false;
});
}
fn maybe_enable_compression(&self) {
if self.count.get() <= self.cfg.compression_threshold {
return;
};
let mut is_enabled = self.compression_transition_check.lock();
if *is_enabled {
return;
};
*is_enabled = true;
drop(is_enabled);
let corenn = self.clone();
spawn(move || {
tracing::warn!(
threshold = corenn.cfg.compression_threshold,
n = corenn.count.get(),
"enabling compression"
);
let compressor: Arc<dyn Compressor> = match corenn.cfg.compression_mode {
CompressionMode::PQ => {
let pq = ProductQuantizer::<f32>::train_from_corenn(&corenn);
PQ_MODEL.put(&corenn.db, (), &pq);
Arc::new(pq)
}
CompressionMode::Trunc => Arc::new(TruncCompressor::new(corenn.cfg.trunc_dims)),
};
*corenn.mode.write() = Mode::Compressed(
compressor.clone(),
new_cv_cache(corenn.db.clone(), compressor),
);
tracing::info!("enabled compression");
});
}
pub fn insert<D>(&self, key: &String, vec: &[D])
where
D: num::Float,
VecData: From<Vec<D>>,
{
let vec = VecData::from(nan_to_num(vec));
self.insert_vec(key, vec)
}
pub fn insert_vec(&self, key: &String, vec: VecData) {
let vec = Arc::new(vec);
let id = self.next_id.inc();
let lock = self.key_lock.get(key.to_string());
let _g = lock.lock();
let mut txn = Vec::new();
if let Some(existing_id) = KEY_TO_ID.read(&self.db, key) {
ID_TO_KEY.batch_delete(&mut txn, existing_id);
DELETED.batch_put(&mut txn, existing_id, ());
self.deleted.insert(existing_id);
}
ID_TO_KEY.batch_put(&mut txn, id, key);
KEY_TO_ID.batch_put(&mut txn, key, id);
ADD_EDGES.batch_put(&mut txn, id, &vec![]);
{
let mut is_first = self.first_insert_lock.lock();
if *is_first {
*is_first = false;
debug!("first graph update");
NODE.batch_put(&mut txn, 0usize, DbNodeData {
version: 0,
neighbors: vec![id],
vector: vec.clone(),
});
ADD_EDGES.batch_put(&mut txn, 0, vec![]);
NODE.batch_put(&mut txn, id, DbNodeData {
version: 0,
neighbors: vec![0],
vector: vec.clone(),
});
ADD_EDGES.batch_put(&mut txn, id, vec![]);
self.db.write(txn);
return;
};
};
let candidates = self.search(&vec, 1, self.cfg.update_search_list_cap).1;
let neighbors = self.prune_candidates(&vec, &candidates.into_iter().collect_vec());
NODE.batch_put(&mut txn, id, DbNodeData {
version: 0,
neighbors: neighbors.clone(),
vector: vec.clone(),
});
for j in neighbors {
let lock = self.node_write_lock.get(j);
let _g = lock.lock();
let mut add_edges = self
.add_edges
.get(&j)
.map(|e| e.clone())
.unwrap_or_default();
if add_edges.len() + 1 >= self.cfg.max_add_edges {
let Some(DbNodeData {
version,
mut neighbors,
vector,
}) = NODE.read(&self.db, j)
else {
continue;
};
neighbors.extend_from_slice(&add_edges);
neighbors = self.prune_candidates(&vector, &neighbors);
let new_node = DbNodeData {
version: version + 1,
neighbors,
vector,
};
NODE.batch_put(&mut txn, j, &new_node);
if let Mode::Uncompressed(cache) = &*self.mode.read() {
cache.insert(j, Arc::new(new_node));
};
add_edges.clear();
}
add_edges.push(id);
ADD_EDGES.batch_put(&mut txn, j, &add_edges);
self.add_edges.insert(j, add_edges);
}
self.db.write(txn);
self.maybe_enable_compression();
self.maybe_compact();
}
pub fn delete(&self, key: &String) {
let lock = self.key_lock.get(key.to_string());
let _g = lock.lock();
let mut txn = Vec::new();
let Some(existing_id) = KEY_TO_ID.read(&self.db, key) else {
return;
};
ID_TO_KEY.batch_delete(&mut txn, existing_id);
KEY_TO_ID.batch_delete(&mut txn, key);
DELETED.batch_put(&mut txn, existing_id, ());
self.deleted.insert(existing_id);
self.db.write(txn);
self.maybe_compact();
}
}