Naar de inhoud
NLEN
Illustratie: Re-ranking uitgelegd: cross-encoders vs vector-similarity

Re-ranking uitgelegd: cross-encoders versus vector-similarity

Door Ivo Donker — samengesteld met AI-ondersteuning (Claude & Gemini) · 20 augustus 2026

Wat je vooraf moet weten: Dit artikel valt binnen Module 3: Werken met je eigen data. Voor een goed begrip is basiskennis nodig van hoe tekst wordt omgezet in getallenvectoren. Lees eerst hoe embeddings semantische concepten vangen. Weet je nog niet hoe een opzoekketen met documenten werkt, bekijk dan de uitleg over de basisprincipes van Retrieval-Augmented Generation. Voor inzicht in hoe databases vectoren indexeren, helpt het overzicht over vector-indexering met algoritmes zoals HNSW en IVF.

In een standaard Retrieval-Augmented Generation (RAG)-architectuur vormt het ophalen van relevante context de eerste en vaak meest kwetsbare stap. De gangbare aanpak leunt vrijwel uitsluitend op zogeheten bi-encoders: de zoekvraag en documentfragmenten worden afzonderlijk omgezet in dichte vectoren, waarna een cosinussimilariteit of dot-product de rangorde bepaalt. Hoewel dit proces razendsnel miljoenen vectoren kan doorzoeken, levert het regelmatig een ranglijst op waarin cruciale semantische nuances ontbreken. Re-ranking met behulp van cross-encoders lost deze tekortkoming op door een tweetrapsraket in te richten: een snelle, grove voorselectie gevolgd door een grondige herbeoordeling.

In dit artikel ontleden we de wiskundige en computationele verschillen tussen bi-encoders en cross-encoders. We bekijken waarom pure vector-similarity steken laat vallen bij negaties, feitelijke restricties en zinsvolgorde, hoe de full-attention van cross-encoders deze interacties wél ontrafelt, en hoe je deze componenten efficiënt integreert in productiesystemen zonder onacceptabele vertragingen op te lopen.

Het fundamentele probleem van bi-encoders en vector similarity

Bi-encoders werken volgens een strikt gescheiden schema: tekstblok A en tekstblok B gaan door hetzelfde (of een identiek getraind) transformermodel en resulteren elk in één vaste representatieve vector van bijvoorbeeld 768, 1024 of 1536 dimensies. De uiteindelijke gelijkenis is niets meer dan de geometrische hoek tussen die twee statische punten in een hoogdimensionale vectorruimte. Dit principe stelt vectordatabases in staat om met benaderende zoekalgoritmen binnen milliseconden top-k resultaten te filteren.

De inherente zwakte van deze techniek is zogeheten information bottlenecking. Een complete alinea met meerdere feiten, voorwaarden en uitzonderingen moet worden samengeperst in één enkel numeriek ankerpunt. Tijdens deze compressie gaat de subtiele interactie tussen individuele woorden verloren. Zelfaandacht (self-attention) vindt immers uitsluitend plaats binnen de zoekvraag en binnen het documentfragment, maar nooit tussen de zoekvraag en het fragment tegelijkertijd.

Hierdoor ontstaat blindheid voor asymmetrische relaties. Neem een zoekvraag als "Welke subsidies gelden in 2026 niet voor particuliere warmtepompen?". Een bi-encoder projecteert zowel de zoekvraag als beleidsteksten over warmtepompsubsidies naar nagenoeg dezelfde regio in de vectorruimte. Het woordje "niet" heeft in een gemiddelde pooling-vector nauwelijks voldoende numeriek gewicht om documenten over subsidies die wél gelden af te straffen. Het resultaat: de top-5 bevat teksten die semantisch dichtbij liggen qua thema, maar het exacte logische antwoord missen.

De werking van een cross-encoder: gezamenlijke aandachtsmechanismen

