Chunkingstrategieën in RAG-vergelijking: Alternatieven, afwegingen en voorbeelden

Vergelijking van chunkingstrategieën in RAG

Inhoud

Chunking is de meest onderschatte hyperparameter in Retrieval ‑ Augmenteerde Generatie (RAG): het bepaalt stilzwijgend wat je LLM “ziet”, hoe duur de ingesting wordt, en hoeveel van de contextwindow van de LLM je verbruikt per antwoord.

Dit artikel behandelt chunking als een engineering optimalisatieprobleem: doelen definiëren, een strategie kiezen, meten, en dan itereren.

Als je nieuw bent in RAG architectuur, begin dan met de hoofd Retrieval-Augmenteerde Generatie (RAG) Tutorial: Architectuur, Implementatie, en Productiehandleiding.

kleurige tetris op tafel

TL;DR (Executive summary)

RAG-systemen halen chunks op, niet documenten. Chunking bepaalt dus de eenheid van opvragen, de eenheid van embeddingkosten, en de eenheid van bewijs die je kunt tonen of citeren. In de oorspronkelijke RAG-formulering levert opvragen passages op voor generatie; de grenzen van de passage zijn effectief de grenzen van de chunk.

Een goede chunkingstrategie zoekt een Paretofront over: opvragingskwaliteit (herkennen/precisie van bewijs), samenhang (chunks moeten interpreteerbaar zijn), en kosten (embedding, opslag en querylatency). Er is geen globaal optimale chunkgrootte of methode, en productiesystemen mengen vaak strategieën (bijvoorbeeld structuurbewuste chunking voor PDFs + semantisch bewuste splitsen voor proza + AST chunking voor code).

Voor de meeste “documentatie QA” en interne kennisbases is een veilige standaard een structuurrespecterende recursieve splitter met matige overlap (om grensverlies te verminderen), ondersteund door een vectoropslag met metadatafiltering en optioneel reranking. LangChain’s RecursiveCharacterTextSplitter is een veelvoorkomende implementatie van deze hiërarchische-scheider-idee; overlap bestaat specifiek om informatieverlies te verminderen wanneer relevante context wordt gesneden aan grenzen.

Wanneer documenten een sterke structuur hebben (PDFs met koppen, tabellen, lijsten, legenden), kan elementgebaseerde / structuurbewuste chunking beter presteren dan token-aantallen snijden terwijl het minder chunks produceert. Een 2024 studie over SEC-aangiften vond dat elementtypegebaseerde chunking RAG-resultaten verbeterde en ook het aantal chunks (en dus vectoren) ongeveer met de helft verminderde in vergelijking met structuur-onafhankelijke methoden—het indexkosten verminderde en mogelijk de querylatency verbeterde.

Als je meer up-front rekenkracht kunt permitteren, kan semantische chunking (splitsen op themaoverschrijdingen met embedding-afstand) aanzienlijk de opvragingsnauwkeurigheid verbeteren voor narratief tekst en gemengde themapagina’s. Oudere themasegmentatiealgoritmen zoals TextTiling tonen het algemene principe: sterke vocabulaire/semantische overschrijdingen zijn goede grenscandidates.

Voor zeer lange, intern kruisverwijzende materialen (beleidsregels, RFCs, standaarden, grote handleidingen), kan hiërarchische chunking + hiërarchische opvragen/vereniging (ouder/kinderknopen) grotere aaneengesloten context op aanvraag herstellen. LlamaIndex’s hiërarchische knoopparser produceert grof-voor-fijn chunkhiërarchieën, en de AutoMergingRetriever kan bladerknopen in ouderknopen verenigen bij opvragen als genoeg gerelateerde kinderknopen zijn opgevraagd.

Chunkingdoelen en trade-offs

Chunking is niet alleen “tekst splitsen zodat het past in een embeddingmodel”. Het beheerst meerdere downstream en operationele gedragingen.

Opvragingsgranulariteit vs opvragingsrui. Kleine chunks verhogen de kans dat de exacte zin met het antwoord opvragbaar is (hogere potentieel herkennen bij vast top-k). Maar ze produceren ook meer vectoren, wat de indexgrootte verhoogt en soms “bijna overeenkomende” resultaten oplevert die semantisch gelijk zijn maar niet feitelijk bewijselijk (lagere precisie). Dense retrievers zoals DPR zijn ontworpen rond het effectief opvragen van passages voor QA, wat aantoont dat passagegrenzen belangrijk zijn voor eind- tot-eind QA-prestaties.

Contextsamenhang vs grensverlies. Samenhangende chunks helpen de LLM correct te redeneren en verminderen hallucinaties door volledige lokale context te bieden (definities, beperkingen, voorwaarden). Overlap verminderd grensverlies maar creëert duplicaattekst, wat kan leiden tot redundante opvragingsresultaten en uitgebreide promptlengte als je niet deduplicate/verenigt.

