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

# NoSQL : MongoDB

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

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.lines as mlines
import numpy as np
import pandas as pd
import seaborn as sns
from collections import defaultdict
import re

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

> **Note** : Les exemples MongoDB (JavaScript et pymongo) de ce chapitre sont des **blocs illustratifs non exécutables** — MongoDB nécessite un serveur. Les cellules Python exécutables utilisent des listes de dictionnaires pour simuler le comportement d'une collection MongoDB.

## Pourquoi NoSQL ?

Le modèle relationnel est remarquablement adapté à de nombreux problèmes : intégrité des données, requêtes complexes ad hoc, cohérence transactionnelle. Cependant, certains contextes l'ont mis en difficulté avec la montée en puissance du web :

```{prf:remark}
:label: ch15-rem-limites-relationnel

Les **limitations du modèle relationnel** dans certains contextes :

- **Schéma rigide** : modifier le schéma d'une table de plusieurs milliards de lignes peut prendre des heures et bloquer les écritures.
- **Scalabilité horizontale difficile** : sharding d'une base relationnelle est complexe ; la cohérence ACID distribuée (distributed transactions) est coûteuse.
- **Objets hiérarchiques** : stocker un objet JSON imbriqué (commande avec ses lignes et les détails de livraison) en relationnel nécessite plusieurs tables et jointures.
- **Schéma hétérogène** : dans un catalogue de produits, chaque catégorie a des attributs différents (une chaussure a une pointure, un livre a un ISBN) — difficile à modéliser en relationnel sans tables auxiliaires.
```

Les systèmes NoSQL ont émergé pour répondre à ces besoins spécifiques.

## Taxonomie NoSQL

```{prf:definition}
:label: ch15-def-taxonomie-nosql

Les systèmes NoSQL se regroupent en quatre familles principales :

| Famille | Modèle | Exemples | Usage typique |
|---|---|---|---|
| **Document** | Documents JSON/BSON semi-structurés | MongoDB, CouchDB, Firestore | Catalogues, CMS, profils utilisateurs |
| **Clé-valeur** | Paires clé/valeur opaques | Redis, DynamoDB, Riak | Cache, sessions, files de messages |
| **Colonne large** | Tables avec colonnes dynamiques par ligne | Cassandra, HBase | Logs, séries temporelles, IoT |
| **Graphe** | Nœuds et arêtes typés | Neo4j, Amazon Neptune | Réseaux sociaux, recommandations, fraude |
```

## MongoDB : documents, BSON et collections

```{prf:definition}
:label: ch15-def-mongodb-document

Dans MongoDB, les données sont stockées sous forme de **documents** au format **BSON** (*Binary JSON*), une extension binaire de JSON. BSON ajoute des types natifs absents de JSON : `Date`, `ObjectId`, `BinData`, `Int32`, `Int64`, `Decimal128`.

Un **document** MongoDB est l'unité de base — équivalent d'une ligne en relationnel. Une **collection** regroupe des documents — équivalent d'une table. La différence fondamentale : les documents d'une même collection peuvent avoir des structures différentes (**schéma flexible**).
```

```{prf:remark}
:label: ch15-rem-objectid

Chaque document possède un champ `_id` unique dans la collection. Par défaut, MongoDB génère un `ObjectId` : un identifiant de 12 octets encodant un timestamp, un identifiant de machine, un PID et un compteur. Cela garantit l'unicité globale sans coordination centrale — utile dans les clusters distribués.
```

Exemple de document MongoDB (bloc illustratif) :

```javascript
// Collection "produits"
{
  "_id": ObjectId("64f1a2b3c4d5e6f7a8b9c0d1"),
  "nom": "Laptop Pro 15",
  "marque": "Lenko",
  "prix": 1299.99,
  "stock": 42,
  "tags": ["laptop", "pro", "15pouces"],
  "specs": {
    "ram_go":    16,
    "stockage":  "512 Go SSD",
    "processeur": "Core i7-1260P"
  },
  "avis": [
    {"utilisateur": "alice", "note": 5, "commentaire": "Excellent"},
    {"utilisateur": "bob",   "note": 4, "commentaire": "Très bien"}
  ],
  "date_ajout": ISODate("2024-09-01T00:00:00Z")
}
```

