Aller au contenu

Lab 039 : Comparaison des bases de données vectorielles

Niveau : L300 Parcours : 📚 RAG Durée : ~50 min 💰 Coût : Free — Toutes les options ont des niveaux gratuits ou un mode local

Ce que vous apprendrez

  • Comprendre les différences clés entre les principales bases de données vectorielles
  • Comparer pgvector, Chroma, Qdrant et Azure AI Search sur la même tâche
  • Évaluer chacune sur : complexité de mise en place, vitesse de requête, filtrage, intégration cloud
  • Choisir la bonne base de données vectorielle pour les besoins de votre agent

Introduction

Le choix d'une base de données vectorielle est l'une des décisions d'architecture les plus importantes pour un agent basé sur RAG. Le « meilleur » choix dépend de la stack existante de votre équipe, des exigences de mise à l'échelle, des besoins de filtrage et de la stratégie cloud.

Les candidats :

Base de données Type Option gratuite Idéal pour
pgvector Extension PostgreSQL Azure free tier / local Équipes utilisant déjà PostgreSQL
Chroma Embarqué / serveur Entièrement open-source Développement local, petits projets
Qdrant BD vectorielle dédiée Qdrant Cloud free tier Production à grande échelle, filtrage avancé
Azure AI Search Service Azure Free tier (1 index) Azure natif, recherche hybride, entreprise

Prérequis

# Install all clients
pip install chromadb qdrant-client openai

# For Azure AI Search (optional)
pip install azure-search-documents

Pas de clés API nécessaires pour Chroma (local) et Qdrant (mode local). GitHub Token requis pour les embeddings :

export GITHUB_TOKEN=<your PAT>


Configuration : Fonction d'embedding partagée

Les quatre tests utilisent les mêmes données produit OutdoorGear et le même modèle d'embedding :

# shared.py
import os
import math
from openai import OpenAI

client = OpenAI(
    base_url="https://models.inference.ai.azure.com",
    api_key=os.environ["GITHUB_TOKEN"],
)

PRODUCTS = [
    {"id": "P001", "name": "TrailBlazer Tent 2P",          "category": "Tents",         "price": 249.99, "weight": 1800},
    {"id": "P002", "name": "Summit Dome 4P",                "category": "Tents",         "price": 549.99, "weight": 3200},
    {"id": "P003", "name": "TrailBlazer Solo",              "category": "Tents",         "price": 299.99, "weight":  850},
    {"id": "P004", "name": "ArcticDown -20°C Sleeping Bag", "category": "Sleeping Bags", "price": 389.99, "weight": 1400},
    {"id": "P005", "name": "SummerLight +5°C Sleeping Bag", "category": "Sleeping Bags", "price": 149.99, "weight":  700},
    {"id": "P006", "name": "Osprey Atmos 65L Backpack",     "category": "Backpacks",     "price": 289.99, "weight": 1980},
    {"id": "P007", "name": "DayHiker 22L Daypack",          "category": "Backpacks",     "price":  89.99, "weight":  580},
]

def embed(text: str) -> list[float]:
    response = client.embeddings.create(model="text-embedding-3-small", input=text)
    return response.data[0].embedding

def product_text(p: dict) -> str:
    return f"{p['name']}: {p['category']} product, ${p['price']:.2f}, {p['weight']}g"

Option A : Chroma (local, sans configuration)

Chroma est la façon la plus simple de démarrer — pur Python, s'exécute en mémoire ou sur disque :

# option_a_chroma.py
import chromadb
from shared import PRODUCTS, embed, product_text
import time

print("=== Option A: ChromaDB (Local) ===\n")

# In-memory collection (no persistence — great for prototyping)
chroma = chromadb.Client()
collection = chroma.create_collection("outdoorgear_products")

# Ingest
start = time.time()
collection.add(
    ids=[p["id"] for p in PRODUCTS],
    embeddings=[embed(product_text(p)) for p in PRODUCTS],
    documents=[product_text(p) for p in PRODUCTS],
    metadatas=[{"category": p["category"], "price": p["price"]} for p in PRODUCTS],
)
ingest_ms = (time.time() - start) * 1000

# Query
query = "lightweight tent for solo backpacking"
start = time.time()
results = collection.query(
    query_embeddings=[embed(query)],
    n_results=3,
)
query_ms = (time.time() - start) * 1000

print(f"Ingest: {ingest_ms:.0f}ms | Query: {query_ms:.0f}ms")
print(f"\nTop 3 results for '{query}':")
for doc, dist in zip(results["documents"][0], results["distances"][0]):
    similarity = 1 - dist  # Chroma returns distance, not similarity
    print(f"  {similarity:.3f} | {doc}")

