Search ranking stack: BM25, embeddings en reranking

Automatische vertaling

Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.

Deze gids is bedoeld voor Python-engineers die een search ranking pipeline bouwen of verbeteren. Hij legt uit hoe BM25, dense embeddings, rank fusion en rerankers samenwerken, en hoe je meet of je zoekfunctie betere kandidaten of een betere volgorde nodig heeft.

Een zoekopdracht naar “cheap laptop” kan een product missen dat als “budget notebook computer” is beschreven. Bij een andere fout staat een draagtas voor koptelefoons boven de koptelefoon zelf. Het eerste probleem vraagt om betere kandidaten; het tweede om een betere volgorde.

De bijbehorende demorepository toont retrieval en reranking op een gesampelde selectie van de Amazon ESCI-productzoekdata.

Voor een compacte gids over het kiezen van een fase, zie BM25 vs Embeddings vs Rerankers. De reeks experimenten verderop in dit artikel koppelt elke fase aan een meting.

Lexicale, dense en cross-encoder-modellen

De stack is makkelijker te begrijpen als drie lagen. Lexicale retrieval vindt exacte termen, dense retrieval overbrugt verschillen in woordkeuze en rerankers vergelijken de sterkste kandidaten tot in detail.

BM25 en lexicale retrieval

Decennialang was BM25 de standaard. Het is een probabilistisch model dat documenten scoort op de frequentie van queryterms in het document, genormaliseerd op documentlengte en inverse document frequency (IDF).

BM25 is sterk wanneer letterlijke termen de intentie dragen: foutcodes, product-SKU’s, namen en API-identifiers. De belangrijkste beperking is vocabulary mismatch. Een query naar “cheap laptop” kan een document over een “budget notebook computer” missen als de geïndexeerde tekst geen brug tussen die uitdrukkingen bevat.

Toch vond de BEIR-studie BM25 een sterke zero-shot baseline over de uiteenlopende retrievaltaken. De BM25-cijfers van BEIR komen uit Lucene via Pyserini, niet uit de whitespace-getokeniseerde rank_bm25 die deze demo gebruikt. BM25 verslaat ook sommige neurale models bij argumentatieve retrievaltaken zoals Touche-2020.

Dense retrieval en embeddings

BERT-achtige encoders maakten dense retrieval praktisch bruikbaar. Ze plaatsen queries en documenten in een gedeelde vectorruimte en rangschikken kandidaten daarna met een similarity-functie zoals cosine similarity of dot product.

De bi-encoder (of “two-tower”-architectuur) codeert de query en het document apart en levert embeddings met een vaste lengte op. Documentvectoren kunnen vooraf worden berekend en offline worden geïndexeerd, en worden daarna snel opgehaald met Approximate Nearest Neighbor-algoritmen (ANN). Zo komen “cheap laptop” en “budget notebook” dicht bij elkaar te liggen in de vectorruimte.

Sommige bi-encoders gebruiken een Siamese architectuur, zoals Sentence-BERT, waarbij beide kanten dezelfde weights delen. Andere gebruiken aparte query- en documenttowers. Pooling, vectorgrootte, similarity-functie en trainingsdoel zijn modelkeuzes en geen eigenschappen van elke dense retriever.

Deze models worden getraind met contrastive learning, meestal met de InfoNCE-loss. Bij een batch van (query, positive_document)-paren maximaliseert het doel sim(query, positive_doc) en minimaliseert het sim(query, negative_docs). De negatives komen uit de positives van andere queries in dezelfde batch (in-batch negatives). Een temperatuurparameter τ\tau bepaalt hoe scherp het model de twee moet scheiden.

Trainingsdata telt vaak zwaarder dan de embeddingdimensie. Retrievalmodellen leren van paren van een query en een positief document, en van zorgvuldig gekozen hard negatives: aannemelijke maar niet-relevante documenten. In het latere trainingsgedeelte zie je hoe SimANS zowel triviale negatives als waarschijnlijke false negatives vermijdt.

De prijs is de representatie-bottleneck. Bi-encoders persen alle semantische nuance in één vector van vaste grootte, waardoor ze vaak fijne wisselwerkingen tussen specifieke queryterms en specifieke documentinhoud missen.

Cross-encoders en LLMs

Cross-encoders (Nogueira & Cho, 2019) voeren de query en het document samen als één aaneengeschakelde reeks ([CLS] Query [SEP] Document) in een Transformer. Elk querytoken kan daardoor via volledige self-attention naar elk documenttoken kijken. Die diepe interactie vangt nuances op die onafhankelijke codering mist.

LLM reranking gebruikt een model dat via een prompt meerdere kandidaten tegelijk vergelijkt. RankGPT liet met GPT-4 sterke zero-shot listwise resultaten zien op de geëvalueerde benchmarks, maar stabiliteit van de output, kosten en geschiktheid voor je domein moeten nog apart worden getest.

Deze scores kunnen voor willekeurige queries niet vooraf worden berekend, dus reranking komt na retrieval. Dat kostenverschil is de reden voor de multi-stage trechter.


De multi-stage trechter

Een dure cross-encoder of LLM over miljoenen documenten draaien is niet haalbaar. Moderne zoekstacks gebruiken daarom een trechter. Retrieval en pre-ranking verkleinen de kandidatenset zodat full ranking duurdere models op een shortlist kan toepassen. Blending past daarna regels of een rankingbeleid toe; de kosten hangen van dat beleid af.

Een goedkope retriever mist bovendien de uiteindelijke precisie, dus de trechter zet elk model alleen in waar de kosten redelijk zijn. De figuur toont het invoervolume van elke fase en schematische modelkosten, en markeert dat latere fasen weggelaten documenten niet kunnen terughalen:

Retrieval verwerkt miljoenen documenten, pre-ranking duizenden, full ranking honderden en blending tientallen. De modelkosten zijn schematisch voor de eerste drie fasen; de kosten van blending hangen af van het beleid. Een lijn na retrieval markeert dat latere fasen weggelaten documenten niet kunnen terughalen.Retrieval verwerkt miljoenen documenten, pre-ranking duizenden, full ranking honderden en blending tientallen. De modelkosten zijn schematisch voor de eerste drie fasen; de kosten van blending hangen af van het beleid. Een lijn na retrieval markeert dat latere fasen weggelaten documenten niet kunnen terughalen.

FaseInvoerschaalHoofddoelGangbare methodenMeting bij uitgang
RetrievalCorpus of indexRecall van kandidatenBM25, bi-encodersRecall bij de kandidaat-cutoff
Pre-rankingGrote kandidatensetGoedkoop filterenLichte models, regelsBehouden recall per milliseconde
Full rankingShortlistKwaliteit van de topCross-encoders, LLMsNDCG/MRR, latency, kosten
BlendingDefinitieve lijsten of positiesRandvoorwaarden en mixRegels, multi-objective rankBeleid, diversiteit, zakelijke guardrails

