1use std::collections::{BTreeMap, HashMap};
17
18use async_trait::async_trait;
19use serde_json::Value;
20
21use crate::tenant::TenantId;
22
23#[allow(clippy::disallowed_types)]
26type Guarded<T> = std::sync::Mutex<T>;
27
28pub const MAX_NAMESPACE_BYTES: usize = 64;
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34pub struct VectorNamespace(String);
35
36impl VectorNamespace {
37 #[must_use]
40 pub fn for_tenant(tenant: &TenantId) -> Self {
41 Self(tenant.as_str().to_owned())
42 }
43
44 pub fn new(namespace: impl Into<String>) -> Result<Self, VectorIndexError> {
50 let namespace = namespace.into();
51 let bytes = namespace.len();
52 if bytes == 0 || bytes > MAX_NAMESPACE_BYTES {
53 return Err(VectorIndexError::InvalidInput(format!(
54 "namespace must be 1..={MAX_NAMESPACE_BYTES} bytes, got {bytes}"
55 )));
56 }
57 Ok(Self(namespace))
58 }
59
60 #[must_use]
61 pub fn as_str(&self) -> &str {
62 &self.0
63 }
64}
65
66impl std::fmt::Display for VectorNamespace {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 f.write_str(&self.0)
69 }
70}
71
72#[derive(Debug, Clone, PartialEq)]
75pub struct VectorRecord {
76 pub id: String,
77 pub values: Vec<f32>,
78 pub metadata: BTreeMap<String, Value>,
79}
80
81impl VectorRecord {
82 #[must_use]
85 pub fn new(id: impl Into<String>, values: Vec<f32>) -> Self {
86 Self {
87 id: id.into(),
88 values,
89 metadata: BTreeMap::new(),
90 }
91 }
92
93 #[must_use]
95 pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
96 self.metadata.insert(key.into(), value);
97 self
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Default)]
106pub struct VectorFilter {
107 eq: BTreeMap<String, Value>,
108}
109
110impl VectorFilter {
111 #[must_use]
113 pub fn new() -> Self {
114 Self::default()
115 }
116
117 #[must_use]
119 pub fn eq(mut self, key: impl Into<String>, value: Value) -> Self {
120 self.eq.insert(key.into(), value);
121 self
122 }
123
124 #[must_use]
126 pub fn filters(&self) -> &BTreeMap<String, Value> {
127 &self.eq
128 }
129
130 #[must_use]
133 pub fn matches(&self, metadata: &BTreeMap<String, Value>) -> bool {
134 self.eq
135 .iter()
136 .all(|(key, value)| metadata.get(key) == Some(value))
137 }
138}
139
140#[derive(Debug, Clone, PartialEq)]
143pub struct VectorMatch {
144 pub id: String,
145 pub score: f32,
146 pub metadata: BTreeMap<String, Value>,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum VectorIndexError {
153 NotConfigured,
155 InvalidInput(String),
157 Operation(String),
159}
160
161impl std::fmt::Display for VectorIndexError {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 let scrub = crate::logging::scrub_text;
164 match self {
165 Self::NotConfigured => f.write_str("no vector index is wired"),
166 Self::InvalidInput(message) => {
167 write!(f, "vector index rejected the request: {}", scrub(message))
168 }
169 Self::Operation(message) => {
170 write!(f, "vector index operation failed: {}", scrub(message))
171 }
172 }
173 }
174}
175
176impl std::error::Error for VectorIndexError {}
177
178#[async_trait]
184pub trait VectorIndex: Send + Sync {
185 async fn upsert(
193 &self,
194 namespace: &VectorNamespace,
195 records: &[VectorRecord],
196 ) -> Result<(), VectorIndexError>;
197
198 async fn query(
206 &self,
207 namespace: &VectorNamespace,
208 vector: &[f32],
209 k: usize,
210 filter: &VectorFilter,
211 ) -> Result<Vec<VectorMatch>, VectorIndexError>;
212
213 async fn delete(
220 &self,
221 namespace: &VectorNamespace,
222 ids: &[String],
223 ) -> Result<(), VectorIndexError>;
224}
225
226type Stored = (Vec<f32>, BTreeMap<String, Value>);
228
229pub struct ExactVectorIndex {
235 dimensions: usize,
236 #[allow(clippy::disallowed_types)]
239 namespaces: Guarded<HashMap<String, BTreeMap<String, Stored>>>,
240}
241
242impl ExactVectorIndex {
243 #[must_use]
246 pub fn new(dimensions: usize) -> Self {
247 Self {
248 dimensions,
249 namespaces: Guarded::new(HashMap::new()),
250 }
251 }
252
253 fn validate(&self, records: &[VectorRecord]) -> Result<(), VectorIndexError> {
256 for record in records {
257 if record.values.is_empty() {
258 return Err(VectorIndexError::InvalidInput(format!(
259 "vector for record `{}` is empty",
260 record.id
261 )));
262 }
263 if record.values.len() != self.dimensions {
264 return Err(VectorIndexError::InvalidInput(format!(
265 "vector for record `{}` has {} dimensions, index expects {}",
266 record.id,
267 record.values.len(),
268 self.dimensions
269 )));
270 }
271 if record.values.iter().any(|value| !value.is_finite()) {
272 return Err(VectorIndexError::InvalidInput(format!(
273 "vector for record `{}` has a non-finite component",
274 record.id
275 )));
276 }
277 if !vectorize_metadata(&record.metadata) {
278 return Err(VectorIndexError::InvalidInput(format!(
279 "metadata of record `{}` holds a value Vectorize cannot store",
280 record.id
281 )));
282 }
283 }
284 Ok(())
285 }
286}
287
288fn vectorize_metadata(metadata: &BTreeMap<String, Value>) -> bool {
291 metadata.values().all(|value| match value {
292 Value::String(_) | Value::Number(_) | Value::Bool(_) => true,
293 Value::Array(items) => items.iter().all(Value::is_string),
294 _ => false,
295 })
296}
297
298fn cosine(a: &[f32], b: &[f32]) -> f32 {
300 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
301 let norms =
302 a.iter().map(|x| x * x).sum::<f32>().sqrt() * b.iter().map(|y| y * y).sum::<f32>().sqrt();
303 if norms == 0.0 { 0.0 } else { dot / norms }
304}
305
306#[async_trait]
307impl VectorIndex for ExactVectorIndex {
308 async fn upsert(
309 &self,
310 namespace: &VectorNamespace,
311 records: &[VectorRecord],
312 ) -> Result<(), VectorIndexError> {
313 self.validate(records)?;
314 let mut namespaces = self.namespaces.lock().expect("lock uncontended");
315 let index = namespaces.entry(namespace.as_str().to_owned()).or_default();
316 for record in records {
317 index.insert(
318 record.id.clone(),
319 (record.values.clone(), record.metadata.clone()),
320 );
321 }
322 Ok(())
323 }
324
325 async fn query(
326 &self,
327 namespace: &VectorNamespace,
328 vector: &[f32],
329 k: usize,
330 filter: &VectorFilter,
331 ) -> Result<Vec<VectorMatch>, VectorIndexError> {
332 if vector.is_empty() {
333 return Err(VectorIndexError::InvalidInput(
334 "query vector is empty".to_owned(),
335 ));
336 }
337 if vector.len() != self.dimensions {
338 return Err(VectorIndexError::InvalidInput(format!(
339 "query vector has {} dimensions, index expects {}",
340 vector.len(),
341 self.dimensions
342 )));
343 }
344 if vector.iter().any(|value| !value.is_finite()) {
345 return Err(VectorIndexError::InvalidInput(
346 "query vector has a non-finite component".to_owned(),
347 ));
348 }
349 if k == 0 {
350 return Ok(Vec::new());
351 }
352 let namespaces = self.namespaces.lock().expect("lock uncontended");
353 let Some(index) = namespaces.get(namespace.as_str()) else {
354 return Ok(Vec::new());
355 };
356 let mut scored: Vec<(f32, &Stored, &String)> = index
360 .iter()
361 .filter(|(_, (_, metadata))| filter.matches(metadata))
362 .map(|(id, stored)| (cosine(vector, &stored.0), stored, id))
363 .collect();
364 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
365 Ok(scored
366 .into_iter()
367 .take(k)
368 .map(|(score, stored, id)| VectorMatch {
369 id: id.clone(),
370 score,
371 metadata: stored.1.clone(),
372 })
373 .collect())
374 }
375
376 async fn delete(
377 &self,
378 namespace: &VectorNamespace,
379 ids: &[String],
380 ) -> Result<(), VectorIndexError> {
381 let mut namespaces = self.namespaces.lock().expect("lock uncontended");
382 if let Some(index) = namespaces.get_mut(namespace.as_str()) {
383 for id in ids {
384 index.remove(id);
385 }
386 }
387 Ok(())
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn namespaces_are_one_to_max_namespace_bytes() {
397 assert!(VectorNamespace::new("").is_err());
398 assert!(VectorNamespace::new("x".repeat(MAX_NAMESPACE_BYTES)).is_ok());
399 assert!(VectorNamespace::new("x".repeat(MAX_NAMESPACE_BYTES + 1)).is_err());
400 let tenant = crate::tenant::TenantId::new("tenant-1");
403 assert_eq!(VectorNamespace::for_tenant(&tenant).as_str(), "tenant-1");
404 }
405
406 #[test]
409 fn error_display_scrubs_provider_text() {
410 let error = VectorIndexError::Operation(
411 "vectorize 500 at https://api.example.test?token=secret-abcdef".to_owned(),
412 );
413 let text = error.to_string();
414 assert!(text.contains("vector index operation failed"), "{text}");
415 assert!(!text.contains("secret-abcdef"), "{text}");
416 assert!(format!("{error:?}").contains("secret-abcdef"));
417 assert_eq!(
418 VectorIndexError::NotConfigured.to_string(),
419 "no vector index is wired"
420 );
421 }
422
423 #[test]
424 fn malformed_vectors_and_metadata_are_invalid_input() {
425 let index = ExactVectorIndex::new(2);
426 let ns = VectorNamespace::new("ns").expect("valid");
427 let upsert = |values: &[f32]| {
428 pollster::block_on(index.upsert(&ns, &[VectorRecord::new("r", values.to_owned())]))
429 .unwrap_err()
430 };
431 let query = |vector: &[f32]| {
432 pollster::block_on(index.query(&ns, vector, 5, &VectorFilter::default())).unwrap_err()
433 };
434 for (error, message) in [
435 (upsert(&[1.0, 0.0, 0.0]), "3 dimensions"),
436 (upsert(&[]), "is empty"),
437 (upsert(&[f32::NAN, 1.0]), "non-finite"),
438 (query(&[]), "is empty"),
439 (query(&[1.0, 0.0, 0.0]), "3 dimensions"),
440 (query(&[f32::INFINITY, 1.0]), "non-finite"),
441 ] {
442 assert!(
443 matches!(error, VectorIndexError::InvalidInput(_)),
444 "{error}"
445 );
446 assert!(error.to_string().contains(message), "{error}");
447 }
448 let error = pollster::block_on(
451 index.upsert(
452 &ns,
453 &[VectorRecord::new("r", vec![1.0, 0.0])
454 .with_metadata("nested", serde_json::json!({ "a": 1 }))],
455 ),
456 )
457 .unwrap_err();
458 assert!(
459 matches!(error, VectorIndexError::InvalidInput(_)),
460 "{error}"
461 );
462 assert!(
463 error.to_string().contains("Vectorize cannot store"),
464 "{error}"
465 );
466 }
467}