# Filtered query (Chroma supports simple metadata filtering)
start = time.time()
filtered = collection.query(
    query_embeddings=[embed(query)],
    n_results=3,
    where={"category": "Tents"},   # ← metadata filter
)
filtered_ms = (time.time() - start) * 1000

print(f"\nFiltered to 'Tents' only ({filtered_ms:.0f}ms):")
for doc in filtered["documents"][0]:
    print(f"  {doc}")

Option B : Qdrant (mode serveur local)

Qdrant offre un filtrage avancé et passe à l'échelle à des centaines de millions de vecteurs :

# Run Qdrant locally with Docker (or use Qdrant Cloud free tier)
docker run -p 6333:6333 qdrant/qdrant
# option_b_qdrant.py
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue, Range
from shared import PRODUCTS, embed, product_text
import time

print("=== Option B: Qdrant (Local) ===\n")

client = QdrantClient("localhost", port=6333)

COLLECTION = "outdoorgear"
VECTOR_SIZE = 1536  # text-embedding-3-small

# Create collection
client.recreate_collection(
    collection_name=COLLECTION,
    vectors_config=VectorParams(size=VECTOR_SIZE, distance=Distance.COSINE),
)

# Ingest with payload (rich metadata support)
start = time.time()
points = [
    PointStruct(
        id=int(p["id"][1:]),   # P001 → 1
        vector=embed(product_text(p)),
        payload={
            "id":       p["id"],
            "name":     p["name"],
            "category": p["category"],
            "price":    p["price"],
            "weight":   p["weight"],
        },
    )
    for p in PRODUCTS
]
client.upsert(collection_name=COLLECTION, points=points)
ingest_ms = (time.time() - start) * 1000

# Semantic search
query = "something warm for cold winter nights"
start = time.time()
results = client.search(
    collection_name=COLLECTION,
    query_vector=embed(query),
    limit=3,
)
query_ms = (time.time() - start) * 1000

print(f"Ingest: {ingest_ms:.0f}ms | Query: {query_ms:.0f}ms")
print(f"\nTop 3 for '{query}':")
for r in results:
    print(f"  {r.score:.3f} | [{r.payload['id']}] {r.payload['name']}")

# Advanced filter: Tents under $300
start = time.time()
filtered = client.search(
    collection_name=COLLECTION,
    query_vector=embed("lightweight shelter"),
    limit=3,
    query_filter=Filter(
        must=[
            FieldCondition(key="category", match=MatchValue(value="Tents")),
            FieldCondition(key="price", range=Range(lte=300.0)),
        ]
    ),
)
filtered_ms = (time.time() - start) * 1000

print(f"\nTents under $300 ({filtered_ms:.0f}ms):")
for r in filtered:
    print(f"  {r.score:.3f} | [{r.payload['id']}] {r.payload['name']} ${r.payload['price']:.2f}")

Option C : pgvector (Azure PostgreSQL ou local)

Voir le Lab 031 pour la configuration complète de pgvector. Comparaison rapide :

# option_c_pgvector_query.py
import psycopg2
import os
from shared import embed

# Using Azure PostgreSQL with pgvector
conn = psycopg2.connect(
    host=os.environ["PG_HOST"],
    dbname=os.environ["PG_DATABASE"],
    user=os.environ["PG_USER"],
    password=os.environ["PG_PASSWORD"],
    sslmode="require",
)
cur = conn.cursor()

query_vec = embed("lightweight tent for solo backpacking")
query_str = "[" + ",".join(str(v) for v in query_vec) + "]"

# Cosine similarity search using <=> operator
cur.execute("""
    SELECT p.name, p.category, p.price_usd,
           1 - (pe.embedding <=> %s::vector) AS similarity
    FROM product_embeddings pe
    JOIN products p ON p.id = pe.product_id
    ORDER BY pe.embedding <=> %s::vector
    LIMIT 3;
""", [query_str, query_str])

print("=== Option C: pgvector ===")
for name, category, price, sim in cur.fetchall():
    print(f"  {sim:.3f} | {name} ({category}) ${price:.2f}")

Option D : Azure AI Search (recherche hybride)

Azure AI Search prend en charge de manière unique la recherche hybride : recherche vectorielle + BM25 par mots-clés combinée avec un re-classement sémantique :

# option_d_azure_search.py
# pip install azure-search-documents
from azure.search.documents import SearchClient
from azure.search.documents.models import VectorizedQuery
from azure.core.credentials import AzureKeyCredential
from shared import embed
import os

