1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Unified vector search backend trait.
//!
//! Provides a common interface for HNSW, DiskANN, and hybrid vector
//! search backends. The kernel creates the appropriate backend at boot
//! time based on [`VectorConfig`](clawft_types::config::VectorConfig).
//!
//! This module is compiled only when the `ecc` feature is enabled.
use serde::{Deserialize, Serialize};
// ── Search result ────────────────────────────────────────────────────────
/// A single search result returned by any [`VectorBackend`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
/// Entry identifier (numeric, for fast dedup/sorting).
pub id: u64,
/// String key originally associated with this vector (e.g., node id).
pub key: String,
/// Distance from the query vector (lower is closer).
pub distance: f32,
/// Arbitrary metadata stored alongside the embedding.
#[serde(default)]
pub metadata: serde_json::Value,
}
impl SearchResult {
/// Create a new search result.
pub fn new(id: u64, key: String, distance: f32, metadata: serde_json::Value) -> Self {
Self {
id,
key,
distance,
metadata,
}
}
}
// ── Error ────────────────────────────────────────────────────────────────
/// Errors that can occur in vector backend operations.
#[derive(Debug, thiserror::Error)]
pub enum VectorError {
/// The vector dimensions do not match the expected dimensionality.
#[error("dimension mismatch: expected {expected}, got {got}")]
DimensionMismatch { expected: usize, got: usize },
/// I/O error during persistence operations.
#[error("io error: {0}")]
Io(#[from] std::io::Error),
/// The backend capacity has been exceeded.
#[error("capacity exceeded: max {max} entries")]
CapacityExceeded { max: usize },
/// Generic backend error.
#[error("{0}")]
Other(String),
}
/// Result alias for vector backend operations.
pub type VectorResult<T> = Result<T, VectorError>;
// ── Trait ────────────────────────────────────────────────────────────────
/// Unified vector search backend interface.
///
/// Implementations must be `Send + Sync` so they can be shared across
/// async tasks via `Arc`.
pub trait VectorBackend: Send + Sync {
/// Insert a vector with the given numeric ID, string key, and metadata.
///
/// If a vector with the same `id` already exists, it is replaced
/// (upsert semantics).
fn insert(
&self,
id: u64,
key: &str,
vector: &[f32],
metadata: serde_json::Value,
) -> VectorResult<()>;
/// Search for the `k` nearest vectors to `query`.
///
/// Results are sorted by ascending distance (closest first).
fn search(&self, query: &[f32], k: usize) -> Vec<SearchResult>;
/// Return the number of vectors currently stored.
fn len(&self) -> usize;
/// Return `true` if the store contains no vectors.
fn is_empty(&self) -> bool {
self.len() == 0
}
/// Check whether a vector with the given ID exists.
fn contains(&self, id: u64) -> bool;
/// Remove a vector by ID. Returns `true` if it was present.
fn remove(&self, id: u64) -> bool;
/// Persist any in-memory state to durable storage.
///
/// For purely in-memory backends (HNSW), this is a no-op.
fn flush(&self) -> VectorResult<()>;
/// Return a human-readable name for this backend (e.g. "hnsw", "diskann", "hybrid").
fn backend_name(&self) -> &str;
}
// ── Backend configuration dispatch ───────────────────────────────────────
/// Which backend to construct, read from configuration.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum VectorBackendKind {
/// In-memory HNSW (default, fast, suitable for <1M vectors).
#[default]
Hnsw,
/// SSD-backed DiskANN (large scale, 1M+ vectors).
DiskAnn,
/// Hot HNSW cache + cold DiskANN store.
Hybrid,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn search_result_new() {
let sr = SearchResult::new(1, "foo".into(), 0.5, serde_json::json!({"a": 1}));
assert_eq!(sr.id, 1);
assert_eq!(sr.key, "foo");
assert!((sr.distance - 0.5).abs() < f32::EPSILON);
}
#[test]
fn backend_kind_default_is_hnsw() {
assert_eq!(VectorBackendKind::default(), VectorBackendKind::Hnsw);
}
#[test]
fn backend_kind_deserialize() {
let kind: VectorBackendKind = serde_json::from_str(r#""hnsw""#).unwrap();
assert_eq!(kind, VectorBackendKind::Hnsw);
let kind: VectorBackendKind = serde_json::from_str(r#""diskann""#).unwrap();
assert_eq!(kind, VectorBackendKind::DiskAnn);
let kind: VectorBackendKind = serde_json::from_str(r#""hybrid""#).unwrap();
assert_eq!(kind, VectorBackendKind::Hybrid);
}
}