1use crate::bitmap::{Postings, RoarPostings};
12use crate::index::InfonIndex;
13use crate::tokenql::evaluate;
14use serde_json::{json, Value};
15use std::collections::{HashMap, HashSet};
16
17type Ix = InfonIndex<RoarPostings>;
18
19const CONTEXT_FACETS: &[&str] = &[
24 "geo", "region", "location", "loc", "place", "country", "site", "city", "date", "time", "ts",
25 "when", "year", "month", "qty", "unit", "value", "amount", "measure", "num", "metric", "price",
26 "numeric", "dur", "rel", "pol", "doctype", "kano", "sentiment", "src",
27];
28
29const MAX_STRUCT_NODES: usize = 2000;
32
33fn facet_of(t: &str) -> &str {
34 t.split('/').next().unwrap_or(t)
35}
36fn leaf_of(t: &str) -> &str {
37 match t.find('/') {
38 Some(i) => &t[i + 1..],
39 None => t,
40 }
41}
42fn is_context(t: &str) -> bool {
43 CONTEXT_FACETS.contains(&facet_of(t))
44}
45fn admissible(t: &str, noise: &HashSet<String>) -> bool {
48 !is_context(t) && !noise.contains(t)
49}
50
51pub fn breakdown(ix: &Ix, anchor: &str, facet: &str, k: usize) -> Value {
57 let base = evaluate(ix, anchor);
58 let mut rows: Vec<(String, usize)> = Vec::new();
59 for t in ix.facet_members(facet) {
60 let n = base.and(&ix.post(t)).len();
61 if n > 0 {
62 rows.push((t.clone(), n));
63 }
64 }
65 rows.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
66 rows.truncate(k);
67 json!({
68 "program": "breakdown", "anchor": anchor, "facet": facet, "total": base.len(),
69 "partition": rows.iter().map(|(v, n)| json!({ "value": leaf_of(v), "token": v, "count": n })).collect::<Vec<_>>(),
70 })
71}
72
73pub fn crosstab(ix: &Ix, anchor: &str, facet_a: &str, facet_b: &str, k: usize) -> Value {
76 let base = evaluate(ix, anchor);
77 let top = |facet: &str| -> Vec<String> {
78 let mut v: Vec<(String, usize)> = ix
79 .facet_members(facet)
80 .into_iter()
81 .map(|t| (t.clone(), base.and(&ix.post(t)).len()))
82 .filter(|(_, n)| *n > 0)
83 .collect();
84 v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
85 v.truncate(k);
86 v.into_iter().map(|(t, _)| t).collect()
87 };
88 let (a_toks, b_toks) = (top(facet_a), top(facet_b));
89 let matrix: Vec<Value> = a_toks
90 .iter()
91 .map(|a| {
92 let ba = base.and(&ix.post(a));
93 let cells: Vec<Value> = b_toks
94 .iter()
95 .map(|b| json!({ "col": leaf_of(b), "count": ba.and(&ix.post(b)).len() }))
96 .collect();
97 json!({ "row": leaf_of(a), "cells": cells })
98 })
99 .collect();
100 json!({
101 "program": "crosstab", "anchor": anchor, "row_facet": facet_a, "col_facet": facet_b,
102 "cols": b_toks.iter().map(|t| leaf_of(t)).collect::<Vec<_>>(), "matrix": matrix, "total": base.len(),
103 })
104}
105
106fn minmax(vals: &[f64]) -> Vec<f64> {
109 let (lo, hi) = vals.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| (lo.min(v), hi.max(v)));
110 let rng = hi - lo;
111 vals.iter().map(|&v| if rng > 0.0 { (v - lo) / rng } else { 0.0 }).collect()
112}
113
114pub fn rank(ix: &Ix, facet: &str, k: usize, noise: &HashSet<String>) -> Value {
118 let st = Structure::build(ix, noise);
119 let ents: Vec<String> = ix.facet_members(facet).into_iter().cloned().collect();
122 if ents.is_empty() {
123 return json!({ "program": "rank", "facet": facet, "ranked": [] });
124 }
125 let freq: Vec<f64> = ents.iter().map(|t| ix.post_len(t) as f64).collect();
126 let breadth: Vec<f64> = ents.iter().map(|t| st.breadth_of(&ix.post(t)) as f64).collect();
127 let (nf, nb) = (minmax(&freq), minmax(&breadth));
128 let mut scored: Vec<(String, f64, f64, f64)> = ents
129 .iter()
130 .enumerate()
131 .map(|(i, t)| {
132 let mdus = 0.5 * nf[i] + 0.5 * nb[i];
133 (t.clone(), mdus, nf[i], nb[i])
134 })
135 .collect();
136 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
137 scored.truncate(k);
138 json!({
139 "program": "rank", "facet": facet,
140 "ranked": scored.iter().map(|(t, m, f, b)| json!({
141 "token": leaf_of(t), "mdus": (m * 1000.0).round() / 1000.0,
142 "components": { "freq": (f * 1000.0).round() / 1000.0, "breadth": (b * 1000.0).round() / 1000.0 }
143 })).collect::<Vec<_>>(),
144 })
145}
146
147struct Structure {
154 names: Vec<String>,
155 posts: Vec<RoarPostings>,
156 index: HashMap<String, usize>,
157 forward: HashMap<u32, Vec<usize>>,
158}
159
160impl Structure {
161 fn build(ix: &Ix, noise: &HashSet<String>) -> Structure {
162 let mut nodes: Vec<(&String, usize)> = ix
163 .tokens()
164 .filter(|t| admissible(t, noise))
165 .map(|t| (t, ix.post_len(t)))
166 .filter(|(_, n)| *n > 0)
167 .collect();
168 nodes.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
169 nodes.truncate(MAX_STRUCT_NODES);
170
171 let names: Vec<String> = nodes.iter().map(|(t, _)| (*t).clone()).collect();
172 let posts: Vec<RoarPostings> = names.iter().map(|t| ix.post(t)).collect();
173 let index: HashMap<String, usize> = names.iter().enumerate().map(|(i, t)| (t.clone(), i)).collect();
174 let mut forward: HashMap<u32, Vec<usize>> = HashMap::new();
175 for (i, p) in posts.iter().enumerate() {
176 for sid in p.to_sorted() {
177 forward.entry(sid).or_default().push(i);
178 }
179 }
180 Structure { names, posts, index, forward }
181 }
182
183 fn candidates(&self, post: &RoarPostings) -> HashSet<usize> {
185 let mut out = HashSet::new();
186 for sid in post.to_sorted() {
187 if let Some(idxs) = self.forward.get(&sid) {
188 out.extend(idxs.iter().copied());
189 }
190 }
191 out
192 }
193
194 fn breadth_of(&self, post: &RoarPostings) -> usize {
196 self.candidates(post).len()
197 }
198
199 fn neighbors(&self, i: usize, s: usize) -> Vec<usize> {
201 self.candidates(&self.posts[i])
202 .into_iter()
203 .filter(|&j| j != i && self.posts[i].and(&self.posts[j]).len() >= s)
204 .collect()
205 }
206}
207
208
209#[derive(Debug, Clone, serde::Serialize)]
221pub struct Level {
222 pub s: usize,
223 pub primal: Graph,
224 pub dual: Graph,
225}
226
227#[derive(Debug, Clone, serde::Serialize)]
229pub struct Graph {
230 pub nodes: usize,
231 pub edges: usize,
232 pub components: usize,
234 pub sample: Vec<(usize, usize)>,
236}
237
238fn components(n: usize, edges: &[(usize, usize)]) -> usize {
240 let mut parent: Vec<usize> = (0..n).collect();
241 fn find(p: &mut [usize], x: usize) -> usize {
242 let mut r = x;
243 while p[r] != r {
244 r = p[r];
245 }
246 let mut c = x;
248 while p[c] != r {
249 let next = p[c];
250 p[c] = r;
251 c = next;
252 }
253 r
254 }
255 for &(a, b) in edges {
256 let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
257 if ra != rb {
258 parent[ra] = rb;
259 }
260 }
261 let mut roots = HashSet::new();
262 for i in 0..n {
263 roots.insert(find(&mut parent, i));
264 }
265 roots.len()
266}
267
268pub fn s_filtration(ix: &Ix, max_s: usize, noise: &HashSet<String>, sample_cap: usize) -> Vec<Level> {
277 let tags: Vec<(&String, &RoarPostings)> = ix
279 .postings()
280 .filter(|(t, _)| !noise.contains(t.as_str()))
281 .collect();
282 let n_sit = ix.situations() as usize;
283
284 let mut tags_of: Vec<Vec<usize>> = vec![Vec::new(); n_sit];
286 for (ti, (_, post)) in tags.iter().enumerate() {
287 for sid in post.to_sorted() {
288 if let Some(slot) = tags_of.get_mut(sid as usize) {
289 slot.push(ti);
290 }
291 }
292 }
293
294 let overlap = |a: &[usize], b: &[usize]| -> usize {
295 let (mut i, mut j, mut n) = (0, 0, 0);
297 while i < a.len() && j < b.len() {
298 match a[i].cmp(&b[j]) {
299 std::cmp::Ordering::Equal => {
300 n += 1;
301 i += 1;
302 j += 1;
303 }
304 std::cmp::Ordering::Less => i += 1,
305 std::cmp::Ordering::Greater => j += 1,
306 }
307 }
308 n
309 };
310
311 (1..=max_s.clamp(1, 16))
312 .map(|s| {
313 let mut p_edges: Vec<(usize, usize)> = Vec::new();
314 for i in 0..n_sit {
315 for j in (i + 1)..n_sit {
316 if overlap(&tags_of[i], &tags_of[j]) >= s {
317 p_edges.push((i, j));
318 }
319 }
320 }
321 let mut d_edges: Vec<(usize, usize)> = Vec::new();
322 for a in 0..tags.len() {
323 for b in (a + 1)..tags.len() {
324 if tags[a].1.and(tags[b].1).len() >= s {
325 d_edges.push((a, b));
326 }
327 }
328 }
329 Level {
330 s,
331 primal: Graph {
332 nodes: n_sit,
333 edges: p_edges.len(),
334 components: components(n_sit, &p_edges),
335 sample: p_edges.iter().take(sample_cap).copied().collect(),
336 },
337 dual: Graph {
338 nodes: tags.len(),
339 edges: d_edges.len(),
340 components: components(tags.len(), &d_edges),
341 sample: d_edges.iter().take(sample_cap).copied().collect(),
342 },
343 }
344 })
345 .collect()
346}
347
348pub fn dual_node_names(ix: &Ix, noise: &HashSet<String>) -> Vec<String> {
350 ix.postings().filter(|(t, _)| !noise.contains(t.as_str())).map(|(t, _)| t.clone()).collect()
351}
352
353pub fn cooccurs(ix: &Ix, token: &str, k: usize, noise: &HashSet<String>) -> Value {
355 let st = Structure::build(ix, noise);
356 let focus = ix.post(token);
357 let mut scored: Vec<(usize, usize)> = st
358 .candidates(&focus)
359 .into_iter()
360 .filter(|&j| st.names[j] != token)
361 .map(|j| (j, focus.and(&st.posts[j]).len()))
362 .filter(|(_, n)| *n > 0)
363 .collect();
364 scored.sort_by(|a, b| b.1.cmp(&a.1).then(st.names[a.0].cmp(&st.names[b.0])));
365 scored.truncate(k);
366 json!({
367 "program": "structure", "op": "cooccurs", "focus": token,
368 "cooccurs": scored.iter().map(|(j, n)| json!({ "token": st.names[*j], "shared": n })).collect::<Vec<_>>(),
369 })
370}
371
372pub fn s_path(ix: &Ix, a: &str, b: &str, s: usize, noise: &HashSet<String>) -> Value {
375 let st = Structure::build(ix, noise);
376 let path = match (st.index.get(a), st.index.get(b)) {
377 (Some(&src), Some(&dst)) => bfs_path(&st, src, dst, s.max(1)),
378 _ => None,
379 };
380 json!({
381 "program": "structure", "op": "s_path", "a": a, "b": b, "s": s,
382 "path": path.map(|p| p.iter().map(|&i| st.names[i].clone()).collect::<Vec<_>>()),
383 })
384}
385
386fn bfs_path(st: &Structure, src: usize, dst: usize, s: usize) -> Option<Vec<usize>> {
387 if src == dst {
388 return Some(vec![src]);
389 }
390 let mut prev: HashMap<usize, Option<usize>> = HashMap::new();
391 prev.insert(src, None);
392 let mut queue = std::collections::VecDeque::from([src]);
393 while let Some(u) = queue.pop_front() {
394 for v in st.neighbors(u, s) {
395 if let std::collections::hash_map::Entry::Vacant(e) = prev.entry(v) {
396 e.insert(Some(u));
397 if v == dst {
398 let mut path = vec![dst];
399 let mut n = dst;
400 while let Some(Some(p)) = prev.get(&n) {
401 path.push(*p);
402 n = *p;
403 }
404 path.reverse();
405 return Some(path);
406 }
407 queue.push_back(v);
408 }
409 }
410 }
411 None
412}
413
414pub fn s_clusters(ix: &Ix, s: usize, k: usize, noise: &HashSet<String>) -> Value {
416 let st = Structure::build(ix, noise);
417 let s = s.max(1);
418 let mut seen = vec![false; st.names.len()];
419 let mut clusters: Vec<Vec<usize>> = Vec::new();
420 for start in 0..st.names.len() {
421 if seen[start] {
422 continue;
423 }
424 let mut comp = Vec::new();
425 let mut stack = vec![start];
426 seen[start] = true;
427 while let Some(u) = stack.pop() {
428 comp.push(u);
429 for v in st.neighbors(u, s) {
430 if !seen[v] {
431 seen[v] = true;
432 stack.push(v);
433 }
434 }
435 }
436 if comp.len() > 1 {
437 clusters.push(comp);
438 }
439 }
440 clusters.sort_by(|a, b| b.len().cmp(&a.len()));
441 clusters.truncate(k);
442 json!({
443 "program": "structure", "op": "s_clusters", "s": s,
444 "clusters": clusters.iter().map(|c| json!({
445 "size": c.len(),
446 "tokens": c.iter().take(12).map(|&i| st.names[i].clone()).collect::<Vec<_>>(),
447 })).collect::<Vec<_>>(),
448 })
449}
450
451pub fn narrow(ix: &Ix, scope: &[String], filters: &[String]) -> Value {
456 let mut parts: Vec<String> = Vec::new();
457 let mut steps: Vec<Value> = Vec::new();
458 let mut empty_at: Option<String> = None;
459 let seq: Vec<(&str, &String)> = scope
460 .iter()
461 .map(|t| ("concept", t))
462 .chain(filters.iter().map(|t| ("filter", t)))
463 .collect();
464 for (kind, piece) in seq {
465 parts.push(piece.clone());
466 let cur = if parts.len() == 1 { parts[0].clone() } else { format!("(and {})", parts.join(" ")) };
467 let n = evaluate(ix, &cur).len();
468 steps.push(json!({ "add": piece, "kind": kind, "expr": cur, "remaining": n }));
469 if n == 0 && empty_at.is_none() {
470 empty_at = Some(piece.clone());
471 }
472 }
473 let answerable = steps.last().and_then(|s| s.get("remaining")).and_then(|n| n.as_u64()).unwrap_or(0) > 0;
474 json!({
475 "program": "narrow", "steps": steps,
476 "verdict": if answerable { "ANSWERABLE" } else { "NOT ANSWERABLE" }, "empty_at": empty_at,
477 })
478}