Chunking en IA: cómo dividir datos de forma inteligente

En el procesamiento de lenguaje natural, los sistemas de recuperación de información y las aplicaciones que trabajan con grandes volúmenes de texto, hay una técnica que marca la diferencia entre un sistema limitado y uno realmente útil: el chunking.
El chunking consiste en dividir un texto o un conjunto de datos en fragmentos más pequeños y manejables llamados chunks. Aunque suena simple, la forma en que se realiza esta división tiene un impacto directo en la calidad de los resultados de un modelo de lenguaje, en la precisión de un sistema RAG y en el rendimiento general de cualquier aplicación que procese documentos largos.

¿Por qué es tan importante el chunking?

Los modelos de lenguaje tienen un límite de contexto. Si se intenta enviar un documento de 50 páginas de una sola vez, el modelo simplemente no puede procesarlo completo o pierde información relevante. Además, cuando se trabaja con bases de datos vectoriales, los embeddings se generan a nivel de chunk. Un mal chunking genera embeddings poco representativos y, por tanto, recuperaciones irrelevantes.
Un buen chunking busca el equilibrio entre:
  • Mantener suficiente contexto semántico.
  • No superar los límites de tokens del modelo.
  • Permitir una recuperación precisa y eficiente.

Principales estrategias de chunking

No existe una estrategia universal. La mejor elección depende del tipo de documento y del objetivo. Estas son las más utilizadas:

Chunking por tamaño fijo

Se divide el texto cada X caracteres o tokens, normalmente con un pequeño solapamiento, o overlap, para no cortar ideas a la mitad. Es simple y rápido, pero puede romper oraciones o párrafos en puntos poco naturales.

Chunking por oraciones o párrafos

Se respetan los límites naturales del lenguaje. Funciona bien con textos bien estructurados, pero puede generar chunks de tamaños muy desiguales.

Chunking recursivo

Es una de las estrategias más usadas actualmente. Intenta dividir primero por separadores de mayor prioridad, como doble salto de línea, salto de línea, punto, espacio…, y solo si el fragmento sigue siendo demasiado grande, baja al siguiente separador. Ofrece un buen equilibrio entre coherencia semántica y control de tamaño.

Chunking semántico

Utiliza embeddings o modelos de lenguaje para agrupar frases que tratan del mismo tema. Es más costoso computacionalmente, pero produce chunks mucho más coherentes.

Chunking basado en estructura del documento

Especialmente útil en PDFs, Markdown o HTML: se aprovechan títulos, secciones, tablas o listas para crear límites naturales.

En la práctica, la combinación más efectiva suele ser el chunking recursivo con solapamiento, normalmente entre el 10 % y el 20 % del tamaño del chunk.

Ejemplo práctico en PHP para PDFs

La mayoría de los documentos reales llegan en formato PDF. Por eso es importante extraer el texto de forma limpia antes de aplicar el chunking.Para este ejemplo usaremos la librería "smalot/pdfparser", una de las más utilizadas y estables en PHP.

