mirror of
https://github.com/ruvnet/RuView
synced 2026-08-09 20:21:43 +00:00
Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
//! Collection management endpoints
|
||||
|
||||
use crate::{error::Error, state::AppState, Result};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use ruvector_core::{types::DbOptions, DistanceMetric, VectorDB};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Collection creation request
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateCollectionRequest {
|
||||
/// Collection name
|
||||
pub name: String,
|
||||
/// Vector dimension
|
||||
pub dimension: usize,
|
||||
/// Distance metric (optional, defaults to Cosine)
|
||||
pub metric: Option<DistanceMetric>,
|
||||
}
|
||||
|
||||
/// Collection info response
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CollectionInfo {
|
||||
/// Collection name
|
||||
pub name: String,
|
||||
/// Vector dimension
|
||||
pub dimension: usize,
|
||||
/// Distance metric
|
||||
pub metric: DistanceMetric,
|
||||
}
|
||||
|
||||
/// List of collections response
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CollectionsList {
|
||||
/// Collection names
|
||||
pub collections: Vec<String>,
|
||||
}
|
||||
|
||||
/// Create collection routes
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", post(create_collection).get(list_collections))
|
||||
.route("/:name", get(get_collection).delete(delete_collection))
|
||||
}
|
||||
|
||||
/// Create a new collection
|
||||
///
|
||||
/// POST /collections
|
||||
async fn create_collection(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<CreateCollectionRequest>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
if state.contains_collection(&req.name) {
|
||||
return Err(Error::CollectionExists(req.name));
|
||||
}
|
||||
|
||||
let mut options = DbOptions::default();
|
||||
options.dimensions = req.dimension;
|
||||
options.distance_metric = req.metric.unwrap_or(DistanceMetric::Cosine);
|
||||
// Use in-memory storage for server (storage path will be ignored for memory storage)
|
||||
options.storage_path = format!("memory://{}", req.name);
|
||||
|
||||
let db = VectorDB::new(options.clone()).map_err(Error::Core)?;
|
||||
state.insert_collection(req.name.clone(), Arc::new(db));
|
||||
|
||||
let info = CollectionInfo {
|
||||
name: req.name,
|
||||
dimension: req.dimension,
|
||||
metric: options.distance_metric,
|
||||
};
|
||||
|
||||
Ok((StatusCode::CREATED, Json(info)))
|
||||
}
|
||||
|
||||
/// List all collections
|
||||
///
|
||||
/// GET /collections
|
||||
async fn list_collections(State(state): State<AppState>) -> Result<impl IntoResponse> {
|
||||
let collections = state.collection_names();
|
||||
Ok(Json(CollectionsList { collections }))
|
||||
}
|
||||
|
||||
/// Get collection information
|
||||
///
|
||||
/// GET /collections/:name
|
||||
async fn get_collection(
|
||||
State(state): State<AppState>,
|
||||
Path(name): Path<String>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let _db = state
|
||||
.get_collection(&name)
|
||||
.ok_or_else(|| Error::CollectionNotFound(name.clone()))?;
|
||||
|
||||
// Note: VectorDB doesn't expose config directly, so we return basic info
|
||||
let info = CollectionInfo {
|
||||
name,
|
||||
dimension: 0, // Would need to be stored separately or queried from DB
|
||||
metric: DistanceMetric::Cosine, // Default assumption
|
||||
};
|
||||
|
||||
Ok(Json(info))
|
||||
}
|
||||
|
||||
/// Delete a collection
|
||||
///
|
||||
/// DELETE /collections/:name
|
||||
async fn delete_collection(
|
||||
State(state): State<AppState>,
|
||||
Path(name): Path<String>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
state
|
||||
.remove_collection(&name)
|
||||
.ok_or_else(|| Error::CollectionNotFound(name))?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//! Health check endpoints
|
||||
|
||||
use crate::{state::AppState, Result};
|
||||
use axum::{extract::State, response::IntoResponse, Json};
|
||||
use serde::Serialize;
|
||||
|
||||
/// Health status response
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct HealthStatus {
|
||||
/// Server status
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// Readiness status response
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ReadinessStatus {
|
||||
/// Server status
|
||||
pub status: String,
|
||||
/// Number of collections
|
||||
pub collections: usize,
|
||||
/// Total number of points across all collections
|
||||
pub total_points: usize,
|
||||
}
|
||||
|
||||
/// Simple health check endpoint
|
||||
///
|
||||
/// GET /health
|
||||
pub async fn health_check() -> Result<impl IntoResponse> {
|
||||
Ok(Json(HealthStatus {
|
||||
status: "healthy".to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Readiness check endpoint with stats
|
||||
///
|
||||
/// GET /ready
|
||||
pub async fn readiness(State(state): State<AppState>) -> Result<impl IntoResponse> {
|
||||
let collections_count = state.collection_count();
|
||||
|
||||
// Note: VectorDB doesn't expose count directly, so we report collections only
|
||||
Ok(Json(ReadinessStatus {
|
||||
status: "ready".to_string(),
|
||||
collections: collections_count,
|
||||
total_points: 0, // Would require tracking or querying each DB
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! API routes
|
||||
|
||||
pub mod collections;
|
||||
pub mod health;
|
||||
pub mod points;
|
||||
@@ -0,0 +1,122 @@
|
||||
//! Point operations endpoints
|
||||
|
||||
use crate::{error::Error, state::AppState, Result};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{get, post, put},
|
||||
Json, Router,
|
||||
};
|
||||
use ruvector_core::{SearchQuery, SearchResult, VectorEntry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Point upsert request
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpsertPointsRequest {
|
||||
/// Points to upsert
|
||||
pub points: Vec<VectorEntry>,
|
||||
}
|
||||
|
||||
/// Search request
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchRequest {
|
||||
/// Query vector
|
||||
pub vector: Vec<f32>,
|
||||
/// Number of results to return
|
||||
#[serde(default = "default_limit")]
|
||||
pub k: usize,
|
||||
/// Optional score threshold
|
||||
pub score_threshold: Option<f32>,
|
||||
/// Optional metadata filters
|
||||
pub filter: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
fn default_limit() -> usize {
|
||||
10
|
||||
}
|
||||
|
||||
/// Search response
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SearchResponse {
|
||||
/// Search results
|
||||
pub results: Vec<SearchResult>,
|
||||
}
|
||||
|
||||
/// Upsert response
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UpsertResponse {
|
||||
/// IDs of upserted points
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Create point routes
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/collections/:name/points", put(upsert_points))
|
||||
.route("/collections/:name/points/search", post(search_points))
|
||||
.route("/collections/:name/points/:id", get(get_point))
|
||||
}
|
||||
|
||||
/// Upsert points into a collection
|
||||
///
|
||||
/// PUT /collections/:name/points
|
||||
async fn upsert_points(
|
||||
State(state): State<AppState>,
|
||||
Path(name): Path<String>,
|
||||
Json(req): Json<UpsertPointsRequest>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let db = state
|
||||
.get_collection(&name)
|
||||
.ok_or_else(|| Error::CollectionNotFound(name.clone()))?;
|
||||
|
||||
let ids = db.insert_batch(req.points).map_err(Error::Core)?;
|
||||
|
||||
Ok((StatusCode::OK, Json(UpsertResponse { ids })))
|
||||
}
|
||||
|
||||
/// Search for similar points
|
||||
///
|
||||
/// POST /collections/:name/points/search
|
||||
async fn search_points(
|
||||
State(state): State<AppState>,
|
||||
Path(name): Path<String>,
|
||||
Json(req): Json<SearchRequest>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let db = state
|
||||
.get_collection(&name)
|
||||
.ok_or_else(|| Error::CollectionNotFound(name))?;
|
||||
|
||||
let query = SearchQuery {
|
||||
vector: req.vector,
|
||||
k: req.k,
|
||||
filter: req.filter,
|
||||
ef_search: None,
|
||||
};
|
||||
|
||||
let mut results = db.search(query).map_err(Error::Core)?;
|
||||
|
||||
// Apply score threshold if provided
|
||||
if let Some(threshold) = req.score_threshold {
|
||||
results.retain(|r| r.score >= threshold);
|
||||
}
|
||||
|
||||
Ok(Json(SearchResponse { results }))
|
||||
}
|
||||
|
||||
/// Get a point by ID
|
||||
///
|
||||
/// GET /collections/:name/points/:id
|
||||
async fn get_point(
|
||||
State(state): State<AppState>,
|
||||
Path((name, id)): Path<(String, String)>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let db = state
|
||||
.get_collection(&name)
|
||||
.ok_or_else(|| Error::CollectionNotFound(name))?;
|
||||
|
||||
let entry = db.get(&id).map_err(Error::Core)?;
|
||||
|
||||
Ok(Json(entry))
|
||||
}
|
||||
Reference in New Issue
Block a user