use std::path::Path;
use std::time::Instant;
use steeldb::index::InfonIndex;
use steeldb::tokenql::evaluate;
use steeldb::{Postings, RoarPostings, SetPostings};
const QUERIES: &[(&str, &str)] = &[
("dense ∩ dense", "(and source/sentinel-2-l2a class/vegetation)"),
("dense − sparse", "(and class/vegetation (not class/water))"),
("or of years", "(or time/2023 time/2024)"),
("sparse ∩ dense", "(and time/2024 class/water)"),
("3-way and", "(and source/sentinel-2-l2a time/2024 class/vegetation)"),
("not (closed-world)", "(not source/sentinel-2-l2a)"),
("wildcard union", "rel/part_whole/within/+^/*"),
("wildcard ∩ class", "(and rel/part_whole/within/+^/* class/vegetation)"),
];
fn time_query<B: Postings, S: steeldb::TokenStore<B>>(store: &S, expr: &str, iters: u32) -> (usize, f64) {
let card = evaluate(store, expr).len();
let t = Instant::now();
for _ in 0..iters {
std::hint::black_box(evaluate(store, expr).len());
}
let us = t.elapsed().as_secs_f64() * 1e6 / iters as f64;
(card, us)
}
fn human_bytes(b: usize) -> String {
let b = b as f64;
if b >= 1e9 {
format!("{:.2} GB", b / 1e9)
} else if b >= 1e6 {
format!("{:.1} MB", b / 1e6)
} else if b >= 1e3 {
format!("{:.1} KB", b / 1e3)
} else {
format!("{b} B")
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let path = args.get(1).map(|s| s.as_str()).unwrap_or_else(|| {
eprintln!("usage: bench <situations.jsonl> [max_lines]");
std::process::exit(2);
});
let max_lines = args.get(2).and_then(|s| s.parse::<usize>().ok());
let t = Instant::now();
let (raw, n) = steeldb::jsonl::load(Path::new(path), max_lines).expect("load jsonl");
let load_s = t.elapsed().as_secs_f64();
let n_postings: usize = raw.values().map(|v| v.len()).sum();
println!(
"loaded {n} situations, {} tokens, {n_postings} postings in {load_s:.2}s\n",
raw.len()
);
let t = Instant::now();
let set_ix: InfonIndex<SetPostings> = InfonIndex::from_postings(raw.clone(), n);
let set_build = t.elapsed().as_secs_f64();
let t = Instant::now();
let roar_ix: InfonIndex<RoarPostings> = InfonIndex::from_postings(raw, n);
let roar_build = t.elapsed().as_secs_f64();
println!("{:<24} {:>14} {:>14}", "", "HashSet<u32>", "roaring");
println!(
"{:<24} {:>14} {:>14}",
"build",
format!("{set_build:.2}s"),
format!("{roar_build:.2}s")
);
println!(
"{:<24} {:>14} {:>14}",
"postings in memory",
human_bytes(set_ix.postings_native_bytes()),
human_bytes(roar_ix.postings_native_bytes())
);
println!(
"{:<24} {:>14} {:>14} (portable, identical)",
"postings delta-gap",
human_bytes(set_ix.postings_deltagap_bytes()),
human_bytes(roar_ix.postings_deltagap_bytes())
);
println!(
"\n{:<22} {:>10} {:>13} {:>13} {:>8}",
"query", "result", "HashSet µs", "roaring µs", "speedup"
);
println!("{}", "-".repeat(70));
let iters = 200u32;
for (label, expr) in QUERIES {
let (c1, set_us) = time_query(&set_ix, expr, iters);
let (c2, roar_us) = time_query(&roar_ix, expr, iters);
let parity = if c1 == c2 { "" } else { " ‼ MISMATCH" };
println!(
"{:<22} {:>10} {:>13.1} {:>13.1} {:>7.1}x{}",
label,
c1,
set_us,
roar_us,
set_us / roar_us,
parity
);
assert_eq!(c1, c2, "parity failure on `{expr}`: {c1} vs {c2}");
}
println!("\nparity: all queries returned identical cardinality on both backends.");
}