Embedding- en indexkosten. Embeddingkosten zijn meestal evenredig met het aantal ingebedde tokens, en de ingestingstijd schaalt met het aantal chunks (plus vector DB schrijfoverhead). Voor OpenAI embeddings zijn aanvragen beperkt op per-input maximaal aantal tokens (8192 tokens voor alle embeddingmodellen) en een maximaal totaal aantal tokens per aanvraag (300.000 tokens). Voor grote corpora kan de Batch API de kosten met ongeveer 50% verminderen met een asynchrone, 24-uur omloop—handig voor backfills en periodieke herindexering.

Vectorindexgrootte, RAM en latency. Meer chunks betekent meer vectoren en mogelijk meer geheugen en langzamere queries (afhankelijk van de indexsoort). FAISS stelt expliciet indexontwerp voor als een set trade-offs tussen zoektijd, zoekkwaliteit en geheugen per ingebedde vector; het biedt ook GPU-implementaties voor snelle exacte en benaderde zoekopdrachten.

Downstream LLM promptlengte / contextwindowgebruik. Het opvragingsresultaat wordt promptbudget. Een chunkingstrategie die consistent “net genoeg” context oplevert kan de antwoordkwaliteit verbeteren en kosten verminderen. Aan de andere kant verhoogt overlap en te grote chunks de promptlengte. In de praktijk stel je vaak af: (chunkgrootte, overlap, top-k, reranking/vereniging) samen.

Update/ingestingkosten en deduplicatie. Chunking beïnvloedt hoe duur het is om gegevens bij te werken. Kleine chunks maken gedeeltelijke updates goedkoper (je kunt alleen de gewijzigde sectie herembedden) maar maken ook deduplicatie moeilijker als overlatende of bijna-duplicatieve chunks prolifereren.

Waar chunking zich bevindt in de RAG-werkstroom

chunking in rag flow

Chunkingstrategieën en alternatieven

Hieronder staan de belangrijkste chunkingfamilies die je tegenkomt in moderne RAG. In de praktijk meng je vaak twee: structuur-georiënteerde chunking (documentgrenzen respecteren) plus token-budgetverplichting (zorg dat chunks passen in je embedding- en promptbudgets).

Vaste grootte chunking

Wat het is. Tekst splitsen in gelijke blokken op basis van karakters of tokens.

Waarom het bestaat. Het is eenvoudig, snel, voorspelbaar en makkelijk te paralleliseren. Het is ook de eenvoudigste strategie voor streaming ingestion waarbij je geen volledige documentcontext hebt.

Waar het mislukt. Het negeert grenzen (zinnen, secties, codeblokken) en kan definities breken of “vraag/antwoordparen” over chunks splitsen, wat het opvragingsfoutpercentage verhoogt.

Operationele profiel. Laagste ingestingscomplexiteit; voorspelbaar aantal chunks; makkelijkste caching. Maar je hebt meestal overlap (onderaan) nodig om grensverlies te vermijden.

Overlap chunking

Wat het is. Elke strategie waarbij opeenvolgende chunks een vaste overlapregio delen (bijvoorbeeld 10–20% van de chunkgrootte). Overlap is standaard in veel frameworks omdat het informatieverlies vermindert wanneer context wordt verdeeld.

Waarom het belangrijk is. Overlap is effectief een “zachte grens”—het laat opvragen een feit dat over een grens ligt.

Kosten en valkuilen. Meer ingebedde tokens; meer duplicatetekst in de index; hoger risico op het opvragen van meerdere bijna identieke chunks tenzij je deduplicate tijdens het opvragen (bijvoorbeeld samenvoegen op bronoffsets of MMR gebruiken).

Zin- en paragraafgebaseerd chunking

Wat het is. Tekst splitsen op zin- of paragraafgrenzen en vervolgens zinnen/paragrafen in chunks pakken tot een tokenbudget.

Waarom ingenieurs het leuk vinden. Het verbetert samenhang voor natuurlijke taal en is robuust voor documenten met conventionele interpunctie en spaties.

Tooling. NLTK’s sent_tokenize() gebruikt standaard Punkt zinbegrenzing, en spaCy biedt regelgebaseerde zinbegrenzingsgereedschappen zoals Sentencizer (handig wanneer je zinsplitsing wilt zonder een volledig afhankelijkheidsmodel).

Mislukkingsmodi. Niet-standaard interpunctie (logboeken, chattranscripten), tabellen, code en lijsten kunnen zinssegmentatieaannames breken.

Glijdend venster chunking

Wat het is. Chunken maken met een vaste venstergrootte en een stap (stapgrootte). Dit is de “systematische overlap” versie van chunking.

Wanneer het goed is. Tijdreeks tekst, transcripten, chatlogboeken, vergaderminuten—alles waar relevante feiten kunnen verschijnen in lokale omgevingen en je robuuste herkennen wilt.

Wanneer het slecht is. Het versterkt redundantie en kan duur zijn op schaal. Het tendingt ook tot het opvragen van redundante vensters tenzij deduped.

Recursief / scheiderhiërarchie chunking

