Startseite / Artikel

Ein Retriever, drei Backend-Systeme: Portierung der NumPy-Suche zu Chroma, FAISS und Pinecone

Bringen Sie einen handgefertigten NumPy-Kosinus-Suchalgorithmus in Chroma, FAISS und Pinecone ein und erfahren Sie, was Indizes, Metadatenfilter und Persistenz beitragen, sowie welche Fehler die Relevanz beeinträchtigen.

Verlag: REACTAPP.TOP11 Min. Lesezeit2493 Wörter

Quellen: Vector Databases, Hands-On — From Your numpy Array to Pinecone

Der einfachste Vektor-Speicher besteht aus einer NumPy-Matrix mit Embeddings sowie einer einzigen Rechenzeile: chunk_vectors @ q_vec bewertet jeden Chunk im Vergleich zur Abfrage, und ein Sortieren wählt die besten Ergebnisse aus. Das ist eine echte Vektor-Datenbank – nur in ihrer absolut minimalen Form. In dieser Anleitung wird genau dieses Suchverfahren genommen und auf drei tatsächliche Backend-Systeme, nämlich Chroma, FAISS und Pinecone, übertragen, damit Sie genau erkennen können, was jedes System hinzufügt, welches der vielen Produkte (Chroma, FAISS, Qdrant, Weaviate, Milvus, pgvector, Pinecone) zu welcher Situation passt und welche Fehler heimlich die Relevanz beeinträchtigen.

Die lokalen Beispiele benötigen keine API-Schlüssel und kosten nichts. Pinecone bietet eine kostenlose serverlose Starter-Ebene an, die eine Registrierung und einen Schlüssel erfordert, aber zum Zeitpunkt der Erstellung keine Zahlungskarte – daher kann auch der verwaltete Ansatz kostenlos ausprobiert werden. Der Code geht von einem einzigen Skript, vdb.py, aus, das man Block für Block ausführt.

Wo ein NumPy-Array nicht mehr ausreicht

Ein manuell erstellter Suchalgorithmus berechnet bei jeder Abfrage die Kosinusähnlichkeit zu jedem gespeicherten Datenblock. Bei einigen hundert Blöcken ist das sofort erledigt. Bei einigen Millionen wird es langsam, die gesamte Matrix muss im RAM gespeichert werden, und alle Einbettungen verschwinden, sobald der Prozess beendet wird. Eine Vektordatenbank behebt drei dieser Probleme:

  1. Geschwindigkeit in großem Maßstab. Anstatt eine genaue O(n)-Vergleich mit jedem Vektor durchzuführen, wird ein annähernder Nähesten-Nachbarn-Index wie HNSW erstellt, der nahegelegene Vektoren in etwa O(log n) Zeit findet. Man gibt etwas an Genauigkeit auf, um eine erhebliche Geschwindigkeitssteigerung zu erzielen.
  2. Persistenz. Vektoren werden auf der Festplatte oder auf einem verwalteten Server gespeichert und über Neustarts hinweg erhalten bleiben.
  3. Metadaten und Filtern. Jeder Vektor kann Attribute wie {"source": "doc12", "date": "2026-01"} enthalten, und eine Abfrage kann auf diese beschränkt werden, beispielsweise auf die nähesten Vektoren mit Quelle doc12.

Der Kontrast zwischen den beiden Abfragemethoden lässt sich in zwei Sätzen zusammenfassen:

your numpy array:   query ──► compare to ALL n vectors ──► sort ──► top-k     (exact, slow at scale)
a vector database:  query ──► walk HNSW graph ──► ~top-k     (approximate, fast, on disk, filterable)

Das ist die gesamte Wertvorstellung. Alles Weitere sind nur technische Details.

