1use std::collections::HashMap;
4
5use daimon_core::vector_store::VectorStore;
6use daimon_core::{DaimonError, Document, Result, ScoredDocument};
7use deadpool_postgres::Pool;
8use pgvector::Vector;
9
10use crate::DistanceMetric;
11
12pub struct PgVectorStore {
20 pub(crate) pool: Pool,
21 pub(crate) table: String,
22 pub(crate) dimensions: usize,
23 pub(crate) distance_metric: DistanceMetric,
24}
25
26impl PgVectorStore {
27 fn distance_operator(&self) -> &'static str {
29 match self.distance_metric {
30 DistanceMetric::Cosine => "<=>",
31 DistanceMetric::L2 => "<->",
32 DistanceMetric::InnerProduct => "<#>",
33 }
34 }
35
36 fn score_expr(&self) -> String {
43 let op = self.distance_operator();
44 match self.distance_metric {
45 DistanceMetric::Cosine => format!("1.0 - (embedding {op} $1)"),
46 DistanceMetric::L2 => format!("1.0 / (1.0 + (embedding {op} $1))"),
47 DistanceMetric::InnerProduct => format!("-(embedding {op} $1)"),
48 }
49 }
50
51 pub fn pool(&self) -> &Pool {
53 &self.pool
54 }
55
56 pub fn table(&self) -> &str {
58 &self.table
59 }
60
61 pub fn dimensions(&self) -> usize {
63 self.dimensions
64 }
65}
66
67const MAX_ROWS_PER_INSERT: usize = 65_535 / 4;
70
71fn multi_upsert_sql(table: &str, rows: usize) -> String {
73 let mut values = String::with_capacity(rows * 24);
74 for i in 0..rows {
75 if i > 0 {
76 values.push_str(", ");
77 }
78 let base = i * 4;
79 values.push_str(&format!(
80 "(${}, ${}, ${}, ${})",
81 base + 1,
82 base + 2,
83 base + 3,
84 base + 4
85 ));
86 }
87 format!(
88 "INSERT INTO {table} (id, embedding, content, metadata) VALUES {values} \
89 ON CONFLICT (id) DO UPDATE SET embedding = EXCLUDED.embedding, \
90 content = EXCLUDED.content, metadata = EXCLUDED.metadata"
91 )
92}
93
94impl VectorStore for PgVectorStore {
95 async fn upsert(&self, id: &str, embedding: Vec<f32>, document: Document) -> Result<()> {
96 if embedding.len() != self.dimensions {
97 return Err(DaimonError::Other(format!(
98 "embedding dimension mismatch: expected {}, got {}",
99 self.dimensions,
100 embedding.len()
101 )));
102 }
103
104 let client = self
105 .pool
106 .get()
107 .await
108 .map_err(|e| DaimonError::Other(format!("pgvector pool error: {e}")))?;
109
110 let vec = Vector::from(embedding);
111 let metadata = serde_json::to_value(&document.metadata)
112 .map_err(|e| DaimonError::Other(format!("metadata serialization error: {e}")))?;
113
114 let sql = format!(
115 "INSERT INTO {} (id, embedding, content, metadata) VALUES ($1, $2, $3, $4) \
116 ON CONFLICT (id) DO UPDATE SET embedding = EXCLUDED.embedding, \
117 content = EXCLUDED.content, metadata = EXCLUDED.metadata",
118 self.table
119 );
120
121 let stmt = client
122 .prepare_cached(&sql)
123 .await
124 .map_err(|e| DaimonError::Other(format!("pgvector prepare error: {e}")))?;
125 client
126 .execute(&stmt, &[&id, &vec, &document.content, &metadata])
127 .await
128 .map_err(|e| DaimonError::Other(format!("pgvector upsert error: {e}")))?;
129
130 Ok(())
131 }
132
133 async fn upsert_many(&self, items: Vec<(String, Vec<f32>, Document)>) -> Result<()> {
134 if items.is_empty() {
135 return Ok(());
136 }
137 for (_, embedding, _) in &items {
138 if embedding.len() != self.dimensions {
139 return Err(DaimonError::Other(format!(
140 "embedding dimension mismatch: expected {}, got {}",
141 self.dimensions,
142 embedding.len()
143 )));
144 }
145 }
146
147 let mut index: HashMap<String, usize> = HashMap::with_capacity(items.len());
151 let mut rows: Vec<(String, Vector, String, serde_json::Value)> =
152 Vec::with_capacity(items.len());
153 for (id, embedding, document) in items {
154 let metadata = serde_json::to_value(&document.metadata)
155 .map_err(|e| DaimonError::Other(format!("metadata serialization error: {e}")))?;
156 let row = (id, Vector::from(embedding), document.content, metadata);
157 match index.entry(row.0.clone()) {
158 std::collections::hash_map::Entry::Occupied(e) => rows[*e.get()] = row,
159 std::collections::hash_map::Entry::Vacant(e) => {
160 e.insert(rows.len());
161 rows.push(row);
162 }
163 }
164 }
165
166 let client = self
167 .pool
168 .get()
169 .await
170 .map_err(|e| DaimonError::Other(format!("pgvector pool error: {e}")))?;
171
172 for chunk in rows.chunks(MAX_ROWS_PER_INSERT) {
173 let sql = multi_upsert_sql(&self.table, chunk.len());
174 let mut params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> =
175 Vec::with_capacity(chunk.len() * 4);
176 for (id, vec, content, metadata) in chunk {
177 params.push(id);
178 params.push(vec);
179 params.push(content);
180 params.push(metadata);
181 }
182 let stmt = client
183 .prepare_cached(&sql)
184 .await
185 .map_err(|e| DaimonError::Other(format!("pgvector prepare error: {e}")))?;
186 client
187 .execute(&stmt, ¶ms)
188 .await
189 .map_err(|e| DaimonError::Other(format!("pgvector upsert error: {e}")))?;
190 }
191
192 Ok(())
193 }
194
195 async fn query(&self, embedding: Vec<f32>, top_k: usize) -> Result<Vec<ScoredDocument>> {
196 if embedding.len() != self.dimensions {
197 return Err(DaimonError::Other(format!(
198 "embedding dimension mismatch: expected {}, got {}",
199 self.dimensions,
200 embedding.len()
201 )));
202 }
203
204 let client = self
205 .pool
206 .get()
207 .await
208 .map_err(|e| DaimonError::Other(format!("pgvector pool error: {e}")))?;
209
210 let vec = Vector::from(embedding);
211 let op = self.distance_operator();
212 let score_expr = self.score_expr();
213
214 let sql = format!(
215 "SELECT id, content, metadata, {score_expr} AS score \
216 FROM {} ORDER BY embedding {op} $1 LIMIT $2",
217 self.table
218 );
219
220 let stmt = client
221 .prepare_cached(&sql)
222 .await
223 .map_err(|e| DaimonError::Other(format!("pgvector prepare error: {e}")))?;
224 let rows = client
225 .query(&stmt, &[&vec, &(top_k as i64)])
226 .await
227 .map_err(|e| DaimonError::Other(format!("pgvector query error: {e}")))?;
228
229 let mut results = Vec::with_capacity(rows.len());
230 for row in rows {
231 let id: String = row.try_get("id").map_err(|e| {
232 DaimonError::Other(format!("pgvector row missing/invalid id column: {e}"))
233 })?;
234 let content: String = row.try_get("content").map_err(|e| {
235 DaimonError::Other(format!("pgvector row missing/invalid content column: {e}"))
236 })?;
237 let metadata_val: serde_json::Value = row.try_get("metadata").map_err(|e| {
238 DaimonError::Other(format!("pgvector row missing/invalid metadata column: {e}"))
239 })?;
240 let score: f64 = row.try_get("score").map_err(|e| {
241 DaimonError::Other(format!("pgvector row missing/invalid score column: {e}"))
242 })?;
243
244 let metadata: HashMap<String, serde_json::Value> =
245 serde_json::from_value(metadata_val).unwrap_or_default();
246
247 let doc = Document {
248 content,
249 metadata,
250 score: Some(score),
251 };
252 results.push(ScoredDocument::new(id, doc, score));
253 }
254
255 Ok(results)
256 }
257
258 async fn delete(&self, id: &str) -> Result<bool> {
259 let client = self
260 .pool
261 .get()
262 .await
263 .map_err(|e| DaimonError::Other(format!("pgvector pool error: {e}")))?;
264
265 let sql = format!("DELETE FROM {} WHERE id = $1", self.table);
266 let stmt = client
267 .prepare_cached(&sql)
268 .await
269 .map_err(|e| DaimonError::Other(format!("pgvector prepare error: {e}")))?;
270 let deleted = client
271 .execute(&stmt, &[&id])
272 .await
273 .map_err(|e| DaimonError::Other(format!("pgvector delete error: {e}")))?;
274
275 Ok(deleted > 0)
276 }
277
278 async fn count(&self) -> Result<usize> {
279 let client = self
280 .pool
281 .get()
282 .await
283 .map_err(|e| DaimonError::Other(format!("pgvector pool error: {e}")))?;
284
285 let sql = format!("SELECT COUNT(*) AS cnt FROM {}", self.table);
286 let stmt = client
287 .prepare_cached(&sql)
288 .await
289 .map_err(|e| DaimonError::Other(format!("pgvector prepare error: {e}")))?;
290 let row = client
291 .query_one(&stmt, &[])
292 .await
293 .map_err(|e| DaimonError::Other(format!("pgvector count error: {e}")))?;
294
295 let count: i64 = row.get("cnt");
296 Ok(count as usize)
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_distance_operator() {
306 let store = PgVectorStore {
307 pool: create_dummy_pool(),
308 table: "t".into(),
309 dimensions: 3,
310 distance_metric: DistanceMetric::Cosine,
311 };
312 assert_eq!(store.distance_operator(), "<=>");
313
314 let store = PgVectorStore {
315 dimensions: 3,
316 distance_metric: DistanceMetric::L2,
317 ..store
318 };
319 assert_eq!(store.distance_operator(), "<->");
320
321 let store = PgVectorStore {
322 distance_metric: DistanceMetric::InnerProduct,
323 ..store
324 };
325 assert_eq!(store.distance_operator(), "<#>");
326 }
327
328 #[test]
329 fn test_score_expr_per_metric() {
330 let base = PgVectorStore {
331 pool: create_dummy_pool(),
332 table: "t".into(),
333 dimensions: 3,
334 distance_metric: DistanceMetric::Cosine,
335 };
336 assert_eq!(base.score_expr(), "1.0 - (embedding <=> $1)");
337
338 let l2 = PgVectorStore {
339 distance_metric: DistanceMetric::L2,
340 ..base
341 };
342 assert_eq!(l2.score_expr(), "1.0 / (1.0 + (embedding <-> $1))");
344
345 let ip = PgVectorStore {
346 distance_metric: DistanceMetric::InnerProduct,
347 ..l2
348 };
349 assert_eq!(ip.score_expr(), "-(embedding <#> $1)");
350 }
351
352 #[test]
353 fn test_multi_upsert_sql_placeholders() {
354 let one = multi_upsert_sql("t", 1);
355 assert!(one.contains("VALUES ($1, $2, $3, $4) ON CONFLICT"));
356
357 let two = multi_upsert_sql("t", 2);
358 assert!(two.contains("VALUES ($1, $2, $3, $4), ($5, $6, $7, $8) ON CONFLICT"));
359
360 let max = multi_upsert_sql("t", MAX_ROWS_PER_INSERT);
362 assert!(max.contains(&format!("${}", MAX_ROWS_PER_INSERT * 4)));
363 assert!(!max.contains(&format!("${}", MAX_ROWS_PER_INSERT * 4 + 1)));
364 const { assert!(MAX_ROWS_PER_INSERT * 4 <= 65_535) };
365 }
366
367 fn create_dummy_pool() -> Pool {
368 let cfg = deadpool_postgres::Config {
369 host: Some("localhost".into()),
370 port: Some(5432),
371 dbname: Some("test".into()),
372 ..Default::default()
373 };
374 cfg.create_pool(None, tokio_postgres::NoTls).unwrap()
375 }
376}