use super::searchable::SearchableModel;
use reinhardt_db::orm::{Field, Lookup, Model};
pub struct MultiTermSearch;
#[derive(Debug, Clone, PartialEq)]
pub struct SearchTerm {
pub value: String,
pub term_type: TermType,
pub field: Option<String>,
pub operator: Operator,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TermType {
Word,
Phrase,
Wildcard,
FieldValue,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Operator {
And,
Or,
Not,
}
impl MultiTermSearch {
pub fn search_terms<M: SearchableModel>(terms: Vec<&str>) -> Vec<Vec<Lookup<M>>> {
let fields = M::searchable_fields();
terms
.into_iter()
.map(|term| {
fields
.iter()
.map(|field| {
let new_field = Field::<M, String>::new(field.path().to_vec());
new_field.icontains(term)
})
.collect()
})
.collect()
}
pub fn exact_terms<M: SearchableModel>(terms: Vec<&str>) -> Vec<Vec<Lookup<M>>> {
let fields = M::searchable_fields();
terms
.into_iter()
.map(|term| {
fields
.iter()
.map(|field| {
let new_field = Field::<M, String>::new(field.path().to_vec());
new_field.iexact(term.to_string())
})
.collect()
})
.collect()
}
pub fn prefix_terms<M: SearchableModel>(terms: Vec<&str>) -> Vec<Vec<Lookup<M>>> {
let fields = M::searchable_fields();
terms
.into_iter()
.map(|term| {
fields
.iter()
.map(|field| {
let new_field = Field::<M, String>::new(field.path().to_vec());
new_field.startswith(term)
})
.collect()
})
.collect()
}
pub fn parse_search_terms(search: &str) -> Vec<String> {
let mut terms = Vec::new();
let mut current_term = String::new();
let mut in_quotes = false;
let chars = search.chars().peekable();
for c in chars {
match c {
'"' => {
in_quotes = !in_quotes;
}
',' if !in_quotes => {
let trimmed = current_term.trim().to_string();
if !trimmed.is_empty() {
terms.push(trimmed);
}
current_term.clear();
}
_ => {
current_term.push(c);
}
}
}
let trimmed = current_term.trim().to_string();
if !trimmed.is_empty() {
terms.push(trimmed);
}
terms
}
pub fn compile_to_sql<M: Model>(term_lookups: Vec<Vec<Lookup<M>>>) -> Option<String> {
if term_lookups.is_empty() {
return None;
}
use reinhardt_db::orm::QueryFieldCompiler;
let term_clauses: Vec<String> = term_lookups
.into_iter()
.filter(|lookups| !lookups.is_empty())
.map(|lookups| {
let field_conditions: Vec<String> = lookups
.iter()
.map(|lookup| QueryFieldCompiler::compile(lookup))
.collect();
if field_conditions.len() == 1 {
field_conditions[0].clone()
} else {
format!("({})", field_conditions.join(" OR "))
}
})
.collect();
if term_clauses.is_empty() {
return None;
}
if term_clauses.len() == 1 {
Some(term_clauses[0].clone())
} else {
Some(format!("({})", term_clauses.join(" AND ")))
}
}
pub fn parse_query(query: &str) -> Vec<SearchTerm> {
let mut terms = Vec::new();
let mut current_term = String::new();
let mut in_quotes = false;
let mut chars = query.chars().peekable();
let mut current_operator = Operator::And;
while let Some(ch) = chars.next() {
match ch {
'"' => {
in_quotes = !in_quotes;
if !in_quotes && !current_term.is_empty() {
terms.push(SearchTerm {
value: current_term.clone(),
term_type: TermType::Phrase,
field: None,
operator: current_operator.clone(),
});
current_term.clear();
current_operator = Operator::And;
}
}
' ' if !in_quotes => {
if !current_term.is_empty() {
match current_term.to_uppercase().as_str() {
"AND" => {
current_operator = Operator::And;
}
"OR" => {
current_operator = Operator::Or;
}
"NOT" => {
current_operator = Operator::Not;
}
_ => {
terms.push(Self::parse_single_term(
¤t_term,
current_operator.clone(),
));
current_operator = Operator::And;
}
}
current_term.clear();
}
}
':' if !in_quotes => {
let field = current_term.clone();
current_term.clear();
let mut field_in_quotes = false;
if let Some(&'"') = chars.peek() {
chars.next(); field_in_quotes = true;
}
for next_ch in chars.by_ref() {
if field_in_quotes {
if next_ch == '"' {
break;
} else {
current_term.push(next_ch);
}
} else if next_ch == ' ' {
break;
} else {
current_term.push(next_ch);
}
}
terms.push(SearchTerm {
value: current_term.clone(),
term_type: TermType::FieldValue,
field: Some(field),
operator: current_operator.clone(),
});
current_term.clear();
current_operator = Operator::And;
}
_ => {
current_term.push(ch);
}
}
}
if !current_term.is_empty() {
if in_quotes {
terms.push(SearchTerm {
value: current_term,
term_type: TermType::Phrase,
field: None,
operator: current_operator,
});
} else {
match current_term.to_uppercase().as_str() {
"AND" | "OR" | "NOT" => {
}
_ => {
terms.push(Self::parse_single_term(¤t_term, current_operator));
}
}
}
}
terms
}
fn parse_single_term(term: &str, operator: Operator) -> SearchTerm {
let term_type = if term.ends_with('*') {
TermType::Wildcard
} else {
TermType::Word
};
SearchTerm {
value: term.to_string(),
term_type,
field: None,
operator,
}
}
}