mod cache;
mod embed;
mod mrl;
use crate::model::{Kind, SchemaRecord};
use embed::{default_embedder, Embedder};
use mrl::{compress_matryoshka_vector, cosine_similarity};
const TAIL_CUTOFF: f64 = 0.7;
fn bound_tail<T>(hits: &mut Vec<(f64, T)>) {
let Some(top) = hits.first().map(|h| h.0).filter(|t| *t > 0.0) else {
return;
};
let floor = top * TAIL_CUTOFF;
hits.retain(|(s, _)| *s >= floor);
}
pub fn search<'a>(
query: &str,
records: &'a [SchemaRecord],
kind: Option<Kind>,
parent: Option<&str>,
limit: usize,
model: Option<&str>,
refresh: bool,
) -> Vec<(f64, &'a SchemaRecord)> {
let embedder = default_embedder(model);
if embedder.kind() == "onnx" {
crate::detail!("semantic search via onnx embeddings");
} else {
crate::status!(
"embedding model unavailable — semantic results will be weaker (-v for why)"
);
}
let cache_path = cache::path(records, embedder.kind(), model);
let cached = if refresh {
None
} else {
cache_path
.as_deref()
.and_then(|p| cache::load(p, records.len()))
};
let vectors = match cached {
Some(v) => {
if let Some(p) = cache_path.as_deref() {
crate::detail!("vector cache hit: {}", crate::paths::display(p));
cache::touch(p); }
v
}
None => {
if let Some(p) = cache_path.as_deref() {
crate::detail!("vector cache miss: {}", crate::paths::display(p));
}
use rayon::prelude::*;
use std::io::IsTerminal;
use std::sync::atomic::{AtomicUsize, Ordering};
let total = records.len();
crate::status!("embedding {total} records (one-time; may take a minute)…");
let done = AtomicUsize::new(0);
use std::cell::RefCell;
thread_local! {
static EMBEDDER: RefCell<Option<Box<dyn Embedder>>> = const { RefCell::new(None) };
}
let v: Vec<Vec<f32>> = std::thread::scope(|scope| {
let show_progress =
std::io::stderr().is_terminal() && total > 500 && !crate::logging::is_quiet();
if show_progress {
scope.spawn(|| loop {
std::thread::sleep(std::time::Duration::from_millis(300));
let d = done.load(Ordering::Relaxed);
eprint!("\rgqls: embedded {d}/{total}… ");
if d >= total {
eprintln!();
break;
}
});
}
records
.par_iter()
.map(|r| {
let out = EMBEDDER.with(|cell| {
let mut slot = cell.borrow_mut();
let emb = slot.get_or_insert_with(|| default_embedder(model));
compress_matryoshka_vector(&emb.embed(&record_text(r)))
});
done.fetch_add(1, Ordering::Relaxed);
out
})
.collect()
});
if let Some(p) = cache_path.as_deref() {
cache::store(p, &v);
cache::prune(cache::max_files()); }
v
}
};
if query.is_empty() && limit == 0 {
return Vec::new();
}
let query_vec = compress_matryoshka_vector(&embedder.embed(query));
let mut hits: Vec<(f64, &SchemaRecord)> = records
.iter()
.zip(&vectors)
.filter(|(r, _)| kind.is_none_or(|k| r.kind == k))
.filter(|(r, _)| {
parent.is_none_or(|p| {
r.parent
.as_deref()
.is_some_and(|rp| rp.eq_ignore_ascii_case(p))
})
})
.map(|(r, v)| (cosine_similarity(&query_vec, v) as f64, r))
.collect();
hits.sort_by(|a, b| b.0.total_cmp(&a.0));
bound_tail(&mut hits);
hits.truncate(limit);
hits
}
fn record_text(r: &SchemaRecord) -> String {
let mut s = r.path.clone();
if let Some(d) = &r.description {
s.push_str(" — ");
s.push_str(d);
}
if let Some(t) = &r.type_ref {
s.push_str(" : ");
s.push_str(t);
}
s
}
pub fn clear_cache() -> usize {
cache::clear()
}
pub fn is_cached(records: &[SchemaRecord], model: Option<&str>) -> bool {
cache::exists(records, "onnx", model)
}
pub fn warm(records: &[SchemaRecord], model: Option<&str>, refresh: bool) -> usize {
let _ = search("", records, None, None, 0, model, refresh);
records.len()
}
#[cfg(test)]
mod tests {
use super::bound_tail;
#[test]
fn bounds_the_weak_tail_relative_to_the_top() {
let mut hits = vec![(1.0, "a"), (0.8, "b"), (0.6, "c")];
bound_tail(&mut hits);
assert_eq!(hits.len(), 2);
}
#[test]
fn keeps_everything_when_the_top_is_not_positive() {
let mut hits = vec![(-0.1, "a"), (-0.5, "b")];
bound_tail(&mut hits);
assert_eq!(hits.len(), 2);
}
}