Pourquoi le partitionnement est-il nécessaire ? Les limites de pgvector
Par défaut, pgvector stocke les données vectorielles dans une table unique. Lorsque le volume de données augmente, cela crée trois goulets d'étranglement majeurs :
- Performances des requêtes : Le temps de scan séquentiel d'une table non partitionnée croît linéairement avec la taille des données. Même avec des index HNSW ou IVFFlat, la latence de requête augmente significativement au-delà de 10 millions de vecteurs.
- Maintenance de l'index : Le temps de construction d'un index HNSW est proportionnel au carré du volume de données. La création d'un index sur 100 millions de vecteurs peut prendre plusieurs heures.
- Évolutivité du stockage : Une table unique ne peut pas s'étendre sur plusieurs périphériques de stockage, ce qui rend l'extension verticale coûteuse.
Stratégie 1 : Partitionenment de tables intégré à PostgreSQL
La méthode la plus simple consiste à utiliser les fonctionnalités de partitionnement de tables de PostgreSQL.
Partitionnement par liste (catégorie métier)
CREATE TABLE produits (
id bigserial,
categorie_id int,
embedding vector(3)
) PARTITION BY LIST(categorie_id);
CREATE TABLE produits_electronique PARTITION OF produits FOR VALUES IN (1);
CREATE TABLE produits_vetements PARTITION OF produits FOR VALUES IN (2);
Partitionnement par plage (données temporelles)
CREATE TABLE series_temporelles (
horodatage timestamptz,
metrique vector(100)
) PARTITION BY RANGE (horodatage);
CREATE TABLE series_temporelles_202401 PARTITION OF series_temporelles
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
Stratégie 2 : Partitionnement par hachage
Lorsqu'aucune clé de partitionnement métier claire n'est disponible, le hachage permet de répartir les données de manière uniforme.
CREATE TABLE articles (
identifiant bigserial,
representation vector(1536)
) PARTITION BY HASH (identifiant);
CREATE TABLE articles_0 PARTITION OF articles FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE articles_1 PARTITION OF articles FOR VALUES WITH (MODULUS 8, REMAINDER 1);
-- ... jusqu'à articles_7
Chaque partition peut utiliser des paramètres d'index différents adaptés à son schéma d'accès.
Stratégie 3 : Cluster distribué avec Citus
Pour les données dépassant l'échelle du milliard, Citus permet de transformer PostgreSQL en une base de données distribuée avec une extension horizontale réelle.
-- Sur le nœud de coordination Citus
SELECT create_distributed_table('articles', 'identifiant');
-- L'index est automatiquement créé sur tous les nœuds de travail
CREATE INDEX ON articles USING hnsw (representation vector_l2_ops);
-- La requête est automatiquement routée vers les shards concernés
SELECT * FROM articles ORDER BY representation <-> '[3,1,2]' LIMIT 5;
Stratégie 4 : Partitionnement au niveau applicatif
En l'absence de solution comme Citus, la logique de routage peut être implémentée directement dans l'application.
import psycopg2
from hashlib import sha256
def obtenir_connexion(id_article):
# Calcul de l'index de shard (0-7)
index_shard = int(sha256(str(id_article).encode()).hexdigest(), 16) % 8
# Connexion à la base de données correspondante
return psycopg2.connect(f"host=pg-shard-{index_shard} dbname=vecteurs user=postgres")
def inserer_vecteur(id_article, embedding):
connexion = obtenir_connexion(id_article)
with connexion.cursor() as curseur:
curseur.execute("INSERT INTO articles (identifiant, representation) VALUES (%s, %s)",
(id_article, embedding))
connexion.commit()
def recherche_globale(vecteur_requete, limite=5):
resultats = []
for index_shard in range(8):
connexion = psycopg2.connect(f"host=pg-shard-{index_shard} dbname=vecteurs user=postgres")
with connexion.cursor() as curseur:
curseur.execute("""
SELECT identifiant, representation <-> %s AS distance
FROM articles
ORDER BY distance
LIMIT %s
""", (vecteur_requete, limite))
resultats.extend(curseur.fetchall())
return sorted(resultats, key=lambda x: x[1])[:limite]
Comparaison des stratégies
| Stratégie | Échelle de données | Complexité | Capacité d'extension |
|---|---|---|---|
| Table partitionnée | Millions à dizaines de millions | Faible | Moyenne |
| Cluster Citus | Au-delà du milliard | Moyenne | Élevée |
| Partitionnement applicatif | Toute échelle | Élevée | Très élevée |
Optimisations de performance
1. Choix de la clé de partitionnement
Sélectionner une clé avec une cardinalité élevée et une distribution uniforme pour éviter le déséquilibre des données. Ordre de priorité recommandé : identifiant utilisateur/article (hachage), horodatage (plage), catégorie métier (liste).
2. Ajustement des paramètres d'index
Utiliser des paramètres d'index différenciés selon les partitions :
-- Partition chaude : précision de recherche élevée
SET hnsw.ef_search = 200;
-- Partition froide : vitesse de recherche prioritaire
SET hnsw.ef_search = 40;
3. Requêtes parallèles
Exploiter les capacités de requêtes parallèles de PostgreSQL :
SET max_parallel_workers_per_gather = 8;
4. Surveillance et maintenance
Vérifier régulièrement la taille et les performances de chaque partition :
SELECT nom_partition, pg_size_pretty(pg_total_relation_size(nom_partition))
FROM pg_catalog.pg_partitions
WHERE table_name = 'articles';