Skip to main content

oxirs_vec/
vector_store.rs

1//! Enhanced vector store with embedding management, advanced features, and persistence.
2
3use anyhow::Result;
4use parking_lot::Mutex;
5use std::collections::HashMap;
6
7use crate::embeddings;
8use crate::vector_index::{MemoryVectorIndex, VectorIndex};
9use crate::{BatchSearchResult, Vector, VectorId, VectorStoreTrait};
10
11/// Configuration for vector store
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13pub struct VectorStoreConfig {
14    pub auto_embed: bool,
15    pub cache_embeddings: bool,
16    pub similarity_threshold: f32,
17    pub max_results: usize,
18}
19
20impl Default for VectorStoreConfig {
21    fn default() -> Self {
22        Self {
23            auto_embed: true,
24            cache_embeddings: true,
25            similarity_threshold: 0.7,
26            max_results: 100,
27        }
28    }
29}
30
31/// Enhanced vector store with embedding management and advanced features
32pub struct VectorStore {
33    index: Box<dyn VectorIndex>,
34    /// Embedding manager guarded by a mutex for interior mutability: `get_embedding`
35    /// requires `&mut self` (it maintains an internal cache), but read-only search
36    /// APIs (`similarity_search`, `threshold_search`, `advanced_search`) only have
37    /// `&self`. Wrapping in a `Mutex` lets those paths compute *real* embeddings
38    /// instead of silently falling back to a hash-based pseudo-vector.
39    embedding_manager: Mutex<Option<embeddings::EmbeddingManager>>,
40    config: VectorStoreConfig,
41}
42
43impl VectorStore {
44    /// Create a new vector store with default memory index
45    pub fn new() -> Self {
46        Self {
47            index: Box::new(MemoryVectorIndex::new()),
48            embedding_manager: Mutex::new(None),
49            config: VectorStoreConfig::default(),
50        }
51    }
52
53    /// Create vector store with specific embedding strategy
54    pub fn with_embedding_strategy(strategy: embeddings::EmbeddingStrategy) -> Result<Self> {
55        let embedding_manager = embeddings::EmbeddingManager::new(strategy, 1000)?;
56
57        Ok(Self {
58            index: Box::new(MemoryVectorIndex::new()),
59            embedding_manager: Mutex::new(Some(embedding_manager)),
60            config: VectorStoreConfig::default(),
61        })
62    }
63
64    /// Create vector store with custom index
65    pub fn with_index(index: Box<dyn VectorIndex>) -> Self {
66        Self {
67            index,
68            embedding_manager: Mutex::new(None),
69            config: VectorStoreConfig::default(),
70        }
71    }
72
73    /// Create vector store with custom index and embedding strategy
74    pub fn with_index_and_embeddings(
75        index: Box<dyn VectorIndex>,
76        strategy: embeddings::EmbeddingStrategy,
77    ) -> Result<Self> {
78        let embedding_manager = embeddings::EmbeddingManager::new(strategy, 1000)?;
79
80        Ok(Self {
81            index,
82            embedding_manager: Mutex::new(Some(embedding_manager)),
83            config: VectorStoreConfig::default(),
84        })
85    }
86
87    /// Set vector store configuration
88    pub fn with_config(mut self, config: VectorStoreConfig) -> Self {
89        self.config = config;
90        self
91    }
92
93    /// Index a resource with automatic embedding generation
94    pub fn index_resource(&mut self, uri: String, content: &str) -> Result<()> {
95        let mut guard = self.embedding_manager.lock();
96        if let Some(ref mut embedding_manager) = *guard {
97            let embeddable_content = embeddings::EmbeddableContent::Text(content.to_string());
98            let vector = embedding_manager.get_embedding(&embeddable_content)?;
99            drop(guard);
100            self.index.insert(uri, vector)
101        } else {
102            drop(guard);
103            // No embedding manager configured: fall back to a hash-based vector.
104            tracing::warn!(
105                "index_resource: no embedding manager configured for '{}', using hash-based fallback vector",
106                uri
107            );
108            let vector = self.generate_fallback_vector(content);
109            self.index.insert(uri, vector)
110        }
111    }
112
113    /// Index an RDF resource with structured content
114    pub fn index_rdf_resource(
115        &mut self,
116        uri: String,
117        label: Option<String>,
118        description: Option<String>,
119        properties: std::collections::HashMap<String, Vec<String>>,
120    ) -> Result<()> {
121        let mut guard = self.embedding_manager.lock();
122        if let Some(ref mut embedding_manager) = *guard {
123            let embeddable_content = embeddings::EmbeddableContent::RdfResource {
124                uri: uri.clone(),
125                label,
126                description,
127                properties,
128            };
129            let vector = embedding_manager.get_embedding(&embeddable_content)?;
130            drop(guard);
131            self.index.insert(uri, vector)
132        } else {
133            Err(anyhow::anyhow!(
134                "Embedding manager required for RDF resource indexing"
135            ))
136        }
137    }
138
139    /// Index a pre-computed vector
140    pub fn index_vector(&mut self, uri: String, vector: Vector) -> Result<()> {
141        self.index.insert(uri, vector)
142    }
143
144    /// Compute a query vector for `text`, using the configured embedding manager
145    /// when present (real embedding, including cache reuse) and only falling
146    /// back to a hash-based pseudo-vector when no manager is configured at all.
147    fn compute_query_vector(&self, text: &str) -> Result<Vector> {
148        let mut guard = self.embedding_manager.lock();
149        if let Some(ref mut embedding_manager) = *guard {
150            let embeddable_content = embeddings::EmbeddableContent::Text(text.to_string());
151            embedding_manager.get_embedding(&embeddable_content)
152        } else {
153            drop(guard);
154            tracing::warn!(
155                "similarity search: no embedding manager configured, using hash-based fallback vector for query"
156            );
157            Ok(self.generate_fallback_vector(text))
158        }
159    }
160
161    /// Search for similar resources using text query
162    pub fn similarity_search(&self, query: &str, limit: usize) -> Result<Vec<(String, f32)>> {
163        let query_vector = self.compute_query_vector(query)?;
164        self.index.search_knn(&query_vector, limit)
165    }
166
167    /// Search for similar resources using a vector query
168    pub fn similarity_search_vector(
169        &self,
170        query: &Vector,
171        limit: usize,
172    ) -> Result<Vec<(String, f32)>> {
173        self.index.search_knn(query, limit)
174    }
175
176    /// Find resources within similarity threshold
177    pub fn threshold_search(&self, query: &str, threshold: f32) -> Result<Vec<(String, f32)>> {
178        let query_vector = self.compute_query_vector(query)?;
179        self.index.search_threshold(&query_vector, threshold)
180    }
181
182    /// Advanced search with multiple options
183    pub fn advanced_search(&self, options: SearchOptions) -> Result<Vec<(String, f32)>> {
184        let query_vector = match options.query {
185            SearchQuery::Text(text) => self.compute_query_vector(&text)?,
186            SearchQuery::Vector(vector) => vector,
187        };
188
189        let results = match options.search_type {
190            SearchType::KNN(k) => self.index.search_knn(&query_vector, k)?,
191            SearchType::Threshold(threshold) => {
192                self.index.search_threshold(&query_vector, threshold)?
193            }
194        };
195
196        Ok(results)
197    }
198
199    fn generate_fallback_vector(&self, text: &str) -> Vector {
200        // Simple hash-based vector generation for fallback
201        use std::collections::hash_map::DefaultHasher;
202        use std::hash::{Hash, Hasher};
203
204        let mut hasher = DefaultHasher::new();
205        text.hash(&mut hasher);
206        let hash = hasher.finish();
207
208        let mut values = Vec::with_capacity(384); // Standard embedding size
209        let mut seed = hash;
210
211        for _ in 0..384 {
212            seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
213            let normalized = (seed as f32) / (u64::MAX as f32);
214            values.push((normalized - 0.5) * 2.0); // Range: -1.0 to 1.0
215        }
216
217        Vector::new(values)
218    }
219
220    /// Get embedding manager statistics
221    pub fn embedding_stats(&self) -> Option<(usize, usize)> {
222        self.embedding_manager
223            .lock()
224            .as_ref()
225            .map(|em| em.cache_stats())
226    }
227
228    /// Build vocabulary for TF-IDF embeddings
229    pub fn build_vocabulary(&mut self, documents: &[String]) -> Result<()> {
230        if let Some(ref mut embedding_manager) = *self.embedding_manager.lock() {
231            embedding_manager.build_vocabulary(documents)
232        } else {
233            Ok(()) // No-op if no embedding manager
234        }
235    }
236
237    /// Calculate similarity between two resources by their URIs
238    pub fn calculate_similarity(&self, uri1: &str, uri2: &str) -> Result<f32> {
239        // If the URIs are identical, return perfect similarity
240        if uri1 == uri2 {
241            return Ok(1.0);
242        }
243
244        // Get the vectors for both URIs
245        let vector1 = self
246            .index
247            .get_vector(uri1)
248            .ok_or_else(|| anyhow::anyhow!("Vector not found for URI: {}", uri1))?;
249
250        let vector2 = self
251            .index
252            .get_vector(uri2)
253            .ok_or_else(|| anyhow::anyhow!("Vector not found for URI: {}", uri2))?;
254
255        // Calculate cosine similarity between the vectors
256        vector1.cosine_similarity(vector2)
257    }
258
259    /// Get a vector by its ID (delegates to VectorIndex)
260    pub fn get_vector(&self, id: &str) -> Option<&Vector> {
261        self.index.get_vector(id)
262    }
263
264    /// Iterate all (id, vector) pairs stored in the underlying index.
265    ///
266    /// Only index types that override [`VectorIndex::iter_vectors`]
267    /// (e.g. `MemoryVectorIndex`) return a non-empty list; other
268    /// implementations return an empty `Vec` by default.
269    pub fn iter_vectors(&self) -> Vec<(String, Vector)> {
270        self.index.iter_vectors()
271    }
272
273    /// Index a vector with metadata (stub)
274    pub fn index_vector_with_metadata(
275        &mut self,
276        uri: String,
277        vector: Vector,
278        _metadata: HashMap<String, String>,
279    ) -> Result<()> {
280        // For now, just delegate to index_vector, ignoring metadata
281        // Future: Extend VectorIndex trait to support metadata
282        self.index_vector(uri, vector)
283    }
284
285    /// Index a resource with metadata (stub)
286    pub fn index_resource_with_metadata(
287        &mut self,
288        uri: String,
289        content: &str,
290        _metadata: HashMap<String, String>,
291    ) -> Result<()> {
292        // For now, just delegate to index_resource, ignoring metadata
293        // Future: Store and utilize metadata
294        self.index_resource(uri, content)
295    }
296
297    /// Search with additional parameters (stub)
298    pub fn similarity_search_with_params(
299        &self,
300        query: &str,
301        limit: usize,
302        _params: HashMap<String, String>,
303    ) -> Result<Vec<(String, f32)>> {
304        // For now, just delegate to similarity_search, ignoring params
305        // Future: Use params for filtering, threshold, etc.
306        self.similarity_search(query, limit)
307    }
308
309    /// Vector search with additional parameters (stub)
310    pub fn vector_search_with_params(
311        &self,
312        query: &Vector,
313        limit: usize,
314        _params: HashMap<String, String>,
315    ) -> Result<Vec<(String, f32)>> {
316        // For now, just delegate to similarity_search_vector, ignoring params
317        // Future: Use params for filtering, distance metric selection, etc.
318        self.similarity_search_vector(query, limit)
319    }
320
321    /// Get all vector IDs (stub)
322    pub fn get_vector_ids(&self) -> Result<Vec<String>> {
323        // VectorIndex trait doesn't provide this method yet
324        // Future: Add to VectorIndex trait or track separately
325        Ok(Vec::new())
326    }
327
328    /// Remove a vector by its URI (stub)
329    pub fn remove_vector(&mut self, uri: &str) -> Result<()> {
330        // Delegate to VectorIndex trait's remove_vector method
331        self.index.remove_vector(uri.to_string())
332    }
333
334    /// Get store statistics (stub)
335    pub fn get_statistics(&self) -> Result<HashMap<String, String>> {
336        // Return basic statistics as a map
337        // Future: Provide comprehensive stats from index
338        let mut stats = HashMap::new();
339        stats.insert("type".to_string(), "VectorStore".to_string());
340
341        if let Some((cache_size, cache_capacity)) = self.embedding_stats() {
342            stats.insert("embedding_cache_size".to_string(), cache_size.to_string());
343            stats.insert(
344                "embedding_cache_capacity".to_string(),
345                cache_capacity.to_string(),
346            );
347        }
348
349        Ok(stats)
350    }
351
352    /// Save store to disk.
353    ///
354    /// Creates `{path}/metadata.json` (config + vector count) and
355    /// `{path}/vectors.json` (all `(id, Vector)` pairs).  The embedding
356    /// manager (in-memory cache only) is **not** persisted; call
357    /// `with_embedding_strategy` again after loading if needed.
358    ///
359    /// Only index types that report [`VectorIndex::supports_enumeration`] as
360    /// `true` (e.g. `MemoryVectorIndex`, `HnswIndex`, `IvfIndex`, `PQIndex`)
361    /// can be saved this way. Index types that cannot enumerate their vectors
362    /// cause this to return an error rather than silently persisting an
363    /// empty (and misleading) snapshot.
364    pub fn save_to_disk(&self, path: &str) -> Result<()> {
365        use anyhow::Context as _;
366
367        if !self.index.supports_enumeration() {
368            return Err(anyhow::anyhow!(
369                "save_to_disk: the configured index type does not support full \
370                 vector enumeration (VectorIndex::supports_enumeration() == false); \
371                 refusing to write a silently-empty vectors.json. Use an index type \
372                 that implements iter_vectors/supports_enumeration (e.g. \
373                 MemoryVectorIndex, HnswIndex, IvfIndex, PQIndex)."
374            ));
375        }
376
377        std::fs::create_dir_all(path)
378            .with_context(|| format!("Failed to create directory: {}", path))?;
379
380        // --- metadata ---
381        let vectors = self.index.iter_vectors();
382        let metadata = serde_json::json!({
383            "config": self.config,
384            "vector_count": vectors.len(),
385            "index_type": "memory",
386        });
387        let metadata_path = std::path::Path::new(path).join("metadata.json");
388        let metadata_str = serde_json::to_string_pretty(&metadata)
389            .with_context(|| "Failed to serialize VectorStore metadata")?;
390        std::fs::write(&metadata_path, metadata_str)
391            .with_context(|| format!("Failed to write {}", metadata_path.display()))?;
392
393        // --- vectors ---
394        let vectors_path = std::path::Path::new(path).join("vectors.json");
395        let vectors_str = serde_json::to_string_pretty(&vectors)
396            .with_context(|| "Failed to serialize VectorStore vectors")?;
397        std::fs::write(&vectors_path, vectors_str)
398            .with_context(|| format!("Failed to write {}", vectors_path.display()))?;
399
400        Ok(())
401    }
402
403    /// Load a store that was previously saved with [`VectorStore::save_to_disk`].
404    ///
405    /// Reconstructs a `VectorStore` backed by a fresh `MemoryVectorIndex` and
406    /// re-inserts all vectors from the saved snapshot.  The embedding manager
407    /// is not restored; create a new one with `with_embedding_strategy` if
408    /// needed.
409    pub fn load_from_disk(path: &str) -> Result<Self> {
410        use anyhow::Context as _;
411
412        // --- read metadata ---
413        let metadata_path = std::path::Path::new(path).join("metadata.json");
414        let metadata_str = std::fs::read_to_string(&metadata_path)
415            .with_context(|| format!("Failed to read {}", metadata_path.display()))?;
416        let metadata: serde_json::Value = serde_json::from_str(&metadata_str)
417            .with_context(|| "Failed to parse VectorStore metadata")?;
418
419        let config: VectorStoreConfig = serde_json::from_value(metadata["config"].clone())
420            .with_context(|| "Failed to deserialize VectorStoreConfig from metadata")?;
421
422        // --- read vectors ---
423        let vectors_path = std::path::Path::new(path).join("vectors.json");
424        let vectors_str = std::fs::read_to_string(&vectors_path)
425            .with_context(|| format!("Failed to read {}", vectors_path.display()))?;
426        let entries: Vec<(String, Vector)> = serde_json::from_str(&vectors_str)
427            .with_context(|| "Failed to deserialize VectorStore vectors")?;
428
429        // --- reconstruct ---
430        let mut store = Self {
431            index: Box::new(MemoryVectorIndex::new()),
432            embedding_manager: Mutex::new(None),
433            config,
434        };
435
436        for (id, vector) in entries {
437            store
438                .index
439                .insert(id.clone(), vector)
440                .with_context(|| format!("Failed to re-insert vector '{}'", id))?;
441        }
442
443        Ok(store)
444    }
445
446    /// Optimize the underlying index (stub)
447    pub fn optimize_index(&mut self) -> Result<()> {
448        // Stub implementation - optimization not yet implemented
449        // Future: Trigger index compaction, rebalancing, etc.
450        Ok(())
451    }
452}
453
454impl Default for VectorStore {
455    fn default() -> Self {
456        Self::new()
457    }
458}
459
460impl VectorStoreTrait for VectorStore {
461    fn insert_vector(&mut self, id: VectorId, vector: Vector) -> Result<()> {
462        self.index.insert(id, vector)
463    }
464
465    fn add_vector(&mut self, vector: Vector) -> Result<VectorId> {
466        // Generate a unique ID for the vector
467        let id = format!("vec_{}", uuid::Uuid::new_v4());
468        self.index.insert(id.clone(), vector)?;
469        Ok(id)
470    }
471
472    fn get_vector(&self, id: &VectorId) -> Result<Option<Vector>> {
473        Ok(self.index.get_vector(id).cloned())
474    }
475
476    fn get_all_vector_ids(&self) -> Result<Vec<VectorId>> {
477        // Delegates to the same iter_vectors() enumeration used by
478        // save_to_disk/VectorStore::iter_vectors, so this only returns real
479        // IDs for index types that support enumeration; other index types
480        // return an empty list (consistent with `len()` below).
481        Ok(self
482            .index
483            .iter_vectors()
484            .into_iter()
485            .map(|(id, _)| id)
486            .collect())
487    }
488
489    fn search_similar(&self, query: &Vector, k: usize) -> Result<Vec<(VectorId, f32)>> {
490        self.index.search_knn(query, k)
491    }
492
493    fn remove_vector(&mut self, id: &VectorId) -> Result<bool> {
494        // Delegate to the underlying VectorIndex::remove_vector, matching the
495        // crate's own inherent `VectorStore::remove_vector` method.
496        match self.index.remove_vector(id.clone()) {
497            Ok(()) => Ok(true),
498            Err(_) => Ok(false),
499        }
500    }
501
502    fn len(&self) -> usize {
503        // Consistent with get_all_vector_ids(): counts vectors via the same
504        // enumeration path so both agree for enumerable index types.
505        self.index.iter_vectors().len()
506    }
507}
508
509/// Search query types
510#[derive(Debug, Clone)]
511pub enum SearchQuery {
512    Text(String),
513    Vector(Vector),
514}
515
516/// Search operation types
517#[derive(Debug, Clone)]
518pub enum SearchType {
519    KNN(usize),
520    Threshold(f32),
521}
522
523/// Advanced search options
524#[derive(Debug, Clone)]
525pub struct SearchOptions {
526    pub query: SearchQuery,
527    pub search_type: SearchType,
528}
529
530/// Vector operation results with enhanced metadata
531#[derive(Debug, Clone)]
532pub struct VectorOperationResult {
533    pub uri: String,
534    pub similarity: f32,
535    pub vector: Option<Vector>,
536    pub metadata: Option<std::collections::HashMap<String, String>>,
537    pub rank: usize,
538}
539
540/// Document batch processing utilities
541pub struct DocumentBatchProcessor;
542
543impl DocumentBatchProcessor {
544    /// Process multiple documents in batch for efficient indexing
545    pub fn batch_index(
546        store: &mut VectorStore,
547        documents: &[(String, String)], // (uri, content) pairs
548    ) -> Result<Vec<Result<()>>> {
549        let mut results = Vec::new();
550
551        for (uri, content) in documents {
552            let result = store.index_resource(uri.clone(), content);
553            results.push(result);
554        }
555
556        Ok(results)
557    }
558
559    /// Process multiple queries in batch
560    pub fn batch_search(
561        store: &VectorStore,
562        queries: &[String],
563        limit: usize,
564    ) -> Result<BatchSearchResult> {
565        let mut results = Vec::new();
566
567        for query in queries {
568            let result = store.similarity_search(query, limit);
569            results.push(result);
570        }
571
572        Ok(results)
573    }
574}