Een reranker herordent de kandidaten die retrieval teruggeeft; hij kan er geen toevoegen. De dekking van de opgehaalde kandidaten beperkt dus wat latere fasen kunnen terughalen. Bij meer dan 100 kandidaten kan reranking een relevant document naar de top 100 verplaatsen en Recall@100 verbeteren. In deze demo blijven alle 100 kandidaten behouden, dus reranking kan Recall@100 niet veranderen.

Tools per fase

De demo gebruikt kleine in-memory bibliotheken. Buiten een laptop heeft elke fase enkele gangbare implementaties:

FaseToolGebruik wanneer
BM25rank_bm25Het corpus in het geheugen past en je snel een baseline wilt
Elasticsearch, OpenSearch of Lucene via PyseriniHet corpus een inverted index, analyzers en filters nodig heeft
Dense retrievalFAISSJe een ANN-bibliotheek in je eigen service wilt
pgvectorDe data al in Postgres staat
Qdrant, Weaviate, Vespa of Elasticsearch kNNJe metadatafilters, frequente updates en een beheerde index nodig hebt
FusionElasticsearch RRF retriever, Qdrant Query API met rrfJe rank fusion binnen de zoekmachine wilt
Weaviate hybrid searchJe Weaviate gebruikt; het biedt ranked fusion en relative-score fusion
Embedding modelsDe retrievaltab van MTEB, daarna je eigen beoordeelde queriesJe een model kiest; het leaderboard beperkt de lijst, je eigen queries beslissen
Rerankerms-marco-MiniLM-L12-v2Engelse tekst, CPU-budget
bge-reranker-v2-m3Meertalige tekst
Elastic RerankEngelse tekst die al in Elasticsearch staat; vereist een geschikt abonnement of een trial

De demo: een pipeline met vijf fasen

Om dit concreet te maken bouwde ik een search-ranking-stack-demo die een pipeline met vijf fasen draait op de productzoekbenchmark Amazon ESCI. Elke fase schrijft metrics weg.

BM25 en dense retrieval leveren elk een top 100 die RRF samenvoegt tot 100 kandidaten; de cross-encoder sorteert rang 1 tot 50 opnieuw, de optionele LLM herordent rang 1 tot 10, en Recall@100 leest alle 100 rangen.BM25 en dense retrieval leveren elk een top 100 die RRF samenvoegt tot 100 kandidaten; de cross-encoder sorteert rang 1 tot 50 opnieuw, de optionele LLM herordent rang 1 tot 10, en Recall@100 leest alle 100 rangen.

De pipeline:

  1. BM25 sparse retrieval — lexicale baseline (rank_bm25)
  2. Dense bi-encoder retrieval — semantische kandidaatgeneratie (all-MiniLM-L6-v2)
  3. Hybride RRF-fusion — rank-gebaseerde fusion van sparse en dense resultaten
  4. Cross-encoder reranking — pairwise relevantiescores (ms-marco-MiniLM-L12-v2)
  5. LLM listwise reranking — via een prompt vergelijken van de definitieve shortlist (Ollama, API of lokaal model)

Stap 1—3 vormen de retrieval-fase van de trechter (recall maximaliseren); stap 4—5 vormen de full ranking-fase (precisie maximaliseren). De demo slaat pre-ranking en blending over. Het diagram toont het aantal kandidaten per stap: de cross-encoder scoort de eerste 50 van 100 hybride kandidaten en laat de rest in hybride volgorde staan, en de LLM ziet de eerste 10.

Quick start

De downloadstap haalt de dataset één keer op en schrijft een kleine sample. run-all draait daarna elke fase, print één metricsrij per fase en schrijft de rijen naar results/metrics.json en een grafiek naar results/metrics_comparison.png. Voeg --llm-mode toe om de LLM-fase mee te nemen.

git clone https://github.com/slavadubrov/search-ranking-stack.git
cd search-ranking-stack
uv sync

# Download and sample ESCI dataset (~2.5GB download, ~5MB sample)
uv run download-data

# Run BM25, dense, hybrid RRF, and the cross-encoder
uv run run-all

# Add LLM reranking with a local Ollama model (ollama pull llama3.2:3b first)
uv run run-all --llm-mode ollama

Dataset en sampling: Amazon ESCI

De demo gebruikt de Amazon Shopping Queries Dataset (ESCI) van KDD Cup 2022 — een echte productzoekbenchmark met relevantielabels op vier niveaus:

LabelGainBetekenisVoorbeeld (query: “wireless headphones”)
Exact (E)3Voldoet aan alle eisen van de querySony WH-1000XM5 Wireless Headphones
Substitute (S)2Functioneel alternatiefKoptelefoon met kabel en Bluetooth-adapter
Complement (C)1Verwant nuttig itemDraagtas voor een koptelefoon
Irrelevant (I)0Geen betekenisvol verbandUSB-oplaadkabel

Gegradeerde relevantie is belangrijk omdat je er NDCG (Normalized Discounted Cumulative Gain) mee kunt gebruiken. Die onderscheidt een “perfecte” ranking van een “slechts voldoende”. Binaire metrics geven beide dezelfde score.

Ik gebruikte de small_version-sample van de demo: 500 queries, 9,870 producten en 9,984 beoordelingen. De downloader leest de enige train-split van tasksource/esci, filtert op de Engelse us-locale en small_version == 1, en trekt daarna met seed 42 zonder teruglegging maximaal 500 unieke query-ID’s. Het corpus bevat de unieke producten uit de geselecteerde beoordelingsrijen. Dit is geen held-out split en ook geen onafhankelijk corpus. De sample is klein genoeg voor een laptop, maar te klein en te domeinspecifiek om een productieranking mee vast te stellen. Gebruik haar om de fasen te reproduceren en foutpatronen te bekijken; gebruik held-out, representatieve queries voor beslissingen over deployment.


Evaluatie: meten wat telt

De demo gebruikt drie metrics, die elk vanuit een andere hoek naar de ranking kijken. De figuur past alle drie toe op de resultaten van één query, met de ESCI-gains van hierboven:

Vijf gerangschikte resultaten met de grades Irrelevant, Complement, Exact, Substitute, Irrelevant, plus één gemiste Exact: de DCG-termen tellen op tot 2.99 tegenover een ideale 6.32, wat NDCG@10 0.47 geeft; het eerste relevante resultaat op rang 2 geeft MRR 0.50, en 3 van de 4 relevante producten geven Recall@100 0.75.Vijf gerangschikte resultaten met de grades Irrelevant, Complement, Exact, Substitute, Irrelevant, plus één gemiste Exact: de DCG-termen tellen op tot 2.99 tegenover een ideale 6.32, wat NDCG@10 0.47 geeft; het eerste relevante resultaat op rang 2 geeft MRR 0.50, en 3 van de 4 relevante producten geven Recall@100 0.75.

NDCG@10 (hoofdmetric)

Normalized Discounted Cumulative Gain meet de kwaliteit van de top-10-ranking met gegradeerde relevantie. Het beloont het hoog plaatsen van zeer relevante documenten met een logaritmische korting:

DCG@k=∑i=1krelilog⁡2(i+1)NDCG@k=DCG@kIDCG@k\text{DCG@k} = \sum_{i=1}^{k} \frac{rel_i}{\log_2(i + 1)} \qquad \text{NDCG@k} = \frac{\text{DCG@k}}{\text{IDCG@k}}

