use super::QueueWorker;
use crate::error::DbError;
use crate::server::llm_client::LLMClient;
use crate::storage::collection::vector::pending_embed_count;
use crate::storage::index::{extract_field_value, VectorIndexConfig};
use crate::storage::Collection;
use std::sync::atomic::{AtomicU64, Ordering};
const EMBED_BATCH: usize = 128;
const ERROR_BACKOFF_SECS: u64 = 60;
static RETRY_AFTER: AtomicU64 = AtomicU64::new(0);
fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
impl QueueWorker {
pub(crate) async fn check_embeddings(&self) {
if pending_embed_count() == 0 {
return;
}
if now_secs() < RETRY_AFTER.load(Ordering::Relaxed) {
return;
}
for db_name in self.storage.list_databases() {
let db = match self.storage.get_database(&db_name) {
Ok(d) => d,
Err(_) => continue,
};
for coll_name in db.list_collections() {
let coll = match db.system_collection(&coll_name) {
Ok(c) => c,
Err(_) => continue,
};
let configs = coll.get_all_vector_index_configs();
for config in configs {
if config.embedding_source.is_none() {
continue;
}
let pending = coll.list_embed_pending(&config.name, EMBED_BATCH);
if pending.is_empty() {
continue;
}
if let Err(e) = self
.embed_pending_batch(&db_name, &coll, &config, &pending)
.await
{
tracing::warn!(
"Auto-embed worker: {}/{} index '{}' failed: {} (backing off {}s)",
db_name,
coll_name,
config.name,
e,
ERROR_BACKOFF_SECS
);
RETRY_AFTER.store(now_secs() + ERROR_BACKOFF_SECS, Ordering::Relaxed);
return;
}
}
}
}
}
async fn embed_pending_batch(
&self,
db_name: &str,
coll: &Collection,
config: &VectorIndexConfig,
doc_keys: &[String],
) -> Result<(), DbError> {
let source_field = config.embedding_source.as_deref().unwrap_or_default();
let mut keys: Vec<String> = Vec::new();
let mut texts: Vec<String> = Vec::new();
for dk in doc_keys {
let doc = match coll.get(dk) {
Ok(d) => d,
Err(_) => {
coll.clear_embed_pending(&config.name, dk);
continue;
}
};
let value = doc.to_value();
match extract_field_value(&value, source_field).as_str() {
Some(t) if !t.trim().is_empty() => {
keys.push(dk.clone());
texts.push(t.to_string());
}
_ => coll.clear_embed_pending(&config.name, dk),
}
}
if keys.is_empty() {
return Ok(());
}
let provider = config
.embedding_provider
.clone()
.unwrap_or_else(|| "openai".to_string());
let client = LLMClient::from_storage(
&self.storage,
db_name,
Some(&provider),
config.embedding_model.clone(),
)?;
let text_refs: Vec<&str> = texts.iter().map(|s| s.as_str()).collect();
let embeddings = client.embed_batch(&text_refs).await?;
for (dk, emb) in keys.iter().zip(embeddings) {
if emb.len() != config.dimension {
tracing::warn!(
"Auto-embed worker: dim mismatch for '{}' index '{}' (got {}, expected {}); dropping marker",
dk,
config.name,
emb.len(),
config.dimension
);
coll.clear_embed_pending(&config.name, dk);
continue;
}
let doc = match coll.get(dk) {
Ok(d) => d,
Err(_) => {
coll.clear_embed_pending(&config.name, dk);
continue;
}
};
let mut value = doc.to_value();
if let Some(obj) = value.as_object_mut() {
obj.insert(config.field.clone(), serde_json::json!(emb));
if let Err(e) = coll.update(dk, value) {
tracing::warn!(
"Auto-embed worker: failed to persist vector for '{}': {}",
dk,
e
);
}
}
}
Ok(())
}
}