Skip to content

Commit ab46bbd

Browse files
authored
Merge PR #718: feat(types): add QualityScore metadata
feat(types): add QualityScore metadata (K/L/S) to IndexedDocument
2 parents d2a8b72 + d32d26d commit ab46bbd

3 files changed

Lines changed: 188 additions & 1 deletion

File tree

.cachebro/cache.db

88 KB
Binary file not shown.

crates/terraphim_rolegraph/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,7 @@ impl RoleGraph {
633633
rank: total_rank,
634634
tags: vec![normalized_term.to_string()],
635635
nodes: vec![node_id],
636+
quality_score: None,
636637
});
637638
}
638639
Entry::Occupied(mut e) => {
@@ -732,6 +733,7 @@ impl RoleGraph {
732733
rank: total_rank,
733734
tags: vec![normalized_term.to_string()],
734735
nodes: vec![node_id],
736+
quality_score: None,
735737
});
736738
}
737739
Entry::Occupied(mut e) => {
@@ -835,6 +837,7 @@ impl RoleGraph {
835837
rank: total_rank,
836838
tags: vec![normalized_term.to_string()],
837839
nodes: vec![node_id],
840+
quality_score: None,
838841
});
839842
}
840843
Entry::Occupied(mut e) => {
@@ -938,6 +941,7 @@ impl RoleGraph {
938941
rank: total_rank,
939942
tags: vec![normalized_term.to_string()],
940943
nodes: vec![node_id],
944+
quality_score: None,
941945
},
942946
vec![term.to_string()],
943947
));

crates/terraphim_types/src/lib.rs

Lines changed: 184 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -744,8 +744,67 @@ impl IntoIterator for Index {
744744
}
745745
}
746746

