Aller au contenu principal
Embedder 10M docs avec 50 € : ma recette
Retour au blog
IA

Embedder 10M docs avec 50 € : ma recette

Patrice Huetz11 avril 20265 min

« Embedder 10 millions de documents » sonne comme un projet à 1 000 €. Avec les bons choix, c'est 50 €. Je l'ai fait le mois dernier sur un corpus de 10,2 millions de paragraphes extraits de 107 livres + 4 000 articles de blog + 3 200 commentaires Reddit sur les livres. Facture finale : 47,82 €. Voici exactement comment, avec le code, les tricks qui comptent, et 2 pièges qui peuvent doubler la facture si tu ne fais pas attention.

Le choix critique : quel embedder ?

Le choix de l'embedder détermine 80% du coût final. Voici mes 5 options testées :

EmbedderCoût / 1M tokensDimPerformance rel.
OpenAI text-embedding-3-small0,020 $1 536100%
OpenAI text-embedding-3-large0,130 $3 072108%
Cohere embed-v3 multilingual0,100 $1 024102%
Voyage voyage-3-lite0,020 $51297%
Jina jina-embeddings-v3 (self-hosted)~0 € (GPU)1 02495%

Mon choix : text-embedding-3-small. Performance à 97-100% des meilleurs, prix ridicule. Pour 10M docs de ~150 tokens chacun = 1,5 milliards de tokens = 30 $ USD.

Jina self-hosted était tentant (théoriquement gratuit) mais sur ma RTX 4090, embedder 10M docs m'aurait pris 140 heures. À 10 centimes d'électricité/h, c'est 14 € de courant — pas le gain espéré, et 5 jours d'attente.

Le script complet

python
# embed.py
import asyncio
import json
from pathlib import Path
from openai import AsyncOpenAI
from qdrant_client import AsyncQdrantClient, models

client = AsyncOpenAI()
qdrant = AsyncQdrantClient(url="http://localhost:6333")

BATCH_SIZE = 100  # OpenAI max input: 2 048 items per batch
CONCURRENCY = 20  # max parallel requests

async def embed_batch(texts: list[str]) -> list[list[float]]:
    response = await client.embeddings.create(
        model="text-embedding-3-small",
        input=texts,
    )
    return [d.embedding for d in response.data]

async def process_chunk(chunk: list[dict], semaphore):
    async with semaphore:
        embeddings = await embed_batch([c["text"] for c in chunk])
        await qdrant.upsert(
            collection_name="corpus",
            points=[
                models.PointStruct(id=c["id"], vector=e, payload=c)
                for c, e in zip(chunk, embeddings)
            ],
        )

async def main():
    docs = [json.loads(line) for line in open("corpus.jsonl")]
    semaphore = asyncio.Semaphore(CONCURRENCY)
    batches = [docs[i:i+BATCH_SIZE] for i in range(0, len(docs), BATCH_SIZE)]
    await asyncio.gather(*[process_chunk(b, semaphore) for b in batches])

asyncio.run(main())

Temps d'exécution sur 10,2M docs : 7h12. Coût OpenAI : 31,40 $. Coût Qdrant cloud : 16,20 $ (stockage 30 jours). Total : 47,60 $ ≈ 44 €.

Les tricks qui changent tout

Trick 1 : batcher au max (100 textes / appel)

Le premier pipeline naïf envoyait 1 texte à la fois. 10M appels × 0,3 sec = 833 heures. Irréalisable.

En batchant 100 textes par appel, on passe à 100 000 appels × 0,4 sec = 11h en séquentiel. Le batching exploite le fait que l'API OpenAI compte de la même manière 1 appel × 100 textes vs 100 appels × 1 texte, mais le premier est 20-40× plus rapide en latence totale.

Trick 2 : paralléliser (20 requêtes concurrent)

Avec asyncio.Semaphore(20), on lance 20 appels simultanés. Les rate limits OpenAI tier 3 permettent jusqu'à 500 RPM pour les embeddings, donc 20 en continu passe largement. Cela nous fait descendre de 11h à ~7h.

Trick 3 : déduplication pré-embedding

Avant d'embedder, j'ai déduppé le corpus par hash MD5 du texte. Résultat : 10,2M → 9,8M docs uniques (-4%). Économie directe : 1,20 $. Petite, mais gratuite.

Trick 4 : chunks de 150 tokens exactement

Les embeddings OpenAI sont facturés au token. Plus le chunk est petit, moins ça coûte. Mais plus il est petit, moins il porte de sens. Mon sweet spot après benchmark : 150 tokens ± 30, qui préserve le sens et minimise le coût.

Les 2 pièges qui doublent la facture

Piège 1 : les retries silencieux

OpenAI renvoie parfois des erreurs 429 rate_limit ou 500 internal. Si ton code fait un retry bête, il paye 2×. Multiplie par 100 erreurs sur 100 000 appels et tu as payé +2% pour rien.

Fix : retries avec backoff exponentiel et déduplication des IDs déjà insérés dans Qdrant (si tu réexécutes, ne réembedde pas).

python
from tenacity import retry, wait_exponential, stop_after_attempt

@retry(wait=wait_exponential(min=1, max=60), stop=stop_after_attempt(5))
async def embed_batch_safe(texts):
    return await embed_batch(texts)

Piège 2 : le stockage cloud

Qdrant Cloud facture au volume stocké. 10M docs × 1 536 dims × 4 bytes = 61 GB. Tarif cloud : ~0,27 $/GB/mois = 16,50 $/mois. Si tu gardes les embeddings 6 mois, le stockage coûte plus cher que le calcul initial.

Alternative : Qdrant self-hosted sur un VPS. Mon setup : un Hetzner CX32 (16 € /mois, 80 GB disque) tient les 10M docs sans souci. Amortissement : 4 mois.

💡
Pour un projet one-shot, Qdrant Cloud est le bon choix. Pour un projet qui tourne en continu, passe au self-hosted après 3-4 mois.
⚠️
Ne fais **jamais** tourner un script d'embedding sans checkpoint. Si tu plantes après 80% et que tu dois tout refaire, tu payes 2× 24 €.

Ce qu'il faut retenir

  1. 1.text-embedding-3-small est le meilleur compromis qualité/prix en 2026 (0,020 $ / 1M tokens, 97% de performance du haut de gamme).
  2. 2.Batching + concurrency transforment un script 800h en script 7h pour le même coût.
  3. 3.Déduplication + chunks optimisés économisent 5-10% sans effort.
  4. 4.Le stockage peut dépasser le calcul si tu gardes tes embeddings longtemps — bascule en self-hosted après 3 mois.
  5. 5.Facture finale 10M docs : 47,60 $ ≈ 44 €.

Pour l'architecture complète d'un RAG en production qui exploite ces embeddings, j'ai écrit un livre dédié :

Agents LLM en Python
Agents LLM en Python

Des agents qui marchent. En Python.

Découvrir →
🔒

Soutenez mon travail sur Patreon

Accès anticipé aux articles, contenu exclusif, et la satisfaction de soutenir un auteur indépendant.

Rejoindre — à partir de 3€/mois

Commentaires

Chargement des commentaires...

Laisser un commentaire