## CRUD MongoDB

```{prf:definition}
:label: ch15-def-crud-mongodb

Les opérations CRUD dans MongoDB :

| Opération | Méthode MongoDB | SQL équivalent |
|---|---|---|
| Créer | `insertOne()`, `insertMany()` | `INSERT INTO` |
| Lire | `find()`, `findOne()` | `SELECT` |
| Modifier | `updateOne()`, `updateMany()` | `UPDATE` |
| Supprimer | `deleteOne()`, `deleteMany()` | `DELETE` |
```

Exemples illustratifs (shell MongoDB — requiert un serveur MongoDB) :

```javascript
// Insertion
db.produits.insertOne({
  nom: "Souris sans fil",
  prix: 49.99,
  stock: 150,
  tags: ["peripherique", "sans_fil"]
});

// Insertion multiple
db.produits.insertMany([
  { nom: "Clavier mécanique", prix: 89.99, stock: 80 },
  { nom: "Moniteur 27\"",     prix: 399.0, stock: 30 }
]);

// Lecture de tous les documents
db.produits.find({});

// Lecture avec filtre
db.produits.find({ prix: { $lt: 100 } });

// Mise à jour d'un document
db.produits.updateOne(
  { nom: "Souris sans fil" },
  { $set: { prix: 44.99 }, $inc: { stock: -1 } }
);

// Suppression
db.produits.deleteOne({ nom: "Clavier mécanique" });
```

## Opérateurs de requête

```{prf:remark}
:label: ch15-rem-operateurs-requete

MongoDB dispose d'un riche ensemble d'opérateurs de requête :

| Opérateur | Description | Exemple |
|---|---|---|
| `$eq` | Égalité | `{ prix: { $eq: 49.99 } }` |
| `$ne` | Différent | `{ stock: { $ne: 0 } }` |
| `$gt`, `$gte` | Supérieur (ou égal) | `{ prix: { $gt: 100 } }` |
| `$lt`, `$lte` | Inférieur (ou égal) | `{ prix: { $lt: 50 } }` |
| `$in` | Dans une liste | `{ marque: { $in: ["Lenko","Apple"] } }` |
| `$nin` | Pas dans une liste | `{ marque: { $nin: ["X"] } }` |
| `$and` | ET logique | `{ $and: [{prix: {$gt:50}}, {stock: {$gt:0}}] }` |
| `$or` | OU logique | `{ $or: [{stock: 0}, {prix: {$gt:500}}] }` |
| `$regex` | Expression régulière | `{ nom: { $regex: "^Souris" } }` |
| `$exists` | Existence d'un champ | `{ specs: { $exists: true } }` |
```

## Pipeline d'agrégation

```{prf:definition}
:label: ch15-def-aggregation-pipeline

Le **pipeline d'agrégation** est le mécanisme de traitement analytique de MongoDB. Un pipeline est une liste de **stages** (étapes) : chaque stage reçoit des documents, les transforme, et passe le résultat au stage suivant.

| Stage | Description | SQL équivalent |
|---|---|---|
| `$match` | Filtre les documents | `WHERE` |
| `$group` | Regroupe et agrège | `GROUP BY` + fonctions d'agrégat |
| `$sort` | Trie les résultats | `ORDER BY` |
| `$project` | Sélectionne/transforme les champs | `SELECT` |
| `$limit` | Limite le nombre de documents | `LIMIT` |
| `$lookup` | Jointure avec une autre collection | `JOIN` |
| `$unwind` | Décompose un tableau en documents | `LATERAL + unnest` |
```

Exemple illustratif (shell MongoDB) :

```javascript
// Chiffre d'affaires par marque, pour les marques avec CA > 1000
db.produits.aggregate([
  { $match: { stock: { $gt: 0 } } },
  { $group: {
      _id:    "$marque",
      ca:     { $sum: { $multiply: ["$prix", "$stock"] } },
      nb_ref: { $count: {} },
      prix_moyen: { $avg: "$prix" }
  }},
  { $match: { ca: { $gt: 1000 } } },
  { $sort:  { ca: -1 } },
  { $project: {
      marque:     "$_id",
      ca:         { $round: ["$ca", 2] },
      nb_ref:     1,
      prix_moyen: { $round: ["$prix_moyen", 2] }
  }}
]);
```

