1use std::collections::HashMap;
4
5use async_trait::async_trait;
6use parking_lot::Mutex as ParkingMutex;
7use rskit_errors::{AppError, AppResult, ErrorCode};
8use tracing::debug;
9
10use crate::config::VectorStoreLimits;
11use crate::store::{PointPayload, SearchFilter, SearchResult, SimilarityMetric, VectorStore};
12
13struct StoredPoint {
14 id: String,
15 vector: Vec<f32>,
16 payload: PointPayload,
17}
18
19struct Collection {
20 dimensions: usize,
21 metric: SimilarityMetric,
22 points: Vec<StoredPoint>,
23}
24
25pub struct InMemoryVectorStore {
29 default_metric: SimilarityMetric,
30 limits: VectorStoreLimits,
31 collections: ParkingMutex<HashMap<String, Collection>>,
32}
33
34impl InMemoryVectorStore {
35 pub fn new() -> Self {
37 Self::with_metric(SimilarityMetric::Cosine)
38 }
39
40 #[must_use]
42 pub fn with_metric(default_metric: SimilarityMetric) -> Self {
43 Self::with_options(default_metric, VectorStoreLimits::default())
44 }
45
46 #[must_use]
48 pub fn with_options(default_metric: SimilarityMetric, limits: VectorStoreLimits) -> Self {
49 Self {
50 default_metric,
51 limits,
52 collections: ParkingMutex::new(HashMap::new()),
53 }
54 }
55}
56
57impl Default for InMemoryVectorStore {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
64 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
65 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
66 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
67 if norm_a == 0.0 || norm_b == 0.0 {
68 return 0.0;
69 }
70 dot / (norm_a * norm_b)
71}
72
73fn dot_product(a: &[f32], b: &[f32]) -> f32 {
74 a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
75}
76
77fn l2_score(a: &[f32], b: &[f32]) -> f32 {
78 -a.iter()
79 .zip(b.iter())
80 .map(|(x, y)| {
81 let delta = x - y;
82 delta * delta
83 })
84 .sum::<f32>()
85 .sqrt()
86}
87
88fn similarity_score(metric: SimilarityMetric, a: &[f32], b: &[f32]) -> f32 {
89 match metric {
90 SimilarityMetric::Cosine => cosine_similarity(a, b),
91 SimilarityMetric::Dot => dot_product(a, b),
92 SimilarityMetric::L2 => l2_score(a, b),
93 }
94}
95
96fn matches_filter(payload: &PointPayload, filter: &SearchFilter) -> bool {
97 for condition in &filter.must {
98 match payload.fields.get(&condition.field) {
99 Some(actual) if actual == &condition.equals => {}
100 _ => return false,
101 }
102 }
103 true
104}
105
106fn compare_score_desc(a: f32, b: f32) -> std::cmp::Ordering {
107 b.partial_cmp(&a).unwrap_or(std::cmp::Ordering::Equal)
108}
109
110#[async_trait]
111impl VectorStore for InMemoryVectorStore {
112 async fn ensure_collection(&self, collection: &str, dimensions: usize) -> AppResult<()> {
113 self.limits.validate_dimensions(dimensions)?;
114 let mut collections = self.collections.lock();
115 collections
116 .entry(collection.to_string())
117 .or_insert_with(|| Collection {
118 dimensions,
119 metric: self.default_metric,
120 points: Vec::new(),
121 });
122 Ok(())
123 }
124
125 async fn upsert(
126 &self,
127 collection: &str,
128 id: &str,
129 vector: Vec<f32>,
130 payload: PointPayload,
131 ) -> AppResult<()> {
132 debug!(collection, id, "InMemory: upserting vector point");
133
134 self.limits.validate_dimensions(vector.len())?;
135 payload.validate_limits(&self.limits)?;
136
137 let mut collections = self.collections.lock();
138
139 let col = collections.get_mut(collection).ok_or_else(|| {
140 AppError::new(
141 ErrorCode::NotFound,
142 format!("collection '{collection}' does not exist"),
143 )
144 })?;
145
146 if vector.len() != col.dimensions {
147 return Err(AppError::new(
148 ErrorCode::InvalidInput,
149 format!(
150 "vector dimensions mismatch: expected {}, got {}",
151 col.dimensions,
152 vector.len()
153 ),
154 ));
155 }
156
157 if let Some(point) = col.points.iter_mut().find(|p| p.id == id) {
159 point.vector = vector;
160 point.payload = payload;
161 } else {
162 col.points.push(StoredPoint {
163 id: id.to_string(),
164 vector,
165 payload,
166 });
167 }
168
169 Ok(())
170 }
171
172 async fn search(
173 &self,
174 collection: &str,
175 vector: Vec<f32>,
176 limit: usize,
177 filter: Option<SearchFilter>,
178 ) -> AppResult<Vec<SearchResult>> {
179 debug!(collection, limit, "InMemory: searching vectors");
180
181 validate_search(limit, filter.as_ref(), &self.limits)?;
182 self.limits.validate_dimensions(vector.len())?;
183
184 let collections = self.collections.lock();
185
186 let col = collections.get(collection).ok_or_else(|| {
187 AppError::new(
188 ErrorCode::NotFound,
189 format!("collection '{collection}' does not exist"),
190 )
191 })?;
192
193 if vector.len() != col.dimensions {
194 return Err(AppError::new(
195 ErrorCode::InvalidInput,
196 format!(
197 "vector dimensions mismatch: expected {}, got {}",
198 col.dimensions,
199 vector.len()
200 ),
201 ));
202 }
203
204 fn validate_search(
205 limit: usize,
206 filter: Option<&SearchFilter>,
207 limits: &VectorStoreLimits,
208 ) -> AppResult<()> {
209 limits.validate_search_limit(limit)?;
210 if let Some(filter) = filter {
211 filter.validate_limits(limits)?;
212 }
213 Ok(())
214 }
215
216 let mut scored: Vec<SearchResult> = col
217 .points
218 .iter()
219 .filter(|p| {
220 filter
221 .as_ref()
222 .is_none_or(|f| matches_filter(&p.payload, f))
223 })
224 .map(|p| SearchResult {
225 id: p.id.clone(),
226 score: similarity_score(col.metric, &vector, &p.vector),
227 payload: p.payload.clone(),
228 })
229 .collect();
230
231 scored.sort_by(|a, b| compare_score_desc(a.score, b.score));
232 scored.truncate(limit);
233
234 Ok(scored)
235 }
236
237 async fn delete(&self, collection: &str, id: &str) -> AppResult<()> {
238 debug!(collection, id, "InMemory: deleting vector point");
239
240 let mut collections = self.collections.lock();
241
242 let col = collections.get_mut(collection).ok_or_else(|| {
243 AppError::new(
244 ErrorCode::NotFound,
245 format!("collection '{collection}' does not exist"),
246 )
247 })?;
248
249 col.points.retain(|p| p.id != id);
250 Ok(())
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[tokio::test]
259 async fn test_ensure_collection_creates_new() {
260 let store = InMemoryVectorStore::new();
261 store.ensure_collection("test", 3).await.unwrap();
262 store.ensure_collection("test", 3).await.unwrap();
264 }
265
266 #[tokio::test]
267 async fn test_upsert_and_search() {
268 let store = InMemoryVectorStore::new();
269 store.ensure_collection("test", 3).await.unwrap();
270
271 let payload = PointPayload::new().with_field("name", "doc1");
272 store
273 .upsert("test", "1", vec![1.0, 0.0, 0.0], payload)
274 .await
275 .unwrap();
276
277 let payload = PointPayload::new().with_field("name", "doc2");
278 store
279 .upsert("test", "2", vec![0.0, 1.0, 0.0], payload)
280 .await
281 .unwrap();
282
283 let results = store
284 .search("test", vec![1.0, 0.0, 0.0], 10, None)
285 .await
286 .unwrap();
287
288 assert_eq!(results.len(), 2);
289 assert_eq!(results[0].id, "1");
290 assert!((results[0].score - 1.0).abs() < 1e-6);
291 }
292
293 #[tokio::test]
294 async fn configured_metric_is_used_for_new_collections() {
295 let store = InMemoryVectorStore::with_metric(SimilarityMetric::Dot);
296 store.ensure_collection("test", 2).await.unwrap();
297
298 store
299 .upsert("test", "long", vec![10.0, 0.0], PointPayload::new())
300 .await
301 .unwrap();
302 store
303 .upsert("test", "unit", vec![1.0, 0.0], PointPayload::new())
304 .await
305 .unwrap();
306
307 let results = store
308 .search("test", vec![1.0, 0.0], 10, None)
309 .await
310 .unwrap();
311
312 assert_eq!(results[0].id, "long");
313 assert_eq!(results[0].score, 10.0);
314 }
315
316 #[tokio::test]
317 async fn test_upsert_updates_existing() {
318 let store = InMemoryVectorStore::new();
319 store.ensure_collection("test", 2).await.unwrap();
320
321 let payload = PointPayload::new().with_field("v", "old");
322 store
323 .upsert("test", "1", vec![1.0, 0.0], payload)
324 .await
325 .unwrap();
326
327 let payload = PointPayload::new().with_field("v", "new");
328 store
329 .upsert("test", "1", vec![0.0, 1.0], payload)
330 .await
331 .unwrap();
332
333 let results = store
334 .search("test", vec![0.0, 1.0], 10, None)
335 .await
336 .unwrap();
337
338 assert_eq!(results.len(), 1);
339 assert_eq!(results[0].id, "1");
340 assert_eq!(
341 results[0].payload.fields.get("v").and_then(|v| v.as_str()),
342 Some("new")
343 );
344 }
345
346 #[tokio::test]
347 async fn test_search_with_filter() {
348 let store = InMemoryVectorStore::new();
349 store.ensure_collection("test", 2).await.unwrap();
350
351 store
352 .upsert(
353 "test",
354 "1",
355 vec![1.0, 0.0],
356 PointPayload::new().with_field("type", "a"),
357 )
358 .await
359 .unwrap();
360
361 store
362 .upsert(
363 "test",
364 "2",
365 vec![1.0, 0.0],
366 PointPayload::new().with_field("type", "b"),
367 )
368 .await
369 .unwrap();
370
371 let filter = SearchFilter::new().must_match("type", "a");
372 let results = store
373 .search("test", vec![1.0, 0.0], 10, Some(filter))
374 .await
375 .unwrap();
376
377 assert_eq!(results.len(), 1);
378 assert_eq!(results[0].id, "1");
379 }
380
381 #[tokio::test]
382 async fn test_delete() {
383 let store = InMemoryVectorStore::new();
384 store.ensure_collection("test", 2).await.unwrap();
385
386 store
387 .upsert("test", "1", vec![1.0, 0.0], PointPayload::new())
388 .await
389 .unwrap();
390
391 store.delete("test", "1").await.unwrap();
392
393 let results = store
394 .search("test", vec![1.0, 0.0], 10, None)
395 .await
396 .unwrap();
397 assert!(results.is_empty());
398 }
399
400 #[tokio::test]
401 async fn test_upsert_wrong_dimensions() {
402 let store = InMemoryVectorStore::new();
403 store.ensure_collection("test", 3).await.unwrap();
404
405 let result = store
406 .upsert("test", "1", vec![1.0, 0.0], PointPayload::new())
407 .await;
408
409 assert!(result.is_err());
410 }
411
412 #[tokio::test]
413 async fn test_search_wrong_dimensions_returns_invalid_input() {
414 let store = InMemoryVectorStore::new();
415 store.ensure_collection("test", 3).await.unwrap();
416
417 let err = store
418 .search("test", vec![1.0, 0.0], 10, None)
419 .await
420 .expect_err("dimension mismatch must fail");
421
422 assert_eq!(err.code(), ErrorCode::InvalidInput);
423 }
424
425 #[tokio::test]
426 async fn search_rejects_limit_above_configured_bound() {
427 let store = InMemoryVectorStore::with_options(
428 SimilarityMetric::Cosine,
429 VectorStoreLimits::new().with_max_search_limit(1),
430 );
431 store.ensure_collection("test", 2).await.unwrap();
432
433 let err = store
434 .search("test", vec![1.0, 0.0], 2, None)
435 .await
436 .expect_err("search limit above configured bound must fail");
437
438 assert_eq!(err.code(), ErrorCode::InvalidInput);
439 }
440
441 #[tokio::test]
442 async fn upsert_rejects_payload_above_configured_bound() {
443 let store = InMemoryVectorStore::with_options(
444 SimilarityMetric::Cosine,
445 VectorStoreLimits::new().with_max_payload_bytes(4),
446 );
447 store.ensure_collection("test", 2).await.unwrap();
448
449 let err = store
450 .upsert(
451 "test",
452 "1",
453 vec![1.0, 0.0],
454 PointPayload::new().with_field("name", "too-large"),
455 )
456 .await
457 .expect_err("payload above configured bound must fail");
458
459 assert_eq!(err.code(), ErrorCode::InvalidInput);
460 }
461
462 #[tokio::test]
463 async fn search_rejects_filter_above_configured_bound() {
464 let store = InMemoryVectorStore::with_options(
465 SimilarityMetric::Cosine,
466 VectorStoreLimits::new().with_max_payload_bytes(4),
467 );
468 store.ensure_collection("test", 2).await.unwrap();
469
470 let filter = SearchFilter::new().must_match("name", "too-large");
471 let err = store
472 .search("test", vec![1.0, 0.0], 1, Some(filter))
473 .await
474 .expect_err("filter above configured bound must fail");
475
476 assert_eq!(err.code(), ErrorCode::InvalidInput);
477 }
478
479 #[tokio::test]
480 async fn search_rejects_non_finite_filter_float() {
481 let store = InMemoryVectorStore::new();
482 store.ensure_collection("test", 2).await.unwrap();
483
484 let filter = SearchFilter::new().must_match("score", f64::NAN);
485 let err = store
486 .search("test", vec![1.0, 0.0], 1, Some(filter))
487 .await
488 .expect_err("non-finite filter float must fail");
489
490 assert_eq!(err.code(), ErrorCode::InvalidInput);
491 }
492
493 #[tokio::test]
494 async fn test_upsert_missing_collection() {
495 let store = InMemoryVectorStore::new();
496 let result = store
497 .upsert("nonexistent", "1", vec![1.0], PointPayload::new())
498 .await;
499
500 assert!(result.is_err());
501 }
502}