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