1#![allow(clippy::pedantic)] #![allow(clippy::nursery)] #![allow(clippy::doc_markdown)] #![allow(clippy::uninlined_format_args)] #![allow(clippy::manual_let_else)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::ref_option)] #![allow(clippy::match_same_arms)] #![allow(clippy::trivially_copy_pass_by_ref)] #![allow(clippy::map_unwrap_or)] #![allow(clippy::enum_glob_use)] #![allow(clippy::unused_async)] #![allow(clippy::needless_for_each)] pub mod auth;
28pub mod config;
29mod handlers;
30pub mod tls;
31mod types;
32
33use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
34use utoipa::OpenApi;
35use velesdb_core::guardrails::QueryLimits;
36use velesdb_core::Database;
37
38pub use types::*;
40
41pub use handlers::{
43 aggregate, analyze_collection, batch_search, collection_sanity, create_collection,
44 create_index, delete_collection, delete_index, delete_point, explain, flush_collection,
45 get_collection, get_collection_config, get_collection_stats, get_guardrails, get_point,
46 health_check, hybrid_search, is_empty, list_collections, list_indexes, match_query,
47 multi_query_search, query, readiness_check, search, search_ids, stream_insert,
48 stream_upsert_points, text_search, update_guardrails, upsert_points,
49};
50
51pub use handlers::graph::{
53 add_edge, get_edges, get_node_degree, stream_traverse, traverse_graph, DegreeResponse,
54 StreamDoneEvent, StreamNodeEvent, StreamStatsEvent, StreamTraverseParams, TraversalResultItem,
55 TraversalStats, TraverseRequest, TraverseResponse,
56};
57
58#[cfg(feature = "prometheus")]
60pub use handlers::metrics::{health_metrics, prometheus_metrics};
61
62#[derive(OpenApi)]
68#[openapi(
69 info(
70 title = "VelesDB API",
71 version = env!("CARGO_PKG_VERSION"),
72 description = "High-performance vector database for AI applications. \
73 Supports semantic search, HNSW indexing, and multiple distance metrics. \
74 Authentication is optional — when API keys are configured via VELESDB_API_KEYS, \
75 all endpoints except /health and /ready require a valid Bearer token.",
76 license(name = "VelesDB Core License 1.0", url = "https://github.com/cyberlife-coder/VelesDB/blob/main/LICENSE"),
77 contact(name = "VelesDB Team", url = "https://github.com/cyberlife-coder/VelesDB")
78 ),
79 security(
80 ("bearer_auth" = [])
81 ),
82 modifiers(&SecurityAddon),
83 servers(
84 (url = "/", description = "Local server")
85 ),
86 tags(
87 (name = "health", description = "Health check endpoints"),
88 (name = "collections", description = "Collection management"),
89 (name = "points", description = "Vector point operations"),
90 (name = "search", description = "Vector similarity search"),
91 (name = "query", description = "VelesQL query execution"),
92 (name = "indexes", description = "Property index management (EPIC-009)"),
93 (name = "graph", description = "Graph traversal and edge operations"),
94 (name = "guardrails", description = "Query guard-rails configuration (EPIC-048)")
95 ),
96 paths(
97 handlers::health::health_check,
98 handlers::health::readiness_check,
99 handlers::collections::list_collections,
100 handlers::collections::create_collection,
101 handlers::collections::get_collection,
102 handlers::collections::delete_collection,
103 handlers::collections::collection_sanity,
104 handlers::collections::is_empty,
105 handlers::collections::flush_collection,
106 handlers::admin::analyze_collection,
107 handlers::admin::get_collection_stats,
108 handlers::admin::get_guardrails,
109 handlers::admin::update_guardrails,
110 handlers::points::upsert_points,
111 handlers::points::stream_upsert_points,
112 handlers::points::stream_insert,
113 handlers::points::get_point,
114 handlers::points::delete_point,
115 handlers::search::search,
116 handlers::search::batch_search,
117 handlers::search::multi_query_search,
118 handlers::search::text_search,
119 handlers::search::hybrid_search,
120 handlers::search::search_ids,
121 handlers::admin::get_collection_config,
122 handlers::query::query,
123 handlers::query::aggregate,
124 handlers::query::explain,
125 handlers::indexes::create_index,
126 handlers::indexes::list_indexes,
127 handlers::indexes::delete_index,
128 handlers::graph::handlers::get_edges,
129 handlers::graph::handlers::add_edge,
130 handlers::graph::handlers::traverse_graph,
131 handlers::graph::handlers::get_node_degree,
132 handlers::graph::stream::stream_traverse,
133 handlers::match_query::match_query
134 ),
135 components(
136 schemas(
137 CreateCollectionRequest,
138 CollectionResponse,
139 UpsertPointsRequest,
140 PointRequest,
141 StreamInsertRequest,
142 SearchRequest,
143 BatchSearchRequest,
144 TextSearchRequest,
145 HybridSearchRequest,
146 MultiQuerySearchRequest,
147 SearchResponse,
148 BatchSearchResponse,
149 SearchResultResponse,
150 SearchIdsResponse,
151 IdScoreResult,
152 CollectionConfigResponse,
153 ErrorResponse,
154 QueryRequest,
155 QueryResponse,
156 QueryResponseMeta,
157 AggregationResponse,
158 QueryErrorResponse,
159 QueryErrorDetail,
160 VelesqlErrorResponse,
161 VelesqlErrorDetail,
162 ExplainRequest,
163 ExplainResponse,
164 ExplainStep,
165 ExplainCost,
166 ExplainFeatures,
167 CreateIndexRequest,
168 IndexResponse,
169 ListIndexesResponse,
170 CollectionStatsResponse,
171 ColumnStatsResponse,
172 IndexStatsResponse,
173 GuardRailsConfigRequest,
174 GuardRailsConfigResponse,
175 handlers::graph::TraverseRequest,
176 handlers::graph::TraverseResponse,
177 handlers::graph::TraversalResultItem,
178 handlers::graph::TraversalStats,
179 handlers::graph::DegreeResponse,
180 handlers::graph::AddEdgeRequest,
181 handlers::graph::EdgesResponse,
182 handlers::graph::EdgeResponse,
183 handlers::graph::StreamNodeEvent,
184 handlers::graph::StreamStatsEvent,
185 handlers::graph::StreamDoneEvent,
186 handlers::match_query::MatchQueryRequest,
187 handlers::match_query::MatchQueryResponse,
188 handlers::match_query::MatchQueryResultItem,
189 handlers::match_query::MatchQueryMeta,
190 handlers::match_query::MatchQueryError
191 )
192 )
193)]
194pub struct ApiDoc;
195
196struct SecurityAddon;
198
199impl utoipa::Modify for SecurityAddon {
200 fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
201 if let Some(components) = openapi.components.as_mut() {
202 components.add_security_scheme(
203 "bearer_auth",
204 utoipa::openapi::security::SecurityScheme::Http(
205 utoipa::openapi::security::Http::new(
206 utoipa::openapi::security::HttpAuthScheme::Bearer,
207 ),
208 ),
209 );
210 }
211 }
212}
213
214pub struct AppState {
220 pub db: Database,
222 pub onboarding_metrics: OnboardingMetrics,
224 pub query_limits: parking_lot::RwLock<QueryLimits>,
226 pub ready: AtomicBool,
228}
229
230#[derive(Default)]
232pub struct OnboardingMetrics {
233 pub search_requests_total: AtomicU64,
234 pub dimension_mismatch_total: AtomicU64,
235 pub empty_search_results_total: AtomicU64,
236 pub filter_parse_errors_total: AtomicU64,
237}
238
239impl OnboardingMetrics {
240 pub fn record_search_request(&self) {
241 self.search_requests_total.fetch_add(1, Ordering::Relaxed);
242 }
243
244 pub fn record_dimension_mismatch(&self) {
245 self.dimension_mismatch_total
246 .fetch_add(1, Ordering::Relaxed);
247 }
248
249 pub fn record_empty_search_results(&self) {
250 self.empty_search_results_total
251 .fetch_add(1, Ordering::Relaxed);
252 }
253
254 pub fn record_filter_parse_error(&self) {
255 self.filter_parse_errors_total
256 .fetch_add(1, Ordering::Relaxed);
257 }
258}
259
260#[cfg(test)]
265mod tests {
266 use super::*;
267 use utoipa::OpenApi;
268
269 #[test]
270 fn test_openapi_spec_generation() {
271 let openapi = ApiDoc::openapi();
272 let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
273 assert!(!json.is_empty(), "OpenAPI spec should not be empty");
274 assert!(json.contains("VelesDB API"), "Should contain API title");
275 assert!(
276 json.contains(env!("CARGO_PKG_VERSION")),
277 "Should contain version"
278 );
279 }
280
281 #[test]
282 fn test_openapi_has_all_endpoints() {
283 let openapi = ApiDoc::openapi();
284 let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
285 assert!(json.contains("/health"), "Should document /health");
286 assert!(
287 json.contains("/collections"),
288 "Should document /collections"
289 );
290 assert!(
291 json.contains(r"/collections/{name}"),
292 "Should document collections by name"
293 );
294 assert!(json.contains("/points"), "Should document points endpoint");
295 assert!(
296 json.contains(r"/collections/{name}/points/stream"),
297 "Should document points stream endpoint"
298 );
299 assert!(json.contains("/search"), "Should document search endpoint");
300 assert!(json.contains("/query"), "Should document /query");
301 assert!(json.contains("/aggregate"), "Should document /aggregate");
302 assert!(
303 json.contains("/query/explain"),
304 "Should document /query/explain"
305 );
306 }
307
308 #[test]
309 fn test_openapi_has_all_tags() {
310 let openapi = ApiDoc::openapi();
311 let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
312 assert!(json.contains("\"health\""), "Should have health tag");
313 assert!(
314 json.contains("\"collections\""),
315 "Should have collections tag"
316 );
317 assert!(json.contains("\"points\""), "Should have points tag");
318 assert!(json.contains("\"search\""), "Should have search tag");
319 assert!(json.contains("\"query\""), "Should have query tag");
320 }
321
322 #[test]
323 fn test_openapi_has_schemas() {
324 let openapi = ApiDoc::openapi();
325 let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
326 assert!(
327 json.contains("CreateCollectionRequest"),
328 "Should have CreateCollectionRequest schema"
329 );
330 assert!(
331 json.contains("CollectionResponse"),
332 "Should have CollectionResponse schema"
333 );
334 assert!(
335 json.contains("SearchRequest"),
336 "Should have SearchRequest schema"
337 );
338 assert!(
339 json.contains("SearchResponse"),
340 "Should have SearchResponse schema"
341 );
342 assert!(
343 json.contains("ErrorResponse"),
344 "Should have ErrorResponse schema"
345 );
346 }
347
348 #[test]
349 fn generate_openapi_spec_files() {
350 let openapi = ApiDoc::openapi();
351 let json = openapi
352 .to_pretty_json()
353 .expect("Failed to serialize OpenAPI JSON");
354 let yaml = serde_yaml::to_string(&openapi).expect("Failed to serialize OpenAPI YAML");
355
356 let docs_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
358 .parent()
359 .unwrap()
360 .parent()
361 .unwrap()
362 .join("docs");
363 std::fs::create_dir_all(&docs_dir).expect("Failed to create docs dir");
364
365 std::fs::write(docs_dir.join("openapi.json"), &json).expect("Failed to write openapi.json");
366 std::fs::write(docs_dir.join("openapi.yaml"), &yaml).expect("Failed to write openapi.yaml");
367
368 assert!(
370 json.contains("sparse"),
371 "OpenAPI spec should contain sparse endpoints"
372 );
373 assert!(
374 json.contains("/graph/edges"),
375 "Should contain graph edge endpoints"
376 );
377 assert!(
378 json.contains("/graph/traverse"),
379 "Should contain graph traverse endpoint"
380 );
381 assert!(
382 json.contains("/stream/insert"),
383 "Should contain stream insert endpoint"
384 );
385 assert!(
386 json.contains("/match"),
387 "Should contain match query endpoint"
388 );
389 assert!(
390 json.contains("/search/multi"),
391 "Should contain multi-query search endpoint"
392 );
393 }
394
395 #[test]
396 fn test_openapi_has_license() {
397 let openapi = ApiDoc::openapi();
398 let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
399 assert!(
400 json.contains("VelesDB Core License 1.0"),
401 "Should have VelesDB Core License 1.0"
402 );
403 }
404
405 #[test]
406 fn test_openapi_pretty_json() {
407 let openapi = ApiDoc::openapi();
408 let pretty_json = openapi
409 .to_pretty_json()
410 .expect("Failed to serialize pretty JSON");
411 assert!(
412 pretty_json.contains('\n'),
413 "Pretty JSON should have newlines"
414 );
415 assert!(
416 pretty_json.len() > 1000,
417 "OpenAPI spec should be substantial"
418 );
419 }
420
421 #[test]
422 fn test_openapi_has_all_metrics_documented() {
423 let openapi = ApiDoc::openapi();
424 let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
425 assert!(json.contains("cosine"), "Should document cosine metric");
426 assert!(
427 json.contains("euclidean"),
428 "Should document euclidean metric"
429 );
430 assert!(json.contains("dot"), "Should document dot product metric");
431 assert!(json.contains("hamming"), "Should document hamming metric");
432 assert!(json.contains("jaccard"), "Should document jaccard metric");
433 }
434
435 #[test]
436 fn test_openapi_has_storage_mode_documented() {
437 let openapi = ApiDoc::openapi();
438 let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
439 assert!(
440 json.contains("storage_mode"),
441 "Should document storage_mode parameter"
442 );
443 }
444
445 #[test]
446 fn test_openapi_has_search_types_documented() {
447 let openapi = ApiDoc::openapi();
448 let json = openapi.to_json().expect("Failed to serialize OpenAPI spec");
449 assert!(json.contains("text_search"), "Should document text search");
450 assert!(
451 json.contains("hybrid_search"),
452 "Should document hybrid search"
453 );
454 assert!(json.contains("batch"), "Should document batch search");
455 }
456
457 #[test]
458 fn test_create_collection_request_default_metric() {
459 let json = r#"{"name": "test", "dimension": 128}"#;
460 let req: CreateCollectionRequest = serde_json::from_str(json).unwrap();
461 assert_eq!(req.metric, "cosine");
462 }
463
464 #[test]
465 fn test_create_collection_request_with_hamming() {
466 let json = r#"{"name": "test", "dimension": 128, "metric": "hamming"}"#;
467 let req: CreateCollectionRequest = serde_json::from_str(json).unwrap();
468 assert_eq!(req.metric, "hamming");
469 }
470
471 #[test]
472 fn test_create_collection_request_with_jaccard() {
473 let json = r#"{"name": "test", "dimension": 128, "metric": "jaccard"}"#;
474 let req: CreateCollectionRequest = serde_json::from_str(json).unwrap();
475 assert_eq!(req.metric, "jaccard");
476 }
477
478 #[test]
479 fn test_create_collection_request_with_storage_mode() {
480 let json = r#"{"name": "test", "dimension": 128, "storage_mode": "sq8"}"#;
481 let req: CreateCollectionRequest = serde_json::from_str(json).unwrap();
482 assert_eq!(req.storage_mode, "sq8");
483 }
484
485 #[test]
486 fn test_search_request_deserialize() {
487 let json = r#"{"vector": [0.1, 0.2, 0.3], "top_k": 5}"#;
488 let req: SearchRequest = serde_json::from_str(json).unwrap();
489 assert_eq!(req.vector, vec![0.1, 0.2, 0.3]);
490 assert_eq!(req.top_k, 5);
491 }
492
493 #[test]
494 fn test_batch_search_request_deserialize() {
495 let json = r#"{"searches": [{"vector": [0.1, 0.2], "top_k": 3}]}"#;
496 let req: BatchSearchRequest = serde_json::from_str(json).unwrap();
497 assert_eq!(req.searches.len(), 1);
498 assert_eq!(req.searches[0].top_k, 3);
499 }
500
501 #[test]
502 fn test_text_search_request_deserialize() {
503 let json = r#"{"query": "machine learning", "top_k": 10}"#;
504 let req: TextSearchRequest = serde_json::from_str(json).unwrap();
505 assert_eq!(req.query, "machine learning");
506 assert_eq!(req.top_k, 10);
507 }
508
509 #[test]
510 fn test_hybrid_search_request_deserialize() {
511 let json = r#"{"vector": [0.1, 0.2], "query": "test", "top_k": 5}"#;
512 let req: HybridSearchRequest = serde_json::from_str(json).unwrap();
513 assert_eq!(req.vector, vec![0.1, 0.2]);
514 assert_eq!(req.query, "test");
515 assert_eq!(req.top_k, 5);
516 }
517
518 #[test]
519 fn test_upsert_points_request_deserialize() {
520 let json = r#"{"points": [{"id": 1, "vector": [0.1, 0.2]}]}"#;
521 let req: UpsertPointsRequest = serde_json::from_str(json).unwrap();
522 assert_eq!(req.points.len(), 1);
523 assert_eq!(req.points[0].id, 1);
524 }
525
526 #[test]
527 fn test_collection_response_serialize() {
528 let resp = CollectionResponse {
529 name: "test".to_string(),
530 dimension: 128,
531 metric: "cosine".to_string(),
532 storage_mode: "full".to_string(),
533 point_count: 100,
534 };
535 let json = serde_json::to_string(&resp).unwrap();
536 assert!(json.contains("\"name\":\"test\""));
537 assert!(json.contains("\"dimension\":128"));
538 assert!(json.contains("\"metric\":\"cosine\""));
539 assert!(json.contains("\"storage_mode\":\"full\""));
540 assert!(json.contains("\"point_count\":100"));
541 }
542
543 #[test]
544 fn test_search_response_serialize() {
545 let resp = SearchResponse {
546 results: vec![SearchResultResponse {
547 id: 1,
548 score: 0.95,
549 payload: None,
550 }],
551 };
552 let json = serde_json::to_string(&resp).unwrap();
553 assert!(json.contains("\"results\""));
554 assert!(json.contains("\"id\":1"));
555 }
556
557 #[test]
558 fn test_error_response_serialize() {
559 let resp = ErrorResponse {
560 error: "Test error".to_string(),
561 };
562 let json = serde_json::to_string(&resp).unwrap();
563 assert!(json.contains("\"error\":\"Test error\""));
564 }
565}