# Retrieval optimaliseren in RAG [[TOC]] ## Waarom standaard retrieval tekortschiet De naieve RAG-aanpak zoekt op de letterlijke gebruikersvraag. Maar gebruikers formuleren vragen vaak anders dan documenten de informatie beschrijven. Iemand die vraagt "hoe meld ik me ziek?" mist documenten waarin de term "verzuimprocedure" staat. Drie veelvoorkomende retrieval-problemen: 1. **Vocabulary mismatch**: vraag en document gebruiken andere woorden voor hetzelfde. 2. **Redundantie**: de top-k resultaten lijken te veel op elkaar en leveren weinig nieuwe informatie. 3. **Context te smal**: de gevonden chunk heeft onvoldoende omringende tekst voor een goed antwoord. De vier technieken hieronder pakken elk een deel van deze problemen aan. Je kunt ze los gebruiken of combineren in een pipeline. :::tip title="Begin klein en meet" Voeg deze technieken een voor een toe en meet het effect met een vaste evaluatieset. Multi-query en MMR geven vaak de grootste winst voor de minste complexiteit; begin daarmee voordat je contextual compression toevoegt. ::: ## Multi-query retrieval Genereer meerdere varianten van de vraag en combineer de resultaten. Zo vang je dezelfde intentie op met verschillende bewoordingen: ```typescript async function multiQueryRetrieval( question: string, vectorDB: VectorDB, k = 5 ): Promise { const queryVariants = await generateQueryVariants(question); const allResults = await Promise.all( queryVariants.map(q => vectorDB.search(q, k)) ); const seen = new Set(); const unique: Document[] = []; for (const results of allResults) { for (const doc of results) { if (!seen.has(doc.id)) { seen.add(doc.id); unique.push(doc); } } } return unique.slice(0, k * 2); } async function generateQueryVariants(question: string): Promise { const response = await anthropic.messages.create({ model: "claude-haiku-4-5", max_tokens: 300, messages: [{ role: "user", content: `Genereer 3 verschillende zoekvarianten voor deze vraag. Elke variant moet de vraag anders formuleren maar hetzelfde zoeken. Geef alleen de varianten terug, een per regel. Vraag: ${question}`, }], }); const text = response.content[0].type === "text" ? response.content[0].text : ""; return [question, ...text.split(" ").filter(Boolean).slice(0, 3)]; } ``` Gebruik een snel en goedkoop model voor het herschrijven, zodat de extra latentie beperkt blijft. ## Maximal Marginal Relevance (MMR) MMR balanceert relevantie en diversiteit. Het kiest chunks die relevant zijn voor de vraag maar niet te veel lijken op de chunks die je al hebt geselecteerd: ```typescript function maximalMarginalRelevance( queryEmbedding: number[], candidateEmbeddings: { embedding: number[]; doc: Document }[], k: number, lambda = 0.5 ): Document[] { const selected: number[] = []; const remaining = candidateEmbeddings.map((_, i) => i); for (let iter = 0; iter < k && remaining.length > 0; iter++) { let bestIdx = -1; let bestScore = -Infinity; for (const candidateIdx of remaining) { const relevance = cosineSimilarity( queryEmbedding, candidateEmbeddings[candidateIdx].embedding ); const maxSimilarityToSelected = selected.length === 0 ? 0 : Math.max(...selected.map(selIdx => cosineSimilarity( candidateEmbeddings[candidateIdx].embedding, candidateEmbeddings[selIdx].embedding ) )); const score = lambda * relevance - (1 - lambda) * maxSimilarityToSelected; if (score > bestScore) { bestScore = score; bestIdx = candidateIdx; } } if (bestIdx >= 0) { selected.push(bestIdx); remaining.splice(remaining.indexOf(bestIdx), 1); } } return selected.map(i => candidateEmbeddings[i].doc); } ``` Met lambda 0.5 wegen relevantie en diversiteit even zwaar. Verhoog de waarde voor meer relevantie, verlaag hem voor meer diversiteit. ## Parent-child retrieval Indexeer kleine chunks voor precisie, maar retourneer de grotere bovenliggende sectie als context. Zo zoek je nauwkeurig maar geef je het model genoeg omringende tekst: ```typescript interface ParentChildChunk { childId: string; parentId: string; childContent: string; parentContent: string; metadata: Record; } async function buildParentChildIndex( documents: Document[], childChunkSize = 150, parentChunkSize = 500 ): Promise { for (const doc of documents) { const parentChunks = recursiveChunk(doc.content, parentChunkSize, 50); for (let pi = 0; pi < parentChunks.length; pi++) { const parentId = `${doc.id}-parent-${pi}`; await parentStore.set(parentId, parentChunks[pi]); const childChunks = recursiveChunk(parentChunks[pi], childChunkSize, 20); for (let ci = 0; ci < childChunks.length; ci++) { const childId = `${parentId}-child-${ci}`; const embedding = await embedText(childChunks[ci]); await vectorDB.upsert(childId, embedding, { content: childChunks[ci], parent_id: parentId, source: doc.source, }); } } } } async function parentChildRetrieval(question: string, k = 5): Promise { const childResults = await vectorDB.search(question, k * 2); const parentIds = new Set(childResults.map(r => r.metadata.parent_id)); const parentContents = await Promise.all( [...parentIds].slice(0, k).map(id => parentStore.get(id)) ); return parentContents.filter(Boolean) as string[]; } ``` ## Contextual compression Comprimeer de gevonden context tot alleen de delen die echt relevant zijn voor de vraag. Dit verkleint de prompt en vermindert ruis: ```typescript async function contextualCompression( query: string, documents: Document[] ): Promise { return Promise.all( documents.map(async (doc) => { const response = await anthropic.messages.create({ model: "claude-haiku-4-5", max_tokens: 300, messages: [{ role: "user", content: `Extraheer alleen de relevante informatie uit dit document voor de gegeven vraag. Als het document geen relevante informatie bevat, geef een lege string terug. Vraag: ${query} Document: ${doc.content} Relevante informatie:`, }], }); return response.content[0].type === "text" ? response.content[0].text : ""; }) ); } ``` :::warn title="Let op de kosten en latentie" Contextual compression doet een aparte LLM-aanroep per gevonden document. Bij tien documenten zijn dat tien extra aanroepen voordat je antwoord begint. Gebruik deze techniek alleen waar kwaliteit zwaarder weegt dan snelheid, of beperk hem tot de top-3 resultaten. ::: ## Technieken combineren Een sterke pipeline ziet er vaak zo uit: :::howto title="Retrieval-pipeline opbouwen" 1. Herschrijf de vraag met **multi-query** naar enkele varianten. 2. Haal voor elke variant kandidaten op uit de **vector-database**. 3. Pas **MMR** toe op de gecombineerde kandidaten voor relevante maar diverse resultaten. 4. Gebruik **parent-child retrieval** om per treffer voldoende context terug te geven. 5. Pas optioneel **contextual compression** toe op de top-resultaten voordat je ze aan het model geeft. ::: :::faq ### Hoeveel query-varianten moet ik genereren? Twee tot vier is meestal genoeg. Meer varianten geven marginale verbeteringen maar verhogen kosten en latentie vrijwel lineair. ### Wat is de optimale lambda voor MMR? Start met 0.5. Als de resultaten te eenzijdig zijn, verlaag naar 0.3. Als de relevantie tegenvalt, verhoog naar 0.7. Toets de waarde tegen een vaste evaluatieset. ### Vertraagt contextual compression de pipeline? Ja, vaak fors, omdat je een LLM-aanroep per gevonden document doet. Gebruik het alleen voor use cases waar kwaliteit prioriteit heeft boven snelheid, of beperk het tot enkele topresultaten. ### Wanneer gebruik ik parent-child retrieval? Als je chunks veel context nodig hebben om begrijpelijk te zijn, zoals technische specificaties of wetteksten waarin een paragraaf verwijst naar eerdere definities. ### Kan ik deze technieken combineren? Ja. Multi-query, MMR en parent-child laten zich goed stapelen in een pipeline. Voeg contextual compression als laatste stap toe wanneer je de prompt verder wilt aanscherpen. :::