Zum Hauptinhalt springen
Lektion 12 von 18 · Embeddings, RAG und semantische Suche

Vektor-Speicher mit FAISS

Lektion 12 von 18 Aktualisiert am 2. November 2026 Entwickler
Auf dieser Seite
  1. FAISS: Hochleistungs-Vektorsuche
  2. Erstellen Sie Ihren ersten Index
  3. Die richtige Indexart wählen
  4. Abfragen des Index
  5. Umstellung auf Kosinus-Ähnlichkeit
  6. Wenn das Korpus massiv wird
  7. Kernaussagen

FAISS: Hochleistungs-Vektorsuche

Sie haben Ihre Embeddings berechnet und gespeichert, doch eine NumPy-Matrix ist kein Suchmaschinen-System: Der Vergleich einer Abfrage mit 100.000 Vektoren mittels einer Python-Schleife würde mehrere Sekunden pro Frage erfordern. FAISS (Facebook AI Similarity Search) löst genau dieses Problem. Diese Open-Source-Bibliothek von Meta ist für Ähnlichkeitssuchen über große Vektorsätze konzipiert; sie ist schnell, speichereffizient und funktioniert sowohl mit wenigen hundert als auch mit Millionen von Vektoren gleich gut. Die Installation erfolgt mit einem einzigen Befehl. faiss-cpu reicht in den meisten Fällen aus; faiss-gpu ist nur dann gerechtfertigt, wenn die Rechenleistung zum Flaschenhals wird.

# pip install faiss-cpu
# oder faiss-gpu für GPU-Beschleunigung

import faiss
import numpy as np

Erstellen Sie Ihren ersten Index

Der einfachste Index ist IndexFlatL2, der eine erschöpfende Suche unter Verwendung der L2-Distanz (euklidisch) durchführt. „Erschöpfend” bedeutet, dass die Abfrage tatsächlich mit jedem gespeicherten Vektor verglichen wird: keine Approximation, somit keine verpassten Ergebnisse. Sie initialisieren ihn mit der Vektordimension – 1024 für mistral-embed, einen Wert, den Sie direkt aus der Matrixform ablesen, anstatt ihn fest zu codieren. Die Umwandlung in float32 ist nicht optional: FAISS lehnt das float64 ab, das NumPy standardmäßig erzeugt, und das Vergessen dieses Details ist der erste Fehler, den jeder Anfänger begeht.

# Nehmen wir an, unsere Embeddings sind eine NumPy-Matrix
# embeddings.shape = (n_chunks, 1024)

# Index erstellen
dimension = embeddings.shape[1]  # 1024 für mistral-embed
index = faiss.IndexFlatL2(dimension)

# Vektoren hinzufügen
index.add(embeddings.astype(np.float32))  # FAISS erfordert float32

print(f"Index erstellt: {index.ntotal} Vektoren der Dimension {dimension}")

Die richtige Indexart wählen

FAISS bietet nicht einen einzelnen Index, sondern eine Familie von ihnen, und die Wahl beinhaltet einen Kompromiss zwischen Genauigkeit und Geschwindigkeit. Die ersten beiden, IndexFlatL2 und IndexFlatIP, sind exakt: sie unterscheiden sich nur in der verwendeten Metrik, euklidische Distanz für das eine, Skalarprodukt für das andere. IndexIVFFlat ändert die Logik, indem er den Raum in Cluster unterteilt und nur die vielversprechendsten durchsucht, was die Suche erheblich beschleunigt, jedoch auf Kosten einer leichten Abnahme der Recall-Rate – und erfordert eine vorherige Schulung an Ihren Daten. Schließlich baut IndexHNSWFlat einen Nachbargraphen und erweist sich bei Abfragen als sehr schnell, wobei der Parameter 32 hier die Anzahl der pro Knoten beibehaltenen Nachbarn festlegt.

dimension = 1024

# 1. IndexFlatL2: Exakte Suche, L2-Distanz
# Höchste Genauigkeit, aber langsamste für große Volumina
index_flat_l2 = faiss.IndexFlatL2(dimension)

# 2. IndexFlatIP: Exakte Suche, Skalarprodukt (inneres Produkt)
# Verwendung mit normalisierten Vektoren für Kosinus-Ähnlichkeit
index_flat_ip = faiss.IndexFlatIP(dimension)

# 3. IndexIVFFlat: Approximative Suche mit Partitionierung
# Schneller für große Volumina, leichter Genauigkeitsverlust
nlist = 100  # Anzahl der Partitionen (Cluster)
quantizer = faiss.IndexFlatL2(dimension)
index_ivf = faiss.IndexIVFFlat(quantizer, dimension, nlist)
# Erfordert Schulung vor der Nutzung:
# index_ivf.train(embeddings)
# index_ivf.add(embeddings)

# 4. IndexHNSWFlat: Nachbargraph (sehr schnelle Suche)
index_hnsw = faiss.IndexHNSWFlat(dimension, 32)  # 32 = Anzahl der Nachbarn

Abfragen des Index

Die Suche erfolgt in drei Schritten. Erstens codieren Sie die Frage des Nutzers mit demselben Modell wie das Korpus – eine durch ein anderes Modell codierte Abfrage würde bedeutungslose Distanzen ergeben. FAISS gibt dann zwei parallele Arrays zurück: die Distanzen und die Indizes der k nächsten Nachbarn. Diese Indizes sind Positionen in Ihrer Liste von Chunks: hier zahlt sich die Disziplin der vorherigen Lektion aus, da die Ausrichtung zwischen Matrix und Metadaten es ermöglicht, zurück zum Text und seiner Quelle zu gelangen. Der Test if idx >= 0 deckt den Fall ab, in dem der Index weniger als k Vektoren enthält; in diesem Fall füllt FAISS die fehlenden Stellen mit -1.

