---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Bases vectorielles

```{code-cell} python
:tags: [hide-input]

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns
from matplotlib.colors import LinearSegmentedColormap

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
```

Les bases de données relationnelles excellent pour trouver une ligne dont l'identifiant est exactement 42, ou dont le statut est exactement "actif". Mais comment retrouver les documents dont le **sens** est proche d'une requête en langage naturel ? Comment détecter que deux descriptions de produits sont des doublons malgré une formulation différente ? Ces questions relèvent de la **recherche par similarité**, et c'est précisément ce que les bases vectorielles permettent.

Le principe : transformer chaque document, image ou audio en un vecteur dense de nombres réels (un **embedding**), puis indexer ces vecteurs de telle sorte que l'on puisse retrouver rapidement ceux qui sont les plus proches d'un vecteur requête. Cette approche est au cœur des moteurs de recherche sémantique, des systèmes RAG (*Retrieval-Augmented Generation*) et des recommandations modernes.

## Embeddings

```{prf:definition}
:label: ch17-def-embedding

Un **embedding** est une représentation vectorielle dense d'un objet (texte, image, son) dans un espace de haute dimension. Un modèle de type *sentence-transformer* encode une phrase de longueur arbitraire en un vecteur de dimension fixe (typiquement 384, 768 ou 1536). Deux objets sémantiquement proches ont des vecteurs proches dans l'espace vectoriel — c'est la propriété fondamentale qui rend la recherche par similarité possible.
```

```{prf:remark}
:label: ch17-rem-sentence-transformers

Les modèles de la famille **sentence-transformers** (Hugging Face) sont les plus utilisés pour les embeddings textuels. Le modèle `all-MiniLM-L6-v2` produit des vecteurs de dimension 384 et pèse environ 80 Mo — un bon compromis entre qualité et légèreté. Les modèles OpenAI (`text-embedding-3-small`) produisent des vecteurs de dimension 1536 via API. Pour les images, on utilise CLIP ; pour le code, CodeBERT ou StarCoder.
```

## Mesures de similarité

```{prf:definition}
:label: ch17-def-cosinus

La **similarité cosinus** entre deux vecteurs **u** et **v** est :

$$\text{cos}(\mathbf{u}, \mathbf{v}) = \frac{\mathbf{u} \cdot \mathbf{v}}{\|\mathbf{u}\| \cdot \|\mathbf{v}\|}$$

Elle vaut 1 si les vecteurs sont identiques, 0 s'ils sont orthogonaux, −1 s'ils sont opposés. Pour des embeddings normalisés (norme unitaire), la similarité cosinus est équivalente au produit scalaire. C'est la mesure de référence pour la recherche sémantique textuelle, car elle est insensible à la magnitude des vecteurs.
```

```{prf:definition}
:label: ch17-def-distance-l2

La **distance euclidienne** (L2) entre deux vecteurs est :

$$d(\mathbf{u}, \mathbf{v}) = \|\mathbf{u} - \mathbf{v}\|_2 = \sqrt{\sum_i (u_i - v_i)^2}$$

Elle est sensible à la magnitude et donc moins adaptée aux embeddings textuels qui peuvent varier en norme. Elle est en revanche naturelle pour les embeddings d'images et les coordonnées géographiques. FAISS utilise par défaut la distance L2.
```

```{prf:remark}
:label: ch17-rem-choix-metrique

Le choix de la métrique dépend du modèle d'embedding : si le modèle produit des vecteurs normalisés (comme `sentence-transformers`), distance L2 et similarité cosinus sont équivalentes. Il est recommandé de normaliser les vecteurs avant indexation pour bénéficier des optimisations de produit scalaire interne (IndexFlatIP dans FAISS).
```

## ANN : Approximate Nearest Neighbor

```{prf:definition}
:label: ch17-def-ann

La **recherche exacte des K plus proches voisins** (kNN exacte) nécessite de calculer la distance entre le vecteur requête et chaque vecteur de l'index — O(N·D) où N est le nombre de vecteurs et D la dimension. Pour N = 10 millions et D = 768, c'est prohibitif. Les algorithmes **ANN** (Approximate Nearest Neighbor) sacrifient une légère précision pour gagner plusieurs ordres de grandeur en vitesse, grâce à des structures d'index pré-construites.
```

