mod cache;
mod embed;
mod mrl;
use crate::model::{Kind, SchemaRecord};
use embed::default_embedder;
use mrl::{compress_matryoshka_vector, cosine_similarity};
pub fn search<'a>(
query: &str,
records: &'a [SchemaRecord],
kind: Option<Kind>,
limit: usize,
model: Option<&str>,
refresh: bool,
) -> Vec<(f64, &'a SchemaRecord)> {
let embedder = default_embedder(model);
eprintln!("gqls: semantic search via {} embeddings", embedder.kind());
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() {
cache::touch(p); }
v
}
None => {
use rayon::prelude::*;
use std::io::IsTerminal;
use std::sync::atomic::{AtomicUsize, Ordering};
let total = records.len();
eprintln!(
"gqls: embedding {total} records (one-time, then cached; a large schema can take ~a minute)…"
);
let done = AtomicUsize::new(0);
let v: Vec<Vec<f32>> = std::thread::scope(|scope| {
let show_progress = std::io::stderr().is_terminal() && total > 500;
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_init(
|| default_embedder(model),
|emb, r| {
let out = 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
}
};
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))
.map(|(r, v)| (cosine_similarity(&query_vec, v) as f64, r))
.collect();
hits.sort_by(|a, b| b.0.total_cmp(&a.0));
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) || cache::exists(records, "hash", model)
}
pub fn warm(records: &[SchemaRecord], model: Option<&str>, refresh: bool) -> usize {
let _ = search("", records, None, 0, model, refresh);
records.len()
}