use rete_core::{Rete, DEFAULT_TILE_BUDGET};
use crate::commands::range_source::open_local;
use crate::commands::render::literal_lexical;
struct CommunityRecord {
community: usize,
members: Vec<String>,
text: Vec<String>,
top_types: Vec<(String, u32)>,
top_predicates: Vec<(String, u32)>,
top_terms: Vec<(String, u32)>,
}
fn top_k(counts: std::collections::HashMap<String, u32>, k: usize) -> Vec<(String, u32)> {
let mut v: Vec<(String, u32)> = counts.into_iter().collect();
v.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
v.truncate(k);
v
}
fn content_words(text: &str) -> impl Iterator<Item = String> + '_ {
const STOP: &[&str] = &[
"the", "and", "for", "with", "that", "this", "from", "are", "was", "were", "has", "have",
"had", "into", "over", "its", "their", "they", "them", "but", "not", "all", "can", "via",
"use", "uses", "using", "based", "study", "studies", "between", "which", "such", "these",
"those", "than", "then", "also", "more", "most", "may", "our", "new",
];
text.split(|c: char| !c.is_alphanumeric())
.map(|w| w.to_ascii_lowercase())
.filter(|w| w.len() >= 3 && !STOP.contains(&w.as_str()))
}
fn collect_communities(
rete: &Rete,
round: Option<usize>,
min_size: usize,
profile: bool,
predicate: Option<&str>,
) -> anyhow::Result<Vec<CommunityRecord>> {
use std::collections::{HashMap, HashSet};
let dict = rete.dictionary();
let ids = match predicate {
None => rete.match_ids((None, None, None)),
Some(p) => {
let pid = dict
.predicate_id(p)
.ok_or_else(|| anyhow::anyhow!("predicate not found in graph: {p}"))?;
rete.match_ids((None, Some(pid), None))
}
};
let g = rete_core::project_graph(dict, &ids);
let dend = rete_core::build_dendrogram(&g);
let round = round.unwrap_or_else(|| {
rete_core::choose_round_for_budget(dict, &ids, &dend, DEFAULT_TILE_BUDGET)
});
let tiles = rete_core::tile_by_community(dict, &ids, &dend, round);
const RDF_TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
let rdf_type_pid = dict.predicate_id(RDF_TYPE);
let mut out = Vec::new();
for tile in &tiles {
let mut seen = HashSet::new();
let mut members = Vec::new();
let mut text = Vec::new();
let mut types: HashMap<String, u32> = HashMap::new();
let mut preds: HashMap<String, u32> = HashMap::new();
let mut terms: HashMap<String, u32> = HashMap::new();
for &(s, p, o) in &tile.triples {
if let Some(term) = dict.subject_term(s) {
if seen.insert(term.clone()) {
members.push(term);
}
}
let obj = dict.object_term(o);
if let Some(ref obj) = obj {
if let Some(lex) = literal_lexical(obj) {
if profile {
for w in content_words(&lex) {
*terms.entry(w).or_default() += 1;
}
}
text.push(lex);
}
}
if profile {
if let Some(pt) = dict.predicate_term(p) {
*preds.entry(pt).or_default() += 1;
}
if Some(p) == rdf_type_pid {
if let Some(ot) = obj {
*types.entry(ot).or_default() += 1;
}
}
}
}
if members.len() >= min_size {
out.push(CommunityRecord {
community: tile.community,
members,
text,
top_types: if profile { top_k(types, 5) } else { Vec::new() },
top_predicates: if profile { top_k(preds, 5) } else { Vec::new() },
top_terms: if profile { top_k(terms, 8) } else { Vec::new() },
});
}
}
Ok(out)
}
pub(crate) fn communities(
file: &str,
json: bool,
round: Option<usize>,
min_size: usize,
profile: bool,
predicate: Option<&str>,
) -> anyhow::Result<()> {
let rete = open_local(file)?;
let records = collect_communities(&rete, round, min_size, profile, predicate)?;
if json {
use serde_json::{json, Value};
let arr: Vec<Value> = records
.iter()
.map(|r| {
let mut obj = json!({
"community": r.community,
"size": r.members.len(),
"members": r.members,
"text": r.text,
});
if profile {
obj["profile"] = json!({
"types": r.top_types,
"predicates": r.top_predicates,
"terms": r.top_terms,
});
}
obj
})
.collect();
let body = json!({
"schemaVersion": crate::JSON_SCHEMA_VERSION,
"communities": Value::Array(arr),
});
println!(
"{}",
serde_json::to_string_pretty(&body).unwrap_or_default()
);
return Ok(());
}
if records.is_empty() {
println!("(no communities at this round / min-size)");
return Ok(());
}
let fmt_pairs = |pairs: &[(String, u32)]| {
pairs
.iter()
.map(|(k, n)| format!("{k} ({n})"))
.collect::<Vec<_>>()
.join(", ")
};
for r in &records {
println!(
"community {}: {} members, {} literals",
r.community,
r.members.len(),
r.text.len()
);
if profile {
if !r.top_terms.is_empty() {
println!(" topic words : {}", fmt_pairs(&r.top_terms));
}
if !r.top_types.is_empty() {
println!(" classes : {}", fmt_pairs(&r.top_types));
}
if !r.top_predicates.is_empty() {
println!(" predicates : {}", fmt_pairs(&r.top_predicates));
}
}
for m in r.members.iter().take(5) {
println!(" {m}");
}
if r.members.len() > 5 {
println!(" … ({} more)", r.members.len() - 5);
}
}
Ok(())
}