747+
/// Quality scores for Knowledge/Learning/Synthesis (K/L/S) dimensions.
748+
///
749+
/// These scores represent the quality of a document across three dimensions:
750+
/// - Knowledge: Depth and accuracy of domain knowledge
751+
/// - Learning: Educational value and clarity
752+
/// - Synthesis: Integration of concepts and insight
753+
///
754+
/// All scores are optional and range from 0.0 to 1.0 when present.
755+
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
756+
pub struct QualityScore {
757+
/// Knowledge quality score (0.0-1.0)
758+
pub knowledge: Option<f64>,
759+
/// Learning quality score (0.0-1.0)
760+
pub learning: Option<f64>,
761+
/// Synthesis quality score (0.0-1.0)
762+
pub synthesis: Option<f64>,
763+
}
764+
765+
impl QualityScore {
766+
/// Calculate the composite score by averaging all available scores.
767+
///
768+
/// Returns 0.0 if no scores are available.
769+
///
770+
/// # Examples
771+
///
772+
/// ```
773+
/// use terraphim_types::QualityScore;
774+
///
775+
/// let score = QualityScore {
776+
/// knowledge: Some(0.8),
777+
/// learning: Some(0.6),
778+
/// synthesis: None,
779+
/// };
780+
/// assert_eq!(score.composite(), 0.7); // (0.8 + 0.6) / 2
781+
///
782+
/// let empty = QualityScore::default();
783+
/// assert_eq!(empty.composite(), 0.0);
784+
/// ```
785+
pub fn composite(&self) -> f64 {
786+
let mut sum = 0.0;
787+
let mut count = 0;
788+
789+
if let Some(k) = self.knowledge {
790+
sum += k;
791+
count += 1;
792+
}
793+
if let Some(l) = self.learning {
794+
sum += l;
795+
count += 1;
796+
}
797+
if let Some(s) = self.synthesis {
798+
sum += s;
799+
count += 1;
800+
}
801+
802+
if count == 0 { 0.0 } else { sum / count as f64 }
803+
}
804+
}
805+
747806
/// Reference to external storage of documents
748-
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
807+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
749808
pub struct IndexedDocument {
750809
/// UUID of the indexed document, matching external storage id
751810
pub id: String,
@@ -758,6 +817,9 @@ pub struct IndexedDocument {
758817
pub tags: Vec<String>,
759818
/// List of node IDs for validation of matching
760819
pub nodes: Vec<u64>,
820+
/// Quality scores for K/L/S dimensions
821+
#[serde(default)]
822+
pub quality_score: Option<QualityScore>,
761823
}
762824

763825
impl IndexedDocument {
@@ -771,6 +833,7 @@ impl IndexedDocument {
771833
rank: 0,
772834
tags: document.tags.unwrap_or_default(),
773835
nodes: Vec::new(),
836+
quality_score: None,
774837
}
775838
}
776839
}
@@ -2931,4 +2994,124 @@ mod tests {
29312994
let deserialized: SearchQuery = serde_json::from_str(&json).unwrap();
29322995
assert_eq!(deserialized.layer, Layer::Two);
29332996
}
2997+
2998+
#[test]
2999+
fn test_quality_score_composite() {
3000+
// Test with all three scores
3001+
let full_score = QualityScore {
3002+
knowledge: Some(0.8),
3003+
learning: Some(0.6),
3004+
synthesis: Some(0.7),
3005+
};
3006+
assert!((full_score.composite() - 0.7).abs() < f64::EPSILON); // (0.8 + 0.6 + 0.7) / 3
3007+
3008+
// Test with two scores
3009+
let partial_score = QualityScore {
3010+
knowledge: Some(0.9),
3011+
learning: None,
3012+
synthesis: Some(0.5),
3013+
};
3014+
assert!((partial_score.composite() - 0.7).abs() < f64::EPSILON); // (0.9 + 0.5) / 2
3015+
3016+
// Test with one score
3017+
let single_score = QualityScore {
3018+
knowledge: Some(0.8),
3019+
learning: None,
3020+
synthesis: None,
3021+
};
3022+
assert!((single_score.composite() - 0.8).abs() < f64::EPSILON);
3023+
3024+
// Test with no scores (default)
3025+
let empty_score = QualityScore::default();
3026+
assert_eq!(empty_score.composite(), 0.0);
3027+
}
3028+
3029+
#[test]
3030+
fn test_quality_score_serialization() {
3031+
let score = QualityScore {
3032+
knowledge: Some(0.8),
3033+
learning: Some(0.6),
3034+
synthesis: Some(0.7),
3035+
};
3036+
3037+
let json = serde_json::to_string(&score).unwrap();
3038+
assert!(json.contains("0.8"));
3039+
assert!(json.contains("0.6"));
3040+
assert!(json.contains("0.7"));
3041+
3042+
let deserialized: QualityScore = serde_json::from_str(&json).unwrap();
3043+
assert_eq!(deserialized.knowledge, Some(0.8));
3044+
assert_eq!(deserialized.learning, Some(0.6));
3045+
assert_eq!(deserialized.synthesis, Some(0.7));
3046+
}
3047+
3048+
#[test]
3049+
fn test_quality_score_default_serialization() {
3050+
// Test that default QualityScore serializes/deserializes correctly
3051+
let score = QualityScore::default();
3052+
let json = serde_json::to_string(&score).unwrap();
3053+
let deserialized: QualityScore = serde_json::from_str(&json).unwrap();
3054+
assert!(deserialized.knowledge.is_none());
3055+
assert!(deserialized.learning.is_none());
3056+
assert!(deserialized.synthesis.is_none());
3057+
}
3058+
3059+
#[test]
3060+
fn test_indexed_document_with_quality_score() {
3061+
let doc = IndexedDocument {
3062+
id: "test-doc-1".to_string(),
3063+
matched_edges: vec![],
3064+
rank: 10,
3065+
tags: vec!["rust".to_string()],
3066+
nodes: vec![1, 2],
3067+
quality_score: Some(QualityScore {
3068+
knowledge: Some(0.8),
3069+
learning: Some(0.6),
3070+
synthesis: Some(0.7),
3071+
}),
3072+
};
3073+
3074+
assert_eq!(doc.id, "test-doc-1");
3075+
assert!((doc.quality_score.as_ref().unwrap().composite() - 0.7).abs() < f64::EPSILON);
3076+
}
3077+
3078+
#[test]
3079+
fn test_indexed_document_from_document_quality_score_none() {
3080+
let doc = Document {
3081+
id: "doc-1".to_string(),
3082+
url: "https://example.com".to_string(),
3083+
title: "Test".to_string(),
3084+
body: "Body".to_string(),
3085+
description: None,
3086+
summarization: None,
3087+
stub: None,
3088+
tags: None,
3089+
rank: None,
3090+
source_haystack: None,
3091+
doc_type: DocumentType::Document,
3092+
synonyms: None,
3093+
route: None,
3094+
priority: None,
3095+
};
3096+
3097+
let indexed = IndexedDocument::from_document(doc);
3098+
assert!(indexed.quality_score.is_none());
3099+
}
3100+
3101+
#[test]
3102+
fn test_indexed_document_serialization_backward_compat() {
3103+
// Test that IndexedDocument without quality_score deserializes correctly
3104+
// This simulates old data that doesn't have the quality_score field
3105+
let json = r#"{
3106+
"id": "doc-1",
3107+
"matched_edges": [],
3108+
"rank": 5,
3109+
"tags": ["test"],
3110+
"nodes": [1]
3111+
}"#;
3112+
3113+
let doc: IndexedDocument = serde_json::from_str(json).unwrap();
3114+
assert_eq!(doc.id, "doc-1");
3115+
assert!(doc.quality_score.is_none());
3116+
}
29343117
}

0 commit comments

Comments
 (0)