composer require smalot/pdfparser
Clase completa que:
  • Extrae el texto de un PDF
  • Limpia el contenido (saltos de línea excesivos, caracteres extraños, etc.
  • Aplica chunking recursivo con solapamiento
require 'vendor/autoload.php';

use Smalot\PdfParser\Parser;

class PdfTextChunker
{
    private array $separators;
    private int $chunkSize;
    private int $chunkOverlap;

    public function __construct(
        int $chunkSize = 1000,
        int $chunkOverlap = 200,
        array $separators = ["\n\n", "\n", ". ", " ", ""]
    ) {
        $this->chunkSize = $chunkSize;
        $this->chunkOverlap = $chunkOverlap;
        $this->separators = $separators;
    }

    /**
     * Extrae el texto de un PDF y lo divide en chunks
     */
    public function splitFromPdf(string $pdfPath): array
    {
        $parser = new Parser();
        $pdf = $parser->parseFile($pdfPath);
        $text = $pdf->getText();

        // Limpieza básica del texto extraído
        $text = $this->cleanText($text);

        return $this->split($text);
    }

    /**
     * Limpia el texto extraído del PDF
     */
    private function cleanText(string $text): string
    {
        // Normalizar saltos de línea
        $text = str_replace(["\r\n", "\r"], "\n", $text);

        // Eliminar múltiples saltos de línea consecutivos
        $text = preg_replace("/\n{3,}/", "\n\n", $text);

        // Eliminar espacios múltiples
        $text = preg_replace("/[ \t]+/", " ", $text);

        // Eliminar caracteres de control (excepto saltos de línea y tabs)
        $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);

        return trim($text);
    }

    public function split(string $text): array
    {
        return $this->recursiveSplit($text, $this->separators);
    }

    private function recursiveSplit(string $text, array $separators): array
    {
        $finalChunks = [];
        $separator = $separators[0] ?? "";
        $remainingSeparators = array_slice($separators, 1);

        if (mb_strlen($text) <= $this->chunkSize) {
            return $text === "" ? [] : [$text];
        }

        $splits = $separator === ""
            ? mb_str_split($text, $this->chunkSize)
            : explode($separator, $text);

        $currentChunk = "";
        foreach ($splits as $split) {
            $candidate = $currentChunk === ""
                ? $split
                : $currentChunk . $separator . $split;

            if (mb_strlen($candidate) <= $this->chunkSize) {
                $currentChunk = $candidate;
            } else {
                if ($currentChunk !== "") {
                    $finalChunks[] = $currentChunk;
                }

                if (mb_strlen($split) > $this->chunkSize && !empty($remainingSeparators)) {
                    $subChunks = $this->recursiveSplit($split, $remainingSeparators);
                    $finalChunks = array_merge($finalChunks, $subChunks);
                    $currentChunk = "";
                } else {
                    $currentChunk = $split;
                }
            }
        }

        if ($currentChunk !== "") {
            $finalChunks[] = $currentChunk;
        }

        return $this->applyOverlap($finalChunks);
    }

    private function applyOverlap(array $chunks): array
    {
        if ($this->chunkOverlap <= 0 || count($chunks) <= 1) {
            return $chunks;
        }

        $result = [];
        $previous = "";

        foreach ($chunks as $chunk) {
            if ($previous !== "") {
                $overlapText = mb_substr($previous, -$this->chunkOverlap);
                $chunk = $overlapText . $chunk;
            }
            $result[] = $chunk;
            $previous = $chunk;
        }

        return $result;
    }
}

// ==================== Ejemplo de uso ====================

$chunker = new PdfTextChunker(
    chunkSize: 900,      // Ajusta según el modelo (aprox. 200-300 tokens)
    chunkOverlap: 150
);

$pdfPath = 'documento.pdf';   // Cambia por la ruta de tu archivo

try {
    $chunks = $chunker->splitFromPdf($pdfPath);

    echo "Total de chunks generados: " . count($chunks) . PHP_EOL . PHP_EOL;

    foreach ($chunks as $i => $chunk) {
        echo "=== Chunk " . ($i + 1) . " (" . mb_strlen($chunk) . " caracteres) ===" . PHP_EOL;
        echo mb_substr($chunk, 0, 180) . "..." . PHP_EOL . PHP_EOL;
    }
} catch (Exception $e) {
    echo "Error al procesar el PDF: " . $e->getMessage() . PHP_EOL;
}

Notas importantes sobre PDFs

  • Calidad de extracción: Algunos PDFs, especialmente los escaneados o con diseños complejos, extraen el texto de forma desordenada. En esos casos conviene usar OCR, por ejemplo con "thiagoalessio/tesseract_ocr" antes del chunking.
  • Páginas: Si se necesita mantener la información de página, se puede modificar el método para recorrer "$pdf->getPages()" y añadir metadatos a cada chunk.
  • Rendimiento: Para PDFs muy grandes, cientos de páginasm es recomendable procesar página por página en lugar de cargar todo el texto de golpe.

Consejos prácticos para obtener mejores resultados

  • Ajustar el tamaño del chunk según el modelo que vayas a usar (por ejemplo, 512–1024 tokens para muchos modelos actuales.
  • El solapamiento es especialmente útil cuando el contexto entre oraciones es importante.
  • Limpiar el texto antes de hacer chunking, eliminar saltos de línea excesivos, caracteres de control, etc.
  • Si se trabaja con documentos técnicos o código, considerar separadores específicos, como class, function, #, etc.
  • Evaluar la calidad de los chunks recuperando información y midiendo la relevancia de los resultados.
El chunking no es solo una etapa técnica más del pipeline. Es una de las decisiones de diseño que más influyen en la calidad final de cualquier sistema basado en IA generativa o recuperación semántica. Dedicar tiempo a elegir y ajustar la estrategia correcta suele devolver resultados mucho más consistentes que simplemente aumentar el tamaño del modelo o la cantidad de datos.