Skip to main content

grafeo_core/index/vector/
accessor.rs

1//! Vector accessor trait for reading vectors by node ID.
2//!
3//! This module provides the [`VectorAccessor`] trait, which decouples vector
4//! storage from vector indexing. In the current architecture, HNSW stores a
5//! copy of each vector internally. In a future version (0.5.0), HNSW will
6//! become topology-only (neighbor lists) and read vectors through this trait
7//! from [`PropertyStorage`] — the single source of truth — halving memory
8//! usage for vector workloads.
9//!
10//! # Example
11//!
12//! ```ignore
13//! use grafeo_core::index::vector::VectorAccessor;
14//! use grafeo_common::types::NodeId;
15//! use std::sync::Arc;
16//!
17//! // Closure-based accessor for tests
18//! let accessor = |id: NodeId| -> Option<Arc<[f32]>> {
19//!     Some(vec![1.0, 2.0, 3.0].into())
20//! };
21//! assert!(accessor.get_vector(NodeId::new(1)).is_some());
22//! ```
23
24use std::sync::Arc;
25
26use grafeo_common::types::{NodeId, PropertyKey, Value};
27
28use crate::graph::lpg::LpgStore;
29
30/// Trait for reading vectors by node ID.
31///
32/// Enables HNSW to be topology-only in the future — vectors live in
33/// property storage, not in HNSW nodes.
34pub trait VectorAccessor: Send + Sync {
35    /// Returns the vector associated with the given node ID, if it exists.
36    fn get_vector(&self, id: NodeId) -> Option<Arc<[f32]>>;
37}
38
39/// Reads vectors from [`LpgStore`]'s property storage for a given property key.
40///
41/// This is the primary accessor used by the engine when performing vector
42/// operations. It reads directly from the property store, avoiding any
43/// duplication.
44pub struct PropertyVectorAccessor<'a> {
45    store: &'a LpgStore,
46    property: PropertyKey,
47}
48
49impl<'a> PropertyVectorAccessor<'a> {
50    /// Creates a new accessor for the given store and property key.
51    #[must_use]
52    pub fn new(store: &'a LpgStore, property: impl Into<PropertyKey>) -> Self {
53        Self {
54            store,
55            property: property.into(),
56        }
57    }
58}
59
60impl VectorAccessor for PropertyVectorAccessor<'_> {
61    fn get_vector(&self, id: NodeId) -> Option<Arc<[f32]>> {
62        match self.store.get_node_property(id, &self.property) {
63            Some(Value::Vector(v)) => Some(v),
64            _ => None,
65        }
66    }
67}
68
69/// Blanket implementation for closures, useful in tests.
70impl<F> VectorAccessor for F
71where
72    F: Fn(NodeId) -> Option<Arc<[f32]>> + Send + Sync,
73{
74    fn get_vector(&self, id: NodeId) -> Option<Arc<[f32]>> {
75        self(id)
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn test_closure_accessor() {
85        let vectors: std::collections::HashMap<NodeId, Arc<[f32]>> = [
86            (NodeId::new(1), Arc::from(vec![1.0_f32, 0.0, 0.0])),
87            (NodeId::new(2), Arc::from(vec![0.0_f32, 1.0, 0.0])),
88        ]
89        .into_iter()
90        .collect();
91
92        let accessor = move |id: NodeId| -> Option<Arc<[f32]>> { vectors.get(&id).cloned() };
93
94        assert!(accessor.get_vector(NodeId::new(1)).is_some());
95        assert_eq!(accessor.get_vector(NodeId::new(1)).unwrap().len(), 3);
96        assert!(accessor.get_vector(NodeId::new(3)).is_none());
97    }
98
99    #[test]
100    fn test_property_vector_accessor() {
101        let store = LpgStore::new();
102        let id = store.create_node(&["Test"]);
103        let vec_data: Arc<[f32]> = vec![1.0, 2.0, 3.0].into();
104        store.set_node_property(id, "embedding", Value::Vector(vec_data.clone()));
105
106        let accessor = PropertyVectorAccessor::new(&store, "embedding");
107        let result = accessor.get_vector(id);
108        assert!(result.is_some());
109        assert_eq!(result.unwrap().as_ref(), vec_data.as_ref());
110
111        // Non-existent node
112        assert!(accessor.get_vector(NodeId::new(999)).is_none());
113
114        // Wrong property type
115        store.set_node_property(id, "name", Value::from("hello"));
116        let name_accessor = PropertyVectorAccessor::new(&store, "name");
117        assert!(name_accessor.get_vector(id).is_none());
118    }
119}