Wat het is. Begin met grote “natuurlijke” scheiders (bijvoorbeeld \n\n voor paragrafen) en splits recursief in kleinere eenheden (zinnen, spaties) alleen als nodig om onder een groottebudget te blijven. LangChain beschrijft dit gedrag expliciet: een recursieve splitter probeert grotere eenheden intact te houden en valt pas terug op kleinere scheiders als een eenheid nog steeds de chunkgrootte overschrijdt. Waarom het een sterke standaard is. Het respecteert structuur zonder dat complexe documentverwerking vereist is. Het is een pragmatische sweet spot voor Markdown, HTML als tekst en documentatie.

Belangrijke afstemmingsknopen. chunk_size, chunk_overlap, en de length_function (karakters vs tokens), plus aangepaste scheiders voor meertalige codebases.

Semantisch (embeddingbewuste) chunking

Wat het is. Detecteer themaverschuivingen met behulp van semantische representaties (embeddings) en splits waar de gelijkenis daalt. Dit spiegelt klassieke segmentatieideeën zoals TextTiling, die gebruikmaakt van shifts in lexicaal samenhang om subthema grenzen te vinden.

Waarom het kan uitpresteren op basis van grootte. Je stopt met splitsen op willekeurige tokenaantallen en align chunks met themagrenzen—vaak het opvragingsprecisie verbeteren voor meertopic documenten (blogs, designdocumenten, tickets, incidentrapporten).

Kosten. Je hebt mogelijk extra embeddings nodig tijdens chunking (zinsniveau of paragraafniveau embeddings) voor eindchunk embeddings. Dat kan de aantal embeddings verdubbelen of drievoudig maken tenzij je tussenliggende embeddings hergebruikt.

Praktische truc. “Semantisch bewuste verpakking”: bereken zinsembeddings één keer, groepeer zinnen in thema-coherente segmenten, en embed vervolgens elk eindsegment.

Hiërarchisch chunking (ouder/kinder)

Wat het is. Bouw een multigranulariteitsrepresentatie: grove ouderchunks (bijvoorbeeld sectie-omvang) met fijne kinderchunks (bijvoorbeeld paragraaf-omvang). LlamaIndex’s hiërarchische knoopparsing produceert standaard “grof-voor-fijn” hiërarchieën (bijvoorbeeld 2048 → 512 → 128 token schalen), en de AutoMergingRetriever kan kinderknopen in ouderknopen verenigen tijdens opvragen als genoeg gerelateerde kinderknopen zijn opgevraagd.

Waarom het helpt. Het vermijdt het kiezen tussen “kleine chunks voor herkennen” en “grote chunks voor samenhang” door beide op te slaan en te selecteren tijdens het queryen.

Kosten. Meer complexe ingestings- en opvragingslogica, plus potentieel meer opslag (omdat je meerdere granulariteiten opslaat).

Adaptief / LLM-gebaseerd chunking

Wat het is. Gebruik een LLM om te beslissen over chunkgrenzen (en optioneel samenvattingen of contextuele koppen genereren). Weaviate beschrijft LLM-gebaseerd chunking expliciet als het gebruik van een LLM om semantisch samenhangende chunks te maken, in plaats van op vastgestelde regels of embeddinggelijkenis te vertrouwen.

Wanneer het waard is. Hoogwaardige corpora waarin correctheid overheerst boven kosten (recht, naleving, ondersteuningsrunbooks), en waarin documenten ongeordend, heterogeen en slecht gesegmenteerd zijn.

Risico’s. Kosten, latency en niet-determinisme. Je zult caching, deterministische decoding en regressietests willen (zie evaluatiereeks).

Structuur- en elementgebaseerd chunking (documenten zijn niet platte tekst)

Wat het is. Parseer het document in elementen (titels, paragrafen, lijsten, tabellen, legenden) met behulp van een documentverstandlaag, en chunk dan met behulp van die elementen. Unstructured’s chunkingfuncties gebruiken expliciet metadata en documentelementen (die worden gegenereerd door partitionering) om chunks voor RAG te maken. Docling’s HierarchicalChunker maakt chunks per gedetecteerd documentelement en voegt structurele metadata toe zoals koppen/legenden.

Bewijs uit recente werkzaamheden. Een 2024 studie over SEC-aangiften stelt dat paragraaf-only chunking de documentstructuur negeert en voorstelt chunking op structurele elementen; het rapporteert verbeterde RAG-resultaten en minder chunks/vectoren dan structuur-onafhankelijke aanpakken.

Waarom het belangrijk is voor multimodale. Tabellen, figuren en legenden bevatten vaak de grondwaarheid. “Flattenen” ze in platte tekst kan signalen vernietigen die opvraging anders zou gebruiken.

Codebewuste chunking (AST/structuur)

Wat het is. Chunk code op syntactische eenheden (functies, klassen, modules), met optionele inbegrip van docstrings en opmerkingen.

Waarom het belangrijk is. Vaste grootte token splitsen neigen ertoe functies in tweeën te snijden en docstrings van implementaties te scheiden—slecht voor codezoekopdrachten en “leg deze functie uit” RAG-gebruiksgevallen.