```{prf:definition}
:label: ch17-def-hnsw

**HNSW** (*Hierarchical Navigable Small World*) est l'algorithme ANN le plus populaire. Il construit un graphe hiérarchique multi-couches où les couches supérieures sont des graphes épars (navigation rapide) et les couches inférieures sont denses (précision fine). La recherche part de la couche haute, navigue vers le voisin le plus proche à chaque niveau, puis descend. Complexité de recherche : O(log N). HNSW est utilisé par FAISS, Qdrant, Weaviate et pgvector.
```

```{prf:definition}
:label: ch17-def-ivf-pq

**IVF** (*Inverted File Index*) divise l'espace vectoriel en clusters (k-means). Pour une requête, on cherche uniquement dans les `nprobe` clusters les plus proches — réduisant le calcul de O(N) à O(N/k · nprobe). **PQ** (*Product Quantization*) compresse chaque vecteur en un code compact (8 à 64 octets) en le décomposant en sous-vecteurs quantifiés, permettant de stocker des milliards de vecteurs en mémoire limitée. La combinaison IVF+PQ (IndexIVFPQ dans FAISS) est le standard pour les très grandes collections.
```

## FAISS

FAISS (*Facebook AI Similarity Search*) est une bibliothèque C++ exposée en Python pour la recherche de plus proches voisins. Elle supporte CPU et GPU.

```{prf:definition}
:label: ch17-def-faiss-index

Les index FAISS principaux :

- **IndexFlatL2** : recherche exacte par distance L2, aucune structure d'index, O(N·D) à la recherche. Référence de précision.
- **IndexFlatIP** : recherche exacte par produit scalaire (équivalent à cosinus sur vecteurs normalisés).
- **IndexIVFFlat** : IVF sans compression. Rapide, précis, nécessite un entraînement sur des données représentatives.
- **IndexHNSWFlat** : HNSW sans compression. Excellent compromis vitesse/précision.
- **IndexIVFPQ** : IVF + Product Quantization. Pour les très grandes collections (> 10 M vecteurs).
```

## Cellule exécutable : FAISS + recherche sémantique

```{code-cell} python
import numpy as np
import faiss

# Corpus de documents (simulation d'embeddings sémantiques)
# En production, on utiliserait sentence-transformers pour encoder ces textes
documents = [
    "Python est un langage de programmation polyvalent",
    "SQL permet d'interroger des bases de données relationnelles",
    "Les réseaux de neurones apprennent à partir de données",
    "Redis est une base de données en mémoire très rapide",
    "PostgreSQL supporte les requêtes analytiques avancées",
    "Le machine learning utilise des algorithmes d'optimisation",
    "Les bases vectorielles stockent des embeddings de haute dimension",
    "Django est un framework web Python complet",
    "DuckDB excelle pour l'analyse de données OLAP",
    "Les transformers ont révolutionné le traitement du langage naturel",
    "SQLAlchemy est un ORM pour Python",
    "Les index accélèrent considérablement les requêtes SQL",
    "Les embeddings capturent la sémantique des textes",
    "MongoDB est une base de données orientée documents",
    "Le sharding distribue les données sur plusieurs serveurs",
]

# Simulation d'embeddings réalistes avec structure sémantique
np.random.seed(42)
DIM = 128  # dimension réduite pour la démo

def simulate_embeddings(texts, dim):
    """
    Simule des embeddings avec une structure sémantique cohérente :
    - cluster 'bases de données' : SQL, Redis, PostgreSQL, MongoDB...
    - cluster 'ML/IA' : réseaux de neurones, transformers, embeddings...
    - cluster 'Python' : Django, SQLAlchemy, DuckDB...
    """
    # Centres de clusters thématiques
    clusters = {
        'db':     np.random.randn(dim) * 0.1,
        'ml':     np.random.randn(dim) * 0.1 + 2.0,
        'python': np.random.randn(dim) * 0.1 - 2.0,
    }
    # Association texte -> cluster
    theme_map = {
        0: 'python', 1: 'db',     2: 'ml',   3: 'db',
        4: 'db',     5: 'ml',     6: 'db',   7: 'python',
        8: 'db',     9: 'ml',     10: 'python', 11: 'db',
        12: 'ml',    13: 'db',    14: 'db',
    }
    embeddings = np.zeros((len(texts), dim), dtype='float32')
    for i in range(len(texts)):
        theme = theme_map.get(i, 'db')
        embeddings[i] = clusters[theme] + np.random.randn(dim) * 0.3
    return embeddings

embeddings = simulate_embeddings(documents, DIM)

# Normalisation pour cosinus via produit scalaire
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
embeddings_norm = (embeddings / norms).astype('float32')

# Construction de l'index FAISS
# IndexFlatIP : recherche exacte par produit scalaire (= cosinus sur vecteurs normalisés)
index_exact = faiss.IndexFlatIP(DIM)
index_exact.add(embeddings_norm)
print(f"Index FAISS créé : {index_exact.ntotal} vecteurs, dimension {DIM}")

# --- Requête 1 : recherche sémantique ---
# Simuler l'embedding d'une requête "bases de données SQL performance"
# On crée un vecteur proche du cluster 'db'
query_db = np.random.randn(DIM).astype('float32') * 0.3
query_db += np.array([0.1] * DIM, dtype='float32')  # centroïde cluster 'db'
query_db_norm = (query_db / np.linalg.norm(query_db)).reshape(1, -1)

k = 5
scores, indices = index_exact.search(query_db_norm, k)

print("\nRequête : 'bases de données et performances'")
print("Top-5 résultats :")
for rank, (idx, score) in enumerate(zip(indices[0], scores[0]), 1):
    print(f"  {rank}. [{score:.3f}] {documents[idx]}")

# --- Index IVF : version approchée ---
nlist = 4  # nombre de clusters (petit corpus)
quantizer = faiss.IndexFlatIP(DIM)
index_ivf = faiss.IndexIVFFlat(quantizer, DIM, nlist, faiss.METRIC_INNER_PRODUCT)
index_ivf.train(embeddings_norm)
index_ivf.add(embeddings_norm)
index_ivf.nprobe = 2  # chercher dans les 2 clusters les plus proches

scores_ivf, indices_ivf = index_ivf.search(query_db_norm, k)
print(f"\nIndex IVF (nlist={nlist}, nprobe=2) — mêmes {k} résultats trouvés : "
      f"{set(indices[0]) == set(indices_ivf[0])}")
```

