Hybridsökning och RRF
Kunna kombinera vektor- och nyckelordsträffar med reciprocal rank fusion.
Förkunskaper
Intuition
Vektorsökning och nyckelordssökning misslyckas på olika frågor. Det är precis därför de går att kombinera.
| Fråga | BM25 | Vektor |
|---|---|---|
| «felkod E404» | hittar | suddar ut den exakta strängen |
| «hur fixar jag att sidan inte finns» | missar | hittar |
| «Södertälje kommun diarienummer 2026-114» | hittar | missar |
| «vad kostar det att bo här» | missar omskrivningar | hittar |
En hybrid som tar båda listorna och slår ihop dem hittar allt det som vardera hittar — plus att den rankar upp det som båda är eniga om.
Problemet med att slå ihop poäng: BM25 ger tal mellan 0 och 30, cosinuslikhet mellan −1 och 1. De är inte jämförbara, och normalisering är godtycklig eftersom skalorna varierar med frågan.
RRF löser det genom att kasta bort poängen och bara använda ordningen.
Formellt
Tre egenskaper som gör den svår att slå:
- Ingen kalibrering behövs. Bara rangordningen används, så olika poängskalor spelar ingen roll.
- Konsensus belönas. Ett dokument som är tvåa i båda listorna slår ett som är etta i den ena och femma i den andra.
- Robust mot utstickare. En enskild lista kan inte dominera, eftersom bidraget är begränsat till .
Vad k gör: stora plattar ut skillnaden mellan rangerna och gör fusionen mer demokratisk; små ger topplaceringarna mer vikt. kommer från originalartikeln och fungerar förvånansvärt bra utan justering.
Alternativ och när de är bättre:
| Metod | När |
|---|---|
| RRF | standardvalet — inga poäng behövs |
| Viktad poängsumma | när du har kalibrerade poäng och tid att ställa in vikterna |
| Omrankning med cross-encoder | när kvalitet går före latens; slår ofta båda |
| Lärd fusion (LTR) | när du har klickdata i mängd |
Den mest effektiva uppsättningen i praktiken är tvåstegs: hämta brett med hybrid (RRF över BM25 och vektorsök), och ranka sedan om topp 50 med en cross-encoder. Den första delen är billig och har hög recall; den andra är dyr men körs bara på 50 dokument.
Utvärdera alltid hybriden mot varje del för sig. Ibland är den sämre — särskilt om den ena källan är mycket sämre än den andra, eftersom RRF då släpper in brus. Mät:
| Mått | Frågan |
|---|---|
| recall@k per källa | vad hittar var och en? |
| recall@k för hybriden | hittar den mer? |
| nDCG@10 | ligger det rätta högt upp? |
| Andel frågor där hybriden är sämre | finns en regression? |
Den sista raden är den som avslöjar när hybriden skadar: ett medelvärde kan förbättras samtidigt som en fjärdedel av frågorna blir sämre.
Kod
def rrf(rankningar: list[list[str]], k: int = 60, topp: int = 10) -> list[str]:
poang: dict[str, float] = {}
for lista in rankningar:
for pos, doc in enumerate(lista):
poang[doc] = poang.get(doc, 0.0) + 1.0 / (k + pos + 1)
return sorted(poang, key=lambda d: (-poang[d], d))[:topp]
# Konsensus vinner över en ensam förstaplats
text = ["a", "b", "c", "d"]
bild = ["d", "b", "e", "f"]
print(rrf([text, bild], topp=3)) # ['b', 'd', 'a']
# b är tvåa i båda → 1/62 + 1/62 = 0.03226
# d är etta och fyra → 1/61 + 1/64 = 0.03202
def hybrid_sok(fraga, bm25_sok, vektor_sok, hamta=50, topp=10, k=60):
b = bm25_sok(fraga, hamta)
v = vektor_sok(fraga, hamta)
return rrf([b, v], k=k, topp=topp)
# Utvärdera: hybriden MOT varje del för sig, och andelen regressioner
def utvardera(facit, bm25_sok, vektor_sok, k=10):
resultat = {"bm25": 0, "vektor": 0, "hybrid": 0}
samre = 0
for fall in facit:
b = bm25_sok(fall["fraga"], k)
v = vektor_sok(fall["fraga"], k)
h = rrf([b, v], topp=k)
traff = {"bm25": fall["ratt"] in b, "vektor": fall["ratt"] in v,
"hybrid": fall["ratt"] in h}
for namn, t in traff.items():
resultat[namn] += int(t)
if (traff["bm25"] or traff["vektor"]) and not traff["hybrid"]:
samre += 1
n = len(facit)
return {namn: round(v / n, 3) for namn, v in resultat.items()} | {
"regressioner": round(samre / n, 3)}
# Tvåstegs: bred hämtning med hybrid, sedan dyr omrankning av topp 50
def tva_stegs(fraga, bm25_sok, vektor_sok, cross_encoder, topp=10):
kandidater = hybrid_sok(fraga, bm25_sok, vektor_sok, hamta=50, topp=50)
poang = cross_encoder.predict([(fraga, d) for d in kandidater])
return [d for _, d in sorted(zip(poang, kandidater), reverse=True)][:topp]
regressioner i utvärderingen är det tal som oftast saknas: andelen frågor där någon av delarna hittade rätt men hybriden inte gjorde det. Stiger medelvärdet samtidigt som den siffran är 0,15 har du gjort en sjättedel av frågorna sämre.
Behärskning innebär
- Implementerar hybridsökning
- Motiverar RRF framför poängsammanslagning
- Utvärderar hybriden mot varje del för sig
Logga in för att göra övningarna och bygga upp din behärskning.
Källor
- Cormack m.fl. — Reciprocal Rank Fusion (SIGIR 2009) — abstrakt fritt; författarkopior finns
- Manning, Raghavan & Schütze — Introduction to Information Retrieval — fri att läsa online (författarnas utgåva)
- Qdrant — dokumentation (Apache-2.0) — Apache-2.0