bgustscraper 0.2.0

Advanced semantic scraping engine with AI-driven compliance checks and legal terms validation
Documentation
use linfa::traits::Fit;
use linfa_clustering::KMeans;
use ndarray::Array2;

/// Agrupa las oraciones por similitud semántica y selecciona las oraciones más cercanas
/// a los centroides de cada uno de los `k` clusters para lograr compresión extractiva sin redundancia.
pub fn compress_text_by_clustering(
    sentences: &[String],
    embeddings: &[Vec<f32>],
    k: usize,
) -> Vec<String> {
    if sentences.is_empty() || embeddings.is_empty() {
        return Vec::new();
    }

    let n = embeddings.len();
    let d = embeddings[0].len();

    // Si hay menos oraciones que clusters deseados, retornar todas las oraciones
    if n <= k {
        return sentences.to_vec();
    }

    // 1. Construir la matriz de ndarray (linfa utiliza f64 para cálculos de precisión)
    let mut data = Array2::zeros((n, d));
    for (i, row) in embeddings.iter().enumerate() {
        for (j, &val) in row.iter().enumerate() {
            data[[i, j]] = val as f64;
        }
    }

    // Envolver los datos en un Dataset de Linfa
    let dataset = linfa::Dataset::from(data);

    // 2. Ejecutar el algoritmo K-Means con linfa-clustering
    let model = match KMeans::params(k)
        .max_n_iterations(100)
        .tolerance(1e-4)
        .fit(&dataset)
    {
        Ok(m) => m,
        Err(e) => {
            eprintln!("⚠️ Error al ejecutar K-Means: {}. Usando fallback de primer bloque.", e);
            return sentences.iter().take(k).cloned().collect();
        }
    };

    // 3. Obtener los centroides de los clusters calculados
    let centroids = model.centroids();

    // 4. Para cada centroide, buscar la oración más cercana en el espacio vectorial (Distancia Euclidiana)
    let mut selected_indices = Vec::new();
    for cluster_idx in 0..centroids.nrows() {
        let centroid = centroids.row(cluster_idx);
        let mut min_dist = f64::MAX;
        let mut best_idx = 0;

        for sentence_idx in 0..n {
            let mut dist = 0.0;
            for j in 0..d {
                let diff = embeddings[sentence_idx][j] as f64 - centroid[j];
                dist += diff * diff;
            }
            if dist < min_dist {
                min_dist = dist;
                best_idx = sentence_idx;
            }
        }

        // Evitar duplicar la misma oración si es la más cercana a múltiples centroides
        if !selected_indices.contains(&best_idx) {
            selected_indices.push(best_idx);
        }
    }

    // 5. Ordenar los índices para que las oraciones mantengan su orden de lectura original en el documento
    selected_indices.sort();

    selected_indices
        .into_iter()
        .map(|idx| sentences[idx].clone())
        .collect()
}