Wenn Sie eine Suchanfrage in eine Suchmaschine eingeben, muss etwas entscheiden, welche Dokumente tatsächlich related sind – und wie sie eingestuft werden. BM25 (Greatest Matching 25)der Algorithmus, der Suchmaschinen wie Elasticsearch und Lucene antreibt, ist seit Jahrzehnten die vorherrschende Antwort auf diese Frage.
Es bewertet Dokumente anhand von drei Dingen: wie oft Ihre Suchbegriffe in einem Dokument vorkommen, wie selten diese Begriffe in der gesamten Sammlung vorkommen und ob ein Dokument ungewöhnlich lang ist. Das Schlaue daran ist, dass BM25 Key phrase-Stuffing nicht belohnt – ein 20-mal vorkommendes Wort macht ein Dokument aufgrund der Begriffshäufigkeitssättigung nicht 20-mal relevanter. Aber BM25 hat einen grundlegenden blinden Fleck: Es findet nur die Wörter, die Sie eingegeben haben, nicht das, was Sie gemeint haben. Suchen nach „Ähnliche Inhalte ohne genaue Wortüberschneidung finden“ und BM25 erwidert einen leeren Blick.
Das ist genau die Lücke, die Retrieval-Augmented Era (RAG) mit Vektoreinbettungen wurde zum Füllen entwickelt – durch übereinstimmende Bedeutung, nicht nur durch Schlüsselwörter. In diesem Artikel erklären wir, wie jeder Ansatz funktioniert, wo jeder gewinnt und warum Produktionssysteme zunehmend beide zusammen verwenden.
So funktioniert BM25
Im Kern weist BM25 jedem Dokument in der Sammlung für eine bestimmte Abfrage eine Relevanzbewertung zu und ordnet die Dokumente dann nach dieser Bewertung. Für jeden Begriff in Ihrer Anfrage stellt BM25 drei Fragen: Wie oft kommt dieser Begriff im Dokument vor? Wie selten ist dieser Begriff in allen Dokumenten? Und ist dieses Dokument ungewöhnlich lang? Der Endwert ist die Summe der gewichteten Antworten auf diese Fragen über alle Suchbegriffe hinweg.
Beim Begriff Frequenzkomponente wird BM25 intelligent. Anstatt rohe Vorkommnisse zu zählen, gilt es Sättigung — Die Punktzahl steigt zunächst schnell an, flacht aber mit zunehmender Häufigkeit ab. Ein Begriff, der fünfmal vorkommt, trägt viel mehr dazu bei als ein Begriff, der einmal vorkommt, aber ein Begriff, der 50-mal vorkommt, trägt kaum mehr bei als einer, der 20-mal vorkommt. Dies wird durch den Parameter gesteuert k₁ (normalerweise zwischen 1,2 und 2,0 eingestellt). Stellen Sie den Wert niedrig ein und die Sättigung setzt schnell ein; Stellen Sie es hoch ein, dann ist die Rohfrequenz wichtiger. Diese einzige Designauswahl macht BM25 resistent gegen Key phrase-Stuffing – die hundertfache Wiederholung eines Wortes in einem Dokument bringt keinen Einfluss auf die Punktzahl.
Längennormalisierung und IDF
Der zweite Tuning-Parameter, B (normalerweise 0,75) steuert, wie stark die Länge eines Dokuments bestraft wird. Ein langes Dokument enthält natürlich mehr Wörter, sodass die Wahrscheinlichkeit größer ist, dass Ihr Suchbegriff darin enthalten ist – nicht weil er relevanter ist, sondern einfach weil er länger ist. BM25 vergleicht die Länge jedes Dokuments mit der durchschnittlichen Dokumentlänge in der Sammlung und skaliert den Begriffshäufigkeitswert entsprechend herunter. Einstellung b = 0 deaktiviert diese Strafe vollständig; b = 1 Wendet die vollständige Normalisierung an.
Endlich, IDF (Inverse Doc Frequency) stellt sicher, dass seltene Begriffe mehr Gewicht haben als häufige. Wenn das Wort „Abruf“ nur in 3 von 10.000 Dokumenten vorkommt, ist dies bei Übereinstimmung ein starkes Sign für Relevanz. Wenn das Wort „Die“ kommt in allen 10.000 vor, eine Übereinstimmung sagt Ihnen quick nichts. IDF ist es, was BM25 dazu bringt, auf die Wörter zu achten, die tatsächlich zwischen Dokumenten unterscheiden. Ein wichtiger Vorbehalt: Da BM25 ausschließlich auf der Häufigkeit von Begriffen basiert, ist ihm die Wortreihenfolge, der Kontext oder die Bedeutungsübereinstimmung nicht bekannt „Financial institution“ in einer Anfrage zu Finanzen und „Financial institution“ in einem Dokument über Flüsse sieht identisch aus mit BM25. Diese Beschränkung auf den Wortschatz ist von grundlegender Bedeutung und kein Abstimmungsproblem.




