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
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
numpy-float32-Arrays als Bytes übertragen und anschließend wieder innumpy-Arrays dekodierenusearch-Erweiterung. Ich verwende binäre Vektoren und sortiere dann die Top 100 mitfloat32neu. 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 gutWirklich 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
Schaut euch
usearchvon Unum an. Es schlägt alles und ist sehr einfach zu verwenden. Es macht genau das, was man brauchtWenn ihr es ausprobieren wollt, könnt ihr auf HF lazy laden und Filter anwenden
POLARS_MAX_THREADSauf Ray Actors anwenden und es je nach Auslastungsgrad des einzelnen Nodes abstimmenHier gibt es viele großartige Erkenntnisse
jsonresume. Ich sende derzeit die komplettejson-Version als String, um Embeddings zu erzeugen, experimentiere aber auch mit einem Modell, das zuerstresume.jsonin eine Volltextversion übersetzt und dann Embeddings erzeugt. Die Ergebnisse scheinen besser zu sein, aber ich habe dazu noch keine konkrete Meinung gesehenIn der Vespa-Dokumentation gibt es einen eleganten Trick, bei dem Vektoren in Binärform umgewandelt und dann als hexadezimale Darstellung verwendet 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-vecexperimentiert). Das scheint eine wirklich interessante Idee zu seinIch empfehle auch
lancedbals weitere Bibliothek mit hervorragender Performance und Funktionen wie Volltextindizierung und Versionsverwaltung von Änderungen