Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
76
vendor/ruvector/crates/ruvector-server/src/error.rs
vendored
Normal file
76
vendor/ruvector/crates/ruvector-server/src/error.rs
vendored
Normal 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()
|
||||
}
|
||||
}
|
||||
125
vendor/ruvector/crates/ruvector-server/src/lib.rs
vendored
Normal file
125
vendor/ruvector/crates/ruvector-server/src/lib.rs
vendored
Normal 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()
|
||||
}
|
||||
}
|
||||
121
vendor/ruvector/crates/ruvector-server/src/routes/collections.rs
vendored
Normal file
121
vendor/ruvector/crates/ruvector-server/src/routes/collections.rs
vendored
Normal 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)
|
||||
}
|
||||
46
vendor/ruvector/crates/ruvector-server/src/routes/health.rs
vendored
Normal file
46
vendor/ruvector/crates/ruvector-server/src/routes/health.rs
vendored
Normal 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
|
||||
}))
|
||||
}
|
||||
5
vendor/ruvector/crates/ruvector-server/src/routes/mod.rs
vendored
Normal file
5
vendor/ruvector/crates/ruvector-server/src/routes/mod.rs
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
//! API routes
|
||||
|
||||
pub mod collections;
|
||||
pub mod health;
|
||||
pub mod points;
|
||||
122
vendor/ruvector/crates/ruvector-server/src/routes/points.rs
vendored
Normal file
122
vendor/ruvector/crates/ruvector-server/src/routes/points.rs
vendored
Normal 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))
|
||||
}
|
||||
60
vendor/ruvector/crates/ruvector-server/src/state.rs
vendored
Normal file
60
vendor/ruvector/crates/ruvector-server/src/state.rs
vendored
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user