```{code-cell} python
# Calcul de la matrice de similarité cosinus
similarity_matrix = embeddings_norm @ embeddings_norm.T

# Labels courts pour la visualisation
short_labels = [
    "Python polyvalent", "SQL requêtes", "Réseaux neurones", "Redis mémoire",
    "PostgreSQL OLAP", "ML algorithmes", "Bases vectorielles", "Django web",
    "DuckDB OLAP", "Transformers NLP", "SQLAlchemy ORM", "Index SQL",
    "Embeddings sémantique", "MongoDB docs", "Sharding distribué"
]

print("Matrice de similarité cosinus calculée :", similarity_matrix.shape)
print(f"Similarité max (hors diagonale) : {np.max(similarity_matrix - np.eye(15)):.3f}")
print(f"Similarité min : {np.min(similarity_matrix):.3f}")
```

## Visualisation : PCA 2D et heatmap de similarité

```{code-cell} python
:tags: [hide-input]

from sklearn.decomposition import PCA

# PCA 2D pour visualiser les clusters
pca = PCA(n_components=2, random_state=42)
coords_2d = pca.fit_transform(embeddings_norm)

fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# --- Plot 1 : PCA des embeddings ---
ax = axes[0]
theme_map_vis = {
    0: (2, 'Python'), 1: (0, 'Bases de données'), 2: (1, 'ML/IA'),
    3: (0, 'Bases de données'), 4: (0, 'Bases de données'), 5: (1, 'ML/IA'),
    6: (0, 'Bases de données'), 7: (2, 'Python'), 8: (0, 'Bases de données'),
    9: (1, 'ML/IA'), 10: (2, 'Python'), 11: (0, 'Bases de données'),
    12: (1, 'ML/IA'), 13: (0, 'Bases de données'), 14: (0, 'Bases de données')
}

palette = sns.color_palette("muted", 3)
theme_names = ['Bases de données', 'ML/IA', 'Python']
theme_plotted = set()

for i, (x, y) in enumerate(coords_2d):
    theme_idx, theme_name = theme_map_vis[i]
    label = theme_name if theme_name not in theme_plotted else None
    ax.scatter(x, y, color=palette[theme_idx], s=100, alpha=0.8,
               label=label, edgecolors='white', linewidth=0.8)
    theme_plotted.add(theme_name)
    ax.annotate(short_labels[i][:16], (x, y), textcoords="offset points",
                xytext=(5, 3), fontsize=6.5, color='#333')

ax.set_title("PCA 2D des embeddings\n(simulation avec structure sémantique)", fontsize=11, fontweight='bold')
ax.set_xlabel(f"PC1 ({pca.explained_variance_ratio_[0]*100:.1f}% variance)")
ax.set_ylabel(f"PC2 ({pca.explained_variance_ratio_[1]*100:.1f}% variance)")
ax.legend(fontsize=9, loc='best')
ax.grid(True, alpha=0.3)

# --- Plot 2 : Heatmap de similarité ---
ax2 = axes[1]
very_short = [lbl[:12] for lbl in short_labels]

cmap = LinearSegmentedColormap.from_list("sim", ["#f7fbff", "#2171b5"])
im = ax2.imshow(similarity_matrix, cmap=cmap, vmin=-0.2, vmax=1.0, aspect='auto')
ax2.set_xticks(range(15))
ax2.set_yticks(range(15))
ax2.set_xticklabels(very_short, rotation=45, ha='right', fontsize=6.5)
ax2.set_yticklabels(very_short, fontsize=6.5)
ax2.set_title("Heatmap de similarité cosinus", fontsize=11, fontweight='bold')
plt.colorbar(im, ax=ax2, shrink=0.8, label='Similarité cosinus')

plt.suptitle("Visualisation des embeddings vectoriels", fontsize=13, fontweight='bold', y=1.01)
plt.tight_layout()
plt.show()
```

