Skip to content

Commit 2295586

Browse files
committed
perf(core): predicate-first memory scan materializes only winning vectors
1 parent ec12668 commit 2295586

1 file changed

Lines changed: 69 additions & 1 deletion

File tree

‎crates/ruvector-core/src/storage_memory.rs‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! making it suitable for WebAssembly environments.
55
66
use crate::error::{Result, RuvectorError};
7-
use crate::types::{VectorEntry, VectorId};
7+
use crate::types::{DistanceMetric, SearchQuery, SearchResult, VectorEntry, VectorId};
88
use dashmap::DashMap;
99
use serde_json::Value as JsonValue;
1010
use std::sync::atomic::{AtomicU64, Ordering};
@@ -122,6 +122,74 @@ impl MemoryStorage {
122122
}
123123
}
124124

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+
125193
/// Delete a vector by ID
126194
pub fn delete(&self, id: &str) -> Result<bool> {
127195
let vector_removed = self.vectors.remove(id).is_some();

0 commit comments

Comments
 (0)