7 Punkte von GN⁺ 2025-03-06 | 1 Kommentare | Auf WhatsApp teilen

Die beste portable Methode für Text-Embeddings: Parquet und Polars

  • Text-Embeddings sind von großen Sprachmodellen erzeugte Vektoren, die Wörter, Sätze und Dokumente numerisch darstellen.
  • Stand Februar 2025 wurden insgesamt 32.254 Embeddings für "Magic: The Gathering"-Karten erzeugt.
  • Damit lässt sich die Ähnlichkeit auf Basis des Designs und der mechanischen Eigenschaften der Karten mathematisch analysieren.
  • Die erzeugten Embeddings lassen sich über eine 2D-Dimensionsreduktion mit UMAP visualisieren.
  • Verwendet wurde das Embedding-Modell gte-modernbert-base; der genaue Ablauf ist im GitHub-Repository dokumentiert.
  • Der entsprechende Embedding-Datensatz wird auf Hugging Face bereitgestellt.

Die Notwendigkeit von Vektor-Datenbanken neu bewerten

  • Üblicherweise werden Embeddings mit Vektor-Datenbanken (faiss, qdrant, Pinecone) gespeichert und durchsucht.
  • Vektor-Datenbanken erfordern jedoch oft eine komplexe Einrichtung, und Cloud-Dienste können teuer sein.
  • Bei kleineren Datenmengen im Bereich von einigen Zehntausend lassen sich schnelle Ähnlichkeitssuchen auch ohne Vektor-Datenbank mit numpy umsetzen.
  • Mit der numpy-Operation dot product lässt sich eine einfache Cosine Similarity berechnen; für 32.254 Embeddings dauert das im Schnitt 1,08 ms.
def fast_dot_product(query, matrix, k=3):  
    dot_products = query @ matrix.T  
  
    idx = np.argpartition(dot_products, -k)[-k:]  
    idx = idx[np.argsort(dot_products[idx])[::-1]]  
  
    score = dot_products[idx]  
  
    return idx, score  
  • Wer eine Vektor-Datenbank nutzt, bindet sich leicht an bestimmte Bibliotheken und Dienste.
  • Wenn Embeddings auf einem GPU-Server erzeugt und anschließend lokal heruntergeladen werden, ist eine effiziente Methode zum Speichern und Übertragen der Daten wichtig.

Die schlechtesten Methoden zum Speichern von Embeddings

  • CSV-Dateien
    • Werden Gleitkommadaten (float32) als Text gespeichert, wächst die Größe um mehr als das Sechsfache.
    • Selbst im offiziellen Tutorial von OpenAI wird CSV nur für kleine Datensätze empfohlen.
    • Beim Speichern mit numpy .savetxt() wächst die Dateigröße auf 631,5 MB.
  • pickle-Dateien
    • Sie lassen sich schnell speichern und laden, bergen aber Sicherheitsrisiken und haben eine schwache Versionskompatibilität.
    • Die Dateigröße beträgt 94,49 MB und entspricht damit der ursprünglichen Größe im Speicher, die Portabilität ist jedoch gering.

Nicht schlecht, aber nicht optimal: Speicherformate

  • Das .npy-Format von numpy
    • Mit der Einstellung allow_pickle=False lässt sich das Speichern per pickle verhindern.
    • Dateigröße und Geschwindigkeit entsprechen der pickle-Methode, aber einzelne Metadaten lassen sich nur schwer gemeinsam speichern.
  • Probleme einer von Metadaten getrennten Speicherstruktur
    • Werden Daten als numpy-Array (.npy) gespeichert, liegen Karteninformationen wie Name und Text getrennt von den Embeddings vor.
    • Wenn sich Daten ändern, also hinzugefügt oder gelöscht werden, wird das Zuordnen von Metadaten und Embeddings schwierig.
    • Vektor-Datenbanken speichern Metadaten zusammen mit den Vektoren und bieten Filterfunktionen.

Die optimale Methode zum Speichern von Embeddings: Parquet + polars

