use std::sync::Arc;
use numpy::{AllowTypeChange, IntoPyArray, PyArray1, PyArray2, PyArrayLike1, PyArrayLike2};
use numpy::{PyReadonlyArray2, PyUntypedArrayMethods};
use pyo3::exceptions::{PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use rayon::prelude::*;
use crate::core::error::HmsError;
use crate::core::qgraph::{
BuildParams, Consolidation, ConsolidationMode, ConsolidationPolicy, Graph, Metric, Residency,
UpdatableVGraph, UpdateError, VGraph, VSearchParams, VSearcher,
};
const THREE_BYTE_IDS_BELOW: usize = (1 << 24) - 1;
fn err(e: HmsError) -> PyErr {
PyValueError::new_err(e.to_string())
}
struct Built {
searcher: VSearcher<'static>,
graph: Arc<VGraph>,
}
impl Built {
fn new(graph: VGraph) -> Self {
let graph = Arc::new(graph);
let shared: &'static VGraph = unsafe { &*Arc::as_ptr(&graph) };
Self {
searcher: shared.searcher(),
graph,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Distance {
Angular,
Euclidean,
InnerProduct,
}
impl Distance {
fn name(self) -> &'static str {
match self {
Self::Angular => "angular",
Self::Euclidean => "euclidean",
Self::InnerProduct => "ip",
}
}
fn graph_metric(self) -> Metric {
match self {
Self::Angular => Metric::Angular,
Self::Euclidean | Self::InnerProduct => Metric::Euclidean,
}
}
fn graph_dim(self, dim: usize) -> usize {
match self {
Self::InnerProduct => dim + 1,
Self::Angular | Self::Euclidean => dim,
}
}
}
fn augment_rows(rows: &[f32], dim: usize) -> Vec<f32> {
let norms2: Vec<f64> = rows
.chunks_exact(dim)
.map(|r| r.iter().map(|&v| f64::from(v) * f64::from(v)).sum())
.collect();
let max2 = norms2.iter().copied().fold(0.0, f64::max);
let mut out = Vec::with_capacity(norms2.len() * (dim + 1));
for (r, n2) in rows.chunks_exact(dim).zip(norms2) {
out.extend_from_slice(r);
out.push((max2 - n2).max(0.0).sqrt() as f32);
}
out
}
fn augment_queries(rows: &[f32], dim: usize) -> Vec<f32> {
rows.chunks_exact(dim)
.flat_map(|q| q.iter().copied().chain([0.0]))
.collect()
}
#[pyclass(name = "VectorIndex")]
struct PyVectorIndex {
metric: Distance,
degree: usize,
build_ef: usize,
code_bits: u8,
residual_bits: u8,
compact_adjacency: bool,
direct_ip: bool,
pool: Option<rayon::ThreadPool>,
ef: usize,
rerank: usize,
reservoir: usize,
patience: usize,
dim: usize,
built: Option<Built>,
query_buf: Vec<f32>,
}
impl PyVectorIndex {
fn run<R: Send>(&self, f: impl FnOnce() -> R + Send) -> R {
match &self.pool {
Some(pool) => pool.install(f),
None => f(),
}
}
fn params(&self) -> VSearchParams {
VSearchParams {
ef: self.ef,
rerank: self.rerank,
reservoir: self.reservoir,
patience: self.patience,
..VSearchParams::default()
}
}
fn check_k(k: usize) -> PyResult<()> {
if k == 0 {
return Err(PyValueError::new_err("k must be positive"));
}
Ok(())
}
}
fn row_major(x: &PyReadonlyArray2<'_, f32>) -> Vec<f32> {
match x.as_slice() {
Ok(s) if x.is_c_contiguous() => s.to_vec(),
_ => x.as_array().iter().copied().collect(),
}
}
fn approximate_neighbors(
index: &VGraph,
queries: &[f32],
dim: usize,
k: usize,
ef: usize,
) -> Result<Vec<u32>, HmsError> {
let params = VSearchParams {
ef: ef.max(k),
rerank: ef.max(k),
..VSearchParams::default()
};
let nq = queries.len() / dim;
let chunk = nq.div_ceil(rayon::current_num_threads() * 16).max(1);
let mut ids = vec![0u32; nq * k];
ids.par_chunks_mut(chunk * k)
.zip(queries.par_chunks(chunk * dim))
.try_for_each(|(out, rows)| {
let mut searcher = index.searcher();
let mut found = Vec::with_capacity(k);
for (q, slot) in rows.chunks_exact(dim).zip(out.chunks_exact_mut(k)) {
searcher.search(q, k, params, &mut found)?;
if found.len() != k {
return Err(HmsError::InvalidParam {
details: format!(
"a training query found {} of {k} neighbours",
found.len()
),
});
}
slot.copy_from_slice(&found);
}
Ok(())
})?;
Ok(ids)
}
fn ids_array<'py>(py: Python<'py>, ids: &[u32]) -> Bound<'py, PyArray1<i64>> {
ids.iter()
.map(|&i| i64::from(i))
.collect::<Vec<_>>()
.into_pyarray(py)
}
#[pymethods]
impl PyVectorIndex {
#[new]
#[pyo3(signature = (metric="angular", degree=64, build_ef=500, threads=0, code_bits=4, compact_adjacency=false, direct_ip=false, residual_bits=8))]
#[allow(clippy::too_many_arguments)]
fn new(
metric: &str,
degree: usize,
build_ef: usize,
threads: usize,
code_bits: u8,
compact_adjacency: bool,
direct_ip: bool,
residual_bits: u8,
) -> PyResult<Self> {
let metric = match metric {
"angular" => Distance::Angular,
"euclidean" => Distance::Euclidean,
"ip" => Distance::InnerProduct,
other => {
return Err(PyValueError::new_err(format!(
"unknown metric {other:?}; VectorIndex supports 'angular', 'euclidean' and 'ip'"
)))
}
};
if direct_ip && metric != Distance::InnerProduct {
return Err(PyValueError::new_err("direct_ip requires metric 'ip'"));
}
if degree == 0 || !degree.is_multiple_of(2) {
return Err(PyValueError::new_err(format!(
"degree must be positive and even, not {degree}"
)));
}
if build_ef == 0 {
return Err(PyValueError::new_err("build_ef must be positive"));
}
if !matches!(code_bits, 4 | 8) {
return Err(PyValueError::new_err(format!(
"code_bits must be 4 or 8, not {code_bits}"
)));
}
if !matches!(residual_bits, 0 | 4 | 8) {
return Err(PyValueError::new_err(format!(
"residual_bits must be 0, 4 or 8, not {residual_bits}"
)));
}
let pool = match threads {
0 => None,
n => Some(
rayon::ThreadPoolBuilder::new()
.num_threads(n)
.build()
.map_err(|e| PyRuntimeError::new_err(format!("thread pool: {e}")))?,
),
};
Ok(Self {
metric,
degree,
build_ef,
code_bits,
residual_bits,
compact_adjacency,
direct_ip,
pool,
ef: 64,
rerank: 64,
reservoir: 0,
patience: 0,
dim: 0,
built: None,
query_buf: Vec::new(),
})
}
#[pyo3(signature = (x, training_queries=None, training_limit=32768, training_neighbors=100, projected_degree=32, training_ef=0))]
#[allow(clippy::too_many_arguments)]
fn fit(
&mut self,
py: Python<'_>,
x: PyArrayLike2<'_, f32, AllowTypeChange>,
training_queries: Option<PyArrayLike2<'_, f32, AllowTypeChange>>,
training_limit: usize,
training_neighbors: usize,
projected_degree: usize,
training_ef: usize,
) -> PyResult<()> {
let (n, dim) = (x.shape()[0], x.shape()[1]);
if n == 0 || dim == 0 {
return Err(PyValueError::new_err(format!(
"fit expects a non-empty matrix, got shape ({n}, {dim})"
)));
}
if training_limit == 0 || training_neighbors < 2 || projected_degree == 0 {
return Err(PyValueError::new_err(
"training_limit and projected_degree must be positive and training_neighbors at least 2",
));
}
let data = match (self.metric, x.as_slice()) {
(Distance::InnerProduct, Ok(s)) if x.is_c_contiguous() => augment_rows(s, dim),
(Distance::InnerProduct, _) => augment_rows(&row_major(&x), dim),
_ => row_major(&x),
};
if data.iter().any(|v| !v.is_finite()) {
return Err(PyValueError::new_err(
"fit base vectors contain non-finite values",
));
}
let training_data = training_queries
.map(|queries| {
let (count, query_dim) = (queries.shape()[0], queries.shape()[1]);
if count == 0 || query_dim != dim {
return Err(PyValueError::new_err(format!(
"training queries must be non-empty with {dim} dimensions, got shape ({count}, {query_dim})"
)));
}
let used = count.min(training_limit) * dim;
let rows: Vec<f32> = match queries.as_slice() {
Ok(s) if queries.is_c_contiguous() => s[..used].to_vec(),
_ => queries.as_array().iter().take(used).copied().collect(),
};
if rows.iter().any(|v| !v.is_finite()) {
return Err(PyValueError::new_err("training queries contain non-finite values"));
}
if self.metric == Distance::Angular
&& rows.chunks_exact(dim).any(|row| row.iter().all(|&v| v == 0.0))
{
return Err(PyValueError::new_err("training queries contain a zero-norm row"));
}
Ok(rows)
})
.transpose()?;
let params = BuildParams {
metric: self.metric.graph_metric(),
degree: self.degree,
build_ef: self.build_ef,
quantized_search: true,
..BuildParams::default()
};
self.built = None;
let id_bytes = if n < THREE_BYTE_IDS_BELOW { 3 } else { 4 };
let metric = self.metric;
let code_bits = self.code_bits;
let residual_bits = self.residual_bits;
let compact_adjacency = self.compact_adjacency;
let direct_ip = self.direct_ip;
let graph = py
.detach(|| {
self.run(|| {
let encode = |g: &Graph| {
if direct_ip {
VGraph::from_lifted_ip_graph(
g,
code_bits,
residual_bits,
true,
true,
id_bytes,
)
} else {
VGraph::from_graph_with(
g,
code_bits,
residual_bits,
true,
true,
id_bytes,
false,
)
}
};
let graph_dim = metric.graph_dim(dim);
let mut g = Graph::build_owned(data, graph_dim, ¶ms)?;
if let Some(queries) = training_data {
let lifted = match metric {
Distance::InnerProduct => augment_queries(&queries, dim),
Distance::Angular | Distance::Euclidean => queries.clone(),
};
if training_ef == 0 {
g.apply_training_queries_with(
&lifted,
training_limit,
training_neighbors,
projected_degree,
)?;
} else {
let k = training_neighbors.min(n);
let pd = projected_degree.min(n.saturating_sub(1)).max(1);
let ids = {
let untrained = encode(&g)?;
let (served, served_dim) = if direct_ip {
(&queries, dim)
} else {
(&lifted, graph_dim)
};
approximate_neighbors(
&untrained,
served,
served_dim,
k,
training_ef,
)?
};
if k >= 2 && pd < n {
g.project_training_neighbors(&lifted, &ids, k, pd)?;
}
}
}
let mut index = encode(&g)?;
if compact_adjacency {
index.compact_adjacency()?;
}
Ok(index)
})
})
.map_err(err)?;
self.dim = dim;
self.built = Some(Built::new(graph));
Ok(())
}
#[pyo3(signature = (ef, rerank=64, patience=0, reservoir=0))]
fn set_query_arguments(
&mut self,
ef: usize,
rerank: usize,
patience: usize,
reservoir: usize,
) -> PyResult<()> {
if ef == 0 {
return Err(PyValueError::new_err("ef must be positive"));
}
self.ef = ef;
self.rerank = rerank;
self.patience = patience;
self.reservoir = reservoir;
Ok(())
}
fn query<'py>(
&mut self,
py: Python<'py>,
v: PyArrayLike1<'py, f32, AllowTypeChange>,
k: usize,
) -> PyResult<Bound<'py, PyArray1<i64>>> {
Self::check_k(k)?;
let params = self.params();
let Self {
built,
query_buf,
metric,
dim,
direct_ip,
..
} = self;
let built = built
.as_mut()
.ok_or_else(|| PyRuntimeError::new_err("the index is not fitted; call fit first"))?;
query_buf.clear();
match v.as_slice() {
Ok(s) => query_buf.extend_from_slice(s),
Err(_) => query_buf.extend(v.as_array().iter().copied()),
}
if *metric == Distance::InnerProduct && !*direct_ip {
if query_buf.len() != *dim {
return Err(PyValueError::new_err(format!(
"the query has {} dimensions, the index has {dim}",
query_buf.len()
)));
}
query_buf.push(0.0);
}
let mut out = Vec::with_capacity(k);
let q = &query_buf[..];
let searcher = &mut built.searcher;
py.detach(|| searcher.search(q, k, params, &mut out))
.map_err(err)?;
Ok(ids_array(py, &out))
}
fn batch_query<'py>(
&self,
py: Python<'py>,
x: PyArrayLike2<'py, f32, AllowTypeChange>,
k: usize,
) -> PyResult<Vec<Bound<'py, PyArray1<i64>>>> {
Self::check_k(k)?;
let built = self
.built
.as_ref()
.ok_or_else(|| PyRuntimeError::new_err("the index is not fitted; call fit first"))?;
let (nq, dim) = (x.shape()[0], x.shape()[1]);
if dim != self.dim {
return Err(PyValueError::new_err(format!(
"queries have {dim} dimensions, the index has {}",
self.dim
)));
}
let mut queries = row_major(&x);
let params = self.params();
let graph = &*built.graph;
let metric = self.metric;
if metric == Distance::InnerProduct && !self.direct_ip {
queries = augment_queries(&queries, dim);
}
let dim = graph.dim();
let results: Vec<Vec<u32>> = py
.detach(|| {
self.run(|| {
let chunk = nq.div_ceil(rayon::current_num_threads() * 4).max(1);
queries
.par_chunks(chunk * dim)
.map(|rows| {
let mut s = graph.searcher();
rows.chunks_exact(dim)
.map(|q| {
let mut out = Vec::with_capacity(k);
s.search(q, k, params, &mut out).map(|_| out)
})
.collect::<Result<Vec<_>, HmsError>>()
})
.collect::<Result<Vec<_>, HmsError>>()
.map(|c| c.into_iter().flatten().collect())
})
})
.map_err(err)?;
Ok(results.iter().map(|ids| ids_array(py, ids)).collect())
}
fn batch_query_array<'py>(
&self,
py: Python<'py>,
x: PyArrayLike2<'py, f32, AllowTypeChange>,
k: usize,
) -> PyResult<Bound<'py, PyArray2<i64>>> {
Self::check_k(k)?;
let built = self
.built
.as_ref()
.ok_or_else(|| PyRuntimeError::new_err("the index is not fitted; call fit first"))?;
let (nq, dim) = (x.shape()[0], x.shape()[1]);
if dim != self.dim {
return Err(PyValueError::new_err(format!(
"queries have {dim} dimensions, the index has {}",
self.dim
)));
}
let mut queries = row_major(&x);
if self.metric == Distance::InnerProduct && !self.direct_ip {
queries = augment_queries(&queries, dim);
}
let params = self.params();
let graph = &*built.graph;
let dim = graph.dim();
let mut ids = vec![-1i64; nq * k];
py.detach(|| {
self.run(|| {
let chunk = nq.div_ceil(rayon::current_num_threads() * 4).max(1);
ids.par_chunks_mut(chunk * k)
.zip(queries.par_chunks(chunk * dim))
.try_for_each(|(out, rows)| {
let mut s = graph.searcher();
let mut found = Vec::with_capacity(k);
for (q, slot) in rows.chunks_exact(dim).zip(out.chunks_exact_mut(k)) {
s.search(q, k, params, &mut found)?;
for (o, &id) in slot.iter_mut().zip(&found) {
*o = i64::from(id);
}
}
Ok::<(), HmsError>(())
})
})
})
.map_err(err)?;
let array = numpy::ndarray::Array2::from_shape_vec((nq, k), ids)
.map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
Ok(array.into_pyarray(py))
}
fn save(&self, py: Python<'_>, path: std::path::PathBuf) -> PyResult<()> {
let built = self
.built
.as_ref()
.ok_or_else(|| PyRuntimeError::new_err("the index is not fitted; call fit first"))?;
let graph = &*built.graph;
py.detach(|| graph.save(&path))
.map_err(|e| PyRuntimeError::new_err(format!("save {}: {e}", path.display())))
}
fn load(&mut self, py: Python<'_>, path: std::path::PathBuf) -> PyResult<()> {
let graph = py
.detach(|| VGraph::open(&path, Residency::Heap))
.map_err(|e| PyRuntimeError::new_err(format!("load {}: {e}", path.display())))?;
let lifted = self.metric == Distance::InnerProduct && !self.direct_ip;
let dim = graph.dim() - usize::from(lifted);
if dim == 0 {
return Err(PyValueError::new_err(
"the saved index does not match this metric",
));
}
self.code_bits = graph.code_bits();
self.residual_bits = graph.residual_bits();
self.dim = dim;
self.built = Some(Built::new(graph));
Ok(())
}
#[getter]
fn index_bytes(&self) -> usize {
self.built.as_ref().map_or(0, |b| b.graph.index_bytes())
}
#[getter]
fn memory_breakdown(&self) -> std::collections::BTreeMap<String, usize> {
let bytes = self
.built
.as_ref()
.map_or_else(Default::default, |b| b.graph.memory_breakdown());
[
("adjacency_ids_bytes", bytes.adjacency_ids_bytes),
("adjacency_offsets_bytes", bytes.adjacency_offsets_bytes),
("traversal_codes_bytes", bytes.traversal_codes_bytes),
("traversal_scales_bytes", bytes.traversal_scales_bytes),
("residual_codes_bytes", bytes.residual_codes_bytes),
("residual_scales_bytes", bytes.residual_scales_bytes),
("mean_bytes", bytes.mean_bytes),
("norms_bytes", bytes.norms_bytes),
("original_ids_bytes", bytes.original_ids_bytes),
("screen_bytes", bytes.screen_bytes),
("neighbor_blocks_bytes", bytes.neighbor_blocks_bytes),
("upper_layers_bytes", bytes.upper_layers_bytes),
]
.into_iter()
.map(|(name, value)| (name.to_owned(), value))
.collect()
}
#[getter]
fn code_bits(&self) -> u8 {
self.code_bits
}
#[getter]
fn residual_bits(&self) -> u8 {
self.residual_bits
}
#[getter]
fn dim(&self) -> usize {
self.dim
}
fn __len__(&self) -> usize {
self.built.as_ref().map_or(0, |b| b.graph.len())
}
fn __repr__(&self) -> String {
format!(
"VectorIndex(metric='{}', degree={}, build_ef={}, ef={}, rerank={}, direct_ip={}, reservoir={}, code_bits={}, residual_bits={}, n={})",
self.metric.name(),
self.degree,
self.build_ef,
self.ef,
self.rerank,
self.direct_ip,
self.reservoir,
self.code_bits,
self.residual_bits,
self.__len__()
)
}
}
fn update_err(e: UpdateError) -> PyErr {
match e {
UpdateError::Io(_) | UpdateError::Persistence(_) | UpdateError::RecoveryRequired => {
PyRuntimeError::new_err(e.to_string())
}
_ => PyValueError::new_err(e.to_string()),
}
}
fn u32_ids(ids: &PyArrayLike1<'_, i64, AllowTypeChange>) -> PyResult<Vec<u32>> {
ids.as_array()
.iter()
.map(|&i| {
u32::try_from(i)
.ok()
.filter(|&i| i != u32::MAX)
.ok_or_else(|| PyValueError::new_err(format!("id {i} is outside 0..2^32-1")))
})
.collect()
}
#[pyclass(name = "UpdatableIndex")]
struct PyUpdatableIndex {
index: UpdatableVGraph,
pool: Option<rayon::ThreadPool>,
ef: usize,
rerank: usize,
}
impl PyUpdatableIndex {
fn run<R: Send>(&self, f: impl FnOnce() -> R + Send) -> R {
match &self.pool {
Some(pool) => pool.install(f),
None => f(),
}
}
fn rows<'a>(
&self,
x: &'a PyArrayLike2<'_, f32, AllowTypeChange>,
n: usize,
) -> PyResult<std::borrow::Cow<'a, [f32]>> {
let (rows, dim) = (x.shape()[0], x.shape()[1]);
if rows != n || dim != self.index.dim() {
return Err(PyValueError::new_err(format!(
"expected a ({n}, {}) matrix, got ({rows}, {dim})",
self.index.dim()
)));
}
Ok(match x.as_slice() {
Ok(rows) if x.is_c_contiguous() => std::borrow::Cow::Borrowed(rows),
_ => std::borrow::Cow::Owned(row_major(x)),
})
}
}
fn thread_pool(threads: usize) -> PyResult<Option<rayon::ThreadPool>> {
match threads {
0 => Ok(None),
n => rayon::ThreadPoolBuilder::new()
.num_threads(n)
.build()
.map(Some)
.map_err(|e| PyRuntimeError::new_err(format!("thread pool: {e}"))),
}
}
#[pymethods]
impl PyUpdatableIndex {
#[staticmethod]
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (path, dim, metric="euclidean", degree=32, build_ef=100, alpha=1.0, threads=0))]
fn create(
py: Python<'_>,
path: std::path::PathBuf,
dim: usize,
metric: &str,
degree: usize,
build_ef: usize,
alpha: f32,
threads: usize,
) -> PyResult<Self> {
let metric = match metric {
"angular" => Metric::Angular,
"euclidean" => Metric::Euclidean,
other => {
return Err(PyValueError::new_err(format!(
"unknown metric {other:?}; UpdatableIndex supports 'angular' and 'euclidean'"
)))
}
};
if degree == 0 || !degree.is_multiple_of(2) || build_ef == 0 || dim == 0 {
return Err(PyValueError::new_err(
"dim and build_ef must be positive and degree positive and even",
));
}
if !(alpha.is_finite() && alpha >= 1.0) {
return Err(PyValueError::new_err("alpha must be at least 1"));
}
let params = BuildParams {
metric,
degree,
build_ef,
alpha,
..BuildParams::default()
};
let pool = thread_pool(threads)?;
let index = py
.detach(|| UpdatableVGraph::create(&path, &[], dim, ¶ms))
.map_err(update_err)?;
Ok(Self {
index,
pool,
ef: 64,
rerank: 64,
})
}
#[staticmethod]
#[pyo3(signature = (path, threads=0))]
fn open(py: Python<'_>, path: std::path::PathBuf, threads: usize) -> PyResult<Self> {
let pool = thread_pool(threads)?;
let index = py
.detach(|| UpdatableVGraph::open(&path))
.map_err(update_err)?;
Ok(Self {
index,
pool,
ef: 64,
rerank: 64,
})
}
#[pyo3(signature = (ef, rerank=64))]
fn set_query_arguments(&mut self, ef: usize, rerank: usize) -> PyResult<()> {
if ef == 0 {
return Err(PyValueError::new_err("ef must be positive"));
}
self.ef = ef;
self.rerank = rerank;
Ok(())
}
fn insert(
&self,
py: Python<'_>,
x: PyArrayLike2<'_, f32, AllowTypeChange>,
ids: PyArrayLike1<'_, i64, AllowTypeChange>,
) -> PyResult<()> {
let ids = u32_ids(&ids)?;
let rows = self.rows(&x, ids.len())?;
let dim = self.index.dim();
let step = self.index.batch_capacity();
py.detach(|| {
self.run(|| {
for (ids, rows) in ids.chunks(step).zip(rows.chunks(step * dim)) {
self.index.insert_batch(ids, rows)?;
self.index.commit()?;
}
Ok(())
})
})
.map_err(update_err)
}
fn delete(&self, py: Python<'_>, ids: PyArrayLike1<'_, i64, AllowTypeChange>) -> PyResult<()> {
let ids = u32_ids(&ids)?;
let step = self.index.batch_capacity();
py.detach(|| {
for chunk in ids.chunks(step) {
self.index.delete_batch(chunk)?;
self.index.commit()?;
}
Ok(())
})
.map_err(update_err)
}
fn replace(
&self,
py: Python<'_>,
x: PyArrayLike2<'_, f32, AllowTypeChange>,
ids: PyArrayLike1<'_, i64, AllowTypeChange>,
) -> PyResult<()> {
let ids = u32_ids(&ids)?;
let rows = self.rows(&x, ids.len())?;
let dim = self.index.dim();
let step = self.index.batch_capacity();
py.detach(|| {
for (ids, rows) in ids.chunks(step).zip(rows.chunks(step * dim)) {
self.index.replace_batch(ids, rows)?;
self.index.commit()?;
}
Ok(())
})
.map_err(update_err)
}
fn search<'py>(
&self,
py: Python<'py>,
x: PyArrayLike2<'py, f32, AllowTypeChange>,
k: usize,
) -> PyResult<Bound<'py, PyArray2<i64>>> {
PyVectorIndex::check_k(k)?;
let nq = x.shape()[0];
let queries = self.rows(&x, nq)?;
let dim = self.index.dim();
let params = VSearchParams {
ef: self.ef,
rerank: self.rerank,
..VSearchParams::default()
};
let mut out = vec![-1i64; nq * k];
py.detach(|| {
self.run(|| {
out.par_chunks_mut(k)
.zip(queries.par_chunks(dim))
.try_for_each_init(Vec::new, |found, (row, q)| {
self.index.search_into(q, k, params, found)?;
for (slot, &id) in row.iter_mut().zip(found.iter()) {
*slot = i64::from(id);
}
Ok::<_, UpdateError>(())
})
})
})
.map_err(update_err)?;
numpy::ndarray::Array2::from_shape_vec((nq, k), out)
.map(|a| a.into_pyarray(py))
.map_err(|e| PyRuntimeError::new_err(e.to_string()))
}
#[pyo3(signature = (delta_ratio=0.1, tombstone_ratio=0.2, min_delta=1024))]
fn needs_consolidation(
&self,
delta_ratio: f64,
tombstone_ratio: f64,
min_delta: usize,
) -> PyResult<bool> {
self.index
.needs_consolidation(ConsolidationPolicy {
delta_ratio,
tombstone_ratio,
min_delta,
})
.map_err(update_err)
}
#[pyo3(signature = (merge=false, rebuild_after=1.0))]
fn consolidate(
&self,
py: Python<'_>,
merge: bool,
rebuild_after: f64,
) -> PyResult<&'static str> {
if rebuild_after.is_nan() || rebuild_after < 0.0 {
return Err(PyValueError::new_err("rebuild_after must be non-negative"));
}
let mode = if merge {
ConsolidationMode::Merge { rebuild_after }
} else {
ConsolidationMode::Rebuild
};
let done = py
.detach(|| self.run(|| self.index.consolidate_with(mode)))
.map_err(update_err)?;
Ok(match done {
Consolidation::Unchanged => "unchanged",
Consolidation::Merged => "merged",
Consolidation::Rebuilt => "rebuilt",
})
}
fn streaming_diagnostics(
&self,
py: Python<'_>,
) -> Option<std::collections::BTreeMap<String, f64>> {
py.detach(|| self.index.streaming_diagnostics())
}
#[getter]
fn dim(&self) -> usize {
self.index.dim()
}
fn __len__(&self) -> usize {
self.index.len()
}
}
pub(super) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyVectorIndex>()?;
m.add_class::<PyUpdatableIndex>()
}