1use thiserror::Error;
2
3#[derive(Error, Debug, Clone)]
4pub enum VectorError {
5 #[error("Vector index not found: {0}")]
6 IndexNotFound(String),
7
8 #[error("Vector index already exists: {0}")]
9 IndexAlreadyExists(String),
10
11 #[error("Vector engine not found for space {space_id}, tag {tag_name}, field {field_name}")]
12 EngineNotFound {
13 space_id: u64,
14 tag_name: String,
15 field_name: String,
16 },
17
18 #[error("Vector engine unavailable: {0}")]
19 EngineUnavailable(String),
20
21 #[error("Vector index corrupted: {0}")]
22 IndexCorrupted(String),
23
24 #[error("Qdrant engine error: {0}")]
25 QdrantError(String),
26
27 #[error("Vector dimension mismatch: expected {expected}, got {actual}")]
28 DimensionMismatch { expected: usize, actual: usize },
29
30 #[error("Invalid vector: {0}")]
31 InvalidVector(String),
32
33 #[error("Invalid point ID: {0}")]
34 InvalidPointId(String),
35
36 #[error("Vector index configuration error: {0}")]
37 ConfigError(String),
38
39 #[error("Vector search timeout")]
40 Timeout,
41
42 #[error("Vector index is locked: {0}")]
43 Locked(String),
44
45 #[error("Vector operation cancelled")]
46 Cancelled,
47
48 #[error("Embedding service error: {0}")]
49 EmbeddingError(String),
50
51 #[error("Connection failed: {0}")]
52 ConnectionFailed(String),
53
54 #[error("Collection not found: {0}")]
55 CollectionNotFound(String),
56
57 #[error("Internal error: {0}")]
58 Internal(String),
59}
60
61#[derive(Error, Debug, Clone)]
62pub enum VectorCoordinatorError {
63 #[error("Vector index error: {0}")]
64 Vector(#[from] VectorError),
65
66 #[error("Sync error: {0}")]
67 Sync(String),
68
69 #[error("Vector index creation failed for {tag_name}.{field_name}: {reason}")]
70 IndexCreationFailed {
71 tag_name: String,
72 field_name: String,
73 reason: String,
74 },
75
76 #[error("Vector index drop failed for {tag_name}.{field_name}: {reason}")]
77 IndexDropFailed {
78 tag_name: String,
79 field_name: String,
80 reason: String,
81 },
82
83 #[error("Vector index rebuild failed: {0}")]
84 IndexRebuildFailed(String),
85
86 #[error("Vector rebuild busy: {0}")]
87 RebuildBusy(String),
88
89 #[error("Vertex change processing failed: {0}")]
90 VertexChangeFailed(String),
91
92 #[error("Space not found: {0}")]
93 SpaceNotFound(u64),
94
95 #[error("Tag not found: {0}")]
96 TagNotFound(String),
97
98 #[error("Field not vector indexed: {tag_name}.{field_name}")]
99 FieldNotIndexed {
100 tag_name: String,
101 field_name: String,
102 },
103
104 #[error("Vector coordinator not initialized")]
105 NotInitialized,
106
107 #[error("Vector coordinator is shutting down")]
108 ShuttingDown,
109
110 #[error(
111 "Vector engine is disabled by configuration; \
112 enable the vector engine (or check VectorEngineState) before issuing vector operations"
113 )]
114 EngineDisabled,
115
116 #[error("Invalid operation: {0}")]
117 InvalidOperation(String),
118
119 #[error("Embedding service not available")]
120 EmbeddingServiceNotAvailable,
121
122 #[error("Embedding error: {0}")]
123 EmbeddingError(String),
124
125 #[error("Collection config conflict for {collection_name}: existing {existing_size}/{existing_dist}, requested {requested_size}/{requested_dist}")]
126 CollectionConfigConflict {
127 collection_name: String,
128 existing_size: usize,
129 existing_dist: String,
130 requested_size: usize,
131 requested_dist: String,
132 },
133
134 #[error("Internal error: {0}")]
135 Internal(String),
136
137 #[error("Buffer error: {0}")]
138 BufferError(String),
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum VectorErrorKind {
144 Retryable,
146 NonRetryable,
148 Auth,
150}
151
152impl VectorErrorKind {
153 pub fn classify(err: &VectorError) -> Self {
155 match err {
156 VectorError::DimensionMismatch { .. }
157 | VectorError::InvalidVector(_)
158 | VectorError::InvalidPointId(_)
159 | VectorError::ConfigError(_)
160 | VectorError::IndexAlreadyExists(_)
161 | VectorError::CollectionNotFound(_)
162 | VectorError::IndexNotFound(_) => Self::NonRetryable,
163 VectorError::ConnectionFailed(_)
164 | VectorError::EngineUnavailable(_)
165 | VectorError::Timeout
166 | VectorError::QdrantError(_)
167 | VectorError::IndexCorrupted(_)
168 | VectorError::Locked(_)
169 | VectorError::Internal(_) => Self::Retryable,
170 VectorError::Cancelled
171 | VectorError::EmbeddingError(_)
172 | VectorError::EngineNotFound { .. } => Self::NonRetryable,
173 }
174 }
175
176 pub fn classify_str(msg: &str) -> Self {
179 let lower = msg.to_ascii_lowercase();
180 if lower.contains("dimensionmismatch")
181 || lower.contains("dimension mismatch")
182 || lower.contains("invalidconfig")
183 || lower.contains("invalid vector")
184 || lower.contains("invalid point id")
185 || lower.contains("invalidargument")
186 || lower.contains("configerror")
187 {
188 return Self::NonRetryable;
189 }
190 if lower.contains("auth")
191 || lower.contains("unauthorized")
192 || lower.contains("forbidden")
193 || lower.contains("permission")
194 || lower.contains("apikey")
195 || lower.contains("api key")
196 {
197 return Self::Auth;
198 }
199 if lower.contains("engine is disabled") || lower.contains("enginedisabled") {
200 return Self::Retryable;
201 }
202 if lower.contains("timeout")
203 || lower.contains("unavailable")
204 || lower.contains("connection")
205 || lower.contains("transport")
206 || lower.contains("deadline")
207 || lower.contains("temporarily")
208 || lower.contains("rate limit")
209 || lower.contains("qdrant")
210 {
211 return Self::Retryable;
212 }
213 if lower.contains("not found") || lower.contains("already exists") {
218 return Self::NonRetryable;
219 }
220 Self::Retryable
221 }
222}
223
224impl VectorError {
225 pub fn kind(&self) -> VectorErrorKind {
227 VectorErrorKind::classify(self)
228 }
229}
230
231impl VectorCoordinatorError {
232 pub fn kind(&self) -> VectorErrorKind {
234 match self {
235 VectorCoordinatorError::Vector(err) => VectorErrorKind::classify(err),
236 VectorCoordinatorError::EngineDisabled => VectorErrorKind::Retryable,
237 VectorCoordinatorError::CollectionConfigConflict { .. }
238 | VectorCoordinatorError::FieldNotIndexed { .. }
239 | VectorCoordinatorError::InvalidOperation(_)
240 | VectorCoordinatorError::EmbeddingServiceNotAvailable
241 | VectorCoordinatorError::EmbeddingError(_)
242 | VectorCoordinatorError::NotInitialized
243 | VectorCoordinatorError::ShuttingDown
244 | VectorCoordinatorError::BufferError(_) => VectorErrorKind::NonRetryable,
245 VectorCoordinatorError::IndexCreationFailed { .. }
246 | VectorCoordinatorError::IndexDropFailed { .. }
247 | VectorCoordinatorError::IndexRebuildFailed(_)
248 | VectorCoordinatorError::RebuildBusy(_)
249 | VectorCoordinatorError::VertexChangeFailed(_)
250 | VectorCoordinatorError::SpaceNotFound(_)
251 | VectorCoordinatorError::TagNotFound(_)
252 | VectorCoordinatorError::Sync(_)
253 | VectorCoordinatorError::Internal(_) => VectorErrorKind::Retryable,
254 }
255 }
256
257 pub fn kind_str(msg: &str) -> VectorErrorKind {
259 VectorErrorKind::classify_str(msg)
260 }
261}
262
263pub type VectorResult<T> = std::result::Result<T, VectorError>;
264pub type VectorCoordinatorResult<T> = std::result::Result<T, VectorCoordinatorError>;
265
266#[cfg(feature = "vector")]
267impl From<simvec::VectorSearchError> for VectorError {
268 fn from(err: simvec::VectorSearchError) -> Self {
269 match err {
270 simvec::VectorSearchError::CollectionNotFound(name) => VectorError::IndexNotFound(name),
271 simvec::VectorSearchError::CollectionAlreadyExists(name) => {
272 VectorError::IndexAlreadyExists(name)
273 }
274 simvec::VectorSearchError::CollectionIncomplete { dir, file } => {
275 VectorError::IndexCorrupted(format!(
276 "collection incomplete (missing {}): {}",
277 file,
278 dir.display()
279 ))
280 }
281 simvec::VectorSearchError::InvalidCollectionName(name) => {
282 VectorError::ConfigError(format!("Invalid collection name: {}", name))
283 }
284 simvec::VectorSearchError::InvalidConfig(msg) => VectorError::ConfigError(msg),
285 simvec::VectorSearchError::InsufficientSpace {
286 operation,
287 needed,
288 available,
289 } => VectorError::ConfigError(format!(
290 "insufficient space for {}: needs {} bytes, {} available",
291 operation, needed, available
292 )),
293 simvec::VectorSearchError::CapacityExceeded {
294 collection,
295 max_points,
296 requested,
297 } => VectorError::ConfigError(format!(
298 "collection {} capacity exceeded: limit {}, requested total {}",
299 collection, max_points, requested
300 )),
301 simvec::VectorSearchError::InvalidVectorDimension { expected, actual } => {
302 VectorError::DimensionMismatch { expected, actual }
303 }
304 simvec::VectorSearchError::InvalidPointId(id) => VectorError::InvalidPointId(id),
305 simvec::VectorSearchError::NonFiniteElement(index) => {
306 VectorError::InvalidVector(format!("non-finite element at index {}", index))
307 }
308 simvec::VectorSearchError::UnsupportedMetric(metric) => {
309 VectorError::ConfigError(format!("metric not supported: {:?}", metric))
310 }
311 simvec::VectorSearchError::Filter(msg) => VectorError::InvalidVector(msg),
312 simvec::VectorSearchError::CorruptData(msg) => VectorError::IndexCorrupted(msg),
313 simvec::VectorSearchError::Io(e) => VectorError::Internal(e.to_string()),
314 simvec::VectorSearchError::Serialization(e) => VectorError::Internal(e.to_string()),
315 simvec::VectorSearchError::Json(e) => VectorError::Internal(e.to_string()),
316 simvec::VectorSearchError::Internal(msg) => VectorError::Internal(msg),
317 }
318 }
319}
320
321#[cfg(feature = "vector")]
322impl From<simvec::VectorSearchError> for VectorCoordinatorError {
323 fn from(err: simvec::VectorSearchError) -> Self {
324 VectorCoordinatorError::Vector(VectorError::from(err))
325 }
326}
327
328#[cfg(feature = "vector")]
329impl From<simvec::VectorEngineError> for VectorCoordinatorError {
330 fn from(err: simvec::VectorEngineError) -> Self {
331 match err {
332 simvec::VectorEngineError::Local(msg) => {
333 VectorCoordinatorError::Vector(VectorError::Internal(msg))
334 }
335 simvec::VectorEngineError::Remote(msg) => {
336 VectorCoordinatorError::Vector(VectorError::QdrantError(msg))
337 }
338 simvec::VectorEngineError::Internal(msg) => {
339 VectorCoordinatorError::Vector(VectorError::Internal(msg))
340 }
341 simvec::VectorEngineError::NotSupported(op) => VectorCoordinatorError::Vector(
342 VectorError::ConfigError(format!("Operation not supported: {}", op)),
343 ),
344 }
345 }
346}
347
348#[cfg(feature = "vector-qdrant")]
349impl From<vector_client::VectorClientError> for VectorError {
350 fn from(err: vector_client::VectorClientError) -> Self {
351 match err {
352 vector_client::VectorClientError::ConnectionFailed(msg) => {
353 VectorError::ConnectionFailed(msg)
354 }
355 vector_client::VectorClientError::CollectionNotFound(name) => {
356 VectorError::CollectionNotFound(name)
357 }
358 vector_client::VectorClientError::CollectionAlreadyExists(name) => {
359 VectorError::IndexAlreadyExists(name)
360 }
361 vector_client::VectorClientError::PointNotFound(id, _collection) => {
362 VectorError::IndexNotFound(id)
363 }
364 vector_client::VectorClientError::InvalidVectorDimension { expected, actual } => {
365 VectorError::DimensionMismatch { expected, actual }
366 }
367 vector_client::VectorClientError::InvalidCollectionName(name) => {
368 VectorError::ConfigError(format!("Invalid collection name: {}", name))
369 }
370 vector_client::VectorClientError::InvalidPointId(id) => VectorError::InvalidPointId(id),
371 vector_client::VectorClientError::Timeout(_msg) => VectorError::Timeout,
372 vector_client::VectorClientError::InvalidConfig(msg) => VectorError::ConfigError(msg),
373 vector_client::VectorClientError::SearchError(msg) => VectorError::QdrantError(msg),
374 vector_client::VectorClientError::UpsertError(msg) => VectorError::QdrantError(msg),
375 vector_client::VectorClientError::DeleteError(msg) => VectorError::QdrantError(msg),
376 vector_client::VectorClientError::PayloadError(msg) => VectorError::QdrantError(msg),
377 vector_client::VectorClientError::FilterError(msg) => VectorError::InvalidVector(msg),
378 vector_client::VectorClientError::HealthCheckFailed(msg) => {
379 VectorError::ConnectionFailed(msg)
380 }
381 vector_client::VectorClientError::EngineNotInitialized => {
382 VectorError::EngineUnavailable("Engine not initialized".to_string())
383 }
384 vector_client::VectorClientError::EngineNotAvailable(name) => {
385 VectorError::EngineUnavailable(format!("Engine {} not available", name))
386 }
387 vector_client::VectorClientError::IndexAlreadyExists(name) => {
388 VectorError::IndexAlreadyExists(name)
389 }
390 vector_client::VectorClientError::IoError(e) => VectorError::Internal(e.to_string()),
391 vector_client::VectorClientError::SerializationError(e) => {
392 VectorError::Internal(e.to_string())
393 }
394 vector_client::VectorClientError::InternalError(msg) => VectorError::Internal(msg),
395 vector_client::VectorClientError::QdrantHttpError { status, message } => {
396 VectorError::QdrantError(format!("HTTP {}: {}", status, message))
397 }
398 vector_client::VectorClientError::QdrantGrpcError(msg) => VectorError::QdrantError(msg),
399 vector_client::VectorClientError::NotSupported(op) => {
400 VectorError::ConfigError(format!("Operation not supported: {}", op))
401 }
402 }
403 }
404}
405
406#[cfg(feature = "vector-qdrant")]
407impl From<vector_client::VectorClientError> for VectorCoordinatorError {
408 fn from(err: vector_client::VectorClientError) -> Self {
409 VectorCoordinatorError::Vector(VectorError::from(err))
410 }
411}