use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use std::sync::Arc;
use crate::types::{CollectionResponse, CreateCollectionRequest, ErrorResponse};
use crate::AppState;
use velesdb_core::{DistanceMetric, StorageMode};
use super::helpers::{core_error_response, error_response, get_collection_or_404};
#[utoipa::path(
get,
path = "/collections",
tag = "collections",
responses(
(status = 200, description = "List of collections", body = Object)
)
)]
pub async fn list_collections(State(state): State<Arc<AppState>>) -> impl IntoResponse {
let collections = state.db.list_collections();
Json(serde_json::json!({ "collections": collections }))
}
#[utoipa::path(
post,
path = "/collections",
tag = "collections",
request_body = CreateCollectionRequest,
responses(
(status = 201, description = "Collection created", body = Object),
(status = 400, description = "Invalid request", body = ErrorResponse)
)
)]
pub async fn create_collection(
State(state): State<Arc<AppState>>,
Json(req): Json<CreateCollectionRequest>,
) -> impl IntoResponse {
let metric = match parse_distance_metric(&req.metric) {
Ok(m) => m,
Err(resp) => return resp,
};
let storage_mode = match parse_storage_mode(&req.storage_mode) {
Ok(s) => s,
Err(resp) => return resp,
};
let result = match dispatch_create(&state, &req, metric, storage_mode) {
Ok(r) => r,
Err(resp) => return resp,
};
match result {
Ok(()) => create_collection_success_response(&req),
Err(e) => core_error_response(StatusCode::BAD_REQUEST, &e),
}
}
#[allow(clippy::result_large_err)]
fn parse_distance_metric(raw: &str) -> Result<DistanceMetric, axum::response::Response> {
raw.parse::<DistanceMetric>()
.map_err(|e| error_response(StatusCode::BAD_REQUEST, e.to_string()))
}
#[allow(clippy::result_large_err)]
fn parse_storage_mode(raw: &str) -> Result<StorageMode, axum::response::Response> {
raw.parse::<StorageMode>()
.map_err(|e| error_response(StatusCode::BAD_REQUEST, e))
}
#[allow(clippy::result_large_err)]
fn create_vector_collection(
state: &AppState,
req: &CreateCollectionRequest,
metric: DistanceMetric,
storage_mode: StorageMode,
) -> Result<velesdb_core::error::Result<()>, axum::response::Response> {
let dimension = req.dimension.ok_or_else(|| {
error_response(
StatusCode::BAD_REQUEST,
"dimension is required for vector collections".to_string(),
)
})?;
if req.hnsw_m.is_some() || req.hnsw_ef_construction.is_some() {
Ok(state.db.create_vector_collection_with_hnsw(
&req.name,
dimension,
metric,
storage_mode,
req.hnsw_m,
req.hnsw_ef_construction,
))
} else {
Ok(state.db.create_vector_collection_with_options(
&req.name,
dimension,
metric,
storage_mode,
))
}
}
#[allow(clippy::result_large_err)]
fn dispatch_create(
state: &AppState,
req: &CreateCollectionRequest,
metric: DistanceMetric,
storage_mode: StorageMode,
) -> Result<velesdb_core::error::Result<()>, axum::response::Response> {
match req.collection_type.to_lowercase().as_str() {
"metadata_only" | "metadata-only" | "metadata" => {
Ok(state.db.create_metadata_collection(&req.name))
}
"graph" | "knowledge_graph" | "kg" => {
use velesdb_core::GraphSchema;
Ok(state
.db
.create_graph_collection(&req.name, GraphSchema::schemaless()))
}
"vector" | "" => create_vector_collection(state, req, metric, storage_mode),
_ => Err(error_response(
StatusCode::BAD_REQUEST,
format!(
"Invalid collection_type: {}. Valid: vector, graph, metadata_only",
req.collection_type
),
)),
}
}
fn create_collection_success_response(req: &CreateCollectionRequest) -> axum::response::Response {
let mut warnings = Vec::new();
let is_vector = matches!(req.collection_type.to_lowercase().as_str(), "vector" | "");
if is_vector {
warnings.push("Collection dimension and metric are immutable after creation. If your embedding model changes, create a new collection and reindex data.");
warnings.push("For first queries, start without strict filters/thresholds, then tighten progressively.");
}
(
StatusCode::CREATED,
Json(serde_json::json!({
"message": "Collection created",
"name": req.name,
"type": req.collection_type,
"warnings": warnings
})),
)
.into_response()
}
#[utoipa::path(
get,
path = "/collections/{name}",
tag = "collections",
params(
("name" = String, Path, description = "Collection name")
),
responses(
(status = 200, description = "Collection details", body = CollectionResponse),
(status = 404, description = "Collection not found", body = ErrorResponse)
)
)]
pub async fn get_collection(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
) -> impl IntoResponse {
let collection = match get_collection_or_404(&state, &name) {
Ok(c) => c,
Err(resp) => return resp,
};
let config = collection.config();
Json(CollectionResponse {
name: config.name,
dimension: config.dimension,
metric: format!("{:?}", config.metric).to_lowercase(),
point_count: config.point_count,
storage_mode: format!("{:?}", config.storage_mode).to_lowercase(),
})
.into_response()
}
#[utoipa::path(
get,
path = "/collections/{name}/sanity",
tag = "collections",
params(
("name" = String, Path, description = "Collection name")
),
responses(
(status = 200, description = "Collection sanity status", body = Object),
(status = 404, description = "Collection not found", body = ErrorResponse)
)
)]
pub async fn collection_sanity(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
) -> impl IntoResponse {
let collection = match get_collection_or_404(&state, &name) {
Ok(c) => c,
Err(resp) => return resp,
};
let config = collection.config();
build_sanity_response(&state, &config, &collection)
}
#[allow(deprecated)]
fn build_sanity_response(
state: &AppState,
config: &velesdb_core::collection::CollectionConfig,
collection: &velesdb_core::Collection,
) -> axum::response::Response {
let has_data = config.point_count > 0;
Json(serde_json::json!({
"collection": config.name,
"dimension": config.dimension,
"metric": format!("{:?}", config.metric).to_lowercase(),
"point_count": config.point_count,
"is_empty": collection.is_empty(),
"checks": {
"has_vectors": has_data,
"search_ready": has_data,
"dimension_configured": config.dimension > 0
},
"diagnostics": {
"search_requests_total": state.onboarding_metrics.search_requests_total.load(std::sync::atomic::Ordering::Relaxed),
"dimension_mismatch_total": state.onboarding_metrics.dimension_mismatch_total.load(std::sync::atomic::Ordering::Relaxed),
"empty_search_results_total": state.onboarding_metrics.empty_search_results_total.load(std::sync::atomic::Ordering::Relaxed),
"filter_parse_errors_total": state.onboarding_metrics.filter_parse_errors_total.load(std::sync::atomic::Ordering::Relaxed)
},
"hints": if has_data {
vec![
"Run a search without strict filters first, then tighten filters progressively."
]
} else {
vec![
"Insert at least one known vector before evaluating search quality.",
"Verify you are querying the intended collection."
]
}
}))
.into_response()
}
#[utoipa::path(
delete,
path = "/collections/{name}",
tag = "collections",
params(
("name" = String, Path, description = "Collection name")
),
responses(
(status = 200, description = "Collection deleted", body = Object),
(status = 404, description = "Collection not found", body = ErrorResponse)
)
)]
pub async fn delete_collection(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
) -> impl IntoResponse {
match state.db.delete_collection(&name) {
Ok(()) => Json(serde_json::json!({
"message": "Collection deleted",
"name": name
}))
.into_response(),
Err(e) => core_error_response(StatusCode::NOT_FOUND, &e),
}
}
#[utoipa::path(
get,
path = "/collections/{name}/empty",
tag = "collections",
params(
("name" = String, Path, description = "Collection name")
),
responses(
(status = 200, description = "Empty status", body = Object),
(status = 404, description = "Collection not found", body = ErrorResponse)
)
)]
pub async fn is_empty(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
) -> impl IntoResponse {
let collection = match get_collection_or_404(&state, &name) {
Ok(c) => c,
Err(resp) => return resp,
};
Json(serde_json::json!({
"is_empty": collection.is_empty()
}))
.into_response()
}
#[utoipa::path(
post,
path = "/collections/{name}/flush",
tag = "collections",
params(
("name" = String, Path, description = "Collection name")
),
responses(
(status = 200, description = "Flushed successfully", body = Object),
(status = 404, description = "Collection not found", body = ErrorResponse),
(status = 500, description = "Flush failed", body = ErrorResponse)
)
)]
pub async fn flush_collection(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
) -> impl IntoResponse {
let collection = match get_collection_or_404(&state, &name) {
Ok(c) => c,
Err(resp) => return resp,
};
match collection.flush() {
Ok(()) => Json(serde_json::json!({
"message": "Flushed successfully",
"collection": name
}))
.into_response(),
Err(e) => core_error_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
}
}