const STOPWORDS: &[&str] = &[
"a", "an", "the", "this", "that", "these", "those", "i", "me", "my", "we", "our", "you", "your", "it", "its", "in", "on", "at", "to", "for", "of", "with", "from", "by", "as", "and", "or", "but", "so", "if", "when", "while", "is", "are", "was", "were", "be", "been", "being", "do", "does", "did", "have", "has", "had",
"can", "could", "would", "should", "will", "shall", "may", "might",
"please", "help", "need", "want", "like", "just", "also", "about", "some", "any", "all", "each",
"every", "here", "there", "where", "what", "which", "who", "how", "why",
"check", "look", "see", "show", "tell", "explain", "make", "let", "know", "think", "try", "use",
"using", "used",
];
const STRIP_PREFIXES: &[&str] = &[
"can you help me",
"can you",
"could you",
"please help me",
"please help",
"i need to",
"i want to",
"help me",
"how do i",
"how to",
"what is",
"what are",
"where is",
"where are",
"show me",
"tell me about",
"tell me",
"let me know",
"i'm looking for",
"looking for",
];
pub fn preprocess_for_keywords(query: &str) -> String {
let query = query.trim();
if query.is_empty() {
return String::new();
}
let lower = query.to_lowercase();
let mut stripped = query;
for prefix in STRIP_PREFIXES {
if lower.starts_with(prefix) {
stripped = &query[prefix.len()..];
break;
}
}
let stripped = stripped.trim_start_matches(|c: char| c == ' ' || c == ',' || c == ':');
let mut preserved = Vec::new();
let mut remaining = String::new();
let mut in_quote = false;
let mut quote_buf = String::new();
for ch in stripped.chars() {
if ch == '"' || ch == '\'' || ch == '`' {
if in_quote {
if !quote_buf.is_empty() {
preserved.push(quote_buf.clone());
}
quote_buf.clear();
in_quote = false;
} else {
in_quote = true;
}
} else if in_quote {
quote_buf.push(ch);
} else {
remaining.push(ch);
}
}
let words: Vec<&str> = remaining
.split_whitespace()
.filter(|w| {
let lower = w.to_lowercase();
let clean = lower.trim_matches(|c: char| !c.is_alphanumeric() && c != '_' && c != '.');
if clean.is_empty() {
return false;
}
if clean.contains('_') || clean.contains('.') || clean.contains("::") {
return true;
}
if clean.chars().all(|c| c.is_uppercase() || c == '_') && clean.len() > 1 {
return true;
}
!STOPWORDS.contains(&clean)
})
.collect();
let mut result_parts: Vec<String> = preserved;
result_parts.extend(words.iter().map(|w| w.to_string()));
let result = result_parts.join(" ");
if result.is_empty() {
stripped.to_string()
} else {
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_passthrough_technical_query() {
assert_eq!(
preprocess_for_keywords("authentication middleware token refresh"),
"authentication middleware token refresh"
);
}
#[test]
fn test_strip_conversational_prefix() {
assert_eq!(
preprocess_for_keywords("can you help me fix the auth bug"),
"fix auth bug"
);
}
#[test]
fn test_preserve_code_identifiers() {
assert_eq!(
preprocess_for_keywords("where is the verify_token function"),
"verify_token function"
);
}
#[test]
fn test_preserve_paths() {
assert_eq!(
preprocess_for_keywords("show me src/auth.rs"),
"src/auth.rs"
);
}
#[test]
fn test_preserve_quoted_phrases() {
let result = preprocess_for_keywords("search for \"connection pool\" in the code");
assert!(result.contains("connection pool"));
}
#[test]
fn test_empty_after_filter_falls_back() {
let result = preprocess_for_keywords("how to do it");
assert!(!result.is_empty());
}
#[test]
fn test_empty_query() {
assert_eq!(preprocess_for_keywords(""), "");
assert_eq!(preprocess_for_keywords(" "), "");
}
#[test]
fn test_mixed_query() {
let result =
preprocess_for_keywords("I need to fix the HybridSearch scoring in search/hybrid.rs");
assert!(result.contains("HybridSearch"));
assert!(result.contains("scoring"));
assert!(result.contains("search/hybrid.rs"));
assert!(!result.contains("need"));
}
}