Dit komt overeen met de lineaire gains van ndcg_cut in trec_eval, via pytrec_eval gekozen als ndcg_cut_10. IDCG is de DCG van de ideale volgorde van de beoordeelde grades; een query zonder positieve gain krijgt nul. Exponentiële gains zoals 2rel−12^{rel}-1 vormen een andere conventie en zouden deze cijfers veranderen.

Van de drie is NDCG de enige metric die de vier relevantieniveaus van ESCI volledig benut. Een systeem dat een Exact-match op positie 1 zet, scoort hoger dan een systeem dat daar een Substitute zet. Daarom is dit hier de hoofdmetric.

MRR (eerste resultaat van Complement of hoger)

Mean Reciprocal Rank gebruikt de positie van het eerste resultaat dat als relevant telt. In deze demo geeft de evaluator de relevantiebeoordelingen van ESCI door aan pytrec_eval en gebruikt hij recip_rank over elke teruggegeven lijst van maximaal 100 kandidaten. De binaire drempel is relevance > 0: Complement (1), Substitute (2) en Exact (3) tellen als relevant. Irrelevant (0) niet. Een relevant resultaat op positie 1 geeft een reciprocal rank van 1.0; op positie 3 is het 0.333. Dit is MRR over de teruggegeven kandidatenlijsten, niet MRR@10.

Recall@100 (retrievaldekking)

Recall@100 meet welk deel van de beoordeelde relevante documenten in de top 100 staat. Het blijft gelijk bij reranking van dezelfde 100 kandidaten, zoals in deze demo. Met een grotere kandidatenset kan reranking een relevant document naar de top 100 verplaatsen en de metric verhogen, maar het kan geen document terughalen dat retrieval heeft weggelaten.

De noemer bevat alleen beoordeelde relevante documenten, zoals in de recallimplementatie van trec_eval. Een onbeoordeeld product levert geen positieve relevantie op en staat niet in die noemer. Onvolledige beoordelingen kunnen er daardoor voor zorgen dat gemeten recall afwijkt van de dekking van alle werkelijk relevante producten.


De taak van de retrievallaag is recall te maximaliseren: zoveel mogelijk relevante documenten in de kandidatenset krijgen.

BM25: de lexicale baseline

BM25 scoort documenten op overlap van termen met de query, met verzadiging van term frequency en normalisatie van documentlengte:

BM25(q,d)=∑t∈qIDF(t)⋅tf(t,d)⋅(k1+1)tf(t,d)+k1⋅(1−b+b⋅∣d∣/avgdl)\text{BM25}(q, d) = \sum_{t \in q} \text{IDF}(t) \cdot \frac{tf(t,d) \cdot (k_1 + 1)}{tf(t,d) + k_1 \cdot (1 - b + b \cdot |d|/\text{avgdl})}

Hierbij is IDF(t)\text{IDF}(t) de inverse document frequency van term tt, is tf(t,d)tf(t,d) de term frequency in document dd, is ∣d∣|d| de documentlengte en is avgdl\text{avgdl} de gemiddelde documentlengte over het corpus. Twee parameters tellen: k1k_1 (meestal 1.2—2.0) bepaalt de TF-verzadiging — hoe snel herhaalde termen geen waarde meer toevoegen — en bb (meestal 0.75) bepaalt de normalisatie van documentlengte.

De grafiek toont het term-frequencydeel van de formule voor beide uiteinden van het gebruikelijke k1k_1-bereik:

Voor een document van gemiddelde lengte stijgt de termbijdrage vanaf 1 bij één voorkomen en vlakt af richting 2.2 met k1 = 1.2 en richting 3.0 met k1 = 2.0, terwijl een ruwe telling blijft stijgen.Voor een document van gemiddelde lengte stijgt de termbijdrage vanaf 1 bij één voorkomen en vlakt af richting 2.2 met k1 = 1.2 en richting 3.0 met k1 = 2.0, terwijl een ruwe telling blijft stijgen.

De implementatie is kort. Gewone whitespace tokenization met rank_bm25:

# src/search_ranking_stack/stages/s01_bm25.py

from rank_bm25 import BM25Okapi

def run_bm25(data: ESCIData, top_k: int = 100):
    doc_ids = list(data.corpus.keys())
    tokenized_corpus = [text.lower().split() for text in data.corpus.values()]

    bm25 = BM25Okapi(tokenized_corpus)

    results = {}
    for query_id, query_text in data.queries.items():
        scores = bm25.get_scores(query_text.lower().split())
        top_indices = np.argsort(scores)[::-1][:top_k]
        results[query_id] = {doc_ids[idx]: float(scores[idx]) for idx in top_indices}

    return results

BM25 haalt een Recall@100 van 0.741 — 74% van de beoordeelde relevante producten staat ergens in de top 100. Een reranker die alleen die BM25-lijst krijgt, kan de ontbrekende 26% van de beoordeelde relevante items niet terughalen.

Dense bi-encoder retrieval

De bi-encoder zet queries en documenten onafhankelijk van elkaar om in een gedeelde embeddingruimte:

# Simplified from src/search_ranking_stack/stages/s02_dense.py

import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
doc_texts = ["Sony WH-1000XM5 Wireless Headphones", "Headphone carrying case"]
query_texts = ["wireless headphones"]

# Encode corpus once, cache to disk
corpus_embeddings = model.encode(
    doc_texts,
    batch_size=128,
    normalize_embeddings=True,  # Cosine sim = dot product
    convert_to_numpy=True,
)

# At query time: encode query, compute dot product
query_embeddings = model.encode(query_texts, normalize_embeddings=True)
similarity_matrix = np.dot(query_embeddings, corpus_embeddings.T)

Bij genormaliseerde embeddings is cosine similarity gelijk aan een dot product. De demo berekent de volledige query-bij-corpus-matrix, omdat 9,870 documenten ruim in het geheugen passen; een productiecorpus zou normaal een approximate-nearest-neighbor-index gebruiken. In deze sample verhoogt all-MiniLM-L6-v2 de Recall@100 van 0.741 naar 0.825.

Hoe bi-encoders goede representaties leren

Pretraining van een encoder en fine-tuning van embeddings lossen verschillende problemen op. Een voorgetrainde Transformer levert tokenrepresentaties; contrastieve fine-tuning leert een gepoolde vector welke teksten dicht bij elkaar horen. NLI-, zinsgelijkenis- en query-passagedata zijn mogelijke ingrediënten, geen vaste volgorde. De modelkaart van all-MiniLM-L6-v2 uit de demo beschrijft een voorgetrainde MiniLM-encoder die is gefinetuned op meer dan een miljard heterogene zinsparen.

Fine-tuning voor retrieval kan hard negative mining gebruiken. Willekeurige negatives, zoals een kookdocument bij een query over koptelefoons, zijn mogelijk te makkelijk om veel van te leren. Een retriever kan in plaats daarvan aannemelijke kandidaten aanleveren die volgens de beoordelingen niet relevant zijn. Onbeoordeelde documenten met een hoge score vragen om voorzichtigheid: sommige zijn misschien ontbrekende positives.