Implementatieopties. Voor Python is de ingebouwde ast module vaak voldoende. Voor meertalige repositories zijn tree-sitter-gebaseerde chunkers gebruikelijk.

Evaluatie dimensies en hoe chunkingstrategieën te vergelijken

Chunking moet worden getest als een systeemonderdeel.

Opvragingskwaliteitsmetrieken

Gebruik standaard IR-metrieken voor de opvragingslaag:

  • Recall@k / Precision@k: Bevatte de top-k het goudbewijs?
  • MRR / nDCG: Stond het goudbewijs hoog in de ranglijst?

BEIR is een algemeen gebruikte heterogene benchmark voor IR-evaluatie over taken/domänen, en benadrukt trade-offs tussen sparza, dicht, late-interactie en rerankingbenaderingen.

Chunking beïnvloedt deze metrieken omdat het bepaalt wat telt als “een relevant opgevraagd item”.

Eind- tot-eind RAG-antwoordkwaliteitsmetrieken

Als je QA of assistenten bouwt, zijn opvragingsmetrieken nodig maar niet voldoende. Je hebt ook nodig:

  • Contextherkennen / precisie: of opgevraagde contexten relevante bewijs bevatten en ongegrond ruis vermijden.
  • Faithfulness: of het gegenereerde antwoord wordt ondersteund door de opgevraagde context.

RAGAS biedt concrete definities en implementaties voor “faithfulness” en andere RAG-gerichte metrieken.

Systeemkosten en prestatiedimensies

Chunking verandert deze hefbomen:

Latency (p50/p95). Query latency neemt meestal toe met meer vectoren en meer postverwerking. Je vectorindex is ook belangrijk: FAISS-indexsoorten verhouden zich tot zoektijd, kwaliteit, geheugen en training/addingtijd.[^faiss]

Embeddingkosten en doorvoer. OpenAI embeddings worden per token in rekening gebracht; de embeddings API heeft expliciete per-input en per-aanvraaglimieten.[^openai_embed_create] Voor offline ingestion verlaagt de Batch API de kosten en biedt hogere quota in ruil voor niet-realtime omloop.[^openai_batch]

Indexgrootte en geheugen. Ongeveer, opslaan van N float32 vectoren van dimensie d kost ~4 * N * d bytes alleen voor de ruwe vectoren (plus metadata + indexoverhead). Chunking beïnvloedt N. Embeddingdimensie beïnvloedt d, en OpenAI’s embeddings API stelt u in staat om de uitvoerdimensie te bepalen via het dimensions parameter.[^openai_embed_create]

LLM promptbudget. Grotere chunks en overlap vergroten prompttokens. Dit kan latency en kosten verhogen en “verloren in het midden” stijl falenmodes waarbij modellen minder aandacht geven aan sommige context. In de praktijk stel je vaak:

  1. kleine chunks opvragen,
  2. samenvoegen/dedupliceren,
  3. optioneel samenvatten,
  4. een compact bewijsset naar de LLM sturen.

Update/ingestingkosten. Kleine chunks toestaan gedeeltelijke herembedding maar vergroten administratie. Voor streaming ingestion voorkeur geven aan deterministische, incrementele chunking (vast of glijdend venster) en stabiele ID’s (document_id, offsetbereiken, hash) toevoegen.

Experimenteel ontwerp: een pragmatische benchmarkloop

Een herhaalbare chunkingbenchmark heeft meestal:

  • Een vaste corporussnapshot + vaste set queries met goudbewijs (of tenminste verwachte antwoordspannen).
  • Een vaste embeddingmodel en vectorindexconfiguratie.
  • Een “alleen opvragen” evaluatie (recall@k, nDCG) plus “RAG” evaluatie (faithfulness, antwoordrelevantie).
  • Kostenmeting: #chunks, ingebedde tokens, $/maand opslag, p95 querylatency, prompttokens.

Het Unstructured SEC-aangiften paper is een goed voorbeeld van het evalueren van chunkingstrategieën met zowel opvragingsgerichte metrieken als QA-nauwkeurigheidsmaatstaven.

Praktische richtlijnen, beslissingsmatrix en aanbevolen standaarden

Aanbevolen standaarden die verrassend goed werken

Als je een robuuste “dag 1”-strategie nodig hebt voor algemene documentatie QA:

  1. Licht analyseren: behoud koppen en basismetadata (bron, sectietitel, URL/naam, tijdstip).
  2. Chunk met een recursieve scheider splitter (paragraaf → zin → woord), met matige overlap.
  3. Embed met een sterke algemene embeddingmodel.
  4. Index met metadata (doc id, sectie, ACLs) en deduplicate tijdens het opvragen.
  5. Voeg reranking of hiërarchische vereniging alleen toe als je evaluatie een gap toont.

Dit correspondeert met hoe veelvoorkomende RAG-frameworks chunkoverlap en structuurrespecterende splitsing beschrijven.

Welke chunkingmethode te gebruiken - Beslissingsmatrix

Welke chunkingmethode te gebruiken - Diagram