## pgvector : PostgreSQL vectoriel

pgvector est une extension PostgreSQL qui ajoute un type `VECTOR` et les opérateurs de similarité, transformant PostgreSQL en base vectorielle complète.

```{prf:definition}
:label: ch17-def-pgvector

**pgvector** ajoute à PostgreSQL :
- Le type `VECTOR(n)` pour stocker un vecteur de dimension n.
- L'opérateur `<->` : distance L2 euclidienne.
- L'opérateur `<#>` : produit scalaire négatif (inner product, pour cosinus sur vecteurs normalisés).
- L'opérateur `<=>` : distance cosinus.
- Les index `ivfflat` et `hnsw` pour les recherches ANN.
```

Les blocs suivants nécessitent PostgreSQL avec l'extension pgvector installée.

```sql
-- Activation de l'extension
CREATE EXTENSION IF NOT EXISTS vector;

-- Table avec colonne vectorielle
CREATE TABLE articles (
    id          SERIAL PRIMARY KEY,
    titre       TEXT NOT NULL,
    contenu     TEXT,
    embedding   VECTOR(384)    -- dimension du modèle MiniLM
);

-- Insertion d'un article avec son embedding
INSERT INTO articles (titre, contenu, embedding)
VALUES (
    'Introduction à SQL',
    'SQL est le langage de requête standard...',
    '[0.12, -0.34, 0.89, ...]'   -- vecteur de 384 composantes
);

-- Recherche par similarité cosinus (5 plus proches voisins)
SELECT id, titre,
       1 - (embedding <=> '[0.11, -0.33, 0.87, ...]'::vector) AS similarite
FROM articles
ORDER BY embedding <=> '[0.11, -0.33, 0.87, ...]'::vector
LIMIT 5;
```

```sql
-- Index HNSW pour la recherche ANN (recommandé pour > 100 000 vecteurs)
CREATE INDEX ON articles USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- Index IVFFlat (alternative, plus léger à construire)
CREATE INDEX ON articles USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

-- Recherche hybride : similarité vectorielle + filtre SQL classique
SELECT id, titre, similarite
FROM (
    SELECT id, titre,
           1 - (embedding <=> $1::vector) AS similarite
    FROM articles
    WHERE created_at > '2024-01-01'  -- filtre SQL standard
    ORDER BY embedding <=> $1::vector
    LIMIT 20
) sub
WHERE similarite > 0.7;
```

```{prf:remark}
:label: ch17-rem-pgvector-vs-specialise

pgvector permet de **ne pas ajouter un nouveau système** à l'infrastructure : on stocke vecteurs et données relationnelles dans le même PostgreSQL, avec des transactions ACID et les jointures habituelles. En contrepartie, il est moins performant que des bases vectorielles spécialisées (Pinecone, Qdrant, Weaviate) pour des collections de plusieurs centaines de millions de vecteurs. Pour la plupart des projets (< 10 M vecteurs), pgvector est le choix pragmatique.
```

## Pipeline RAG simplifié

```{prf:definition}
:label: ch17-def-rag