SimANS trekt negatives uit een kansverdeling die geconcentreerd is rond de relevantiescore van het positieve document. Het legt minder nadruk op zowel makkelijke negatives als op de kandidaten met de hoogste score, die false negatives kunnen zijn. Dit is gewogen sampling uit opgehaalde kandidaten, geen harde regel die elk document boven of onder een rangdrempel uitsluit. De curve toont hoe vaak een opgehaalde kandidaat wordt getrokken, naar zijn score ten opzichte van het positieve document:

Een samplingcurve over de kandidaatscore ten opzichte van het positieve document d+: een kookdocument ver onder d+ wordt zelden gesampled, Sony-vervangende oorkussens net onder d+ worden het vaakst gesampled, en kandidaten ruim boven d+ worden zelden gesampled omdat sommige ongelabelde positives kunnen zijn.Een samplingcurve over de kandidaatscore ten opzichte van het positieve document d+: een kookdocument ver onder d+ wordt zelden gesampled, Sony-vervangende oorkussens net onder d+ worden het vaakst gesampled, en kandidaten ruim boven d+ worden zelden gesampled omdat sommige ongelabelde positives kunnen zijn.

# What a training triplet looks like after hard negative mining
training_triplet = {
    "query": "wireless noise canceling headphones",
    "positive": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
    "negative": "Sony headphone replacement ear pads",  # Hard negative: same brand, related product, but wrong intent
}
# The bi-encoder must learn that "ear pads" is NOT what the user wants,
# even though it shares many tokens with the positive document.

De contrastive loss-functie (InfoNCE) brengt dit samen. Voor elke query qq met positief document d+d^+ en een set negatieve documenten {d1−,…,dn−}\{d^-_1, \ldots, d^-_n\}:

L=−log⁡esim(q,d+)/τesim(q,d+)/τ+∑i=1nesim(q,di−)/τ\mathcal{L} = -\log \frac{e^{\text{sim}(q, d^+) / \tau}}{e^{\text{sim}(q, d^+) / \tau} + \sum_{i=1}^{n} e^{\text{sim}(q, d^-_i) / \tau}}

Hierbij is sim(q,d)\text{sim}(q, d) de cosine similarity tussen de embeddings van query en document, en is τ\tau de temperatuurparameter (meestal 0.05—0.1). Lagere waarden maken de loss gevoeliger voor hard negatives. Het is een softmax cross-entropy: de similarity van het positieve paar omhoog duwen ten opzichte van alle negatives. Bij een kleine τ\tau geven zelfs lichte verschillen in similarity grote gradients, wat het model dwingt fijnere onderscheidingen te maken.

Eén encoder met gedeelde weights zet een query, de positieve en een hard negative om in embeddings; in de embeddingruimte trekt de update de positieve naar de query en duwt de hard negative weg, terwijl de positieven van andere queries als in-batch negatives dienen.Eén encoder met gedeelde weights zet een query, de positieve en een hard negative om in embeddings; in de embeddingruimte trekt de update de positieve naar de query en duwt de hard negative weg, terwijl de positieven van andere queries als in-batch negatives dienen.

Bi-encoder-embeddings op schaal serveren

Het architecturale voordeel van een bi-encoder is de offline/online-splitsing. Documentembeddings worden bij het indexeren berekend en in een vectorindex opgeslagen. Op querytijd codeert het systeem de query en zoekt het in die opgeslagen vectoren. De latency hangt af van de encoder, de hardware, de index, de filters en het recalldoel, dus profileer beide stappen apart.

In de demo valt de rekensom mee: 9,870 documenten ×\times 384 dimensies ×\times 4 bytes per float = ~15 MB aan embeddings. Op productieschaal is dat niet meer zo: 1 miljard documenten met embeddings van 768 dimensies hebben ~3 TiB opslag nodig. Daar komen quantization (32-bit floats comprimeren tot 8-bit integers), product quantization (vectoren opsplitsen in deelruimtes) en SSD-gebaseerde indexen zoals DiskANN om de hoek kijken. De sectie over het indexeren van dense vectoren behandelt de indexalgoritmen.

Op indexeertijd worden N documenten eenmalig gecodeerd tot een N-bij-D-matrix en een vectorindex; op querytijd codeert elk verzoek één query en doorzoekt de opgeslagen vectoren; de opslag is ongeveer 15 MB in de demo en ongeveer 3 TiB voor 1 miljard vectoren van 768 dimensies.Op indexeertijd worden N documenten eenmalig gecodeerd tot een N-bij-D-matrix en een vectorindex; op querytijd codeert elk verzoek één query en doorzoekt de opgeslagen vectoren; de opslag is ongeveer 15 MB in de demo en ongeveer 3 TiB voor 1 miljard vectoren van 768 dimensies.

Waarom hybride retrieval testen

De twee methoden falen vaak op verschillende manieren. BM25 past goed bij eigennamen, product-SKU’s en foutcodes. Dense retrieval kan vocabulary mismatch opvangen, zoals “cheap laptop” tegenover “budget notebook computer”. Of fusion helpt, hangt af van hoe vaak die complementaire gevallen voorkomen in de doelverzameling queries.

Een gebruikelijk vervolgexperiment is hybrid search: beide retrievalmethoden draaien en hun gerangschikte lijsten daarna samenvoegen.

Reciprocal rank fusion (RRF)

BM25 en dense retrieval leveren scores op met verschillende betekenis en schaal. Een lineaire combinatie vraagt daarom om calibration en validatie telkens wanneer de retrievers of het corpus veranderen.

BM25 en dense retrieval rangschikken elk drie documenten met ruwe scores op verschillende schalen; RRF telt 1/(60 + rang) van elke lijst op, dus Doc-X en Doc-Y delen 0.0325, en rang 50 in beide lijsten (0.0182) wint van rang 1 in één lijst (0.0164).BM25 en dense retrieval rangschikken elk drie documenten met ruwe scores op verschillende schalen; RRF telt 1/(60 + rang) van elke lijst op, dus Doc-X en Doc-Y delen 0.0325, en rang 50 in beide lijsten (0.0182) wint van rang 1 in één lijst (0.0164).

Reciprocal Rank Fusion (Cormack et al., 2009) laat de ruwe scores helemaal vallen en gebruikt alleen de rangpositie:

RRF(d)=∑r∈Rankings1k+rank(d,r)\text{RRF}(d) = \sum_{r \in \text{Rankings}} \frac{1}{k + \text{rank}(d, r)}

Hierbij is kk een smoothingconstante; 60 is een gangbare beginwaarde. RRF beloont items die over de invoerlijsten heen dicht bij de top staan, zonder hun ruwe scores te vergelijken. Het voorkomt calibration van de scoreschaal, maar de retrievalcutoffs, de weights en kk vragen nog steeds om evaluatie.

De implementatie:

# src/search_ranking_stack/stages/s03_hybrid_rrf.py

from collections import defaultdict