Een cross-encoder pakt het vergelijkingsprobleem fundamenteel anders aan. In plaats van twee afzonderlijke vectoren te berekenen en die met elkaar te vermenigvuldigen, worden de zoekvraag ($Q$) en het kandidaatdocument ($D$) samengevoegd tot één enkele invoersequentie, gescheiden door een speciaal scheidingsteken:

[CLS] Zoekvraag Q [SEP] Kandidaatdocument D [SEP]

Deze gecombineerde reeks wordt in zijn geheel door alle transformerlagen van het model geleid. Vanaf de allereerste encoderlaag kan elk token in de zoekvraag via het self-attention-mechanisme directe aandacht richten op elk token in het kandidaatdocument, en vice versa. De output is geen vectorrepresentatie, maar een directe numerieke relevantiescore (meestal genormaliseerd via een logit of sigmoid) tussen 0 en 1.

Dankzij deze kruislingse aandacht kan het netwerk complexe patronen herkennen:

Wiskundige en computationele verschillen

Waarom gebruiken we cross-encoders dan niet simpelweg voor de complete database? Het antwoord ligt in de computationele complexiteit en de mogelijkheid tot pre-computing. Bij bi-encoders worden documentembeddings offline berekend tijdens het indexeren van de database; tijdens runtime hoeft alleen de query geëmbed te worden ($O(1)$ inferenties), waarna de vectorindex een snelle vergelijking uitvoert.

Bij een cross-encoder is pre-computing per definitie onmogelijk. Omdat de berekening afhankelijk is van de unieke combinatie van vraag én document, moet voor elk kandidaatdocument een volledige voorwaartse run (forward pass) door het neurale netwerk plaatsvinden. Zou je een collectie van 100.000 documenten doorzoeken met een cross-encoder, dan moeten er 100.000 transformer-evaluaties worden uitgevoerd voor één enkele zoekvraag. Dit leidt tot wachttijden van tientallen seconden of zelfs minuten.

Eigenschap Bi-encoder (Vector Similarity) Cross-encoder (Re-ranker)
Invoerstructuur Gescheiden: $E(Q)$ en $E(D)$ Gezamenlijk: $M(Q \circ D)$
Aandacht (Attention) Alleen intern binnen Q en D Volledige cross-attention tussen alle tokens
Pre-computing mogelijk? Ja, documentvectoren staan klaar in de index Nee, berekening vereist runtime interactie
Schaalbaarheid Miljoenen documenten in <10 ms Praktisch beperkt tot 20–100 documenten per query
Uitvoer Hoogdimensionale vectoren (bijv. 1024d float) Enkele scalairescore (relevantie 0.0 tot 1.0)
Kwaliteit / Precisie Matig tot goed (gevoelig voor ruis) Zeer hoog (vangt diepe semantische interactie)

De tweetraps retrieval-pipeline (Two-Stage Retrieval)

De standaardoplossing om het beste van twee werelden te combineren is een tweetraps retrieval-pipeline. Hierin heeft elke trap een eigen, afgebakend doel: fase 1 maximaliseert de recall (zoveel mogelijk potentiële treffers vinden), terwijl fase 2 de precision optimaliseert (de beste documenten bovenaan zetten).

Het proces verloopt als volgt:

  1. Eerste fase (Dense/Sparse Retrieval): De zoekvraag wordt afgevuurd op een vectordatabase (zoals Qdrant, Milvus of pgvector) of een hybride index (vectoren gecombineerd met BM25). De index haalt razendsnel een ruime kandidatenlijst op, bijvoorbeeld de top-50 of top-100 documenten. Wie zelf infrastructuur beheert, kan een lokale vectordatabase opzetten om deze eerste zoektrap met minimale netwerklatentie uit te voeren.
  2. Tweede fase (Cross-Encoder Re-ranking): De ruwe lijst van 50 tekstfragmenten wordt samen met de originele zoekvraag aangeboden aan het cross-encoder model. Dit model berekent voor alle 50 paren een exacte relevantiescore en sorteert de lijst opnieuw.
  3. Truncatie & Context-injectie: Alleen de best scorende 3 tot 5 fragmenten worden geselecteerd en geplaatst in de uiteindelijke prompt voor het generatieve taalmodel.

