oxirs_vec/vector_index.rs
1//! In-memory vector index implementations and the `VectorIndex` trait.
2
3use anyhow::Result;
4use std::collections::HashMap;
5
6use crate::similarity;
7use crate::Vector;
8use crate::VectorId;
9
10/// Vector index trait for efficient similarity search.
11///
12/// # Score contract (must be honored by every implementation)
13///
14/// All `VectorIndex` methods that return `(id, score)` tuples use **similarity**
15/// semantics, *not* raw distance:
16///
17/// * The `f32` score is a **similarity**: larger means *more similar / closer*.
18/// * [`VectorIndex::search_knn`] returns results sorted by **descending**
19/// similarity (best match first).
20/// * [`VectorIndex::search_threshold`] returns every vector whose similarity is
21/// `>= threshold` (i.e. the comparison is `similarity >= threshold`, never
22/// `distance <= threshold`).
23///
24/// This mirrors the public [`crate::VectorStore::similarity_search`] API and the
25/// reference [`MemoryVectorIndex`] implementation. Backends whose internal
26/// algorithm works in distance space (HNSW, LSH, memory-mapped, IVF, PQ, NSG)
27/// **must** convert their distances to a monotonically-decreasing similarity
28/// (the crate convention is `similarity = 1.0 / (1.0 + distance)`) before
29/// returning, so that all backends agree on units and ordering. A caller that
30/// dispatches the same logical query to different backends (e.g.
31/// `DynamicIndexSelector`) must be able to compare and re-rank the returned
32/// scores without knowing which backend produced them.
33pub trait VectorIndex: Send + Sync {
34 /// Insert a vector with associated URI
35 fn insert(&mut self, uri: String, vector: Vector) -> Result<()>;
36
37 /// Find the `k` nearest neighbors, returned as `(id, similarity)` sorted by
38 /// **descending** similarity (best match first). See the trait-level score
39 /// contract: the score is a similarity, not a distance.
40 fn search_knn(&self, query: &Vector, k: usize) -> Result<Vec<(String, f32)>>;
41
42 /// Find all vectors whose **similarity** to `query` is `>= threshold`.
43 ///
44 /// The score is a similarity (larger = closer), consistent with
45 /// [`VectorIndex::search_knn`]; the filter is `similarity >= threshold`.
46 fn search_threshold(&self, query: &Vector, threshold: f32) -> Result<Vec<(String, f32)>>;
47
48 /// Get a vector by its URI
49 fn get_vector(&self, uri: &str) -> Option<&Vector>;
50
51 /// Add a vector with associated ID and metadata
52 fn add_vector(
53 &mut self,
54 id: VectorId,
55 vector: Vector,
56 _metadata: Option<HashMap<String, String>>,
57 ) -> Result<()> {
58 // Default implementation that delegates to insert
59 self.insert(id, vector)
60 }
61
62 /// Update an existing vector
63 fn update_vector(&mut self, id: VectorId, vector: Vector) -> Result<()> {
64 // Default implementation that delegates to insert
65 self.insert(id, vector)
66 }
67
68 /// Update metadata for a vector
69 fn update_metadata(&mut self, _id: VectorId, _metadata: HashMap<String, String>) -> Result<()> {
70 // Default implementation (no-op)
71 Ok(())
72 }
73
74 /// Remove a vector by its ID
75 fn remove_vector(&mut self, _id: VectorId) -> Result<()> {
76 // Default implementation (no-op)
77 Ok(())
78 }
79
80 /// Iterate all stored (id, vector) pairs.
81 ///
82 /// The default returns an empty list; concrete index types that hold their
83 /// vectors in memory (or can reconstruct them, e.g. via decoding quantized
84 /// codes) should override this **and** [`VectorIndex::supports_enumeration`]
85 /// so callers like `VectorStore::save_to_disk` can tell real emptiness
86 /// apart from "this index type cannot enumerate its vectors".
87 fn iter_vectors(&self) -> Vec<(String, Vector)> {
88 Vec::new()
89 }
90
91 /// Whether [`VectorIndex::iter_vectors`] returns a real, complete
92 /// enumeration of the vectors held by this index.
93 ///
94 /// Index types that override `iter_vectors` with a real implementation
95 /// (e.g. [`MemoryVectorIndex`], `HnswIndex`, `IvfIndex`, `PQIndex`) must
96 /// also override this to return `true`. Callers that need to persist or
97 /// otherwise fully enumerate an index (e.g. `VectorStore::save_to_disk`)
98 /// should check this flag and fail loudly instead of silently persisting
99 /// an empty snapshot when it is `false`.
100 fn supports_enumeration(&self) -> bool {
101 false
102 }
103}
104
105/// In-memory vector index implementation
106pub struct MemoryVectorIndex {
107 vectors: Vec<(String, Vector)>,
108 similarity_config: similarity::SimilarityConfig,
109}
110
111impl MemoryVectorIndex {
112 /// Create a new empty in-memory vector index with default similarity config.
113 pub fn new() -> Self {
114 Self {
115 vectors: Vec::new(),
116 similarity_config: similarity::SimilarityConfig::default(),
117 }
118 }
119
120 /// Create a new in-memory vector index with a custom similarity configuration.
121 pub fn with_similarity_config(config: similarity::SimilarityConfig) -> Self {
122 Self {
123 vectors: Vec::new(),
124 similarity_config: config,
125 }
126 }
127}
128
129impl Default for MemoryVectorIndex {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135impl VectorIndex for MemoryVectorIndex {
136 fn insert(&mut self, uri: String, vector: Vector) -> Result<()> {
137 // Check if vector already exists and update it
138 if let Some(pos) = self.vectors.iter().position(|(id, _)| id == &uri) {
139 self.vectors[pos] = (uri, vector);
140 } else {
141 self.vectors.push((uri, vector));
142 }
143 Ok(())
144 }
145
146 fn search_knn(&self, query: &Vector, k: usize) -> Result<Vec<(String, f32)>> {
147 let metric = self.similarity_config.primary_metric;
148 let query_f32 = query.as_f32();
149 let mut similarities: Vec<(String, f32)> = self
150 .vectors
151 .iter()
152 .map(|(uri, vec)| {
153 let vec_f32 = vec.as_f32();
154 let sim = metric.similarity(&query_f32, &vec_f32).unwrap_or(0.0);
155 (uri.clone(), sim)
156 })
157 .collect();
158
159 similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
160 similarities.truncate(k);
161
162 Ok(similarities)
163 }
164
165 fn search_threshold(&self, query: &Vector, threshold: f32) -> Result<Vec<(String, f32)>> {
166 let metric = self.similarity_config.primary_metric;
167 let query_f32 = query.as_f32();
168 let similarities: Vec<(String, f32)> = self
169 .vectors
170 .iter()
171 .filter_map(|(uri, vec)| {
172 let vec_f32 = vec.as_f32();
173 let sim = metric.similarity(&query_f32, &vec_f32).unwrap_or(0.0);
174 if sim >= threshold {
175 Some((uri.clone(), sim))
176 } else {
177 None
178 }
179 })
180 .collect();
181
182 Ok(similarities)
183 }
184
185 fn get_vector(&self, uri: &str) -> Option<&Vector> {
186 self.vectors.iter().find(|(u, _)| u == uri).map(|(_, v)| v)
187 }
188
189 fn update_vector(&mut self, id: VectorId, vector: Vector) -> Result<()> {
190 if let Some(pos) = self.vectors.iter().position(|(uri, _)| uri == &id) {
191 self.vectors[pos] = (id, vector);
192 Ok(())
193 } else {
194 Err(anyhow::anyhow!("Vector with id '{}' not found", id))
195 }
196 }
197
198 fn remove_vector(&mut self, id: VectorId) -> Result<()> {
199 if let Some(pos) = self.vectors.iter().position(|(uri, _)| uri == &id) {
200 self.vectors.remove(pos);
201 Ok(())
202 } else {
203 Err(anyhow::anyhow!("Vector with id '{}' not found", id))
204 }
205 }
206
207 fn iter_vectors(&self) -> Vec<(String, Vector)> {
208 self.vectors.clone()
209 }
210
211 fn supports_enumeration(&self) -> bool {
212 true
213 }
214}