Einführung in das Dateiformat Parquet

  • Apache Parquet ist ein spaltenbasiertes Datenspeicherformat, in dem sich der Datentyp jeder Spalte klar festlegen lässt.
  • Da auch Listendaten (float32-Arrays) gespeichert werden können, eignet es sich gut für Embeddings.
  • Es bietet schnelleres Speichern und Laden als CSV, und es lassen sich gezielt nur Teilmengen der Daten laden.
  • Zwar unterstützt es Kompression, bei Embedding-Daten ist der Effekt wegen der geringen Redundanz jedoch klein.

Einsatz von Parquet-Dateien in Python

  • Parquet-Dateien mit pandas speichern und laden:
    df = pd.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])  
    df  
    
    • pandas verarbeitet verschachtelte Daten wie Listen nicht effizient und wandelt sie in numpy object um.
    • Für die Umwandlung in ein numpy-Array ist zusätzlicher Aufwand mit np.vstack() nötig, was die Performance verschlechtern kann.
  • Parquet-Dateien mit polars speichern und laden:
    df = pl.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])  
    df  
    
    • polars behält float32-Arrays unverändert bei und kann bei to_numpy() sofort ein 2D-numpy-Array zurückgeben.
    • Mit der Einstellung allow_copy=False lassen sich unnötige Datenkopien vermeiden.
    embeddings = df["embedding"].to_numpy(allow_copy=False)  
    
  • Auch beim Hinzufügen neuer Embeddings lassen sich diese einfach als Spalte ergänzen und speichern.
    df = df.with_columns(embedding=embeddings)  
    df.write_parquet("mtg-embeddings.parquet")  
    

Ähnlichkeitssuche und Filterung mit Parquet + polars

  • Zunächst können nur Daten gefiltert werden, die bestimmte Bedingungen erfüllen, und danach wird die Ähnlichkeitssuche ausgeführt.
  • Beispiel: Es sollen Karten gesucht werden, die einer bestimmten Karte (query_embed) ähneln, aber nur vom Typ 'Sorcery' sind und die Farbe 'Black' enthalten.
    df_filter = df.filter(  
        pl.col("type").str.contains("Sorcery"),  
        pl.col("manaCost").str.contains("B"),  
    )  
    
    embeddings_filter = df_filter["embedding"].to_numpy(allow_copy=False)  
    idx, _ = fast_dot_product(query_embed, embeddings_filter, k=4)  
    related_cards = df_filter[idx]  
    
  • Mit einer durchschnittlichen Laufzeit von 1,48 ms ist das 37 % langsamer als die Suche über den Gesamtdatensatz, aber weiterhin schnell.

Alternativen für die Verarbeitung großer Vektordatenmengen

  • Der Ansatz mit Parquet und dot product reicht für mehrere Hunderttausend Embeddings aus.
  • Bei noch größeren Datensätzen kann der Einsatz einer Vektor-Datenbank nötig werden.
  • Als Alternative lassen sich mit sqlite-vec auf Basis von SQLite zusätzliche Vektorsuchen und Filterungen umsetzen.

Fazit

  • Eine Vektor-Datenbank ist nicht zwingend erforderlich.
  • Die Kombination aus Parquet + polars ist eine starke Alternative, um Embeddings effizient zu speichern, zu durchsuchen und zu filtern.
  • Besonders bei kleineren Projekten ist die Nutzung von Parquet-Dateien schneller und kosteneffizienter.
  • Je nach Projekt ist es wichtig, zwischen Parquet und einer Vektor-Datenbank die passende Lösung auszuwählen.
  • Im GitHub-Repository lassen sich Code und Daten einsehen.

