1use crate::{EmbeddingModel, Vector};
41use anyhow::{anyhow, Result};
42use serde::{Deserialize, Serialize};
43use std::collections::{HashMap, HashSet};
44use std::sync::Arc;
45use tokio::sync::RwLock;
46use tracing::{debug, info, trace};
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct SparqlExtensionConfig {
51 pub default_similarity_threshold: f32,
53 pub max_expansions_per_element: usize,
55 pub enable_query_rewriting: bool,
57 pub enable_semantic_caching: bool,
59 pub semantic_cache_size: usize,
61 pub enable_fuzzy_matching: bool,
63 pub min_expansion_confidence: f32,
65 pub enable_parallel_processing: bool,
67}
68
69impl Default for SparqlExtensionConfig {
70 fn default() -> Self {
71 Self {
72 default_similarity_threshold: 0.7,
73 max_expansions_per_element: 10,
74 enable_query_rewriting: true,
75 enable_semantic_caching: true,
76 semantic_cache_size: 1000,
77 enable_fuzzy_matching: true,
78 min_expansion_confidence: 0.6,
79 enable_parallel_processing: true,
80 }
81 }
82}
83
84pub struct SparqlExtension {
86 model: Arc<RwLock<Box<dyn EmbeddingModel>>>,
87 config: SparqlExtensionConfig,
88 semantic_cache: Arc<RwLock<SemanticCache>>,
89 query_statistics: Arc<RwLock<QueryStatistics>>,
90}
91
92impl SparqlExtension {
93 pub fn new(model: Box<dyn EmbeddingModel>) -> Self {
95 Self {
96 model: Arc::new(RwLock::new(model)),
97 config: SparqlExtensionConfig::default(),
98 semantic_cache: Arc::new(RwLock::new(SemanticCache::new(1000))),
99 query_statistics: Arc::new(RwLock::new(QueryStatistics::default())),
100 }
101 }
102
103 pub fn with_config(model: Box<dyn EmbeddingModel>, config: SparqlExtensionConfig) -> Self {
105 let cache_size = config.semantic_cache_size;
106 Self {
107 model: Arc::new(RwLock::new(model)),
108 config,
109 semantic_cache: Arc::new(RwLock::new(SemanticCache::new(cache_size))),
110 query_statistics: Arc::new(RwLock::new(QueryStatistics::default())),
111 }
112 }
113
114 pub async fn vec_similarity(&self, entity1: &str, entity2: &str) -> Result<f32> {
123 trace!("Computing similarity between {} and {}", entity1, entity2);
124
125 if self.config.enable_semantic_caching {
127 let cache = self.semantic_cache.read().await;
128 let cache_key = format!("sim:{}:{}", entity1, entity2);
129 if let Some(cached_result) = cache.get(&cache_key) {
130 debug!("Cache hit for similarity computation");
131 return Ok(cached_result);
132 }
133 }
134
135 let model = self.model.read().await;
136 let emb1 = model.get_entity_embedding(entity1)?;
137 let emb2 = model.get_entity_embedding(entity2)?;
138
139 let similarity = cosine_similarity(&emb1, &emb2)?;
140
141 if self.config.enable_semantic_caching {
143 let mut cache = self.semantic_cache.write().await;
144 let cache_key = format!("sim:{}:{}", entity1, entity2);
145 cache.put(cache_key, similarity);
146 }
147
148 let mut stats = self.query_statistics.write().await;
150 stats.similarity_computations += 1;
151
152 Ok(similarity)
153 }
154
155 pub async fn vec_nearest(
165 &self,
166 entity: &str,
167 k: usize,
168 min_similarity: Option<f32>,
169 ) -> Result<Vec<(String, f32)>> {
170 info!("Finding {} nearest neighbors for {}", k, entity);
171
172 let model = self.model.read().await;
173 let target_emb = model.get_entity_embedding(entity)?;
174 let all_entities = model.get_entities();
175
176 let threshold = min_similarity.unwrap_or(self.config.default_similarity_threshold);
177
178 let mut similarities: Vec<(String, f32)> = if self.config.enable_parallel_processing {
180 self.compute_similarities_parallel(&all_entities, &target_emb, entity)
181 .await?
182 } else {
183 self.compute_similarities_sequential(&all_entities, &target_emb, entity, &**model)
184 .await?
185 };
186
187 similarities.retain(|(_, sim)| *sim >= threshold);
189 similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
190
191 let result: Vec<(String, f32)> = similarities.into_iter().take(k).collect();
193
194 let mut stats = self.query_statistics.write().await;
196 stats.nearest_neighbor_queries += 1;
197
198 Ok(result)
199 }
200
201 pub async fn vec_similar_entities(
210 &self,
211 entity: &str,
212 threshold: f32,
213 ) -> Result<Vec<(String, f32)>> {
214 debug!(
215 "Finding entities similar to {} (threshold: {})",
216 entity, threshold
217 );
218
219 let model = self.model.read().await;
220 let target_emb = model.get_entity_embedding(entity)?;
221 let all_entities = model.get_entities();
222
223 let similarities = if self.config.enable_parallel_processing {
224 self.compute_similarities_parallel(&all_entities, &target_emb, entity)
225 .await?
226 } else {
227 self.compute_similarities_sequential(&all_entities, &target_emb, entity, &**model)
228 .await?
229 };
230
231 let result: Vec<(String, f32)> = similarities
232 .into_iter()
233 .filter(|(_, sim)| *sim >= threshold)
234 .collect();
235
236 Ok(result)
237 }
238
239 pub async fn vec_similar_relations(
248 &self,
249 relation: &str,
250 threshold: f32,
251 ) -> Result<Vec<(String, f32)>> {
252 debug!(
253 "Finding relations similar to {} (threshold: {})",
254 relation, threshold
255 );
256
257 let model = self.model.read().await;
258 let target_emb = model.get_relation_embedding(relation)?;
259 let all_relations = model.get_relations();
260
261 let mut similarities = Vec::new();
262 for rel in &all_relations {
263 if rel == relation {
264 continue; }
266
267 let rel_emb = model.get_relation_embedding(rel)?;
268 let sim = cosine_similarity(&target_emb, &rel_emb)?;
269
270 if sim >= threshold {
271 similarities.push((rel.clone(), sim));
272 }
273 }
274
275 similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
276
277 Ok(similarities)
278 }
279
280 pub async fn expand_query_semantically(&self, query: &str) -> Result<ExpandedQuery> {
288 info!("Performing semantic query expansion");
289
290 let mut stats = self.query_statistics.write().await;
291 stats.query_expansions += 1;
292 drop(stats);
293
294 let model = self.model.read().await;
295
296 let parsed = parse_sparql_query(query)?;
298
299 let mut entity_expansions = HashMap::new();
300 let mut relation_expansions = HashMap::new();
301
302 for entity in &parsed.entities {
304 let similar = self
305 .vec_similar_entities(entity, self.config.min_expansion_confidence)
306 .await?;
307
308 let expansions: Vec<Expansion> = similar
309 .into_iter()
310 .take(self.config.max_expansions_per_element)
311 .map(|(uri, confidence)| Expansion {
312 original: entity.clone(),
313 expanded: uri,
314 confidence,
315 expansion_type: ExpansionType::Entity,
316 })
317 .collect();
318
319 if !expansions.is_empty() {
320 entity_expansions.insert(entity.clone(), expansions);
321 }
322 }
323
324 for relation in &parsed.relations {
326 let similar = self
327 .vec_similar_relations(relation, self.config.min_expansion_confidence)
328 .await?;
329
330 let expansions: Vec<Expansion> = similar
331 .into_iter()
332 .take(self.config.max_expansions_per_element)
333 .map(|(uri, confidence)| Expansion {
334 original: relation.clone(),
335 expanded: uri,
336 confidence,
337 expansion_type: ExpansionType::Relation,
338 })
339 .collect();
340
341 if !expansions.is_empty() {
342 relation_expansions.insert(relation.clone(), expansions);
343 }
344 }
345
346 drop(model);
347
348 let expanded_query = if self.config.enable_query_rewriting {
349 self.rewrite_query_with_expansions(query, &entity_expansions, &relation_expansions)
350 .await?
351 } else {
352 query.to_string()
353 };
354
355 let expansion_count = entity_expansions.len() + relation_expansions.len();
356
357 Ok(ExpandedQuery {
358 original_query: query.to_string(),
359 expanded_query,
360 entity_expansions,
361 relation_expansions,
362 expansion_count,
363 })
364 }
365
366 pub async fn fuzzy_match_entity(
375 &self,
376 entity_name: &str,
377 k: usize,
378 ) -> Result<Vec<(String, f32)>> {
379 if !self.config.enable_fuzzy_matching {
380 return Ok(vec![]);
381 }
382
383 debug!("Performing fuzzy match for entity: {}", entity_name);
384
385 let model = self.model.read().await;
386 let all_entities = model.get_entities();
387
388 let mut matches = Vec::new();
389
390 for entity in &all_entities {
391 let score = fuzzy_match_score(entity_name, entity);
392 if score > 0.5 {
393 matches.push((entity.clone(), score));
395 }
396 }
397
398 matches.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
399
400 Ok(matches.into_iter().take(k).collect())
401 }
402
403 pub async fn get_statistics(&self) -> QueryStatistics {
405 self.query_statistics.read().await.clone()
406 }
407
408 pub async fn clear_cache(&self) {
410 let mut cache = self.semantic_cache.write().await;
411 cache.clear();
412 info!("Semantic cache cleared");
413 }
414
415 async fn compute_similarities_parallel(
418 &self,
419 entities: &[String],
420 target_emb: &Vector,
421 exclude_entity: &str,
422 ) -> Result<Vec<(String, f32)>> {
423 use rayon::prelude::*;
424
425 let model = self.model.read().await;
426 let embeddings: Vec<_> = entities
427 .iter()
428 .filter(|e| e.as_str() != exclude_entity)
429 .filter_map(|e| {
430 model
431 .get_entity_embedding(e)
432 .ok()
433 .map(|emb| (e.clone(), emb))
434 })
435 .collect();
436 drop(model);
437
438 let target_emb_clone = target_emb.clone();
439 let similarities: Vec<(String, f32)> = embeddings
440 .par_iter()
441 .filter_map(|(entity, emb)| {
442 cosine_similarity(&target_emb_clone, emb)
443 .ok()
444 .map(|sim| (entity.clone(), sim))
445 })
446 .collect();
447
448 Ok(similarities)
449 }
450
451 async fn compute_similarities_sequential(
452 &self,
453 entities: &[String],
454 target_emb: &Vector,
455 exclude_entity: &str,
456 model: &dyn EmbeddingModel,
457 ) -> Result<Vec<(String, f32)>> {
458 let mut similarities = Vec::new();
459
460 for entity in entities {
461 if entity == exclude_entity {
462 continue;
463 }
464
465 if let Ok(entity_emb) = model.get_entity_embedding(entity) {
466 if let Ok(sim) = cosine_similarity(target_emb, &entity_emb) {
467 similarities.push((entity.clone(), sim));
468 }
469 }
470 }
471
472 Ok(similarities)
473 }
474
475 async fn rewrite_query_with_expansions(
476 &self,
477 original_query: &str,
478 entity_expansions: &HashMap<String, Vec<Expansion>>,
479 relation_expansions: &HashMap<String, Vec<Expansion>>,
480 ) -> Result<String> {
481 let mut rewritten = original_query.to_string();
484
485 for (original, expansions) in entity_expansions {
487 if let Some(first_expansion) = expansions.first() {
488 let union_clause = format!(
489 "\n UNION {{ # Semantic expansion for {}\n # Similar entity: {} (confidence: {:.2})\n }}",
490 original, first_expansion.expanded, first_expansion.confidence
491 );
492 rewritten.push_str(&union_clause);
493 }
494 }
495
496 for (original, expansions) in relation_expansions {
498 if let Some(first_expansion) = expansions.first() {
499 let comment = format!(
500 "\n # Relation '{}' can be expanded to '{}' (confidence: {:.2})",
501 original, first_expansion.expanded, first_expansion.confidence
502 );
503 rewritten.push_str(&comment);
504 }
505 }
506
507 Ok(rewritten)
508 }
509}
510
511struct SemanticCache {
513 cache: HashMap<String, f32>,
514 max_size: usize,
515 access_count: HashMap<String, u64>,
516}
517
518impl SemanticCache {
519 fn new(max_size: usize) -> Self {
520 Self {
521 cache: HashMap::new(),
522 max_size,
523 access_count: HashMap::new(),
524 }
525 }
526
527 fn get(&self, key: &str) -> Option<f32> {
528 self.cache.get(key).copied()
529 }
530
531 fn put(&mut self, key: String, value: f32) {
532 if self.cache.len() >= self.max_size {
534 if let Some(lru_key) = self
535 .access_count
536 .iter()
537 .min_by_key(|(_, &count)| count)
538 .map(|(k, _)| k.clone())
539 {
540 self.cache.remove(&lru_key);
541 self.access_count.remove(&lru_key);
542 }
543 }
544
545 self.cache.insert(key.clone(), value);
546 *self.access_count.entry(key).or_insert(0) += 1;
547 }
548
549 fn clear(&mut self) {
550 self.cache.clear();
551 self.access_count.clear();
552 }
553}
554
555#[derive(Debug, Clone, Default, Serialize, Deserialize)]
557pub struct QueryStatistics {
558 pub similarity_computations: u64,
559 pub nearest_neighbor_queries: u64,
560 pub query_expansions: u64,
561 pub fuzzy_matches: u64,
562 pub cache_hits: u64,
563 pub cache_misses: u64,
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct ExpandedQuery {
569 pub original_query: String,
570 pub expanded_query: String,
571 pub entity_expansions: HashMap<String, Vec<Expansion>>,
572 pub relation_expansions: HashMap<String, Vec<Expansion>>,
573 pub expansion_count: usize,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
578pub struct Expansion {
579 pub original: String,
580 pub expanded: String,
581 pub confidence: f32,
582 pub expansion_type: ExpansionType,
583}
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
587pub enum ExpansionType {
588 Entity,
589 Relation,
590 Pattern,
591}
592
593#[derive(Debug, Clone)]
595struct ParsedQuery {
596 entities: Vec<String>,
597 relations: Vec<String>,
598 variables: HashSet<String>,
599}
600
601fn parse_sparql_query(query: &str) -> Result<ParsedQuery> {
603 let mut entities = Vec::new();
604 let mut relations = Vec::new();
605 let mut variables = HashSet::new();
606
607 let uri_pattern = regex::Regex::new(r"<(https?://[^>]+)>").unwrap();
609 let var_pattern = regex::Regex::new(r"\?(\w+)").unwrap();
610
611 for line in query.lines() {
612 if line.contains("http://") || line.contains("https://") {
614 for cap in uri_pattern.captures_iter(line) {
616 let uri = cap[1].to_string();
617 if line.contains(&format!(" <{uri}> ")) {
619 relations.push(uri.clone());
620 } else {
621 entities.push(uri);
622 }
623 }
624 }
625
626 for cap in var_pattern.captures_iter(line) {
628 variables.insert(cap[1].to_string());
629 }
630 }
631
632 Ok(ParsedQuery {
633 entities,
634 relations,
635 variables,
636 })
637}
638
639fn cosine_similarity(v1: &Vector, v2: &Vector) -> Result<f32> {
641 if v1.dimensions != v2.dimensions {
642 return Err(anyhow!(
643 "Vector dimensions must match: {} vs {}",
644 v1.dimensions,
645 v2.dimensions
646 ));
647 }
648
649 let dot_product: f32 = v1
650 .values
651 .iter()
652 .zip(v2.values.iter())
653 .map(|(a, b)| a * b)
654 .sum();
655
656 let norm1: f32 = v1.values.iter().map(|x| x * x).sum::<f32>().sqrt();
657 let norm2: f32 = v2.values.iter().map(|x| x * x).sum::<f32>().sqrt();
658
659 if norm1 == 0.0 || norm2 == 0.0 {
660 return Ok(0.0);
661 }
662
663 Ok(dot_product / (norm1 * norm2))
664}
665
666fn fuzzy_match_score(s1: &str, s2: &str) -> f32 {
668 let s1_lower = s1.to_lowercase();
669 let s2_lower = s2.to_lowercase();
670
671 if s1_lower == s2_lower {
673 return 1.0;
674 }
675
676 if s1_lower.contains(&s2_lower) || s2_lower.contains(&s1_lower) {
678 let max_len = s1.len().max(s2.len()) as f32;
679 let min_len = s1.len().min(s2.len()) as f32;
680 return min_len / max_len;
681 }
682
683 let distance = levenshtein_distance(&s1_lower, &s2_lower);
685 let max_len = s1.len().max(s2.len()) as f32;
686
687 if max_len == 0.0 {
688 return 1.0;
689 }
690
691 1.0 - (distance as f32 / max_len)
692}
693
694#[allow(clippy::needless_range_loop)]
696fn levenshtein_distance(s1: &str, s2: &str) -> usize {
697 let len1 = s1.len();
698 let len2 = s2.len();
699
700 if len1 == 0 {
701 return len2;
702 }
703 if len2 == 0 {
704 return len1;
705 }
706
707 let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
708
709 for i in 0..=len1 {
710 matrix[i][0] = i;
711 }
712 for j in 0..=len2 {
713 matrix[0][j] = j;
714 }
715
716 let s1_chars: Vec<char> = s1.chars().collect();
717 let s2_chars: Vec<char> = s2.chars().collect();
718
719 for i in 1..=len1 {
720 for j in 1..=len2 {
721 let cost = if s1_chars[i - 1] == s2_chars[j - 1] {
722 0
723 } else {
724 1
725 };
726
727 matrix[i][j] = (matrix[i - 1][j] + 1)
728 .min(matrix[i][j - 1] + 1)
729 .min(matrix[i - 1][j - 1] + cost);
730 }
731 }
732
733 matrix[len1][len2]
734}
735
736#[cfg(test)]
737mod tests {
738 use super::*;
739 use crate::models::TransE;
740 use crate::{ModelConfig, NamedNode, Triple};
741
742 fn create_test_model() -> TransE {
743 let config = ModelConfig::default().with_dimensions(10);
744 let mut model = TransE::new(config);
745
746 let triples = vec![
748 ("alice", "knows", "bob"),
749 ("bob", "knows", "charlie"),
750 ("alice", "likes", "music"),
751 ("charlie", "likes", "art"),
752 ];
753
754 for (s, p, o) in triples {
755 let triple = Triple::new(
756 NamedNode::new(&format!("http://example.org/{s}")).unwrap(),
757 NamedNode::new(&format!("http://example.org/{p}")).unwrap(),
758 NamedNode::new(&format!("http://example.org/{o}")).unwrap(),
759 );
760 model.add_triple(triple).unwrap();
761 }
762
763 model
764 }
765
766 #[tokio::test]
767 async fn test_vec_similarity() -> Result<()> {
768 let model = create_test_model();
769 let ext = SparqlExtension::new(Box::new(model));
770
771 {
773 let mut model = ext.model.write().await;
774 model.train(Some(10)).await?;
775 }
776
777 let sim = ext
778 .vec_similarity("http://example.org/alice", "http://example.org/bob")
779 .await?;
780
781 assert!((0.0..=1.0).contains(&sim));
782 Ok(())
783 }
784
785 #[tokio::test]
786 async fn test_vec_nearest() -> Result<()> {
787 let model = create_test_model();
788 let ext = SparqlExtension::new(Box::new(model));
789
790 {
791 let mut model = ext.model.write().await;
792 model.train(Some(10)).await?;
793 }
794
795 let neighbors = ext
797 .vec_nearest("http://example.org/alice", 2, Some(0.0))
798 .await?;
799
800 assert!(neighbors.len() <= 2);
803
804 for (entity, sim) in neighbors {
805 assert!(!entity.is_empty());
806 assert!((0.0..=1.0).contains(&sim));
807 }
808
809 Ok(())
810 }
811
812 #[tokio::test]
813 async fn test_semantic_query_expansion() -> Result<()> {
814 let model = create_test_model();
815 let ext = SparqlExtension::new(Box::new(model));
816
817 {
818 let mut model = ext.model.write().await;
819 model.train(Some(10)).await?;
820 }
821
822 let query = r#"
823 SELECT ?s ?o WHERE {
824 ?s <http://example.org/knows> ?o
825 }
826 "#;
827
828 let expanded = ext.expand_query_semantically(query).await?;
829
830 assert_eq!(expanded.original_query, query);
831 assert!(!expanded.expanded_query.is_empty());
832
833 Ok(())
834 }
835
836 #[tokio::test]
837 async fn test_fuzzy_match() -> Result<()> {
838 let model = create_test_model();
839 let ext = SparqlExtension::new(Box::new(model));
840
841 let matches = ext.fuzzy_match_entity("alice", 3).await?;
842
843 assert!(matches.len() <= 3);
848 for (entity, score) in matches {
849 assert!(!entity.is_empty());
850 assert!((0.0..=1.0).contains(&score));
851 }
852
853 Ok(())
854 }
855
856 #[test]
857 fn test_parse_sparql_query() -> Result<()> {
858 let query = r#"
859 SELECT ?s ?o WHERE {
860 ?s <http://example.org/knows> ?o .
861 <http://example.org/alice> <http://example.org/likes> ?o .
862 }
863 "#;
864
865 let parsed = parse_sparql_query(query)?;
866
867 assert!(parsed.variables.contains("s"));
870 assert!(parsed.variables.contains("o"));
871
872 assert!(
875 !parsed.entities.is_empty() || !parsed.relations.is_empty(),
876 "Should extract at least some URIs from the query"
877 );
878
879 Ok(())
880 }
881
882 #[test]
883 fn test_cosine_similarity() -> Result<()> {
884 let v1 = Vector::new(vec![1.0, 0.0, 0.0]);
885 let v2 = Vector::new(vec![1.0, 0.0, 0.0]);
886 let sim = cosine_similarity(&v1, &v2)?;
887 assert!((sim - 1.0).abs() < 1e-6);
888
889 let v3 = Vector::new(vec![0.0, 1.0, 0.0]);
890 let sim2 = cosine_similarity(&v1, &v3)?;
891 assert!((sim2 - 0.0).abs() < 1e-6);
892
893 Ok(())
894 }
895
896 #[test]
897 fn test_levenshtein_distance() {
898 assert_eq!(levenshtein_distance("alice", "alice"), 0);
899 assert_eq!(levenshtein_distance("alice", "alise"), 1);
900 assert_eq!(levenshtein_distance("alice", "bob"), 5);
901 assert_eq!(levenshtein_distance("", "abc"), 3);
902 assert_eq!(levenshtein_distance("abc", ""), 3);
903 }
904
905 #[test]
906 fn test_fuzzy_match_score() {
907 assert!((fuzzy_match_score("alice", "alice") - 1.0).abs() < 1e-6);
908 assert!(fuzzy_match_score("alice", "alise") > 0.7);
909 assert!(fuzzy_match_score("alice", "bob") < 0.5);
910 }
911
912 #[tokio::test]
913 async fn test_statistics_tracking() -> Result<()> {
914 let model = create_test_model();
915 let ext = SparqlExtension::new(Box::new(model));
916
917 {
918 let mut model = ext.model.write().await;
919 model.train(Some(10)).await?;
920 }
921
922 let _ = ext
924 .vec_similarity("http://example.org/alice", "http://example.org/bob")
925 .await;
926 let _ = ext.vec_nearest("http://example.org/alice", 2, None).await;
927
928 let stats = ext.get_statistics().await;
929
930 assert!(stats.similarity_computations > 0);
931 assert!(stats.nearest_neighbor_queries > 0);
932
933 Ok(())
934 }
935
936 #[tokio::test]
937 async fn test_semantic_cache() -> Result<()> {
938 let model = create_test_model();
939 let ext = SparqlExtension::new(Box::new(model));
940
941 {
942 let mut model = ext.model.write().await;
943 model.train(Some(10)).await?;
944 }
945
946 let sim1 = ext
948 .vec_similarity("http://example.org/alice", "http://example.org/bob")
949 .await?;
950
951 let sim2 = ext
953 .vec_similarity("http://example.org/alice", "http://example.org/bob")
954 .await?;
955
956 assert!((sim1 - sim2).abs() < 1e-6);
957
958 ext.clear_cache().await;
960
961 Ok(())
962 }
963}