scone_core/index/
vectors.rs1use std::path::{Path, PathBuf};
8
9use usearch::{Index, IndexOptions, MetricKind, ScalarKind};
10
11use crate::error::{Result, SconeError};
12
13const FILE_NAME: &str = "vectors.usearch";
14
15pub struct VectorIndex {
16 index: Index,
17 path: PathBuf,
18 dim: usize,
19}
20
21fn ix(e: impl std::fmt::Display) -> SconeError {
22 SconeError::Index(format!("vectors: {e}"))
23}
24
25fn new_index(dim: usize) -> Result<Index> {
26 let options = IndexOptions {
27 dimensions: dim,
28 metric: MetricKind::Cos,
29 quantization: ScalarKind::F32,
30 ..Default::default()
31 };
32 usearch::new_index(&options).map_err(ix)
33}
34
35impl std::fmt::Debug for VectorIndex {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("VectorIndex")
38 .field("path", &self.path)
39 .field("dim", &self.dim)
40 .field("size", &self.index.size())
41 .finish()
42 }
43}
44
45impl VectorIndex {
46 pub fn open_or_reset(dir: &Path, dim: usize) -> Result<Self> {
49 match Self::open(dir, dim) {
50 Err(SconeError::Index(_)) => {
51 std::fs::remove_file(dir.join(FILE_NAME))?;
52 Self::open(dir, dim)
53 }
54 other => other,
55 }
56 }
57
58 pub fn open(dir: &Path, dim: usize) -> Result<Self> {
59 std::fs::create_dir_all(dir)?;
60 let path = dir.join(FILE_NAME);
61 let index = new_index(dim)?;
62 if path.exists() {
63 let p = path
64 .to_str()
65 .ok_or_else(|| ix("index path is not valid UTF-8"))?;
66 index.load(p).map_err(ix)?;
67 let stored = index.dimensions();
68 if stored != dim {
69 return Err(SconeError::Index(format!(
70 "vectors: stored dimension {stored} != embedder dimension {dim}; \
71 run `scone doctor --rebuild`"
72 )));
73 }
74 }
75 Ok(Self { index, path, dim })
76 }
77
78 pub fn add(&mut self, rows: &[(u64, &[f32])]) -> Result<()> {
82 let needed = self.index.size() + rows.len();
83 if self.index.capacity() < needed {
84 self.index.reserve(needed).map_err(ix)?;
85 }
86 for (key, vector) in rows {
87 if vector.len() != self.dim {
88 return Err(SconeError::Index(format!(
89 "vectors: vector for key {key} has dimension {} != {}",
90 vector.len(),
91 self.dim
92 )));
93 }
94 if self.index.contains(*key) {
95 continue;
96 }
97 self.index.add(*key, vector).map_err(ix)?;
98 }
99 Ok(())
100 }
101
102 pub fn flush(&self) -> Result<()> {
104 self.save()
105 }
106
107 pub fn search(&self, query: &[f32], k: usize) -> Result<Vec<(u64, f32)>> {
110 if self.index.size() == 0 {
111 return Ok(Vec::new());
112 }
113 let matches = self.index.search(query, k.max(1)).map_err(ix)?;
114 Ok(matches
115 .keys
116 .iter()
117 .zip(&matches.distances)
118 .map(|(key, dist)| (*key, 1.0 - dist))
119 .collect())
120 }
121
122 pub fn wipe(&mut self) -> Result<()> {
123 self.index = new_index(self.dim)?;
124 self.save()
125 }
126
127 fn save(&self) -> Result<()> {
128 let p = self
129 .path
130 .to_str()
131 .ok_or_else(|| ix("index path is not valid UTF-8"))?;
132 self.index.save(p).map_err(ix)
133 }
134}