#![warn(missing_docs)]
use qexpr::{Near, Phrase, QExpr, Term};
use std::collections::HashSet;
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum Error {
#[error("unsupported operator: Or")]
UnsupportedOr,
#[error("unsupported operator: Not")]
UnsupportedNot,
#[error("unsupported operator: Field")]
UnsupportedField,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConjunctivePlan {
pub bag_terms: Vec<String>,
pub phrases: Vec<Vec<String>>,
pub nears: Vec<NearPlan>,
}
impl ConjunctivePlan {
pub fn is_bag_only(&self) -> bool {
self.phrases.is_empty() && self.nears.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NearPlan {
pub terms: Vec<String>,
pub window: u32,
pub ordered: bool,
}
pub fn compile_conjunctive(expr: &QExpr) -> Result<ConjunctivePlan, Error> {
let mut bag: Vec<String> = Vec::new();
let mut phrases: Vec<Vec<String>> = Vec::new();
let mut nears: Vec<NearPlan> = Vec::new();
fn push_term(bag: &mut Vec<String>, t: &Term) {
if !t.is_blank() {
bag.push(t.0.clone());
}
}
fn push_phrase(bag: &mut Vec<String>, phrases: &mut Vec<Vec<String>>, p: &Phrase) {
let mut ts: Vec<String> = Vec::new();
for t in &p.terms {
if !t.is_blank() {
ts.push(t.0.clone());
bag.push(t.0.clone());
}
}
if ts.len() >= 2 {
phrases.push(ts);
}
}
fn push_near(bag: &mut Vec<String>, nears: &mut Vec<NearPlan>, n: &Near) {
let mut ts: Vec<String> = Vec::new();
for t in &n.terms {
if !t.is_blank() {
ts.push(t.0.clone());
bag.push(t.0.clone());
}
}
if ts.len() >= 2 && n.window > 0 {
nears.push(NearPlan {
terms: ts,
window: n.window,
ordered: n.ordered,
});
}
}
fn walk(
e: &QExpr,
bag: &mut Vec<String>,
phrases: &mut Vec<Vec<String>>,
nears: &mut Vec<NearPlan>,
) -> Result<(), Error> {
match e {
QExpr::Term(t) => {
push_term(bag, t);
Ok(())
}
QExpr::Phrase(p) => {
push_phrase(bag, phrases, p);
Ok(())
}
QExpr::Near(n) => {
push_near(bag, nears, n);
Ok(())
}
QExpr::And(xs) => {
for x in xs {
walk(x, bag, phrases, nears)?;
}
Ok(())
}
QExpr::Or(_) => Err(Error::UnsupportedOr),
QExpr::Not(_) => Err(Error::UnsupportedNot),
QExpr::Field(_, _) => Err(Error::UnsupportedField),
}
}
walk(expr, &mut bag, &mut phrases, &mut nears)?;
let mut seen: HashSet<String> = HashSet::new();
let mut deduped: Vec<String> = Vec::with_capacity(bag.len());
for t in bag {
if seen.insert(t.clone()) {
deduped.push(t);
}
}
Ok(ConjunctivePlan {
bag_terms: deduped,
phrases,
nears,
})
}
#[cfg(test)]
mod tests {
use super::*;
use qexpr::{FieldName, QExpr, Term};
#[test]
fn compiles_terms_and_phrases_into_bag_plus_constraints() {
let q = QExpr::And(vec![
QExpr::Term(Term::new("alpha")),
QExpr::Phrase(Phrase::new(vec![Term::new("new"), Term::new("york")])),
]);
let p = compile_conjunctive(&q).unwrap();
assert_eq!(p.phrases.len(), 1);
assert!(p.bag_terms.contains(&"alpha".to_string()));
assert!(p.bag_terms.contains(&"new".to_string()));
assert!(p.bag_terms.contains(&"york".to_string()));
}
#[test]
fn rejects_or_not_field() {
let q = QExpr::Or(vec![
QExpr::Term(Term::new("a")),
QExpr::Term(Term::new("b")),
]);
assert_eq!(compile_conjunctive(&q).unwrap_err(), Error::UnsupportedOr);
let q = QExpr::Not(Box::new(QExpr::Term(Term::new("a"))));
assert_eq!(compile_conjunctive(&q).unwrap_err(), Error::UnsupportedNot);
let q = QExpr::Field(
FieldName::new("title"),
Box::new(QExpr::Term(Term::new("a"))),
);
assert_eq!(
compile_conjunctive(&q).unwrap_err(),
Error::UnsupportedField
);
}
}