Hoppa till innehållet
AI-grafen
F· AI engineeringrag-informationssokning· ca 90 min· volatil — kontrolleras ofta· verifierad 2026-09-21

Omrankning med cross-encoders

Kunna lägga till en reranker och mäta precision@k före och efter.

Förkunskaper

Intuition

En bi-encoder kodar fråga och dokument var för sig och jämför vektorerna. Det gör det möjligt att förkoda hela indexet — men modellen ser aldrig fråga och dokument tillsammans.

En cross-encoder matar in dem ihop och ger ett relevanspoäng. Den ser hur orden i frågan förhåller sig till orden i dokumentet, och blir därför betydligt mer träffsäker.

Bi-encoderCross-encoder
Indatafråga och dokument separatihop, i samma sekvens
Kan förkoda indexjanej
Anrop per fråga1ett per dokument
Kvalitetgodbättre
1 M dokument~10 msflera timmar

Lösningen är tvåstegshämtning: låt bi-encodern hämta 50–100 kandidater brett och billigt, och låt cross-encodern ranka om dem. Du får den bättre modellens kvalitet till en kostnad som skalar med 50, inte med en miljon.

Formellt

Arkitekturen i praktiken:

fråga → BM25 + vektorsök → RRF → 50 kandidater → cross-encoder → topp 5 → LLM
         (~10 ms)                                 (~100 ms)

Vad det är värt. Typiska förbättringar av nDCG@10 vid omrankning av topp-50 ligger på 10–20 % relativt. Den avgörande effekten är dock en annan: precisionen i topp-3 stiger kraftigt, och det är de dokumenten som faktiskt hamnar i LLM-promptens kontext.

Tre familjer av rerankers:

TypHurKostnad
Cross-encoder (BGE, mxbai, Cohere Rerank)ett anrop per parmedel
ColBERT (sen interaktion)tokenvis likhet, förkodningsbarlåg, men större index
LLM som domarebe en modell rangordnahög

ColBERT är en intressant mellanväg: den behåller en vektor per token och beräknar maxlikhet per frågetoken. Det ger nästan cross-encoderkvalitet med förkodning — till priset av ett tio till hundra gånger större index.

Hur många kandidater ska rankas om? Det är en avvägning:

KandidaterRecall från steg 1Latens
20kan missa rätt dokumentlåg
50oftast rätt balans~100 ms
200nästan alltid medflera hundra ms

Grundregeln: en reranker kan bara förbättra det som steg 1 hämtade. Är recall@50 bara 0,7 kan ingen omrankning i världen komma över det — då är det hämtningen som ska förbättras, inte rankningen.

Mät alltid båda stegen separat:

MåttBerättar
recall@50 (steg 1)taket för hela systemet
nDCG@10 före omrankningutgångsläget
nDCG@10 eftervinsten
precision@3vad som faktiskt hamnar i prompten
Latens p50 och p95kostnaden
Andel frågor som blev sämreregressioner

Den sista raden gäller här som vid hybridsökning: ett förbättrat medelvärde kan dölja att en femtedel av frågorna försämrats.

Kod

import time
import numpy as np
from sentence_transformers import CrossEncoder, SentenceTransformer

bi = SentenceTransformer("intfloat/multilingual-e5-base")
cross = CrossEncoder("BAAI/bge-reranker-v2-m3", max_length=512)

def hamta(fraga, index, k=50):
    q = bi.encode(f"query: {fraga}", normalize_embeddings=True)
    return index.search(q, k)                      # [(dok_id, text, poang), ...]

def ranka_om(fraga, kandidater, topp=5):
    par = [(fraga, k["text"]) for k in kandidater]
    poang = cross.predict(par, batch_size=32)
    ordning = np.argsort(-poang)
    return [kandidater[i] | {"reranker_poang": float(poang[i])} for i in ordning[:topp]]

def tva_stegs(fraga, index, hamta_k=50, topp=5):
    return ranka_om(fraga, hamta(fraga, index, hamta_k), topp)

# Mät båda stegen separat — och regressionerna
def utvardera(facit, index, hamta_k=50):
    tak, fore, efter, samre = 0, 0, 0, 0
    latens = []
    for f in facit:
        kand = hamta(f["fraga"], index, hamta_k)
        ids = [k["id"] for k in kand]
        tak += int(f["ratt"] in ids)                        # taket: recall@hamta_k
        fore += int(f["ratt"] in ids[:3])
        t0 = time.perf_counter()
        omrankad = [k["id"] for k in ranka_om(f["fraga"], kand, topp=3)]
        latens.append((time.perf_counter() - t0) * 1000)
        traff_efter = f["ratt"] in omrankad
        efter += int(traff_efter)
        samre += int((f["ratt"] in ids[:3]) and not traff_efter)
    n = len(facit)
    latens.sort()
    return {
        f"recall@{hamta_k} (taket)": round(tak / n, 3),
        "precision@3 före": round(fore / n, 3),
        "precision@3 efter": round(efter / n, 3),
        "regressioner": round(samre / n, 3),
        "reranker_p50_ms": round(latens[n // 2], 1),
        "reranker_p95_ms": round(latens[int(0.95 * n) - 1], 1),
    }

# Hur många kandidater lönar sig? Kör kurvan på egen data.
def svep_kandidater(facit, index, kandidater=(10, 20, 50, 100, 200)):
    for k in kandidater:
        r = utvardera(facit, index, hamta_k=k)
        print(f"  k={k:>3}: tak {r[f'recall@{k} (taket)']:.3f}  "
              f"p@3 {r['precision@3 efter']:.3f}  p95 {r['reranker_p95_ms']:>6.1f} ms")

Den första raden i utvärderingen är den viktigaste. Ligger recall@50 på 0,70 är 0,70 taket för hela systemet — rerankern kan aldrig hitta något som steg 1 inte hämtade. Är den siffran låg är det hämtningen som ska förbättras först.

Behärskning innebär

  • Förklarar skillnaden mellan bi- och cross-encoder
  • Sätter upp tvåstegshämtning
  • Mäter effekten och kostnaden

Logga in för att göra övningarna och bygga upp din behärskning.

Källor

Alla källor och licenser