tensorlogic_oxirs_bridge/
interned_graph.rs1use std::collections::HashMap;
22use std::collections::HashSet;
23
24use crate::quad_store::QuadStore;
25use crate::rdf_bulk_io::{BulkIoError, RdfBulkImporter, RdfTriple};
26use crate::schema::nquads::Quad;
27
28#[cfg(not(test))]
31const PARALLEL_THRESHOLD: usize = 1_000_000;
32
33#[cfg(test)]
35const PARALLEL_THRESHOLD: usize = 4;
36
37pub struct InternedGraph {
41 dict: HashMap<String, u32>,
43 terms: Vec<String>,
45 pub(crate) triples: Vec<(u32, u32, u32)>,
47 by_predicate: HashMap<u32, Vec<(u32, u32)>>,
49}
50
51impl InternedGraph {
52 pub fn new() -> Self {
54 Self {
55 dict: HashMap::new(),
56 terms: Vec::new(),
57 triples: Vec::new(),
58 by_predicate: HashMap::new(),
59 }
60 }
61
62 pub fn intern(&mut self, term: &str) -> u32 {
64 if let Some(&id) = self.dict.get(term) {
65 return id;
66 }
67 let id = self.dict.len() as u32;
68 self.dict.insert(term.to_string(), id);
69 self.terms.push(term.to_string());
70 id
71 }
72
73 pub fn intern_or_none(&self, term: &str) -> Option<u32> {
75 self.dict.get(term).copied()
76 }
77
78 pub fn term(&self, id: u32) -> Option<&str> {
80 self.terms.get(id as usize).map(String::as_str)
81 }
82
83 pub fn add_triple(&mut self, s: &str, p: &str, o: &str) {
85 let s_id = self.intern(s);
86 let p_id = self.intern(p);
87 let o_id = self.intern(o);
88 self.triples.push((s_id, p_id, o_id));
89 self.by_predicate
90 .entry(p_id)
91 .or_default()
92 .push((s_id, o_id));
93 }
94
95 pub fn predicate_pairs(&self, pred_id: u32) -> &[(u32, u32)] {
98 match self.by_predicate.get(&pred_id) {
99 Some(pairs) => pairs.as_slice(),
100 None => &[],
101 }
102 }
103
104 pub fn num_entities(&self) -> usize {
106 self.dict.len()
107 }
108
109 pub fn num_triples(&self) -> usize {
111 self.triples.len()
112 }
113
114 pub fn from_rdf_triples(triples: Vec<RdfTriple>) -> Self {
119 if triples.is_empty() {
120 return Self::new();
121 }
122
123 let num_triples = triples.len();
124 let use_parallel = num_triples <= PARALLEL_THRESHOLD;
125
126 if use_parallel {
127 Self::from_rdf_triples_parallel(triples)
128 } else {
129 Self::from_rdf_triples_sequential(triples)
130 }
131 }
132
133 fn from_rdf_triples_sequential(triples: Vec<RdfTriple>) -> Self {
136 let mut g = Self::new();
137 for t in &triples {
138 g.add_triple(&t.subject, &t.predicate, &t.object);
139 }
140 g
141 }
142
143 fn from_rdf_triples_parallel(triples: Vec<RdfTriple>) -> Self {
151 let raw_cpus = std::thread::available_parallelism()
152 .map(|n| n.get())
153 .unwrap_or(2);
154 let num_cpus = raw_cpus.clamp(1, 8);
155 let num_chunks = num_cpus.min(triples.len()).max(1);
156
157 eprintln!(
158 "[InternedGraph] parallel bulk-load: {} triples, {} chunks",
159 triples.len(),
160 num_chunks
161 );
162
163 let chunk_size = triples.len().div_ceil(num_chunks);
164 let chunks: Vec<&[RdfTriple]> = triples.chunks(chunk_size).collect();
165
166 let mut per_chunk_terms: Vec<HashSet<String>> =
168 (0..chunks.len()).map(|_| HashSet::new()).collect();
169
170 std::thread::scope(|scope| {
171 let mut handles = Vec::with_capacity(chunks.len());
172 for chunk in &chunks {
173 let handle = scope.spawn(|| {
174 let mut local: HashSet<String> = HashSet::new();
175 for t in chunk.iter() {
176 local.insert(t.subject.clone());
177 local.insert(t.predicate.clone());
178 local.insert(t.object.clone());
179 }
180 local
181 });
182 handles.push(handle);
183 }
184 for (i, handle) in handles.into_iter().enumerate() {
185 per_chunk_terms[i] = handle.join().unwrap_or_default();
188 }
189 });
190
191 let mut all_terms: HashSet<String> = HashSet::new();
194 for chunk_terms in per_chunk_terms {
195 all_terms.extend(chunk_terms);
196 }
197 let mut sorted_terms: Vec<String> = all_terms.into_iter().collect();
198 sorted_terms.sort_unstable();
199
200 let mut dict: HashMap<String, u32> = HashMap::with_capacity(sorted_terms.len());
202 let mut terms_vec: Vec<String> = Vec::with_capacity(sorted_terms.len());
203 for (idx, term) in sorted_terms.into_iter().enumerate() {
204 dict.insert(term.clone(), idx as u32);
205 terms_vec.push(term);
206 }
207
208 let num_triples = triples.len();
210 let mut stored_triples: Vec<(u32, u32, u32)> = Vec::with_capacity(num_triples);
211 let mut by_predicate: HashMap<u32, Vec<(u32, u32)>> = HashMap::new();
212
213 for t in &triples {
214 let s_id = *dict.get(t.subject.as_str()).unwrap_or(&0);
216 let p_id = *dict.get(t.predicate.as_str()).unwrap_or(&0);
217 let o_id = *dict.get(t.object.as_str()).unwrap_or(&0);
218 stored_triples.push((s_id, p_id, o_id));
219 by_predicate.entry(p_id).or_default().push((s_id, o_id));
220 }
221
222 Self {
223 dict,
224 terms: terms_vec,
225 triples: stored_triples,
226 by_predicate,
227 }
228 }
229
230 pub fn into_quad_store(&self) -> QuadStore {
234 let mut qs = QuadStore::new();
235 for (s_id, p_id, o_id) in &self.triples {
236 let s = self.terms[*s_id as usize].clone();
237 let p = self.terms[*p_id as usize].clone();
238 let o = self.terms[*o_id as usize].clone();
239 qs.insert_quad(Quad {
240 subject: s,
241 predicate: p,
242 object: o,
243 graph: None,
244 });
245 }
246 qs
247 }
248}
249
250impl Default for InternedGraph {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256pub fn rdf_bulk_importer_into_interned(
261 importer: &RdfBulkImporter,
262 input: &str,
263) -> Result<InternedGraph, BulkIoError> {
264 let (triples, _stats) = importer.parse_auto(input)?;
265 Ok(InternedGraph::from_rdf_triples(triples))
266}
267
268#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
279 fn test_intern_idempotent() {
280 let mut g = InternedGraph::new();
281 let id1 = g.intern("Alice");
282 let id2 = g.intern("Alice");
283 assert_eq!(id1, id2, "intern must be idempotent");
284 let id3 = g.intern("Bob");
285 assert_ne!(id1, id3, "different terms must have different IDs");
286 }
287
288 #[test]
289 fn test_term_round_trip() {
290 let mut g = InternedGraph::new();
291 let id = g.intern("http://example.org/Alice");
292 assert_eq!(g.term(id), Some("http://example.org/Alice"));
293 }
294
295 #[test]
296 fn test_intern_or_none_known_and_unknown() {
297 let mut g = InternedGraph::new();
298 g.intern("known");
299 assert!(g.intern_or_none("known").is_some());
300 assert!(g.intern_or_none("unknown").is_none());
301 }
302
303 #[test]
306 fn test_add_triple_and_predicate_pairs() {
307 let mut g = InternedGraph::new();
308 g.add_triple("Alice", "knows", "Bob");
309 g.add_triple("Alice", "knows", "Carol");
310 g.add_triple("Bob", "age", "30");
311
312 let knows_id = g.intern_or_none("knows").expect("knows must be interned");
313 let pairs: Vec<(u32, u32)> = g.predicate_pairs(knows_id).to_vec();
315 assert_eq!(pairs.len(), 2, "should have 2 knows pairs");
316
317 let alice_id = g.intern("Alice");
318 let bob_id = g.intern("Bob");
319 let carol_id = g.intern("Carol");
320 assert!(
321 pairs.contains(&(alice_id, bob_id)),
322 "Alice knows Bob should be present"
323 );
324 assert!(
325 pairs.contains(&(alice_id, carol_id)),
326 "Alice knows Carol should be present"
327 );
328 }
329
330 #[test]
331 fn test_predicate_pairs_absent_returns_empty() {
332 let g = InternedGraph::new();
333 assert_eq!(g.predicate_pairs(99), &[]);
334 }
335
336 #[test]
339 fn test_num_entities_and_triples() {
340 let mut g = InternedGraph::new();
341 assert_eq!(g.num_entities(), 0);
342 assert_eq!(g.num_triples(), 0);
343
344 g.add_triple("A", "p", "B");
345 assert_eq!(g.num_entities(), 3);
347 assert_eq!(g.num_triples(), 1);
348
349 g.add_triple("A", "p", "B");
351 assert_eq!(g.num_entities(), 3);
352 assert_eq!(g.num_triples(), 2);
353 }
354
355 #[test]
358 fn test_from_rdf_triples_bulk() {
359 let triples = vec![
360 RdfTriple::new("Alice", "knows", "Bob"),
361 RdfTriple::new("Bob", "knows", "Carol"),
362 RdfTriple::new("Carol", "age", "25"),
363 ];
364 let g = InternedGraph::from_rdf_triples(triples);
365 assert_eq!(g.num_triples(), 3);
366 assert_eq!(g.num_entities(), 6);
368 }
369
370 #[test]
371 fn test_parallel_equivalent_to_sequential() {
372 let triples: Vec<RdfTriple> = vec![
375 RdfTriple::new("s1", "p", "o1"),
376 RdfTriple::new("s2", "p", "o2"),
377 RdfTriple::new("s3", "p", "o3"),
378 ];
379
380 let parallel = InternedGraph::from_rdf_triples(triples.clone());
381 let sequential = InternedGraph::from_rdf_triples_sequential(triples);
382
383 assert_eq!(parallel.num_triples(), sequential.num_triples());
384 assert_eq!(parallel.num_entities(), sequential.num_entities());
385
386 let p_id = parallel.intern_or_none("p").expect("p must be interned");
387 let pairs_p = parallel.predicate_pairs(p_id);
388 assert_eq!(pairs_p.len(), 3);
389
390 let q_id = sequential.intern_or_none("p").expect("p must be interned");
391 let pairs_q = sequential.predicate_pairs(q_id);
392 assert_eq!(pairs_q.len(), 3);
393 }
394
395 #[test]
398 fn test_into_quad_store_contains_correct_triples() {
399 let triples = vec![
400 RdfTriple::new("Alice", "knows", "Bob"),
401 RdfTriple::new("Bob", "knows", "Carol"),
402 ];
403 let g = InternedGraph::from_rdf_triples(triples);
404 let qs = g.into_quad_store();
405 assert_eq!(qs.total_quads(), 2);
407 let pairs = qs.query_predicate(None, "knows");
408 assert_eq!(pairs.len(), 2);
409 }
410
411 #[test]
414 fn test_bulk_importer_into_interned_ntriples() {
415 let input = "<Alice> <knows> <Bob> .\n<Bob> <knows> <Carol> .\n";
416 let importer = RdfBulkImporter::new();
417 let g =
418 rdf_bulk_importer_into_interned(&importer, input).expect("bulk import should succeed");
419 assert_eq!(g.num_triples(), 2);
420 }
421}