ruve-db 0.1.1

A hybrid vector and full-text search database with HNSW approximate nearest-neighbour indexing and BM25
Documentation
use reqwest::blocking::Client;
use serde::Deserialize;

pub enum EmbedderBackend {
    OpenAI,
    Ollama,
}

pub struct Embedder {
    client: Client,
    backend: EmbedderBackend,
    api_key: Option<String>,
}

// OpenAI response shape
#[derive(Deserialize)]
struct OpenAIResponse {
    data: Vec<OpenAIEmbedding>,
}
#[derive(Deserialize)]
struct OpenAIEmbedding {
    embedding: Vec<f32>,
}

// Ollama response shape
#[derive(Deserialize)]
struct OllamaResponse {
    embeddings: Vec<Vec<f32>>,
}

impl Embedder {
    pub fn openai() -> Self {
        dotenvy::dotenv().ok();
        let api_key = std::env::var("OPENAI_API_KEY")
            .expect("OPENAI_API_KEY not set in environment or .env");
        Embedder {
            client: Client::new(),
            backend: EmbedderBackend::OpenAI,
            api_key: Some(api_key),
        }
    }

    pub fn ollama() -> Self {
        Embedder {
            client: Client::new(),
            backend: EmbedderBackend::Ollama,
            api_key: None,
        }
    }

    // keep existing new() pointing to openai so nothing else breaks
    pub fn new() -> Self {
        Self::openai()
    }

    pub fn embed(&self, text: &str) -> Vec<f32> {
        match self.backend {
            EmbedderBackend::OpenAI => {
                let response: OpenAIResponse = self.client
                    .post("https://api.openai.com/v1/embeddings")
                    .bearer_auth(self.api_key.as_deref().unwrap())
                    .json(&serde_json::json!({
                        "input": text,
                        "model": "text-embedding-3-large"
                    }))
                    .send()
                    .expect("failed to send OpenAI embedding request")
                    .json()
                    .expect("failed to parse OpenAI embedding response");
                response.data.into_iter().next().unwrap().embedding
            }
            EmbedderBackend::Ollama => {
                let response: OllamaResponse = self.client
                    .post("http://localhost:11434/api/embed")
                    .json(&serde_json::json!({
                        "model": "nomic-embed-text",
                        "input": text
                    }))
                    .send()
                    .expect("failed to send Ollama embedding request — is ollama running?")
                    .json()
                    .expect("failed to parse Ollama embedding response");
                response.embeddings.into_iter().next().unwrap()
            }
        }
    }
}