use std::sync::OnceLock;
use anyhow::{Context, Result, anyhow};
use xberg::embeddings::EMBEDDING_PRESETS;
use xberg::{EmbeddingConfig, EmbeddingModelType};
static EMBED_POOL: OnceLock<rayon::ThreadPool> = OnceLock::new();
pub fn resolve_embed_threads(max_threads: usize) -> usize {
if max_threads == 0 {
std::cmp::max(2, rayon::current_num_threads() / 4)
} else {
max_threads
}
}
pub fn embed_pool(max_threads: usize) -> &'static rayon::ThreadPool {
EMBED_POOL.get_or_init(|| {
let n = resolve_embed_threads(max_threads);
rayon::ThreadPoolBuilder::new()
.num_threads(n)
.thread_name(|i| format!("bm-embed-{i}"))
.build()
.expect("failed to build embedding rayon pool")
})
}
#[derive(Clone)]
pub struct SharedEmbedder {
config: EmbeddingConfig,
dim: u16,
model_name: String,
max_embed_threads: usize,
}
impl SharedEmbedder {
pub fn load(preset: &str, max_embed_threads: usize) -> Result<Self> {
let meta = EMBEDDING_PRESETS.iter().find(|p| p.name == preset).ok_or_else(|| {
anyhow!(
"unknown embedding preset '{preset}'; \
available: fast, balanced, quality, multilingual"
)
})?;
let dim = u16::try_from(meta.dimensions)
.with_context(|| format!("preset '{preset}' dimension {} exceeds u16", meta.dimensions))?;
let config = EmbeddingConfig {
model: EmbeddingModelType::Preset {
name: preset.to_string(),
},
normalize: true,
batch_size: 32,
show_download_progress: false,
cache_dir: None,
acceleration: None,
max_embed_duration_secs: Some(60),
};
Ok(Self {
config,
dim,
model_name: preset.to_string(),
max_embed_threads,
})
}
pub fn dim(&self) -> u16 {
self.dim
}
pub fn model(&self) -> &str {
&self.model_name
}
pub fn embed(&self, text: &str) -> Result<Vec<f32>> {
if text.is_empty() {
return Err(anyhow!("embed: input text must not be empty"));
}
embed_pool(self.max_embed_threads).install(|| {
let mut results = xberg::embeddings::embed_texts(&[text], &self.config)
.with_context(|| format!("embed_texts(preset={})", self.model_name))?;
results
.pop()
.ok_or_else(|| anyhow!("embed_texts returned empty result"))
})
}
#[cfg(feature = "code-search")]
pub fn embed_batch(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>> {
if texts.is_empty() {
return Ok(Vec::new());
}
embed_pool(self.max_embed_threads).install(|| {
xberg::embeddings::embed_texts(texts, &self.config)
.with_context(|| format!("embed_texts(preset={}, batch={})", self.model_name, texts.len()))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_embed_threads_nonzero_passthrough() {
assert_eq!(resolve_embed_threads(4), 4);
assert_eq!(resolve_embed_threads(1), 1);
assert_eq!(resolve_embed_threads(16), 16);
}
#[test]
fn resolve_embed_threads_zero_gives_auto() {
let got = resolve_embed_threads(0);
let expected = std::cmp::max(2, rayon::current_num_threads() / 4);
assert_eq!(
got, expected,
"resolve_embed_threads(0) should yield max(2, cores/4) = {expected}"
);
assert!(got >= 2, "auto embed cap must be >= 2, got {got}");
}
}