1use std::path::Path;
6
7use crate::{
8 error::MemoryError,
9 types::{Scope, ScopeFilter},
10};
11
12pub mod in_memory;
14pub mod usearch;
16
17pub use in_memory::InMemoryStore;
18pub use usearch::UsearchStore;
19
20pub(crate) mod sealed {
25 pub trait Sealed {}
26}
27
28pub trait VectorStore: Send + Sync + sealed::Sealed {
41 fn add(
45 &self,
46 scope: &Scope,
47 vector: &[f32],
48 qualified_name: String,
49 ) -> Result<u64, MemoryError>;
50
51 fn remove(&self, scope: &Scope, qualified_name: &str) -> Result<(), MemoryError>;
54
55 fn search(
61 &self,
62 filter: &ScopeFilter,
63 query: &[f32],
64 limit: usize,
65 ) -> Result<Vec<(u64, String, f32)>, MemoryError>;
66
67 fn find_by_name(&self, qualified_name: &str) -> Option<u64>;
71
72 fn save(&self, dir: &Path) -> Result<(), MemoryError>;
74
75 fn is_ready(&self) -> bool;
81
82 fn dimensions(&self) -> usize;
84
85 fn commit_sha(&self) -> Option<String>;
87
88 fn set_commit_sha(&self, sha: Option<&str>);
90}
91
92#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::index::{InMemoryStore, UsearchStore};
100
101 fn vec_a() -> Vec<f32> {
102 vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
103 }
104
105 fn vec_b() -> Vec<f32> {
106 vec![0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
107 }
108
109 fn check_contract(store: &dyn VectorStore) {
111 let scope = Scope::Global;
112 let name = "global/contract-test".to_string();
113
114 store
116 .add(&scope, &vec_a(), name.clone())
117 .expect("add should succeed");
118 assert!(
119 store.find_by_name(&name).is_some(),
120 "TC-02a: find_by_name should return Some after add"
121 );
122
123 let results = store
125 .search(&ScopeFilter::GlobalOnly, &vec_a(), 5)
126 .expect("search should succeed");
127 assert!(
128 results.iter().any(|(_, n, _)| n == &name),
129 "TC-02a: search should return added entry"
130 );
131
132 store
134 .add(&scope, &vec_b(), name.clone())
135 .expect("upsert should succeed");
136 let results = store
137 .search(&ScopeFilter::All, &vec_b(), 10)
138 .expect("search after upsert should succeed");
139 assert_eq!(
140 results.iter().filter(|(_, n, _)| n == &name).count(),
141 1,
142 "TC-02c: upsert should leave exactly one entry"
143 );
144
145 store.remove(&scope, &name).expect("remove should succeed");
147 assert!(
148 store.find_by_name(&name).is_none(),
149 "TC-02b: find_by_name should return None after remove"
150 );
151 let results_after = store
152 .search(&ScopeFilter::GlobalOnly, &vec_a(), 5)
153 .expect("search after remove should succeed");
154 assert!(
155 !results_after.iter().any(|(_, n, _)| n == &name),
156 "TC-02b: search should not return removed entry"
157 );
158
159 let proj_scope = Scope::Project("testproj".to_string());
161 store
162 .add(
163 &Scope::Global,
164 &vec_a(),
165 "global/contract-global".to_string(),
166 )
167 .expect("re-add global entry for TC-02d");
168 store
169 .add(
170 &proj_scope,
171 &vec_b(),
172 "projects/testproj/contract-proj".to_string(),
173 )
174 .expect("add project entry should succeed");
175 let pag_results = store
176 .search(
177 &ScopeFilter::ProjectAndGlobal("testproj".to_string()),
178 &vec_a(),
179 10,
180 )
181 .expect("ProjectAndGlobal search should succeed");
182 let pag_names: Vec<&str> = pag_results.iter().map(|(_, n, _)| n.as_str()).collect();
183 assert!(
184 pag_names.contains(&"projects/testproj/contract-proj"),
185 "TC-02d: ProjectAndGlobal should include project entries"
186 );
187 assert!(
188 pag_names.contains(&"global/contract-global"),
189 "TC-02d: ProjectAndGlobal should include global entries"
190 );
191 store
193 .remove(&proj_scope, "projects/testproj/contract-proj")
194 .expect("remove project entry");
195 store
196 .remove(&Scope::Global, "global/contract-global")
197 .expect("remove global entry");
198
199 assert!(
201 store.is_ready(),
202 "TC-06: is_ready() should return true for a functioning store"
203 );
204
205 assert_eq!(
207 store.dimensions(),
208 8,
209 "dimensions() should return 8 (the value passed to new)"
210 );
211
212 assert!(
214 store.commit_sha().is_none(),
215 "commit_sha() should be None on a fresh store"
216 );
217 store.set_commit_sha(Some("deadbeef"));
218 assert_eq!(
219 store.commit_sha(),
220 Some("deadbeef".to_string()),
221 "commit_sha() should reflect set_commit_sha(Some(...))"
222 );
223 store.set_commit_sha(None);
224 assert!(
225 store.commit_sha().is_none(),
226 "commit_sha() should be None after set_commit_sha(None)"
227 );
228 }
229
230 #[test]
231 fn trait_contract_usearch_store() {
232 let store = UsearchStore::new(8).expect("create UsearchStore");
233 check_contract(&store);
234 }
235
236 #[test]
237 fn trait_contract_in_memory_store() {
238 let store = InMemoryStore::new(8);
239 check_contract(&store);
240 }
241}