Skip to content

Commit 7f48e4b

Browse files
committed
More flexibility on numerical/string doc id and quantization
1 parent 7e1ab9f commit 7f48e4b

2 files changed

Lines changed: 84 additions & 3 deletions

File tree

src/jsonl2ciff.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ struct Args {
1515
input: PathBuf,
1616
#[structopt(short, long, help = "Output basename")]
1717
output: PathBuf,
18+
#[structopt(short, long, help = "Quantize scores to integers")]
19+
quantize: bool,
1820
}
1921

2022
fn main() {
2123
let args = Args::from_args();
2224

2325
let mut converter = JsonlToCiff::default();
24-
converter.input_path(args.input).output_path(args.output);
26+
converter
27+
.input_path(args.input)
28+
.output_path(args.output)
29+
.quantize(args.quantize);
2530

2631
if let Err(error) = converter.convert() {
2732
eprintln!("ERROR: {error}");

src/lib.rs

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,9 @@ fn pisa_to_ciff_from_paths(
778778
#[derive(Debug, Deserialize)]
779779
struct 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)]
795811
pub struct JsonlToCiff {
796812
input: Option<PathBuf>,
797813
output: Option<PathBuf>,
814+
quantize: bool,
798815
}
799816

800817
impl 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

Comments
 (0)