use std::collections::HashMap;
use crate::query::{AtomicScorer, Query, QueryConfig};
use crate::truth::Truth;
pub trait CandidateSource {
fn candidates(&self, anchor: usize, relation: usize) -> Option<Vec<usize>>;
}
impl CandidateSource for crate::FuzzyKg {
fn candidates(&self, anchor: usize, relation: usize) -> Option<Vec<usize>> {
Some(self.tails(anchor, relation))
}
}
pub fn answer_query_topk_pruned<T: Truth>(
scorer: &dyn AtomicScorer,
source: &dyn CandidateSource,
query: &Query,
config: &QueryConfig,
k: usize,
) -> Vec<(usize, f32)> {
if has_inverting_connective(query) {
return crate::query::answer_query_topk::<T>(scorer, query, config, k);
}
let sparse = eval_sparse::<T>(scorer, source, query, config);
let mut pairs: Vec<(usize, f32)> = sparse.into_iter().collect();
pairs.sort_unstable_by(|a, b| {
b.1.partial_cmp(&a.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.0.cmp(&b.0))
});
pairs.truncate(k);
pairs
}
fn has_inverting_connective(query: &Query) -> bool {
match query {
Query::Anchor { .. } => false,
Query::Project { inner, .. } => has_inverting_connective(inner),
Query::Intersection { branches } | Query::Union { branches } => {
branches.iter().any(has_inverting_connective)
}
Query::Negation { .. } | Query::Implication { .. } => true,
}
}
fn eval_sparse<T: Truth>(
scorer: &dyn AtomicScorer,
source: &dyn CandidateSource,
query: &Query,
config: &QueryConfig,
) -> HashMap<usize, f32> {
match query {
Query::Anchor { entity, relation } => hop(scorer, source, *entity, *relation),
Query::Project { inner, relation } => {
let inner_scores = eval_sparse::<T>(scorer, source, inner, config);
let pairs: Vec<(usize, f32)> = inner_scores.into_iter().collect();
let beam = top_k_descending_sparse(&pairs, config.beam_k);
let mut out: HashMap<usize, f32> = HashMap::new();
for &(v, v_score) in &beam {
if v_score <= 0.0 {
continue;
}
for (t, t_score) in hop(scorer, source, v, *relation) {
let d = T::and(v_score, t_score);
let e = out.entry(t).or_insert(0.0);
if d > *e {
*e = d;
}
}
}
out
}
Query::Intersection { branches } => {
let mut iter = branches.iter();
let Some(first) = iter.next() else {
return HashMap::new();
};
let mut acc = eval_sparse::<T>(scorer, source, first, config);
for branch in iter {
let s = eval_sparse::<T>(scorer, source, branch, config);
acc = acc
.into_iter()
.filter_map(|(e, a)| s.get(&e).map(|&b| (e, T::and(a, b))))
.collect();
}
acc
}
Query::Union { branches } => {
let mut acc: HashMap<usize, f32> = HashMap::new();
for branch in branches {
for (e, b) in eval_sparse::<T>(scorer, source, branch, config) {
let a = acc.entry(e).or_insert(T::bot());
*a = T::or(*a, b);
}
}
acc
}
Query::Negation { .. } | Query::Implication { .. } => {
crate::query::answer_query::<T>(scorer, query, config)
.into_iter()
.enumerate()
.filter(|(_, d)| *d > 0.0)
.collect()
}
}
}
fn hop(
scorer: &dyn AtomicScorer,
source: &dyn CandidateSource,
anchor: usize,
relation: usize,
) -> HashMap<usize, f32> {
match source.candidates(anchor, relation) {
Some(mut cand) => {
cand.sort_unstable();
cand.dedup();
let degrees = scorer.project_subset(anchor, relation, &cand);
cand.into_iter()
.zip(degrees)
.filter(|(_, d)| *d > 0.0)
.collect()
}
None => scorer
.project(anchor, relation)
.into_iter()
.enumerate()
.filter(|(_, d)| *d > 0.0)
.collect(),
}
}
fn top_k_descending_sparse(pairs: &[(usize, f32)], k: usize) -> Vec<(usize, f32)> {
let mut v = pairs.to_vec();
v.sort_unstable_by(|a, b| {
b.1.partial_cmp(&a.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.0.cmp(&b.0))
});
v.truncate(k);
v
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kg::FuzzyKg;
use crate::query::answer_query_topk;
use crate::truth::{Godel, Lukasiewicz, Product};
fn kg() -> FuzzyKg {
let mut kg = FuzzyKg::new(5);
kg.add_edge(2, 0, 1, 0.9); kg.add_edge(3, 0, 1, 0.8); kg.add_edge(1, 0, 0, 1.0); kg.add_edge(3, 1, 4, 0.7); kg.add_edge(2, 1, 4, 0.4); kg
}
fn assert_same_topk(dense: &[(usize, f32)], pruned: &[(usize, f32)]) {
let canon = |xs: &[(usize, f32)]| {
let mut v: Vec<(usize, f32)> = xs.iter().copied().filter(|(_, d)| *d > 0.0).collect();
v.sort_unstable_by(|a, b| {
b.1.partial_cmp(&a.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.0.cmp(&b.0))
});
v
};
let (dense, pruned) = (canon(dense), canon(pruned));
assert_eq!(dense.len(), pruned.len(), "{dense:?} vs {pruned:?}");
for ((de, dd), (pe, pd)) in dense.iter().zip(pruned.iter()) {
assert_eq!(de, pe, "{dense:?} vs {pruned:?}");
assert!((dd - pd).abs() < 1e-6, "degree {dd} vs {pd}");
}
}
#[test]
fn pruned_matches_dense_on_epfo_shapes() {
let kg = kg();
let cfg = QueryConfig::default();
let queries = [
Query::anchor(2, 0), Query::anchor(3, 0).then(0), Query::intersection(vec![Query::anchor(2, 0), Query::anchor(3, 0)]), Query::union(vec![Query::anchor(2, 1), Query::anchor(3, 1)]), Query::intersection(vec![
Query::anchor(2, 0).then(0),
Query::anchor(3, 0).then(0),
]), Query::union(vec![Query::anchor(2, 0), Query::anchor(3, 0)]).then(0), ];
for q in &queries {
assert_same_topk(
&answer_query_topk::<Godel>(&kg, q, &cfg, 5),
&answer_query_topk_pruned::<Godel>(&kg, &kg, q, &cfg, 5),
);
assert_same_topk(
&answer_query_topk::<Product>(&kg, q, &cfg, 5),
&answer_query_topk_pruned::<Product>(&kg, &kg, q, &cfg, 5),
);
assert_same_topk(
&answer_query_topk::<Lukasiewicz>(&kg, q, &cfg, 5),
&answer_query_topk_pruned::<Lukasiewicz>(&kg, &kg, q, &cfg, 5),
);
}
}
#[test]
fn negation_falls_back_to_dense() {
let kg = kg();
let cfg = QueryConfig::default();
let q = Query::intersection(vec![Query::anchor(2, 0), Query::anchor(3, 0).negate()]);
assert_same_topk(
&answer_query_topk::<Lukasiewicz>(&kg, &q, &cfg, 5),
&answer_query_topk_pruned::<Lukasiewicz>(&kg, &kg, &q, &cfg, 5),
);
}
#[test]
fn missing_candidate_is_a_recall_loss() {
struct Blind;
impl CandidateSource for Blind {
fn candidates(&self, _: usize, _: usize) -> Option<Vec<usize>> {
Some(vec![]) }
}
let kg = kg();
let out = answer_query_topk_pruned::<Godel>(
&kg,
&Blind,
&Query::anchor(2, 0),
&QueryConfig::default(),
5,
);
assert!(out.is_empty());
}
#[test]
fn none_means_no_pruning() {
struct NoOpinion;
impl CandidateSource for NoOpinion {
fn candidates(&self, _: usize, _: usize) -> Option<Vec<usize>> {
None
}
}
let kg = kg();
let cfg = QueryConfig::default();
let q = Query::anchor(3, 0).then(0);
assert_same_topk(
&answer_query_topk::<Godel>(&kg, &q, &cfg, 5),
&answer_query_topk_pruned::<Godel>(&kg, &NoOpinion, &q, &cfg, 5),
);
}
}