use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use anyhow::{Context, Result};
use tracing::info;
use hermes_core::{FsDirectory, IndexConfig};
pub fn train_centroids(
input: PathBuf,
field: String,
output: PathBuf,
clusters: Option<usize>,
max_iters: usize,
sample_size: Option<usize>,
seed: u64,
) -> Result<()> {
use hermes_core::structures::CoarseCentroids;
info!("Loading vectors from {:?}, field '{}'", input, field);
let file = File::open(&input).context("Failed to open input file")?;
let reader = BufReader::new(file);
let mut vectors: Vec<Vec<f32>> = Vec::new();
let max_samples = sample_size.unwrap_or(usize::MAX);
for line in reader.lines() {
if vectors.len() >= max_samples {
break;
}
let line = line?;
if line.trim().is_empty() {
continue;
}
let json: serde_json::Value =
serde_json::from_str(&line).context("Failed to parse JSON line")?;
if let Some(vec_value) = json.get(&field)
&& let Some(arr) = vec_value.as_array()
{
let vec: Vec<f32> = arr
.iter()
.filter_map(|v| v.as_f64().map(|f| f as f32))
.collect();
if !vec.is_empty() {
vectors.push(vec);
}
}
}
if vectors.is_empty() {
anyhow::bail!("No vectors found in input file");
}
let dim = vectors[0].len();
info!("Loaded {} vectors of dimension {}", vectors.len(), dim);
let num_clusters = clusters.unwrap_or_else(|| {
let sqrt = (vectors.len() as f64).sqrt() as usize;
sqrt.clamp(16, 65536)
});
info!(
"Training {} clusters with {} iterations (seed={})",
num_clusters, max_iters, seed
);
let start = std::time::Instant::now();
let coarse_config = hermes_core::structures::CoarseConfig::new(dim, num_clusters)
.with_max_iters(max_iters)
.with_seed(seed);
let centroids = CoarseCentroids::train(&coarse_config, &vectors);
let elapsed = start.elapsed();
info!(
"Training complete in {:.2}s, saving to {:?}",
elapsed.as_secs_f64(),
output
);
centroids
.save(&output)
.context("Failed to save centroids")?;
eprintln!(
"Trained {} clusters from {} vectors ({} dims) in {:.2}s",
num_clusters,
vectors.len(),
dim,
elapsed.as_secs_f64()
);
eprintln!("Saved to: {:?}", output);
Ok(())
}
pub async fn retrain_centroids(
index_path: PathBuf,
_field: String,
clusters: Option<usize>,
max_iters: usize,
sample_size: Option<usize>,
seed: u64,
) -> Result<()> {
use hermes_core::structures::CoarseCentroids;
info!("Opening index at {:?}", index_path);
let dir = FsDirectory::new(index_path.clone());
let config = IndexConfig::default();
let index = hermes_core::Index::open(dir, config)
.await
.context("Failed to open index")?;
info!("Collecting vectors from index segments...");
let vectors: Vec<Vec<f32>> = Vec::new();
let _max_samples = sample_size.unwrap_or(1_000_000);
if vectors.is_empty() {
eprintln!("Note: Vector extraction from existing index not yet implemented.");
eprintln!("Please use 'train-centroids' with a JSONL file containing vectors.");
eprintln!();
eprintln!("Example workflow:");
eprintln!(
" 1. Export vectors: hermes-tool export-vectors -i {} -f {} > vectors.jsonl",
index_path.display(),
_field
);
eprintln!(
" 2. Train centroids: hermes-tool train-centroids -i vectors.jsonl -f {} -o centroids.bin",
_field
);
eprintln!(" 3. Rebuild index with new centroids");
return Ok(());
}
let dim = vectors[0].len();
let num_clusters = clusters.unwrap_or_else(|| {
let sqrt = (vectors.len() as f64).sqrt() as usize;
sqrt.clamp(16, 65536)
});
info!(
"Training {} clusters from {} vectors",
num_clusters,
vectors.len()
);
let coarse_config = hermes_core::structures::CoarseConfig::new(dim, num_clusters)
.with_max_iters(max_iters)
.with_seed(seed);
let centroids = CoarseCentroids::train(&coarse_config, &vectors);
let centroids_path = index_path.join("coarse_centroids.bin");
centroids.save(¢roids_path)?;
info!("Saved centroids to {:?}", centroids_path);
eprintln!(
"Trained {} clusters, saved to {:?}",
num_clusters, centroids_path
);
eprintln!("Note: Index rebuild with new centroids not yet implemented.");
drop(index);
Ok(())
}