Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
23
vendor/ruvector/crates/ruvector-server/Cargo.toml
vendored
Normal file
23
vendor/ruvector/crates/ruvector-server/Cargo.toml
vendored
Normal 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 }
|
||||
229
vendor/ruvector/crates/ruvector-server/README.md
vendored
Normal file
229
vendor/ruvector/crates/ruvector-server/README.md
vendored
Normal file
@@ -0,0 +1,229 @@
|
||||
# Ruvector Server
|
||||
|
||||
[](https://crates.io/crates/ruvector-server)
|
||||
[](https://docs.rs/ruvector-server)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](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)**
|
||||
|
||||
[](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>
|
||||
38
vendor/ruvector/crates/ruvector-server/package.json
vendored
Normal file
38
vendor/ruvector/crates/ruvector-server/package.json
vendored
Normal 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"
|
||||
}
|
||||
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