use std::collections::HashSet;
use metatheca::Uuid;
use taxopsis::{Atom, Direction, Expr, Op, OrderBy, QueryValue};
use crate::error::{Error, Result};
use crate::snapshot::Snapshot;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FindSpec {
pub tags: Vec<String>,
pub categories: Vec<String>,
pub since: Option<String>,
pub until: Option<String>,
pub todo: bool,
pub filter: Option<String>,
pub raw: String,
}
impl FindSpec {
pub fn is_empty(&self) -> bool {
self.tags.is_empty()
&& self.categories.is_empty()
&& self.since.is_none()
&& self.until.is_none()
&& !self.todo
&& self.filter.is_none()
}
}
fn atom(predicate: &str, op: Op, value: QueryValue) -> Expr {
Expr::Atom(Atom {
predicate: predicate.to_string(),
op,
value,
})
}
fn and(a: Expr, b: Expr) -> Expr {
Expr::And(Box::new(a), Box::new(b))
}
fn or(a: Expr, b: Expr) -> Expr {
Expr::Or(Box::new(a), Box::new(b))
}
fn both(member: &str, op: Op, value: &str) -> Expr {
or(
atom(
&format!("jot/index/{member}"),
op,
QueryValue::Str(value.to_string()),
),
atom(
&format!("cuj/applied/{member}"),
op,
QueryValue::Str(value.to_string()),
),
)
}
fn id_order() -> OrderBy {
OrderBy {
predicate: "cuj/id/id".into(),
direction: Direction::Asc,
}
}
fn taxopsis_open(app: &cuj::App) -> Result<(taxopsis::Chain, bool)> {
let chain = cuj::ext::open_taxopsis(app.root())?;
let stale = chain.current_state()?.metatheca != app.vault.current_state_hash()?;
Ok((chain, stale))
}
fn logopsis_open(app: &cuj::App) -> Result<(logopsis::Chain, bool)> {
let chain = cuj::ext::open_logopsis(app.root())?;
let pinned = chain.get_search_state(&chain.current_hash()?)?.metatheca;
let stale = pinned != app.vault.current_state_hash()?;
Ok((chain, stale))
}
pub fn find(app: &cuj::App, spec: &FindSpec) -> Result<(Vec<Uuid>, bool)> {
let (chain, stale) = taxopsis_open(app)?;
let mut expr: Option<Expr> = None;
let mut add = |e: Expr| {
expr = Some(match expr.take() {
Some(prev) => and(prev, e),
None => e,
});
};
for t in &spec.tags {
add(both("tags", Op::Eq, &t.to_lowercase()));
}
for c in &spec.categories {
add(both("categories", Op::Prefix, &c.to_lowercase()));
}
if let Some(s) = &spec.since {
let ns =
cuj::binding::due_to_ns(s).ok_or_else(|| Error::Usage(format!("bad date {s:?}")))?;
add(atom("cuj/jot/created_at_ns", Op::Ge, QueryValue::Int(ns)));
}
if let Some(u) = &spec.until {
let ns =
cuj::binding::due_to_ns(u).ok_or_else(|| Error::Usage(format!("bad date {u:?}")))?;
add(atom("cuj/jot/created_at_ns", Op::Le, QueryValue::Int(ns)));
}
if spec.todo {
add(both("todos/done", Op::Eq, "false"));
}
if let Some(f) = &spec.filter {
add(taxopsis::parse_filter(f)?);
}
let expr = expr.ok_or_else(|| {
Error::Usage(
"find needs at least one filter (tag, category, date, --todo, --filter)".into(),
)
})?;
let sref = chain.current_hash()?.to_hex();
let page = chain.query(&expr, &sref, Some(&id_order()), None, None)?;
Ok((page.rows.iter().map(|r| r.entry).collect(), stale))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SearchEngine {
Hybrid,
Bm25,
}
pub type SearchResult = (Vec<(Uuid, f64)>, bool, SearchEngine);
pub fn search(
app: &cuj::App,
snap: &Snapshot,
query: &str,
k: usize,
) -> Result<SearchResult> {
let want = k * 4 + 16;
let zchain = match zetetes::Chain::open(app.root()) {
Ok(chain) => {
let state = chain.get_state(&chain.current_hash()?)?;
if state.semopsis.is_none() && state.logopsis.is_none() {
None
} else {
Some((chain, state))
}
}
Err(zetetes::Error::NotFound(_)) => None,
Err(e) => return Err(e.into()),
};
let (raw, stale, engine) = match zchain {
Some((mut chain, state)) => {
let stale = state.metatheca != app.vault.current_state_hash()?;
let hits = chain.query(query, want, "current")?;
let raw: Vec<(Uuid, f64)> = hits.into_iter().map(|h| (h.entry, h.score)).collect();
(raw, stale, SearchEngine::Hybrid)
}
None => {
let (chain, stale) = logopsis_open(app)?;
let expr = logopsis::parse_query(query)?;
let hits = chain.query(&expr, want, "current")?;
let raw: Vec<(Uuid, f64)> = hits.into_iter().map(|h| (h.entry, h.score)).collect();
(raw, stale, SearchEngine::Bm25)
}
};
let mut seen = HashSet::new();
let mut out = Vec::new();
for (entry, score) in raw {
let Some(i) = snap.owning_row(entry) else {
continue;
};
let jot = snap.rows[i].entry;
if seen.insert(jot) {
out.push((jot, score));
if out.len() >= k {
break;
}
}
}
Ok((out, stale, engine))
}
pub fn backlinks(app: &cuj::App, of: Uuid) -> Result<(Vec<Uuid>, bool)> {
let (chain, stale) = taxopsis_open(app)?;
let expr = both("references/entry", Op::Eq, &of.hyphenated().to_string());
let sref = chain.current_hash()?.to_hex();
let page = chain.query(&expr, &sref, Some(&id_order()), None, None)?;
Ok((page.rows.iter().map(|r| r.entry).collect(), stale))
}