def reciprocal_rank_fusion(ranked_lists, k=60, top_k=100):
    all_query_ids = set().union(*(results.keys() for results in ranked_lists))
    fused_results = {}

    for query_id in all_query_ids:
        rrf_scores = defaultdict(float)

        for results in ranked_lists:
            sorted_docs = sorted(results.get(query_id, {}).items(),
                                 key=lambda x: x[1], reverse=True)

            for rank, (doc_id, _score) in enumerate(sorted_docs, start=1):
                rrf_scores[doc_id] += 1.0 / (k + rank)

        sorted_rrf = sorted(rrf_scores.items(),
                            key=lambda x: x[1], reverse=True)[:top_k]
        fused_results[query_id] = dict(sorted_rrf)

    return fused_results

Hybride RRF komt uit op een Recall@100 van 0.842 en een NDCG@10 van 0.628 — beter dan BM25 (0.585) en Dense (0.611) afzonderlijk. Een document dat slechts één retriever vindt, krijgt nog steeds een RRF-score, maar kan onder de cutoff van de top 100 vallen. Met k=60k=60 scoort rang 1 in één lijst 1/611/61, minder dan rang 50 in beide lijsten (2/1102/110). Overlap kan dus zwaarder wegen dan een hoge rang in één lijst.


Cross-encoder reranking

Met 100 hybride kandidaten per query kun je je een duurder model veroorloven. De cross-encoder verwerkt de query en het document samen in één Transformer, met self-attention over de gecombineerde query en het document.

Een bi-encoder codeert de query en het document apart en vergelijkt een per verzoek berekende queryvector met een opgeslagen documentvector via het inwendig product; een cross-encoder draait één Transformer over de samengevoegde tokens en scoort het paar met een lineaire kop op de uitvoer van het classificatietoken.Een bi-encoder codeert de query en het document apart en vergelijkt een per verzoek berekende queryvector met een opgeslagen documentvector via het inwendig product; een cross-encoder draait één Transformer over de samengevoegde tokens en scoort het paar met een lineaire kop op de uitvoer van het classificatietoken.

Interactie op tokenniveau

Het verschil zit in de attention-matrix. In een bi-encoder is attention blokdiagonaal: querytokens kijken alleen naar andere querytokens en documenttokens alleen naar andere documenttokens. De twee representaties ontmoeten elkaar op tokenniveau nooit — ze kruisen elkaar pas aan het eind via een dot product. Een cross-encoder berekent de volledige attention-matrix, waarin elk querytoken naar elk documenttoken kijkt en omgekeerd. Door die gezamenlijke self-attention kan het model query- en documenttokens direct vergelijken.

Twee attention-rasters voor de query "headphones that are not wireless" en de titel "Sony WH-1000XM5 Wireless Headphones": de bi-encoder berekent alleen de blokken binnen elke tekst, de cross-encoder berekent ook de blokken query-bij-titel, inclusief waar "not" en "Wireless" elkaar ontmoeten.Twee attention-rasters voor de query "headphones that are not wireless" en de titel "Sony WH-1000XM5 Wireless Headphones": de bi-encoder berekent alleen de blokken binnen elke tekst, de cross-encoder berekent ook de blokken query-bij-titel, inclusief waar "not" en "Wireless" elkaar ontmoeten.

In een bi-encoder wordt de query gecodeerd voordat een document is gezien. Een cross-encoder ziet de query en de kandidaat samen en kan dus hun relatie op tokenniveau gebruiken. Dat kan helpen bij gevallen zoals:

  • Negatie: “headphones that are not wireless”. Een gepoolde embedding kan de negatie ondergewichten, terwijl gezamenlijke codering het model een directe query-documentinteractie geeft. Dit is een hypothese die je met een gerichte slice moet toetsen, geen garantie.
  • Kwalificatie: “laptop under $500”. Gezamenlijke codering kan de voorwaarde koppelen aan een prijs in de producttekst, maar gestructureerde prijsfilters zijn veiliger als het veld beschikbaar is.

De invoer van de cross-encoder heeft de vorm [CLS] query tokens [SEP] document tokens [SEP]. [CLS] is een classificatietoken waarvan de laatste hidden state via een lineaire head één relevantiescore oplevert. Segment embeddings onderscheiden querytokens van documenttokens, en [SEP] markeert de grens tussen segmenten.

Hoe cross-encoders worden getraind

Cross-encoders kunnen leren van (query, document, relevance_label)-voorbeelden met pointwise, pairwise of listwise doelstellingen. Een pointwise voorbeeld koppelt “wireless headphones” aan “Sony WH-1000XM5 Wireless Headphones” en het label 1.0. Een gangbare classifier zet de laatste [CLS]-representatie om in een score. Binaire labels kunnen binary cross-entropy gebruiken; gegradeerde relevantie kan regressie, ordinale, pairwise of listwise losses gebruiken. Kies op basis van held-out rankingmetrics, en ga er niet van uit dat één doelstelling altijd beter is.

Hard negative mining helpt ook bij het trainen van cross-encoders. Cross-encoders zijn duur om te trainen — elk trainingsvoorbeeld vraagt een volledige forward pass over de aaneengeschakelde reeks — dus je kunt je geen verspilde rekenkracht aan triviaal makkelijke negatives permitteren. Het praktische recept: gebruik een bi-encoder om voor elke trainingsquery de top-K kandidaten op te halen en haal de hard negatives uit specifieke rangbereiken (bijvoorbeeld rang 10–100). Zo krijgt de cross-encoder voorbeelden waarbij het onderscheid tussen relevant en irrelevant diepe tokeninteractie vraagt.

De opgehaalde kandidaten reranken

De fase stuurt (query, document)-paren voor de eerste 50 hybride kandidaten naar CrossEncoder.predict, zet die kandidaten op scorevolgorde en voegt daarna de niet-gescoorde kandidaten in hun vorige volgorde toe. De figuur sorteert vier van de 100 kandidaten op beide manieren en toont waarom de merge nieuwe rangscores toekent in plaats van de ruwe scores te behouden:

Gesorteerd op ruwe score is de volgorde 4.1, 0.016, 0.009, min 3.2, dus het kopdocument op rang 50 valt onder beide staartdocumenten; gesorteerd op rangscores 100, 51, 50, 1 blijven rang 1 tot 50 boven rang 51 tot 100.Gesorteerd op ruwe score is de volgorde 4.1, 0.016, 0.009, min 3.2, dus het kopdocument op rang 50 valt onder beide staartdocumenten; gesorteerd op rangscores 100, 51, 50, 1 blijven rang 1 tot 50 boven rang 51 tot 100.

# From src/search_ranking_stack/stages/s04_cross_encoder.py

def rank_scores(ordered_ids):
    return {doc_id: float(len(ordered_ids) - rank) for rank, doc_id in enumerate(ordered_ids)}

def merge_head_and_tail(original, scored_head):
    """original: hybrid (doc_id, rrf_score) pairs in rank order.
    scored_head: (doc_id, cross_encoder_score) pairs for the first len(scored_head)."""
    head = sorted(scored_head, key=lambda item: (-item[1], item[0]))
    tail = original[len(scored_head) :]
    return rank_scores([doc_id for doc_id, _ in head + tail])

