@@ -778,7 +778,9 @@ fn pisa_to_ciff_from_paths(
778778#[ derive( Debug , Deserialize ) ]
779779struct JsonDoc {
780780 /// Collection docid. CIFF will automatically assign an integer ID to this.
781+ /// Can be provided as either string or integer, will be converted to string.
781782 #[ serde( default ) ]
783+ #[ serde( deserialize_with = "deserialize_id_to_string" ) ]
782784 id : String ,
783785
784786 /// Optional textual content for the document.
@@ -790,11 +792,26 @@ struct JsonDoc {
790792 vector : HashMap < String , f64 > ,
791793}
792794
795+ fn deserialize_id_to_string < ' de , D > ( deserializer : D ) -> std:: result:: Result < String , D :: Error >
796+ where
797+ D : serde:: Deserializer < ' de > ,
798+ {
799+ use serde:: de:: Error ;
800+ use serde_json:: Value ;
801+
802+ match Value :: deserialize ( deserializer) . map_err ( |_| Error :: custom ( "failed to deserialize" ) ) ? {
803+ Value :: String ( s) => Ok ( s) ,
804+ Value :: Number ( n) => Ok ( n. to_string ( ) ) ,
805+ _ => Err ( Error :: custom ( "id must be string or number" ) ) ,
806+ }
807+ }
808+
793809/// PISA to CIFF converter.
794810#[ derive( Debug , Default , Clone ) ]
795811pub struct JsonlToCiff {
796812 input : Option < PathBuf > ,
797813 output : Option < PathBuf > ,
814+ quantize : bool ,
798815}
799816
800817impl JsonlToCiff {
@@ -810,6 +827,15 @@ impl JsonlToCiff {
810827 self
811828 }
812829
830+ /// Set whether to quantize scores to integers.
831+ /// If false, scores are assumed to be already pre-quantized and are cast directly to integers.
832+ /// If true, performs 8-bit scalar quantization by mapping the score range [min, max] to [1, 256].
833+ /// Default is false.
834+ pub fn quantize ( & mut self , quantize : bool ) -> & mut Self {
835+ self . quantize = quantize;
836+ self
837+ }
838+
813839 /// Performs the conversion from JSONL to CIFF.
814840 ///
815841 /// # Errors
@@ -831,13 +857,49 @@ impl JsonlToCiff {
831857 File :: open ( input_path) . with_context ( || format ! ( "Cannot open {input_path:?}" ) ) ?;
832858 let total_input_size = input_file. metadata ( ) ?. len ( ) ;
833859
860+ // If quantization is enabled, we need to find min/max first
861+ let ( min_score, max_score) = if self . quantize {
862+ eprintln ! ( "Finding score range for quantization" ) ;
863+ let reader = BufReader :: new ( & input_file) ;
864+ let mut min_val = f64:: INFINITY ;
865+ let mut max_val = f64:: NEG_INFINITY ;
866+
867+ let pb = ProgressBar :: new ( total_input_size) ;
868+ pb. set_style ( pb_style ( ) ) ;
869+ for line_result in reader. lines ( ) {
870+ let line = line_result?;
871+ let jdoc: JsonDoc = serde_json:: from_str ( & line)
872+ . map_err ( |e| anyhow ! ( "Invalid JSON line:\n `{}`\n Error: {}" , line, e) ) ?;
873+
874+ for ( _, score) in jdoc. vector {
875+ if score > 0.0 { // Only consider positive scores
876+ min_val = min_val. min ( score) ;
877+ max_val = max_val. max ( score) ;
878+ }
879+ }
880+ pb. inc ( line. len ( ) as u64 + 1 ) ;
881+ }
882+ pb. finish ( ) ;
883+
884+ if min_val. is_infinite ( ) || max_val. is_infinite ( ) {
885+ return Err ( anyhow ! ( "No valid scores found for quantization" ) ) ;
886+ }
887+
888+ eprintln ! ( "Score range: {} to {}" , min_val, max_val) ;
889+ ( min_val, max_val)
890+ } else {
891+ ( 0.0 , 0.0 ) // Not used when quantize is false
892+ } ;
893+
894+ // Reopen the file for the actual processing
895+ let input_file = File :: open ( input_path) . with_context ( || format ! ( "Cannot open {input_path:?}" ) ) ?;
834896 let reader = BufReader :: new ( input_file) ;
835897
836898 // We'll store doc-level info:
837899 let mut doc_records: Vec < DocRecord > = Vec :: new ( ) ;
838900
839901 // We'll map "term" -> (docid, tf).
840- // Because CIFF uses integer tf, we round any float to i32.
902+ // Because CIFF uses integer tf, we convert scores to i32.
841903 let mut postings_map: HashMap < String , Vec < ( i32 , i32 ) > > = HashMap :: new ( ) ;
842904
843905 let mut total_terms_in_collection: i64 = 0 ;
@@ -873,7 +935,21 @@ impl JsonlToCiff {
873935 // Sum of tf's in this doc => doc_length
874936 let mut doc_length = 0i64 ;
875937 for ( term, score) in jdoc. vector {
876- let tf = score. round ( ) as i32 ;
938+ let tf = if self . quantize {
939+ // 8-bit scalar quantization: map [min_score, max_score] to [1, 256]
940+ // We use 1-256 to avoid zero values which get filtered out
941+ if score <= 0.0 {
942+ 0 // Will be filtered out below
943+ } else {
944+ let normalized = ( score - min_score) / ( max_score - min_score) ;
945+ let quantized = ( normalized * 254.0 + 1.0 ) . round ( ) as i32 ;
946+ quantized. max ( 1 ) . min ( 255 )
947+ }
948+ } else {
949+ // Assume scores are already pre-quantized integers
950+ score as i32
951+ } ;
952+
877953 if tf <= 0 {
878954 continue ; // skip zero or negative
879955 }
0 commit comments