|
4 | 4 | //! making it suitable for WebAssembly environments. |
5 | 5 |
|
6 | 6 | use crate::error::{Result, RuvectorError}; |
7 | | -use crate::types::{VectorEntry, VectorId}; |
| 7 | +use crate::types::{DistanceMetric, SearchQuery, SearchResult, VectorEntry, VectorId}; |
8 | 8 | use dashmap::DashMap; |
9 | 9 | use serde_json::Value as JsonValue; |
10 | 10 | use std::sync::atomic::{AtomicU64, Ordering}; |
@@ -122,6 +122,74 @@ impl MemoryStorage { |
122 | 122 | } |
123 | 123 | } |
124 | 124 |
|
| 125 | + /// Predicate-first exact retrieval without cloning excluded vectors. |
| 126 | + /// This is faster than materializing all entries for selective filters. |
| 127 | + pub(crate) fn search_filtered_exact( |
| 128 | + &self, |
| 129 | + query: &SearchQuery, |
| 130 | + metric: DistanceMetric, |
| 131 | + ) -> Result<Vec<SearchResult>> { |
| 132 | + let Some(filter) = query.filter.as_ref() else { |
| 133 | + return Err(RuvectorError::InvalidInput( |
| 134 | + "filtered search requires a filter".into(), |
| 135 | + )); |
| 136 | + }; |
| 137 | + if query.k == 0 { |
| 138 | + return Ok(Vec::new()); |
| 139 | + } |
| 140 | + if query.vector.len() != self.dimensions { |
| 141 | + return Err(RuvectorError::DimensionMismatch { |
| 142 | + expected: self.dimensions, |
| 143 | + actual: query.vector.len(), |
| 144 | + }); |
| 145 | + } |
| 146 | + // Rank IDs first. Materialize payloads only for the winning k; |
| 147 | + // cloning every eligible high-dimensional vector is unnecessary. |
| 148 | + let mut candidates = Vec::new(); |
| 149 | + for entry in self.vectors.iter() { |
| 150 | + let Some(raw) = self.metadata.get(entry.key()) else { |
| 151 | + continue; |
| 152 | + }; |
| 153 | + let JsonValue::Object(metadata) = raw.value() else { |
| 154 | + continue; |
| 155 | + }; |
| 156 | + if !filter.iter().all(|(k, v)| metadata.get(k) == Some(v)) { |
| 157 | + continue; |
| 158 | + } |
| 159 | + let score = crate::distance::distance(&query.vector, entry.value(), metric)?; |
| 160 | + candidates.push((entry.key().clone(), score)); |
| 161 | + } |
| 162 | + candidates.sort_unstable_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0))); |
| 163 | + candidates.truncate(query.k); |
| 164 | + let mut results = Vec::with_capacity(candidates.len()); |
| 165 | + for (id, _) in candidates { |
| 166 | + let Some(entry) = self.vectors.get(&id) else { |
| 167 | + continue; |
| 168 | + }; |
| 169 | + let Some(raw) = self.metadata.get(&id) else { |
| 170 | + continue; |
| 171 | + }; |
| 172 | + let JsonValue::Object(map) = raw.value() else { |
| 173 | + continue; |
| 174 | + }; |
| 175 | + // Recheck at materialization to avoid returning a record that |
| 176 | + // changed namespaces during a concurrent mutation. |
| 177 | + if !filter.iter().all(|(k, v)| map.get(k) == Some(v)) { |
| 178 | + continue; |
| 179 | + } |
| 180 | + let metadata = Some(map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()); |
| 181 | + let score = crate::distance::distance(&query.vector, entry.value(), metric)?; |
| 182 | + results.push(SearchResult { |
| 183 | + id, |
| 184 | + score, |
| 185 | + vector: Some(entry.value().clone()), |
| 186 | + metadata, |
| 187 | + }); |
| 188 | + } |
| 189 | + results.sort_unstable_by(|a, b| a.score.total_cmp(&b.score).then_with(|| a.id.cmp(&b.id))); |
| 190 | + Ok(results) |
| 191 | + } |
| 192 | + |
125 | 193 | /// Delete a vector by ID |
126 | 194 | pub fn delete(&self, id: &str) -> Result<bool> { |
127 | 195 | let vector_removed = self.vectors.remove(id).is_some(); |
|
0 commit comments