manifoldb_vector/index/
manager.rs

1//! HNSW index manager for named vector system.
2//!
3//! This module provides the `HnswIndexManager` which coordinates HNSW index
4//! lifecycle with the collection system. It handles:
5//!
6//! - Creating and dropping indexes for named vectors
7//! - Updating indexes on point insert/update/delete
8//! - Index recovery and rebuild
9
10use std::collections::HashMap;
11use std::marker::PhantomData;
12use std::sync::{Arc, RwLock};
13
14use manifoldb_core::PointId;
15use manifoldb_storage::{StorageEngine, Transaction};
16
17use crate::distance::DistanceMetric;
18use crate::error::VectorError;
19use crate::types::{CollectionName, Embedding, NamedVector};
20
21use super::config::HnswConfig;
22use super::hnsw::HnswIndex;
23use super::registry::{HnswIndexEntry, HnswRegistry};
24use super::traits::VectorIndex;
25
26/// Manages HNSW indexes for the named vector system.
27///
28/// The `HnswIndexManager` provides a unified interface for creating, updating,
29/// and querying HNSW indexes that are associated with named vectors in collections.
30///
31/// # Type Parameters
32///
33/// * `E` - The storage engine type. The engine must be clonable so it can be
34///   shared across multiple HNSW indexes.
35pub struct HnswIndexManager<E: StorageEngine> {
36    /// In-memory cache of loaded HNSW indexes.
37    /// Key is the index name (e.g., "documents_embedding_hnsw").
38    indexes: RwLock<HashMap<String, Arc<RwLock<HnswIndex<E>>>>>,
39    /// Marker for the storage engine type.
40    _phantom: PhantomData<E>,
41}
42
43impl<E: StorageEngine> HnswIndexManager<E> {
44    /// Create a new empty index manager.
45    ///
46    /// The manager doesn't own the storage engine. Instead, each operation
47    /// that requires storage access takes the engine as a parameter.
48    #[must_use]
49    pub fn new() -> Self {
50        Self { indexes: RwLock::new(HashMap::new()), _phantom: PhantomData }
51    }
52
53    // ========================================================================
54    // Index Lifecycle
55    // ========================================================================
56
57    /// Create an HNSW index for a specific named vector.
58    ///
59    /// # Arguments
60    ///
61    /// * `engine` - The storage engine to use
62    /// * `collection` - The collection name
63    /// * `vector_name` - The vector name
64    /// * `dimension` - Vector dimension
65    /// * `distance_metric` - Distance metric to use
66    /// * `config` - HNSW configuration
67    ///
68    /// # Returns
69    ///
70    /// The name of the created index.
71    pub fn create_index_for_vector(
72        &self,
73        engine: E,
74        collection: &CollectionName,
75        vector_name: &str,
76        dimension: usize,
77        distance_metric: DistanceMetric,
78        config: &HnswConfig,
79    ) -> Result<String, VectorError> {
80        let collection_str = collection.as_str();
81        let index_name = HnswRegistry::index_name_for_vector(collection_str, vector_name);
82
83        // Check if index already exists
84        {
85            let tx = engine.begin_read()?;
86            if HnswRegistry::exists(&tx, &index_name)? {
87                return Err(VectorError::InvalidName(format!(
88                    "index '{}' already exists",
89                    index_name
90                )));
91            }
92        }
93
94        // Register in the registry first
95        let entry = HnswIndexEntry::for_named_vector(
96            collection_str,
97            vector_name,
98            dimension,
99            distance_metric,
100            config,
101        );
102
103        {
104            let mut tx = engine.begin_write()?;
105            HnswRegistry::register(&mut tx, &entry)?;
106            tx.commit()?;
107        }
108
109        // Create the HNSW index
110        let hnsw = HnswIndex::new(engine, &index_name, dimension, distance_metric, config.clone())?;
111
112        // Cache the index
113        {
114            let mut indexes = self.indexes.write().map_err(|_| VectorError::LockPoisoned)?;
115            indexes.insert(index_name.clone(), Arc::new(RwLock::new(hnsw)));
116        }
117
118        Ok(index_name)
119    }
120
121    /// Drop all HNSW indexes for a collection.
122    ///
123    /// This is called when a collection is deleted.
124    ///
125    /// Note: This method takes the engine as a parameter for registry operations
126    /// but cannot actually delete the index data since the indexes own their engines.
127    pub fn drop_indexes_for_collection(
128        &self,
129        engine: &E,
130        collection: &CollectionName,
131    ) -> Result<Vec<String>, VectorError> {
132        let collection_str = collection.as_str();
133        let mut dropped = Vec::new();
134
135        // Get all indexes for this collection from the registry
136        let entries = {
137            let tx = engine.begin_read()?;
138            HnswRegistry::list_for_collection(&tx, collection_str)?
139        };
140
141        // Drop each index from registry and cache
142        for entry in entries {
143            // Remove from cache
144            {
145                let mut indexes = self.indexes.write().map_err(|_| VectorError::LockPoisoned)?;
146                indexes.remove(&entry.name);
147            }
148
149            // Remove from registry
150            {
151                let mut tx = engine.begin_write()?;
152                HnswRegistry::drop(&mut tx, &entry.name)?;
153                // Note: Index data cleanup should be done by the index itself when dropped
154                tx.commit()?;
155            }
156
157            dropped.push(entry.name);
158        }
159
160        Ok(dropped)
161    }
162
163    /// Drop a specific HNSW index by name.
164    pub fn drop_index(&self, engine: &E, index_name: &str) -> Result<bool, VectorError> {
165        // Remove from cache
166        {
167            let mut indexes = self.indexes.write().map_err(|_| VectorError::LockPoisoned)?;
168            indexes.remove(index_name);
169        }
170
171        // Remove from registry
172        let mut tx = engine.begin_write()?;
173        let existed = HnswRegistry::drop(&mut tx, index_name)?;
174        if existed {
175            // Clear the index data from storage
176            super::persistence::clear_index_tx(
177                &mut tx,
178                &super::persistence::table_name(index_name),
179            )?;
180        }
181        tx.commit()?;
182
183        Ok(existed)
184    }
185
186    // ========================================================================
187    // Index Updates
188    // ========================================================================
189
190    /// Update HNSW indexes when a point is inserted or updated.
191    ///
192    /// This inserts/updates the point's vectors in all relevant HNSW indexes.
193    ///
194    /// # Arguments
195    ///
196    /// * `collection` - The collection name
197    /// * `point_id` - The point ID
198    /// * `vectors` - Map of vector names to their values
199    pub fn on_point_upsert(
200        &self,
201        collection: &CollectionName,
202        point_id: PointId,
203        vectors: &HashMap<String, NamedVector>,
204    ) -> Result<(), VectorError> {
205        let collection_str = collection.as_str();
206
207        for (vector_name, vector) in vectors {
208            // Only handle dense vectors for HNSW
209            if let NamedVector::Dense(data) = vector {
210                // Check if there's an index for this vector in the cache
211                let index_name = HnswRegistry::index_name_for_vector(collection_str, vector_name);
212
213                let indexes = self.indexes.read().map_err(|_| VectorError::LockPoisoned)?;
214                if let Some(index) = indexes.get(&index_name) {
215                    // Convert to embedding and insert
216                    let embedding = Embedding::new(data.clone())?;
217                    // Use point_id as entity_id (they're both u64 wrappers)
218                    let entity_id = manifoldb_core::EntityId::new(point_id.as_u64());
219
220                    let mut index_guard = index.write().map_err(|_| VectorError::LockPoisoned)?;
221                    index_guard.insert(entity_id, &embedding)?;
222                }
223            }
224        }
225
226        Ok(())
227    }
228
229    /// Update HNSW indexes when a specific vector is updated.
230    pub fn on_vector_update(
231        &self,
232        collection: &CollectionName,
233        point_id: PointId,
234        vector_name: &str,
235        vector: &NamedVector,
236    ) -> Result<(), VectorError> {
237        // Only handle dense vectors for HNSW
238        if let NamedVector::Dense(data) = vector {
239            let collection_str = collection.as_str();
240            let index_name = HnswRegistry::index_name_for_vector(collection_str, vector_name);
241
242            let indexes = self.indexes.read().map_err(|_| VectorError::LockPoisoned)?;
243            if let Some(index) = indexes.get(&index_name) {
244                let embedding = Embedding::new(data.clone())?;
245                let entity_id = manifoldb_core::EntityId::new(point_id.as_u64());
246
247                let mut index_guard = index.write().map_err(|_| VectorError::LockPoisoned)?;
248                index_guard.insert(entity_id, &embedding)?;
249            }
250        }
251
252        Ok(())
253    }
254
255    /// Update HNSW indexes when a point is deleted.
256    ///
257    /// This removes the point from all HNSW indexes in the collection that are cached.
258    pub fn on_point_delete(
259        &self,
260        collection: &CollectionName,
261        point_id: PointId,
262    ) -> Result<(), VectorError> {
263        let collection_str = collection.as_str();
264        let entity_id = manifoldb_core::EntityId::new(point_id.as_u64());
265
266        let indexes = self.indexes.read().map_err(|_| VectorError::LockPoisoned)?;
267
268        // Delete from each cached index for this collection
269        for (name, index) in indexes.iter() {
270            if name.starts_with(&format!("{}_", collection_str)) && name.ends_with("_hnsw") {
271                let mut index_guard = index.write().map_err(|_| VectorError::LockPoisoned)?;
272                let _ = index_guard.delete(entity_id)?;
273            }
274        }
275
276        Ok(())
277    }
278
279    /// Update HNSW indexes when a specific vector is deleted from a point.
280    pub fn on_vector_delete(
281        &self,
282        collection: &CollectionName,
283        point_id: PointId,
284        vector_name: &str,
285    ) -> Result<bool, VectorError> {
286        let collection_str = collection.as_str();
287        let index_name = HnswRegistry::index_name_for_vector(collection_str, vector_name);
288        let entity_id = manifoldb_core::EntityId::new(point_id.as_u64());
289
290        let indexes = self.indexes.read().map_err(|_| VectorError::LockPoisoned)?;
291        if let Some(index) = indexes.get(&index_name) {
292            let mut index_guard = index.write().map_err(|_| VectorError::LockPoisoned)?;
293            return index_guard.delete(entity_id);
294        }
295
296        Ok(false)
297    }
298
299    // ========================================================================
300    // Index Access
301    // ========================================================================
302
303    /// Get an HNSW index for a specific collection and vector name from the cache.
304    ///
305    /// Returns `None` if no index is loaded for this vector.
306    pub fn get_index(
307        &self,
308        collection: &str,
309        vector_name: &str,
310    ) -> Result<Option<Arc<RwLock<HnswIndex<E>>>>, VectorError> {
311        let index_name = HnswRegistry::index_name_for_vector(collection, vector_name);
312        let indexes = self.indexes.read().map_err(|_| VectorError::LockPoisoned)?;
313        Ok(indexes.get(&index_name).cloned())
314    }
315
316    /// Get an HNSW index by name from the cache.
317    pub fn get_index_by_name(
318        &self,
319        index_name: &str,
320    ) -> Result<Option<Arc<RwLock<HnswIndex<E>>>>, VectorError> {
321        let indexes = self.indexes.read().map_err(|_| VectorError::LockPoisoned)?;
322        Ok(indexes.get(index_name).cloned())
323    }
324
325    /// Load an existing HNSW index from storage into the cache.
326    ///
327    /// This is used after a restart to reload indexes that were registered.
328    pub fn load_index(&self, engine: E, index_name: &str) -> Result<(), VectorError> {
329        // Check if already loaded
330        {
331            let indexes = self.indexes.read().map_err(|_| VectorError::LockPoisoned)?;
332            if indexes.contains_key(index_name) {
333                return Ok(());
334            }
335        }
336
337        // Load the index
338        let hnsw = HnswIndex::open(engine, index_name)?;
339
340        // Cache it
341        {
342            let mut indexes = self.indexes.write().map_err(|_| VectorError::LockPoisoned)?;
343            indexes.insert(index_name.to_string(), Arc::new(RwLock::new(hnsw)));
344        }
345
346        Ok(())
347    }
348
349    /// Check if an index exists in the registry.
350    pub fn has_index(
351        &self,
352        engine: &E,
353        collection: &str,
354        vector_name: &str,
355    ) -> Result<bool, VectorError> {
356        let tx = engine.begin_read()?;
357        HnswRegistry::exists_for_named_vector(&tx, collection, vector_name)
358    }
359
360    /// Check if an index is loaded in the cache.
361    pub fn is_index_loaded(
362        &self,
363        collection: &str,
364        vector_name: &str,
365    ) -> Result<bool, VectorError> {
366        let index_name = HnswRegistry::index_name_for_vector(collection, vector_name);
367        let indexes = self.indexes.read().map_err(|_| VectorError::LockPoisoned)?;
368        Ok(indexes.contains_key(&index_name))
369    }
370
371    /// List all index entries for a collection from the registry.
372    pub fn list_indexes(
373        &self,
374        engine: &E,
375        collection: &str,
376    ) -> Result<Vec<HnswIndexEntry>, VectorError> {
377        let tx = engine.begin_read()?;
378        HnswRegistry::list_for_collection(&tx, collection)
379    }
380
381    // ========================================================================
382    // Index Recovery
383    // ========================================================================
384
385    /// Rebuild an HNSW index from the point store data.
386    ///
387    /// This is used for crash recovery or when an index needs to be rebuilt.
388    ///
389    /// # Arguments
390    ///
391    /// * `collection` - The collection name
392    /// * `vector_name` - The vector name
393    /// * `points` - Iterator of (point_id, embedding) pairs
394    pub fn rebuild_index<I>(
395        &self,
396        collection: &str,
397        vector_name: &str,
398        points: I,
399    ) -> Result<usize, VectorError>
400    where
401        I: IntoIterator<Item = (PointId, Vec<f32>)>,
402    {
403        let index_name = HnswRegistry::index_name_for_vector(collection, vector_name);
404
405        // Get the index from cache
406        let indexes = self.indexes.read().map_err(|_| VectorError::LockPoisoned)?;
407        let index = indexes.get(&index_name).ok_or_else(|| {
408            VectorError::SpaceNotFound(format!("index '{}' not found in cache", index_name))
409        })?;
410
411        let mut index_guard = index.write().map_err(|_| VectorError::LockPoisoned)?;
412
413        // Collect points for batch insert
414        let embeddings: Vec<(manifoldb_core::EntityId, Embedding)> = points
415            .into_iter()
416            .map(|(pid, data)| {
417                let entity_id = manifoldb_core::EntityId::new(pid.as_u64());
418                let embedding = Embedding::new(data)?;
419                Ok((entity_id, embedding))
420            })
421            .collect::<Result<Vec<_>, VectorError>>()?;
422
423        let count = embeddings.len();
424
425        // Batch insert all embeddings
426        let refs: Vec<(manifoldb_core::EntityId, &Embedding)> =
427            embeddings.iter().map(|(id, emb)| (*id, emb)).collect();
428
429        index_guard.insert_batch(&refs)?;
430        index_guard.flush()?;
431
432        Ok(count)
433    }
434
435    /// Load all registered indexes for a collection on startup.
436    ///
437    /// This is called during database recovery to restore HNSW indexes
438    /// from persisted storage. For each registered index, it attempts to
439    /// open the index from storage and load it into the cache.
440    ///
441    /// # Arguments
442    ///
443    /// * `engine_factory` - A closure that creates a new engine instance for each index
444    /// * `collection` - The collection name to load indexes for
445    ///
446    /// # Returns
447    ///
448    /// A vector of (index_name, result) pairs indicating success or failure for each index.
449    pub fn load_indexes_for_collection<F>(
450        &self,
451        engine: &E,
452        engine_factory: F,
453        collection: &str,
454    ) -> Result<Vec<(String, Result<(), VectorError>)>, VectorError>
455    where
456        F: Fn() -> Result<E, VectorError>,
457    {
458        // Get all registered indexes for this collection
459        let entries = {
460            let tx = engine.begin_read()?;
461            HnswRegistry::list_for_collection(&tx, collection)?
462        };
463
464        let mut results = Vec::with_capacity(entries.len());
465
466        for entry in entries {
467            let index_name = entry.name.clone();
468
469            // Try to load each index
470            let load_result = (|| -> Result<(), VectorError> {
471                // Check if already loaded
472                {
473                    let indexes = self.indexes.read().map_err(|_| VectorError::LockPoisoned)?;
474                    if indexes.contains_key(&index_name) {
475                        return Ok(());
476                    }
477                }
478
479                // Create a new engine instance for this index
480                let new_engine = engine_factory()?;
481
482                // Try to open the index
483                let hnsw = HnswIndex::open(new_engine, &index_name)?;
484
485                // Cache it
486                {
487                    let mut indexes =
488                        self.indexes.write().map_err(|_| VectorError::LockPoisoned)?;
489                    indexes.insert(index_name.clone(), Arc::new(RwLock::new(hnsw)));
490                }
491
492                Ok(())
493            })();
494
495            results.push((index_name, load_result));
496        }
497
498        Ok(results)
499    }
500
501    /// Verify and repair an index if needed.
502    ///
503    /// This checks if the index data is consistent with the stored point data.
504    /// If inconsistencies are found, the index can be rebuilt.
505    ///
506    /// # Arguments
507    ///
508    /// * `collection` - The collection name
509    /// * `vector_name` - The vector name
510    /// * `expected_point_count` - The expected number of points in the index
511    ///
512    /// # Returns
513    ///
514    /// A `RecoveryStatus` indicating whether the index is valid or needs repair.
515    pub fn verify_index(
516        &self,
517        collection: &str,
518        vector_name: &str,
519        expected_point_count: usize,
520    ) -> Result<RecoveryStatus, VectorError> {
521        let index_name = HnswRegistry::index_name_for_vector(collection, vector_name);
522
523        let indexes = self.indexes.read().map_err(|_| VectorError::LockPoisoned)?;
524        let index = match indexes.get(&index_name) {
525            Some(idx) => idx,
526            None => return Ok(RecoveryStatus::NotLoaded),
527        };
528
529        let guard = index.read().map_err(|_| VectorError::LockPoisoned)?;
530        let actual_count = guard.len()?;
531
532        if actual_count == expected_point_count {
533            Ok(RecoveryStatus::Valid)
534        } else {
535            Ok(RecoveryStatus::NeedsRebuild {
536                expected: expected_point_count,
537                actual: actual_count,
538            })
539        }
540    }
541
542    /// Clear and rebuild an index from scratch.
543    ///
544    /// This creates a fresh index with the same configuration and inserts
545    /// all provided points. Used when verification detects inconsistencies.
546    ///
547    /// # Arguments
548    ///
549    /// * `engine` - The storage engine
550    /// * `collection` - The collection name
551    /// * `vector_name` - The vector name
552    /// * `points` - Iterator of (point_id, embedding) pairs
553    ///
554    /// # Returns
555    ///
556    /// The number of points inserted into the rebuilt index.
557    pub fn rebuild_index_from_scratch<I>(
558        &self,
559        engine: E,
560        collection: &str,
561        vector_name: &str,
562        points: I,
563    ) -> Result<usize, VectorError>
564    where
565        I: IntoIterator<Item = (PointId, Vec<f32>)>,
566    {
567        let index_name = HnswRegistry::index_name_for_vector(collection, vector_name);
568
569        // Get the entry from registry to get configuration
570        let entry = {
571            let tx = engine.begin_read()?;
572            HnswRegistry::get(&tx, &index_name)?.ok_or_else(|| {
573                VectorError::SpaceNotFound(format!("index '{}' not in registry", index_name))
574            })?
575        };
576
577        // Remove old index from cache if present
578        {
579            let mut indexes = self.indexes.write().map_err(|_| VectorError::LockPoisoned)?;
580            indexes.remove(&index_name);
581        }
582
583        // Clear old index data
584        {
585            let mut tx = engine.begin_write()?;
586            super::persistence::clear_index_tx(
587                &mut tx,
588                &super::persistence::table_name(&index_name),
589            )?;
590            tx.commit()?;
591        }
592
593        // Create a fresh index with the same config
594        let config = entry.config();
595        let distance_metric = entry.distance_metric.into();
596        let hnsw = HnswIndex::new(engine, &index_name, entry.dimension, distance_metric, config)?;
597
598        // Collect and insert points
599        let embeddings: Vec<(manifoldb_core::EntityId, Embedding)> = points
600            .into_iter()
601            .map(|(pid, data)| {
602                let entity_id = manifoldb_core::EntityId::new(pid.as_u64());
603                let embedding = Embedding::new(data)?;
604                Ok((entity_id, embedding))
605            })
606            .collect::<Result<Vec<_>, VectorError>>()?;
607
608        let count = embeddings.len();
609
610        if embeddings.is_empty() {
611            // Cache empty index
612            {
613                let mut indexes = self.indexes.write().map_err(|_| VectorError::LockPoisoned)?;
614                indexes.insert(index_name, Arc::new(RwLock::new(hnsw)));
615            }
616        } else {
617            let refs: Vec<(manifoldb_core::EntityId, &Embedding)> =
618                embeddings.iter().map(|(id, emb)| (*id, emb)).collect();
619            let mut hnsw_guard = hnsw;
620            hnsw_guard.insert_batch(&refs)?;
621            hnsw_guard.flush()?;
622
623            // Cache the rebuilt index
624            {
625                let mut indexes = self.indexes.write().map_err(|_| VectorError::LockPoisoned)?;
626                indexes.insert(index_name, Arc::new(RwLock::new(hnsw_guard)));
627            }
628        }
629
630        Ok(count)
631    }
632}
633
634/// Status of index recovery/verification.
635#[derive(Debug, Clone, PartialEq, Eq)]
636pub enum RecoveryStatus {
637    /// Index is valid and consistent.
638    Valid,
639    /// Index is not loaded in the cache.
640    NotLoaded,
641    /// Index needs to be rebuilt due to inconsistencies.
642    NeedsRebuild {
643        /// Expected number of points.
644        expected: usize,
645        /// Actual number of points in the index.
646        actual: usize,
647    },
648}
649
650impl<E: StorageEngine> Default for HnswIndexManager<E> {
651    fn default() -> Self {
652        Self::new()
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659    use manifoldb_storage::backends::RedbEngine;
660
661    fn create_test_manager() -> HnswIndexManager<RedbEngine> {
662        HnswIndexManager::new()
663    }
664
665    #[test]
666    fn test_create_index_for_vector() {
667        let manager = create_test_manager();
668        let engine = RedbEngine::in_memory().unwrap();
669        let collection = CollectionName::new("documents").unwrap();
670
671        let index_name = manager
672            .create_index_for_vector(
673                engine,
674                &collection,
675                "embedding",
676                384,
677                DistanceMetric::Cosine,
678                &HnswConfig::default(),
679            )
680            .unwrap();
681
682        assert_eq!(index_name, "documents_embedding_hnsw");
683
684        // Check it's in the cache
685        assert!(manager.is_index_loaded("documents", "embedding").unwrap());
686    }
687
688    #[test]
689    fn test_point_upsert_and_delete() {
690        let manager = create_test_manager();
691        let engine = RedbEngine::in_memory().unwrap();
692        let collection = CollectionName::new("documents").unwrap();
693
694        // Create an index
695        manager
696            .create_index_for_vector(
697                engine,
698                &collection,
699                "embedding",
700                4,
701                DistanceMetric::Euclidean,
702                &HnswConfig::default(),
703            )
704            .unwrap();
705
706        // Insert a point
707        let point_id = PointId::new(1);
708        let mut vectors = HashMap::new();
709        vectors.insert("embedding".to_string(), NamedVector::Dense(vec![1.0, 2.0, 3.0, 4.0]));
710
711        manager.on_point_upsert(&collection, point_id, &vectors).unwrap();
712
713        // Verify it's in the index
714        let index = manager.get_index("documents", "embedding").unwrap().unwrap();
715        let guard = index.read().unwrap();
716        assert!(guard.contains(manifoldb_core::EntityId::new(1)).unwrap());
717        drop(guard);
718
719        // Delete the point
720        manager.on_point_delete(&collection, point_id).unwrap();
721
722        // Verify it's gone
723        let guard = index.read().unwrap();
724        assert!(!guard.contains(manifoldb_core::EntityId::new(1)).unwrap());
725    }
726
727    #[test]
728    fn test_vector_update() {
729        let manager = create_test_manager();
730        let engine = RedbEngine::in_memory().unwrap();
731        let collection = CollectionName::new("documents").unwrap();
732
733        manager
734            .create_index_for_vector(
735                engine,
736                &collection,
737                "embedding",
738                4,
739                DistanceMetric::Euclidean,
740                &HnswConfig::default(),
741            )
742            .unwrap();
743
744        let point_id = PointId::new(1);
745
746        // Insert initial vector
747        let vector = NamedVector::Dense(vec![1.0, 2.0, 3.0, 4.0]);
748        manager.on_vector_update(&collection, point_id, "embedding", &vector).unwrap();
749
750        // Update to new vector
751        let new_vector = NamedVector::Dense(vec![5.0, 6.0, 7.0, 8.0]);
752        manager.on_vector_update(&collection, point_id, "embedding", &new_vector).unwrap();
753
754        // Point should still be in index (same entity, updated embedding)
755        let index = manager.get_index("documents", "embedding").unwrap().unwrap();
756        let guard = index.read().unwrap();
757        assert!(guard.contains(manifoldb_core::EntityId::new(1)).unwrap());
758        assert_eq!(guard.len().unwrap(), 1);
759    }
760
761    #[test]
762    fn test_sparse_vector_ignored() {
763        let manager = create_test_manager();
764        let engine = RedbEngine::in_memory().unwrap();
765        let collection = CollectionName::new("documents").unwrap();
766
767        // Create a dense index
768        manager
769            .create_index_for_vector(
770                engine,
771                &collection,
772                "embedding",
773                4,
774                DistanceMetric::Euclidean,
775                &HnswConfig::default(),
776            )
777            .unwrap();
778
779        // Try to upsert with sparse vector - should be silently ignored
780        let point_id = PointId::new(1);
781        let mut vectors = HashMap::new();
782        vectors.insert("sparse_vec".to_string(), NamedVector::Sparse(vec![(0, 1.0), (5, 0.5)]));
783
784        // This should succeed (sparse vectors are ignored for HNSW)
785        manager.on_point_upsert(&collection, point_id, &vectors).unwrap();
786    }
787
788    #[test]
789    fn test_get_index() {
790        let manager = create_test_manager();
791        let engine = RedbEngine::in_memory().unwrap();
792        let collection = CollectionName::new("test").unwrap();
793
794        // No index yet
795        assert!(manager.get_index("test", "v1").unwrap().is_none());
796
797        // Create index
798        manager
799            .create_index_for_vector(
800                engine,
801                &collection,
802                "v1",
803                64,
804                DistanceMetric::Cosine,
805                &HnswConfig::default(),
806            )
807            .unwrap();
808
809        // Now it should exist
810        let index = manager.get_index("test", "v1").unwrap();
811        assert!(index.is_some());
812    }
813}