Door deze scheiding blijft de totale reactietijd binnen acceptabele grenzen (typisch tussen de 30 en 150 milliseconden voor de herbeoordelingsstap op een bescheiden GPU of snelle CPU), terwijl de kwaliteit van de context dramatisch toeneemt.

Praktijkvergelijking en kwaliteitsmetingen

Om te evalueren wat de toevoeging van een cross-encoder oplevert, kijken we naar gestandaardiseerde metrieken uit de Information Retrieval: MRR@k (Mean Reciprocal Rank) en NDCG@k (Normalized Discounted Cumulative Gain). Waar bi-encoders op benchmarkdatasets zoals BEIR vaak een solide recall@100 halen, keldert de NDCG@10 wanneer documenten met oppervlakkige woordovereenkomsten per ongeluk hoger scoren dan inhoudelijk correcte alinea's.

In productiemetingen zien we een vast patroon: het toevoegen van een re-ranker bovenop een bi-encoder zorgt gemiddeld voor een stijging van 15% tot 35% in MRR@10. Vooral in domeinen met dichte vaktermen (juridisch, financieel en medisch) voorkomt de re-ranker dat documenten die toevallig dezelfde termen herhalen, maar een andere conclusie trekken, de context vervuilen.

Daarnaast speelt documentsegmentatie een cruciale rol. Als de tekstblokken in de database onhandig zijn opgedeeld, kan zelfs de beste re-ranker de contextuele samenhang niet herstellen. Bekijk voor een gedegen opzet daarom de verschillende chunking-strategieën om documenten slim op te knippen voor RAG, zodat fragmenten exact de juiste lengte behouden voor de aandachtsspanne van de cross-encoder.

Concreet Nederlands praktijkvoorbeeld: beleidsdocumenten doorzoeken

Laten we een realistische casus bekijken binnen een Nederlandse gemeente die gebruikmaakt van een RAG-systeem voor het beantwoorden van vragen over lokale verordeningen (APV).

De zoekvraag van de burger:
"Heb ik een kapvergunning nodig voor een zieke berk met een stamomtrek van 45 cm op particulier terrein?"

In de vectordatabase bevinden zich drie relevante fragmenten uit het kapbeleid:

Wat gebeurt er bij pure bi-encoder vector similarity? Fragment A scoort zeer hoog op termen als "kapvergunning", "kappen van bomen" en "ongeacht gezondheid". Fragment B scoort hoog op "particuliere boomeigenaren" en "stamomtrek". Fragment C heeft door zijn abstractere juridische formulering ("houtopstanden", "vellen", "uitzonderingsbepaling") vaak een lagere cosinussimilariteit ten opzichte van de spreektaalvraag van de burger. De kans is groot dat Fragment A en B als top-2 uit de vectorzoektocht komen, waardoor het taalmodel concludeert dat er géén vergunning nodig is op basis van de 50 cm regel, terwijl de specifieke uitzonderingsregel voor zieke bomen (Fragment C) over het hoofd wordt gezien.

Plaatsen we een cross-encoder achter de zoektrap, dan analyseert deze de gecombineerde interactie. Het model herkent dat de zoekvraag tegelijkertijd vraagt om "zieke berk" én "particulier terrein" én "45 cm". In Fragment C ziet de self-attention matrix direct de correlatie tussen "zieke bomen", "particulier terrein" en de grenswaarde. Fragment C krijgt de hoogste prioriteit (score 0.94), gevolgd door B (score 0.71), terwijl A ver wegzakt (score 0.18). Het LLM ontvangt de juiste context en geeft een foutloos antwoord.

Populaire re-ranking modellen en technologische varianten

Wie vandaag een re-ranking component implementeert, heeft keuze uit verschillende open-source architecturen en gespecialiseerde services:

