use anyhow::Result;
use ndarray::Array2;
use next_plaid_onnx::{Colbert, ColbertConfig};
use parking_lot::Mutex;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use tokenizers::Tokenizer;
pub type TokenEmbeddings = Array2<f32>;
const DEFAULT_ORT_BATCH: usize = 32;
fn default_index_threads(cores: usize, max_concurrent_builds: usize) -> usize {
if max_concurrent_builds <= 1 {
cores.clamp(2, 8)
} else {
(cores / 2).clamp(2, 8)
}
}
fn index_ort_threads() -> usize {
let cores = std::thread::available_parallelism().map_or(4, std::num::NonZeroUsize::get);
let default = default_index_threads(cores, crate::index::gate::max_concurrent_builds());
crate::config::env_usize("SEMANTEX_INDEX_ORT_THREADS", default)
}
pub(crate) fn build_doc_token_ids(alignment: &DocIdAlignment, text: &str) -> Result<Vec<u32>> {
let processed = if alignment.do_lower_case {
text.trim().to_lowercase()
} else {
text.trim().to_string()
};
let encoding = alignment
.tokenizer
.encode(processed.as_str(), true)
.map_err(|e| anyhow::anyhow!("tokenization error: {e}"))?;
Ok(assemble_and_filter_ids(
alignment.document_prefix_id,
alignment.document_length,
&alignment.skiplist_ids,
encoding.get_ids(),
))
}
fn assemble_and_filter_ids(
document_prefix_id: u32,
document_length: usize,
skiplist_ids: &HashSet<u32>,
ids: &[u32],
) -> Vec<u32> {
let real_len = ids.len().max(1);
let truncate_limit = document_length.saturating_sub(1);
let (content_prefix_len, keep_sep) = if real_len > truncate_limit {
(truncate_limit.saturating_sub(1), true)
} else {
(real_len, false)
};
let mut token_ids = Vec::with_capacity(content_prefix_len + 2);
token_ids.push(ids[0]); token_ids.push(document_prefix_id); token_ids.extend(ids.iter().take(content_prefix_len).skip(1).copied());
if keep_sep {
token_ids.push(ids[real_len - 1]); }
token_ids.retain(|id| !skiplist_ids.contains(id));
token_ids
}
pub struct ColbertEmbedder {
model_dir: PathBuf,
threads: usize,
use_coreml: bool,
encoder: OnceLock<Mutex<Colbert>>,
build_lock: std::sync::Mutex<()>,
#[doc(hidden)]
build_count: std::sync::atomic::AtomicUsize,
id_alignment: OnceLock<DocIdAlignment>,
}
pub(crate) struct DocIdAlignment {
tokenizer: Tokenizer,
document_prefix_id: u32,
document_length: usize,
do_lower_case: bool,
skiplist_ids: HashSet<u32>,
}
pub(crate) fn load_doc_id_alignment(model_dir: &Path) -> Result<DocIdAlignment> {
let tokenizer_path = model_dir.join("tokenizer.json");
let tokenizer = Tokenizer::from_file(&tokenizer_path).map_err(|e| {
anyhow::anyhow!("failed to load tokenizer {}: {e}", tokenizer_path.display())
})?;
let config = ColbertConfig::from_file(model_dir.join("onnx_config.json"))?;
let document_prefix_id = tokenizer
.token_to_id(&config.document_prefix)
.ok_or_else(|| {
anyhow::anyhow!(
"document prefix token '{}' not found in tokenizer vocabulary",
config.document_prefix
)
})?;
let skiplist_ids = config
.skiplist_words
.iter()
.filter_map(|word| tokenizer.token_to_id(word))
.collect();
Ok(DocIdAlignment {
tokenizer,
document_prefix_id,
document_length: config.document_length,
do_lower_case: config.do_lower_case,
skiplist_ids,
})
}
static GLOBAL_COLBERT: OnceLock<ColbertEmbedder> = OnceLock::new();
static COLBERT_INIT_LOCK: parking_lot::Mutex<()> = parking_lot::Mutex::new(());
impl ColbertEmbedder {
pub fn new(model_dir: &Path) -> Result<Self> {
Self::with_threads(
model_dir,
crate::config::env_usize("SEMANTEX_ORT_THREADS", 4),
)
}
pub fn for_indexing(model_dir: &Path) -> Result<Self> {
Self::with_threads(model_dir, index_ort_threads())
}
fn with_threads(model_dir: &Path, threads: usize) -> Result<Self> {
if !model_dir.exists() {
anyhow::bail!("ColBERT model dir does not exist: {}", model_dir.display());
}
let use_coreml = std::env::var("SEMANTEX_COREML").is_ok_and(|v| v == "1");
Ok(Self {
model_dir: model_dir.to_path_buf(),
threads: threads.max(1),
use_coreml,
encoder: OnceLock::new(),
build_lock: std::sync::Mutex::new(()),
build_count: std::sync::atomic::AtomicUsize::new(0),
id_alignment: OnceLock::new(),
})
}
fn build_encoder(&self) -> Result<Colbert> {
self.build_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let batch_size = crate::config::env_usize("SEMANTEX_ORT_BATCH", DEFAULT_ORT_BATCH);
#[allow(unused_mut)]
let mut builder = Colbert::builder(&self.model_dir)
.with_quantized(true)
.with_threads(self.threads)
.with_batch_size(batch_size);
#[cfg(target_os = "macos")]
{
let provider = if self.use_coreml {
next_plaid_onnx::ExecutionProvider::CoreML
} else {
next_plaid_onnx::ExecutionProvider::Cpu
};
builder = builder.with_execution_provider(provider);
}
#[cfg(not(target_os = "macos"))]
{
let _ = self.use_coreml;
builder = builder.with_execution_provider(next_plaid_onnx::ExecutionProvider::Cpu);
}
builder.build()
}
fn encoder(&self) -> Result<&Mutex<Colbert>> {
if let Some(enc) = self.encoder.get() {
return Ok(enc);
}
let _guard = self
.build_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(enc) = self.encoder.get() {
return Ok(enc);
}
let built = self.build_encoder()?;
let _ = self.encoder.set(Mutex::new(built));
Ok(self
.encoder
.get()
.expect("encoder set in previous statement"))
}
pub fn warm_up(&self) -> Result<()> {
let enc = self.encoder()?;
let _ = enc.lock().encode_queries(&["warmup"])?;
Ok(())
}
pub fn global(model_dir: &Path) -> Result<&'static ColbertEmbedder> {
if let Some(embedder) = GLOBAL_COLBERT.get() {
return Ok(embedder);
}
let _guard = COLBERT_INIT_LOCK.lock();
if let Some(embedder) = GLOBAL_COLBERT.get() {
return Ok(embedder);
}
tracing::info!("Initializing global ColBERT encoder singleton (lazy)");
let embedder = Self::new(model_dir)?;
let _ = GLOBAL_COLBERT.set(embedder);
Ok(GLOBAL_COLBERT.get().expect("just set"))
}
pub fn encode_query(&self, text: &str) -> Result<TokenEmbeddings> {
let encoder = self.encoder()?;
let mut embeddings = encoder.lock().encode_queries(&[text])?;
embeddings
.pop()
.ok_or_else(|| anyhow::anyhow!("encode_queries returned empty result"))
}
pub fn encode_documents(&self, texts: &[String]) -> Result<Vec<TokenEmbeddings>> {
let encoder = self.encoder()?;
let refs: Vec<&str> = texts.iter().map(String::as_str).collect();
let embeddings = encoder.lock().encode_documents(&refs, None)?;
Ok(embeddings)
}
fn id_alignment(&self) -> Result<&DocIdAlignment> {
if let Some(alignment) = self.id_alignment.get() {
return Ok(alignment);
}
let alignment = load_doc_id_alignment(&self.model_dir)?;
let _ = self.id_alignment.set(alignment);
Ok(self
.id_alignment
.get()
.expect("id_alignment set in previous statement"))
}
pub fn encode_documents_with_ids(
&self,
texts: &[String],
) -> Result<Vec<(Vec<u32>, TokenEmbeddings)>> {
let embeddings = self.encode_documents(texts)?;
let alignment = self.id_alignment()?;
texts
.iter()
.zip(embeddings)
.map(|(text, emb)| {
let ids = build_doc_token_ids(alignment, text)?;
anyhow::ensure!(
ids.len() == emb.nrows(),
"token id/embedding row mismatch ({} ids vs {} rows) for document {:?}",
ids.len(),
emb.nrows(),
text
);
Ok((ids, emb))
})
.collect()
}
pub fn tokenizer_vocab_size(&self) -> Result<usize> {
Ok(self.id_alignment()?.tokenizer.get_vocab_size(true))
}
pub fn tokenizer_reachable_vocab_count(&self) -> Result<usize> {
let alignment = self.id_alignment()?;
if !alignment.do_lower_case {
return Ok(alignment.tokenizer.get_vocab_size(true));
}
Ok(alignment
.tokenizer
.get_vocab(true)
.values()
.filter(|&&id| {
alignment
.tokenizer
.decode(&[id], false)
.is_ok_and(|decoded| !decoded.chars().any(char::is_uppercase))
})
.count())
}
#[doc(hidden)]
pub fn is_initialized(&self) -> bool {
self.encoder.get().is_some()
}
#[doc(hidden)]
pub fn build_count(&self) -> usize {
self.build_count.load(std::sync::atomic::Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_model_dir() -> Option<PathBuf> {
let dir = crate::config::SemantexConfig::default()
.models_dir()
.join("LateOn-Code-edge");
dir.join("model_int8.onnx").exists().then_some(dir)
}
#[test]
#[ignore = "loads the ColBERT model and builds a real ONNX session; run with --ignored"]
fn encode_with_ids_rows_align() {
let Some(model_dir) = test_model_dir() else {
return;
}; let e = ColbertEmbedder::new(&model_dir).unwrap();
let texts = vec!["fn main() { println!(\"hi\"); }".to_string()];
let out = e.encode_documents_with_ids(&texts).unwrap();
assert_eq!(out.len(), 1);
let (ids, emb) = &out[0];
assert_eq!(ids.len(), emb.nrows(), "one token id per embedding row");
assert!(!ids.is_empty());
let plain = e.encode_documents(&texts).unwrap();
assert_eq!(emb, &plain[0]);
}
#[test]
fn truncated_ids_keep_trailing_sep_and_stay_aligned() {
const CLS: u32 = 1;
const SEP: u32 = 2;
const DOC_MARKER: u32 = 999;
const DOCUMENT_LENGTH: usize = 10;
let content: Vec<u32> = (10..5010).collect();
let mut ids = vec![CLS];
ids.extend(&content);
ids.push(SEP);
let skiplist_ids = HashSet::new();
let out = assemble_and_filter_ids(DOC_MARKER, DOCUMENT_LENGTH, &skiplist_ids, &ids);
assert_eq!(
out.len(),
DOCUMENT_LENGTH,
"truncated sequence must land exactly at document_length"
);
assert_eq!(out[0], CLS, "[CLS] must stay first");
assert_eq!(out[1], DOC_MARKER, "[D] marker inserted at position 1");
assert_eq!(
&out[2..out.len() - 1],
&content[..DOCUMENT_LENGTH - 3],
"kept content must be a PREFIX of the original content (truncation drops the tail)"
);
assert_eq!(
*out.last().unwrap(),
SEP,
"truncated document must keep the trailing [SEP], not cut it off mid-content"
);
}
#[test]
#[ignore = "loads the ColBERT model and builds a real ONNX session; run with --ignored"]
fn reachable_vocab_is_smaller_than_raw_vocab_for_lowercasing_model() {
let Some(model_dir) = test_model_dir() else {
return;
};
let e = ColbertEmbedder::new(&model_dir).unwrap();
let raw = e.tokenizer_vocab_size().unwrap();
let reachable = e.tokenizer_reachable_vocab_count().unwrap();
assert!(
reachable < raw,
"LateOn lowers input: reachable {reachable} must be < raw {raw}"
);
assert!(
reachable > raw / 2,
"sanity: ~74% of vocab is lowercase-only (reachable={reachable}, raw={raw})"
);
}
#[test]
#[ignore = "loads the ColBERT model and builds a real ONNX session; run with --ignored"]
fn encode_with_ids_stays_aligned_for_a_long_document() {
let Some(model_dir) = test_model_dir() else {
return;
};
let e = ColbertEmbedder::new(&model_dir).unwrap();
let long_text: String = (0..3000)
.map(|i| format!("identifier_{i}"))
.collect::<Vec<_>>()
.join(" ");
let texts = vec![long_text];
let out = e.encode_documents_with_ids(&texts).unwrap();
assert_eq!(out.len(), 1);
let (ids, emb) = &out[0];
assert_eq!(
ids.len(),
emb.nrows(),
"one token id per embedding row, even for a long/truncated document"
);
assert!(!ids.is_empty());
let alignment = e.id_alignment().unwrap();
let raw = alignment.tokenizer.encode(texts[0].as_str(), true).unwrap();
assert_eq!(
raw.get_ids().len(),
alignment.document_length - 1,
"tokenizer.json's embedded truncation should cap raw ids at document_length - 1"
);
let sep_id = alignment
.tokenizer
.token_to_id("[SEP]")
.expect("[SEP] must be in the tokenizer vocabulary");
assert_eq!(
*ids.last().unwrap(),
sep_id,
"long document must still end with [SEP]"
);
}
#[test]
fn default_index_threads_uses_all_cores_when_only_one_build_slot_exists() {
assert_eq!(default_index_threads(4, 1), 4);
assert_eq!(default_index_threads(2, 1), 2, "clamped to floor of 2");
assert_eq!(default_index_threads(16, 1), 8, "clamped to ceiling of 8");
assert_eq!(default_index_threads(1, 1), 2, "clamped to floor of 2");
}
#[test]
fn default_index_threads_halves_cores_when_multiple_build_slots_exist() {
assert_eq!(default_index_threads(8, 2), 4);
assert_eq!(default_index_threads(32, 4), 8, "clamped to ceiling of 8");
assert_eq!(default_index_threads(4, 2), 2, "clamped to floor of 2");
}
#[test]
fn new_is_lazy_does_not_build_session() {
let tmp = tempfile::TempDir::new().unwrap();
let embedder = ColbertEmbedder::new(tmp.path())
.expect("constructor should succeed for any existing directory");
assert!(
!embedder.is_initialized(),
"ONNX session must not be materialized at construction time"
);
}
#[test]
fn new_rejects_missing_model_dir() {
let res = ColbertEmbedder::new(Path::new("/nonexistent/path/that/does/not/exist"));
assert!(res.is_err(), "missing model dir must fail at construction");
}
#[test]
fn encoder_init_is_serialized_under_concurrency() {
use std::sync::Arc;
use std::sync::Barrier;
use std::sync::atomic::{AtomicUsize, Ordering};
struct TestEmbedder {
encoder: OnceLock<u32>,
build_lock: std::sync::Mutex<()>,
build_count: AtomicUsize,
}
impl TestEmbedder {
fn new() -> Self {
Self {
encoder: OnceLock::new(),
build_lock: std::sync::Mutex::new(()),
build_count: AtomicUsize::new(0),
}
}
fn get_or_init(&self) -> &u32 {
if let Some(v) = self.encoder.get() {
return v;
}
let _guard = self
.build_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(v) = self.encoder.get() {
return v;
}
self.build_count.fetch_add(1, Ordering::Relaxed);
std::thread::sleep(std::time::Duration::from_millis(50));
let _ = self.encoder.set(42);
self.encoder.get().expect("encoder set above")
}
}
let embedder = Arc::new(TestEmbedder::new());
let n_threads = 8;
let barrier = Arc::new(Barrier::new(n_threads));
let mut handles = Vec::with_capacity(n_threads);
for _ in 0..n_threads {
let emb = Arc::clone(&embedder);
let b = Arc::clone(&barrier);
handles.push(std::thread::spawn(move || {
b.wait();
let v = emb.get_or_init();
assert_eq!(*v, 42);
}));
}
for h in handles {
h.join().unwrap();
}
let count = embedder.build_count.load(Ordering::Relaxed);
assert_eq!(
count, 1,
"build path must be invoked exactly once under {n_threads} concurrent \
first-callers (observed {count}). If this fails, the check-build-set \
pattern in `ColbertEmbedder::encoder` has regressed — see Finding 10."
);
}
#[test]
fn build_count_does_not_grow_on_cached_reads() {
let tmp = tempfile::TempDir::new().unwrap();
let embedder = ColbertEmbedder::new(tmp.path()).unwrap();
assert_eq!(embedder.build_count(), 0);
assert!(!embedder.is_initialized());
}
}