use std::collections::HashSet;
use vibesql_ast::FulltextMode;
use crate::errors::ExecutorError;
const STOPWORDS: &[&str] = &[
"a",
"about",
"after",
"all",
"am",
"an",
"and",
"any",
"are",
"as",
"at",
"be",
"been",
"before",
"being",
"by",
"can",
"could",
"did",
"do",
"does",
"doing",
"down",
"during",
"each",
"even",
"for",
"from",
"further",
"had",
"has",
"have",
"having",
"he",
"her",
"here",
"hers",
"herself",
"him",
"himself",
"his",
"how",
"i",
"if",
"in",
"into",
"is",
"it",
"its",
"itself",
"just",
"me",
"might",
"my",
"myself",
"no",
"nor",
"not",
"of",
"off",
"on",
"once",
"only",
"or",
"other",
"out",
"over",
"own",
"same",
"should",
"so",
"some",
"than",
"that",
"the",
"their",
"theirs",
"them",
"themselves",
"then",
"there",
"these",
"they",
"this",
"those",
"through",
"to",
"too",
"under",
"until",
"up",
"very",
"was",
"we",
"were",
"what",
"when",
"where",
"which",
"while",
"who",
"whom",
"why",
"with",
"would",
"you",
"your",
"yours",
"yourself",
"yourselves",
];
fn tokenize(text: &str) -> Vec<String> {
text.to_lowercase()
.split(|c: char| !c.is_alphanumeric() && c != '*')
.filter(|s| !s.is_empty() && s.len() >= 3) .map(|s| s.to_string())
.collect()
}
fn is_stopword(word: &str) -> bool {
STOPWORDS.contains(&word.to_lowercase().as_str())
}
fn remove_stopwords(tokens: Vec<String>) -> Vec<String> {
tokens.into_iter().filter(|token| !is_stopword(token)).collect()
}
pub fn eval_natural_language_search(search_terms: &str, text_values: &[String]) -> bool {
let search_tokens = remove_stopwords(tokenize(search_terms));
if search_tokens.is_empty() {
return true; }
let combined_text = text_values.join(" ");
let text_tokens = tokenize(&combined_text);
let text_token_set: HashSet<_> = text_tokens.into_iter().collect();
search_tokens.iter().any(|term| text_token_set.contains(term))
}
pub fn eval_boolean_search(search_query: &str, text_values: &[String]) -> bool {
let combined_text = text_values.join(" ");
let text_lower = combined_text.to_lowercase();
let mut required_terms = Vec::new();
let mut prohibited_terms = Vec::new();
let mut optional_terms = Vec::new();
let mut phrases = Vec::new();
let mut current_op = ' '; let mut in_phrase = false;
let mut current_term = String::new();
for ch in search_query.chars() {
match ch {
'+' | '-' => {
if !current_term.is_empty() && !in_phrase {
match current_op {
'+' => required_terms.push(current_term.clone()),
'-' => prohibited_terms.push(current_term.clone()),
_ => optional_terms.push(current_term.clone()),
}
current_term.clear();
}
current_op = ch;
}
'"' => {
if in_phrase {
if !current_term.is_empty() {
phrases.push(current_term.clone());
current_term.clear();
}
in_phrase = false;
} else {
in_phrase = true;
}
}
' ' | '\t' if !in_phrase => {
if !current_term.is_empty() {
match current_op {
'+' => required_terms.push(current_term.clone()),
'-' => prohibited_terms.push(current_term.clone()),
_ => optional_terms.push(current_term.clone()),
}
current_term.clear();
current_op = ' ';
}
}
_ => {
current_term.push(ch);
}
}
}
if !current_term.is_empty() {
if in_phrase {
phrases.push(current_term.clone());
} else {
match current_op {
'+' => required_terms.push(current_term),
'-' => prohibited_terms.push(current_term),
_ => optional_terms.push(current_term),
}
}
}
for term in &required_terms {
if !text_lower.contains(&term.to_lowercase()) {
return false;
}
}
for term in &prohibited_terms {
if text_lower.contains(&term.to_lowercase()) {
return false;
}
}
for phrase in &phrases {
if !text_lower.contains(&phrase.to_lowercase()) {
return false;
}
}
optional_terms.is_empty()
|| optional_terms.iter().any(|term| text_lower.contains(&term.to_lowercase()))
}
pub fn eval_query_expansion_search(search_terms: &str, text_values: &[String]) -> bool {
let combined_text = text_values.join(" ");
let tokens = tokenize(&combined_text);
let _tokens_filtered = remove_stopwords(tokens);
if eval_natural_language_search(search_terms, text_values) {
return true;
}
false
}
pub fn eval_match_against(
search_query: &str,
text_values: &[arcstr::ArcStr],
mode: &FulltextMode,
) -> Result<bool, ExecutorError> {
let values: Vec<String> = text_values
.iter()
.filter_map(|val| if val.is_empty() { None } else { Some(val.to_string()) })
.collect();
if values.is_empty() {
return Ok(false);
}
let result = match mode {
FulltextMode::NaturalLanguage => eval_natural_language_search(search_query, &values),
FulltextMode::Boolean => eval_boolean_search(search_query, &values),
FulltextMode::QueryExpansion => eval_query_expansion_search(search_query, &values),
};
Ok(result)
}