Wortschatz, auf den Sie immer wieder stoßen werden

  • Embedding oder Vektor: die Liste von Zahlen, die eine Bedeutung kodiert; die hier gezeigten Beispiele verwenden all-MiniLM-L6-v2.
  • Abstandsmetrik: die Methode, mit der die Nähe gemessen wird. Kosinus (der Winkel zwischen Vektoren) ist die übliche Wahl für Texte; Skalarprodukt und euklidischer Abstand (L2) sind Alternativen. Verwenden Sie die Methode, für die Ihr Embedding-Modell trainiert wurde.
  • Index: die Struktur, die Suchvorgänge beschleunigt. HNSW, ein navigierbarer Graph, ist in den meisten Datenbanken Standard; IVF und PQ tauschen auf unterschiedliche Weise Speicherkapazität gegen Geschwindigkeit ein.
  • Upsert: Ein Vektor wird über die ID eingefügt oder aktualisiert, falls die ID bereits vorhanden ist.
  • Metadatenfilterung: Eingrenzung der Suche nach gespeicherten Attributen.
  • Namensraum oder Sammlung: Eine Aufteilung innerhalb einer Datenbank, sodass unverwandte Daten niemals miteinander vermischt werden.
  • Chroma: die natürliche erste Erweiterung

    Chroma läuft nach einer einzigen Installation innerhalb Ihres Python-Prozesses – ohne Server und ohne Docker. Dadurch ist es der direkteste Schritt nach einer NumPy-Array-Lösung. Installieren Sie es zusammen mit der Embedding-Bibliothek:

    pip install chromadb sentence-transformers
    

    Der untenstehende Script verwendet ein kleines Korpus, das ein fiktives Notizbuch-Produkt namens Nimbus beschreibt, wobei jedes Dokument mit einem Thema-Tag versehen ist. Es erstellt einen Client im Arbeitsspeicher, öffnet eine für die Kosinus-Distanz konfigurierte Sammlung über hnsw:space, embeddet die Dokumente explizit und fügt sie zusammen mit IDs sowie Metadaten hinzu. Anschließend embeddet es eine Frage und fragt nach den beiden nächsten Dokumenten, wobei jedes zusammen mit seiner Distanz ausgegeben wird. Beachten Sie den Kommentar zu chromadb.Client(): Es reicht aus, PersistentClient(path=...) einzusetzen, um die Daten auf der Festplatte zu speichern.

    # vdb.py
    import chromadb
    from sentence_transformers import SentenceTransformer
    
    embedder = SentenceTransformer("all-MiniLM-L6-v2")
    
    # Same Nimbus corpus from the RAG tutorial
    DOCS = [
        "Nimbus Pro costs 8 dollars per month billed annually, with 50 GB storage.",
        "Nimbus offers a 30-day refund policy on all paid plans.",
        "Nimbus Lite (free) includes 1 GB of storage and up to 50 notes.",
        "Nimbus stores notes encrypted at rest with AES-256; E2E encryption is Pro-only.",
        "Live chat support is Pro-only, Mon-Fri 9-6 UTC; free users get 48h email support.",
    ]
    metadatas = [{"topic": t} for t in ["pricing","refund","pricing","security","support"]]
    
    client = chromadb.Client()                       # in-memory; use PersistentClient(path=...) to save to disk
    collection = client.create_collection(
        "nimbus", metadata={"hnsw:space": "cosine"}   # choose the distance metric
    )
    
    # Embed and add. (↔ your chunk_vectors = embedder.encode(...))
    collection.add(
        ids=[f"doc{i}" for i in range(len(DOCS))],
        embeddings=embedder.encode(DOCS).tolist(),
        documents=DOCS,
        metadatas=metadatas,
    )
    
    # Query. (↔ your retrieve(): embed query, find nearest)
    q = "How much does the paid plan cost?"
    res = collection.query(query_embeddings=embedder.encode([q]).tolist(), n_results=2)
    for doc, dist in zip(res["documents"][0], res["distances"][0]):
        print(f"[{dist:.3f}] {doc}")
    

    Wenn man dies mit der handgemachten Version vergleicht, gibt es konzeptionell nichts Neues. collection.add ersetzt die Kodierung der Dokumente sowie das Speichern der Matrix. collection.query ersetzt den Matrizenprodukt, argsort sowie das Auswählen der Top-k-Einträge. Was Chroma beiträgt, ist der HNSW-Index sowie die Unterstützung für Persistenz und Metadaten – alles basierend auf derselben Idee.

    Falls man documents= ohne embeddings= übergeben lässt, kann Chroma mithilfe eines integrierten Modells auch Texte für einen einbetten. Durch explizite Einbettung, wie oben beschrieben, bleiben die Mechanismen sichtbar und die Wahl des Modells liegt in den eigenen Händen – was wichtig ist, da dasselbe Modell sowohl für das Hinzufügen als auch für die Abfrage verwendet werden muss.

    Filtern nach Metadaten

    Filtern ist eine Funktion, die ein einfacher Array niemals hatte. Die nächste Abfrage sucht nach „Data Security“, beschränkt die Ergebnisse jedoch auf Vektoren, deren topic den Wert security hat, und zwar über das Argument where:

    res = collection.query(
        query_embeddings=embedder.encode(["data security"]).tolist(),
        n_results=3,
        where={"topic": "security"},   # only search vectors tagged security
    )
    print(res["documents"][0])
    

    In der Produktion sorgt dies dafür, dass Anfragen wie „Ähnliche Tickets dieses Kunden“ oder „In diesem Jahr aktualisierte Dokumentation“ bearbeitet werden können. Der Filter wird zusammen mit der Vektorabfrage angewendet und nicht als Nachverarbeitungsschritt, sodass weiterhin bis zu n_results Ergebnisse zurückgegeben werden, die den Filter erfüllen. Als Übung ändern Sie den Filter auf where={"topic": "pricing"} und überprüfen Sie, dass keine Rückerstattungs- oder Sicherheitsdokumente mehr angezeigt werden.

    FAISS: Eine schnelle Indexbibliothek, keine Datenbank

    FAISS von Meta ist überhaupt keine Datenbank. Es handelt sich um eine hochoptimierte Bibliothek für das Suchen nach dem nächsten Nachbarn, die keine Metadaten und keine eigene Speicherschicht besitzt und dazu gedacht ist, in Ihrer Anwendung eingebettet zu werden. Viele andere Systeme verwenden sie intern. Installieren Sie die CPU-Version:

    pip install faiss-cpu
    

    Der Code kodiert die Dokumente mit normalize_embeddings=True und wandelt sie in float32 um, was von FAISS erforderlich ist. Anschließend wird ein IndexFlatIP, ein Index auf Basis des Skalarprodukts, erstellt; da die Vektoren auf Länge eins normalisiert sind, entspricht das Skalarprodukt der Kosinusähnlichkeit. Nachdem die Vektoren hinzugefügt wurden, wird die Abfrage auf dieselbe Weise kodiert und search aufgerufen, um die beiden besten Ergebnisse zu erhalten. Danach werden die zurückgegebenen Positionen wieder auf die ursprünglichen Dokumente abgebildet.

    import faiss, numpy as np
    
    vectors = embedder.encode(DOCS, normalize_embeddings=True).astype("float32")
    index = faiss.IndexFlatIP(vectors.shape[1])   # IP = inner product = cosine on normalized vectors
    index.add(vectors)                            # ↔ exactly your numpy array, but optimized in C++
    
    q = embedder.encode(["refund window?"], normalize_embeddings=True).astype("float32")
    scores, idx = index.search(q, k=2)            # ↔ your argsort top-k
    for s, i in zip(scores[0], idx[0]):
        print(f"[{s:.3f}] {DOCS[i]}")
    

    Das ist im Grunde genommen der NumPy-Retriever: IndexFlatIP ist eine brute-force-basierte Kosinus-Suche, die lediglich in optimiertem C++ implementiert ist. Der Vorteil zeigt sich, wenn man ihn durch einen annähernden Index wie IndexHNSWFlat oder IndexIVFFlat ersetzt, die dieselbe Schnittstelle beibehalten, aber in sublinearer Zeit suchen. Wählen Sie FAISS, wenn Sie maximale Geschwindigkeit innerhalb eines Prozesses wünschen und bereit sind, die Persistenz sowie Metadaten selbst zu verwalten – beispielsweise durch Aufrechterhaltung einer separaten Zuordnung von Positionen zu Dokumentenregistern.

    Pinecone: vollständig verwaltet und serverlos

    Bis jetzt lief alles in Ihrem eigenen Prozess. Pinecone ist eine verwaltete Vektordatenbank, die über das Netzwerk erreichbar ist: Es gibt keinen Server zu betreiben, und sie skaliert automatisch. Sie eignet sich für Teams, die keine Infrastruktur verwalten möchten.

    Achten Sie darauf, dass sich das Pinecone SDK erheblich geändert hat. Der ältere Stil pinecone.init(api_key=, environment=) existiert nicht mehr; Sie erstellen nun ein Pinecone-Client-Objekt, und das Paket heißt pinecone, nicht pinecone-client. Überprüfen Sie die aktuelle SDK-Dokumentation, falls etwas von dem Folgenden fehlschlägt.

    Der Block beginnt mit dem Installationsbefehl und danach mit dem Python-Code (teilen Sie sie, wenn Sie ihn kopieren). Er erstellt einen Client sowie einen serverlosen Index mit dem Namen nimbus, sofern dieser noch nicht existiert, wobei dimension=384 verwendet wird, um mit MiniLM übereinzustimmen, und metric="cosine". Anschließend wird eine Verbindung zu diesem Index hergestellt. Es werden (id, vector, metadata)-Tupel eingefügt oder aktualisiert, wobei der ursprüngliche Text in den Metadaten gespeichert wird, da Pinecone nur das zurückgibt, was Sie gespeichert haben. Schließlich werden die beiden besten Ergebnisse unter Verwendung eines Mongo-ähnlichen $eq-Filters auf topic abgerufen, wobei jeder Score sowie der zugehörige Text ausgegeben werden. Im echten Code sollte die Schlüsselwerte aus einer Umgebungsvariable oder einem Geheimnisspeicher gelesen werden, anstatt sie im Quellcode festzulegen.

    pip install pinecone
    
    from pinecone import Pinecone, ServerlessSpec
    
    pc = Pinecone(api_key="YOUR_API_KEY")          # get a free key at app.pinecone.io
    
    # Create a serverless index (only once). dimension must match your embedder: MiniLM = 384.
    if not pc.has_index("nimbus"):
        pc.create_index(
            name="nimbus",
            dimension=384,
            metric="cosine",
            spec=ServerlessSpec(cloud="aws", region="us-east-1"),
        )
    index = pc.Index("nimbus")
    
    # Upsert: (id, vector, metadata) tuples
    index.upsert(vectors=[
        (f"doc{i}", embedder.encode(d).tolist(), {"text": d, "topic": m["topic"]})
        for i, (d, m) in enumerate(zip(DOCS, metadatas))
    ])
    
    # Query with metadata filtering
    res = index.query(
        vector=embedder.encode("how much is pro?").tolist(),
        top_k=2,
        include_metadata=True,
        filter={"topic": {"$eq": "pricing"}},      # Mongo-style filter operators
    )
    for match in res["matches"]:
        print(f"[{match['score']:.3f}] {match['metadata']['text']}")
    

    Die Struktur entspricht Chroma genau: Erstellen Sie einen Index, fügen Sie IDs, Vektoren und Metadaten hinzu oder aktualisieren Sie diese, und führen Sie anschließend eine Abfrage nach den Top-k-Werten mit optionaler Filterung durch. Der Abrufvorgang ist derselbe wie zuvor, wird nun jedoch auf der Autoscaling-Infrastruktur eines Dritten über eine API ausgeführt. Pinecone unterstützt außerdem Namespaces, mit denen ein einziger Index in Teile aufgeteilt werden kann, sowie integrierte Inferenzfunktionen, bei denen upsert_records und search_records den Text für Sie embedden, sodass Sie keinen eigenen Embedder betreiben müssen.

    Die wichtigere Erkenntnis ist, dass der Wechsel von Chroma zu Pinecone nur die Art und Weise verändert hat, wie man auf den Speicher zugreift und diesen aufruft – nicht die dahinterliegende RAG-Logik. In einer gut strukturierten Anwendung ist der Vektor-Speicher ein austauschbarer Bestandteil, weshalb Framework-Methoden wie LangChain’s .as_retriever() unabhängig vom Backend identisch aussehen.

    Die Wahl eines Backends für Ihre Anforderungen

    Keine Vektor-Datenbank ist im Allgemeinen die beste; eine eignet sich am besten für einen bestimmten Satz an Anforderungen. Im Großen und Ganzen:

    • NumPy-Array (manuell erstellt): läuft im Prozess selbst; geeignet für etwa weniger als 10.000 Vektoren, zum Lernen oder wenn Sie vollständige Kontrolle wünschen.
    • Chroma: eingebettete Datenbank, im Prozess selbst oder als lokaler Server; ideal für Prototypen sowie kleine bis mittlere Anwendungen – der einfachste Einstieg.
    • FAISS: Indexierungsbibliothek, im Prozess selbst; maximale Geschwindigkeit, wenn Sie die Speicherung und Metadaten selbst verwalten.
    • pgvector: eine Erweiterung für Postgres innerhalb Ihrer bestehenden Datenbank; ideal, wenn Sie bereits Postgres nutzen und Vektoren neben relationalen Daten haben möchten.
    • Qdrant: selbst gehostete Datenbank mit Docker oder in der Cloud; starke Filterfunktionen, Leistung durch Rust, Open Source sowie hybride Suchfunktionen.
  • Weaviate: Datenbank, selbst gehostet oder in der Cloud; integrierte Module, hybrides Suchen sowie eine GraphQL-API.
  • Milvus: Datenbank, selbst gehostet (erfordert mehr Ressourcen) oder in der Cloud; entwickelt für Milliarden von Vektoren.
  • Pinecone: ausschließlich als Managed-Dienst in seiner eigenen Cloud; keine Betriebsaufwände und serverloses Autoscaling.
  • Die ständig wiederkehrende Entscheidung betrifft verwaltete versus selbst gehostete Lösungen. Verwaltete Dienste wie Pinecone oder die Cloud-Versionen der anderen Anbieter beseitigen die Betriebsaufwände, verursachen jedoch laufende Kosten und lagern Ihre Daten auf den Servern des Anbieters. Das Selbsthosten von Chroma, Qdrant, Weaviate, Milvus oder pgvector bedeutet, dass Sie die Lösung selbst betreiben und somit die Kontrolle über Kosten sowie die Datenunterbringung behalten. Eine häufig zitierte Faustregel besagt, dass sich das Selbsthosten erst dann lohnt, wenn die monatliche Rechnung für verwaltete Dienste in die niedrigen Hundertdollargrenzen steigt; unter diesem Niveau kosten die für den Betrieb aufgewendeten Arbeitsstunden in der Regel mehr, als sie einsparen. Behandeln Sie dies als Heuristik und nicht als Gesetz – schließlich spielt die Betriebskapazität Ihres Teams genauso eine Rolle wie die Rechnungshöhe.

    Für das Lernen und die meisten Nebenprojekte reichen Chroma oder pgvector aus. Pinecone oder Qdrant Cloud sind sinnvoll, wenn die Infrastruktur etwas ist, das Ihr Team lieber nicht selbst betreuen möchte, und Milvus kommt zum Einsatz, wenn Sie tatsächlich mit Milliarden von Vektoren arbeiten. Wenn Sie abwägen, ob Sie überhaupt einen dedizierten Vektor-Speicher benötigen, lesen Sie warum sich RAG in der Produktion von dedizierten Vektordatenbanken löst.

    Fehler, die Probleme verursachen, sobald eine Datenbank läuft

    • Dimensionenunterschied ist der häufigste Fehler. Die Index-Dimension muss der Ausgabegröße des Embedders entsprechen: 384 für MiniLM, 1536 für viele OpenAI-Embedding-Modelle. Das Mischen von Modellen in einem Index führt garantiert zu Fehlern.
  • Metrikunterschiede zerstören heimlich die Relevanz – beispielsweise durch den Einsatz der euklidischen Distanz bei einem für Kosinus-Distanzen konzipierten Modell. Passen Sie die Metrik dem Modell an.
  • Abschätzungen sind nicht exakt. ANN-Indizes übersehen gelegentlich den wahren nächsten Nachbarn; das ist der Preis für Geschwindigkeit. Die meisten Datenbanken bieten Einstellungen wie ef_search für HNSW oder nprobe für IVF, die die Trefferquote erhöhen, jedoch zu höherer Latenz führen. Weitere Informationen zu diesen Parametern finden Sie unter Tuning von HNSW-Indizes für produktive RAG-Anwendungen.
  • Erneutes Einbetten bei jedem Neustart tritt auf, wenn nichts gespeichert wird – dadurch entsteht bei jedem Lauf erneut die Kosten für das Einbetten. Verwenden Sie Chromas PersistentClient oder eine serverbasierte Datenbank.
  • Filter haben ihren eigenen Kostenfaktor. Intensives Filtern von Metadaten kann Abfragen verlangsamen; indizieren Sie die Felder, nach denen Sie filtern, sofern Ihre Datenbank dies unterstützt.
  • Eine gute Übung besteht darin, ein vorhandenes RAG-Skript auf Basis von NumPy zu nehmen und lediglich seine Abruffunktion durch die zuvor gezeigte Chroma-Sammlung zu ersetzen. Der Generierungsanteil ändert sich überhaupt nicht, was zeigt, dass der Abruf tatsächlich eine saubere, austauschbare Schicht darstellt.

    Schnellreferenz zum Fehlerbeheben

    • dimension does not match: Ihre Vektoren haben nicht die von dem Index erwartete Größe. Erstellen Sie den Index mit der richtigen dimension neu oder verwenden Sie den Embedder, für den er ursprünglich erstellt wurde.
    • Die Chroma-Ergebnisse wirken zufällig: Die Metriken sind vermutlich gemischt, oder es wurden unterschiedliche Embedder zum Hinzufügen und Abfragen verwendet. Beide müssen übereinstimmen.
  • Fehler bei Pinecone has_index oder create_index: Stellen Sie sicher, dass Sie die aktuelle SDK-Version verwenden (from pinecone import Pinecone) und nicht mehr die veraltete pinecone.init-Methode.
  • FAISS search gibt -1 als IDs zurück: Der Index enthält weniger Vektoren als k. Fügen Sie Daten hinzu oder verringern Sie den Wert von k.
  • Alles ist bei großen Datenmengen langsam: Wahrscheinlich verwenden Sie einen einfachen, exakten Index. Wechseln Sie auf HNSW oder IVF und passen Sie den Recall-Parameter an.
  • Kernpunkte

    • Eine Vektordatenbank ist ein selbst erstellter Suchmechanismus, der schnell, dauerhaft und filterbar ist. Das Hinzufügen von Vektoren beinhaltet die Kodierung sowie die Speicherung; die Abfragen erfolgen mithilfe von Cosine-Top-K.
    • Der ANN-Index ist der einzige wirklich innovative Bestandteil: Durch die Akzeptanz von annähernden Ergebnissen bleibt die Suche auch bei wachsenden Datenmengen schnell.
  • Metadaten-Filterung und -Persistenz sind Funktionen, die ein einfacher Array nicht bietet.
  • Wenn die Qualität der Abfrage schlecht ist, muss zunächst entschieden werden, ob das Problem bei den Embeddings liegt (falsches Modell oder Metrik) oder beim Index (zu niedriger Recall). Die zurückgegebenen Entfernungen geben in der Regel Aufschluss darüber.
  • Halten Sie den Speicher hinter einer eingeschränkten Schnittstelle verborgen, sodass ein Wechsel von Chroma zu Pinecone oder einer anderen Plattform nur die Verbindungs- und Aufrufcodes betrifft.