use super::resolve::{resolve_key, Resolved};
use super::target::{done_result, PendingEmbed};
use super::write::write_vector;
use crate::commands::enrich::extraction::EnrichItemResult;
use crate::commands::enrich::postprocess::record_enrich_backend;
use crate::errors::AppError;
use rusqlite::Connection;
use std::time::Instant;
pub(in crate::commands::enrich) struct BatchItemOutcome {
pub item_key: String,
pub result: EnrichItemResult,
}
pub(in crate::commands::enrich) fn call_reembed_batch(
conn: &Connection,
namespace: &str,
item_keys: &[String],
paths: &crate::paths::AppPaths,
backends: crate::cli::BackendChoice,
) -> Result<Vec<BatchItemOutcome>, AppError> {
let started = Instant::now();
let dim = crate::constants::embedding_dim();
let mut outcomes: Vec<Option<EnrichItemResult>> = Vec::with_capacity(item_keys.len());
let mut pending: Vec<PendingEmbed> = Vec::with_capacity(item_keys.len());
for (slot, key) in item_keys.iter().enumerate() {
match resolve_key(conn, namespace, key, dim) {
Resolved::Settled(result) => outcomes.push(Some(result)),
Resolved::NeedsEmbedding { target, text } => {
outcomes.push(None);
pending.push(PendingEmbed { slot, target, text });
}
}
}
if !pending.is_empty() {
let texts: Vec<String> = pending.iter().map(|p| p.text.clone()).collect();
let vectors = crate::embedder::embed_passages_parallel_shared(
&paths.models,
std::sync::Arc::from(texts),
crate::constants::DEFAULT_REEMBED_CLAIM_BATCH,
crate::constants::DEFAULT_REEMBED_CLAIM_BATCH,
backends,
)?;
if vectors.len() != pending.len() {
return Err(AppError::Embedding(
crate::i18n::errors_ops::batch_embedding_count_mismatch(
vectors.len(),
pending.len(),
),
));
}
record_enrich_backend(effective_backend_label(backends));
let tx = conn.unchecked_transaction()?;
for (item, embedding) in pending.iter().zip(vectors.iter()) {
if embedding.is_empty() {
outcomes[item.slot] = Some(EnrichItemResult::Skipped {
cost: 0.0,
reason: crate::i18n::validation::embedding_backend_returned_empty_vector(),
});
continue;
}
write_vector(&tx, namespace, &item.target, embedding)?;
outcomes[item.slot] = Some(done_result(&item.target, item.text.chars().count()));
}
tx.commit()?;
}
let elapsed_ms = started.elapsed().as_millis() as u64;
let _ = elapsed_ms;
Ok(item_keys
.iter()
.zip(outcomes)
.map(|(key, result)| BatchItemOutcome {
item_key: key.clone(),
result: result.unwrap_or_else(|| EnrichItemResult::Skipped {
cost: 0.0,
reason: crate::i18n::validation::reembed_batch_no_outcome(),
}),
})
.collect())
}
fn effective_backend_label(backends: crate::cli::BackendChoice) -> &'static str {
let crate::cli::BackendChoice {
llm: llm_backend,
embedding: embedding_backend,
} = backends;
let chain = embedding_backend.to_chain(llm_backend);
match chain.first() {
Some(&crate::embedder::LlmBackendKind::OpenRouter)
if crate::embedder::is_openrouter_initialized() =>
{
crate::embedder::LlmBackendKind::OpenRouter.as_str()
}
Some(kind) => kind.as_str(),
None => crate::embedder::LlmBackendKind::None.as_str(),
}
}