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 tx = conn.unchecked_transaction()?;
94 let inserted = tx.execute(
96 "INSERT OR IGNORE INTO _adb_vectors
97 (id, collection_id, vector, metadata, created_at, updated_at)
98 VALUES (?1, ?2, ?3, ?4, ?5, ?5)",
99 params![entry.id, self.id, blob, meta, now],
100 )?;
101 if inserted == 0 {
102 tx.execute(
103 "UPDATE _adb_vectors SET vector = ?1, metadata = ?2, updated_at = ?5
104 WHERE id = ?3 AND collection_id = ?4",
105 params![blob, meta, entry.id, self.id, now],
106 )?;
107 }
108 tx.execute(
109 "INSERT INTO _adb_hnsw_index (collection_id, index_blob, built_at, is_dirty)
110 VALUES (?1, X'', ?2, 1)
111 ON CONFLICT(collection_id) DO UPDATE SET is_dirty = 1",
112 params![self.id, now_ms()],
113 )?;
114 if inserted > 0 {
115 tx.execute(
116 "UPDATE _adb_collections SET count = count + 1 WHERE id = ?1",
117 params![self.id],
118 )?;
119 }
120 tx.commit()?;
121 *self.index.lock().unwrap() = None;
122 Ok(())
123 }
124
125 pub fn upsert_batch(&self, entries: Vec<BatchEntry>) -> Result<usize> {
127 if entries.is_empty() {
128 return Ok(0);
129 }
130 for e in &entries {
131 if e.vector.len() != self.dim {
132 return Err(AgentDbError::DimensionMismatch {
133 expected: self.dim,
134 got: e.vector.len(),
135 });
136 }
137 }
138 let conn = self.conn.lock().unwrap();
139 conn.execute_batch("BEGIN")?;
140 let result: Result<usize> = (|| {
141 let mut new_rows: usize = 0;
142 for e in &entries {
143 let blob: Vec<u8> = e.vector.iter().flat_map(|f| f.to_le_bytes()).collect();
144 let meta = e.metadata.as_ref().map(|m| m.to_string());
145 let now = now_ms();
146 let inserted = conn.execute(
148 "INSERT OR IGNORE INTO _adb_vectors
149 (id, collection_id, vector, metadata, created_at, updated_at)
150 VALUES (?1, ?2, ?3, ?4, ?5, ?5)",
151 params![e.id, self.id, blob, meta, now],
152 )?;
153 if inserted == 0 {
154 conn.execute(
155 "UPDATE _adb_vectors SET vector = ?1, metadata = ?2, updated_at = ?5
156 WHERE id = ?3 AND collection_id = ?4",
157 params![blob, meta, e.id, self.id, now],
158 )?;
159 } else {
160 new_rows += 1;
161 }
162 }
163 conn.execute(
164 "INSERT INTO _adb_hnsw_index (collection_id, index_blob, built_at, is_dirty)
165 VALUES (?1, X'', ?2, 1)
166 ON CONFLICT(collection_id) DO UPDATE SET is_dirty = 1",
167 params![self.id, now_ms()],
168 )?;
169 if new_rows > 0 {
170 conn.execute(
171 "UPDATE _adb_collections SET count = count + ?1 WHERE id = ?2",
172 params![new_rows as i64, self.id],
173 )?;
174 }
175 Ok(new_rows)
176 })();
177 match result {
178 Ok(new_rows) => {
179 conn.execute_batch("COMMIT")?;
180 *self.index.lock().unwrap() = None;
181 Ok(new_rows)
182 }
183 Err(e) => {
184 let _ = conn.execute_batch("ROLLBACK");
185 Err(e)
186 }
187 }
188 }
189
190 pub fn search(&self, query: &[f32], opts: SearchOptions) -> Result<Vec<SearchResult>> {
192 if query.len() != self.dim {
193 return Err(AgentDbError::DimensionMismatch {
194 expected: self.dim,
195 got: query.len(),
196 });
197 }
198 self.ensure_index()?;
199 let guard = self.index.lock().unwrap();
200 let index = guard.as_ref().unwrap();
201 let fetch_k = if opts.filter.is_some() {
202 (opts.top_k * 10).max(50)
203 } else {
204 opts.top_k
205 };
206 let raw = index.search(query, fetch_k);
207
208 if raw.is_empty() {
209 return Ok(vec![]);
210 }
211
212 let conn = self.conn.lock().unwrap();
213
214 let placeholders: String = (1..=raw.len())
216 .map(|i| format!("?{}", i))
217 .collect::<Vec<_>>()
218 .join(",");
219 let sql = format!(
220 "SELECT id, metadata FROM _adb_vectors WHERE collection_id = ?{} AND id IN ({})",
221 raw.len() + 1,
222 placeholders
223 );
224 let mut stmt = conn.prepare(&sql)?;
225 let mut param_values: Vec<Box<dyn rusqlite::ToSql>> = raw
226 .iter()
227 .map(|(id, _)| Box::new(id.clone()) as Box<dyn rusqlite::ToSql>)
228 .collect();
229 param_values.push(Box::new(self.id.clone()));
230 let param_refs: Vec<&dyn rusqlite::ToSql> =
231 param_values.iter().map(|p| p.as_ref()).collect();
232
233 let mut meta_map: std::collections::HashMap<String, Option<Value>> =
234 std::collections::HashMap::new();
235 let rows = stmt.query_map(param_refs.as_slice(), |row| {
236 Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?))
237 })?;
238 for row in rows {
239 let (id, meta_str) = row?;
240 let meta: Option<Value> = meta_str
241 .as_deref()
242 .and_then(|s| serde_json::from_str(s).ok());
243 meta_map.insert(id, meta);
244 }
245
246 let mut out = Vec::new();
247 for (id, score) in raw {
248 let meta = meta_map.get(&id).cloned().unwrap_or(None);
249 if let Some(ref f) = opts.filter {
250 match &meta {
251 Some(m) if filter::matches(m, f) => {}
252 _ => continue,
253 }
254 }
255 out.push(SearchResult {
256 id,
257 score,
258 metadata: meta,
259 });
260 if out.len() >= opts.top_k {
261 break;
262 }
263 }
264 Ok(out)
265 }
266
267 pub fn upsert_with_text(&self, entry: VectorEntry, text: &str) -> Result<()> {
272 let id = entry.id.clone();
273 self.upsert(entry)?;
274 let fts = FullTextStore::new(Arc::clone(&self.conn));
275 fts.index_text(&self.name, &id, &self.id, text)
276 }
277
278 pub fn delete(&self, id: &str) -> Result<()> {
280 let conn = self.conn.lock().unwrap();
281 let deleted = conn.execute(
282 "DELETE FROM _adb_vectors WHERE id = ?1 AND collection_id = ?2",
283 params![id, self.id],
284 )?;
285 if deleted > 0 {
286 conn.execute(
287 "UPDATE _adb_collections SET count = MAX(0, count - 1) WHERE id = ?1",
288 params![self.id],
289 )?;
290 }
291 conn.execute(
292 "UPDATE _adb_hnsw_index SET is_dirty = 1 WHERE collection_id = ?1",
293 params![self.id],
294 )?;
295 *self.index.lock().unwrap() = None;
296 Ok(())
297 }
298
299 pub fn reindex(&self) -> Result<()> {
301 let mut index = HnswIndex::new(16, 200, self.metric.clone());
302 {
304 let conn = self.conn.lock().unwrap();
305 let mut stmt =
306 conn.prepare("SELECT id, vector FROM _adb_vectors WHERE collection_id = ?1")?;
307 let rows = stmt.query_map(params![self.id], |row| {
308 Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
309 })?;
310 for row in rows {
311 let (id, blob) = row?;
312 let vec: Vec<f32> = blob
313 .chunks_exact(4)
314 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
315 .collect();
316 index.insert(&id, vec);
317 }
318 }
319 let serialized = index.serialize()?;
320 {
321 let conn = self.conn.lock().unwrap();
322 conn.execute(
323 "INSERT INTO _adb_hnsw_index (collection_id, index_blob, built_at, is_dirty)
324 VALUES (?1, ?2, ?3, 0)
325 ON CONFLICT(collection_id) DO UPDATE SET
326 index_blob = excluded.index_blob,
327 built_at = excluded.built_at,
328 is_dirty = 0",
329 params![self.id, serialized, now_ms()],
330 )?;
331 }
332 *self.index.lock().unwrap() = Some(index);
333 Ok(())
334 }
335
336 fn ensure_index(&self) -> Result<()> {
337 if self.index.lock().unwrap().is_some() {
338 return Ok(());
339 }
340 let blob_opt: Option<Vec<u8>> = {
343 let conn = self.conn.lock().unwrap();
344 conn.query_row(
345 "SELECT index_blob FROM _adb_hnsw_index
346 WHERE collection_id = ?1 AND is_dirty = 0",
347 params![self.id],
348 |r| r.get::<_, Vec<u8>>(0),
349 )
350 .ok()
351 };
352 if let Some(blob) = blob_opt {
353 if !blob.is_empty() {
354 if let Ok(index) = HnswIndex::deserialize(&blob) {
355 *self.index.lock().unwrap() = Some(index);
356 return Ok(());
357 }
358 }
359 }
360 self.reindex()
361 }
362
363 pub fn count(&self) -> Result<i64> {
365 let conn = self.conn.lock().unwrap();
366 Ok(conn.query_row(
367 "SELECT count FROM _adb_collections WHERE id = ?1",
368 params![self.id],
369 |r| r.get(0),
370 )?)
371 }
372}
373
374pub struct VectorStore {
376 conn: Arc<Mutex<Connection>>,
377}
378
379impl VectorStore {
380 pub(crate) fn new(conn: Arc<Mutex<Connection>>) -> Self {
381 Self { conn }
382 }
383
384 pub fn collection(&self, name: &str, dim: usize) -> Result<Collection> {
385 self.collection_with_metric(name, dim, DistanceMetric::Cosine)
386 }
387
388 pub fn get_collection(&self, name: &str) -> Result<Collection> {
390 let conn = self.conn.lock().unwrap();
391 let (id, dim, mstr): (String, usize, String) = conn
392 .query_row(
393 "SELECT id, dim, metric FROM _adb_collections WHERE name = ?1",
394 params![name],
395 |row| Ok((row.get(0)?, row.get::<_, i64>(1)? as usize, row.get(2)?)),
396 )
397 .map_err(|_| AgentDbError::InvalidArgument(format!("collection not found: {name}")))?;
398 let metric = match mstr.as_str() {
399 "euclidean" => DistanceMetric::Euclidean,
400 "dot" => DistanceMetric::DotProduct,
401 _ => DistanceMetric::Cosine,
402 };
403 Ok(Collection::new(
404 id,
405 name.to_string(),
406 dim,
407 metric,
408 Arc::clone(&self.conn),
409 ))
410 }
411
412 pub fn collection_with_metric(
413 &self,
414 name: &str,
415 dim: usize,
416 metric: DistanceMetric,
417 ) -> Result<Collection> {
418 let conn = self.conn.lock().unwrap();
419 let existing: Option<(String, usize, String)> = conn
420 .query_row(
421 "SELECT id, dim, metric FROM _adb_collections WHERE name = ?1",
422 params![name],
423 |row| Ok((row.get(0)?, row.get::<_, i64>(1)? as usize, row.get(2)?)),
424 )
425 .ok();
426 if let Some((id, edim, mstr)) = existing {
427 if edim != dim {
428 return Err(AgentDbError::DimensionMismatch {
429 expected: edim,
430 got: dim,
431 });
432 }
433 let m = match mstr.as_str() {
434 "euclidean" => DistanceMetric::Euclidean,
435 "dot" => DistanceMetric::DotProduct,
436 _ => DistanceMetric::Cosine,
437 };
438 return Ok(Collection::new(
439 id,
440 name.to_string(),
441 dim,
442 m,
443 Arc::clone(&self.conn),
444 ));
445 }
446 let id = Uuid::new_v4().to_string();
447 let mstr = match &metric {
448 DistanceMetric::Cosine => "cosine",
449 DistanceMetric::Euclidean => "euclidean",
450 DistanceMetric::DotProduct => "dot",
451 };
452 conn.execute(
453 "INSERT INTO _adb_collections (id, name, dim, metric, count, created_at)
454 VALUES (?1, ?2, ?3, ?4, 0, ?5)",
455 params![id, name, dim as i64, mstr, now_ms()],
456 )?;
457 Ok(Collection::new(
458 id,
459 name.to_string(),
460 dim,
461 metric,
462 Arc::clone(&self.conn),
463 ))
464 }
465
466 pub fn list_collections(&self) -> Result<Vec<(String, usize, i64)>> {
467 let conn = self.conn.lock().unwrap();
468 let mut stmt =
469 conn.prepare("SELECT name, dim, count FROM _adb_collections ORDER BY name")?;
470 let rows = stmt.query_map([], |row| {
471 Ok((
472 row.get::<_, String>(0)?,
473 row.get::<_, i64>(1)? as usize,
474 row.get::<_, i64>(2)?,
475 ))
476 })?;
477 rows.map(|r| r.map_err(AgentDbError::Sqlite)).collect()
478 }
479
480 pub fn drop_collection(&self, name: &str) -> Result<()> {
481 let conn = self.conn.lock().unwrap();
482 conn.execute(
483 "DELETE FROM _adb_collections WHERE name = ?1",
484 params![name],
485 )?;
486 Ok(())
487 }
488}