client = SearchClient(
    endpoint=os.environ["AZURE_SEARCH_ENDPOINT"],
    index_name="outdoorgear-products",
    credential=AzureKeyCredential(os.environ["AZURE_SEARCH_KEY"]),
)

query = "lightweight backpacking shelter"
query_vec = embed(query)

# Hybrid search: vector + keyword + semantic reranking
results = client.search(
    search_text=query,           # BM25 keyword search
    vector_queries=[
        VectorizedQuery(
            vector=query_vec,
            k_nearest_neighbors=3,
            fields="embedding",  # the vector field in your index
        )
    ],
    query_type="semantic",       # semantic reranking (requires Semantic tier)
    semantic_configuration_name="default",
    top=3,
)

print("=== Option D: Azure AI Search (Hybrid) ===")
for r in results:
    print(f"  @score={r['@search.score']:.3f} | [{r['id']}] {r['name']}")

Cadre de décision

Commencez ici :
Utilisez-vous déjà PostgreSQL ?
  → OUI → Utilisez pgvector (Lab 031)
  → NON ↓

Besoin d'Azure natif + recherche hybride ?
  → OUI → Azure AI Search
  → NON ↓

Développement local / prototype ?
  → OUI → Chroma (zéro configuration)
  → NON ↓

Besoin de filtrage avancé + grande échelle ?
  → OUI → Qdrant
  → NON → Chroma ou pgvector

Tableau comparatif

pgvector Chroma Qdrant Azure AI Search
Configuration Moyenne (BD nécessaire) Minimale Facile (Docker) Moyenne (Azure)
Dev local ✅ (Docker) ✅ (en mémoire) ✅ (Docker) ❌ (Azure uniquement)
Recherche hybride ✅ (manuelle) ✅ (intégrée)
Filtrage SQL WHERE Métadonnées basiques Avancé OData complet
Échelle Modérée (< 1M) Petite (< 100K) Grande (> 100M) Grande (entreprise)
Intégration Azure ✅ (PG managé) Partielle ✅ (native)
Coût (niveau gratuit) Free tier PG Gratuit Qdrant Cloud free 1 index gratuit

🧠 Quiz de connaissances

1. Qu'est-ce que la recherche hybride et pourquoi est-elle souvent meilleure que la recherche vectorielle pure ?

La recherche hybride combine la recherche vectorielle (sémantique) avec la recherche par mots-clés (BM25) et classe les résultats en utilisant les deux signaux. La recherche vectorielle excelle dans la compréhension sémantique (« abri chaud » → sac de couchage) mais peut manquer les correspondances exactes (un identifiant produit spécifique). BM25 est excellent pour les correspondances exactes de mots-clés mais manque les synonymes. Les combiner surpasse l'un ou l'autre seul, en particulier pour les noms de produits, les SKU et la terminologie spécialisée.

2. Pourquoi choisiriez-vous pgvector plutôt qu'une base de données vectorielle dédiée comme Qdrant ?

Si vous avez déjà PostgreSQL comme base de données principale, pgvector ajoute la recherche vectorielle sans ajouter un autre service à exploiter, maintenir et payer. Les données cohabitent avec vos données relationnelles — vous pouvez faire un JOIN entre les enregistrements produit et leurs embeddings dans une seule requête. Pour la plupart des applications de moins de 1M de vecteurs, les performances de pgvector sont excellentes. Choisissez Qdrant quand vous avez besoin de > 100M de vecteurs ou d'un filtrage très avancé.

3. Qu'est-ce que l'index IVFFlat dans pgvector et quand devriez-vous utiliser HNSW à la place ?

IVFFlat (Inverted File Index with Flat quantization) : rapide à construire, utilise moins de mémoire, bon pour les jeux de données qui ne changent pas fréquemment. Utilise une recherche approximative — le paramètre lists contrôle le compromis rappel/vitesse. HNSW (Hierarchical Navigable Small World) : meilleur rappel, requêtes plus rapides, mais utilisation mémoire plus élevée et construction plus lente. Utilisez IVFFlat pour les jeux de données < 1M qui ne changent pas beaucoup ; utilisez HNSW pour les jeux de données fréquemment mis à jour ou quand le rappel est critique. Les deux nécessitent pgvector ≥ 0.5.0.


Résumé

Pour le scénario du hub d'apprentissage OutdoorGear (< 10K produits, infrastructure Azure, équipe connaissant SQL) :

Recommandé : pgvector sur Azure Database for PostgreSQL Flexible Server.

  • Pas de nouveau service à apprendre
  • SQL + vectoriel dans une seule requête
  • Niveau gratuit disponible
  • Prêt pour la production avec les migrations du Lab 031

Prochaines étapes