Techniques de partitionnement pour pgvector : gestion des données vectorielles à grande échelle

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';

Étiquettes: pgvector PostgreSQL partitionnement de base de données vecteurs de grande dimension Citus

Publié le 20 juin à 21h25