Gebruiksaanwijzing Aanbevolen chunkingstandaard Belangrijke parameters om af te stemmen Gewone mislukking Upgradepad
Korte vorm QA over documenten (FAQs, interne wiki) Recursief/scheider chunking + overlap chunk_size, overlap, top-k Mist kruiszinnenbewijs aan grens Voeg semantisch chunking of reranker toe
Langere vorm QA (beleidsregels, standaarden, handleidingen) Hiërarchisch chunking + verenigingsopvraging ouder/kinder grootte, verenigingsschakel Opgaat kleine fragmenten; LLM mist volledige context Automatisch verenigen/hiërarchische opvragen
Samenvatten (per document / per sectie) Structuurbewuste chunks (secties) sectie detectie, max tokens Samenvattingen missen kruissectielinks Hiërarchische samenvatten + sectiegrafiek
Codezoekopdrachten & “leg deze functie uit” AST/functieniveau chunks include docstring/opmerkingen, max tokens Functie gesplitst; verliest signature/gebruik Repo-bewuste hiërarchie (module→klasse→functie)
Multimodale PDFs (tabellen/figuren) Elementgebaseerd chunking (titel/tabel/legenda bewust) tabelserialisatie, legenda samenvoegen Tabelinhoud verloren of verkeerd Gebruik Docling/Unstructured + gestructureerde serializers
Streaming ingestion (logboeken, chats, tickets) Glijdend venster of vaste grootte tokens venster, stap, dedup Over-opvragen van redundante vensters Voeg semantische grensdetectie toe op batches

chunking - Kwalitatieve prestatievergelijking

Behandel dit als “verwachte richting van verandering” (valideer met jouw eigen data).

Strategie Potentieel opvragingsnauwkeurigheid Samenhang van opgevraagde context Ingestingscomplexiteit Aantal vectoren / indexgrootte Embeddingkosten Querylatencyimpact Beste voor
Vaste grootte (geen overlap) Gemiddeld Laag Laag Gemiddeld Laag Gemiddeld snelle prototypen, homogene tekst
Vaste grootte + overlap Gemiddeld–Hoog Gemiddeld Laag Hoog Gemiddeld–Hoog Gemiddeld–Hoog QA waarbij grensverlies schade doet
Zin/paragraafverpakking Hoog (proza) Hoog Gemiddeld Gemiddeld Gemiddeld Gemiddeld documenten, artikelen, nette proza
Glijdend venster Hoog herkennen Gemiddeld Gemiddeld Zeer hoog Zeer hoog Hoog transcripten, logboeken, chat
Recursief/scheider Hoog Hoog Gemiddeld Gemiddeld Gemiddeld Gemiddeld “standaard” documenten RAG
Semantisch chunking Hoog–Zeer hoog Hoog Hoog Gemiddeld Hoog Gemiddeld meertopic pagina’s, narratief tekst
Hiërarchisch (ouder/kinder) Zeer hoog Zeer hoog Hoog Hoog Hoog Gemiddeld lange handleidingen / standaarden
LLM-gebaseerd/adapter Zeer hoog Zeer hoog Zeer hoog Gemiddeld Zeer hoog Gemiddeld–Hoog hoogwaardige corpora
Element-/structuurgebaseerd Hoog–Zeer hoog Hoog Hoog Laag–Gemiddeld Gemiddeld Gemiddeld PDFs, rapporten, tabellen, gemengde lay-outs
Codebewust (AST) Hoog (code) Hoog Gemiddeld Gemiddeld Gemiddeld Gemiddeld codezoekopdrachten, repo-assistenten

DevOps en hardware notities (vaak over het hoofd gezien)

Chunkingkeuzes beïnvloeden hoeveel infrastructuur je nodig hebt:

  • Kleine chunks → meer vectoren → grotere indexen en meer RAM/disk. Voor zelfgehoste FAISS kan dit sharding of disk-gebaseerde indexen dwingen.
  • Als je lokaal embed, wordt embeddingdoorvoer een GPU-schedulingsprobleem; als je via API embed, wordt tokenvolume een FinOps probleem (Batch API is je vriend voor backfills).
  • Sommige engines (FAISS) bieden GPU-geaccelereerde zoekopdrachten; dit kan kosten verplaatsen van RAM-begrensde CPU naar GPU-geheugen en PCIe doorvoer.
  • Structuurbewuste parsing (PDF lay-out, OCR, tabeluitvoer) is vaak CPU-begrens en kan embeddingkosten overschrijden voor gescande documenten; budgetteer het apart.

Chunking - Python-voorbeelden

Alle voorbeelden zijn ontworpen om leesbaar en uitvoerbaar te zijn. Als u een API-sleutel of een draaiende database nodig heeft, is dat duidelijk uit de code.

Gedeelde hulpmiddelen: token tellen en stabiele chunk-IDs

from __future__ import annotations

import hashlib
from dataclasses import dataclass
from typing import Any, Iterable, Optional

from transformers import AutoTokenizer  # pip install transformers

@dataclass(frozen=True)
class Chunk:
    text: str
    meta: dict[str, Any]

