1use std::collections::{BTreeMap, BTreeSet};
18use std::sync::Arc;
19
20use serde::{Deserialize, Serialize};
21
22use crate::{
23 cosine_similarity_bounded, dot_product, euclidean_similarity, manhattan_distance, LoraVector,
24};
25
26use super::hnsw::{seed_from_name, HnswBackend, HnswParams, HnswSnapshot};
27use super::id_map::CowIdMap;
28use super::index_catalog::{IndexConfigValue, StoredIndexEntity};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum VectorSimilarity {
32 Cosine,
33 Euclidean,
34 Dot,
39 Manhattan,
43}
44
45impl VectorSimilarity {
46 pub fn parse(s: &str) -> Option<Self> {
47 if s.eq_ignore_ascii_case("cosine") {
48 Some(VectorSimilarity::Cosine)
49 } else if s.eq_ignore_ascii_case("euclidean") {
50 Some(VectorSimilarity::Euclidean)
51 } else if s.eq_ignore_ascii_case("dot") || s.eq_ignore_ascii_case("dot_product") {
52 Some(VectorSimilarity::Dot)
53 } else if s.eq_ignore_ascii_case("manhattan") {
54 Some(VectorSimilarity::Manhattan)
55 } else {
56 None
57 }
58 }
59
60 pub fn score(self, a: &LoraVector, b: &LoraVector) -> Option<f64> {
61 if a.dimension != b.dimension {
62 return None;
63 }
64 match self {
65 VectorSimilarity::Cosine => cosine_similarity_bounded(a, b),
66 VectorSimilarity::Euclidean => euclidean_similarity(a, b),
67 VectorSimilarity::Dot => dot_product(a, b),
68 VectorSimilarity::Manhattan => manhattan_distance(a, b).map(|d| 1.0 / (1.0 + d)),
69 }
70 }
71
72 pub(super) fn from_options(options: &BTreeMap<String, IndexConfigValue>) -> Option<Self> {
78 match options.get("vector.similarity_function")? {
79 IndexConfigValue::String(s) => Self::parse(s),
80 _ => None,
81 }
82 }
83}
84
85#[derive(Debug, Default, Clone)]
94pub(super) struct FlatBackend {
95 pub(super) items: CowIdMap<Arc<LoraVector>>,
96}
97
98impl FlatBackend {
99 fn insert(&mut self, id: u64, vector: LoraVector) {
100 self.items.insert(id, Arc::new(vector));
101 }
102
103 fn remove(&mut self, id: u64) {
104 self.items.remove(&id);
105 }
106
107 fn query(
108 &self,
109 query: &LoraVector,
110 similarity: VectorSimilarity,
111 restrict_to: Option<&BTreeSet<u64>>,
112 ) -> Vec<(u64, f64)> {
113 let mut out = Vec::with_capacity(self.items.len());
114 for (&id, v) in self.items.iter() {
115 if let Some(set) = restrict_to {
116 if !set.contains(&id) {
117 continue;
118 }
119 }
120 if let Some(score) = similarity.score(v, query) {
121 out.push((id, score));
122 }
123 }
124 out
125 }
126
127 #[cfg(test)]
128 fn len(&self) -> usize {
129 self.items.len()
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum VectorIndexProvider {
137 Flat,
138 Hnsw,
139}
140
141impl VectorIndexProvider {
142 pub fn parse(s: &str) -> Option<Self> {
143 if s.eq_ignore_ascii_case("flat") {
144 Some(VectorIndexProvider::Flat)
145 } else if s.eq_ignore_ascii_case("hnsw") {
146 Some(VectorIndexProvider::Hnsw)
147 } else {
148 None
149 }
150 }
151
152 pub(super) fn from_options(options: &BTreeMap<String, IndexConfigValue>) -> Option<Self> {
156 match options.get("vector.indexProvider")? {
157 IndexConfigValue::String(s) => Self::parse(s),
158 _ => None,
159 }
160 }
161}
162
163#[derive(Debug, Clone)]
168pub(super) enum VectorBackend {
169 Flat(FlatBackend),
170 Hnsw(HnswBackend),
171}
172
173impl VectorBackend {
174 fn insert(&mut self, id: u64, vector: LoraVector) {
175 match self {
176 VectorBackend::Flat(b) => b.insert(id, vector),
177 VectorBackend::Hnsw(b) => b.insert(id, vector),
178 }
179 }
180
181 fn remove(&mut self, id: u64) {
182 match self {
183 VectorBackend::Flat(b) => b.remove(id),
184 VectorBackend::Hnsw(b) => b.remove(id),
185 }
186 }
187
188 fn query(
200 &self,
201 query: &LoraVector,
202 similarity: VectorSimilarity,
203 k: usize,
204 restrict_to: Option<&BTreeSet<u64>>,
205 ) -> Vec<(u64, f64)> {
206 match self {
207 VectorBackend::Flat(b) => b.query(query, similarity, restrict_to),
208 VectorBackend::Hnsw(b) => b.query(query, k, restrict_to),
209 }
210 }
211
212 #[cfg(test)]
213 fn len(&self) -> usize {
214 match self {
215 VectorBackend::Flat(b) => b.len(),
216 VectorBackend::Hnsw(b) => b.len(),
217 }
218 }
219}
220
221#[derive(Debug, Clone)]
225pub(super) struct VectorIndexEntry {
226 pub label: String,
227 pub property: String,
228 pub similarity: VectorSimilarity,
229 pub backend: VectorBackend,
230}
231
232#[derive(Debug, Default, Clone)]
234pub(super) struct VectorIndexRegistry {
235 pub(super) by_name: BTreeMap<String, VectorIndexEntry>,
236}
237
238impl VectorIndexRegistry {
239 pub(super) fn register(
240 &mut self,
241 name: String,
242 label: String,
243 property: String,
244 similarity: VectorSimilarity,
245 provider: VectorIndexProvider,
246 hnsw: HnswParams,
247 ) {
248 let backend = match provider {
249 VectorIndexProvider::Flat => VectorBackend::Flat(FlatBackend::default()),
250 VectorIndexProvider::Hnsw => {
251 let seed = seed_from_name(&name);
252 VectorBackend::Hnsw(HnswBackend::new(similarity, hnsw, seed))
253 }
254 };
255 self.by_name.insert(
256 name,
257 VectorIndexEntry {
258 label,
259 property,
260 similarity,
261 backend,
262 },
263 );
264 }
265
266 pub(super) fn deregister(&mut self, name: &str) {
267 self.by_name.remove(name);
268 }
269
270 pub(super) fn covers_any<'a>(
274 &self,
275 labels: impl IntoIterator<Item = &'a str>,
276 property: &str,
277 ) -> bool {
278 labels.into_iter().any(|label| {
279 self.by_name
280 .values()
281 .any(|entry| entry.label == label && entry.property == property)
282 })
283 }
284
285 pub(super) fn insert_for(
289 &mut self,
290 label: &str,
291 property: &str,
292 entity_id: u64,
293 vector: &LoraVector,
294 ) {
295 for entry in self.by_name.values_mut() {
296 if entry.label == label && entry.property == property {
297 entry.backend.insert(entity_id, vector.clone());
298 }
299 }
300 }
301
302 pub(super) fn remove_for(&mut self, label: &str, property: &str, entity_id: u64) {
305 for entry in self.by_name.values_mut() {
306 if entry.label == label && entry.property == property {
307 entry.backend.remove(entity_id);
308 }
309 }
310 }
311
312 pub(super) fn query(
320 &self,
321 name: &str,
322 query: &LoraVector,
323 k: usize,
324 restrict_to: Option<&BTreeSet<u64>>,
325 ) -> Option<Vec<(u64, f64)>> {
326 let entry = self.by_name.get(name)?;
327 Some(entry.backend.query(query, entry.similarity, k, restrict_to))
328 }
329
330 pub(super) fn to_snapshots(&self, entity: StoredIndexEntity) -> Vec<VectorIndexSnapshot> {
336 let mut out = Vec::new();
337 for (name, entry) in &self.by_name {
338 if let VectorBackend::Hnsw(b) = &entry.backend {
339 out.push(VectorIndexSnapshot {
340 name: name.clone(),
341 entity,
342 label: entry.label.clone(),
343 property: entry.property.clone(),
344 data: VectorBackendSnapshot::Hnsw(b.to_snapshot(entry.similarity)),
345 });
346 }
347 }
348 out
349 }
350
351 pub(super) fn restore_snapshot(&mut self, snapshot: VectorIndexSnapshot) -> bool {
358 let Some(entry) = self.by_name.get_mut(&snapshot.name) else {
359 return false;
360 };
361 if entry.label != snapshot.label || entry.property != snapshot.property {
362 return false;
363 }
364 match snapshot.data {
365 VectorBackendSnapshot::Hnsw(snap) => {
366 if !matches!(entry.backend, VectorBackend::Hnsw(_)) {
367 return false;
368 }
369 entry.similarity = snap.similarity;
370 entry.backend = VectorBackend::Hnsw(HnswBackend::from_snapshot(snap));
371 true
372 }
373 }
374 }
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
383pub struct VectorIndexSnapshot {
384 pub name: String,
385 pub entity: StoredIndexEntity,
386 pub label: String,
387 pub property: String,
388 pub data: VectorBackendSnapshot,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
392pub enum VectorBackendSnapshot {
393 Hnsw(HnswSnapshot),
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use crate::{RawCoordinate, VectorCoordinateType};
400
401 fn vec(values: &[f32]) -> LoraVector {
402 let coords: Vec<RawCoordinate> = values
403 .iter()
404 .map(|v| RawCoordinate::Float(*v as f64))
405 .collect();
406 LoraVector::try_new(coords, values.len() as i64, VectorCoordinateType::Float32).unwrap()
407 }
408
409 fn register_flat(
410 reg: &mut VectorIndexRegistry,
411 name: &str,
412 label: &str,
413 prop: &str,
414 sim: VectorSimilarity,
415 ) {
416 reg.register(
417 name.into(),
418 label.into(),
419 prop.into(),
420 sim,
421 VectorIndexProvider::Flat,
422 HnswParams::default(),
423 );
424 }
425
426 #[test]
427 fn register_and_query_returns_scores() {
428 let mut reg = VectorIndexRegistry::default();
429 register_flat(&mut reg, "vidx", "V", "e", VectorSimilarity::Cosine);
430 reg.insert_for("V", "e", 1, &vec(&[1.0, 0.0, 0.0]));
431 reg.insert_for("V", "e", 2, &vec(&[0.0, 1.0, 0.0]));
432 let scored = reg.query("vidx", &vec(&[1.0, 0.0, 0.0]), 10, None).unwrap();
433 assert_eq!(scored.len(), 2);
435 let by_id: BTreeMap<u64, f64> = scored.into_iter().collect();
436 assert!((by_id[&1] - 1.0).abs() < 1e-9);
437 assert!(by_id[&2] < by_id[&1]);
438 }
439
440 #[test]
441 fn remove_drops_from_backend() {
442 let mut reg = VectorIndexRegistry::default();
443 register_flat(&mut reg, "vidx", "V", "e", VectorSimilarity::Cosine);
444 reg.insert_for("V", "e", 1, &vec(&[1.0, 0.0]));
445 reg.insert_for("V", "e", 2, &vec(&[0.0, 1.0]));
446 reg.remove_for("V", "e", 1);
447 let scored = reg.query("vidx", &vec(&[1.0, 0.0]), 10, None).unwrap();
448 assert_eq!(scored.len(), 1);
449 assert_eq!(scored[0].0, 2);
450 }
451
452 #[test]
453 fn unrelated_scope_is_skipped() {
454 let mut reg = VectorIndexRegistry::default();
455 register_flat(
456 &mut reg,
457 "movie_emb",
458 "Movie",
459 "embedding",
460 VectorSimilarity::Cosine,
461 );
462 reg.insert_for("Other", "embedding", 99, &vec(&[1.0, 0.0]));
464 let scored = reg.query("movie_emb", &vec(&[1.0, 0.0]), 10, None).unwrap();
465 assert!(scored.is_empty());
466 }
467
468 #[test]
469 fn two_indexes_on_same_scope_with_different_metrics() {
470 let mut reg = VectorIndexRegistry::default();
471 register_flat(&mut reg, "by_cos", "V", "e", VectorSimilarity::Cosine);
472 register_flat(&mut reg, "by_euc", "V", "e", VectorSimilarity::Euclidean);
473 reg.insert_for("V", "e", 1, &vec(&[1.0, 0.0]));
474 reg.insert_for("V", "e", 2, &vec(&[0.0, 1.0]));
475 let cos = reg.query("by_cos", &vec(&[1.0, 0.0]), 10, None).unwrap();
476 let euc = reg.query("by_euc", &vec(&[1.0, 0.0]), 10, None).unwrap();
477 assert_eq!(cos.len(), 2);
478 assert_eq!(euc.len(), 2);
479 for entry in reg.by_name.values() {
481 assert_eq!(entry.backend.len(), 2);
482 }
483 }
484
485 #[test]
486 fn a_registry_clone_shares_vectors_and_diverges_on_write() {
487 for provider in [VectorIndexProvider::Flat, VectorIndexProvider::Hnsw] {
488 let mut reg = VectorIndexRegistry::default();
489 reg.register(
490 "v".into(),
491 "V".into(),
492 "e".into(),
493 VectorSimilarity::Cosine,
494 provider,
495 HnswParams::default(),
496 );
497 for i in 0..300u64 {
498 let x = i as f32 / 300.0;
499 reg.insert_for("V", "e", i, &vec(&[x, 1.0 - x, 0.5]));
500 }
501 let q = vec(&[1.0, 0.0, 0.5]);
502 let top = |r: &VectorIndexRegistry| {
504 let mut hits = r.query("v", &q, 10, None).unwrap();
505 hits.sort_by(|a, b| b.1.total_cmp(&a.1).then(a.0.cmp(&b.0)));
506 hits.truncate(10);
507 hits
508 };
509 let before = top(®);
510
511 let mut staged = reg.clone();
512 staged.insert_for("V", "e", 1_000, &vec(&[1.0, 0.0, 0.5]));
513 staged.remove_for("V", "e", 299);
514 let after = top(&staged);
515 assert_eq!(after[0].0, 1_000, "{provider:?}");
516 assert!(after.iter().all(|(id, _)| *id != 299), "{provider:?}");
517 assert_eq!(top(®), before, "{provider:?}");
518 assert_eq!(before[0].0, 299, "{provider:?}");
519
520 let shared = |r: &VectorIndexRegistry| match &r.by_name["v"].backend {
522 VectorBackend::Flat(b) => Arc::clone(b.items.get(&3).unwrap()),
523 VectorBackend::Hnsw(b) => Arc::clone(&b.nodes.get(&3).unwrap().vector),
524 };
525 assert!(Arc::ptr_eq(&shared(®), &shared(&staged)), "{provider:?}");
526 }
527 }
528
529 #[test]
530 fn hnsw_provider_returns_top_k() {
531 let mut reg = VectorIndexRegistry::default();
532 reg.register(
533 "vh".into(),
534 "V".into(),
535 "e".into(),
536 VectorSimilarity::Cosine,
537 VectorIndexProvider::Hnsw,
538 HnswParams::default(),
539 );
540 for i in 0..50u64 {
541 let v = vec(&[(i as f32) / 50.0, 1.0 - (i as f32) / 50.0]);
542 reg.insert_for("V", "e", i, &v);
543 }
544 let hits = reg.query("vh", &vec(&[1.0, 0.0]), 5, None).unwrap();
545 assert_eq!(hits.len(), 5);
546 let ids: Vec<u64> = hits.iter().map(|(id, _)| *id).collect();
548 assert!(ids.contains(&49) || ids.contains(&48), "got {ids:?}");
549 }
550}