Skip to main content

oxirs_tdb/compression/
dictionary.rs

1//! Adaptive dictionary compression implementation
2
3use crate::compression::{
4    AdvancedCompressionType, CompressedData, CompressionAlgorithm, CompressionMetadata,
5    MAX_DICTIONARY_SIZE,
6};
7use anyhow::{anyhow, Result};
8use std::collections::HashMap;
9use std::time::Instant;
10
11/// Adaptive dictionary compression
12pub struct AdaptiveDictionary {
13    /// Dictionary mapping strings to IDs
14    string_to_id: HashMap<Vec<u8>, u32>,
15    /// Dictionary mapping IDs to strings  
16    id_to_string: HashMap<u32, Vec<u8>>,
17    /// Next available ID
18    next_id: u32,
19}
20
21impl AdaptiveDictionary {
22    /// Create new adaptive dictionary
23    pub fn new() -> Self {
24        Self {
25            string_to_id: HashMap::new(),
26            id_to_string: HashMap::new(),
27            next_id: 0,
28        }
29    }
30
31    /// Add string to dictionary and return ID
32    pub fn add_string(&mut self, data: &[u8]) -> Result<u32> {
33        if let Some(&id) = self.string_to_id.get(data) {
34            return Ok(id);
35        }
36
37        if self.string_to_id.len() >= MAX_DICTIONARY_SIZE {
38            return Err(anyhow!(
39                "Dictionary size limit exceeded: {} entries (max: {})",
40                self.string_to_id.len(),
41                MAX_DICTIONARY_SIZE
42            ));
43        }
44
45        let id = self.next_id;
46        self.next_id += 1;
47
48        self.string_to_id.insert(data.to_vec(), id);
49        self.id_to_string.insert(id, data.to_vec());
50
51        Ok(id)
52    }
53
54    /// Get string by ID
55    pub fn get_string(&self, id: u32) -> Option<&[u8]> {
56        self.id_to_string.get(&id).map(|v| v.as_slice())
57    }
58
59    /// Get ID by string
60    pub fn get_id(&self, data: &[u8]) -> Option<u32> {
61        self.string_to_id.get(data).copied()
62    }
63
64    /// Compress data using dictionary
65    pub fn compress_data(&mut self, data: &[u8]) -> Result<Vec<u8>> {
66        if data.is_empty() {
67            return Ok(Vec::new());
68        }
69
70        let mut compressed = Vec::new();
71
72        // Simple word-based compression (split on whitespace)
73        let mut current_word = Vec::new();
74
75        for &byte in data {
76            if byte.is_ascii_whitespace() {
77                if !current_word.is_empty() {
78                    let id = self.add_string(&current_word)?;
79                    compressed.extend_from_slice(&id.to_le_bytes());
80                    current_word.clear();
81                }
82                // Store whitespace as-is (negative ID)
83                compressed.extend_from_slice(&(u32::MAX - byte as u32).to_le_bytes());
84            } else {
85                current_word.push(byte);
86            }
87        }
88
89        // Handle final word
90        if !current_word.is_empty() {
91            let id = self.add_string(&current_word)?;
92            compressed.extend_from_slice(&id.to_le_bytes());
93        }
94
95        Ok(compressed)
96    }
97
98    /// Decompress data using dictionary
99    pub fn decompress_data(&self, compressed: &[u8]) -> Result<Vec<u8>> {
100        if compressed.is_empty() {
101            return Ok(Vec::new());
102        }
103
104        if compressed.len() % 4 != 0 {
105            return Err(anyhow!("Invalid compressed data length"));
106        }
107
108        let mut decompressed = Vec::new();
109
110        for chunk in compressed.chunks_exact(4) {
111            let id = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
112
113            if id > u32::MAX - 256 {
114                // It's a whitespace character
115                let byte = (u32::MAX - id) as u8;
116                decompressed.push(byte);
117            } else {
118                // It's a dictionary ID
119                if let Some(data) = self.get_string(id) {
120                    decompressed.extend_from_slice(data);
121                } else {
122                    return Err(anyhow!("Invalid dictionary ID: {}", id));
123                }
124            }
125        }
126
127        Ok(decompressed)
128    }
129
130    /// Serialize dictionary for storage
131    pub fn serialize_dictionary(&self) -> Vec<u8> {
132        let mut serialized = Vec::new();
133
134        // Store dictionary size
135        let size = self.id_to_string.len() as u32;
136        serialized.extend_from_slice(&size.to_le_bytes());
137
138        // Store each entry
139        for (&id, data) in &self.id_to_string {
140            serialized.extend_from_slice(&id.to_le_bytes());
141            let len = data.len() as u32;
142            serialized.extend_from_slice(&len.to_le_bytes());
143            serialized.extend_from_slice(data);
144        }
145
146        serialized
147    }
148
149    /// Deserialize dictionary from storage
150    pub fn deserialize_dictionary(&mut self, data: &[u8]) -> Result<()> {
151        if data.len() < 4 {
152            return Err(anyhow!("Invalid dictionary data"));
153        }
154
155        let size = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
156        let mut offset = 4;
157
158        self.string_to_id.clear();
159        self.id_to_string.clear();
160        self.next_id = 0;
161
162        for _ in 0..size {
163            if offset + 8 > data.len() {
164                return Err(anyhow!("Truncated dictionary data"));
165            }
166
167            let id = u32::from_le_bytes([
168                data[offset],
169                data[offset + 1],
170                data[offset + 2],
171                data[offset + 3],
172            ]);
173            offset += 4;
174
175            let len = u32::from_le_bytes([
176                data[offset],
177                data[offset + 1],
178                data[offset + 2],
179                data[offset + 3],
180            ]) as usize;
181            offset += 4;
182
183            if offset + len > data.len() {
184                return Err(anyhow!("Truncated dictionary entry"));
185            }
186
187            let string_data = data[offset..offset + len].to_vec();
188            offset += len;
189
190            self.string_to_id.insert(string_data.clone(), id);
191            self.id_to_string.insert(id, string_data);
192            self.next_id = self.next_id.max(id + 1);
193        }
194
195        Ok(())
196    }
197
198    /// Get dictionary statistics
199    pub fn statistics(&self) -> HashMap<String, String> {
200        let mut stats = HashMap::new();
201        stats.insert("entries".to_string(), self.string_to_id.len().to_string());
202        stats.insert("next_id".to_string(), self.next_id.to_string());
203
204        if !self.id_to_string.is_empty() {
205            let avg_length: f64 = self.id_to_string.values().map(|v| v.len()).sum::<usize>() as f64
206                / self.id_to_string.len() as f64;
207            stats.insert("avg_entry_length".to_string(), format!("{avg_length:.2}"));
208        }
209
210        stats
211    }
212}
213
214impl Default for AdaptiveDictionary {
215    fn default() -> Self {
216        Self::new()
217    }
218}
219
220impl CompressionAlgorithm for AdaptiveDictionary {
221    fn compress(&self, data: &[u8]) -> Result<CompressedData> {
222        let mut dict = self.clone();
223        let start = Instant::now();
224        let compressed_data = dict.compress_data(data)?;
225        let compression_time = start.elapsed();
226
227        // Combine dictionary and compressed data
228        let dict_data = dict.serialize_dictionary();
229        let mut final_data = Vec::new();
230        final_data.extend_from_slice(&(dict_data.len() as u32).to_le_bytes());
231        final_data.extend_from_slice(&dict_data);
232        final_data.extend_from_slice(&compressed_data);
233
234        let metadata = CompressionMetadata {
235            algorithm: AdvancedCompressionType::AdaptiveDictionary,
236            original_size: data.len() as u64,
237            compressed_size: final_data.len() as u64,
238            compression_time_us: compression_time.as_micros() as u64,
239            metadata: dict.statistics(),
240        };
241
242        Ok(CompressedData {
243            data: final_data,
244            metadata,
245        })
246    }
247
248    fn decompress(&self, compressed: &CompressedData) -> Result<Vec<u8>> {
249        if compressed.metadata.algorithm != AdvancedCompressionType::AdaptiveDictionary {
250            return Err(anyhow!(
251                "Invalid compression algorithm: expected AdaptiveDictionary, got {}",
252                compressed.metadata.algorithm
253            ));
254        }
255
256        let data = &compressed.data;
257        if data.len() < 4 {
258            return Err(anyhow!("Invalid compressed data"));
259        }
260
261        // Extract dictionary
262        let dict_len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
263        if data.len() < 4 + dict_len {
264            return Err(anyhow!("Invalid compressed data"));
265        }
266
267        let mut dict = AdaptiveDictionary::new();
268        dict.deserialize_dictionary(&data[4..4 + dict_len])?;
269
270        // Decompress data
271        let compressed_data = &data[4 + dict_len..];
272        dict.decompress_data(compressed_data)
273    }
274
275    fn algorithm_type(&self) -> AdvancedCompressionType {
276        AdvancedCompressionType::AdaptiveDictionary
277    }
278}
279
280// Implement Clone for AdaptiveDictionary
281impl Clone for AdaptiveDictionary {
282    fn clone(&self) -> Self {
283        Self {
284            string_to_id: self.string_to_id.clone(),
285            id_to_string: self.id_to_string.clone(),
286            next_id: self.next_id,
287        }
288    }
289}
290
291/// Dictionary encoding specifically for RDF triple components (subjects, predicates, objects).
292///
293/// `TripleDictionary` maps RDF URI strings and literals to compact 64-bit integer IDs,
294/// dramatically reducing storage size for large RDF graphs where the same URIs appear
295/// across millions of triples.  Subjects, predicates and objects maintain separate
296/// namespaces so that ID `42` for a subject is unambiguous relative to predicate ID `42`.
297///
298/// # Compression Characteristics
299///
300/// For a typical SPARQL dataset:
301/// - A 40-byte URI becomes an 8-byte u64 NodeID (~5x size reduction per occurrence)
302/// - With 1M triples sharing 10K unique predicates the savings are dramatic
303/// - `compression_ratio()` reflects actual savings in the live dictionary
304///
305/// # Example
306///
307/// ```rust,no_run
308/// use oxirs_tdb::compression::dictionary::TripleDictionary;
309///
310/// let mut dict = TripleDictionary::new();
311/// let (s_id, p_id, o_id) = dict.encode_triple(
312///     "http://example.org/Alice",
313///     "http://schema.org/knows",
314///     "http://example.org/Bob",
315/// );
316/// let (s, p, o) = dict.decode_triple(s_id, p_id, o_id).unwrap();
317/// assert_eq!(s, "http://example.org/Alice");
318/// ```
319#[derive(Debug)]
320pub struct TripleDictionary {
321    /// Forward mapping: subject string -> subject ID
322    subject_dict: HashMap<String, u64>,
323    /// Forward mapping: predicate string -> predicate ID
324    predicate_dict: HashMap<String, u64>,
325    /// Forward mapping: object string -> object ID
326    object_dict: HashMap<String, u64>,
327    /// Reverse mapping: ID -> string (shared across all three components;
328    /// IDs are globally unique across the entire dictionary)
329    reverse: HashMap<u64, String>,
330    /// Counter for next ID to assign (monotonically increasing)
331    next_id: u64,
332}
333
334impl TripleDictionary {
335    /// Create a new empty `TripleDictionary`.
336    pub fn new() -> Self {
337        Self {
338            subject_dict: HashMap::new(),
339            predicate_dict: HashMap::new(),
340            object_dict: HashMap::new(),
341            reverse: HashMap::new(),
342            next_id: 0,
343        }
344    }
345
346    /// Encode a triple (subject, predicate, object) as three 64-bit integer IDs.
347    ///
348    /// If a component has been seen before its existing ID is returned;
349    /// otherwise a new monotonically-increasing ID is assigned.
350    ///
351    /// Returns `(subject_id, predicate_id, object_id)`.
352    pub fn encode_triple(&mut self, s: &str, p: &str, o: &str) -> (u64, u64, u64) {
353        let s_id = self.intern_subject(s);
354        let p_id = self.intern_predicate(p);
355        let o_id = self.intern_object(o);
356        (s_id, p_id, o_id)
357    }
358
359    /// Decode a triple from three 64-bit integer IDs back to strings.
360    ///
361    /// Returns `Some((subject, predicate, object))` or `None` if any ID is unknown.
362    pub fn decode_triple(
363        &self,
364        s_id: u64,
365        p_id: u64,
366        o_id: u64,
367    ) -> Option<(String, String, String)> {
368        let s = self.reverse.get(&s_id)?.clone();
369        let p = self.reverse.get(&p_id)?.clone();
370        let o = self.reverse.get(&o_id)?.clone();
371        Some((s, p, o))
372    }
373
374    /// Return the total number of unique strings (subjects + predicates + objects combined)
375    /// currently interned in the dictionary.
376    pub fn size(&self) -> usize {
377        self.reverse.len()
378    }
379
380    /// Estimate the compression ratio achieved by this dictionary.
381    ///
382    /// The ratio is computed as:
383    ///   `(total_string_bytes) / (entries * 8)`
384    ///
385    /// where `entries * 8` is the storage cost of the IDs alone (8 bytes per u64).
386    /// A ratio > 1.0 means the dictionary saves space; typical values are 4-10x for
387    /// RDF URI-heavy workloads.
388    pub fn compression_ratio(&self) -> f64 {
389        if self.reverse.is_empty() {
390            return 1.0;
391        }
392        let total_string_bytes: usize = self.reverse.values().map(|s| s.len()).sum();
393        let id_bytes = self.reverse.len() * 8; // 8 bytes per u64 ID
394        if id_bytes == 0 {
395            return 1.0;
396        }
397        total_string_bytes as f64 / id_bytes as f64
398    }
399
400    /// Look up the ID for a subject string, returning `None` if not interned.
401    pub fn get_subject_id(&self, s: &str) -> Option<u64> {
402        self.subject_dict.get(s).copied()
403    }
404
405    /// Look up the ID for a predicate string, returning `None` if not interned.
406    pub fn get_predicate_id(&self, p: &str) -> Option<u64> {
407        self.predicate_dict.get(p).copied()
408    }
409
410    /// Look up the ID for an object string, returning `None` if not interned.
411    pub fn get_object_id(&self, o: &str) -> Option<u64> {
412        self.object_dict.get(o).copied()
413    }
414
415    /// Decode any ID back to the original string, regardless of component type.
416    ///
417    /// Returns `None` if the ID is not known.
418    pub fn decode_id(&self, id: u64) -> Option<&str> {
419        self.reverse.get(&id).map(|s| s.as_str())
420    }
421
422    /// Return the number of unique subjects.
423    pub fn subject_count(&self) -> usize {
424        self.subject_dict.len()
425    }
426
427    /// Return the number of unique predicates.
428    pub fn predicate_count(&self) -> usize {
429        self.predicate_dict.len()
430    }
431
432    /// Return the number of unique objects.
433    pub fn object_count(&self) -> usize {
434        self.object_dict.len()
435    }
436
437    // -- Internal helpers --
438
439    fn intern_subject(&mut self, s: &str) -> u64 {
440        if let Some(&id) = self.subject_dict.get(s) {
441            return id;
442        }
443        let id = self.alloc_id();
444        self.subject_dict.insert(s.to_string(), id);
445        self.reverse.insert(id, s.to_string());
446        id
447    }
448
449    fn intern_predicate(&mut self, p: &str) -> u64 {
450        if let Some(&id) = self.predicate_dict.get(p) {
451            return id;
452        }
453        let id = self.alloc_id();
454        self.predicate_dict.insert(p.to_string(), id);
455        self.reverse.insert(id, p.to_string());
456        id
457    }
458
459    fn intern_object(&mut self, o: &str) -> u64 {
460        if let Some(&id) = self.object_dict.get(o) {
461            return id;
462        }
463        let id = self.alloc_id();
464        self.object_dict.insert(o.to_string(), id);
465        self.reverse.insert(id, o.to_string());
466        id
467    }
468
469    fn alloc_id(&mut self) -> u64 {
470        let id = self.next_id;
471        self.next_id = self.next_id.saturating_add(1);
472        id
473    }
474}
475
476impl Default for TripleDictionary {
477    fn default() -> Self {
478        Self::new()
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    #[test]
487    fn test_dictionary_basic() {
488        let mut dict = AdaptiveDictionary::new();
489
490        let id1 = dict.add_string(b"hello").unwrap();
491        let id2 = dict.add_string(b"world").unwrap();
492        let id3 = dict.add_string(b"hello").unwrap(); // Should reuse
493
494        assert_eq!(id1, id3);
495        assert_ne!(id1, id2);
496        assert_eq!(dict.get_string(id1), Some(&b"hello"[..]));
497        assert_eq!(dict.get_string(id2), Some(&b"world"[..]));
498    }
499
500    #[test]
501    fn test_compression_algorithm_trait() {
502        let dict = AdaptiveDictionary::new();
503        let data = b"hello world hello world";
504
505        let compressed = dict.compress(data).unwrap();
506        assert_eq!(
507            compressed.metadata.algorithm,
508            AdvancedCompressionType::AdaptiveDictionary
509        );
510
511        let decompressed = dict.decompress(&compressed).unwrap();
512        assert_eq!(data, decompressed.as_slice());
513    }
514
515    #[test]
516    fn test_dictionary_serialization() {
517        let mut dict = AdaptiveDictionary::new();
518        dict.add_string(b"test").unwrap();
519        dict.add_string(b"data").unwrap();
520
521        let serialized = dict.serialize_dictionary();
522
523        let mut new_dict = AdaptiveDictionary::new();
524        new_dict.deserialize_dictionary(&serialized).unwrap();
525
526        assert_eq!(dict.get_id(b"test"), new_dict.get_id(b"test"));
527        assert_eq!(dict.get_id(b"data"), new_dict.get_id(b"data"));
528    }
529
530    // -- TripleDictionary tests --
531
532    #[test]
533    fn test_triple_dictionary_encode_decode_roundtrip() {
534        let mut dict = TripleDictionary::new();
535
536        let s = "http://example.org/Alice";
537        let p = "http://schema.org/knows";
538        let o = "http://example.org/Bob";
539
540        let (s_id, p_id, o_id) = dict.encode_triple(s, p, o);
541        let decoded = dict
542            .decode_triple(s_id, p_id, o_id)
543            .expect("decode must succeed");
544        assert_eq!(decoded.0, s);
545        assert_eq!(decoded.1, p);
546        assert_eq!(decoded.2, o);
547    }
548
549    #[test]
550    fn test_triple_dictionary_same_uri_same_id() {
551        let mut dict = TripleDictionary::new();
552
553        let rdf_type = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
554        let class_a = "http://example.org/ClassA";
555        let class_b = "http://example.org/ClassB";
556
557        let (_, p1, _) = dict.encode_triple("http://example.org/s1", rdf_type, class_a);
558        let (_, p2, _) = dict.encode_triple("http://example.org/s2", rdf_type, class_b);
559        // Same predicate URI must get same ID
560        assert_eq!(p1, p2, "same predicate URI must map to same ID");
561    }
562
563    #[test]
564    fn test_triple_dictionary_size() {
565        let mut dict = TripleDictionary::new();
566        assert_eq!(dict.size(), 0);
567
568        // Three distinct URIs
569        dict.encode_triple(
570            "http://example.org/s",
571            "http://example.org/p",
572            "http://example.org/o",
573        );
574        assert_eq!(dict.size(), 3, "three unique URIs should give size 3");
575
576        // Repeat the same triple - no new entries
577        dict.encode_triple(
578            "http://example.org/s",
579            "http://example.org/p",
580            "http://example.org/o",
581        );
582        assert_eq!(
583            dict.size(),
584            3,
585            "duplicate triple must not grow the dictionary"
586        );
587    }
588
589    #[test]
590    fn test_triple_dictionary_compression_ratio() {
591        let mut dict = TripleDictionary::new();
592        // Long URIs give a good compression ratio
593        for i in 0..100 {
594            dict.encode_triple(
595                &format!("http://very-long-namespace.example.org/subject/{}", i),
596                "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
597                "http://very-long-namespace.example.org/SomeClass",
598            );
599        }
600        let ratio = dict.compression_ratio();
601        assert!(
602            ratio > 1.0,
603            "long URIs should have compression_ratio > 1.0, got {}",
604            ratio
605        );
606    }
607
608    #[test]
609    fn test_triple_dictionary_decode_unknown_id_returns_none() {
610        let dict = TripleDictionary::new();
611        let result = dict.decode_triple(9999, 9998, 9997);
612        assert!(result.is_none(), "unknown IDs should return None");
613    }
614
615    #[test]
616    fn test_triple_dictionary_component_counts() {
617        let mut dict = TripleDictionary::new();
618
619        dict.encode_triple(
620            "http://example.org/s1",
621            "http://schema.org/name",
622            "\"Alice\"",
623        );
624        dict.encode_triple("http://example.org/s2", "http://schema.org/name", "\"Bob\"");
625        dict.encode_triple("http://example.org/s1", "http://schema.org/age", "\"30\"");
626
627        assert_eq!(dict.subject_count(), 2, "two distinct subjects");
628        assert_eq!(dict.predicate_count(), 2, "two distinct predicates");
629        assert_eq!(dict.object_count(), 3, "three distinct objects");
630    }
631
632    #[test]
633    fn test_triple_dictionary_large_graph() {
634        let mut dict = TripleDictionary::new();
635
636        // Simulate a medium-sized RDF graph
637        let predicates = [
638            "http://schema.org/name",
639            "http://schema.org/knows",
640            "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
641        ];
642
643        for i in 0..1000 {
644            let s = format!("http://example.org/person/{}", i);
645            let p = predicates[i % predicates.len()];
646            let o = format!("http://example.org/person/{}", (i + 1) % 1000);
647            dict.encode_triple(&s, p, &o);
648        }
649
650        // 1000 unique subjects = 1000 s IDs; 3 unique predicates; subjects also used as objects
651        assert_eq!(dict.subject_count(), 1000);
652        assert_eq!(dict.predicate_count(), 3);
653        // objects are same URIs as subjects (partially reused via the same global ID)
654        // after the full loop all 1000 person URIs appear as objects too, but they get
655        // separate entries in the object_dict (they're tracked independently per role)
656        assert!(dict.object_count() <= 1000);
657
658        let ratio = dict.compression_ratio();
659        // With ~35-byte URIs each mapped to 8 bytes: ratio ~ 35/8 ~ 4.4x
660        assert!(
661            ratio > 2.0,
662            "large graph should achieve > 2x compression ratio, got {}",
663            ratio
664        );
665    }
666
667    #[test]
668    fn test_triple_dictionary_get_id_helpers() {
669        let mut dict = TripleDictionary::new();
670        let s = "http://example.org/s";
671        let p = "http://schema.org/p";
672        let o = "http://example.org/o";
673
674        let (s_id, p_id, o_id) = dict.encode_triple(s, p, o);
675
676        assert_eq!(dict.get_subject_id(s), Some(s_id));
677        assert_eq!(dict.get_predicate_id(p), Some(p_id));
678        assert_eq!(dict.get_object_id(o), Some(o_id));
679        assert_eq!(dict.get_subject_id("unknown"), None);
680    }
681
682    #[test]
683    fn test_triple_dictionary_decode_id() {
684        let mut dict = TripleDictionary::new();
685        let (s_id, p_id, o_id) = dict.encode_triple(
686            "http://example.org/s",
687            "http://schema.org/p",
688            "http://example.org/o",
689        );
690        assert_eq!(dict.decode_id(s_id), Some("http://example.org/s"));
691        assert_eq!(dict.decode_id(p_id), Some("http://schema.org/p"));
692        assert_eq!(dict.decode_id(o_id), Some("http://example.org/o"));
693        assert_eq!(dict.decode_id(99999), None);
694    }
695}