use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Pooling {
Mean,
Cls,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ModelSpec {
pub id: &'static str,
pub name: &'static str,
pub precision: &'static str,
pub repo: &'static str,
pub model_file: &'static str,
pub vocab_file: &'static str,
pub pooling: Pooling,
pub dim: usize,
pub max_seq_len: usize,
pub query_prefix: &'static str,
pub doc_prefix: &'static str,
pub download_bytes: u64,
pub cosine_floor: f64,
pub cosine_weight: f64,
pub bm25_weight: f64,
pub experimental: bool,
pub summary: &'static str,
}
pub const DEFAULT_MODEL_ID: &str = "minilm-l6-v2-q8";
pub const LEGACY_MODEL_ID: &str = "minilm-l6-v2";
const BGE_QUERY_PREFIX: &str = "Represent this sentence for searching relevant passages: ";
pub const MODELS: &[ModelSpec] = &[
ModelSpec {
id: DEFAULT_MODEL_ID,
name: "all-MiniLM-L6-v2",
precision: "int8",
repo: "Xenova/all-MiniLM-L6-v2",
model_file: "onnx/model_quantized.onnx",
vocab_file: "vocab.txt",
pooling: Pooling::Mean,
dim: 384,
max_seq_len: 512,
query_prefix: "",
doc_prefix: "",
download_bytes: 23_203_878,
cosine_floor: 0.0,
cosine_weight: 0.65,
bm25_weight: 0.35,
experimental: false,
summary: "default; MiniLM in int8, same recall as fp32, smaller download, faster indexing",
},
ModelSpec {
id: LEGACY_MODEL_ID,
name: "all-MiniLM-L6-v2",
precision: "fp32",
repo: "Qdrant/all-MiniLM-L6-v2-onnx",
model_file: "model.onnx",
vocab_file: "vocab.txt",
pooling: Pooling::Mean,
dim: 384,
max_seq_len: 512,
query_prefix: "",
doc_prefix: "",
download_bytes: 90_619_138,
cosine_floor: 0.0,
cosine_weight: 0.65,
bm25_weight: 0.35,
experimental: false,
summary: "MiniLM in fp32; palaces made before 0.19 run on it",
},
ModelSpec {
id: "bge-small",
name: "bge-small-en-v1.5",
precision: "fp32",
repo: "Xenova/bge-small-en-v1.5",
model_file: "onnx/model.onnx",
vocab_file: "vocab.txt",
pooling: Pooling::Cls,
dim: 384,
max_seq_len: 512,
query_prefix: BGE_QUERY_PREFIX,
doc_prefix: "",
download_bytes: 133_324_998,
cosine_floor: 0.5,
cosine_weight: 0.5,
bm25_weight: 0.5,
experimental: true,
summary: "12 layers, ~2x slower indexing; below MiniLM on the coding eval",
},
ModelSpec {
id: "bge-small-q8",
name: "bge-small-en-v1.5",
precision: "int8",
repo: "Xenova/bge-small-en-v1.5",
model_file: "onnx/model_quantized.onnx",
vocab_file: "vocab.txt",
pooling: Pooling::Cls,
dim: 384,
max_seq_len: 512,
query_prefix: BGE_QUERY_PREFIX,
doc_prefix: "",
download_bytes: 34_245_934,
cosine_floor: 0.5,
cosine_weight: 0.5,
bm25_weight: 0.5,
experimental: true,
summary: "bge-small in int8; ~1.7x slower indexing than MiniLM",
},
ModelSpec {
id: "bge-base-q8",
name: "bge-base-en-v1.5",
precision: "int8",
repo: "Xenova/bge-base-en-v1.5",
model_file: "onnx/model_quantized.onnx",
vocab_file: "vocab.txt",
pooling: Pooling::Cls,
dim: 768,
max_seq_len: 512,
query_prefix: BGE_QUERY_PREFIX,
doc_prefix: "",
download_bytes: 110_314_845,
cosine_floor: 0.5,
cosine_weight: 0.5,
bm25_weight: 0.5,
experimental: true,
summary: "768-dim, largest download, ~3x slower indexing",
},
];
pub fn find(id: &str) -> Option<&'static ModelSpec> {
let id = id.trim();
MODELS.iter().find(|spec| spec.id.eq_ignore_ascii_case(id))
}
pub fn default_model() -> &'static ModelSpec {
&MODELS[0]
}
pub(crate) fn index_of(spec: &ModelSpec) -> usize {
MODELS
.iter()
.position(|candidate| candidate.id == spec.id)
.unwrap_or(0)
}
pub fn ids() -> String {
MODELS
.iter()
.map(|spec| spec.id)
.collect::<Vec<_>>()
.join(", ")
}
fn hub_cache_dir() -> Option<PathBuf> {
if let Ok(path) = std::env::var("HF_HUB_CACHE") {
return Some(PathBuf::from(path));
}
directories::UserDirs::new().map(|user| {
user.home_dir()
.join(".cache")
.join("huggingface")
.join("hub")
})
}
pub fn is_cached(spec: &ModelSpec) -> bool {
let Some(cache) = hub_cache_dir() else {
return false;
};
let snapshots = cache
.join(format!("models--{}", spec.repo.replace('/', "--")))
.join("snapshots");
let Ok(entries) = std::fs::read_dir(snapshots) else {
return false;
};
entries
.filter_map(Result::ok)
.any(|entry| entry.path().join(spec.model_file).exists())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ids_are_unique_and_default_comes_first() {
let mut ids: Vec<&str> = MODELS.iter().map(|spec| spec.id).collect();
assert_eq!(ids[0], DEFAULT_MODEL_ID);
ids.sort_unstable();
ids.dedup();
assert_eq!(ids.len(), MODELS.len());
}
#[test]
fn find_ignores_case_and_whitespace() {
assert_eq!(find(" BGE-Small ").map(|spec| spec.id), Some("bge-small"));
assert!(find("gpt-embedding").is_none());
}
#[test]
fn default_model_is_not_experimental() {
assert!(!default_model().experimental);
}
#[test]
fn ranking_weights_sum_to_one() {
for spec in MODELS {
assert!(
(spec.cosine_weight + spec.bm25_weight - 1.0).abs() < 1e-9,
"{} weights",
spec.id
);
}
}
}