def sha1_id(*parts: str) -> str:
    h = hashlib.sha1()
    for p in parts:
        h.update(p.encode("utf-8"))
        h.update(b"\x1e")
    return h.hexdigest()

# Gebruik een tokenizer die ongeveer overeenkomt met de tokenisatie van uw LLM/embedding.
TOKENIZER = AutoTokenizer.from_pretrained("bert-base-uncased")

def token_len(text: str) -> int:
    return len(TOKENIZER.encode(text, add_special_tokens=False))

Vaste grootte token chunking

def chunk_fixed_tokens(
    text: str,
    *,
    chunk_size: int = 512,
) -> list[Chunk]:
    token_ids = TOKENIZER.encode(text, add_special_tokens=False)
    out: list[Chunk] = []

    for i in range(0, len(token_ids), chunk_size):
        window = token_ids[i : i + chunk_size]
        chunk_text = TOKENIZER.decode(window)
        out.append(
            Chunk(
                text=chunk_text,
                meta={"strategy": "fixed_tokens", "start_token": i, "end_token": i + len(window)},
            )
        )
    return out

Vaste grootte met overlap + schuivende venster

def chunk_sliding_window(
    text: str,
    *,
    window_tokens: int = 512,
    stride_tokens: int = 384,  # kleinere stap = meer overlap
) -> list[Chunk]:
    assert 1 <= stride_tokens <= window_tokens, "stap moet binnen (0, venster]"
    token_ids = TOKENIZER.encode(text, add_special_tokens=False)
    out: list[Chunk] = []

    start = 0
    while start < len(token_ids):
        end = min(start + window_tokens, len(token_ids))
        window = token_ids[start:end]
        out.append(
            Chunk(
                text=TOKENIZER.decode(window),
                meta={"strategy": "sliding_window", "start_token": start, "end_token": end},
            )
        )
        if end == len(token_ids):
            break
        start += stride_tokens

    return out

Zin gebaseerd chunking (NLTK) met token-budget verpakking

# pip install nltk
import nltk
from nltk.tokenize import sent_tokenize

nltk.download("punkt", quiet=True)

def chunk_by_sentences_nltk(
    text: str,
    *,
    max_tokens: int = 512,
    overlap_sentences: int = 1,
) -> list[Chunk]:
    sents = [s.strip() for s in sent_tokenize(text) if s.strip()]
    out: list[Chunk] = []

    buf: list[str] = []
    buf_tokens = 0

    def flush():
        nonlocal buf, buf_tokens
        if not buf:
            return
        chunk_text = " ".join(buf).strip()
        out.append(
            Chunk(
                text=chunk_text,
                meta={"strategy": "sentences_nltk", "sent_count": len(buf)},
            )
        )
        # Overlap door laatste N zinnen te behouden
        if overlap_sentences > 0:
            buf = buf[-overlap_sentences:]
            buf_tokens = token_len(" ".join(buf))
        else:
            buf, buf_tokens = [], 0

    for s in sents:
        s_tokens = token_len(s)
        # Als een enkele zin het budget overschrijdt, valt terug op vaste grootte chunking voor dat fragment
        if s_tokens > max_tokens:
            flush()
            out.extend(chunk_fixed_tokens(s, chunk_size=max_tokens))
            continue

        if buf_tokens + s_tokens > max_tokens and buf:
            flush()

        buf.append(s)
        buf_tokens += s_tokens

    flush()
    return out

Zin gebaseerd chunking (spaCy) wanneer u regelgebaseerde of modelgebaseerde SBD wilt

# pip install spacy
# python -m spacy download en_core_web_sm
import spacy

def chunk_by_sentences_spacy(
    text: str,
    *,
    max_tokens: int = 512,
) -> list[Chunk]:
    # Voor lichte regelgebaseerde splitsingen (geen afhankelijkheidsanalyse), gebruik sentencizer.
    nlp = spacy.blank("en")
    nlp.add_pipe("sentencizer")  # regelgebaseerde zinsgrensdetectie
    doc = nlp(text)

    sents = [sent.text.strip() for sent in doc.sents if sent.text.strip()]
    return chunk_by_sentences_nltk(" ".join(sents), max_tokens=max_tokens, overlap_sentences=1)

Rekursief scheiding chunking (LangChain)

# pip install langchain-text-splitters
from langchain_text_splitters import RecursiveCharacterTextSplitter

def chunk_recursive_langchain(
    text: str,
    *,
    chunk_size: int = 1200,
    chunk_overlap: int = 150,
) -> list[Chunk]:
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        length_function=token_len,  # token-aware budgeting
        separators=["\n\n", "\n", ". ", " ", ""],  # aanpassen aan uw inhoud (bijv. code)
    )
    pieces = splitter.split_text(text)

    return [
        Chunk(text=p, meta={"strategy": "recursive_langchain", "chunk_size": chunk_size, "overlap": chunk_overlap})
        for p in pieces
    ]

Semantisch chunking met embedding gelijkenis (OpenAI embeddings)