from mistralai.client import Mistral

client = Mistral(api_key=api_key)

def search_index(query, index, chunks, k=5):
    """Suche nach Chunks, die einer Abfrage am nächsten liegen."""
    # 1. Codierung der Abfrage
    response = client.embeddings.create(
        model="mistral-embed",
        inputs=[query],
    )
    query_vector = np.array([response.data[0].embedding], dtype=np.float32)

    # 2. Suche in FAISS
    distances, indices = index.search(query_vector, k)

    # 3. Formatierung der Ergebnisse
    results = []
    for i, (dist, idx) in enumerate(zip(distances[0], indices[0])):
        if idx >= 0:  # FAISS gibt -1 zurück, wenn nicht genügend Ergebnisse vorhanden sind
            results.append({
                "rank": i + 1,
                "distance": float(dist),
                "text": chunks[idx]["text"],
                "source": chunks[idx].get("source", ""),
            })

    return results

# Beispiel-Suche
results = search_index("Wie funktioniert RAG?", index, chunks)
for r in results:
    print(f"  #{r['rank']} (dist: {r['distance']:.4f}) [{r['source']}]")
    print(f"    {r['text'][:100]}...")

Umstellung auf Kosinus-Ähnlichkeit

FAISS bietet keinen expliziten „cosine”-Index, doch die Algebra liefert einen kostenlosen Äquivalent: Bei Vektoren mit Norm 1 ist das Skalarprodukt das Kosinus. Sie teilen einfach jeden Vektor durch seine Norm und verwenden dann IndexFlatIP. Dieser Wechsel ist keineswegs rein theoretisch: Die L2-Distanz ist empfindlich gegenüber der Vektorlänge, sodass ein sehr langes Chunk künstlich weit von einer kurzen Abfrage entfernt sein kann, selbst wenn ihre Themen übereinstimmen. Kosinus misst nur die Orientierung, also die Bedeutung, und das ist in der Regel das, was man vergleichen möchte.

def create_cosine_index(embeddings):
    """Erstellen Sie einen FAISS-Index mit Kosinus-Ähnlichkeit."""
    # Normalisieren der Vektoren (L2-Norm = 1)
    norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
    normalized = (embeddings / norms).astype(np.float32)

    # Verwendung von IndexFlatIP (Skalarprodukt = Kosinus für normalisierte Vektoren)
    dimension = normalized.shape[1]
    index = faiss.IndexFlatIP(dimension)
    index.add(normalized)

    return index

cosine_index = create_cosine_index(embeddings)

Wie bei Embeddings persistiert auch der Index auf der Festplatte. faiss.write_index erzeugt eine Binärdatei, die faiss.read_index unverändert neu lädt, einschließlich interner Strukturen – was vor allem für geschulte Indizes von Bedeutung ist, bei denen sonst der Wiederaufbau neu durchgeführt werden müsste.

# Speichern
faiss.write_index(index, "rag_faiss.index")
print("FAISS-Index gespeichert")

# Laden
index_loaded = faiss.read_index("rag_faiss.index")
print(f"Index geladen: {index_loaded.ntotal} Vektoren")

Wenn das Korpus massiv wird

Ab etwa mehreren hunderttausend Vektoren wird die erschöpfende Suche zu langsam, und der IVF-Index (Inverted File) wird notwendig. Sein Lebenszyklus umfasst einen Schritt, den flache Indizes nicht haben: die Schulung, die die Datenverteilung analysiert, um die nlist Partitionsschwerpunkte zu platzieren. Anschließend folgen das Hinzufügen der Vektoren und das Einstellen von nprobe, dem Regler, der entscheidet, wie viele Partitionen pro Abfrage durchsucht werden. Eine Erhöhung von nprobe verbessert den Recall und verlangsamt die Suche; dies ist der Parameter, den Sie durch Messung an einem repräsentativen Satz von Fragen anpassen werden, um den Punkt zu finden, an dem die Ergebnisse nicht mehr verbessern.

def create_ivf_index(embeddings, nlist=100, nprobe=10):
    """Erstellen Sie einen IVF-Index für große Volumina."""
    dimension = embeddings.shape[1]
    data = embeddings.astype(np.float32)

    # Erstellen des Quantizers und des IVF-Indexes
    quantizer = faiss.IndexFlatL2(dimension)
    index = faiss.IndexIVFFlat(quantizer, dimension, nlist)

    # Schulung des Index (erforderlich für IVF)
    print("Index wird trainiert...")
    index.train(data)

    # Hinzufügen der Vektoren
    index.add(data)

    # Anzahl der Partitionen, die während der Suche durchsucht werden
    index.nprobe = nprobe  # Höher = genauer, aber langsamer

    print(f"IVF-Index erstellt: {index.ntotal} Vektoren, {nlist} Partitionen")
    return index

# Für 100K+ Vektoren
# large_index = create_ivf_index(embeddings, nlist=256, nprobe=20)

Kernaussagen

  • FAISS ist der Standard für schnelle Vektorsuchen
  • IndexFlatL2 für kleine Korpora (unter 100K Vektoren), exakte Suche
  • IndexIVFFlat für große Korpora, approximativ, aber schnelle Suche
  • Vektoren normalisieren und IndexFlatIP für Kosinus-Ähnlichkeit verwenden
  • Index mit faiss.write_index auf Festplatte speichern, um einen Neuaufbau zu vermeiden