Exemple illustratif pymongo (Python — requiert un serveur MongoDB) :

```python
from pymongo import MongoClient

client = MongoClient("mongodb://localhost:27017/")
db     = client["catalogue"]

pipeline = [
    {"$match":   {"stock": {"$gt": 0}}},
    {"$group":   {
        "_id":        "$marque",
        "ca":         {"$sum": {"$multiply": ["$prix", "$stock"]}},
        "nb_ref":     {"$count": {}},
        "prix_moyen": {"$avg": "$prix"}
    }},
    {"$sort":    {"ca": -1}},
    {"$project": {
        "marque": "$_id",
        "ca":     {"$round": ["$ca", 2]},
        "nb_ref": 1
    }}
]

resultats = list(db.produits.aggregate(pipeline))
```

## Index MongoDB

```{prf:remark}
:label: ch15-rem-index-mongodb

MongoDB supporte plusieurs types d'index :

| Type | Description | Création |
|---|---|---|
| Simple | Sur un champ | `db.col.createIndex({champ: 1})` (`1`=asc, `-1`=desc) |
| Composé | Sur plusieurs champs | `db.col.createIndex({a: 1, b: -1})` |
| Texte | Recherche plein texte | `db.col.createIndex({nom: "text"})` |
| Géospatial | 2dsphere pour GeoJSON | `db.col.createIndex({loc: "2dsphere"})` |
| Unique | Contrainte d'unicité | `db.col.createIndex({email: 1}, {unique: true})` |
| Sparse | N'indexe que les docs avec le champ | `db.col.createIndex({x: 1}, {sparse: true})` |
| TTL | Expiration automatique | `db.col.createIndex({ts: 1}, {expireAfterSeconds: 3600})` |

Sans index, toute requête effectue un `COLLSCAN` (parcours complet de la collection). La commande `explain("executionStats")` permet d'analyser l'utilisation des index.
```

## Comparaison SQL vs MongoDB

```{prf:example}
:label: ch15-ex-comparaison-sql-mongodb

La même requête analytique exprimée en SQL et en MongoDB :
```

**SQL (PostgreSQL) :**

```sql
SELECT   marque,
         COUNT(*)                    AS nb_references,
         ROUND(AVG(prix)::numeric,2) AS prix_moyen,
         SUM(prix * stock)           AS chiffre_affaires
FROM     produits
WHERE    stock > 0
GROUP BY marque
HAVING   SUM(prix * stock) > 1000
ORDER BY chiffre_affaires DESC;
```

**MongoDB (agrégation) :**

```javascript
db.produits.aggregate([
  { $match:   { stock: { $gt: 0 } } },
  { $group:   {
      _id:               "$marque",
      nb_references:     { $count: {} },
      prix_moyen:        { $avg: "$prix" },
      chiffre_affaires:  { $sum: { $multiply: ["$prix", "$stock"] } }
  }},
  { $match:   { chiffre_affaires: { $gt: 1000 } } },
  { $sort:    { chiffre_affaires: -1 } }
]);
```

## Simulation Python : agrégation sur liste de dictionnaires

