use std::collections::HashSet;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QueryAst {
pub raw: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CompiledQuery {
pub match_expression: String,
}
const STOPWORDS: &[&str] = &[
"the", "and", "for", "are", "was", "were", "what", "when", "where", "who", "whom", "which",
"how", "why", "did", "does", "do", "is", "of", "to", "in", "on", "at", "by", "an", "a", "it",
"its", "this", "that", "these", "those", "with", "from", "as", "be", "or", "if", "about",
"into", "over", "than", "then", "they", "them", "their", "you", "your", "we", "our", "i",
];
#[must_use]
fn content_tokens(raw: &str) -> Vec<String> {
let stop: HashSet<&str> = STOPWORDS.iter().copied().collect();
let mut seen: HashSet<String> = HashSet::new();
let mut out: Vec<String> = Vec::new();
for token in raw.to_lowercase().split(|c: char| !c.is_alphanumeric()) {
if token.len() < 3 || stop.contains(token) {
continue;
}
if seen.insert(token.to_string()) {
out.push(token.to_string());
}
}
out
}
#[must_use]
pub fn compile_text_query(raw: impl Into<String>) -> CompiledQuery {
let raw = raw.into();
let content = content_tokens(&raw);
let match_expression = if content.is_empty() {
raw.split_whitespace()
.filter(|token| !token.is_empty())
.map(|token| format!("\"{}\"", token.replace('"', "\"\"")))
.collect::<Vec<_>>()
.join(" OR ")
} else {
content.into_iter().map(|token| format!("\"{token}\"")).collect::<Vec<_>>().join(" OR ")
};
CompiledQuery { match_expression }
}
#[cfg(test)]
mod tests {
use super::compile_text_query;
#[test]
fn content_tokens_are_or_joined() {
let compiled = compile_text_query("alpha beta");
assert_eq!(compiled.match_expression, "\"alpha\" OR \"beta\"");
}
#[test]
fn stopwords_and_short_tokens_are_dropped() {
let compiled = compile_text_query("status of the alpha");
assert_eq!(compiled.match_expression, "\"status\" OR \"alpha\"");
}
#[test]
fn duplicate_content_tokens_collapse_in_order() {
let compiled = compile_text_query("alpha beta alpha");
assert_eq!(compiled.match_expression, "\"alpha\" OR \"beta\"");
}
#[test]
fn control_characters_are_stripped_to_literals() {
let compiled = compile_text_query("alpha* AND \"beta\" NEAR(gamma)");
assert_eq!(compiled.match_expression, "\"alpha\" OR \"beta\" OR \"near\" OR \"gamma\"");
}
#[test]
fn all_stopword_query_falls_back_to_raw_or() {
let compiled = compile_text_query("who are we");
assert_eq!(compiled.match_expression, "\"who\" OR \"are\" OR \"we\"");
}
#[test]
fn escapes_double_quotes_in_fallback_tokens() {
let compiled = compile_text_query("an \"of");
assert_eq!(compiled.match_expression, "\"an\" OR \"\"\"of\"");
}
}