**RAG** (*Retrieval-Augmented Generation*) est un pattern d'architecture qui augmente un LLM avec une base de connaissances externe. Au lieu de générer une réponse à partir des seuls paramètres du modèle, on récupère d'abord les passages les plus pertinents par recherche vectorielle, puis on les injecte dans le contexte du prompt. Cela améliore la précision factuelle et réduit les hallucinations.
```

```{code-cell} python
:tags: [hide-input]

# Diagramme du pipeline RAG
fig, ax = plt.subplots(figsize=(14, 5))
ax.set_xlim(0, 14); ax.set_ylim(0, 5); ax.axis('off')
ax.set_title("Pipeline RAG (Retrieval-Augmented Generation)", fontsize=13, fontweight='bold', pad=10)

palette_rag = sns.color_palette("muted", 6)

etapes_index = [
    (0.3, 2.5, "Documents\nsource", palette_rag[0]),
    (2.5, 2.5, "Chunking\n(découpage)", palette_rag[1]),
    (4.7, 2.5, "Embedding\n(encodage)", palette_rag[2]),
    (6.9, 2.5, "Index\nvectoriel", palette_rag[3]),
]

for x, y, label, color in etapes_index:
    box = patches.FancyBboxPatch((x, y - 0.65), 1.9, 1.3, boxstyle="round,pad=0.12",
                                  fc=color, alpha=0.8, ec="none")
    ax.add_patch(box)
    ax.text(x + 0.95, y, label, ha='center', va='center', fontsize=9,
            color='white', fontweight='bold')
    if x < 6.9:
        ax.annotate('', xy=(x + 2.1, y), xytext=(x + 1.9, y),
                    arrowprops=dict(arrowstyle='->', color='#555', lw=2))

ax.text(4.6, 0.5, "Phase d'indexation (offline)", ha='center',
        fontsize=9, color='#666', style='italic')

# Phase de requête
etapes_query = [
    (0.3, 1.5, "Requête\nutilisateur", palette_rag[4]),
    (2.5, 1.5, "Embed\nrequête", palette_rag[2]),
    (4.7, 1.5, "Top-K\nretrieval", palette_rag[3]),
    (6.9, 1.5, "Augment\nprompt", palette_rag[1]),
    (9.1, 1.5, "LLM\nGénération", palette_rag[5]),
    (11.3, 1.5, "Réponse\nfactuelle", palette_rag[0]),
]

for x, y, label, color in etapes_query:
    box = patches.FancyBboxPatch((x, y - 0.55), 1.9, 1.1, boxstyle="round,pad=0.1",
                                  fc=color, alpha=0.75, ec="none")
    ax.add_patch(box)
    ax.text(x + 0.95, y, label, ha='center', va='center', fontsize=8.5,
            color='white', fontweight='bold')
    if x < 11.3:
        ax.annotate('', xy=(x + 2.1, y), xytext=(x + 1.9, y),
                    arrowprops=dict(arrowstyle='->', color='#555', lw=1.8))

ax.text(6.0, 0.2, "Phase de requête (online)", ha='center',
        fontsize=9, color='#666', style='italic')

# Flèche index -> retrieval
ax.annotate('', xy=(5.65, 1.2), xytext=(7.85, 2.0),
            arrowprops=dict(arrowstyle='->', color='#888', lw=1.5, linestyle='dashed'))

plt.show()
```

```{prf:example}
:label: ch17-ex-rag-pipeline

**Pipeline RAG en pratique** avec FAISS et numpy :

1. **Chunking** : découper les documents en passages de 200-500 tokens avec un chevauchement de 50 tokens pour préserver le contexte aux frontières.
2. **Embedding** : encoder chaque chunk avec `sentence-transformers` en vecteur de dimension 384.
3. **Indexation** : ajouter les vecteurs à un `IndexHNSWFlat` FAISS ou à une table pgvector.
4. **Retrieval** : à chaque requête utilisateur, encoder la question, chercher les top-5 chunks les plus proches.
5. **Augmentation** : construire le prompt `[contexte: chunk1, chunk2, ...] Question : {query}`.
6. **Génération** : envoyer le prompt au LLM (GPT-4, Claude, Llama) qui génère une réponse ancrée dans les sources.
```

## Cas d'usage avancés

```{prf:example}
:label: ch17-ex-deduplication

