- Je berekent BM25 met expliciete termfrequentie, documentlengte en IDF-conventie.
- Je combineert twee ranglijsten met reciprocal rank fusion en bewaakt unieke kandidaat-IDs.
Probeer ook echte tekstvectoren in het embedding-lab.
Een productcode zoals XR-2 kan vooral een exacte woordmatch nodig hebben. Een omschrijving met andere woorden kan juist bij een geschikte embedding passen. Hybride zoeken combineert kandidaten uit lexicaal zoeken en dense retrieval.
De twee routes hebben verschillende scoreschalen. We bouwen daarom eerst een lexicale baseline en combineren daarna rangposities. De oefeningen meten eigen zoekberekeningen, geen commerciële zoekdienst of universele kwaliteitswinst.
1. Maak de tokentrace controleerbaar
Onze analysator gebruikt Unicode-casefolding en regex [^\W_]+ voor reeksen letters en cijfers. Leestekens en underscores vormen grenzen. Dubbele woorden blijven behouden, omdat termfrequentie ze straks nodig heeft.
text = 'LAMP-reset_2 Straße café café'
for token in lexical_tokens(text):
print(token)
# lamp
# reset
# 2
# strasse
# café
# café
Maak lexical_tokens voor maximaal 20000 Python-tekens; lege tekst mag. Gebruik voor query en documenten dezelfde analysator. Casefolding maakt hier Straße tot strasse, maar is geen stemming of synoniemenkennis.
Kapot en kapotte blijven verschillende tokens. Ook Unicode-normalisatie ontbreekt: samengevoegde en losse accentvormen kunnen verschillen. Een woordmatch hangt dus mede af van de gekozen tekstbewerking.
012. Verzadig herhaling en corrigeer lengte
BM25 weegt een term met een IDF-factor en een verzadigende frequentiebijdrage. N is het aantal documenten en dfₜ het aantal documenten met term t. We kiezen deze positieve IDF-conventie:
Voor één document is f de termfrequentie, L zijn tokenlengte en A de gemiddelde documentlengte. k₁ bepaalt verzadiging en b lengtecorrectie. Definieer K om de score leesbaar te houden:
Neem documenten lamp en lamp lamp lamp. N=2, df=2, lengtes 1 en 3 en gemiddelde 2 geven I=ln(1.2)≈.182322. Bij k₁=1.2 en b=.75 wordt K voor het korte document .75 en voor het lange 1.65.
De korte score is I×2.2/1.75≈.229204. De lange score is I×6.6/4.65≈.258779. Driemaal dezelfde term levert dus geen verdrievoudigde score op.
docs = ['lamp','lamp lamp lamp']
print(np.round(
bm25_scores(docs,'lamp'),4))
# [0.2292 0.2588]
print(np.round(
bm25_scores(docs,'lamp',b=0),4))
# [0.1823 0.2865]
Bij b=0 verdwijnt de lengtecorrectie, maar de frequentie blijft verzadigen. Maak bm25_scores met een som over unieke querytermen. Herhaling van een queryterm krijgt in deze oefening geen extra gewicht.
same = bm25_scores(docs,'lamp lamp')
print(np.allclose(
same,bm25_scores(docs,'lamp')))
# True
print(np.round(bm25_scores(
['lamp','timer'],'lamp timer'),4))
# [0.6931 0.6931]
Bereken alleen bijdragen waar f>0; anders kan k₁=0 een ongeldige nuldeling veroorzaken. Lege documenten tellen mee in N en gemiddelde lengte; een volledig leeg corpus geeft nulscores. Gebruik de afgesproken parametergrenzen en weiger ongeldige invoer.
De IDF-conventie en gebruikelijke startwaarden komen uit Lucene, maar onze analysator en scoreberekening zijn een eigen variant. Dit is geen exacte reproductie van Lucene’s volledige implementatie. Beoordeel parameterkeuzes op ontwikkeldata en houd de eindtest apart.
023. Combineer rang in plaats van ruwe score
Reciprocal rank fusion (RRF) geeft de eerste hit rang één. Elke aanwezige kandidaat krijgt per lijst een bijdrage; een ontbrekende kandidaat krijgt uit die lijst nul.
r loopt over ranglijsten en c is een positieve constante. Met lijsten [a,b] en [b,c] en didactisch c=1 krijgt a=.5, b=1/3+1/2≈.8333 en c=1/3. De kandidaat die beide routes ondersteunen wint.
rankings = [['a','b'],['b','c']]
for hit in reciprocal_rank_fusion(
rankings,1):
score = round(hit['score'],3)
print(hit['id'],score)
# b 0.833
# a 0.5
# c 0.333
more = reciprocal_rank_fusion(
rankings+[['c']])
print([h['id'] for h in more])
# ['b', 'c', 'a']
Maak reciprocal_rank_fusion voor 1–8 lijsten. IDs moeten binnen iedere lijst uniek zijn, maar mogen in verschillende lijsten voorkomen. Sorteer gelijke totaalscores alfabetisch; gebruik standaard c=60.
De oorspronkelijke publicatie koos 60 na een pilot, geen universeel optimum. RRF gebruikt geen absolute scoreafstand en maakt een zwakke zoekroute niet vanzelf betrouwbaar. Meet de gecombineerde relevante hits op echte ontwikkelvragen.
Iedere lijst geeft 1 / (c + rang). De ruwe BM25- en cosinusscores zijn niet nodig.
- code
- reset
- garantie
- reset
- veiligheid
- code
De balken zijn geschaald naar de hoogste score in deze run. Dit zijn rangbijdragen, geen betrouwbaarheidskansen.
Wat neem je mee?
- De analysator bepaalt welke woorden overeenkomen.
- BM25 verzadigt frequentie en corrigeert lengte volgens een expliciete conventie.
- RRF combineert rangbijdragen over alle lijsten.
Hierna onderzoek je welke gevonden kandidaten werkelijk bruikbaar zijn voor een antwoord.