Skip to main content

oxirs_vec/
rdf_integration.rs

1//! RDF term support integration with oxirs-core
2//!
3//! This module provides seamless integration between oxirs-vec's vector operations
4//! and oxirs-core's RDF term system, enabling semantic vector search on RDF data.
5
6use crate::{similarity::SimilarityMetric, Vector, VectorId, VectorStoreTrait};
7use anyhow::{anyhow, Result};
8use oxirs_core::model::{GraphName, Literal, NamedNode, Term};
9// parking_lot::RwLock (not std::sync::RwLock) per the P1 lock-poisoning
10// finding: a single panic while holding a std::sync lock would poison it
11// permanently, breaking every subsequent call for the process lifetime.
12// parking_lot's RwLock has no poisoning, matching the pattern used
13// elsewhere in this crate (e.g. store_integration_adapters.rs).
14use parking_lot::RwLock;
15use serde::{Deserialize, Serialize};
16use std::collections::{HashMap, HashSet};
17use std::hash::{Hash, Hasher};
18use std::sync::Arc;
19
20/// Configuration for RDF-vector integration
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct RdfVectorConfig {
23    /// Enable automatic URI decomposition for embeddings
24    pub uri_decomposition: bool,
25    /// Include literal types in embeddings
26    pub include_literal_types: bool,
27    /// Enable graph context awareness
28    pub graph_context: bool,
29    /// Namespace prefix handling
30    pub namespace_aware: bool,
31    /// Default similarity metric for RDF term comparisons
32    pub default_metric: SimilarityMetric,
33    /// Cache size for term-to-vector mappings
34    pub cache_size: usize,
35}
36
37impl Default for RdfVectorConfig {
38    fn default() -> Self {
39        Self {
40            uri_decomposition: true,
41            include_literal_types: true,
42            graph_context: true,
43            namespace_aware: true,
44            default_metric: SimilarityMetric::Cosine,
45            cache_size: 10000,
46        }
47    }
48}
49
50/// Mapping between RDF terms and vector identifiers
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct RdfTermMapping {
53    /// Original RDF term
54    pub term: Term,
55    /// Associated vector identifier
56    pub vector_id: VectorId,
57    /// Graph context (if applicable)
58    pub graph_context: Option<GraphName>,
59    /// Term metadata for enhanced processing
60    pub metadata: RdfTermMetadata,
61}
62
63/// Metadata for RDF terms to enhance vector processing
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct RdfTermMetadata {
66    /// Term type for specialized processing
67    pub term_type: RdfTermType,
68    /// Namespace information
69    pub namespace: Option<String>,
70    /// Local name component
71    pub local_name: Option<String>,
72    /// Literal datatype (if applicable)
73    pub datatype: Option<NamedNode>,
74    /// Language tag (if applicable)
75    pub language: Option<String>,
76    /// Term complexity score for weighting
77    pub complexity_score: f32,
78}
79
80/// RDF term type enumeration for processing
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
82pub enum RdfTermType {
83    NamedNode,
84    BlankNode,
85    Literal,
86    Variable,
87    QuotedTriple,
88}
89
90/// Result of RDF-aware vector search
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct RdfVectorSearchResult {
93    /// Matching RDF term
94    pub term: Term,
95    /// Similarity score
96    pub score: f32,
97    /// Vector identifier
98    pub vector_id: VectorId,
99    /// Graph context
100    pub graph_context: Option<GraphName>,
101    /// Search metadata
102    pub metadata: SearchMetadata,
103}
104
105/// Search metadata for RDF vector results
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct SearchMetadata {
108    /// Search algorithm used
109    pub algorithm: String,
110    /// Processing time in microseconds
111    pub processing_time_us: u64,
112    /// Term matching confidence
113    pub confidence: f32,
114    /// Explanation of result relevance
115    pub explanation: Option<String>,
116}
117
118/// RDF-Vector integration engine
119pub struct RdfVectorIntegration {
120    /// Configuration
121    config: RdfVectorConfig,
122    /// Term to vector mappings
123    term_mappings: Arc<RwLock<HashMap<TermHash, RdfTermMapping>>>,
124    /// Vector to term reverse mappings
125    vector_mappings: Arc<RwLock<HashMap<VectorId, RdfTermMapping>>>,
126    /// Graph context cache
127    graph_cache: Arc<RwLock<HashMap<GraphName, HashSet<VectorId>>>>,
128    /// Namespace registry
129    namespace_registry: Arc<RwLock<HashMap<String, String>>>,
130    /// Vector store reference
131    vector_store: Arc<RwLock<dyn VectorStoreTrait>>,
132}
133
134/// Hash wrapper for RDF terms to enable HashMap keys
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
136struct TermHash(u64);
137
138impl TermHash {
139    fn from_term(term: &Term) -> Self {
140        use std::collections::hash_map::DefaultHasher;
141        let mut hasher = DefaultHasher::new();
142
143        match term {
144            Term::NamedNode(node) => {
145                "NamedNode".hash(&mut hasher);
146                node.as_str().hash(&mut hasher);
147            }
148            Term::BlankNode(node) => {
149                "BlankNode".hash(&mut hasher);
150                node.as_str().hash(&mut hasher);
151            }
152            Term::Literal(literal) => {
153                "Literal".hash(&mut hasher);
154                literal.value().hash(&mut hasher);
155                if let Some(lang) = literal.language() {
156                    lang.hash(&mut hasher);
157                }
158                literal.datatype().as_str().hash(&mut hasher);
159            }
160            Term::Variable(var) => {
161                "Variable".hash(&mut hasher);
162                var.as_str().hash(&mut hasher);
163            }
164            Term::QuotedTriple(_) => {
165                "QuotedTriple".hash(&mut hasher);
166                // Simplified hash for quoted triples
167                "quoted_triple".hash(&mut hasher);
168            }
169        }
170
171        TermHash(hasher.finish())
172    }
173}
174
175impl RdfVectorIntegration {
176    /// Create a new RDF-vector integration instance
177    pub fn new(config: RdfVectorConfig, vector_store: Arc<RwLock<dyn VectorStoreTrait>>) -> Self {
178        Self {
179            config,
180            term_mappings: Arc::new(RwLock::new(HashMap::new())),
181            vector_mappings: Arc::new(RwLock::new(HashMap::new())),
182            graph_cache: Arc::new(RwLock::new(HashMap::new())),
183            namespace_registry: Arc::new(RwLock::new(HashMap::new())),
184            vector_store,
185        }
186    }
187
188    /// Register an RDF term with vector representation
189    pub fn register_term(
190        &self,
191        term: Term,
192        vector: Vector,
193        graph_context: Option<GraphName>,
194    ) -> Result<VectorId> {
195        let vector_id = self.vector_store.write().add_vector(vector)?;
196        let metadata = self.extract_term_metadata(&term)?;
197
198        let mapping = RdfTermMapping {
199            term: term.clone(),
200            vector_id: vector_id.clone(),
201            graph_context: graph_context.clone(),
202            metadata,
203        };
204
205        let term_hash = TermHash::from_term(&term);
206
207        // Update mappings
208        {
209            let mut term_mappings = self.term_mappings.write();
210            term_mappings.insert(term_hash, mapping.clone());
211        }
212
213        {
214            let mut vector_mappings = self.vector_mappings.write();
215            vector_mappings.insert(vector_id.clone(), mapping);
216        }
217
218        // Update graph cache if applicable
219        if let Some(graph) = graph_context {
220            let mut graph_cache = self.graph_cache.write();
221            graph_cache
222                .entry(graph)
223                .or_default()
224                .insert(vector_id.clone());
225        }
226
227        Ok(vector_id)
228    }
229
230    /// Find similar RDF terms using vector similarity
231    pub fn find_similar_terms(
232        &self,
233        query_term: &Term,
234        limit: usize,
235        threshold: Option<f32>,
236        graph_context: Option<&GraphName>,
237    ) -> Result<Vec<RdfVectorSearchResult>> {
238        let start_time = std::time::Instant::now();
239
240        // Get vector for query term
241        let query_vector_id = self
242            .get_vector_id(query_term)?
243            .ok_or_else(|| anyhow!("Query term not found in vector store"))?;
244
245        let query_vector = self
246            .vector_store
247            .read()
248            .get_vector(&query_vector_id)?
249            .ok_or_else(|| anyhow!("Query vector not found"))?;
250
251        // Filter by graph context if specified
252        let candidate_vectors = if let Some(graph) = graph_context {
253            let graph_cache = self.graph_cache.read();
254            graph_cache
255                .get(graph)
256                .map(|set| set.iter().cloned().collect::<Vec<_>>())
257                .unwrap_or_default()
258        } else {
259            // Use all vectors if no graph context specified
260            self.vector_store.read().get_all_vector_ids()?
261        };
262
263        // Perform similarity search
264        let mut results = Vec::new();
265        for vector_id in candidate_vectors {
266            if *vector_id == query_vector_id {
267                continue; // Skip self
268            }
269
270            if let Ok(Some(vector)) = self.vector_store.read().get_vector(&vector_id) {
271                let similarity = self.config.default_metric.compute(&query_vector, &vector)?;
272
273                // Apply threshold filtering
274                if let Some(thresh) = threshold {
275                    if similarity < thresh {
276                        continue;
277                    }
278                }
279
280                // Get term mapping
281                let vector_mappings = self.vector_mappings.read();
282                if let Some(mapping) = vector_mappings.get(&vector_id) {
283                    let processing_time = start_time.elapsed().as_micros() as u64;
284
285                    results.push(RdfVectorSearchResult {
286                        term: mapping.term.clone(),
287                        score: similarity,
288                        vector_id: vector_id.clone(),
289                        graph_context: mapping.graph_context.clone(),
290                        metadata: SearchMetadata {
291                            algorithm: "vector_similarity".to_string(),
292                            processing_time_us: processing_time,
293                            confidence: self.calculate_confidence(similarity, &mapping.metadata),
294                            explanation: self.generate_explanation(&mapping.metadata, similarity),
295                        },
296                    });
297                }
298            }
299        }
300
301        // Sort by similarity score (descending)
302        results.sort_by(|a, b| {
303            b.score
304                .partial_cmp(&a.score)
305                .unwrap_or(std::cmp::Ordering::Equal)
306        });
307
308        // Apply limit
309        results.truncate(limit);
310
311        Ok(results)
312    }
313
314    /// Search for terms by text content with RDF-aware processing
315    pub fn search_by_text(
316        &self,
317        query_text: &str,
318        limit: usize,
319        threshold: Option<f32>,
320        graph_context: Option<&GraphName>,
321    ) -> Result<Vec<RdfVectorSearchResult>> {
322        // Create a temporary literal term for text search
323        let literal = Literal::new_simple_literal(query_text);
324        let _query_term = Term::Literal(literal);
325
326        // For text search, we would typically generate an embedding
327        // This is a simplified version - in practice, you'd use an embedding model
328        let query_vector = self.generate_text_embedding(query_text)?;
329
330        // Register temporary term (optional - for caching)
331        let temp_vector_id = self.vector_store.write().add_vector(query_vector.clone())?;
332
333        // Perform similarity search against all terms
334        let candidate_vectors = if let Some(graph) = graph_context {
335            let graph_cache = self.graph_cache.read();
336            graph_cache
337                .get(graph)
338                .map(|set| set.iter().cloned().collect::<Vec<_>>())
339                .unwrap_or_default()
340        } else {
341            self.vector_store.read().get_all_vector_ids()?
342        };
343
344        let mut results = Vec::new();
345        let start_time = std::time::Instant::now();
346
347        for vector_id in candidate_vectors {
348            if let Ok(Some(vector)) = self.vector_store.read().get_vector(&vector_id) {
349                let similarity = self.config.default_metric.compute(&query_vector, &vector)?;
350
351                if let Some(thresh) = threshold {
352                    if similarity < thresh {
353                        continue;
354                    }
355                }
356
357                let vector_mappings = self.vector_mappings.read();
358                if let Some(mapping) = vector_mappings.get(&vector_id) {
359                    let processing_time = start_time.elapsed().as_micros() as u64;
360
361                    results.push(RdfVectorSearchResult {
362                        term: mapping.term.clone(),
363                        score: similarity,
364                        vector_id: vector_id.clone(),
365                        graph_context: mapping.graph_context.clone(),
366                        metadata: SearchMetadata {
367                            algorithm: "text_similarity".to_string(),
368                            processing_time_us: processing_time,
369                            confidence: self.calculate_confidence(similarity, &mapping.metadata),
370                            explanation: Some(format!("Text similarity match: '{query_text}'")),
371                        },
372                    });
373                }
374            }
375        }
376
377        // Clean up temporary vector
378        let _ = self.vector_store.write().remove_vector(&temp_vector_id);
379
380        // Sort and limit results
381        results.sort_by(|a, b| {
382            b.score
383                .partial_cmp(&a.score)
384                .unwrap_or(std::cmp::Ordering::Equal)
385        });
386        results.truncate(limit);
387
388        Ok(results)
389    }
390
391    /// Get vector ID for an RDF term
392    pub fn get_vector_id(&self, term: &Term) -> Result<Option<VectorId>> {
393        let term_hash = TermHash::from_term(term);
394        let term_mappings = self.term_mappings.read();
395        Ok(term_mappings
396            .get(&term_hash)
397            .map(|mapping| mapping.vector_id.clone()))
398    }
399
400    /// Get RDF term for a vector ID
401    pub fn get_term(&self, vector_id: VectorId) -> Result<Option<Term>> {
402        let vector_mappings = self.vector_mappings.read();
403        Ok(vector_mappings
404            .get(&vector_id)
405            .map(|mapping| mapping.term.clone()))
406    }
407
408    /// Register a namespace prefix
409    pub fn register_namespace(&self, prefix: String, uri: String) -> Result<()> {
410        let mut registry = self.namespace_registry.write();
411        registry.insert(prefix, uri);
412        Ok(())
413    }
414
415    /// Extract metadata from RDF term
416    fn extract_term_metadata(&self, term: &Term) -> Result<RdfTermMetadata> {
417        match term {
418            Term::NamedNode(node) => {
419                let uri = node.as_str();
420                let (namespace, local_name) = self.split_uri(uri);
421
422                Ok(RdfTermMetadata {
423                    term_type: RdfTermType::NamedNode,
424                    namespace,
425                    local_name,
426                    datatype: None,
427                    language: None,
428                    complexity_score: self.calculate_uri_complexity(uri),
429                })
430            }
431            Term::BlankNode(_) => {
432                Ok(RdfTermMetadata {
433                    term_type: RdfTermType::BlankNode,
434                    namespace: None,
435                    local_name: None,
436                    datatype: None,
437                    language: None,
438                    complexity_score: 0.5, // Blank nodes have medium complexity
439                })
440            }
441            Term::Literal(literal) => Ok(RdfTermMetadata {
442                term_type: RdfTermType::Literal,
443                namespace: None,
444                local_name: None,
445                datatype: Some(literal.datatype().into()),
446                language: literal.language().map(|s| s.to_string()),
447                complexity_score: self.calculate_literal_complexity(literal),
448            }),
449            Term::Variable(_) => {
450                Ok(RdfTermMetadata {
451                    term_type: RdfTermType::Variable,
452                    namespace: None,
453                    local_name: None,
454                    datatype: None,
455                    language: None,
456                    complexity_score: 0.3, // Variables have low complexity
457                })
458            }
459            Term::QuotedTriple(_) => {
460                Ok(RdfTermMetadata {
461                    term_type: RdfTermType::QuotedTriple,
462                    namespace: None,
463                    local_name: None,
464                    datatype: None,
465                    language: None,
466                    complexity_score: 1.0, // Quoted triples have high complexity
467                })
468            }
469        }
470    }
471
472    /// Split URI into namespace and local name
473    fn split_uri(&self, uri: &str) -> (Option<String>, Option<String>) {
474        // Simple URI splitting logic - can be enhanced
475        if let Some(pos) = uri.rfind(&['#', '/'][..]) {
476            let namespace = uri[..pos + 1].to_string();
477            let local_name = uri[pos + 1..].to_string();
478            (Some(namespace), Some(local_name))
479        } else {
480            (None, Some(uri.to_string()))
481        }
482    }
483
484    /// Calculate URI complexity score
485    fn calculate_uri_complexity(&self, uri: &str) -> f32 {
486        let length_factor = (uri.len() as f32 / 100.0).min(1.0);
487        let segment_count = uri.matches(&['/', '#'][..]).count() as f32 / 10.0;
488        let query_params = if uri.contains('?') { 0.2 } else { 0.0 };
489
490        (length_factor + segment_count + query_params).min(1.0)
491    }
492
493    /// Calculate literal complexity score
494    fn calculate_literal_complexity(&self, literal: &Literal) -> f32 {
495        let value_length = literal.value().len() as f32 / 200.0;
496        let datatype_complexity =
497            if literal.datatype().as_str() == "http://www.w3.org/2001/XMLSchema#string" {
498                0.3
499            } else {
500                0.7
501            };
502        let language_bonus = if literal.language().is_some() {
503            0.2
504        } else {
505            0.0
506        };
507
508        (value_length + datatype_complexity + language_bonus).min(1.0)
509    }
510
511    /// Calculate confidence score for search results
512    fn calculate_confidence(&self, similarity: f32, metadata: &RdfTermMetadata) -> f32 {
513        let base_confidence = similarity;
514        let complexity_bonus = metadata.complexity_score * 0.1;
515        let type_bonus = match metadata.term_type {
516            RdfTermType::NamedNode => 0.1,
517            RdfTermType::Literal => 0.05,
518            RdfTermType::BlankNode => 0.02,
519            RdfTermType::Variable => 0.01,
520            RdfTermType::QuotedTriple => 0.15,
521        };
522
523        (base_confidence + complexity_bonus + type_bonus).min(1.0)
524    }
525
526    /// Generate explanation for search results
527    fn generate_explanation(&self, metadata: &RdfTermMetadata, similarity: f32) -> Option<String> {
528        let term_type_str = match metadata.term_type {
529            RdfTermType::NamedNode => "Named Node",
530            RdfTermType::BlankNode => "Blank Node",
531            RdfTermType::Literal => "Literal",
532            RdfTermType::Variable => "Variable",
533            RdfTermType::QuotedTriple => "Quoted Triple",
534        };
535
536        let mut explanation = format!(
537            "{} with {:.2}% similarity",
538            term_type_str,
539            similarity * 100.0
540        );
541
542        if let Some(namespace) = &metadata.namespace {
543            explanation.push_str(&format!(", namespace: {namespace}"));
544        }
545
546        if let Some(language) = &metadata.language {
547            explanation.push_str(&format!(", language: {language}"));
548        }
549
550        Some(explanation)
551    }
552
553    /// Generate text embedding (placeholder implementation)
554    fn generate_text_embedding(&self, text: &str) -> Result<Vector> {
555        // This is a simplified implementation
556        // In production, you would use a proper embedding model
557        let words: Vec<&str> = text.split_whitespace().collect();
558        let dimension = 384; // Standard sentence transformer dimension
559
560        let mut vector_data = vec![0.0; dimension];
561
562        // Simple word-based embedding generation
563        for word in words.iter() {
564            let word_hash = {
565                use std::collections::hash_map::DefaultHasher;
566                let mut hasher = DefaultHasher::new();
567                word.hash(&mut hasher);
568                hasher.finish()
569            };
570
571            // Distribute word influence across vector dimensions
572            for j in 0..dimension {
573                let index = (word_hash as usize + j) % dimension;
574                vector_data[index] += 1.0 / (words.len() as f32);
575            }
576        }
577
578        // Normalize vector
579        let norm: f32 = vector_data.iter().map(|x| x * x).sum::<f32>().sqrt();
580        if norm > 0.0 {
581            for value in &mut vector_data {
582                *value /= norm;
583            }
584        }
585
586        Ok(Vector::new(vector_data))
587    }
588
589    /// Get statistics about the RDF-vector integration
590    pub fn get_statistics(&self) -> RdfIntegrationStats {
591        let term_mappings = self.term_mappings.read();
592        let graph_cache = self.graph_cache.read();
593        let namespace_registry = self.namespace_registry.read();
594
595        let mut type_counts = HashMap::new();
596        for mapping in term_mappings.values() {
597            *type_counts.entry(mapping.metadata.term_type).or_insert(0) += 1;
598        }
599
600        RdfIntegrationStats {
601            total_terms: term_mappings.len(),
602            total_graphs: graph_cache.len(),
603            total_namespaces: namespace_registry.len(),
604            type_distribution: type_counts,
605            cache_hit_ratio: 0.95, // Placeholder
606        }
607    }
608}
609
610/// Statistics for RDF-vector integration
611#[derive(Debug, Clone, Serialize, Deserialize)]
612pub struct RdfIntegrationStats {
613    pub total_terms: usize,
614    pub total_graphs: usize,
615    pub total_namespaces: usize,
616    pub type_distribution: HashMap<RdfTermType, usize>,
617    pub cache_hit_ratio: f32,
618}
619
620#[cfg(test)]
621mod tests {
622    use super::*;
623    use crate::VectorStore;
624    use anyhow::Result;
625    use oxirs_core::model::{NamedNode, Term};
626
627    #[test]
628    fn test_rdf_term_registration() -> Result<()> {
629        let config = RdfVectorConfig::default();
630        let vector_store = Arc::new(RwLock::new(VectorStore::new()));
631        let integration = RdfVectorIntegration::new(config, vector_store);
632
633        let named_node = NamedNode::new("http://example.org/person")?;
634        let term = Term::NamedNode(named_node);
635        let vector = Vector::new(vec![1.0, 0.0, 0.0]);
636
637        let vector_id = integration.register_term(term.clone(), vector, None)?;
638
639        assert!(integration
640            .get_vector_id(&term)
641            .expect("test value")
642            .is_some());
643        assert_eq!(
644            integration
645                .get_vector_id(&term)
646                .expect("get_vector_id should return Some")
647                .expect("inner Option should be Some"),
648            vector_id
649        );
650        Ok(())
651    }
652
653    #[test]
654    fn test_uri_splitting() {
655        let config = RdfVectorConfig::default();
656        let vector_store = Arc::new(RwLock::new(VectorStore::new()));
657        let integration = RdfVectorIntegration::new(config, vector_store);
658
659        let (namespace, local_name) = integration.split_uri("http://example.org/ontology#Person");
660        assert_eq!(namespace, Some("http://example.org/ontology#".to_string()));
661        assert_eq!(local_name, Some("Person".to_string()));
662    }
663
664    /// Regression test for the P1 finding: a panic while holding one of
665    /// `RdfVectorIntegration`'s locks used to permanently poison it (via
666    /// `std::sync::RwLock` + `.expect("lock poisoned")`), breaking every
667    /// subsequent call for the process lifetime. `parking_lot::RwLock` has
668    /// no poisoning, so a panic in one thread must not prevent other
669    /// threads from continuing to use the lock afterwards.
670    #[test]
671    fn test_lock_survives_panic_while_held() -> Result<()> {
672        let config = RdfVectorConfig::default();
673        let vector_store = Arc::new(RwLock::new(VectorStore::new()));
674        let integration = Arc::new(RdfVectorIntegration::new(config, vector_store));
675
676        let integration_clone = integration.clone();
677        let handle = std::thread::spawn(move || {
678            // Panic while holding a write lock on term_mappings, simulating
679            // a bug elsewhere in a caller that holds this lock.
680            let _guard = integration_clone.term_mappings.write();
681            panic!("simulated panic while holding the lock");
682        });
683        // The panicking thread's lock must not poison the RwLock for
684        // everyone else.
685        assert!(handle.join().is_err());
686
687        // A completely unrelated, later call must still succeed instead of
688        // panicking with "lock poisoned".
689        let named_node = NamedNode::new("http://example.org/after-panic")?;
690        let term = Term::NamedNode(named_node);
691        let vector = Vector::new(vec![1.0, 0.0, 0.0]);
692        let vector_id = integration.register_term(term.clone(), vector, None)?;
693        assert_eq!(
694            integration
695                .get_vector_id(&term)
696                .expect("get_vector_id should not panic after another thread's panic")
697                .expect("vector id should be present"),
698            vector_id
699        );
700        Ok(())
701    }
702
703    #[test]
704    fn test_metadata_extraction() -> Result<()> {
705        let config = RdfVectorConfig::default();
706        let vector_store = Arc::new(RwLock::new(VectorStore::new()));
707        let integration = RdfVectorIntegration::new(config, vector_store);
708
709        let literal = Literal::new_language_tagged_literal("Hello", "en")?;
710        let term = Term::Literal(literal);
711
712        let metadata = integration.extract_term_metadata(&term)?;
713        assert_eq!(metadata.term_type, RdfTermType::Literal);
714        assert_eq!(metadata.language, Some("en".to_string()));
715        Ok(())
716    }
717}