```{code-cell} python
# Simulation d'une collection MongoDB avec une liste de dictionnaires Python

documents = [
    {"_id": 1, "nom": "Laptop Pro 15",     "marque": "Lenko",    "prix": 1299.99, "stock": 12,
     "tags": ["laptop","pro"]},
    {"_id": 2, "nom": "Laptop Air 13",     "marque": "Lenko",    "prix":  899.99, "stock":  8,
     "tags": ["laptop","leger"]},
    {"_id": 3, "nom": "Souris sans fil",   "marque": "Logitux",  "prix":   49.99, "stock": 150,
     "tags": ["peripherique","sans_fil"]},
    {"_id": 4, "nom": "Souris gaming",     "marque": "Logitux",  "prix":   79.99, "stock":  60,
     "tags": ["peripherique","gaming"]},
    {"_id": 5, "nom": "Clavier mécanique", "marque": "Meccasoft","prix":   89.99, "stock":  80,
     "tags": ["peripherique","mecanique"]},
    {"_id": 6, "nom": "Moniteur 27\"",     "marque": "Samsung",  "prix":  399.00, "stock":  30,
     "tags": ["ecran"]},
    {"_id": 7, "nom": "Moniteur 32\"",     "marque": "Samsung",  "prix":  549.00, "stock":  15,
     "tags": ["ecran","4k"]},
    {"_id": 8, "nom": "Webcam HD",         "marque": "Logitux",  "prix":   69.99, "stock":   0,
     "tags": ["peripherique","video"]},
]

print(f"Collection : {len(documents)} documents")
pd.DataFrame([{k: v for k, v in d.items() if k != 'tags'} for d in documents])
```

```{code-cell} python
# Stage $match : stock > 0  (équivalent de WHERE stock > 0)
stage_match = [d for d in documents if d["stock"] > 0]
print(f"Après $match (stock > 0) : {len(stage_match)} documents")

# Stage $group : regrouper par marque
groupes = defaultdict(lambda: {"nb": 0, "somme_prix": 0.0, "ca": 0.0})
for d in stage_match:
    m = d["marque"]
    groupes[m]["nb"]         += 1
    groupes[m]["somme_prix"] += d["prix"]
    groupes[m]["ca"]         += d["prix"] * d["stock"]

resultats = [
    {
        "marque":            marque,
        "nb_references":     g["nb"],
        "prix_moyen":        round(g["somme_prix"] / g["nb"], 2),
        "chiffre_affaires":  round(g["ca"], 2),
    }
    for marque, g in groupes.items()
]

# Stage $match HAVING : CA > 1000
resultats = [r for r in resultats if r["chiffre_affaires"] > 1000]

# Stage $sort : tri décroissant par CA
resultats.sort(key=lambda r: r["chiffre_affaires"], reverse=True)

df_agg = pd.DataFrame(resultats)
print("\nRésultat de l'agrégation (équivalent pipeline MongoDB) :")
display(df_agg)
```

```{code-cell} python
# Simulation de $lookup (jointure) — commandes enrichies avec les détails produit
commandes = [
    {"_id": 101, "client": "Alice", "produit_id": 1, "quantite": 2},
    {"_id": 102, "client": "Bob",   "produit_id": 3, "quantite": 5},
    {"_id": 103, "client": "Alice", "produit_id": 6, "quantite": 1},
    {"_id": 104, "client": "Carol", "produit_id": 2, "quantite": 1},
]

# Équivalent de $lookup (LEFT JOIN)
produit_par_id = {d["_id"]: d for d in documents}

commandes_enrichies = []
for cmd in commandes:
    prod = produit_par_id.get(cmd["produit_id"], {})
    commandes_enrichies.append({
        "id_commande": cmd["_id"],
        "client":      cmd["client"],
        "produit":     prod.get("nom", "Inconnu"),
        "marque":      prod.get("marque", "?"),
        "prix_unit":   prod.get("prix", 0),
        "quantite":    cmd["quantite"],
        "total":       round(prod.get("prix", 0) * cmd["quantite"], 2),
    })

print("Commandes avec $lookup (jointure simulée) :")
pd.DataFrame(commandes_enrichies)
```

```{code-cell} python
# Simulation de $unwind sur les tags
rows_unwind = []
for d in documents:
    for tag in d.get("tags", []):
        rows_unwind.append({"nom": d["nom"], "marque": d["marque"], "tag": tag})

df_unwind = pd.DataFrame(rows_unwind)

# Compter les documents par tag (équivalent $unwind + $group + $sort)
tags_counts = df_unwind.groupby("tag").size().sort_values(ascending=False)
print("Fréquence des tags (après $unwind + $group) :")
print(tags_counts.to_string())
```

## Visualisation : pipeline d'agrégation et comparaison SQL/MongoDB

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