Hardware-impact, doorvoer en productiestrategieën

Het toevoegen van een cross-encoder aan de request-lus vraagt om duidelijke infrastructurele keuzes. Waar vector-similarity puur leunt op geheugensnelheid en simpele vector-berekeningen in C++/Rust, vergt de re-ranker daadwerkelijk transformer-rekenkracht (matrixvermenigvuldigingen).

Hieronder staat een minimalistisch Python-voorbeeld waarin we laten zien hoe je met de bibliotheek sentence-transformers een lijst van kandidaten uit een vectorzoektocht opnieuw rangschikt:

from sentence_transformers import CrossEncoder

# Laad een meertalig re-ranker model
model = CrossEncoder('BAAI/bge-reranker-v2-m3', max_length=512)

query = "Welke regels gelden voor zonnepanelen op een monument?"
kandidaat_documenten = [
  "Subsidie voor isolatie kan worden aangevraagd via het landelijk loket.",
  "Voor monumentale panden is altijd een omgevingsvergunning nodig bij zonnepanelen.",
  "Zonnepanelen op reguliere daken zijn vergunningsvrij mits binnen het dakvlak geplaatst."
]

# Bouw de paren op voor de forward pass
paren = [[query, doc] for doc in kandidaat_documenten]
scores = model.predict(paren)

# Koppel documenten aan hun berekende scores en sorteer aflopend
gerangschikt = sorted(
  zip(kandidaat_documenten, scores),
  key=lambda x: x[1],
  reverse=True
)

for doc, score in gerangschikt:
  print(f"[{score:.4f}] {doc}")

In een productieomgeving met honderden gelijktijdige gebruikers moet je rekening houden met de volgende vuistregels:

Valkuilen, zwakke punten en wanneer géén cross-encoder te gebruiken

Hoewel cross-encoders de antwoordkwaliteit van RAG-applicaties aanzienlijk verhogen, zijn ze geen wondermiddel voor elk architectuurprobleem. Er zijn specifieke situaties waarin het toevoegen van deze laag juist nadelig werkt:

1. Strenge real-time eisen (<50 ms totale roundtrip): Als een applicatie binnen enkele tientallen milliseconden moet reageren (zoals bij type-ahead suggesties of voice-agents), is de extra latentie van een cross-encoder vaak onacceptabel. In die scenario's levert een goed getunede hybride zoekindex (dense + sparse BM25) met Reciprocal Rank Fusion (RRF) een snellere oplossing.

2. Beperkte contextlengte van de re-ranker: Veel populaire cross-encoders hebben een contextvenster van maximaal 512 tokens. Als je documentchunks groter zijn dan deze limiet, worden teksten botweg afgekapt. Woorden aan het einde van een lang fragment tellen dan simpelweg niet mee in de kruislingse aandachtsberekening.

3. Domeinspecifieke mismatch: Een generieke cross-encoder die getraind is op algemene webteksten en Wikipedia begrijpt niet automatisch specialistische broncode, medische dossiers of cryptische logbestanden. Zonder fine-tuning kan een out-of-the-box model relevante technische fragmenten onterecht lager inschalen dan een trefwoordzoekmachine dat zou doen.

Hierna verder met: Nu je weet hoe je de meest relevante fragmenten selecteert, is het zaak deze optimaal aan te bieden aan het generatieve taalmodel. Lees verder in de gids over context-engineering en prompt-optimalisatie of ontdek hoe je het volledige systeem valideert in het artikel over evalueren met testsets en LLM-as-judge.

Door bi-encoders in te zetten voor de grove filtering en cross-encoders voor de fijnmazige herordening ontstaat een robuuste en uiterst betrouwbare zoekbasis. Hiermee voorkom je dat een taalmodel moet hallucineren op basis van half-relevante documenten en benut je de rekenkracht van je hardware precies daar waar het de meeste redactionele winst oplevert.