Wie unterscheidet sich BM25 von Vector Search?
BM25 und Vektorsuche beantworten die gleiche Frage – Welche Dokumente sind für diese Anfrage related? – aber aus grundlegend anderen Blickwinkeln. BM25 ist ein Key phrase-Matching-Algorithmus: Er sucht in jedem Dokument nach den genauen Wörtern Ihrer Suchanfrage, bewertet sie nach Häufigkeit und Seltenheit und ordnet sie entsprechend. Es hat kein Sprachverständnis – es sieht Textual content als eine Tüte voller Zeichen, nicht als Bedeutung.
Im Gegensatz dazu wandelt die Vektorsuche mithilfe eines Einbettungsmodells sowohl die Abfrage als auch jedes Dokument in dichte numerische Vektoren um und findet dann Dokumente, deren Vektoren in die gleiche Richtung wie der Abfragevektor zeigen – gemessen anhand der Kosinusähnlichkeit. Dies bedeutet, dass die Vektorsuche übereinstimmen kann „Herzstillstand“ zu einem Dokument über „Herzinsuffizienz“ Auch wenn sich keines der Wörter überschneidet, weil das Einbettungsmodell gelernt hat, dass diese Konzepte im semantischen Raum eng beieinander leben.
Der Kompromiss ist praktisch: BM25 erfordert kein Modell, keine GPU und keinen API-Aufruf – es ist schnell, leichtgewichtig und vollständig erklärbar. Die Vektorsuche erfordert ein Einbettungsmodell zur Index- und Abfragezeit, erhöht die Latenz und die Kosten und führt zu schwieriger zu interpretierenden Ergebnissen. Keines von beiden ist unbedingt besser; Sie scheitern in entgegengesetzter Richtung, und genau aus diesem Grund ist die Hybridsuche – die Kombination beider – zum Produktionsstandard geworden.