**Déduplication** : dans une base de produits e-commerce, deux descriptions différentes peuvent décrire le même article (variantes de formulation, traductions). En calculant les embeddings de toutes les descriptions et en cherchant les paires dont la similarité cosinus dépasse 0.92, on identifie automatiquement les doublons. Un index HNSW rend cette opération scalable à plusieurs millions de produits.
```

```{prf:example}
:label: ch17-ex-recommandation

**Recommandation de contenu** : encoder les articles de blog en embeddings. Pour un article lu par un utilisateur, chercher les K plus proches voisins dans l'index vectoriel — cela donne les articles les plus proches sémantiquement, indépendamment des mots-clés. Cette approche fonctionne même pour des articles récents qui n'ont pas encore accumulé de données comportementales.
```

```{code-cell} python
:tags: [hide-input]

# Benchmark : recherche exacte vs ANN sur différentes tailles de corpus
import time

sizes = [1000, 5000, 10000, 50000]
times_exact = []
times_hnsw = []
k_bench = 10

np.random.seed(0)
for n in sizes:
    vecs = np.random.randn(n, DIM).astype('float32')
    norms_b = np.linalg.norm(vecs, axis=1, keepdims=True)
    vecs = vecs / norms_b

    # Exact
    idx_exact = faiss.IndexFlatIP(DIM)
    idx_exact.add(vecs)
    q = vecs[:1]
    t0 = time.perf_counter()
    for _ in range(50):
        idx_exact.search(q, k_bench)
    times_exact.append((time.perf_counter() - t0) / 50 * 1000)

    # HNSW
    idx_hnsw = faiss.IndexHNSWFlat(DIM, 16, faiss.METRIC_INNER_PRODUCT)
    idx_hnsw.add(vecs)
    t0 = time.perf_counter()
    for _ in range(50):
        idx_hnsw.search(q, k_bench)
    times_hnsw.append((time.perf_counter() - t0) / 50 * 1000)

fig, ax = plt.subplots(figsize=(10, 5))
x_pos = np.arange(len(sizes))
width = 0.35
palette = sns.color_palette("muted", 2)

bars1 = ax.bar(x_pos - width/2, times_exact, width, label='IndexFlatIP (exact)', color=palette[0], alpha=0.85)
bars2 = ax.bar(x_pos + width/2, times_hnsw,  width, label='IndexHNSWFlat (ANN)', color=palette[1], alpha=0.85)

ax.set_xticks(x_pos)
ax.set_xticklabels([f"{n:,}" for n in sizes])
ax.set_xlabel("Taille du corpus (nombre de vecteurs)")
ax.set_ylabel("Temps de requête (ms, moyenne 50 requêtes)")
ax.set_title(f"Recherche exacte vs HNSW — dim={DIM}, k={k_bench}", fontsize=12, fontweight='bold')
ax.legend()
ax.yaxis.grid(True, alpha=0.4)
ax.set_axisbelow(True)

for bar in bars1:
    h = bar.get_height()
    ax.text(bar.get_x() + bar.get_width()/2, h + 0.005, f"{h:.2f}", ha='center', va='bottom', fontsize=8)
for bar in bars2:
    h = bar.get_height()
    ax.text(bar.get_x() + bar.get_width()/2, h + 0.005, f"{h:.2f}", ha='center', va='bottom', fontsize=8)

plt.show()
```

## Résumé

Les bases vectorielles représentent un changement de paradigme : au lieu de chercher par identité exacte, on cherche par **proximité sémantique**. Cette approche est rendue possible par les modèles d'embedding qui projettent textes, images et sons dans un espace métrique cohérent.

```{prf:remark}
:label: ch17-rem-synthese

Les points clés à retenir :

- Un embedding est un vecteur dense qui encode la sémantique d'un objet — deux objets proches ont des vecteurs proches.
- La similarité cosinus est la métrique standard pour les embeddings textuels normalisés.
- FAISS offre plusieurs types d'index : IndexFlatIP pour la précision exacte, IndexHNSWFlat pour les grandes collections, IndexIVFPQ pour compresser les milliards de vecteurs.
- pgvector transforme PostgreSQL en base vectorielle ; c'est le choix pragmatique pour la plupart des projets.
- Le pipeline RAG (chunk → embed → store → retrieve → augment → generate) est le pattern standard pour ancrer les LLM dans une base de connaissances externe.
- Les cas d'usage clés : recherche sémantique, déduplication, recommandation, question-réponse documentaire.
```
