use std::collections::HashMap;
use std::collections::HashSet;
use crate::quad_store::QuadStore;
use crate::rdf_bulk_io::{BulkIoError, RdfBulkImporter, RdfTriple};
use crate::schema::nquads::Quad;
#[cfg(not(test))]
const PARALLEL_THRESHOLD: usize = 1_000_000;
#[cfg(test)]
const PARALLEL_THRESHOLD: usize = 4;
pub struct InternedGraph {
dict: HashMap<String, u32>,
terms: Vec<String>,
pub(crate) triples: Vec<(u32, u32, u32)>,
by_predicate: HashMap<u32, Vec<(u32, u32)>>,
}
impl InternedGraph {
pub fn new() -> Self {
Self {
dict: HashMap::new(),
terms: Vec::new(),
triples: Vec::new(),
by_predicate: HashMap::new(),
}
}
pub fn intern(&mut self, term: &str) -> u32 {
if let Some(&id) = self.dict.get(term) {
return id;
}
let id = self.dict.len() as u32;
self.dict.insert(term.to_string(), id);
self.terms.push(term.to_string());
id
}
pub fn intern_or_none(&self, term: &str) -> Option<u32> {
self.dict.get(term).copied()
}
pub fn term(&self, id: u32) -> Option<&str> {
self.terms.get(id as usize).map(String::as_str)
}
pub fn add_triple(&mut self, s: &str, p: &str, o: &str) {
let s_id = self.intern(s);
let p_id = self.intern(p);
let o_id = self.intern(o);
self.triples.push((s_id, p_id, o_id));
self.by_predicate
.entry(p_id)
.or_default()
.push((s_id, o_id));
}
pub fn predicate_pairs(&self, pred_id: u32) -> &[(u32, u32)] {
match self.by_predicate.get(&pred_id) {
Some(pairs) => pairs.as_slice(),
None => &[],
}
}
pub fn num_entities(&self) -> usize {
self.dict.len()
}
pub fn num_triples(&self) -> usize {
self.triples.len()
}
pub fn from_rdf_triples(triples: Vec<RdfTriple>) -> Self {
if triples.is_empty() {
return Self::new();
}
let num_triples = triples.len();
let use_parallel = num_triples <= PARALLEL_THRESHOLD;
if use_parallel {
Self::from_rdf_triples_parallel(triples)
} else {
Self::from_rdf_triples_sequential(triples)
}
}
fn from_rdf_triples_sequential(triples: Vec<RdfTriple>) -> Self {
let mut g = Self::new();
for t in &triples {
g.add_triple(&t.subject, &t.predicate, &t.object);
}
g
}
fn from_rdf_triples_parallel(triples: Vec<RdfTriple>) -> Self {
let raw_cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(2);
let num_cpus = raw_cpus.clamp(1, 8);
let num_chunks = num_cpus.min(triples.len()).max(1);
eprintln!(
"[InternedGraph] parallel bulk-load: {} triples, {} chunks",
triples.len(),
num_chunks
);
let chunk_size = triples.len().div_ceil(num_chunks);
let chunks: Vec<&[RdfTriple]> = triples.chunks(chunk_size).collect();
let mut per_chunk_terms: Vec<HashSet<String>> =
(0..chunks.len()).map(|_| HashSet::new()).collect();
std::thread::scope(|scope| {
let mut handles = Vec::with_capacity(chunks.len());
for chunk in &chunks {
let handle = scope.spawn(|| {
let mut local: HashSet<String> = HashSet::new();
for t in chunk.iter() {
local.insert(t.subject.clone());
local.insert(t.predicate.clone());
local.insert(t.object.clone());
}
local
});
handles.push(handle);
}
for (i, handle) in handles.into_iter().enumerate() {
per_chunk_terms[i] = handle.join().unwrap_or_default();
}
});
let mut all_terms: HashSet<String> = HashSet::new();
for chunk_terms in per_chunk_terms {
all_terms.extend(chunk_terms);
}
let mut sorted_terms: Vec<String> = all_terms.into_iter().collect();
sorted_terms.sort_unstable();
let mut dict: HashMap<String, u32> = HashMap::with_capacity(sorted_terms.len());
let mut terms_vec: Vec<String> = Vec::with_capacity(sorted_terms.len());
for (idx, term) in sorted_terms.into_iter().enumerate() {
dict.insert(term.clone(), idx as u32);
terms_vec.push(term);
}
let num_triples = triples.len();
let mut stored_triples: Vec<(u32, u32, u32)> = Vec::with_capacity(num_triples);
let mut by_predicate: HashMap<u32, Vec<(u32, u32)>> = HashMap::new();
for t in &triples {
let s_id = *dict.get(t.subject.as_str()).unwrap_or(&0);
let p_id = *dict.get(t.predicate.as_str()).unwrap_or(&0);
let o_id = *dict.get(t.object.as_str()).unwrap_or(&0);
stored_triples.push((s_id, p_id, o_id));
by_predicate.entry(p_id).or_default().push((s_id, o_id));
}
Self {
dict,
terms: terms_vec,
triples: stored_triples,
by_predicate,
}
}
pub fn into_quad_store(&self) -> QuadStore {
let mut qs = QuadStore::new();
for (s_id, p_id, o_id) in &self.triples {
let s = self.terms[*s_id as usize].clone();
let p = self.terms[*p_id as usize].clone();
let o = self.terms[*o_id as usize].clone();
qs.insert_quad(Quad {
subject: s,
predicate: p,
object: o,
graph: None,
});
}
qs
}
}
impl Default for InternedGraph {
fn default() -> Self {
Self::new()
}
}
pub fn rdf_bulk_importer_into_interned(
importer: &RdfBulkImporter,
input: &str,
) -> Result<InternedGraph, BulkIoError> {
let (triples, _stats) = importer.parse_auto(input)?;
Ok(InternedGraph::from_rdf_triples(triples))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_intern_idempotent() {
let mut g = InternedGraph::new();
let id1 = g.intern("Alice");
let id2 = g.intern("Alice");
assert_eq!(id1, id2, "intern must be idempotent");
let id3 = g.intern("Bob");
assert_ne!(id1, id3, "different terms must have different IDs");
}
#[test]
fn test_term_round_trip() {
let mut g = InternedGraph::new();
let id = g.intern("http://example.org/Alice");
assert_eq!(g.term(id), Some("http://example.org/Alice"));
}
#[test]
fn test_intern_or_none_known_and_unknown() {
let mut g = InternedGraph::new();
g.intern("known");
assert!(g.intern_or_none("known").is_some());
assert!(g.intern_or_none("unknown").is_none());
}
#[test]
fn test_add_triple_and_predicate_pairs() {
let mut g = InternedGraph::new();
g.add_triple("Alice", "knows", "Bob");
g.add_triple("Alice", "knows", "Carol");
g.add_triple("Bob", "age", "30");
let knows_id = g.intern_or_none("knows").expect("knows must be interned");
let pairs: Vec<(u32, u32)> = g.predicate_pairs(knows_id).to_vec();
assert_eq!(pairs.len(), 2, "should have 2 knows pairs");
let alice_id = g.intern("Alice");
let bob_id = g.intern("Bob");
let carol_id = g.intern("Carol");
assert!(
pairs.contains(&(alice_id, bob_id)),
"Alice knows Bob should be present"
);
assert!(
pairs.contains(&(alice_id, carol_id)),
"Alice knows Carol should be present"
);
}
#[test]
fn test_predicate_pairs_absent_returns_empty() {
let g = InternedGraph::new();
assert_eq!(g.predicate_pairs(99), &[]);
}
#[test]
fn test_num_entities_and_triples() {
let mut g = InternedGraph::new();
assert_eq!(g.num_entities(), 0);
assert_eq!(g.num_triples(), 0);
g.add_triple("A", "p", "B");
assert_eq!(g.num_entities(), 3);
assert_eq!(g.num_triples(), 1);
g.add_triple("A", "p", "B");
assert_eq!(g.num_entities(), 3);
assert_eq!(g.num_triples(), 2);
}
#[test]
fn test_from_rdf_triples_bulk() {
let triples = vec![
RdfTriple::new("Alice", "knows", "Bob"),
RdfTriple::new("Bob", "knows", "Carol"),
RdfTriple::new("Carol", "age", "25"),
];
let g = InternedGraph::from_rdf_triples(triples);
assert_eq!(g.num_triples(), 3);
assert_eq!(g.num_entities(), 6);
}
#[test]
fn test_parallel_equivalent_to_sequential() {
let triples: Vec<RdfTriple> = vec![
RdfTriple::new("s1", "p", "o1"),
RdfTriple::new("s2", "p", "o2"),
RdfTriple::new("s3", "p", "o3"),
];
let parallel = InternedGraph::from_rdf_triples(triples.clone());
let sequential = InternedGraph::from_rdf_triples_sequential(triples);
assert_eq!(parallel.num_triples(), sequential.num_triples());
assert_eq!(parallel.num_entities(), sequential.num_entities());
let p_id = parallel.intern_or_none("p").expect("p must be interned");
let pairs_p = parallel.predicate_pairs(p_id);
assert_eq!(pairs_p.len(), 3);
let q_id = sequential.intern_or_none("p").expect("p must be interned");
let pairs_q = sequential.predicate_pairs(q_id);
assert_eq!(pairs_q.len(), 3);
}
#[test]
fn test_into_quad_store_contains_correct_triples() {
let triples = vec![
RdfTriple::new("Alice", "knows", "Bob"),
RdfTriple::new("Bob", "knows", "Carol"),
];
let g = InternedGraph::from_rdf_triples(triples);
let qs = g.into_quad_store();
assert_eq!(qs.total_quads(), 2);
let pairs = qs.query_predicate(None, "knows");
assert_eq!(pairs.len(), 2);
}
#[test]
fn test_bulk_importer_into_interned_ntriples() {
let input = "<Alice> <knows> <Bob> .\n<Bob> <knows> <Carol> .\n";
let importer = RdfBulkImporter::new();
let g =
rdf_bulk_importer_into_interned(&importer, input).expect("bulk import should succeed");
assert_eq!(g.num_triples(), 2);
}
}