De volledige fase voegt het laden van het model toe, de documentcutoff van 2,048 tekens en een controle dat elke score eindig is.

De modelkaart laat zien dat cross-encoder-scores negatief kunnen zijn. Meng ze niet met de RRF-scores van de ongescoorde staart: een positieve RRF-score kan een ongescoord staartdocument boven een negatief gescoord kopdocument sorteren. Ken strikt dalende rangscores toe, zoals rank_scores doet. Dit zijn rangscores voor evaluatie, geen gekalibreerde relevantieschattingen.

De afweging tussen snelheid en kwaliteit

Waarom niet overal cross-encoders voor gebruiken? Omdat je de scores niet vooraf kunt berekenen. De documentembeddings van een bi-encoder zijn onafhankelijk van de query, dus je berekent ze één keer en slaat ze op. De output van een cross-encoder hangt af van de query en het document samen. De relevantiescore voor “wireless headphones” naast een Sony-product komt voort uit de gezamenlijke self-attention tussen precies die tokens. Je kunt een herhaald query-documentpaar cachen, maar zijn score is niet te hergebruiken voor een andere query.

Een bi-encoder heeft één querycodering plus een vector search over vooraf berekende documentembeddings nodig. Een cross-encoder scoort elk query-documentpaar in de shortlist, met kosten die meegroeien met zowel het aantal kandidaten als de lengte van de reeks. Batching helpt, maar 100,000 kandidaten scoren blijft het verkeerde werkpunt; haal eerst op en benchmark de grootste shortlist die aan de doelen voor kwaliteit en latency voldoet.


LLM listwise reranking

De laatste demofase gebruikt een LLM voor listwise reranking. In plaats van elk document onafhankelijk te scoren, ziet het model de top 10 en geeft het een volgorde terug. De prompt is geïnspireerd op RankGPT en maakt relatieve vergelijking expliciet, maar brengt ook beperkingen van de context window, position bias, parsefouten en variatie tussen runs met zich mee.

Pointwise ranking doet één LLM-aanroep per document en sorteert de scores; listwise ranking doet één aanroep met alle kandidaten, accepteert de teruggegeven volgorde alleen als elke ID precies één keer voorkomt, en houdt anders de volgorde van de cross-encoder aan.Pointwise ranking doet één LLM-aanroep per document en sorteert de scores; listwise ranking doet één aanroep met alle kandidaten, accepteert de teruggegeven volgorde alleen als elke ID precies één keer voorkomt, en houdt anders de volgorde van de cross-encoder aan.

De listwise prompt

De prompttemplate vraagt de LLM rekening te houden met de ESCI-relevantiehiërarchie:

# src/search_ranking_stack/stages/s05_llm_rerank.py

def _create_listwise_prompt(query, documents, max_words=200):
    n = len(documents)

    doc_texts = []
    for i, (_doc_id, doc_text) in enumerate(documents, start=1):
        words = doc_text.split()[:max_words]
        doc_texts.append(f"[{i}] {' '.join(words)}")

    docs_formatted = "\n\n".join(doc_texts)

    return (
        f"I will provide you with {n} product listings, each indicated by a numerical "
        f"identifier [1] to [{n}]. Rank the products based on their relevance to the "
        f'search query: "{query}"\n\n'
        "Consider:\n"
        "- Exact matches (product satisfies all query requirements) should rank highest\n"
        "- Substitutes (functional alternatives) should rank above complements\n"
        "- Completely irrelevant products should rank lowest\n\n"
        f"{docs_formatted}\n\n"
        "Rank the products from most relevant to least relevant.\n"
        "Output ONLY a comma-separated list of product identifiers, e.g.: [3], [1], [2], ...\n"
        "Do not explain your reasoning. Only output the ranking."
    )

Drie uitvoeringsmodi

De demo ondersteunt drie backends voor LLM reranking:

ModusModelHoe het draait
ollamallama3.2:3b (wijzig met OLLAMA_MODEL)Lokaal via de Ollama API
apiClaude Haiku (stel LLM_MODEL_API in config.py in)Anthropic API; eerst uv sync --extra api
localQwen/Qwen2.5-1.5B-InstructHuggingFace Transformers; eerst uv sync --extra llm

Parsing en fallback

De parser haalt de genummerde identifiers uit het antwoord van het model. Hij accepteert een ranking alleen als die elke kandidaat precies één keer noemt:

# src/search_ranking_stack/stages/s05_llm_rerank.py

def _parse_ranking(output: str, n: int) -> list[int] | None:
    """Return 0-based positions if the output ranks IDs 1..n exactly once."""
    positions = [int(m) - 1 for m in re.findall(r"\[(\d+)\]", output)]

    # Reject missing, duplicate, and out-of-range IDs.
    if sorted(positions) != list(range(n)):
        return None

    return positions

Als de parser None teruggeeft, behoudt de demo voor die query de volledige volgorde van de cross-encoder en telt een fallback. Een fout in de backend telt ook als fallback. run-all print het fallbackpercentage en slaat het naast de metrics op. Bijvoorbeeld: [3] > [1] > [3] wordt afgewezen: ID 3 komt twee keer voor en ID 2 en 4 tot en met 10 ontbreken.


Resultaten van deze ESCI-sample

De resultaten gebruiken de hierboven beschreven small_version-sample; die is niet held-out. De rijen komen uit één run van uv run run-all --llm-mode ollama met llama3.2:3b als LLM.

FaseNDCG@10MRRRecall@100NDCG-delta
BM250.5850.8120.741—
Dense Bi-Encoder0.6110.8080.825+0.026
Hybrid (RRF)0.6280.8340.842+0.017
+ Cross-Encoder0.6990.8860.842+0.071
+ LLM Reranker (13.4% fallback)0.6990.8840.842+0.000

De grafiek zet de Recall@100 van elke rij uit tegen de NDCG@10:

BM25, dense en hybride RRF schuiven naar rechts naarmate Recall@100 stijgt van 0.741 naar 0.842; de cross-encoder gaat recht omhoog van NDCG@10 0.628 naar 0.699, en de LLM-reranker blijft op hetzelfde punt.BM25, dense en hybride RRF schuiven naar rechts naarmate Recall@100 stijgt van 0.741 naar 0.842; de cross-encoder gaat recht omhoog van NDCG@10 0.628 naar 0.699, en de LLM-reranker blijft op hetzelfde punt.

Wat de vastgelegde resultaten ondersteunen

Hybrid search verslaat elke methode afzonderlijk. Hybride RRF haalt een NDCG@10 van 0.628, boven BM25 (0.585) en Dense (0.611). Het heeft ook de hoogste Recall@100 van de drie retrievalrijen (0.842). De twee methoden kunnen op verschillende queries falen, en fusion haalt documenten terug die elk van beide alleen mist.

De cross-encoder geeft de grootste winst. Hij tilt NDCG@10 van 0.628 naar 0.699 en MRR van 0.834 naar 0.886, terwijl hij maar 50 kandidaten per query scoort. In de top 10 stijgt het aandeel Exact-producten van 36.0% (hybride) naar 41.4%.

