Im Inneren von Vektordatenbanken für RAG: Blöckspeicherung, HNSW, IVF und Neubewertung
Wie RAG-Vektordatenbanken Blöcke speichern, Millionen von Vektoren mit HNSW und IVF durchsuchen, hybride Suchverfahren sowie Neubewertungsmethoden einsetzen und Maße für die Ähnlichkeit vergleichen.
Quellen: Inside Vector Databases for RAG: From Chunk Storage to HNSW & IVF Search
Ein retrieval-augmented generation System muss zunächst eine praktische Frage beantworten: Wo befinden sich die Textabschnitte? Die gängigste Antwort, die auch von den meisten Produktionsystemen verwendet wird, ist, jeden Abschnitt direkt neben seine Embedding-Daten in die Vektordatenbank zu speichern.
Wie ein gespeichertes Datensatz aussieht
In auf FAISS, Pinecone oder Weaviate basierenden Systemen enthält jeder Datensatz drei Elemente: den Embedding-Vektor, den Text des Abschnitts sowie einige Metadaten.
{
"embedding": [0.123, 0.456, ...],
"text": "Transformer models are powerful...",
"metadata": {
"doc_id": "doc1",
"page": 5
}
}
Das Zusammenhalten des Textabschnitts und seines Vektors hat drei Vorteile. Die Suche ist schnell, da der passende Text in derselben Antwort zurückgegeben wird. Die Architektur bleibt einfach, da kein zweiter Speicher abgefragt werden muss. Zudem bleibt die Latenz gering, was wichtig ist, wenn ein LLM in Echtzeit auf das Ergebnis wartet.
Die Abrufung erfolgt anschließend in fünf Schritten. Ein Benutzer stellt eine Frage, diese wird in einen Embedding umgewandelt, die Datenbank findet die nächstgelegenen gespeicherten Embeddings, der passende Textabschnitt wird direkt zurückgegeben, und das LLM formuliert seine Antwort anhand dieses Textes. Es gibt keine zusätzlichen Abfragen irgendwo sonst.
Warum die Suche schnell bleibt
Die übliche einzeilige Beschreibung besagt, dass eine Vektordatenbank ähnliche Embeddings mithilfe der Kosinusähnlichkeit, des Skalarprodukts oder der euklidischen Entfernung findet. Das ist zwar richtig, lässt aber die interessanten technischen Aspekte aus. Wie kann bei Millionen oder Milliarden von Vektoren eine Abfrage bereits in Millisekunden zurückgegeben werden?
Der naive Ansatz ist die Brute-Force-Methode: Man vergleicht die Abfrage mit jedem gespeicherten Vektor. Dadurch wird eine perfekte Genauigkeit erreicht, doch die Geschwindigkeit ist äußerst schlecht. Eine Million Vektoren bedeuten eine Million Vergleiche pro Abfrage, was sich nicht skalieren lässt.
Reale Systeme verwenden eine annähernde Suche nach dem nächsten Nachbarn (ANN). Dabei wird zunächst der Suchraum eingeschränkt, sodass nur ein kleiner Teil der Vektoren überprüft wird. Dadurch geht zwar etwas an Genauigkeit verloren, doch die Geschwindigkeit steigt erheblich. ANN wird mithilfe spezieller Indexstrukturen realisiert. FAISS bietet beispielsweise solche Indexe an, und dort sind sowohl HNSW als auch IVF verfügbar.
HNSW – ein schichtweises Graph
HNSW steht für Hierarchical Navigable Small World. Es handelt sich um einen graphbasierten Ansatz, den Pinecone und Weaviate intern verwenden. Jeder Vektor wird zu einem Knoten, und jeder Knoten verbindet sich mit ähnlichen Nachbarn. Eine Suche erfolgt dabei durch einen Weg durch diesen Graphen.
Das Diagramm weist mehrere Schichten auf. Die obere Schicht enthält nur sehr wenige Knoten und bietet einen groben Überblick, während die untere Schicht jeden Knoten detailliert darstellt. Ein Vergleich zwischen Autobahnen und lokalen Straßen eignet sich hier gut.
Die Erstellung verläuft wie folgt: Wenn ein Embedding eingefügt wird, erhält es eine zufällige Ebene, sodass die meisten Knoten nur in der unteren Schicht landen und nur wenige die obere Schicht erreichen. Innerhalb jeder Ebene verbindet sich der Knoten mit seinen nächstgelegenen Nachbarn, wobei dabei der Kosinus, das Skalarprodukt oder die euklidische Distanz herangezogen werden. Das Ergebnis ist ein Netzwerk, in dem nahe liegende Vektoren eng miteinander verbunden sind, während entfernte Vektoren nur locker miteinander verbunden sind.
Die Suche beginnt in der obersten Schicht. Die Abfrage findet dort den am besten passenden Knoten, wechselt von diesem Knoten aus in die nächste Schicht und wiederholt diesen prozess. Dies setzt sich bis nach unten fort. In den oberen und mittleren Schichten ist die Suche streng gierig: Es wird zum besten Nachbarn gewechselt und die Suche stoppt, wenn sich nichts mehr verbessern lässt, anschließend wird in niedrigere Schichten weitergegangen. Die unterste Schicht ist explorativer. Eine Prioritätswarteschlange mit Kandidaten beginnt am Eingangsknoten, und die Suche fügt weiterhin gute Treffer hinzu und prüft gleichzeitig mehrere Nachbarn.
Im Praxiseinsatz spielen zwei Einstellungen eine wichtige Rolle:
- M ist die Anzahl der Verbindungen pro Knoten. Ein höherer Wert für M erhöht die Trefferquote, kostet aber mehr Speicher und Zeit für den Aufbau.
- efSearch gibt an, wie viele Kandidaten während einer Abfrage untersucht werden. Ein höherer Wert verbessert die Trefferquote, führt jedoch zu höherer Latenz.
Einfach ausgedrückt prägt M die Qualität des Index, während efSearch bei Abfragen Genauigkeit gegen Geschwindigkeit eintauscht.
IVF – zuerst Clustering
IVF (umgekehrter Dateiindex) ist in FAISS weit verbreitet und verfolgt einen völlig anderen Ansatz. Es wird kein Graph erstellt, sondern es erfolgt Clustering, bei dem ähnliche Vektoren gruppiert werden und nur die relevanten Gruppen durchsucht werden. Eine Bibliothek ist ein gutes Beispiel: Die Bücher sind nach Kategorie sortiert, sodass niemand jeden Regalboden durchsuchen muss.
Beim Einfügen teilt ein Clustering-Algorithmus wie k-means die Vektoren in N Clustern auf. Jeder Cluster hat einen Schwerpunkt, und jedem Vektor wird der nächstgelegene zugeordnet. Bei einer Abfrage wird die Embedding-Datenstruktur mit allen Schwerpunkten verglichen, der nächstgelegene Cluster wird ausgewählt, und die Abfrage wird gegen jeden Vektor innerhalb dieses Clusters bewertet, um die besten Ergebnisse zurückzugeben.
Die beiden Methoden unterscheiden sich hinsichtlich des Speicherverbrauchs. IVF speichert weniger Verbindungen und verbraucht daher weniger Speicher. HNSW hingegen behält ein Graph mit vielen Kanten pro Knoten bei, wodurch mehr Speicher benötigt wird.
Wo reine Vektorabfragen versagen
Embeddings erfassen Bedeutung, Kontext und konzeptionelle Nähe. Bei exakten Schlüsselwörtern, Zahlen und IDs sind sie weniger wirksam. Wenn man nach „kafka error code 504“ sucht, kann ein Vektorindex möglicherweise „kafka network issues“ oder „timeouts in distributed systems“ zurückgeben, während das Dokument, das tatsächlich den Code 504 enthält, übersehen wird.
Produktionsreife RAG-Systeme begegnen diesem Problem mit einer hybriden Suchmethode, bei der semantische Suche (Embeddings in Kombination mit ANN-Methoden wie HNSW oder IVF) mit Schlüsselwortsuche für exakte Übereinstimmungen unter Verwendung von BM25 kombiniert wird. In einer typischen Architektur laufen die beiden Suchverfahren auf separaten Systemen, und ihre Ergebnisse werden zusammengeführt und neu rangiert.
Modernere Vektordatenbanken handhaben die hybride Suche intern, sodass kein zweites System erforderlich ist. Sowohl Weaviate als auch Pinecone unterstützen dies nativ. Sie wird zunehmend zur Standardlösung für Produktivumgebungen, da sie das Verständnis von Bedeutung mit der Abgleichung genauer Absichten verbindet.
Heißt „hybrid“ nicht, dass alles zweimal gespeichert wird?
Bei einer externen Konfiguration ja: Dasselbe Datenmaterial befindet sich oft sowohl in der Vektordatenbank als auch in einem Schlüsselwort-Engine wie Elasticsearch. Das ist beabsichtigt. Die Vektordatenbank verwendet einen HNSW- oder IVF-Index für semantische Ähnlichkeit, während die Schlüsselwort-Engine einen umgekehrten Index für präzisen Abgleich verwendet. Jeder Index dient einem eigenen Zweck, daher handelt es sich nicht um Redundanz.
Teams halten in der Regel eine einzige Quelle der Wahrheit, wie eine Datenbank oder ein Data Lake, und synchronisieren diese über Pipelines in beide Systeme. Dadurch bleiben beide Systeme konsistent, während die Abfrage schnell und präzise bleibt.
Neu-Ranking der obersten Ergebnisse
Viele Ingenieure gehen davon aus, dass die Arbeit abgeschlossen ist, sobald die Ergebnisse aus der Vektordatenbank zurückkommen. Das ist nicht der Fall. Diese Ergebnisse sind nur annähernd relevant, nicht perfekt geordnet, und eine feinere Beurteilung der Relevanz kann übersehen werden.
Nehmen wir die Abfrage „kafka error 504 fix“. Die Ergebnisse aus dem top-K könnten „Kafka Timeout-Probleme“, anschließend „Fehler in verteilten Systemen“ und dann „Fehlercode 504 Gateway Timeout“ sein. Das dritte Ergebnis sollte eigentlich an erster Stelle stehen, doch die Vektorsuche schafft das nicht immer.
Durch Neu-Ranking werden die abgerufenen Ergebnisse unter Verwendung einer tieferen Analyse der Abfrage neu geordnet. Vektordatenbanken erledigen das in der Regel nicht selbst, weshalb die Logik in der Anwendungslogik implementiert werden muss. Es gibt zwei vorherrschende Ansätze:
- Re-Ranking auf Basis von LLMs. Senden Sie die Abfrage sowie die Fragmente an ein LLM und bitten Sie es, sie nach Relevanz zu rangieren. Die Umsetzung ist einfach, und das Modell versteht den Kontext gut. Nachteilig ist jedoch die Langsamkeit aufgrund der zusätzlichen Anfrage sowie die steigenden Kosten bei größeren Skalen.
- Cross-Encoder. Dies ist der Standard für hochwertiges Re-Ranking. Das Modell nimmt die Abfrage zusammen mit einem Fragment als Paar entgegen und gibt einen Relevanzwert aus. Diesen Vorgang wiederholen Sie für alle Top-K-Kandidaten und sortieren diese anschließend nach dem Wert. Dieser Ansatz ist deterministisch und erheblich genauer.
Ein kompaktes Beispiel für einen Cross-Encoder mit LangChain:
"""Super-simple CrossEncoder reranking example.
Steps:
1. Define a query and a few short documents.
2. Build (query, doc) pairs.
3. Use a CrossEncoder to get a relevance score for each pair.
4. Print raw scores, then print documents sorted by score.
"""
from sentence_transformers import CrossEncoder
def main() -> None:
# 1. Create model
model_name = "cross-encoder/ms-marco-MiniLM-L-6-v2"
print(f"Loading CrossEncoder model: {model_name}\n")
model = CrossEncoder(model_name)
# 2. Query and documents
query = "What is a vector database?"
documents = [
"A vector database stores embeddings and allows similarity search.",
"Relational databases store structured data in tables.",
"FAISS is a library for efficient similarity search of vectors.",
"Vector databases are used in AI applications like RAG.",
]
# 3. Build (query, doc) pairs
pairs = [(query, doc) for doc in documents]
# 4. Get scores
scores = model.predict(pairs)
print("Query:\n " + query + "\n")
print("Raw scores (higher = more relevant):")
for doc, score in zip(documents, scores):
print(f" score={score:.4f} | doc={doc}")
# 5. Sort by score (descending)
ranked = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True)
print("\nDocuments sorted by cross-encoder score:\n")
for rank, (doc, score) in enumerate(ranked, start=1):
print(f"Rank {rank}: score={score:.4f}")
print(f" {doc}\n")
if __name__ == "__main__":
main()
Output
****************************************************
Query:
What is a vector database?
Raw scores (higher = more relevant):
score=8.4591 | doc=A vector database stores embeddings and allows similarity search.
score=-7.1590 | doc=Relational databases store structured data in tables.
score=1.0106 | doc=FAISS is a library for efficient similarity search of vectors.
score=7.0736 | doc=Vector databases are used in AI applications like RAG.
Documents sorted by cross-encoder score:
Rank 1: score=8.4591
A vector database stores embeddings and allows similarity search.
Rank 2: score=7.0736
Vector databases are used in AI applications like RAG.
Rank 3: score=1.0106
FAISS is a library for efficient similarity search of vectors.
Rank 4: score=-7.1590
Relational databases store structured data in tables.
Ähnlichkeitsmessung
Die Qualität der Informationsabrufung hängt maßgeblich davon ab, wie die Vektornähe berechnet wird. Drei Maße decken fast alles ab.
Kosinusähnlichkeit betrachtet den Winkel zwischen zwei Vektoren, also ausschließlich die Richtung. Zwei Pfeile, die in dieselbe Richtung zeigen, sind sehr ähnlich, während entgegengesetzte Pfeile unabhängig von ihrer Länge völlig unterschiedlich sind. Die Formel lautet cos(θ) = A·B / (‖A‖‖B‖). Bei der Abfrage „Kafka error 504“ würde ein Dokument über „Fehlerbehandlung bei Kafka“ eine hohe Punktzahl erzielen (etwa 0,9), während ein Dokument über „Datenbankindexierung“ eine niedrige Punktzahl hat (etwa 0,2). Sie eignet sich für Embeddings und ist die übliche Wahl in RAG, da die Bedeutung zählt und nicht die Größe. Probieren Sie Abfrage = [1, 1], docA = [2, 2] und docB = [1, 0]: docA zeigt in dieselbe Richtung wie die Abfrage, weshalb seine Kosinusähnlichkeit trotz seiner größeren Länge eine perfekte Übereinstimmung darstellt.
Skalarprodukt verbindet Richtung und Größe – A·B = |A| × |B| × cos(θ). Größere Vektoren gelten als wichtiger, und Vektoren, die in dieselbe Richtung zeigen, gelten als ähnlicher.
Euklidische Distanz misst die Entfernung in gerader Linie zwischen Punkten. Kürzere Entfernungen bedeuten größere Ähnlichkeit. Sie wird hauptsächlich für Clustering-Probleme verwendet.
Wie die Komponenten zusammenpassen
Ein guter RAG-Pipeline ist mehr als nur eine Vektordatenbank, die an ein LLM angebunden ist. Die Richtung in einem Embedding trägt Bedeutung, während die Größe die Signalstärke ausdrückt. FAISS, Pinecone und Weaviate nutzen HNSW und IVF, um in großem Umfang Daten abzurufen. Danach hängt die Qualität von weiteren Entscheidungen ab:
- Teilen Sie Dokumente sorgfältig in Abschnitte auf, damit der Kontext erhalten bleibt
- Verwenden Sie eine hybride Suche, um sowohl semantische als auch schlagwortbasierte Übereinstimmungen abzudecken
- Rangieren Sie die Ergebnisse erneut mithilfe eines Cross-Encoders oder eines LLMs
Der gesamte Ablauf umfasst: Daten, Aufteilung in Blöcke, Embeddings, Indexierung, Abruf, Neubewertung, Anweisungen, LLM, Antwort.