use std::collections::{HashMap, HashSet};
use std::error::Error;
use serde_json::{Map, Value};
use velesdb_memory::DynEmbedder;
use velesdb_memory::{ColumnFilter, ColumnOp, MemoryService, OllamaEmbedder, DEFAULT_OLLAMA_URL};
const CITIES: &[&str] = &[
"Lyon",
"Hamburg",
"Turin",
"Bristol",
"Gothenburg",
"Nantes",
"Kyoto",
"Austin",
"Porto",
"Leeds",
];
const ORGS: &[&str] = &[
"Avenor",
"Brightwell",
"Caldera",
"Dornier",
"Everline",
"Fenix",
"Gravon",
"Halcyon",
"Iberis",
"Juno",
];
const ROLES: &[&str] = &["chief executive officer", "chief technology officer"];
const START: i64 = 1985;
const TENURE: i64 = 4;
const HOLDERS: i64 = 8;
struct Args {
k: usize,
embed: String,
hops: usize,
boost: f64,
}
struct Probe {
city: usize,
role: usize,
lo: i64,
hi: i64,
gold: Vec<u64>,
}
fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse()?;
let store = build(&args)?;
let probes = store.probes();
let mut report = Report::default();
for p in &probes {
let q = format!(
"Who was the {} of the company headquartered in {} from {} to {}?",
ROLES[p.role], CITIES[p.city], p.lo, p.hi
);
report.record(
store.recall_arm(&q, p, &args, false, false)?, store.recall_arm(&q, p, &args, true, false)?, store.recall_arm(&q, p, &args, false, true)?, store.recall_arm(&q, p, &args, true, true)?, );
}
report.print(&args, probes.len());
Ok(())
}
struct Store {
svc: MemoryService<DynEmbedder>,
roles: HashMap<u64, (usize, usize, i64)>,
}
impl Store {
fn is_role(&self, id: u64) -> bool {
self.roles.contains_key(&id)
}
fn probes(&self) -> Vec<Probe> {
let mid = START + (HOLDERS / 2) * TENURE;
let mut out = Vec::new();
for city in 0..CITIES.len() {
for role in 0..ROLES.len() {
for half in [2, 8] {
let (lo, hi) = (mid - half, mid + half);
let gold: Vec<u64> = self
.roles
.iter()
.filter(|(_, (c, r, y))| *c == city && *r == role && *y >= lo && *y <= hi)
.map(|(id, _)| *id)
.collect();
if !gold.is_empty() {
out.push(Probe {
city,
role,
lo,
hi,
gold,
});
}
}
}
}
out
}
fn recall_arm(
&self,
q: &str,
p: &Probe,
args: &Args,
column: bool,
graph: bool,
) -> Result<f64, Box<dyn Error>> {
let pool: Vec<(u64, f64)> = if column {
let filters = vec![
ColumnFilter {
field: "year".into(),
op: ColumnOp::Ge,
value: Value::from(p.lo),
},
ColumnFilter {
field: "year".into(),
op: ColumnOp::Le,
value: Value::from(p.hi),
},
];
self.svc.recall_where(q, args.k * 12 + 16, &filters)?
} else {
self.svc.recall(q, args.k * 12 + 16, None)?
}
.into_iter()
.filter(|r| self.is_role(r.id))
.map(|r| (r.id, f64::from(r.score)))
.collect();
let max = pool
.iter()
.map(|(_, s)| *s)
.fold(f64::MIN, f64::max)
.max(f64::EPSILON);
let mut cand: HashMap<u64, f64> = pool.iter().map(|(id, s)| (*id, s / max)).collect();
if graph {
for n in self.svc.why(q, args.hops, None)?.nodes {
if n.hop >= 1 && self.is_role(n.id) {
let in_range = self
.roles
.get(&n.id)
.is_some_and(|(_, _, y)| !column || (*y >= p.lo && *y <= p.hi));
if in_range {
*cand.entry(n.id).or_insert(0.0) += args.boost;
}
}
}
}
let mut scored: Vec<(u64, f64)> = cand.into_iter().collect();
scored.sort_by(|a, b| b.1.total_cmp(&a.1));
let top: Vec<u64> = scored.into_iter().take(args.k).map(|(id, _)| id).collect();
Ok(recall(&top, &p.gold))
}
}
fn recall(retrieved: &[u64], gold: &[u64]) -> f64 {
let hit = gold.iter().filter(|g| retrieved.contains(g)).count();
if gold.is_empty() {
0.0
} else {
f64::from(u32::try_from(hit).unwrap_or(u32::MAX))
/ f64::from(u32::try_from(gold.len()).unwrap_or(u32::MAX))
}
}
fn build(args: &Args) -> Result<Store, Box<dyn Error>> {
let dir = std::env::temp_dir().join(format!("velesdb-triengine-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let embedder: DynEmbedder = Box::new(OllamaEmbedder::new(DEFAULT_OLLAMA_URL, &args.embed)?);
let svc = MemoryService::open(dir, embedder)?;
let mut roles = HashMap::new();
let mut edges: HashSet<(u64, u64)> = HashSet::new();
for (city, city_name) in CITIES.iter().enumerate() {
let org = format!("{} Corporation", ORGS[city]);
let org_hub = svc.remember(&format!("Entity: {org}"), &[], None)?;
let loc = svc.remember(
&format!("{org} is headquartered in {city_name}."),
&[],
None,
)?;
relate(&svc, &mut edges, loc, org_hub)?;
for (role, role_name) in ROLES.iter().enumerate() {
for h in 0..HOLDERS {
let year = START + h * TENURE;
let person = format!("{}_{}_{h}", ORGS[city], abbr(role_name));
let text = format!("{person} was the {role_name} of {org} in {year}.");
let mut meta = Map::new();
meta.insert("year".into(), Value::from(year));
let id = svc.remember(&text, &[], Some(&meta))?;
roles.insert(id, (city, role, year));
relate(&svc, &mut edges, id, org_hub)?; }
}
}
Ok(Store { svc, roles })
}
fn relate(
svc: &MemoryService<DynEmbedder>,
edges: &mut HashSet<(u64, u64)>,
a: u64,
b: u64,
) -> Result<(), Box<dyn Error>> {
if edges.insert((a, b)) {
svc.relate(a, b, "about")?;
}
if edges.insert((b, a)) {
svc.relate(b, a, "mentions")?;
}
Ok(())
}
fn abbr(role: &str) -> String {
role.split_whitespace()
.map(|w| w.chars().next().unwrap_or('x'))
.collect()
}
#[derive(Default)]
struct Report {
n: u32,
vector: f64,
column: f64,
graph: f64,
both: f64,
}
impl Report {
fn record(&mut self, v: f64, c: f64, g: f64, b: f64) {
self.n += 1;
self.vector += v;
self.column += c;
self.graph += g;
self.both += b;
}
fn print(&self, args: &Args, n: usize) {
println!("\nVelesDB-memory — tri-engine compounding (multi-hop AND time-scoped)");
println!(
"embedder: ollama / {} · {n} probes · k={} · synthetic, generation-free\n",
args.embed, args.k
);
let p = |s: f64| 100.0 * s / f64::from(self.n.max(1));
println!(" answer-bearing recall@k:");
println!(" vector only {:.1}%", p(self.vector));
println!(
" + ColumnStore (year filter) {:.1}% ({:+.1} pp)",
p(self.column),
p(self.column) - p(self.vector)
);
println!(
" + Graph (city→company) {:.1}% ({:+.1} pp)",
p(self.graph),
p(self.graph) - p(self.vector)
);
println!(
" + both engines {:.1}% ({:+.1} pp)",
p(self.both),
p(self.both) - p(self.vector)
);
println!("\n → the engines compound: each fixes one axis (Graph the company, ColumnStore the period); together they nail both.");
}
}
impl Args {
fn parse() -> Result<Self, Box<dyn Error>> {
let mut args = Args {
k: 5,
embed: "mxbai-embed-large".to_string(),
hops: 2,
boost: 0.30,
};
let raw: Vec<String> = std::env::args().skip(1).collect();
let mut i = 0;
while i < raw.len() {
let flag = &raw[i];
let val = raw
.get(i + 1)
.ok_or_else(|| format!("{flag} needs a value"))?;
match flag.as_str() {
"--k" => args.k = val.parse()?,
"--embed-model" => args.embed.clone_from(val),
"--hops" => args.hops = val.parse()?,
"--graph-boost" => args.boost = val.parse()?,
other => return Err(format!("unknown argument: {other}").into()),
}
i += 2;
}
Ok(args)
}
}