In deze demo wordt recall bij retrieval bepaald. Beide rerankers behouden alle 100 hybride kandidaten, dus Recall@100 blijft op 0.842. Wil je die verhogen, verander dan welke 100 kandidaten retrieval teruggeeft.

Een kleine lokale LLM voegt op deze sample niets toe. llama3.2:3b laat NDCG@10 op 0.699 en verlaagt MRR iets. Bij 67 van 500 antwoorden (13.4%) faalde de parser en bleef de volgorde van de cross-encoder staan, dus de LLM-volgorde is op 433 queries toegepast. Een groter model of de API-modus kan zich anders gedragen; meet het op dezelfde manier, met het fallbackpercentage naast de score.

Dense retrieval verslaat BM25 op deze sample. Bekijk slices van queries voordat je een oorzaak toewijst. Vocabulary mismatch is één aannemelijke factor, maar ook de opbouw van de sample, de tokenizer, het trainingsdomein van het model en de corpusvelden beïnvloeden de vergelijking.


Dense vectors indexeren buiten de demo

Dense embeddings worden op schaal pas nuttig met een Approximate Nearest Neighbor (ANN)-index. De demo gebruikt brute-force cosine similarity (prima bij 9,870 documenten), maar productiesystemen hebben gespecialiseerde indexen nodig. De twee gangbare families zoeken op verschillende manieren, en elk heeft één zoekinstelling die recall inruilt tegen queryrekenwerk. De figuur toont het pad dat HNSW volgt en de clusters die IVF scant:

HNSW volgt één pad van pijlen van het ingangspunt in de bovenste laag naar het dichtstbijzijnde knooppunt in de onderste laag; IVF scant de twee clusters die het dichtst bij de query liggen, van de zes.HNSW volgt één pad van pijlen van het ingangspunt in de bovenste laag naar het dichtstbijzijnde knooppunt in de onderste laag; IVF scant de twee clusters die het dichtst bij de query liggen, van de zes.

HNSW (hierarchical navigable small world)

HNSW bouwt een graaf met meerdere lagen: schaarse bovenlagen navigeren grof en dichtere onderlagen verfijnen de omgeving. M bepaalt de connectiviteit van de graaf, terwijl efSearch queryrekenwerk inruilt tegen recall. Bruikbare waarden hangen af van de dimensie, de verdeling van afstanden, filters, de implementatie en de gewenste recall.

Graafindexen kunnen recall verliezen bij veel updates of strenge filters; één Qdrant-issue laat zien dat gefilterde recall pas na een rebuild herstelt. Test je update- en filterpatroon, inclusief de tijd voor een rebuild.

IVF (inverted file)

IVF-indexen verdelen de vectorruimte in clusters en scannen daarna de nprobe clusters die het dichtst bij de query liggen. Ze kunnen een bruikbare afweging tussen geheugen, buildtijd en recall bieden, vooral in combinatie met compressie. Updategedrag en prestaties hangen af van de implementatie en niet alleen van de indexfamilie.

RaBitQ (Gao & Long, SIGMOD 2024) is een gerandomiseerde quantizer die met IVF kan worden gecombineerd. Hij normaliseert gecentreerde vectoren, past een willekeurige orthogonale rotatie toe en codeert tekens met één bit per dimensie. Voor de schatting van afstanden gebruikt hij ook correctiefactoren en vectornormen; de foutgrenzen uit het paper gelden voor die schatter en zijn aannames. Gebruik hem wanneer geheugen de beperking is en de gekwantiseerde afstandsschattingen nog steeds je recalldoel halen.

DimensieHNSW-graafIVF-clusters
QuerycontroleefSearchnprobe
BuildcontroleConnectiviteit en constructiebeamAantal clusters en trainingssample
GeheugenprofielGraafranden plus vectorenCentroids, lijsten en opgeslagen vectoren
UpdategedragDatabasespecifiek herstel/opschoningDatabasespecifiek onderhoud van lijsten
Meestal gekozen alsVectoren in RAM passen en je lage latency bij hoge recall nodig hebtGeheugen of corpusgrootte de beperking is; combineer met PQ of RaBitQ

Meet voor beide indexen de curve van recall, latency en geheugen op je eigen data en querybelasting.

Uber verlaagde latency en CPU door het aantal kandidaten per shard te verlagen, met weinig recallverlies; stel dat aantal af op productie-achtig verkeer.


Optionele uitbreidingen na de kernpipeline

Zodra retrieval en reranking aparte metingen hebben, zijn verschillende uitbreidingen makkelijker te evalueren zonder de kernpipeline te vertroebelen.

Query-understanding

Query-expansie en herschrijven kunnen vocabulary mismatch voor retrieval aanpakken. Query2doc genereerde pseudodocumenten en meldde BM25-winst in zijn MS MARCO-experimenten. Expansie kan ook de verkeerde intentie introduceren, dus vergelijk recall en precisie op slices met dubbelzinnige, navigerende en exacte-identifierqueries.

Praktische patronen: afkortingen uitschrijven, entiteiten verrijken, sub-querydecompositie voor multi-hop reasoning en RAG-Fusion — meerdere queryvarianten genereren en de resultaten via RRF combineren.

Relevantielabels met hulp van een LLM

LLMs kunnen relevantielabels voorstellen wanneer menselijke beoordelingen schaars zijn. Het werk van Pinterest aan relevantielabeling beschrijft hoe trainingsdata verder wordt uitgebreid dan menselijke labels en hoe een op een LLM gebaseerd relevantiemodel wordt gedistilleerd voor serving. Een LLM-label blijft modeloutput: kalibreer het tegen geblindeerde menselijke beoordelingen, bekijk slices met onenigheid en bewaar een menselijke gouden set voor regressietests.

Nuttige controles zijn onder meer:

  • een rubric met concrete relevantiegrenzen en voorbeelden;
  • geblindeerde menselijke calibration en periodieke hercontroles;
  • willekeurige volgorde en herhaalde beoordelingen voor onstabiele gevallen;
  • modelpanels waar hun extra kosten de overeenstemming verbeteren; en
  • expliciete controles op position bias en neiging naar het midden.

Knowledge distillation

Wanneer een LLM-teacher waarde toevoegt maar niet aan de serving-eisen voldoet, is distillatie één optie:

  1. Gebruik een krachtige LLM (de teacher) om duizenden trainingsqueries offline te rerankeren; alleen de kleine student draait per verzoek
  2. Train een kleine, snelle cross-encoder (de student, ~100M–200M parameters) om de rankingverdeling van de LLM na te bootsen
  3. Vergelijk de student met zowel de teacher als de baseline op kwaliteit, calibration en servingkosten

InRanker distilleert MonoT5-3B tot models met 60M en 220M parameters, respectievelijk 50x en ongeveer 13x kleiner, met betere out-of-domain effectiviteit dan hun niet-gedistilleerde versies. De aanpak van Rank-Without-GPT levert open-source listwise rerankers van 7B op die 97% halen van de effectiviteit van de vergeleken GPT-4-reranker, gemeten in nDCG@10 op TREC-DL-19 en TREC-DL-20, met QLoRA-fine-tuning.

