use crate::session::Session;
use crate::session::search::{Query, Target};
use serde::Serialize;
use std::sync::Mutex;
pub const MIN_CHARS: usize = 3;
const LIMIT: usize = 25;
const TOPICAL_WORDS: usize = 4;
const TOPICAL_FLOOR: f32 = 0.22;
const _: () = assert!(TOPICAL_FLOOR > 0.10 && TOPICAL_FLOOR < 0.35);
const TOPICAL_LIMIT: usize = 10;
fn worth_widening(no_hits: bool, needle: &str) -> bool {
no_hits || needle.split_whitespace().count() >= TOPICAL_WORDS
}
#[derive(Default)]
pub struct Topics {
model: Option<crate::embed::Model>,
index: Option<crate::embed::index::Index>,
absent: bool,
}
#[derive(Serialize)]
pub struct Hit {
pub key: String,
pub session_id: String,
pub snippet: String,
pub score: Option<f32>,
}
pub fn run(topics: &Mutex<Topics>, sessions: &[Session], needle: &str) -> Vec<Hit> {
let targets: Vec<Target> = sessions.iter().map(Target::of).collect();
let query = Query::parse(needle);
let mut hits: Vec<Hit> = targets
.iter()
.filter_map(|target| {
crate::session::search::find_query(target, &query).map(|found| Hit {
key: target.key.clone(),
session_id: target.session_id.clone(),
snippet: found.snippet,
score: None,
})
})
.collect();
if worth_widening(hits.is_empty(), needle)
&& let Ok(mut topics) = topics.lock()
{
widen(&mut topics, needle, &targets, &mut hits);
}
hits.truncate(LIMIT);
hits
}
fn widen(topics: &mut Topics, needle: &str, targets: &[Target], hits: &mut Vec<Hit>) {
if topics.absent {
return;
}
if topics.model.is_none() {
if !crate::embed::fetch::present() {
topics.absent = true;
return;
}
match crate::embed::Model::load(&crate::embed::fetch::model_dir()) {
Ok(m) => topics.model = Some(m),
Err(_) => {
topics.absent = true;
return;
}
}
}
let Some(model) = topics.model.as_ref() else {
return;
};
let index = topics.index.get_or_insert_with(|| {
crate::embed::index::Index::load(&crate::config::EMBEDDING_INDEX_FILE).unwrap_or_default()
});
if index.refresh(model, targets) > 0 {
let _ = index.save(&crate::config::EMBEDDING_INDEX_FILE);
}
let query = model.embed(&crate::embed::topic_of(needle));
for (key, snippet, score) in index.search(&query, TOPICAL_FLOOR, TOPICAL_LIMIT) {
if hits.iter().any(|h| h.key == key) {
continue;
}
hits.push(Hit {
session_id: targets
.iter()
.find(|t| t.key == key)
.map(|t| t.session_id.clone())
.unwrap_or_default(),
key,
snippet: format!("~{:.0}% {snippet}", score * 100.0),
score: Some(score),
});
}
}