Deze aanpak berekent embeddings voor kandidaat-eenheden (zinnen/paragrafen), en vindt dan semantische “brekingspunten”.

# pip install openai numpy
import os
import numpy as np
from openai import OpenAI

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", "YOUR_OPENAI_API_KEY"))

def embed_texts_openai(texts: list[str], *, model: str = "text-embedding-3-small") -> np.ndarray:
    # OPMERKING: voor grote batches, respecteer aanvraag tokenlimieten en batchgrootte limieten.
    resp = client.embeddings.create(model=model, input=texts)
    embs = np.array([d.embedding for d in resp.data], dtype=np.float32)
    return embs

def cosine_sim(a: np.ndarray, b: np.ndarray) -> float:
    denom = (np.linalg.norm(a) * np.linalg.norm(b)) + 1e-8
    return float(np.dot(a, b) / denom)

def chunk_semantic(
    text: str,
    *,
    max_tokens: int = 800,
    breakpoint_threshold: float = 0.70,
) -> list[Chunk]:
    # 1) Start vanaf zin kandidaten
    sents = [s.strip() for s in sent_tokenize(text) if s.strip()]
    if len(sents) <= 1:
        return [Chunk(text=text, meta={"strategy": "semantic", "note": "no_split"})]

    # 2) Embed elke zin
    embs = embed_texts_openai(sents)

    # 3) Bereken gelijkenisverlagingen
    sims = [cosine_sim(embs[i], embs[i + 1]) for i in range(len(sents) - 1)]

    # 4) Creëer segmenten aan brekingspunten
    out: list[Chunk] = []
    buf: list[str] = []
    buf_tokens = 0

    for i, s in enumerate(sents):
        # Voeg zin toe
        s_tok = token_len(s)
        if buf and buf_tokens + s_tok > max_tokens:
            out.append(Chunk(text=" ".join(buf), meta={"strategy": "semantic", "reason": "max_tokens"}))
            buf, buf_tokens = [], 0
        buf.append(s)
        buf_tokens += s_tok

        # Bepaal brekingspunt na zin i (op basis van gelijkenis met volgende)
        if i < len(sims) and sims[i] < breakpoint_threshold:
            out.append(
                Chunk(
                    text=" ".join(buf),
                    meta={"strategy": "semantic", "reason": "sim_drop", "sim_to_next": sims[i]},
                )
            )
            buf, buf_tokens = [], 0

    if buf:
        out.append(Chunk(text=" ".join(buf), meta={"strategy": "semantic", "reason": "final"}))

    return out

Hiërarchisch chunking + samenvoegen van ophalen (LlamaIndex)

# pip install llama-index llama-index-llms-openai
from llama_index.core import Document, StorageContext, VectorStoreIndex
from llama_index.core.node_parser import HierarchicalNodeParser, get_leaf_nodes
from llama_index.core.retrievers import AutoMergingRetriever
from llama_index.core.storage.docstore import SimpleDocumentStore

def build_hierarchical_index(text: str):
    docs = [Document(text=text)]

    node_parser = HierarchicalNodeParser.from_defaults()  # standaardwaarden zijn van grof naar fijn
    nodes = node_parser.get_nodes_from_documents(docs)

    docstore = SimpleDocumentStore()
    docstore.add_documents(nodes)
    storage_context = StorageContext.from_defaults(docstore=docstore)

    leaf_nodes = get_leaf_nodes(nodes)
    base_index = VectorStoreIndex(leaf_nodes, storage_context=storage_context)

    base_retriever = base_index.as_retriever(similarity_top_k=6)
    retriever = AutoMergingRetriever(base_retriever, storage_context, verbose=True)
    return retriever

Element gebaseerd chunking voor PDFs (Docling)

# pip install docling
# OPMERKING: kwaliteit van PDF-parsen hangt af van uw omgeving (lettertypen, OCR, enz.)
from docling.document_converter import DocumentConverter
from docling.transforms.chunker.hierarchical_chunker import HierarchicalChunker

def chunk_pdf_docling(pdf_path: str) -> list[Chunk]:
    converter = DocumentConverter()
    doc = converter.convert(pdf_path).document  # DoclingDocument
    chunker = HierarchicalChunker()
    doc_chunks = list(chunker.chunk(doc))

    out: list[Chunk] = []
    for c in doc_chunks:
        # c.text bevat serialiseerde chunk inhoud; c.meta draagt structuurinformatie
        out.append(Chunk(text=c.text, meta={"strategy": "docling_hierarchical", **dict(c.meta)}))
    return out

Code-bewuste chunking voor Python (AST)

import ast

def chunk_python_by_ast(code: str, *, filepath: str = "<memory>") -> list[Chunk]:
    tree = ast.parse(code)
    out: list[Chunk] = []

    for node in tree.body:
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            start = getattr(node, "lineno", None)
            end = getattr(node, "end_lineno", None)
            if start is None or end is None:
                continue

            lines = code.splitlines()
            snippet = "\n".join(lines[start - 1 : end])

            kind = "class" if isinstance(node, ast.ClassDef) else "function"
            name = getattr(node, "name", "<anon>")
            out.append(
                Chunk(
                    text=snippet,
                    meta={
                        "strategy": "python_ast",
                        "kind": kind,
                        "name": name,
                        "filepath": filepath,
                        "start_line": start,
                        "end_line": end,
                    },
                )
            )

    return out

