Betere Vector Search voor Lange Documenten: Chunking in Manticore Search
Stel dat je een zoekfunctie bouwt voor de interne documentatie van je team — handleidingen, runbooks, postmortems. Je hebt een tabel met automatische embeddings: je voert tekst in, Manticore voert het model uit en vult de vector-kolom voor je in. Je uploadt een document van 4.000 woorden. De invoer slaagt, de zoekopdracht werkt en alles lijkt in orde.
Er is echter één probleem: het model dat je hebt gekozen heeft een input-venster van 512 tokens, terwijl dat document ongeveer 5.000 tokens lang is. Het model heeft de eerste 380 woorden gelezen en de overige 3.600 weggegooid. Alles in het document na dat punt kan nooit worden teruggevonden, en niets heeft je hierover geïnformeerd. Bovendien is de embedding mogelijk niet representatief voor het document als geheel.
Tot nu toe moest je het document handmatig in verschillende stukken splitsen, voor elk stuk embeddings maken en vervolgens uitzoeken hoe je de resultaten combineerde als je op documentniveau wilde zoeken in plaats van op chunk-niveau. Manticore regelt dit nu in de tabeldefinitie: voeg chunk_strategy toe aan de vector-kolom in CREATE TABLE. Manticore splitst elk document in chunks, maakt van elke chunk een embedding en doorzoekt ze allemaal.
DROP TABLE IF EXISTS docs;
CREATE TABLE docs (
title text,
content text,
chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
model_name='Xenova/all-MiniLM-L6-v2' from='title,content'
chunk_strategy='sentence' max_tokens='256' overlap_tokens='32'
);
Dit is de volledige functionaliteit. Geen ingest-pipeline, geen splitter-library, geen tweede tabel voor chunks en geen GROUP BY om chunk-hits terug te voeren naar documenten.
Samenvatting (TL;DR)
- Vijf strategieën:
truncate(de oude standaard),mean,fixed,recursiveensentence. Deze stel je in viachunk_strategyop een model-gestuurde vector-kolom. - Kolomtypes:
truncateenmeanproduceren één vector per document en werken op eenfloatvectorkolom.fixed,recursiveensentenceproduceren meerdere vectoren en vereisen daarom eenfloatvector_arraykolom. - Zoekresultaten: Een document blijft één zoekresultaat. Chunks concurreren individueel, en Manticore retourneert het document één keer, waarbij
knn_dist()de afstand tot de dichtstbijzijnde chunk rapporteert. De parameterktelt documenten, niet chunks. - Instellingen:
maxtokens(chunk-grootte),overlaptokens(gedeelde tokens tussen buren) enmax_chunks(plafond per document). - Prestaties: Gemeten op de Manticore-handleiding (189 pagina's, ~298k woorden): voor content die buiten het venster van het model viel, steeg de recall@5 van 55,1% → 83,3% en de MRR van 0,44 → 0,70, tegen ongeveer 2,5× meer RAM en 4× langere ingest-tijd.
- Queries: Zoekopdrachten worden nooit in chunks gesplitst. Een query is kort genoeg om in zijn geheel te embedden; alleen opgeslagen documenten worden gesplitst.
Het probleem in de praktijk
Stel dat je vier documenten hebt:
- Backup and restore runbook — ongeveer 700 woorden (900 tokens). Bevat secties over back-upschema's, retentie, hersteltests, inloggegevens en capaciteitsplanning. De laatste sectie legt uit hoe je het TLS-certificaat roteert dat door de replicatiepoort wordt gebruikt.
- Monitoring and alerting guide — niet relevant.
- Getting started with the CLI — niet relevant.
- TLS and certificates for the HTTP API — een korte pagina die volledig over certificaten gaat, maar nooit over rotatie of replicatie spreekt.
Je kunt de tabel als volgt aanmaken met drie vector-kolommen (één per strategie) om ze te vergelijken:
DROP TABLE IF EXISTS docs;
CREATE TABLE docs (
title text,
body text,
v_truncate float_vector knn_type='hnsw' hnsw_similarity='cosine'
model_name='Xenova/all-MiniLM-L6-v2' from='title,body',
v_mean float_vector knn_type='hnsw' hnsw_similarity='cosine'
model_name='Xenova/all-MiniLM-L6-v2' from='title,body' chunk_strategy='mean',
v_sentence float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
model_name='Xenova/all-MiniLM-L6-v2' from='title,body'
chunk_strategy='sentence' max_tokens='128' overlap_tokens='32'
);
Wanneer je een vraag stelt over het roteren van het TLS-certificaat voor replicatie, zijn dit de resultaten:
| Strategie | 1e resultaat | 2e resultaat |
|---|---|---|
| truncate (default) | TLS and certificates for the HTTP API — 0.762 | Backup and restore runbook — 0.936 |
| mean | Backup and restore runbook — 0.656 | TLS and certificates for the HTTP API — 0.762 |
| sentence (128 tok, 32 ov) | Backup and restore runbook — 0.254 | TLS and certificates for the HTTP API — 0.700 |
Bij truncate verliest het document met het juiste antwoord van een "decoy" (afleider) die simpelweg over certificaten gaat. De vector van het runbook werd namelijk alleen gebouwd op basis van de beginpagina's over back-upschema's, omdat het model niet verder kon lezen.
Met sentence chunking wordt het runbook opgeslagen als negen vectoren in plaats van één. Een van die negen is de paragraaf over certificaatrotatie. Deze matcht de query bijna exact, waardoor het document met een ruime marge wint.
Overzicht van de chunking-strategieën
| Strategie | Vectoren per doc | Kolomtype | Werking |
|---|---|---|---|
| truncate | 1 | float_vector | Embedt zoveel als in het venster van het model past, gooit de rest weg. De standaardinstelling. |
| mean | 1 | float_vector | Splitst het hele document, embedt elk stuk en middelt deze tot één vector. |
| fixed | N | floatvectorarray | Vaste vensters van max_tokens tokens. |
| recursive | N | floatvectorarray | Splitst op basis van een hiërarchie: paragraaf → regel → zin → spatie, om elk stuk binnen max_tokens te houden. |
| sentence | N | floatvectorarray | Gebruikt zin-grenzen (Unicode UAX #29), gepakt tot max_tokens. |
Het cruciale verschil is wat een "match" betekent:
- Eén vector per document: De zoekopdracht vraagt: "Is dit document als geheel vergelijkbaar met de query?" Een relevante paragraaf wordt verwaterd door de rest van de tekst.
- Eén vector per chunk: De zoekopdracht vraagt: "Bevat dit document iets dat vergelijkbaar is?" Elke chunk concurreert op eigen kracht, en Manticore retourneert het document gebaseerd op de beste chunk.
truncate — voor korte documenten
Gebruik dit wanneer je tekst echt in het venster van het model past: producttitels, korte beschrijvingen, tags, chatberichten, logregels of commit-onderwerpen.
- Capaciteit:
all-MiniLM-L6-v2hanteert 512 tokens (ca. 380 Engelse woorden).text-embedding-3-smallhanteert 8.192 tokens.
mean — één vector, maar het hele document
Manticore splitst het document, embedt elke chunk en middelt de vectoren tot één genormaliseerde vector. De opslag- en zoekkosten zijn identiek aan truncate.
- Wanneer gebruiken: Wanneer je wilt dat het hele document meetelt maar geen extra RAM hebt voor meer vectoren, of wanneer de kolom een
float_vectormoet blijven (bijv. bij eenALTER TABLE). - Wanneer niet gebruiken: Als een document verschillende ongerelateerde onderwerpen behandelt. Het middelen van een vrijwaringsclausule met betalingsvoorwaarden resulteert in een vector die tussen beide in ligt en op geen van beide goed matcht.
fixed — voorspelbaar en eenvoudig
Knipt elke max_tokens tokens, ongeacht de inhoud.
- Wanneer gebruiken: Tekst zonder betrouwbare structuur: OCR-output, gescrapte HTML zonder paragrafen, machine-transcripts zonder interpunctie of log-dumps.
recursive — de beste standaard voor proza
Vergelijkbaar met fixed, maar elke knip wordt teruggeschoven naar de dichtstbijzijnde natuurlijke grens (blanke regel, regelafbreking, zin-einde, spatie).
- Wanneer gebruiken: Markdown en HTML-documentatie, wiki's, blogposts en gestructureerde rapporten.
sentence — voor volledige gedachten
Detecteert zin-grenzen en pakt volledige zinnen tot het token-budget is bereikt. Een chunk begint of eindigt nooit midden in een zin.
- Wanneer gebruiken: Support-tickets, e-mailthreads, juridische teksten, medische abstracts of alles waarbij een fragment van een zin de betekenis zou kunnen veranderen. Ideaal als chunks later naar een LLM worden gevoerd.
De drie instellingen (knobs)
chunk_strategy: Keuze uittruncate,mean,fixed,recursiveofsentence.max_tokens: De grootte van de chunk. 0 (standaard) gebruikt de limiet van het model. Kleinere chunks zorgen voor scherpere matches maar meer vectoren. Voor Engelse proza is 128–512 meestal voldoende.overlaptokens: Tokens die gedeeld worden tussen opeenvolgende chunks. Dit voorkomt dat een zin die precies op de grens valt, onbruikbaar wordt. Meestal wordt 10–20% vanmaxtokensingesteld.max_chunks: Een plafond voor het aantal vectoren per document. Dit dient als beveiliging tegen extreem grote documenten (bijv. een PDF van 400 pagina's) die anders duizenden HNSW-nodes zouden creëren.
Hoe de zoekopdracht werkt
Je query verandert niet. Er is geen aparte chunk-tabel of GROUP BY nodig.
SELECT id, title, knn_dist() FROM notes
WHERE knn(chunks, 3, 'how do I replace an expiring certificate on every node');
De regels zijn:
- Een document matcht als een van zijn vectoren dicht bij de query-vector ligt.
- Manticore retourneert elk match-document precies één keer.
knn_dist()is de afstand tot de dichtstbijzijnde chunk.ktelt documenten, niet vectoren.
Prestaties: Testen op de Manticore-handleiding
Er is een test uitgevoerd op de Engelse Manticore-handleiding (189 pagina's, ~298.000 woorden) met twee soorten queries:
- Deep-content queries: Vragen over inhoud die na de eerste 1.200 tekens van een pagina verschijnt.
- Head-content queries: Vragen over de beginsecties (controle-groep).
Deep content — waar chunking voor is
| Strategie | Vectoren | Ingest | Index RAM | hit@1 | hit@5 | hit@10 | MRR |
|---|---|---|---|---|---|---|---|
| truncate | 189 | 21 s | 4.2 MB | 33.7% | 55.1% | 63.2% | 0.44 |
| mean | 189 | 72 s | 4.2 MB | 43.9% | 65.2% | 74.7% | 0.54 |
| fixed | 3,430 | 73 s | 9.5 MB | 56.3% | 81.1% | 86.2% | 0.68 |
| recursive | 4,664 | 86 s | 11.7 MB | 58.7% | 83.3% | 89.5% | 0.70 |
| sentence | 4,041 | 79 s | 10.6 MB | 55.4% | 83.5% | 89.0% | 0.68 |
Chunking transformeert de zoekfunctie van een "muntworp" naar een werkend systeem. De recall@5 stijgt van 55,1% naar 83,3%.
Head content — de controle-groep
Voor content die het model al kon zien, is truncate het meest precies op rank 1 (65,9% vs 58,0% voor recursive), omdat een vector van het hele document de algemene context van de pagina beter vangt. Echter, op rank 5 is het verschil verdwenen (beide 86,4%).
Kostenanalyse
- Index RAM: Steeg van 4,2 MB → 11,7 MB (ca. 2,5×) voor ongeveer 25× zoveel vectoren.
- Data load: Steeg van 21 s → 86 s. Dit komt door de embedding-kosten van het hele corpus in plaats van alleen het begin.
- Query responstijd: Zeer gering verschil (6,3 ms → 8,5 ms bij p50), omdat HNSW efficiënt omgaat met het aantal vectoren.
Aanbevelingen voor het kiezen van een strategie
| Jouw data | Begin met... |
|---|---|
| Titels, namen, korte beschrijvingen, tags, logregels | truncate |
| Lang maar met één onderwerp; of RAM is de harde limiet | mean |
| Documentatie, wiki's, kennisbanken, artikelen, README's | recursive, max_tokens 128–256 |
| Support-tickets, e-mail, transcripts, juridische teksten | sentence, max_tokens 128–256 |
| OCR, gescrapte HTML, ongestructureerde dumps | fixed, max_tokens 256 + overlap |
| Chunks worden doorgegeven aan een LLM als context | sentence, max_tokens 384–512 |
De optimale chunk-grootte bepalen
Uit tests met de recursive strategie blijkt:
- Kleinere chunks winnen consistent: Het gaan van 512 naar 128 tokens verhoogt de recall@5 van 80,2% → 85,2% en verbetert de ranking (MRR) aanzienlijk.
- Overlap heeft weinig effect op kwaliteit: Bij gestructureerd proza voegt overlap nauwelijks kwaliteit toe, maar verhoogt het wel de kosten (meer vectoren en RAM).
Advies: Begin bij 128–256 tokens zonder overlap. Voeg overlap alleen toe als je meet dat feiten routinematig over de grens van een chunk vallen.
Praktijkvoorbeelden (Recipes)
Documentatie en Helpcentra
Lange Markdown-pagina's waarbij gebruikers in eigen woorden zoeken.
CREATE TABLE docs (
url string,
title text,
body text,
chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
model_name='Xenova/all-MiniLM-L6-v2' from='title,body'
chunk_strategy='recursive' max_tokens='192'
);
Support-tickets en E-mailthreads
Threads hebben geen natuurlijke lengtelimiet, dus gebruik max_chunks.
CREATE TABLE tickets (
ticket_id bigint,
customer string,
status string,
thread text,
chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
model_name='Xenova/all-MiniLM-L6-v2' from='thread'
chunk_strategy='sentence' max_tokens='256' overlap_tokens='32' max_chunks='64'
);
Contracten en Beleidsdocumenten
Hier is retrieval op clausule-niveau essentieel.
chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
model_name='Xenova/all-MiniLM-L6-v2' from='body'
chunk_strategy='sentence' max_tokens='128' overlap_tokens='32'
Productcatalogus met lange beschrijvingen
Eén product is meestal één onderwerp.
embedding float_vector knn_type='hnsw' hnsw_similarity='cosine'
model_name='Xenova/all-MiniLM-L6-v2' from='name,description'
chunk_strategy='mean'
RAG: Retrieval voor een LLM
Chunks moeten leesbaar zijn als proza voor de prompt.
chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
model_name='Xenova/all-MiniLM-L6-v2' from='title,body'
chunk_strategy='sentence' max_tokens='512' overlap_tokens='64'
Vergelijking met andere zoekmachines
| Engine | Embeds in-engine | Chunks in-engine | Strategieën | Eén resultaat per doc |
|---|---|---|---|---|
| Manticore Search | Ja | Ja | truncate, mean, fixed, recursive, sentence | Ja, native |
| Elasticsearch | Ja | Ja | sentence, word, recursive (9.1+), none | Ja (via semantic_text) |
| OpenSearch | Ja | Ja | fixedtoken, fixedchar, delimiter | Nee (vereist nested field) |
| Vespa | Ja | Ja | fixed-length, sentence, custom | Ja |
| Azure AI Search | Ja | Ja | pages (chars), sentences | Nee (resultaat per chunk) |
| PostgreSQL+pgai | Ja | Ja | character, recursive character | Nee (aparte tabel + join) |
| Milvus / Qdrant | Ja | Nee | App-side | — |
Manticore minimaliseert de complexiteit: de strategie is een optie op de kolom, de chunks zijn de waarde van de kolom, en de zoekopdracht retourneert documenten.
Wat chunking niet oplost
- Verlies van context: Een chunk weet niet waar hij vandaan komt. Een fragment als "voer één node tegelijk uit" is onbruikbaar zonder te weten welk product wordt bedoeld.
- Oplossing: Voeg context toe aan de tekst voordat je deze invoert (bijv. herhaal de titel in elke sectie).
- Statische grenzen: Manticore splitst eerst en embedt daarna. "Late chunking" (eerst embedden, dan poolen) is een alternatief voor documenten die zwaar leunen op context tussen paragrafen, maar wordt momenteel niet ondersteund.
Beperkingen en valkuilen
max_chunksverwijdert tekst: Overschrijding wordt samengevoegd in de laatste chunk en vervolgens afgekapt tot het model-venster.- Remote modellen (OpenAI, etc.): Gebruiken bytes in plaats van tokens. Manticore schat dit op 3 bytes per token. In de praktijk worden chunks iets kleiner dan gevraagd.
ALTER TABLE: Multi-vector kolommen (floatvectorarray) kunnen niet worden toegevoegd viaALTER. Je moet de tabel opnieuw aanmaken.- Alleen voor auto-embeddings: Handmatig ingevoerde vectoren worden niet door Manticore gesplitst.
- Queries worden niet gesplitst: Een query wordt altijd als één enkele vector behandeld.
Zelf proberen
De kortste weg naar een werkende chunked semantic search:
DROP TABLE IF EXISTS docs;
CREATE TABLE docs (
title text,
content text,
chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
model_name='Xenova/all-MiniLM-L6-v2' from='title,content'
chunk_strategy='recursive' max_tokens='256' overlap_tokens='32'
);
INSERT INTO docs (id, title, content) VALUES
(1, 'Backup and restore runbook', 'Nightly backups run at 02:00 UTC ... ');
SELECT id, title, knn_dist() FROM docs
WHERE knn(chunks, 5, 'how do I rotate the replication certificate');
Groetjes,