semantic_memory/
vector_snapshot.rs1use crate::db;
7use crate::error::MemoryError;
8use rusqlite::Connection;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct EmbeddingSnapshotRow {
14 pub item_id: String,
15 pub source_type: String,
16 pub embedding: Vec<f32>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub struct EmbeddingSnapshotV1 {
22 pub embedding_snapshot_digest: String,
23 pub source_digest: String,
24 pub vector_dim: usize,
25 pub rows: Vec<EmbeddingSnapshotRow>,
26}
27
28fn digest_hex(
29 domain: &str,
30 vector_dim: usize,
31 rows: &[EmbeddingSnapshotRow],
32 include_embeddings: bool,
33) -> String {
34 let mut sorted = rows.to_vec();
35 sorted.sort_by(|a, b| {
36 (a.source_type.as_str(), a.item_id.as_str())
37 .cmp(&(b.source_type.as_str(), b.item_id.as_str()))
38 });
39
40 let mut hasher = blake3::Hasher::new();
41 hasher.update(domain.as_bytes());
42 hasher.update(&[0]);
43 hasher.update(&(vector_dim as u64).to_le_bytes());
44 hasher.update(&[0]);
45 for row in &sorted {
46 hasher.update(row.source_type.as_bytes());
47 hasher.update(&[0]);
48 hasher.update(row.item_id.as_bytes());
49 hasher.update(&[0]);
50 if include_embeddings {
51 for value in &row.embedding {
52 hasher.update(&value.to_le_bytes());
53 }
54 hasher.update(&[0]);
55 }
56 }
57 format!("blake3:{}", hasher.finalize().to_hex())
58}
59
60pub fn build_embedding_snapshot(
62 rows: Vec<EmbeddingSnapshotRow>,
63 vector_dim: usize,
64) -> Result<EmbeddingSnapshotV1, MemoryError> {
65 for row in &rows {
66 if row.item_id.is_empty() || row.source_type.is_empty() {
67 return Err(MemoryError::Other(
68 "embedding snapshot rows require non-empty item_id and source_type".to_string(),
69 ));
70 }
71 if row.embedding.len() != vector_dim {
72 return Err(MemoryError::DimensionMismatch {
73 expected: vector_dim,
74 actual: row.embedding.len(),
75 });
76 }
77 }
78 Ok(EmbeddingSnapshotV1 {
79 embedding_snapshot_digest: digest_hex(
80 "semantic-memory.embedding_snapshot.v1",
81 vector_dim,
82 &rows,
83 true,
84 ),
85 source_digest: digest_hex(
86 "semantic-memory.embedding_source.v1",
87 vector_dim,
88 &rows,
89 false,
90 ),
91 vector_dim,
92 rows,
93 })
94}
95
96pub fn load_embedding_snapshot_from_db(
98 conn: &Connection,
99 vector_dim: usize,
100) -> Result<EmbeddingSnapshotV1, MemoryError> {
101 let mut stmt = conn.prepare(
102 "SELECT id, 'fact', embedding FROM facts WHERE embedding IS NOT NULL
103 UNION ALL
104 SELECT id, 'chunk', embedding FROM chunks WHERE embedding IS NOT NULL
105 UNION ALL
106 SELECT CAST(id AS TEXT), 'message', embedding FROM messages WHERE embedding IS NOT NULL
107 UNION ALL
108 SELECT episode_id, 'episode', embedding FROM episodes WHERE embedding IS NOT NULL",
109 )?;
110 let rows = stmt.query_map([], |row| {
111 Ok((
112 row.get::<_, String>(0)?,
113 row.get::<_, String>(1)?,
114 row.get::<_, Vec<u8>>(2)?,
115 ))
116 })?;
117
118 let mut snapshot_rows = Vec::new();
119 for row in rows {
120 let (item_id, source_type, blob) = row?;
121 snapshot_rows.push(EmbeddingSnapshotRow {
122 item_id,
123 source_type,
124 embedding: db::decode_f32_le(&blob, vector_dim)?,
125 });
126 }
127 build_embedding_snapshot(snapshot_rows, vector_dim)
128}
129
130pub fn embedding_row_digest(
132 row: &EmbeddingSnapshotRow,
133 vector_dim: usize,
134) -> Result<String, MemoryError> {
135 Ok(build_embedding_snapshot(vec![row.clone()], vector_dim)?.embedding_snapshot_digest)
136}