Gepubliceerde compressieresultaten zijn startpunten, geen verwachte productieverhoudingen. Distillatie kan de vooroordelen van de teacher erven en kwaliteit verliezen op zeldzame slices van queries, dus houd de oorspronkelijke relevantiebeoordelingen in de evaluatielus.

Personalisatie en position bias

Een zoekopdracht naar “apple” moet iPhones teruggeven voor een techliefhebber en appelrecepten voor iemand die kookcontent bekijkt.

Een gangbare retrievalarchitectuur voor personalisatie gebruikt een two-tower embeddingmodel: de querytower codeert de query en de gebruikerscontext, terwijl de itemtower items en metadata codeert. De figuur toont welke kant per verzoek draait, welke kant offline wordt berekend en waar de query “apple” voor twee gebruikers terechtkomt:

Per verzoek gaan "apple" plus de geschiedenis van elke gebruiker door de querytower; offline gaan items door de itemtower naar een ANN-index; de queryvector van de kookgebruiker landt bij appelrecepten en die van de tech-liefhebber bij iPhones.Per verzoek gaan "apple" plus de geschiedenis van elke gebruiker door de querytower; offline gaan items door de itemtower naar een ANN-index; de queryvector van de kookgebruiker landt bij appelrecepten en die van de tech-liefhebber bij iPhones.

De offline/online-splitsing maakt approximate-nearest-neighbor-retrieval mogelijk; de latency ervan hangt nog steeds af van de encoder, de index, de filters en het servingsysteem. Drie productieontwerpen gebruiken geleerde embeddings op verschillende plekken. Airbnb leert listing-embeddings uit sessions met klikken van gebruikers en gebruikt de similarity met recent aangeklikte listings als features voor searchranking. Pinterests OmniSearchSage leert één query-embedding samen met pin- en productembeddings. Uber gebruikt een two-towermodel om een paar honderd winkels op te halen voor de Uber Eats-homefeed.

Klikdata bevat position bias en exposure bias. PAL is één aanpak om dat te corrigeren: train gezamenlijk een exposure-module en een module voor klik-gegeven-exposure, en serveer daarna de laatste zonder positie-invoer. Het is geen universele oplossing; gerandomiseerde interventies, inverse-propensity-methoden en counterfactual evaluation kunnen bij een ander product beter passen.

Domeinadaptatie met synthetische queries

Een veelgemaakte fout in de zoekstrategie is aannemen dat een model dat op algemene webdata is getraind (zoals MS MARCO) ook goed werkt op een gespecialiseerd domein. Dit is het out-of-domain-probleem (OOD).

Het genereren van synthetische queries kan het tekort aan gelabelde data verkleinen. InPars genereert queries voor documenten en filtert paren voor training. Generative Pseudo-Labeling (GPL) voegt een stap toe waarin een teacher labelt:

  1. Genereer queries uit het documentcorpus van het doeldomein.
  2. Haal voor elke gegenereerde query negatieve documenten op.
  3. Gebruik een cross-encoder-teacher om positieve en negatieve paren te scoren, wat een marge in relevantiescore oplevert.
  4. Train de dense retriever om die marges te benaderen.

Synthetische paren kunnen helpen wanneer echte queries schaars zijn, maar ze weerspiegelen de generator en de prompt. Ontdubbel ze, filter onaannemelijke queries en valideer op echt held-out verkeer.


Een reeks experimenten

Voeg complexiteit pas toe wanneer de vorige fase een gemeten fout laat zien. Elke stap noemt de fout die hem rechtvaardigt:

  1. Stel een baseline vast. Draai BM25 of het huidige lexicale systeem op een set beoordeelde queries. Leg recall, NDCG, latency en de querygroepen waarop het faalt vast.
  2. Haal ontbrekende kandidaten terug. Test dense retrieval en fusion wanneer de baseline relevante documenten mist. Stel de kandidaat-cutoff af op recall en kosten.
  3. Verbeter de volgorde. Voeg een cross-encoder toe wanneer relevante kandidaten aanwezig zijn maar te laag staan. Kies de grootte van zijn shortlist door kwaliteit en latency bij meerdere cutoffs te meten. In de demo verhoogde deze stap NDCG@10 van 0.628 naar 0.699.
  4. Pas je aan op het domein. Fine-tune of distilleer nadat generieke models stabiele domeinspecifieke fouten laten zien. Houd echte held-out beoordelingen gescheiden van synthetische trainingsdata.
  5. Test dure reranking. Probeer listwise of op reasoning gebaseerde reranking pas wanneer de extra kwaliteit herhaalde runs overleeft en de latency, kosten, privacyeisen en fallbackcomplexiteit rechtvaardigt. In de demo haalde llama3.2:3b deze test niet.

De startstack en de te verzamelen beoordelingen hangen af van het product:

ToepassingMogelijke startstackValideer
ProductzoekenBM25 + dense retrieval + RRF + cross-encoderAttribuutrecall, substituties, latency, zakelijke randvoorwaarden
DocumentatiezoekenHybrid retrieval + cross-encoderExacte identifiers, semantische vragen, versiefilters
Support deflectionHybrid retrieval + citatiecontrolesRetrievalrecall, grounding, onthouding van een antwoord
Marktplaats of listingsLexicale filters + dense retrieval + zakelijke rerankerBeschikbaarheid, versheid, beleid, diversiteit van verkopers
Klein intern corpusBM25-baseline, daarna een rerankerOf vocabulary mismatch een dense index rechtvaardigt
Juridisch of medisch zoeken met hoge inzetOp recall gerichte retrieval plus expertreviewDekking, herkomst, gekalibreerde onthouding van een antwoord

Houd bij elke vergelijking een echte held-out beoordelingsset gescheiden van de trainingsdata. Leg rankings per query, fouten, latency en kosten vast, zodat een totale winst terug te voeren is op de gewijzigde fase. Dure lagen hebben ook expliciet privacy- en fallbackbeleid nodig.


Onderzoeksrichtingen die je apart evalueert

Reasoning-rerankers en agents die search gebruiken zijn veelbelovend, maar beantwoorden andere vragen dan de demo voor productzoeken met vijf fasen.

Rerankers op basis van reasoning

Rank1 traint rerankers met reasoning-traces en meldt sterke resultaten op de BRIGHT-benchmark. Dat bewijs is relevant voor retrieval waarin veel reasoning nodig is, en geen directe voorspelling voor ESCI-productzoeken.

Vergelijk voor juridisch of wetenschappelijk zoeken reasoning-rerankers met sterke cross-encoder- en listwise baselines op expertbeoordelingen, citaties, latency en consistentie van fouten.

Search-o1 bestudeert een model dat tijdens multi-hop vraagbeantwoording extra zoekopdrachten uitvoert. Dit is een orchestration-probleem — queries genereren, stoppen, bewijs gebruiken en antwoorden evalueren — en geen extra rerankingfase. Evalueer het op correctheid van de einduitkomst en op ondersteuning door citaties, niet alleen op retrievalmetrics.


Referenties

Papers

Datasets en benchmarks

Models in de demo

Tools en platforms

Referenties uit de industrie

Demoproject