use reqwest::blocking::Client;
use serde::Deserialize;
pub enum EmbedderBackend {
OpenAI,
Ollama,
}
pub struct Embedder {
client: Client,
backend: EmbedderBackend,
api_key: Option<String>,
}
#[derive(Deserialize)]
struct OpenAIResponse {
data: Vec<OpenAIEmbedding>,
}
#[derive(Deserialize)]
struct OpenAIEmbedding {
embedding: Vec<f32>,
}
#[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,
}
}
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()
}
}
}
}