@@ -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 ) ]
749808pub 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
763825impl 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