grafeo_core/index/vector/
accessor.rs1use std::sync::Arc;
24
25use grafeo_common::types::{NodeId, PropertyKey, Value};
26
27use crate::graph::GraphStore;
28
29pub trait VectorAccessor: Send + Sync {
34 fn get_vector(&self, id: NodeId) -> Option<Arc<[f32]>>;
36}
37
38pub struct PropertyVectorAccessor<'a> {
44 store: &'a dyn GraphStore,
45 property: PropertyKey,
46}
47
48impl<'a> PropertyVectorAccessor<'a> {
49 #[must_use]
51 pub fn new(store: &'a dyn GraphStore, property: impl Into<PropertyKey>) -> Self {
52 Self {
53 store,
54 property: property.into(),
55 }
56 }
57}
58
59impl VectorAccessor for PropertyVectorAccessor<'_> {
60 fn get_vector(&self, id: NodeId) -> Option<Arc<[f32]>> {
61 match self.store.get_node_property(id, &self.property) {
62 Some(Value::Vector(v)) => Some(v),
63 _ => None,
64 }
65 }
66}
67
68pub struct SpillableVectorAccessor<'a> {
75 store: &'a dyn GraphStore,
76 property: PropertyKey,
77 spill_storage: Arc<dyn super::storage::VectorStorage>,
78}
79
80impl<'a> SpillableVectorAccessor<'a> {
81 #[must_use]
84 pub fn new(
85 store: &'a dyn GraphStore,
86 property: impl Into<PropertyKey>,
87 spill_storage: Arc<dyn super::storage::VectorStorage>,
88 ) -> Self {
89 Self {
90 store,
91 property: property.into(),
92 spill_storage,
93 }
94 }
95}
96
97impl VectorAccessor for SpillableVectorAccessor<'_> {
98 fn get_vector(&self, id: NodeId) -> Option<Arc<[f32]>> {
99 if let Some(v) = self.spill_storage.get(id) {
101 return Some(v);
102 }
103 match self.store.get_node_property(id, &self.property) {
105 Some(Value::Vector(v)) => Some(v),
106 _ => None,
107 }
108 }
109}
110
111#[non_exhaustive]
117pub enum VectorAccessorKind<'a> {
118 Property(PropertyVectorAccessor<'a>),
120 Spilled(SpillableVectorAccessor<'a>),
122}
123
124impl VectorAccessor for VectorAccessorKind<'_> {
125 fn get_vector(&self, id: NodeId) -> Option<Arc<[f32]>> {
126 match self {
127 Self::Property(a) => a.get_vector(id),
128 Self::Spilled(a) => a.get_vector(id),
129 }
130 }
131}
132
133impl<F> VectorAccessor for F
135where
136 F: Fn(NodeId) -> Option<Arc<[f32]>> + Send + Sync,
137{
138 fn get_vector(&self, id: NodeId) -> Option<Arc<[f32]>> {
139 self(id)
140 }
141}
142
143#[cfg(all(test, feature = "lpg"))]
144mod tests {
145 use super::*;
146 use crate::graph::lpg::LpgStore;
147
148 #[test]
149 fn test_closure_accessor() {
150 let vectors: std::collections::HashMap<NodeId, Arc<[f32]>> = [
151 (NodeId::new(1), Arc::from(vec![1.0_f32, 0.0, 0.0])),
152 (NodeId::new(2), Arc::from(vec![0.0_f32, 1.0, 0.0])),
153 ]
154 .into_iter()
155 .collect();
156
157 let accessor = move |id: NodeId| -> Option<Arc<[f32]>> { vectors.get(&id).cloned() };
158
159 assert!(accessor.get_vector(NodeId::new(1)).is_some());
160 assert_eq!(accessor.get_vector(NodeId::new(1)).unwrap().len(), 3);
161 assert!(accessor.get_vector(NodeId::new(3)).is_none());
162 }
163
164 #[test]
165 fn test_property_vector_accessor() {
166 let store = LpgStore::new().unwrap();
167 let id = store.create_node(&["Test"]);
168 let vec_data: Arc<[f32]> = vec![1.0, 2.0, 3.0].into();
169 store.set_node_property(id, "embedding", Value::Vector(vec_data.clone()));
170
171 let accessor = PropertyVectorAccessor::new(&store, "embedding");
172 let result = accessor.get_vector(id);
173 assert!(result.is_some());
174 assert_eq!(result.unwrap().as_ref(), vec_data.as_ref());
175
176 assert!(accessor.get_vector(NodeId::new(999)).is_none());
178
179 store.set_node_property(id, "name", Value::from("hello"));
181 let name_accessor = PropertyVectorAccessor::new(&store, "name");
182 assert!(name_accessor.get_vector(id).is_none());
183 }
184}
185
186#[cfg(all(test, feature = "lpg", feature = "vector-index"))]
187mod spill_tests {
188 use super::*;
189 use crate::graph::lpg::LpgStore;
190 use crate::index::vector::storage::{RamStorage, VectorStorage};
191
192 #[test]
193 fn spill_accessor_returns_vector_from_spill_storage() {
194 let store = LpgStore::new().unwrap();
195 let alix_id = store.create_node(&["Person"]);
196 let spill_vec: Vec<f32> = vec![0.1, 0.2, 0.3];
197 let spill = Arc::new(RamStorage::new(3));
198 spill.insert(alix_id, &spill_vec).unwrap();
199
200 let accessor = SpillableVectorAccessor::new(&store as &dyn GraphStore, "embedding", spill);
201 let result = accessor.get_vector(alix_id);
202 assert!(result.is_some());
203 assert_eq!(result.unwrap().as_ref(), spill_vec.as_slice());
204 }
205
206 #[test]
207 fn spill_accessor_falls_back_to_property_store() {
208 let store = LpgStore::new().unwrap();
209 let gus_id = store.create_node(&["Person"]);
210 let prop_vec: Arc<[f32]> = vec![0.4, 0.5, 0.6].into();
211 store.set_node_property(gus_id, "embedding", Value::Vector(prop_vec.clone()));
212
213 let spill: Arc<dyn VectorStorage> = Arc::new(RamStorage::new(3));
214 let accessor = SpillableVectorAccessor::new(&store as &dyn GraphStore, "embedding", spill);
215 let result = accessor.get_vector(gus_id);
216 assert!(result.is_some());
217 assert_eq!(result.unwrap().as_ref(), prop_vec.as_ref());
218 }
219
220 #[test]
221 fn spill_accessor_prefers_spill_over_property_store() {
222 let store = LpgStore::new().unwrap();
223 let vincent_id = store.create_node(&["Person"]);
224 let prop_vec: Arc<[f32]> = vec![1.0, 0.0, 0.0].into();
225 store.set_node_property(vincent_id, "embedding", Value::Vector(prop_vec));
226
227 let spill_vec: Vec<f32> = vec![0.0, 1.0, 0.0];
228 let spill = Arc::new(RamStorage::new(3));
229 spill.insert(vincent_id, &spill_vec).unwrap();
230
231 let accessor = SpillableVectorAccessor::new(&store as &dyn GraphStore, "embedding", spill);
232 let result = accessor.get_vector(vincent_id).unwrap();
233 assert_eq!(result.as_ref(), spill_vec.as_slice());
234 }
235
236 #[test]
237 fn spill_accessor_returns_none_when_missing() {
238 let store = LpgStore::new().unwrap();
239 let spill: Arc<dyn VectorStorage> = Arc::new(RamStorage::new(3));
240 let accessor = SpillableVectorAccessor::new(&store as &dyn GraphStore, "embedding", spill);
241 assert!(accessor.get_vector(NodeId::new(999)).is_none());
242 }
243
244 #[test]
245 fn accessor_kind_property_dispatches() {
246 let store = LpgStore::new().unwrap();
247 let jules_id = store.create_node(&["Person"]);
248 let vec_data: Arc<[f32]> = vec![0.7, 0.8, 0.9].into();
249 store.set_node_property(jules_id, "embedding", Value::Vector(vec_data.clone()));
250
251 let accessor =
252 VectorAccessorKind::Property(PropertyVectorAccessor::new(&store, "embedding"));
253 let result = accessor.get_vector(jules_id);
254 assert!(result.is_some());
255 assert_eq!(result.unwrap().as_ref(), vec_data.as_ref());
256 assert!(accessor.get_vector(NodeId::new(999)).is_none());
257 }
258
259 #[test]
260 fn accessor_kind_spilled_dispatches() {
261 let store = LpgStore::new().unwrap();
262 let mia_id = store.create_node(&["Person"]);
263 let spill_vec: Vec<f32> = vec![0.3, 0.6, 0.9];
264 let spill = Arc::new(RamStorage::new(3));
265 spill.insert(mia_id, &spill_vec).unwrap();
266
267 let accessor = VectorAccessorKind::Spilled(SpillableVectorAccessor::new(
268 &store as &dyn GraphStore,
269 "embedding",
270 spill,
271 ));
272 let result = accessor.get_vector(mia_id);
273 assert!(result.is_some());
274 assert_eq!(result.unwrap().as_ref(), spill_vec.as_slice());
275 }
276
277 #[test]
278 fn accessor_kind_spilled_uses_fallback() {
279 let store = LpgStore::new().unwrap();
280 let butch_id = store.create_node(&["Person"]);
281 let prop_vec: Arc<[f32]> = vec![0.2, 0.4, 0.6].into();
282 store.set_node_property(butch_id, "embedding", Value::Vector(prop_vec.clone()));
283
284 let spill: Arc<dyn VectorStorage> = Arc::new(RamStorage::new(3));
285 let accessor = VectorAccessorKind::Spilled(SpillableVectorAccessor::new(
286 &store as &dyn GraphStore,
287 "embedding",
288 spill,
289 ));
290 let result = accessor.get_vector(butch_id);
291 assert!(result.is_some());
292 assert_eq!(result.unwrap().as_ref(), prop_vec.as_ref());
293 }
294}