La meilleure façon d’utiliser des embeddings de texte de manière portable : Parquet et Polars
- Les embeddings de texte sont des vecteurs générés par des grands modèles de langage, qui représentent numériquement des mots, des phrases et des documents
- En février 2025, un total de 32 254 embeddings de cartes de « Magic: The Gathering » a été généré
- Cela permet d’analyser mathématiquement les similarités entre cartes à partir de leurs propriétés de design et de gameplay
- Il est possible de visualiser les embeddings générés via une réduction de dimension en 2D avec UMAP
- Le modèle d’embedding utilisé est gte-modernbert-base, et le processus détaillé est documenté dans le dépôt GitHub
- Le jeu de données d’embeddings est disponible sur Hugging Face
Repenser la nécessité d’une base de données vectorielle
- En général, on utilise des bases de données vectorielles (faiss, qdrant, Pinecone) pour stocker et rechercher des embeddings
- Cependant, les bases de données vectorielles demandent une configuration complexe, et les services cloud peuvent coûter cher
- Pour des jeux de données de petite taille (de l’ordre de quelques dizaines de milliers d’éléments), il est possible d’effectuer des recherches de similarité rapides avec numpy, sans base de données vectorielle
- En utilisant l’opération de
dot product de numpy, on peut calculer simplement la similarité cosinus ; sur 32 254 embeddings, cela prend en moyenne 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
- Utiliser une base de données vectorielle implique souvent une forte dépendance à une bibliothèque ou à un service spécifique
- Si les embeddings sont générés sur un serveur GPU puis téléchargés en local, il faut un mode de stockage et de transfert efficace
Les pires façons de stocker des embeddings
- Fichiers CSV
- Stocker des données en virgule flottante (
float32) sous forme de texte multiplie la taille par plus de 6
- Le tutoriel officiel d’OpenAI recommande lui aussi le CSV uniquement pour de petits jeux de données
- En enregistrant avec
.savetxt() de numpy, la taille du fichier grimpe à 631,5 MB
- Fichiers pickle
- Ils permettent un enregistrement et un chargement rapides, mais présentent des risques de sécurité et une compatibilité limitée entre versions
- La taille du fichier est de 94,49 MB, identique à celle occupée en mémoire, mais la portabilité est faible
Des méthodes acceptables, mais pas optimales
- Le format
.npy de numpy
- Le paramètre
allow_pickle=False permet d’empêcher l’enregistrement au format pickle
- La taille du fichier et la vitesse sont identiques à celles de pickle, mais il est difficile d’y stocker des métadonnées individuelles
- Le problème d’un stockage séparé des métadonnées
- Si l’on stocke les embeddings dans un tableau numpy (
.npy), les informations de carte (nom, texte, etc.) sont séparées des embeddings
- Quand les données changent (ajout/suppression), il devient difficile de faire correspondre correctement métadonnées et embeddings
- Les bases de données vectorielles stockent les métadonnées avec les vecteurs et offrent des fonctions de filtrage
La meilleure façon de stocker des embeddings : Parquet + polars
Présentation du format de fichier Parquet
- Apache Parquet est un format de stockage colonnaire qui permet de définir clairement le type de données de chaque colonne
- Il peut stocker des données sous forme de listes (tableaux
float32), ce qui le rend adapté aux embeddings
- Il offre de meilleures performances d’écriture et de lecture que le CSV, et permet de charger sélectivement seulement une partie des données
- Il propose la compression, mais comme les données d’embedding contiennent peu de redondance, le gain reste limité
Utiliser des fichiers Parquet en Python
- Enregistrement et chargement d’un fichier Parquet avec pandas :
df = pd.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])
df
- pandas gère mal les données imbriquées (listes) et les convertit en objets numpy
object
- Lors de la conversion en tableau numpy, une opération supplémentaire (
np.vstack()) est nécessaire, ce qui peut dégrader les performances
- Enregistrement et chargement d’un fichier Parquet avec polars :
df = pl.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])
df
- polars conserve les tableaux
float32 tels quels, et un appel à to_numpy() renvoie immédiatement un tableau numpy 2D
- Le paramètre
allow_copy=False permet d’éviter des copies de données inutiles
embeddings = df["embedding"].to_numpy(allow_copy=False)
- Il est aussi simple d’ajouter de nouveaux embeddings : il suffit d’ajouter une colonne puis d’enregistrer
df = df.with_columns(embedding=embeddings)
df.write_parquet("mtg-embeddings.parquet")
Recherche de similarité et filtrage avec Parquet + polars
- Il est possible de filtrer d’abord les données selon certaines conditions, puis d’effectuer la recherche de similarité
- Exemple : trouver des cartes similaires à une carte donnée (
query_embed), mais uniquement parmi celles de type « Sorcery » et dont la couleur inclut « Black »
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]
- Le temps d’exécution moyen est de 1,48 ms : 37 % plus lent qu’une recherche sur l’ensemble des données, mais cela reste très rapide
Alternatives pour traiter de gros volumes de données vectorielles
- L’approche Parquet + dot product est suffisante jusqu’à plusieurs centaines de milliers d’embeddings
- Pour des jeux de données plus volumineux, l’usage d’une base de données vectorielle peut devenir nécessaire
- Comme alternative, on peut utiliser sqlite-vec, basé sur SQLite, pour ajouter des capacités de recherche vectorielle et de filtrage
Conclusion
- Une base de données vectorielle n’est pas forcément indispensable
- La combinaison Parquet + polars est une alternative puissante pour stocker, rechercher et filtrer efficacement des embeddings
- En particulier pour les projets de petite taille, utiliser des fichiers Parquet peut être plus rapide et plus rentable
- Selon le projet, il est important de choisir la solution adaptée entre Parquet et une base de données vectorielle
- Le code et les données sont disponibles dans le dépôt GitHub
1 commentaires
Avis Hacker News
Le problème de Parquet, c’est qu’il est statique. Il n’est pas adapté aux cas où des écritures et des mises à jour continues sont nécessaires. En revanche, j’ai obtenu de bons résultats en utilisant des fichiers Parquet avec DuckDB et du stockage objet. Les temps de chargement sont rapides
numpy float32sous forme d’octets, puis les redécoder en tableaux numpyfloat32. Cela prend environ 2 ms pour environ 20 000 éléments, ce qui est plus rapide que LanceDB. Sur des collections plus grandes, Lance peut l’emporter. Mais pour mon cas d’usage, chaque utilisateur a son propre fichier SQLite dédié, donc cela fonctionne bienArticle vraiment génial. J’apprécie votre travail depuis longtemps. Pour les personnes qui se lancent dans une implémentation SQLite, j’ajouterais que DuckDB a commencé à proposer quelques fonctionnalités de similarité vectorielle qui lisent Parquet et couvrent parfaitement ce cas d’usage
Je n’aime toujours pas vraiment les dataframes, mais Polars est bien meilleur que pandas
Regardez usearch de Unum. Ça bat tout le reste et c’est très facile à utiliser. Ça fait exactement ce qu’il faut
Si vous voulez essayer, vous pouvez faire du lazy loading depuis HF et appliquer des filtres
POLARS_MAX_THREADSà un Ray Actor afin de l’ajuster selon le niveau de saturation d’un nœud uniqueIl y a beaucoup d’excellentes trouvailles
resume.jsonen une version texte complète avant de générer les embeddings. Les résultats semblent meilleurs, mais je n’ai pas vu d’avis très concrets sur le sujetIl y a une astuce élégante dans la documentation de Vespa qui consiste à convertir les vecteurs en binaire, puis à utiliser une représentation hexadécimale
Polars + Parquet, c’est excellent en matière de portabilité et de performances. Ce billet mettait l’accent sur la portabilité Python, mais Polars dispose aussi d’une API Rust facile à utiliser qui permet d’embarquer le moteur à divers endroits
Je suis un grand fan de Polars, mais je n’avais pas pensé à l’utiliser pour stocker des embeddings (j’expérimentais avec sqlite-vec). Cela me semble être une idée vraiment intéressante
Je recommande aussi lancedb, une autre bibliothèque dotée d’excellentes performances et de fonctionnalités comme l’indexation full-text et le versioning des modifications