Vergleich von BM25 und Vektorsuche in Python
Installieren der Abhängigkeiten
pip set up rank_bm25 openai numpy
import math
import re
import numpy as np
from collections import Counter
from rank_bm25 import BM25Okapi
from openai import OpenAI
import os
from getpass import getpass
os.environ('OPENAI_API_KEY') = getpass('Enter OpenAI API Key: ')
Den Korpus definieren
Bevor wir BM25 und Vektorsuche vergleichen können, benötigen wir eine gemeinsame Wissensdatenbank zum Durchsuchen. Wir definieren 12 kurze Textabschnitte, die eine Reihe von Themen abdecken – Python, maschinelles Lernen, BM25, Transformatoren, Einbettungen, RAG, Datenbanken und mehr. Die Themen sind bewusst vielfältig: Einige Blöcke sind eng miteinander verbunden (BM25 und TF-IDF, Einbettungen und Kosinusähnlichkeit), während andere überhaupt nichts miteinander zu tun haben (PostgreSQL, Django). Diese Vielfalt macht den Vergleich aussagekräftig – eine intestine funktionierende Abrufmethode sollte die relevanten Teile aufdecken und das Rauschen ignorieren.
Dieser Korpus fungiert als unser Ersatz für einen echten Dokumentenspeicher. In einer Produktions-RAG-Pipeline würden diese Teile aus der Aufteilung und Bereinigung tatsächlicher Dokumente stammen – PDFs, Wikis, Wissensdatenbanken. Hier halten wir sie kurz und handlich, damit das Abrufverhalten leicht nachvollzogen und begründet werden kann.
CHUNKS = (
# 0
"Python is a high-level, interpreted programming language recognized for its easy and readable syntax. "
"It helps a number of programming paradigms together with procedural, object-oriented, and useful programming.",
# 1
"Machine studying is a subset of synthetic intelligence that allows programs to be taught from information "
"with out being explicitly programmed. Widespread algorithms embody linear regression, resolution bushes, and neural networks.",
# 2
"BM25 stands for Greatest Match 25. It's a bag-of-words retrieval perform utilized by serps "
"to rank paperwork primarily based on the question phrases showing in every doc. "
"BM25 makes use of time period frequency and inverse doc frequency with size normalization.",
# 3
"Transformer structure launched the self-attention mechanism, which permits the mannequin to weigh "
"the significance of various phrases in a sentence no matter their place. "
"BERT and GPT are each primarily based on the Transformer structure.",
# 4
"Vector embeddings signify textual content as dense numerical vectors in a high-dimensional area. "
"Comparable texts are positioned nearer collectively. This permits semantic search -- discovering paperwork "
"that imply the identical factor even when they use totally different phrases.",
# 5
"TF-IDF stands for Time period Frequency-Inverse Doc Frequency. It displays how essential a phrase is "
"to a doc relative to your complete corpus. Uncommon phrases get greater scores than frequent ones like 'the'.",
# 6
"Retrieval-Augmented Era (RAG) combines a retrieval system with a language mannequin. "
"The retriever finds related paperwork; the generator makes use of them as context to supply a solution. "
"This reduces hallucinations and permits the mannequin to quote sources.",
# 7
"Django is a high-level Python internet framework that encourages speedy growth and clear, pragmatic design. "
"It contains an ORM, authentication system, and admin panel out of the field.",
# 8
"Cosine similarity measures the angle between two vectors. A rating of 1 means similar course, "
"0 means orthogonal, and -1 means reverse. It's generally used to match textual content embeddings.",
# 9
"Gradient descent is an optimization algorithm used to reduce a loss perform by iteratively "
"shifting within the course of the steepest descent. It's the spine of coaching neural networks.",
# 10
"PostgreSQL is an open-source relational database recognized for its robustness and help for superior "
"SQL options like window features, CTEs, and JSON storage.",
# 11
"Sparse retrieval strategies like BM25 depend on precise key phrase matches and fail when the question makes use of "
"synonyms or paraphrases not current within the doc. Dense retrieval utilizing embeddings handles "
"this by matching semantic that means reasonably than floor type.",
)
print(f"Corpus loaded: {len(CHUNKS)} chunks")
for i, c in enumerate(CHUNKS):
print(f" ({i:02d}) {c(:75)}...")
Bau des BM25 Retriever
Mit dem definierten Korpus können wir den BM25-Index erstellen. Der Prozess besteht aus zwei Schritten: Tokenisierung und Indizierung. Die tokenize-Funktion schreibt den Textual content klein und teilt ihn in alle nicht alphanumerischen Zeichen auf – so wird „TF-IDF“ zu („tf“, „idf“) und „bag-of-words“ zu („bag“, „of“, „phrases“). Dies ist bewusst einfach: BM25 ist ein Bag-of-Phrases-Modell, daher gibt es keine Wortstammbildung, keine Stoppwortentfernung und keine sprachliche Vorverarbeitung. Jedes Wort wird als unabhängiges Token behandelt.
Sobald jeder Block tokenisiert ist, erstellt BM25Okapi den Index und berechnet Dokumentlängen, durchschnittliche Dokumentlängen und IDF-Scores für jeden eindeutigen Begriff im Korpus. Dies geschieht einmal beim Begin. Zur Abfragezeit tokenisiert bm25_search die eingehende Abfrage auf die gleiche Weise, ruft get_scores auf, um parallel einen BM25-Relevanzwert für jeden Block zu berechnen, sortiert dann die High-Okay-Ergebnisse und gibt sie zurück. Die Plausibilitätsprüfung unten führt eine Testabfrage aus, um zu bestätigen, dass der Index funktioniert, bevor wir mit dem Einbettungs-Retriever fortfahren.
def tokenize(textual content: str) -> record(str):
"""Lowercase and break up on non-alphanumeric characters."""
return re.findall(r'w+', textual content.decrease())
# Construct BM25 index over the corpus
tokenized_corpus = (tokenize(chunk) for chunk in CHUNKS)
bm25 = BM25Okapi(tokenized_corpus)
def bm25_search(question: str, top_k: int = 3) -> record(dict):
"""Return top-k chunks ranked by BM25 rating."""
tokens = tokenize(question)
scores = bm25.get_scores(tokens)
ranked = np.argsort(scores)(::-1)(:top_k)
return (
{"chunk_id": int(i), "rating": spherical(float(scores(i)), 4), "textual content": CHUNKS(i)}
for i in ranked
)
# Fast sanity examine
outcomes = bm25_search("how does BM25 rank paperwork", top_k=3)
print("BM25 take a look at -- question: 'how does BM25 rank paperwork'")
for r in outcomes:
print(f" ({r('chunk_id')}) rating={r('rating')} {r('textual content')(:70)}...")
Aufbau des Embedding Retrievers
Der Einbett-Retriever funktioniert in jedem Schritt anders als BM25. Anstatt Token zu zählen, wandelt es jeden Block mithilfe des Textual content-Embedding-3-Small-Modells von OpenAI in einen dichten numerischen Vektor um – eine Liste mit 1.536 Zahlen. Jede Zahl stellt eine Dimension im semantischen Raum dar, und Blöcke, die ähnliche Dinge bedeuten, haben am Ende Vektoren, die in ähnliche Richtungen zeigen, unabhängig von den verwendeten Wörtern.
Der Indexerstellungsschritt ruft die Einbettungs-API einmal professional Block auf und speichert die resultierenden Vektoren im Speicher. Dies ist der wesentliche Kostenunterschied zu BM25: Die Erstellung des BM25-Index ist reine Arithmetik auf Ihrem eigenen Pc, während die Erstellung des Einbettungsindex einen API-Aufruf professional Block erfordert und Vektoren erzeugt, die Sie speichern müssen. Für 12 Stücke ist das trivial; Bei einer Million Chunks wird dies zu einer echten Infrastrukturentscheidung.
Zum Zeitpunkt der Abfrage bettet embedding_search die eingehende Abfrage mit demselben Modell ein – das ist wichtig, die Abfrage und die Blöcke müssen sich im selben Vektorraum befinden – und berechnet dann die Kosinusähnlichkeit zwischen dem Abfragevektor und jedem gespeicherten Blockvektor. Die Kosinusähnlichkeit misst den Winkel zwischen zwei Vektoren: Ein Wert von 1 bedeutet identische Richtung, 0 bedeutet völlig unabhängig und destructive Werte bedeuten entgegengesetzte Bedeutung. Die Chunks werden dann nach dieser Punktzahl eingestuft und die High-k werden zurückgegeben. Auch hier wird die gleiche Plausibilitätsprüfungsabfrage aus dem BM25-Abschnitt ausgeführt, sodass Sie den ersten direkten Vergleich zwischen den beiden Ansätzen bei identischer Eingabe sehen können.
EMBED_MODEL = "text-embedding-3-small"
def get_embedding(textual content: str) -> np.ndarray:
response = consumer.embeddings.create(mannequin=EMBED_MODEL, enter=textual content)
return np.array(response.information(0).embedding)
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
# Embed all chunks as soon as (that is the "index construct" step in RAG)
print("Constructing embedding index... (one API name per chunk)")
chunk_embeddings = (get_embedding(chunk) for chunk in CHUNKS)
print(f"Performed. Every embedding has {len(chunk_embeddings(0))} dimensions.")
def embedding_search(question: str, top_k: int = 3) -> record(dict):
"""Return top-k chunks ranked by cosine similarity to the question embedding."""
query_emb = get_embedding(question)
scores = (cosine_similarity(query_emb, emb) for emb in chunk_embeddings)
ranked = np.argsort(scores)(::-1)(:top_k)
return (
{"chunk_id": int(i), "rating": spherical(float(scores(i)), 4), "textual content": CHUNKS(i)}
for i in ranked
)
# Fast sanity examine
outcomes = embedding_search("how does BM25 rank paperwork", top_k=3)
print("nEmbedding take a look at -- question: 'how does BM25 rank paperwork'")
for r in outcomes:
print(f" ({r('chunk_id')}) rating={r('rating')} {r('textual content')(:70)}...")
Aspect-by-Aspect-Vergleichsfunktion
Dies ist der Kern des Experiments. Die Vergleichsfunktion führt dieselbe Abfrage gleichzeitig über beide Retriever aus und gibt die Ergebnisse in einem zweispaltigen Format aus – BM25 auf der linken Seite, Einbettungen auf der rechten Seite – sodass die Unterschiede sofort an derselben Rangposition sichtbar sind.
def examine(question: str, top_k: int = 3):
bm25_results = bm25_search(question, top_k)
embed_results = embedding_search(question, top_k)
print(f"n{'═'*70}")
print(f" QUERY: "{question}"")
print(f"{'═'*70}")
print(f"n {'BM25 (key phrase)':<35} {'Embedding RAG (semantic)'}")
print(f" {'─'*33} {'─'*33}")
for rank, (b, e) in enumerate(zip(bm25_results, embed_results), 1):
b_preview = b('textual content')(:55).substitute('n', ' ')
e_preview = e('textual content')(:55).substitute('n', ' ')
similar = "⬅ similar" if b('chunk_id') == e('chunk_id') else ""
print(f" #{rank} ({b('chunk_id'):02d}) {b('rating'):.4f} {b_preview}...")
print(f" ({e('chunk_id'):02d}) {e('rating'):.4f} {e_preview}... {similar}")
print()
examine("BM25 time period frequency inverse doc frequency")
examine("what's RAG and why does it scale back hallucinations")
examine("cosine similarity between vectors")








Schauen Sie sich das an Vollständiges Notizbuch hier. Sie können uns auch gerne weiter folgen Twitter und vergessen Sie nicht, bei uns mitzumachen 120.000+ ML SubReddit und Abonnieren Unser Publication. Warten! Bist du im Telegram? Jetzt können Sie uns auch per Telegram kontaktieren.