1 Kommentare

 
GN⁺ 2025-03-06
Hacker-News-Kommentare
  • Das Problem mit Parquet ist, dass es statisch ist. Für Fälle, in denen kontinuierliches Schreiben und Updates nötig sind, ist es nicht geeignet. Wenn ich jedoch Parquet-Dateien in DuckDB mit Object Storage verwendet habe, hatte ich gute Ergebnisse. Die Ladezeiten sind schnell

    • Wenn man sein eigenes Embedding-Modell hostet, kann man komprimierte numpy-float32-Arrays als Bytes übertragen und anschließend wieder in numpy-Arrays dekodieren
    • Ich persönlich bevorzuge SQLite mit der usearch-Erweiterung. Ich verwende binäre Vektoren und sortiere dann die Top 100 mit float32 neu. Für etwa 20.000 Elemente dauert das rund 2 ms, was schneller ist als LanceDB. Bei größeren Sammlungen könnte Lance gewinnen. In meinem Anwendungsfall hat jedoch jeder Nutzer eine dedizierte SQLite-Datei, daher funktioniert das gut
    • Für Portabilität gibt es Litestream
  • Wirklich ein großartiger Artikel. Ich verfolge deine Arbeit schon lange mit Freude. Für Leute, die tiefer in SQLite-Implementierungen einsteigen wollen, kann man noch ergänzen, dass DuckDB angefangen hat, einige Vektorähnlichkeitsfunktionen bereitzustellen, die Parquet lesen und diesen Anwendungsfall perfekt abdecken

  • Ich mag DataFrames immer noch nicht, aber Polars ist viel besser als pandas

    • Ich habe Zeitreihenberechnungen gemacht, im Grunde einfache Aktienkursanpassungen
    • Ich war überrascht, dass sich der Code tatsächlich lesen und testen ließ
    • Die Ausführung war so schnell, dass es kaputt wirkte
  • Schaut euch usearch von Unum an. Es schlägt alles und ist sehr einfach zu verwenden. Es macht genau das, was man braucht

  • Wenn ihr es ausprobieren wollt, könnt ihr auf HF lazy laden und Filter anwenden

    • Polars ist großartig in der Nutzung und ich empfehle es sehr. Es eignet sich hervorragend dafür, auf einem einzelnen Node die CPU auszulasten, und wenn ihr die Arbeit verteilen müsst, könnt ihr POLARS_MAX_THREADS auf Ray Actors anwenden und es je nach Auslastungsgrad des einzelnen Nodes abstimmen
  • Hier gibt es viele großartige Erkenntnisse

    • Ich frage mich, ob es besser ist, strukturierte Daten an eine Embedding-API zu übergeben oder unstrukturierte Daten. Wenn man ChatGPT fragt, sagt es, dass es besser ist, unstrukturierte Daten zu senden
    • Mein Anwendungsfall ist für jsonresume. Ich sende derzeit die komplette json-Version als String, um Embeddings zu erzeugen, experimentiere aber auch mit einem Modell, das zuerst resume.json in eine Volltextversion übersetzt und dann Embeddings erzeugt. Die Ergebnisse scheinen besser zu sein, aber ich habe dazu noch keine konkrete Meinung gesehen
    • Der Grund, warum unstrukturierte Daten besser sein könnten, ist, dass sie durch natürliche Sprache textuelle/semantische Bedeutung enthalten
  • In der Vespa-Dokumentation gibt es einen eleganten Trick, bei dem Vektoren in Binärform umgewandelt und dann als hexadezimale Darstellung verwendet werden

    • Dieser Trick kann genutzt werden, um die Payload-Größe zu reduzieren. Vespa unterstützt dieses Format und es ist besonders nützlich, wenn derselbe Vektor in einem Dokument mehrfach referenziert wird. In Fällen wie ColBERT oder ColPaLi (mit mehreren Embedding-Vektoren) kann die Größe der auf der Festplatte gespeicherten Vektoren erheblich reduziert werden
  • Polars + Parquet ist großartig in Bezug auf Portabilität und Performance. Dieser Beitrag konzentrierte sich auf Python-Portabilität, aber Polars hat eine leicht zu nutzende Rust-API, mit der sich die Engine an vielen Stellen einbetten lässt

  • Ich bin ein großer Fan von Polars, hatte aber noch nicht darüber nachgedacht, es zum Speichern von Embeddings zu verwenden (ich habe mit sqlite-vec experimentiert). Das scheint eine wirklich interessante Idee zu sein

  • Ich empfehle auch lancedb als weitere Bibliothek mit hervorragender Performance und Funktionen wie Volltextindizierung und Versionsverwaltung von Änderungen

    • Es ist eine Vektordatenbank und komplexer, kann aber auch ohne das Erstellen von Indizes verwendet werden und bietet außerdem hervorragende Zero-Copy-Arrow-Unterstützung für Polars und pandas