palette = sns.color_palette("muted", 6)
plt.rcParams['text.usetex'] = False
plt.rcParams['mathtext.default'] = 'regular'
fig = plt.figure(figsize=(14, 10))

# ---- Partie haute : pipeline d'agrégation ----
ax1 = fig.add_axes([0.05, 0.55, 0.55, 0.40])
ax1.set_xlim(0, 12)
ax1.set_ylim(0, 8)
ax1.axis('off')
ax1.set_title("Pipeline d'agrégation MongoDB", fontsize=12, fontweight='bold')

stages = [
    ("Collection\n(tous les docs)", palette[0]),
    (r"\$match" + "\n(filtre)", palette[1]),
    (r"\$group" + "\n(agrégation)", palette[2]),
    (r"\$sort" + "\n(tri)", palette[4]),
    (r"\$project" + "\n(projection)", palette[3]),
    ("Résultat", palette[5]),
]

for i, (label, color) in enumerate(stages):
    x = i * 2.0 + 0.5
    rect = mpatches.FancyBboxPatch((x, 2.5), 1.6, 2.5,
        boxstyle="round,pad=0.1", facecolor=color, edgecolor='white',
        alpha=0.85, linewidth=1.5, zorder=3)
    ax1.add_patch(rect)
    ax1.text(x + 0.8, 3.75, label, ha='center', va='center',
             fontsize=8.5, color='white', fontweight='bold', zorder=4,
             multialignment='center')
    if i < len(stages) - 1:
        ax1.annotate('', xy=(x + 1.75, 3.75), xytext=(x + 1.6, 3.75),
                     arrowprops=dict(arrowstyle='->', color='#555', lw=2), zorder=2)
    # Nombre de docs (illustratif)
    nb_docs = [8, 7, 4, 4, 4, 4][i]
    ax1.text(x + 0.8, 2.1, f"{nb_docs} docs", ha='center', fontsize=7.5,
             color='#555', style='italic')

# ---- Partie haute droite : graphe CA par marque ----
ax2 = fig.add_axes([0.65, 0.55, 0.32, 0.40])
df_plot = df_agg.sort_values("chiffre_affaires")
colors_bar = [palette[i % len(palette)] for i in range(len(df_plot))]
bars = ax2.barh(df_plot["marque"], df_plot["chiffre_affaires"], color=colors_bar, alpha=0.85)
ax2.set_xlabel("Chiffre d'affaires (€)")
ax2.set_title("CA par marque (résultat agrégation)", fontsize=11, fontweight='bold')
for bar, val in zip(bars, df_plot["chiffre_affaires"]):
    ax2.text(bar.get_width() + 50, bar.get_y() + bar.get_height()/2,
             f"{val:,.0f} €", va='center', fontsize=8.5)

# ---- Partie basse : comparaison SQL / MongoDB côte à côte ----
ax3 = fig.add_axes([0.05, 0.02, 0.90, 0.48])
ax3.set_xlim(0, 20)
ax3.set_ylim(0, 8)
ax3.axis('off')
ax3.set_title("Comparaison SQL vs MongoDB — même requête", fontsize=12,
              fontweight='bold')

# SQL
sql_lines = [
    "SQL (PostgreSQL)",
    "",
    "SELECT   marque,",
    "         COUNT(*)          AS nb_ref,",
    "         AVG(prix)         AS prix_moy,",
    "         SUM(prix * stock) AS ca",
    "FROM     produits",
    "WHERE    stock > 0",
    "GROUP BY marque",
    "HAVING   SUM(prix * stock) > 1000",
    "ORDER BY ca DESC;",
]
mongo_lines = [
    "MongoDB (agrégation)",
    "",
    'db.produits.aggregate([',
    r'  { \$match:  { stock: { \$gt: 0 } } },',
    r'  { \$group:  {',
    r'      _id: "\$marque",',
    r'      nb_ref: { \$count: {} },',
    r'      ca: { \$sum: { \$multiply:',
    r'                   ["\$prix","\$stock"] } }',
    '  }},',
    r'  { \$sort:   { ca: -1 } }',
    ']);',
]

