use std::collections::HashMap;
use std::sync::LazyLock;
static SYNONYMS: LazyLock<HashMap<&'static str, &'static [&'static str]>> = LazyLock::new(|| {
let mut m = HashMap::new();
m.insert("auth", &["authentication", "authorize", "credential"][..]);
m.insert("config", &["configuration", "settings"][..]);
m.insert("cfg", &["configuration", "config", "settings"][..]);
m.insert("err", &["error", "failure", "exception"][..]);
m.insert("fn", &["function", "method"][..]);
m.insert("func", &["function", "method"][..]);
m.insert("init", &["initialize", "setup", "initialization"][..]);
m.insert("parse", &["parsing", "deserialize", "decode"][..]);
m.insert("req", &["request"][..]);
m.insert("res", &["response", "result"][..]);
m.insert("fmt", &["format", "formatting"][..]);
m.insert("db", &["database", "storage"][..]);
m.insert("ctx", &["context"][..]);
m.insert("msg", &["message"][..]);
m.insert("cmd", &["command"][..]);
m.insert("buf", &["buffer"][..]);
m.insert("str", &["string"][..]);
m.insert("impl", &["implementation", "implement"][..]);
m.insert("alloc", &["allocate", "allocation"][..]);
m.insert("dealloc", &["deallocate", "free"][..]);
m.insert("arg", &["argument", "parameter"][..]);
m.insert("args", &["arguments", "parameters"][..]);
m.insert("param", &["parameter", "argument"][..]);
m.insert("params", &["parameters", "arguments"][..]);
m.insert("iter", &["iterator", "iteration"][..]);
m.insert("async", &["asynchronous"][..]);
m.insert("sync", &["synchronous", "synchronize"][..]);
m.insert("env", &["environment"][..]);
m.insert("dir", &["directory", "folder"][..]);
m.insert("deps", &["dependencies", "dependency"][..]);
m.insert("repo", &["repository"][..]);
m
});
pub fn expand_query_for_fts(sanitized_query: &str) -> String {
debug_assert!(
!sanitized_query.contains('"')
&& !sanitized_query.contains('(')
&& !sanitized_query.contains(')'),
"expand_query_for_fts requires pre-sanitized input"
);
let tokens: Vec<&str> = sanitized_query.split_whitespace().collect();
if tokens.is_empty() {
return String::new();
}
let mut parts: Vec<String> = Vec::with_capacity(tokens.len());
let mut has_or_group = false;
for token in &tokens {
let lower = token.to_lowercase();
if let Some(synonyms) = SYNONYMS.get(lower.as_str()) {
let mut group = format!("({}", token);
for syn in *synonyms {
group.push_str(" OR ");
group.push_str(syn);
}
group.push(')');
parts.push(group);
has_or_group = true;
} else {
parts.push(token.to_string());
}
}
let sep = if has_or_group { " AND " } else { " " };
parts.join(sep)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_query_returns_empty() {
assert_eq!(expand_query_for_fts(""), "");
assert_eq!(expand_query_for_fts(" "), "");
}
#[test]
fn no_synonyms_passes_through() {
assert_eq!(expand_query_for_fts("hello world"), "hello world");
}
#[test]
fn single_synonym_expands() {
let result = expand_query_for_fts("auth");
assert!(result.contains("auth"));
assert!(result.contains("authentication"));
assert!(result.contains("authorize"));
assert!(result.contains("credential"));
assert!(result.starts_with('('));
assert!(result.contains(" OR "));
}
#[test]
fn mixed_tokens_expand_selectively() {
let result = expand_query_for_fts("auth middleware");
assert!(result.contains("(auth OR authentication"));
assert!(result.contains("AND middleware"));
assert!(!result.contains("(middleware"));
}
#[test]
fn all_synonyms_expand() {
let result = expand_query_for_fts("config err");
assert!(result.contains("(config OR configuration"));
assert!(result.contains("AND (err OR error"));
}
#[test]
fn no_expansion_uses_implicit_and() {
let result = expand_query_for_fts("hello world");
assert_eq!(result, "hello world");
assert!(!result.contains("AND"));
}
#[test]
fn case_insensitive_lookup() {
let result = expand_query_for_fts("Auth");
assert!(result.contains("Auth"));
assert!(result.contains("authentication"));
}
#[test]
fn synonym_map_has_expected_entries() {
let map = &*SYNONYMS;
assert!(map.contains_key("auth"));
assert!(map.contains_key("config"));
assert!(map.contains_key("err"));
assert!(map.contains_key("fn"));
assert!(map.contains_key("init"));
assert!(map.contains_key("parse"));
assert!(map.contains_key("req"));
assert!(map.contains_key("res"));
assert!(map.contains_key("fmt"));
assert!(map.contains_key("db"));
assert!(map.len() >= 30, "Expected at least 30 synonym entries");
}
}