earl 0.5.2

AI-safe CLI for AI agents
use anyhow::Result;

use crate::config::Config;
use crate::secrets::SecretManager;
use crate::template::catalog::TemplateCatalog;

use super::index::{SearchHit, build_documents};
use super::remote_openai_compat::search_remote;

pub async fn search_templates(
    query: &str,
    catalog: &TemplateCatalog,
    config: &Config,
    secrets: &SecretManager,
    limit: usize,
) -> Result<Vec<SearchHit>> {
    let documents = build_documents(catalog);

    if let Some(remote_hits) =
        search_remote(query, &documents, &config.search.remote, secrets).await?
    {
        return Ok(remote_hits.into_iter().take(limit).collect());
    }

    #[cfg(feature = "local-search")]
    {
        use super::local_fastembed::search_local;

        let query_owned = query.to_string();
        let docs_owned = documents.clone();
        let search_cfg = config.search.clone();

        let local_result = tokio::task::spawn_blocking(move || {
            search_local(&query_owned, &docs_owned, &search_cfg)
        })
        .await;

        if let Ok(Ok(hits)) = local_result {
            let mut hits = hits;
            hits.truncate(limit);
            return Ok(hits);
        }
    }

    let mut hits = lexical_fallback(query, &documents);
    hits.truncate(limit);
    Ok(hits)
}

fn lexical_fallback(query: &str, documents: &[super::index::SearchDocument]) -> Vec<SearchHit> {
    let mut hits: Vec<SearchHit> = documents
        .iter()
        .map(|doc| {
            let score = lexical_score(query, &doc.text);
            SearchHit {
                key: doc.key.clone(),
                score,
                summary: doc.summary.clone(),
            }
        })
        .collect();
    hits.sort_by(|a, b| b.score.total_cmp(&a.score));
    hits
}

fn lexical_score(query: &str, text: &str) -> f32 {
    let query_lower = query.to_ascii_lowercase();
    let text_lower = text.to_ascii_lowercase();
    let mut score = 0.0;
    for token in query_lower.split_whitespace() {
        if text_lower.contains(token) {
            score += 1.0;
        }
    }
    score
}