sql_bg = mpatches.FancyBboxPatch((0.2, 0.3), 8.8, 7.2,
    boxstyle="round,pad=0.2", facecolor=palette[0], alpha=0.08,
    edgecolor=palette[0], linewidth=2, zorder=1)
ax3.add_patch(sql_bg)
mongo_bg = mpatches.FancyBboxPatch((10.0, 0.3), 9.8, 7.2,
    boxstyle="round,pad=0.2", facecolor=palette[2], alpha=0.08,
    edgecolor=palette[2], linewidth=2, zorder=1)
ax3.add_patch(mongo_bg)

for i, line in enumerate(sql_lines):
    y = 7.2 - i * 0.57
    color = palette[0] if i == 0 else '#333'
    fw = 'bold' if i == 0 else 'normal'
    ax3.text(0.5, y, line, fontsize=8.5, family='monospace',
             color=color, fontweight=fw, va='top')

for i, line in enumerate(mongo_lines):
    y = 7.2 - i * 0.57
    color = palette[2] if i == 0 else '#333'
    fw = 'bold' if i == 0 else 'normal'
    ax3.text(10.2, y, line, fontsize=8.5, family='monospace',
             color=color, fontweight=fw, va='top')

# Flèche centrale
ax3.annotate('', xy=(10.0, 3.8), xytext=(9.0, 3.8),
             arrowprops=dict(arrowstyle='<->', color='#888', lw=2))
ax3.text(9.5, 4.15, "≡", ha='center', fontsize=16, color='#888')

plt.savefig("_build_nosql_mongodb.png", dpi=120, bbox_inches='tight')
plt.show()
```

## Quand choisir MongoDB ?

```{prf:remark}
:label: ch15-rem-quand-mongodb

MongoDB est bien adapté quand :

- Les données sont **naturellement hiérarchiques** (documents avec sous-documents imbriqués) et les jointures fréquentes nuiraient aux performances.
- Le **schéma évolue fréquemment** : ajouter un champ à un document ne nécessite pas d'ALTER TABLE.
- Le **volume** nécessite un sharding horizontal (MongoDB gère nativement le sharding par clé de shard).
- Les données sont **hétérogènes** : chaque document peut avoir ses propres champs.

MongoDB est moins adapté quand :
- Les relations entre entités sont nombreuses et complexes (préférer le modèle relationnel avec ses jointures).
- L'intégrité transactionnelle multi-collection est critique (MongoDB supporte les transactions depuis la v4.0, mais c'est plus performant en relationnel).
- Les requêtes analytiques ad hoc sur des données bien structurées sont la norme (PostgreSQL + DuckDB excellent dans ce cas).
```

## Résumé

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

Ce chapitre a introduit MongoDB et le monde NoSQL :

**Contexte** :
- Le NoSQL répond aux limites du modèle relationnel pour les données hétérogènes, les schémas évolutifs et la scalabilité horizontale.
- Quatre familles : document, clé-valeur, colonne large, graphe.

**MongoDB** :
- Les données sont des documents BSON flexibles, regroupés en collections.
- `_id` (ObjectId) identifie chaque document de façon unique et distribuée.
- Le CRUD s'exprime avec `insertOne`, `find`, `updateOne`, `deleteOne` et leurs variantes `*Many`.

**Requêtes et agrégation** :
- Les opérateurs `$eq`, `$gt`, `$in`, `$and`, `$or`, `$regex`, `$exists` filtrent les documents.
- Le **pipeline d'agrégation** (`$match`, `$group`, `$sort`, `$project`, `$lookup`, `$unwind`) est l'équivalent des requêtes analytiques SQL.
- `$lookup` réalise des jointures entre collections.

**Index** :
- Simple, composé, texte, géospatial (2dsphere), unique, TTL.
- Sans index → COLLSCAN (parcours complet) ; avec index → IXSCAN.

**SQL vs MongoDB** :
- Les deux approches expriment les mêmes traitements analytiques mais avec une syntaxe et une philosophie différentes.
- Le choix dépend de la structure des données, des besoins de schéma flexible, et des patterns d'accès.
```
