1use std::collections::HashMap;
23use std::sync::Once;
24
25use async_trait::async_trait;
26use oneai_core::traits::{Filter, Metadata, VectorBackend, VectorHit};
27use rusqlite::ffi::sqlite3_auto_extension;
28use tokio::sync::Mutex;
29
30const OVER_FETCH_FACTOR: usize = 8;
33const MAX_FETCH: usize = 512;
36
37#[allow(clippy::missing_transmute_annotations)]
46fn register_vec_once() {
47 static ONCE: Once = Once::new();
48 ONCE.call_once(|| {
49 unsafe {
57 sqlite3_auto_extension(Some(std::mem::transmute(
58 sqlite_vec::sqlite3_vec_init as *const (),
59 )));
60 }
61 });
62}
63
64pub struct SqliteVecBackend {
66 conn: Mutex<rusqlite::Connection>,
67 dim: usize,
68 rowid_to_id: Mutex<HashMap<i64, String>>,
71 id_to_rowid: Mutex<HashMap<String, i64>>,
72 meta: Mutex<HashMap<String, Metadata>>,
74 next_rowid: Mutex<i64>,
75}
76
77impl SqliteVecBackend {
78 pub fn open(path: &str, dim: usize) -> oneai_core::Result<Self> {
81 register_vec_once();
82 let conn = rusqlite::Connection::open(path)
83 .map_err(|e| oneai_core::OneAIError::Rag(format!("sqlite-vec open {path}: {e}")))?;
84 conn.execute_batch(&format!(
85 "CREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks USING vec0(embedding float[{dim}], document_id text);",
90 ))
91 .map_err(|e| oneai_core::OneAIError::Rag(format!("create vec0: {e}")))?;
92 Ok(Self {
93 conn: Mutex::new(conn),
94 dim,
95 rowid_to_id: Mutex::new(HashMap::new()),
96 id_to_rowid: Mutex::new(HashMap::new()),
97 meta: Mutex::new(HashMap::new()),
98 next_rowid: Mutex::new(1),
99 })
100 }
101
102 pub fn in_memory(dim: usize) -> oneai_core::Result<Self> {
104 Self::open(":memory:", dim)
105 }
106}
107
108#[async_trait]
109impl VectorBackend for SqliteVecBackend {
110 async fn upsert(
111 &self,
112 id: &str,
113 embedding: &[f32],
114 metadata: Metadata,
115 ) -> oneai_core::Result<()> {
116 if embedding.len() != self.dim {
117 return Err(oneai_core::OneAIError::Rag(format!(
118 "SqliteVecBackend: dim mismatch (got {}, expected {})",
119 embedding.len(),
120 self.dim
121 )));
122 }
123 let emb_json = serde_json::to_string(embedding)
125 .map_err(|e| oneai_core::OneAIError::Rag(format!("embed json: {e}")))?;
126 let conn = self.conn.lock().await;
127 let mut id_to_rowid = self.id_to_rowid.lock().await;
128 let mut rowid_to_id = self.rowid_to_id.lock().await;
129 let mut meta = self.meta.lock().await;
130 let mut next_rowid = self.next_rowid.lock().await;
131
132 if let Some(&rowid) = id_to_rowid.get(id) {
133 conn.execute(
135 "DELETE FROM vec_chunks WHERE rowid = ?1",
136 rusqlite::params![rowid],
137 )
138 .map_err(|e| oneai_core::OneAIError::Rag(format!("vec0 delete-row: {e}")))?;
139 conn.execute(
140 "INSERT INTO vec_chunks(rowid, embedding, document_id) VALUES (?1, ?2, ?3)",
141 rusqlite::params![rowid, emb_json, id],
142 )
143 .map_err(|e| oneai_core::OneAIError::Rag(format!("vec0 insert: {e}")))?;
144 } else {
145 let rowid = *next_rowid;
146 *next_rowid += 1;
147 conn.execute(
148 "INSERT INTO vec_chunks(rowid, embedding, document_id) VALUES (?1, ?2, ?3)",
149 rusqlite::params![rowid, emb_json, id],
150 )
151 .map_err(|e| oneai_core::OneAIError::Rag(format!("vec0 insert: {e}")))?;
152 id_to_rowid.insert(id.to_string(), rowid);
153 rowid_to_id.insert(rowid, id.to_string());
154 }
155 meta.insert(id.to_string(), metadata);
156 Ok(())
157 }
158
159 async fn search(
160 &self,
161 query: &[f32],
162 top_k: usize,
163 filter: Option<&Filter>,
164 ) -> oneai_core::Result<Vec<VectorHit>> {
165 if query.len() != self.dim {
166 return Err(oneai_core::OneAIError::Rag(format!(
167 "SqliteVecBackend: query dim mismatch (got {}, expected {})",
168 query.len(),
169 self.dim
170 )));
171 }
172 let q_json = serde_json::to_string(query)
173 .map_err(|e| oneai_core::OneAIError::Rag(format!("query json: {e}")))?;
174 let conn = self.conn.lock().await;
175 let rowid_to_id = self.rowid_to_id.lock().await;
176 let meta = self.meta.lock().await;
177
178 let limit = match filter {
179 Some(f) if !f.metadata_eq.is_empty() && !f.metadata_in.is_empty() => (top_k
180 .saturating_mul(OVER_FETCH_FACTOR))
181 .min(MAX_FETCH)
182 .max(top_k),
183 _ => top_k.max(1),
184 };
185 let sql = "SELECT rowid, distance FROM vec_chunks WHERE embedding MATCH ?1 ORDER BY distance LIMIT ?2";
186 let mut stmt = conn
187 .prepare(sql)
188 .map_err(|e| oneai_core::OneAIError::Rag(format!("vec0 prepare: {e}")))?;
189 let rows = stmt
190 .query_map(rusqlite::params![q_json, limit as i64], |r| {
191 let rowid: i64 = r.get(0)?;
192 let dist: f32 = r.get(1)?;
193 Ok((rowid, dist))
194 })
195 .map_err(|e| oneai_core::OneAIError::Rag(format!("vec0 query: {e}")))?;
196
197 let mut hits: Vec<VectorHit> = Vec::new();
198 for r in rows {
199 let (rowid, dist) =
200 r.map_err(|e| oneai_core::OneAIError::Rag(format!("vec0 row: {e}")))?;
201 let Some(id) = rowid_to_id.get(&rowid) else {
202 continue;
203 };
204 let metadata = meta.get(id).cloned().unwrap_or_default();
205 if let Some(f) = filter {
206 if !f.matches(&metadata) {
207 continue;
208 }
209 }
210 let score = (1.0 - dist).clamp(-1.0, 1.0);
212 hits.push(VectorHit {
213 id: id.clone(),
214 score,
215 metadata,
216 });
217 if filter.is_none() && hits.len() >= top_k {
218 break;
219 }
220 }
221 hits.sort_by(|a, b| {
224 b.score
225 .partial_cmp(&a.score)
226 .unwrap_or(std::cmp::Ordering::Equal)
227 });
228 hits.truncate(top_k);
229 Ok(hits)
230 }
231
232 async fn delete(&self, id: &str) -> oneai_core::Result<()> {
233 let conn = self.conn.lock().await;
234 let mut id_to_rowid = self.id_to_rowid.lock().await;
235 let mut rowid_to_id = self.rowid_to_id.lock().await;
236 let mut meta = self.meta.lock().await;
237 if let Some(rowid) = id_to_rowid.remove(id) {
238 conn.execute(
239 "DELETE FROM vec_chunks WHERE rowid = ?1",
240 rusqlite::params![rowid],
241 )
242 .map_err(|e| oneai_core::OneAIError::Rag(format!("vec0 delete: {e}")))?;
243 rowid_to_id.remove(&rowid);
244 meta.remove(id);
245 }
246 Ok(())
247 }
248
249 fn dimension(&self) -> usize {
250 self.dim
251 }
252}
253
254#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[tokio::test]
264 async fn knn_upsert_search_filter_delete() {
265 let be = SqliteVecBackend::in_memory(4).unwrap();
266 be.upsert(
267 "a",
268 &[0.1, 0.2, 0.3, 0.4],
269 Metadata::from([("k".into(), "x".into())]),
270 )
271 .await
272 .unwrap();
273 be.upsert(
274 "b",
275 &[0.11, 0.21, 0.31, 0.41],
276 Metadata::from([("k".into(), "y".into())]),
277 )
278 .await
279 .unwrap();
280 be.upsert(
281 "c",
282 &[0.9, 0.8, 0.7, 0.6],
283 Metadata::from([("k".into(), "x".into())]),
284 )
285 .await
286 .unwrap();
287
288 let hits = be.search(&[0.1, 0.2, 0.3, 0.4], 3, None).await.unwrap();
289 assert_eq!(hits[0].id, "a");
290 assert_eq!(hits[1].id, "b");
291 assert!(hits[0].score > 0.99);
292
293 let f = Filter::new().with_eq("k", "x");
294 let hits = be.search(&[0.1, 0.2, 0.3, 0.4], 5, Some(&f)).await.unwrap();
295 assert_eq!(hits.len(), 2);
296 assert!(hits.iter().all(|h| h.metadata["k"] == "x"));
297
298 be.delete("b").await.unwrap();
299 let hits = be
300 .search(&[0.11, 0.21, 0.31, 0.41], 10, None)
301 .await
302 .unwrap();
303 assert!(!hits.iter().any(|h| h.id == "b"));
304 }
305
306 #[tokio::test]
307 async fn upsert_replaces_same_id() {
308 let be = SqliteVecBackend::in_memory(4).unwrap();
309 be.upsert("a", &[0.1, 0.2, 0.3, 0.4], Metadata::new())
310 .await
311 .unwrap();
312 be.upsert("a", &[0.9, 0.8, 0.7, 0.6], Metadata::new())
313 .await
314 .unwrap();
315 let hits = be.search(&[0.9, 0.8, 0.7, 0.6], 1, None).await.unwrap();
316 assert_eq!(hits[0].id, "a");
317 assert!(hits[0].score > 0.99);
318 let hits = be.search(&[0.1, 0.2, 0.3, 0.4], 10, None).await.unwrap();
320 assert_eq!(hits.iter().filter(|h| h.id == "a").count(), 1);
321 }
322}