Indexvoorbeelden

FAISS (lokale) — minimale, snelle basislijn

# pip install faiss-cpu numpy
import numpy as np
import faiss

def build_faiss_index(vectors: np.ndarray) -> faiss.Index:
    # vectors: vorm (N, d), type float32
    d = vectors.shape[1]
    index = faiss.IndexFlatIP(d)  # inwendig product; cosinus gelijkenis als vectors genormaliseerd zijn
    faiss.normalize_L2(vectors)
    index.add(vectors)
    return index

def faiss_search(index: faiss.Index, query_vec: np.ndarray, k: int = 5):
    q = query_vec.astype(np.float32).reshape(1, -1)
    faiss.normalize_L2(q)
    scores, ids = index.search(q, k)
    return ids[0].tolist(), scores[0].tolist()

Chroma (lokale) — eenvoudige persistentie voor RAG prototyping

# pip install chromadb
import chromadb

def build_chroma_collection(chunks: list[Chunk], embeddings: np.ndarray, *, path: str = "./chroma_store"):
    client = chromadb.PersistentClient(path=path)
    col = client.get_or_create_collection(name="docs")

    ids = [sha1_id(c.meta.get("strategy", "chunk"), str(i), c.text[:50]) for i, c in enumerate(chunks)]
    col.upsert(
        ids=ids,
        documents=[c.text for c in chunks],
        metadatas=[c.meta for c in chunks],
        embeddings=embeddings.tolist(),
    )
    return col

Weaviate (zelfgehost / cloud) — eigen vectors meenemen

# pip install weaviate-client
import weaviate
from weaviate.classes.config import Configure

def weaviate_upsert_self_provided(
    chunks: list[Chunk],
    embeddings: np.ndarray,
):
    client = weaviate.connect_to_local()  # of connect_to_weaviate_cloud(...)
    try:
        collection = client.collections.create(
            name="Chunk",
            vector_config=Configure.Vectors.self_provided(),  # u levert eigen vectors
        )

        with collection.batch.dynamic() as batch:
            for c, v in zip(chunks, embeddings):
                batch.add_object(
                    properties={"text": c.text, **{f"m_{k}": str(vv) for k, vv in c.meta.items()}},
                    vector=v.tolist(),
                )

        # Zoek op near-vector
        q = embeddings[0]
        res = collection.query.near_vector(near_vector=q.tolist(), limit=5)
        for obj in res.objects:
            print(obj.properties.get("text")[:200])
    finally:
        client.close()

Sommige brondocumenten

  • Patrick Lewis et al., “Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks”, NeurIPS 2020; arXiv:2005.11401.
  • Vladimir Karpukhin et al., “Dense Passage Retrieval for Open-Domain Question Answering”, EMNLP 2020; arXiv:2004.04906.
  • OpenAI API Referentie: Embedding aanmaken (/v1/embeddings) — tokenlimieten (8192 per invoer; 300k totaal per aanvraag) en dimensions parameter.
  • OpenAI Batch API gids + OpenAI API prijspagina (Batch bespaart ongeveer 50% met 24-uurs doorlooptijd).
  • LangChain documentatie: RecursiveCharacterTextSplitter en splitters integratiegids (chunkgrootte/overlap, rekursieve scheiding hiërarchie).
  • LlamaIndex documentatie: HierarchicalNodeParser en AutoMergingRetriever (grof naar fijn knopen; ophalen tijdens ophalen).
  • Weaviate blog: “Chunking Strategies to Improve LLM RAG Pipeline Performance” (LLM-gebaseerde chunking beschrijving en trade-offs).
  • Docling documentatie: HierarchicalChunker maakt chunks van documentelementstructuur en voegt hoofdingen/legenda metadata toe.
  • Jimeno Yepes et al., “Financial Report Chunking for Effective Retrieval Augmented Generation” (arXiv:2402.05131v3, 2024).
  • Marti A. Hearst, “TextTiling: Segmenting Text into Multi-Paragraph Subtopic Passages”, Computational Linguistics, 1997 (ACL Anthology: J97-1003).
  • FAISS documentatie / GitHub repository: trade-offs tussen zoektijd, kwaliteit, geheugen; optionele GPU-ondersteuning.
  • Nandan Thakur et al., “BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models”, NeurIPS 2021; arXiv:2104.08663.
  • RAGAS documentatie: “Faithfulness” metriek en gerelateerde RAG evaluatiemetrieken.
  • NLTK documentatie: nltk.tokenize.sent_tokenize - Punkt-based aanbevolen zintokkizer.
  • spaCy API documentatie: Sentencizer - regelgebaseerde zinsgrensdetectie zonder afhankelijkheidsanalyse.