use chess::Board;
use indicatif::{ProgressBar, ProgressStyle};
use serde_json;
use std::collections::HashSet;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
pub struct StreamingLoader {
pub loaded_count: usize,
pub duplicate_count: usize,
pub total_processed: usize,
}
impl StreamingLoader {
pub fn new() -> Self {
Self {
loaded_count: 0,
duplicate_count: 0,
total_processed: 0,
}
}
pub fn stream_load_json<P: AsRef<Path>>(
&mut self,
path: P,
engine: &mut crate::ChessVectorEngine,
batch_size: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let path_ref = path.as_ref();
println!("Operation complete");
let file = File::open(path_ref)?;
let reader = BufReader::with_capacity(64 * 1024, file);
let total_lines = self.estimate_line_count(path_ref)?;
println!("📊 Estimated {total_lines} lines to process");
let pb = ProgressBar::new(total_lines as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("⚡ Streaming [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} ({percent}%) {msg}")?
.progress_chars("██░")
);
let existing_boards: HashSet<Board> = engine.position_boards.iter().cloned().collect();
let initial_size = existing_boards.len();
let mut batch_boards = Vec::with_capacity(batch_size);
let mut batch_evaluations = Vec::with_capacity(batch_size);
let mut line_count = 0;
for line_result in reader.lines() {
let line = line_result?;
line_count += 1;
if line.trim().is_empty() {
continue;
}
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&line) {
if let Some((board, evaluation)) = self.extract_position_data(&json)? {
if !existing_boards.contains(&board) {
batch_boards.push(board);
batch_evaluations.push(evaluation);
if batch_boards.len() >= batch_size {
self.process_batch(engine, &mut batch_boards, &mut batch_evaluations)?;
pb.set_message(format!(
"{loaded} loaded, {dupes} dupes",
loaded = self.loaded_count,
dupes = self.duplicate_count
));
}
} else {
self.duplicate_count += 1;
}
}
}
self.total_processed += 1;
if line_count % 1000 == 0 {
pb.set_position(line_count as u64);
}
}
if !batch_boards.is_empty() {
self.process_batch(engine, &mut batch_boards, &mut batch_evaluations)?;
}
pb.finish_with_message(format!(
"✅ Complete: {} loaded, {} duplicates from {} lines",
self.loaded_count, self.duplicate_count, line_count
));
let new_positions = engine.position_boards.len() - initial_size;
println!("🎯 Added {new_positions} new positions to engine");
Ok(())
}
pub fn stream_load_binary<P: AsRef<Path>>(
&mut self,
path: P,
engine: &mut crate::ChessVectorEngine,
) -> Result<(), Box<dyn std::error::Error>> {
let path_ref = path.as_ref();
println!("Operation complete");
let data = std::fs::read(path_ref)?;
println!("📦 Read {} bytes", data.len());
let decompressed_data = if let Ok(decompressed) = lz4_flex::decompress_size_prepended(&data)
{
println!(
"🗜️ LZ4 decompressed: {} → {} bytes",
data.len(),
decompressed.len()
);
decompressed
} else {
data
};
let positions: Vec<(String, f32)> = bincode::deserialize(&decompressed_data)?;
let total_positions = positions.len();
println!("📊 Loaded {total_positions} positions from binary");
if total_positions == 0 {
return Ok(());
}
let pb = ProgressBar::new(total_positions as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("⚡ Binary loading [{elapsed_precise}] [{bar:40.blue/green}] {pos}/{len} ({percent}%) {msg}")?
.progress_chars("██░")
);
let existing_boards: HashSet<Board> = engine.position_boards.iter().cloned().collect();
const BATCH_SIZE: usize = 10000;
let mut processed = 0;
for chunk in positions.chunks(BATCH_SIZE) {
let mut batch_boards = Vec::with_capacity(BATCH_SIZE);
let mut batch_evaluations = Vec::with_capacity(BATCH_SIZE);
for (fen, evaluation) in chunk {
if let Ok(board) = fen.parse::<Board>() {
if !existing_boards.contains(&board) {
batch_boards.push(board);
batch_evaluations.push(*evaluation);
} else {
self.duplicate_count += 1;
}
}
processed += 1;
}
if !batch_boards.is_empty() {
self.process_batch(engine, &mut batch_boards, &mut batch_evaluations)?;
}
pb.set_position(processed as u64);
pb.set_message(format!("{count} loaded", count = self.loaded_count));
}
pb.finish_with_message(format!(
"✅ Loaded {count} positions",
count = self.loaded_count
));
Ok(())
}
fn process_batch(
&mut self,
engine: &mut crate::ChessVectorEngine,
boards: &mut Vec<Board>,
evaluations: &mut Vec<f32>,
) -> Result<(), Box<dyn std::error::Error>> {
for (board, evaluation) in boards.iter().zip(evaluations.iter()) {
engine.add_position(board, *evaluation);
self.loaded_count += 1;
}
boards.clear();
evaluations.clear();
Ok(())
}
fn extract_position_data(
&self,
json: &serde_json::Value,
) -> Result<Option<(Board, f32)>, Box<dyn std::error::Error>> {
if let (Some(fen), Some(eval)) = (
json.get("fen").and_then(|v| v.as_str()),
json.get("evaluation").and_then(|v| v.as_f64()),
) {
if let Ok(board) = fen.parse::<Board>() {
return Ok(Some((board, eval as f32)));
}
}
if let (Some(fen), Some(eval)) = (
json.get("board").and_then(|v| v.as_str()),
json.get("eval").and_then(|v| v.as_f64()),
) {
if let Ok(board) = fen.parse::<Board>() {
return Ok(Some((board, eval as f32)));
}
}
if let (Some(fen), Some(eval)) = (
json.get("position").and_then(|v| v.as_str()),
json.get("score").and_then(|v| v.as_f64()),
) {
if let Ok(board) = fen.parse::<Board>() {
return Ok(Some((board, eval as f32)));
}
}
Ok(None)
}
fn estimate_line_count<P: AsRef<Path>>(
&self,
path: P,
) -> Result<usize, Box<dyn std::error::Error>> {
use std::io::Read;
let path_ref = path.as_ref();
let file = File::open(path_ref)?;
let mut reader = BufReader::new(file);
let mut sample = vec![0u8; 1024 * 1024];
let bytes_read = reader.read(&mut sample)?;
if bytes_read == 0 {
return Ok(0);
}
let newlines_in_sample = sample[..bytes_read].iter().filter(|&&b| b == b'\n').count();
let total_size = std::fs::metadata(path_ref)?.len() as usize;
if bytes_read >= total_size {
return Ok(newlines_in_sample);
}
let estimated_lines = (newlines_in_sample * total_size) / bytes_read;
Ok(estimated_lines)
}
}
impl Default for StreamingLoader {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_json_extraction() {
let loader = StreamingLoader::new();
let json = serde_json::json!({
"fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
"evaluation": 0.25
});
let result = loader.extract_position_data(&json).unwrap();
assert!(result.is_some());
let (board, eval) = result.unwrap();
assert_eq!(board, Board::default());
assert_eq!(eval, 0.25);
}
#[test]
fn test_line_estimation() {
let loader = StreamingLoader::new();
let mut temp_file = NamedTempFile::new().unwrap();
for _i in 0..100 {
writeln!(temp_file, "Loading complete").unwrap();
}
let estimated = loader.estimate_line_count(temp_file.path()).unwrap();
assert!((80..=120).contains(&estimated));
}
}