use anyhow::{Context, Result};
use fastembed::{
OnnxSource, RerankInitOptionsUserDefined, TextRerank, TokenizerFiles, UserDefinedRerankingModel,
};
use std::path::{Path, PathBuf};
pub const RERANK_REPO: &str = "jinaai/jina-reranker-v1-turbo-en";
pub const RERANK_REVISION: &str = "b8c14f4e723d9e0aab4732a7b7b93741eeeb77c2";
pub const RERANK_NAME: &str = "jina-reranker-v1-turbo-en-int8";
pub const RERANK_ONNX: &str = "onnx/model_quantized.onnx";
pub const RERANK_FILES: &[(&str, &str)] = &[
(
"tokenizer.json",
"0046da43cc8c424b317f56b092b0512aaaa65c4f925d2f16af9d9eeb4d0ef902",
),
(
"config.json",
"e050ff6a15ae9295e84882fa0e98051bd8754856cd5201395ebf00ce9f2d609b",
),
(
"special_tokens_map.json",
"06e405a36dfe4b9604f484f6a1e619af1a7f7d09e34a8555eb0b77b66318067f",
),
(
"tokenizer_config.json",
"d291c6652d96d56ffdbcf1ea19d9bae5ed79003f7648c627e725a619227ce8fa",
),
(
RERANK_ONNX,
"3defdef1ae34e119bd704216087743e79665934c96aebabcb6077c239dc3ae66",
),
];
pub const RERANK_DEPTH: usize = 12;
pub const RERANK_CHARS: usize = 320;
pub const RERANK_ENV: &str = "SEMLITH_RERANK";
#[must_use]
pub fn enabled() -> bool {
matches!(
std::env::var(RERANK_ENV).ok().as_deref(),
Some("on" | "1" | "true")
)
}
static SHARED: std::sync::OnceLock<std::sync::Mutex<Option<TextRerank>>> =
std::sync::OnceLock::new();
pub fn with<T>(cache: &Path, quiet: bool, f: impl FnOnce(Option<&mut TextRerank>) -> T) -> T {
let cell = SHARED.get_or_init(|| std::sync::Mutex::new(None));
let mut held = cell.lock().unwrap_or_else(|e| e.into_inner());
if held.is_none() && cached(cache) {
match load(cache, quiet) {
Ok(model) => *held = Some(model),
Err(e) => {
if !quiet {
eprintln!("the rescoring model did not load, ranking by fusion alone: {e}");
}
}
}
}
f(held.as_mut())
}
pub fn load(cache_dir: &Path, quiet: bool) -> Result<TextRerank> {
crate::embed::link_runtime()?;
crate::embed::check_cache_dir(cache_dir)?;
crate::embed::refuse_if_airgapped(RERANK_NAME)?;
let repo = hf_hub::api::sync::ApiBuilder::new()
.with_cache_dir(cache_dir.to_path_buf())
.with_progress(!quiet)
.build()
.context("building the Hugging Face client")?
.repo(hf_hub::Repo::with_revision(
RERANK_REPO.to_string(),
hf_hub::RepoType::Model,
RERANK_REVISION.to_string(),
));
let fetch = |name: &str| -> Result<Vec<u8>> {
let path = repo
.get(name)
.with_context(|| format!("fetching {name} from {RERANK_REPO}"))?;
let bytes = std::fs::read(&path).with_context(|| format!("reading {}", path.display()))?;
let expected = RERANK_FILES
.iter()
.find(|(file, _)| *file == name)
.map(|(_, digest)| *digest)
.with_context(|| format!("{name} is not a file semlith pins a digest for"))?;
crate::embed::verify(name, &bytes, expected)?;
Ok(bytes)
};
let tokenizer_files = TokenizerFiles {
tokenizer_file: fetch("tokenizer.json")?,
config_file: fetch("config.json")?,
special_tokens_map_file: fetch("special_tokens_map.json")?,
tokenizer_config_file: fetch("tokenizer_config.json")?,
};
let model =
UserDefinedRerankingModel::new(OnnxSource::Memory(fetch(RERANK_ONNX)?), tokenizer_files);
let options = RerankInitOptionsUserDefined::default()
.with_max_length(512)
.with_intra_threads(crate::embed::embed_threads());
TextRerank::try_new_from_user_defined(model, options)
.map_err(|e| anyhow::anyhow!("loading {RERANK_NAME}: {e}"))
}
#[must_use]
pub fn cached(cache_dir: &Path) -> bool {
let Some(dir) = snapshot(cache_dir) else {
return false;
};
RERANK_FILES.iter().all(|(name, _)| dir.join(name).exists())
}
fn snapshot(cache_dir: &Path) -> Option<PathBuf> {
let owner = RERANK_REPO.replace('/', "--");
let dir = cache_dir
.join(format!("models--{owner}"))
.join("snapshots")
.join(RERANK_REVISION);
dir.is_dir().then_some(dir)
}
pub fn order(model: &mut TextRerank, query: &str, texts: &[String]) -> Result<Vec<usize>> {
if texts.is_empty() {
return Ok(Vec::new());
}
let scored = model
.rerank(
query,
texts.iter().map(String::as_str).collect::<Vec<&str>>(),
false,
None,
)
.map_err(|e| anyhow::anyhow!("reranking: {e}"))?;
Ok(scored.into_iter().map(|result| result.index).collect())
}