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,23 @@
[package]
name = "ruvector-server"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
readme = "README.md"
description = "High-performance REST API server for Ruvector vector databases"
[dependencies]
ruvector-core = {path = "../ruvector-core" }
axum = { version = "0.7", features = ["json", "multipart"] }
tokio = { workspace = true, features = ["full"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
dashmap = { workspace = true }
parking_lot = { workspace = true }

View File

@@ -0,0 +1,229 @@
# Ruvector Server
[![Crates.io](https://img.shields.io/crates/v/ruvector-server.svg)](https://crates.io/crates/ruvector-server)
[![Documentation](https://docs.rs/ruvector-server/badge.svg)](https://docs.rs/ruvector-server)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Rust](https://img.shields.io/badge/rust-1.77%2B-orange.svg)](https://www.rust-lang.org)
**High-performance REST API server for Ruvector vector databases.**
`ruvector-server` provides a production-ready HTTP API built on Axum with CORS support, compression, and OpenAPI documentation. Exposes full Ruvector functionality via RESTful endpoints. Part of the [Ruvector](https://github.com/ruvnet/ruvector) ecosystem.
## Why Ruvector Server?
- **Fast**: Built on Axum and Tokio for high throughput
- **Production Ready**: CORS, compression, tracing built-in
- **RESTful API**: Standard HTTP endpoints for all operations
- **OpenAPI**: Auto-generated API documentation
- **Multi-Collection**: Support multiple vector collections
## Features
### Core Capabilities
- **Vector CRUD**: Insert, get, update, delete vectors
- **Search API**: k-NN search with filtering
- **Batch Operations**: Bulk insert and search
- **Collection Management**: Create and manage collections
- **Health Checks**: Liveness and readiness probes
### Advanced Features
- **CORS Support**: Configurable cross-origin requests
- **Compression**: GZIP response compression
- **Tracing**: Request tracing with tower-http
- **Rate Limiting**: Request rate limiting (planned)
- **Authentication**: API key auth (planned)
## Installation
Add `ruvector-server` to your `Cargo.toml`:
```toml
[dependencies]
ruvector-server = "0.1.1"
```
## Quick Start
### Start Server
```rust
use ruvector_server::{Server, ServerConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure server
let config = ServerConfig {
host: "0.0.0.0".to_string(),
port: 8080,
cors_origins: vec!["*".to_string()],
enable_compression: true,
..Default::default()
};
// Create and start server
let server = Server::new(config)?;
server.run().await?;
Ok(())
}
```
### API Endpoints
```bash
# Health check
GET /health
# Collections
POST /collections # Create collection
GET /collections # List collections
GET /collections/{name} # Get collection info
DELETE /collections/{name} # Delete collection
# Vectors
POST /collections/{name}/vectors # Insert vector(s)
GET /collections/{name}/vectors/{id} # Get vector
DELETE /collections/{name}/vectors/{id} # Delete vector
# Search
POST /collections/{name}/search # k-NN search
POST /collections/{name}/search/batch # Batch search
```
### Example Requests
```bash
# Create collection
curl -X POST http://localhost:8080/collections \
-H "Content-Type: application/json" \
-d '{
"name": "documents",
"dimensions": 384,
"distance_metric": "cosine"
}'
# Insert vector
curl -X POST http://localhost:8080/collections/documents/vectors \
-H "Content-Type: application/json" \
-d '{
"id": "doc-1",
"vector": [0.1, 0.2, 0.3, ...],
"metadata": {"title": "Hello World"}
}'
# Search
curl -X POST http://localhost:8080/collections/documents/search \
-H "Content-Type: application/json" \
-d '{
"vector": [0.1, 0.2, 0.3, ...],
"k": 10,
"filter": {"category": "tech"}
}'
```
## API Overview
### Server Configuration
```rust
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub cors_origins: Vec<String>,
pub enable_compression: bool,
pub max_body_size: usize,
pub request_timeout: Duration,
}
```
### Response Types
```rust
// Search response
pub struct SearchResponse {
pub results: Vec<SearchResult>,
pub took_ms: u64,
}
pub struct SearchResult {
pub id: String,
pub score: f32,
pub vector: Option<Vec<f32>>,
pub metadata: Option<serde_json::Value>,
}
// Collection info
pub struct CollectionInfo {
pub name: String,
pub dimensions: usize,
pub count: usize,
pub distance_metric: String,
}
```
### Error Handling
```rust
// API errors return standard format
pub struct ApiError {
pub code: String,
pub message: String,
pub details: Option<serde_json::Value>,
}
// HTTP status codes:
// 200 - Success
// 201 - Created
// 400 - Bad Request
// 404 - Not Found
// 500 - Internal Error
```
## Docker Deployment
```dockerfile
FROM rust:1.77 as builder
WORKDIR /app
COPY . .
RUN cargo build --release -p ruvector-server
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/ruvector-server /usr/local/bin/
EXPOSE 8080
CMD ["ruvector-server"]
```
```bash
docker build -t ruvector-server .
docker run -p 8080:8080 ruvector-server
```
## Related Crates
- **[ruvector-core](../ruvector-core/)** - Core vector database engine
- **[ruvector-collections](../ruvector-collections/)** - Collection management
- **[ruvector-cli](../ruvector-cli/)** - Command-line interface
## Documentation
- **[Main README](../../README.md)** - Complete project overview
- **[API Documentation](https://docs.rs/ruvector-server)** - Full API reference
- **[GitHub Repository](https://github.com/ruvnet/ruvector)** - Source code
## License
**MIT License** - see [LICENSE](../../LICENSE) for details.
---
<div align="center">
**Part of [Ruvector](https://github.com/ruvnet/ruvector) - Built by [rUv](https://ruv.io)**
[![Star on GitHub](https://img.shields.io/github/stars/ruvnet/ruvector?style=social)](https://github.com/ruvnet/ruvector)
[Documentation](https://docs.rs/ruvector-server) | [Crates.io](https://crates.io/crates/ruvector-server) | [GitHub](https://github.com/ruvnet/ruvector)
</div>

View File

@@ -0,0 +1,38 @@
{
"name": "@ruvector/server",
"version": "0.1.0",
"description": "HTTP/gRPC server for RuVector - REST API with streaming support",
"main": "index.js",
"types": "index.d.ts",
"bin": {
"ruvector-server": "./bin/ruvector-server"
},
"files": [
"*.js",
"*.d.ts",
"bin/"
],
"scripts": {
"build": "cargo build --release",
"start": "cargo run --release",
"test": "cargo test"
},
"repository": {
"type": "git",
"url": "https://github.com/ruvnet/ruvector"
},
"keywords": [
"server",
"http",
"grpc",
"rest-api",
"vector-database",
"streaming"
],
"author": "rUv",
"license": "MIT",
"bugs": {
"url": "https://github.com/ruvnet/ruvector/issues"
},
"homepage": "https://github.com/ruvnet/ruvector"
}

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