1use crate::error::{AgentDbError, Result};
2use crate::filter;
3use crate::fts::FullTextStore;
4use crate::schema::now_ms;
5use crate::vectors::hnsw::{DistanceMetric, HnswIndex};
6use rusqlite::{params, Connection};
7use serde_json::Value;
8use std::sync::{Arc, Mutex};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13pub struct VectorEntry {
14 pub id: String,
15 pub vector: Vec<f32>,
16 pub metadata: Option<Value>,
17}
18
19#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
21pub struct SearchResult {
22 pub id: String,
23 pub score: f32,
24 pub metadata: Option<Value>,
25}
26
27#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
29pub struct SearchOptions {
30 pub top_k: usize,
31 pub metric: DistanceMetric,
32 pub filter: Option<Value>,
33}
34
35impl Default for SearchOptions {
36 fn default() -> Self {
37 Self {
38 top_k: 10,
39 metric: DistanceMetric::Cosine,
40 filter: None,
41 }
42 }
43}
44
45#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
47pub struct BatchEntry {
48 pub id: String,
49 pub vector: Vec<f32>,
50 pub metadata: Option<Value>,
51}
52
53pub struct Collection {
55 pub id: String,
56 pub name: String,
57 pub dim: usize,
58 pub(crate) conn: Arc<Mutex<Connection>>,
59 index: Mutex<Option<HnswIndex>>,
60 metric: DistanceMetric,
61}
62
63impl Collection {
64 pub(crate) fn new(
65 id: String,
66 name: String,
67 dim: usize,
68 metric: DistanceMetric,
69 conn: Arc<Mutex<Connection>>,
70 ) -> Self {
71 Self {
72 id,
73 name,
74 dim,
75 conn,
76 index: Mutex::new(None),
77 metric,
78 }
79 }
80
81 pub fn upsert(&self, entry: VectorEntry) -> Result<()> {
83 if entry.vector.len() != self.dim {
84 return Err(AgentDbError::DimensionMismatch {
85 expected: self.dim,
86 got: entry.vector.len(),
87 });
88 }
89 let blob: Vec<u8> = entry.vector.iter().flat_map(|f| f.to_le_bytes()).collect();
90 let meta = entry.metadata.as_ref().map(|m| m.to_string());
91 let now = now_ms();
92 let conn = self.conn.lock().unwrap();
93 let inserted = conn.execute(
95 "INSERT OR IGNORE INTO _adb_vectors
96 (id, collection_id, vector, metadata, created_at, updated_at)
97 VALUES (?1, ?2, ?3, ?4, ?5, ?5)",
98 params![entry.id, self.id, blob, meta, now],
99 )?;
100 if inserted == 0 {
101 conn.execute(
102 "UPDATE _adb_vectors SET vector = ?1, metadata = ?2, updated_at = ?5
103 WHERE id = ?3 AND collection_id = ?4",
104 params![blob, meta, entry.id, self.id, now],
105 )?;
106 }
107 conn.execute(
108 "INSERT INTO _adb_hnsw_index (collection_id, index_blob, built_at, is_dirty)
109 VALUES (?1, X'', ?2, 1)
110 ON CONFLICT(collection_id) DO UPDATE SET is_dirty = 1",
111 params![self.id, now_ms()],
112 )?;
113 if inserted > 0 {
114 conn.execute(
115 "UPDATE _adb_collections SET count = count + 1 WHERE id = ?1",
116 params![self.id],
117 )?;
118 }
119 *self.index.lock().unwrap() = None;
120 Ok(())
121 }
122
123 pub fn upsert_batch(&self, entries: Vec<BatchEntry>) -> Result<usize> {
125 if entries.is_empty() {
126 return Ok(0);
127 }
128 for e in &entries {
129 if e.vector.len() != self.dim {
130 return Err(AgentDbError::DimensionMismatch {
131 expected: self.dim,
132 got: e.vector.len(),
133 });
134 }
135 }
136 let conn = self.conn.lock().unwrap();
137 conn.execute_batch("BEGIN")?;
138 let result: Result<usize> = (|| {
139 let mut new_rows: usize = 0;
140 for e in &entries {
141 let blob: Vec<u8> = e.vector.iter().flat_map(|f| f.to_le_bytes()).collect();
142 let meta = e.metadata.as_ref().map(|m| m.to_string());
143 let now = now_ms();
144 let inserted = conn.execute(
146 "INSERT OR IGNORE INTO _adb_vectors
147 (id, collection_id, vector, metadata, created_at, updated_at)
148 VALUES (?1, ?2, ?3, ?4, ?5, ?5)",
149 params![e.id, self.id, blob, meta, now],
150 )?;
151 if inserted == 0 {
152 conn.execute(
153 "UPDATE _adb_vectors SET vector = ?1, metadata = ?2, updated_at = ?5
154 WHERE id = ?3 AND collection_id = ?4",
155 params![blob, meta, e.id, self.id, now],
156 )?;
157 } else {
158 new_rows += 1;
159 }
160 }
161 conn.execute(
162 "INSERT INTO _adb_hnsw_index (collection_id, index_blob, built_at, is_dirty)
163 VALUES (?1, X'', ?2, 1)
164 ON CONFLICT(collection_id) DO UPDATE SET is_dirty = 1",
165 params![self.id, now_ms()],
166 )?;
167 if new_rows > 0 {
168 conn.execute(
169 "UPDATE _adb_collections SET count = count + ?1 WHERE id = ?2",
170 params![new_rows as i64, self.id],
171 )?;
172 }
173 Ok(new_rows)
174 })();
175 match result {
176 Ok(new_rows) => {
177 conn.execute_batch("COMMIT")?;
178 *self.index.lock().unwrap() = None;
179 Ok(new_rows)
180 }
181 Err(e) => {
182 let _ = conn.execute_batch("ROLLBACK");
183 Err(e)
184 }
185 }
186 }
187
188 pub fn search(&self, query: &[f32], opts: SearchOptions) -> Result<Vec<SearchResult>> {
190 if query.len() != self.dim {
191 return Err(AgentDbError::DimensionMismatch {
192 expected: self.dim,
193 got: query.len(),
194 });
195 }
196 self.ensure_index()?;
197 let guard = self.index.lock().unwrap();
198 let index = guard.as_ref().unwrap();
199 let fetch_k = if opts.filter.is_some() {
200 (opts.top_k * 10).max(50)
201 } else {
202 opts.top_k
203 };
204 let raw = index.search(query, fetch_k);
205
206 if raw.is_empty() {
207 return Ok(vec![]);
208 }
209
210 let conn = self.conn.lock().unwrap();
211
212 let placeholders: String = (1..=raw.len())
214 .map(|i| format!("?{}", i))
215 .collect::<Vec<_>>()
216 .join(",");
217 let sql = format!(
218 "SELECT id, metadata FROM _adb_vectors WHERE collection_id = ?{} AND id IN ({})",
219 raw.len() + 1,
220 placeholders
221 );
222 let mut stmt = conn.prepare(&sql)?;
223 let mut param_values: Vec<Box<dyn rusqlite::ToSql>> = raw
224 .iter()
225 .map(|(id, _)| Box::new(id.clone()) as Box<dyn rusqlite::ToSql>)
226 .collect();
227 param_values.push(Box::new(self.id.clone()));
228 let param_refs: Vec<&dyn rusqlite::ToSql> =
229 param_values.iter().map(|p| p.as_ref()).collect();
230
231 let mut meta_map: std::collections::HashMap<String, Option<Value>> =
232 std::collections::HashMap::new();
233 let rows = stmt.query_map(param_refs.as_slice(), |row| {
234 Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?))
235 })?;
236 for row in rows {
237 let (id, meta_str) = row?;
238 let meta: Option<Value> = meta_str
239 .as_deref()
240 .and_then(|s| serde_json::from_str(s).ok());
241 meta_map.insert(id, meta);
242 }
243
244 let mut out = Vec::new();
245 for (id, score) in raw {
246 let meta = meta_map.get(&id).cloned().unwrap_or(None);
247 if let Some(ref f) = opts.filter {
248 match &meta {
249 Some(m) if filter::matches(m, f) => {}
250 _ => continue,
251 }
252 }
253 out.push(SearchResult {
254 id,
255 score,
256 metadata: meta,
257 });
258 if out.len() >= opts.top_k {
259 break;
260 }
261 }
262 Ok(out)
263 }
264
265 pub fn upsert_with_text(&self, entry: VectorEntry, text: &str) -> Result<()> {
270 let id = entry.id.clone();
271 self.upsert(entry)?;
272 let fts = FullTextStore::new(Arc::clone(&self.conn));
273 fts.index_text(&self.name, &id, &self.id, text)
274 }
275
276 pub fn delete(&self, id: &str) -> Result<()> {
278 let conn = self.conn.lock().unwrap();
279 let deleted = conn.execute(
280 "DELETE FROM _adb_vectors WHERE id = ?1 AND collection_id = ?2",
281 params![id, self.id],
282 )?;
283 if deleted > 0 {
284 conn.execute(
285 "UPDATE _adb_collections SET count = MAX(0, count - 1) WHERE id = ?1",
286 params![self.id],
287 )?;
288 }
289 conn.execute(
290 "UPDATE _adb_hnsw_index SET is_dirty = 1 WHERE collection_id = ?1",
291 params![self.id],
292 )?;
293 *self.index.lock().unwrap() = None;
294 Ok(())
295 }
296
297 pub fn reindex(&self) -> Result<()> {
299 let mut index = HnswIndex::new(16, 200, self.metric.clone());
300 {
302 let conn = self.conn.lock().unwrap();
303 let mut stmt =
304 conn.prepare("SELECT id, vector FROM _adb_vectors WHERE collection_id = ?1")?;
305 let rows = stmt.query_map(params![self.id], |row| {
306 Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
307 })?;
308 for row in rows {
309 let (id, blob) = row?;
310 let vec: Vec<f32> = blob
311 .chunks_exact(4)
312 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
313 .collect();
314 index.insert(&id, vec);
315 }
316 }
317 let serialized = index.serialize()?;
318 {
319 let conn = self.conn.lock().unwrap();
320 conn.execute(
321 "INSERT INTO _adb_hnsw_index (collection_id, index_blob, built_at, is_dirty)
322 VALUES (?1, ?2, ?3, 0)
323 ON CONFLICT(collection_id) DO UPDATE SET
324 index_blob = excluded.index_blob,
325 built_at = excluded.built_at,
326 is_dirty = 0",
327 params![self.id, serialized, now_ms()],
328 )?;
329 }
330 *self.index.lock().unwrap() = Some(index);
331 Ok(())
332 }
333
334 fn ensure_index(&self) -> Result<()> {
335 if self.index.lock().unwrap().is_some() {
336 return Ok(());
337 }
338 let blob_opt: Option<Vec<u8>> = {
341 let conn = self.conn.lock().unwrap();
342 conn.query_row(
343 "SELECT index_blob FROM _adb_hnsw_index
344 WHERE collection_id = ?1 AND is_dirty = 0",
345 params![self.id],
346 |r| r.get::<_, Vec<u8>>(0),
347 )
348 .ok()
349 };
350 if let Some(blob) = blob_opt {
351 if !blob.is_empty() {
352 if let Ok(index) = HnswIndex::deserialize(&blob) {
353 *self.index.lock().unwrap() = Some(index);
354 return Ok(());
355 }
356 }
357 }
358 self.reindex()
359 }
360
361 pub fn count(&self) -> Result<i64> {
363 let conn = self.conn.lock().unwrap();
364 Ok(conn.query_row(
365 "SELECT count FROM _adb_collections WHERE id = ?1",
366 params![self.id],
367 |r| r.get(0),
368 )?)
369 }
370}
371
372pub struct VectorStore {
374 conn: Arc<Mutex<Connection>>,
375}
376
377impl VectorStore {
378 pub(crate) fn new(conn: Arc<Mutex<Connection>>) -> Self {
379 Self { conn }
380 }
381
382 pub fn collection(&self, name: &str, dim: usize) -> Result<Collection> {
383 self.collection_with_metric(name, dim, DistanceMetric::Cosine)
384 }
385
386 pub fn collection_with_metric(
387 &self,
388 name: &str,
389 dim: usize,
390 metric: DistanceMetric,
391 ) -> Result<Collection> {
392 let conn = self.conn.lock().unwrap();
393 let existing: Option<(String, usize, String)> = conn
394 .query_row(
395 "SELECT id, dim, metric FROM _adb_collections WHERE name = ?1",
396 params![name],
397 |row| Ok((row.get(0)?, row.get::<_, i64>(1)? as usize, row.get(2)?)),
398 )
399 .ok();
400 if let Some((id, edim, mstr)) = existing {
401 if edim != dim {
402 return Err(AgentDbError::DimensionMismatch {
403 expected: edim,
404 got: dim,
405 });
406 }
407 let m = match mstr.as_str() {
408 "euclidean" => DistanceMetric::Euclidean,
409 "dot" => DistanceMetric::DotProduct,
410 _ => DistanceMetric::Cosine,
411 };
412 return Ok(Collection::new(
413 id,
414 name.to_string(),
415 dim,
416 m,
417 Arc::clone(&self.conn),
418 ));
419 }
420 let id = Uuid::new_v4().to_string();
421 let mstr = match &metric {
422 DistanceMetric::Cosine => "cosine",
423 DistanceMetric::Euclidean => "euclidean",
424 DistanceMetric::DotProduct => "dot",
425 };
426 conn.execute(
427 "INSERT INTO _adb_collections (id, name, dim, metric, count, created_at)
428 VALUES (?1, ?2, ?3, ?4, 0, ?5)",
429 params![id, name, dim as i64, mstr, now_ms()],
430 )?;
431 Ok(Collection::new(
432 id,
433 name.to_string(),
434 dim,
435 metric,
436 Arc::clone(&self.conn),
437 ))
438 }
439
440 pub fn list_collections(&self) -> Result<Vec<(String, usize, i64)>> {
441 let conn = self.conn.lock().unwrap();
442 let mut stmt =
443 conn.prepare("SELECT name, dim, count FROM _adb_collections ORDER BY name")?;
444 let rows = stmt.query_map([], |row| {
445 Ok((
446 row.get::<_, String>(0)?,
447 row.get::<_, i64>(1)? as usize,
448 row.get::<_, i64>(2)?,
449 ))
450 })?;
451 rows.map(|r| r.map_err(AgentDbError::Sqlite)).collect()
452 }
453
454 pub fn drop_collection(&self, name: &str) -> Result<()> {
455 let conn = self.conn.lock().unwrap();
456 conn.execute(
457 "DELETE FROM _adb_collections WHERE name = ?1",
458 params![name],
459 )?;
460 Ok(())
461 }
462}