Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
//! Error types for the ruvector server
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
/// Result type for server operations
pub type Result<T> = std::result::Result<T, Error>;
/// Server error types
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// Collection not found
#[error("Collection not found: {0}")]
CollectionNotFound(String),
/// Collection already exists
#[error("Collection already exists: {0}")]
CollectionExists(String),
/// Point not found
#[error("Point not found: {0}")]
PointNotFound(String),
/// Invalid request
#[error("Invalid request: {0}")]
InvalidRequest(String),
/// Core library error
#[error("Core error: {0}")]
Core(#[from] ruvector_core::RuvectorError),
/// Server error
#[error("Server error: {0}")]
Server(String),
/// Configuration error
#[error("Configuration error: {0}")]
Config(String),
/// Serialization error
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
/// Internal error
#[error("Internal error: {0}")]
Internal(String),
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
let (status, error_message) = match self {
Error::CollectionNotFound(_) | Error::PointNotFound(_) => {
(StatusCode::NOT_FOUND, self.to_string())
}
Error::CollectionExists(_) => (StatusCode::CONFLICT, self.to_string()),
Error::InvalidRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()),
Error::Core(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
Error::Server(_) | Error::Internal(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
}
Error::Config(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
Error::Serialization(e) => (StatusCode::BAD_REQUEST, e.to_string()),
};
let body = Json(json!({
"error": error_message,
"status": status.as_u16(),
}));
(status, body).into_response()
}
}

View File

@@ -0,0 +1,125 @@
//! ruvector-server: REST API server for rUvector vector database
//!
//! This crate provides a REST API server built on axum for interacting with rUvector.
pub mod error;
pub mod routes;
pub mod state;
use axum::{routing::get, Router};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use tower_http::{
compression::CompressionLayer,
cors::{Any, CorsLayer},
trace::TraceLayer,
};
pub use error::{Error, Result};
pub use state::AppState;
/// Server configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// Server host address
pub host: String,
/// Server port
pub port: u16,
/// Enable CORS
pub enable_cors: bool,
/// Enable compression
pub enable_compression: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 6333,
enable_cors: true,
enable_compression: true,
}
}
}
/// Main server structure
pub struct RuvectorServer {
config: Config,
state: AppState,
}
impl RuvectorServer {
/// Create a new server instance with default configuration
pub fn new() -> Self {
Self {
config: Config::default(),
state: AppState::new(),
}
}
/// Create a new server instance with custom configuration
pub fn with_config(config: Config) -> Self {
Self {
config,
state: AppState::new(),
}
}
/// Build the router with all routes
fn build_router(&self) -> Router {
let mut router = Router::new()
.route("/health", get(routes::health::health_check))
.route("/ready", get(routes::health::readiness))
.nest("/collections", routes::collections::routes())
.merge(routes::points::routes())
.with_state(self.state.clone());
// Add middleware layers
router = router.layer(TraceLayer::new_for_http());
if self.config.enable_compression {
router = router.layer(CompressionLayer::new());
}
if self.config.enable_cors {
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
router = router.layer(cors);
}
router
}
/// Start the server
///
/// # Errors
///
/// Returns an error if the server fails to bind or start
pub async fn start(self) -> Result<()> {
let addr: SocketAddr = format!("{}:{}", self.config.host, self.config.port)
.parse()
.map_err(|e| Error::Config(format!("Invalid address: {}", e)))?;
let router = self.build_router();
tracing::info!("Starting ruvector-server on {}", addr);
let listener = tokio::net::TcpListener::bind(addr)
.await
.map_err(|e| Error::Server(format!("Failed to bind to {}: {}", addr, e)))?;
axum::serve(listener, router)
.await
.map_err(|e| Error::Server(format!("Server error: {}", e)))?;
Ok(())
}
}
impl Default for RuvectorServer {
fn default() -> Self {
Self::new()
}
}

View File

@@ -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)
}

View File

@@ -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
}))
}

View File

@@ -0,0 +1,5 @@
//! API routes
pub mod collections;
pub mod health;
pub mod points;

View File

@@ -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))
}

View File

@@ -0,0 +1,60 @@
//! Shared application state
use dashmap::DashMap;
use ruvector_core::VectorDB;
use std::sync::Arc;
/// Shared application state
#[derive(Clone)]
pub struct AppState {
/// Map of collection name to VectorDB
pub collections: Arc<DashMap<String, Arc<VectorDB>>>,
}
impl AppState {
/// Create a new application state
pub fn new() -> Self {
Self {
collections: Arc::new(DashMap::new()),
}
}
/// Get a collection by name
pub fn get_collection(&self, name: &str) -> Option<Arc<VectorDB>> {
self.collections.get(name).map(|c| c.clone())
}
/// Insert a collection
pub fn insert_collection(&self, name: String, db: Arc<VectorDB>) {
self.collections.insert(name, db);
}
/// Remove a collection
pub fn remove_collection(&self, name: &str) -> Option<Arc<VectorDB>> {
self.collections.remove(name).map(|(_, c)| c)
}
/// Check if a collection exists
pub fn contains_collection(&self, name: &str) -> bool {
self.collections.contains_key(name)
}
/// Get all collection names
pub fn collection_names(&self) -> Vec<String> {
self.collections
.iter()
.map(|entry| entry.key().clone())
.collect()
}
/// Get the number of collections
pub fn collection_count(&self) -> usize {
self.collections.len()
}
}
impl Default for AppState {
fn default() -> Self {
Self::new()
}
}