Accroche
Tu as suivi RAG-01 à RAG-03. Tes documents sont chunkés, tes chunks transformés en vecteurs (1024 floats chacun avec BGE-M3, par exemple). Tu te retrouves avec 10 000 vecteurs aujourd’hui, peut-être 100 000 dans deux ans. Question concrète : où tu les ranges, et comment tu retrouves les 5 plus proches d’une question utilisateur en moins de 100 ms ?
La réponse naïve — « je les stocke dans une liste Python et je compare un par un à chaque requête » — tient sur 1 000 vecteurs. À 100 000, une seconde par requête (hors budget interface). À 1 million, plusieurs secondes (inutilisable). Il te faut un composant spécialisé : le vector store.
Cette leçon démonte ce composant. Pas de formule. L’idée centrale, les algorithmes (ANN, HNSW, IVF) en intuition, la comparaison des cinq stores qui dominent en 2026, et la matrice de décision pour choisir lequel installer demain matin.
Vue d’ensemble : qu’est-ce qu’un vector store fait vraiment
Un vector store fait trois choses, dans cet ordre.
1. Stocker des vecteurs avec leurs identifiants et leurs metadata (catégorie, date, source, tags). Un vecteur seul ne sert à rien — sans son ID, tu ne sais pas à quel chunk il correspond ; sans ses metadata, tu ne peux pas filtrer.
2. Indexer ces vecteurs pour répondre vite à la question « quels sont les k vecteurs les plus proches de celui-ci ? ». L’index, c’est ce qui évite de comparer linéairement à tout le corpus à chaque requête.
3. Filtrer les résultats sur les metadata avant ou après la recherche par similarité (« les 5 chunks les plus proches, mais uniquement ceux datés de 2026 et catégorie juridique »).
Le schéma en texte
- Vector store : 1. Storage (vecteur + id + metadata), 2. Index ANN (lookup rapide), 3. Filtrage (metadata)
- Question embeddée → 2. Index ANN (lookup rapide)
- 2. Index ANN (lookup rapide) → Candidats top-k
- Candidats top-k → 3. Filtrage (metadata)
- 3. Filtrage (metadata) → Chunks + scores
- Hors liens : 1. Storage (vecteur + id + metadata)
Un vector store n’est pas une magie. C’est un conteneur autour d’un algorithme de recherche par similarité, avec une couche de filtrage par metadata. Le « secret sauce » d’un store n’est pas son nom — c’est l’algorithme d’index qu’il utilise (vu plus loin) et la richesse du filtrage metadata qu’il propose.
Analogie infra : un vector store, c’est une base de données spécialisée, comme tu as ElasticSearch pour les mots-clés, Redis pour les clés-valeurs, Postgres pour le relationnel. Ici, l’opération native n’est ni « match texte » ni « get clé », c’est « trouve les
kvoisins géométriques d’un point dans un espace à 1024 dimensions ».
Le vrai problème que résout un vector store : l’index ANN
Une recherche brute sur N vecteurs, c’est FLAT (ou brute force) : tu calcules la similarité entre la requête et tous les vecteurs du corpus, tu tries, tu prends les k premiers. Recall garanti 100 %, mais coût linéaire en N.
Calcul d’ordres de grandeur. Pour comparer deux vecteurs de 1024 floats par similarité cosinus (rappel RAG-01 : l’angle entre les deux flèches), tu fais ~1024 multiplications :
- 10 000 vecteurs × 1024 dim ≈ 10 millions de mults → ~30 ms sur un i5 récent. Acceptable.
- 1 million × 1024 dim ≈ 1 milliard de mults → ~3 secondes. Hors budget.
- 100 millions : impensable en FLAT.
D’où l’ANN : Approximate Nearest Neighbor, ou « plus proches voisins approximatifs ».
Définition : un index ANN, c’est une structure de données qui élimine 90-99 % des candidats sans les regarder, en échange d’un petit risque de rater un vrai voisin. Tu acceptes 95 à 99 % de précision (le recall, vu RAG-01) pour 100× à 1000× de vitesse.
Analogie infra : c’est la même logique qu’un index B-tree sur une colonne SQL. Sans index, un
WHERE id = 12345scanne toute la table. Avec un B-tree, tu descends en O(log N) — saut direct à la bonne page. L’ANN fait la même promesse, mais pour des points dans un espace à N dimensions au lieu de scalaires triés.
Le schéma en texte
- FLAT (brute force) : Compare aux N vecteurs, Trie + top-k, O(N × dim) 1M × 1024 = 1B mults
- ANN (HNSW) : Navigue la structure, Suit vers le proche, O(log N) ~640 mults
- Compare aux N vecteurs → Trie + top-k
- Navigue la structure → Suit vers le proche
- Question 1 vecteur → Compare aux N vecteurs
- Question 1 vecteur → Navigue la structure
- Hors liens : O(N × dim) 1M × 1024 = 1B mults, O(log N) ~640 mults
Le coût conceptuel à payer : tu sors du déterminisme. Sur FLAT, le top-5 est exact. Sur HNSW (algo ANN, vu plus bas), le top-5 est probable — typiquement 95-98 % des vrais voisins, soit 1 chunk pertinent sur 50 silencieusement raté. Invisible pour un chatbot grand public, critique pour un système juridique. À toi de mesurer le recall sur ton corpus, pas sur un benchmark générique.
Trois familles d’algorithmes ANN
Trois approches dominent en 2026, chacune avec un compromis différent vitesse / précision / mémoire. Tu n’as pas à les implémenter — chaque store en propose une ou plusieurs. Mais tu dois savoir laquelle ton store utilise pour comprendre ce que tu paies.
FLAT — pas vraiment un ANN, mais le défaut quand N est petit
Le calcul exhaustif décrit plus haut. Recall 100 %, complexité O(N), aucun build offline. Suffit tant que N < ~50 000 vecteurs et que ton budget latence est > 100 ms. C’est ce que sqlite-vec utilise par défaut. Pas un ANN à proprement parler — c’est l’absence d’index, l’option à battre.
IVF — Inverted File Index, on découpe l’espace en clusters
Idée : avant la recherche, tu groupes tous tes vecteurs en n_clusters clusters (typiquement √N) avec un k-means. À la recherche, tu trouves d’abord le cluster le plus proche de la requête (coût : N divisé par √N), puis tu fais un FLAT à l’intérieur de ce cluster (coût : √N vecteurs).
Analogie infra : c’est exactement le pattern sharding par hash. Au lieu de chercher dans 1 million de lignes, tu sais à quel shard la clé appartient, tu interroges ce shard, ~1000 lignes. Tu paies un calcul de hash, tu divises ton coût par √N.
Le schéma en texte
- Question → Centroids (préca k-means)
- Centroids (préca k-means) → Cluster 1
- Centroids (préca k-means) → Cluster 2 *
- Centroids (préca k-means) → Cluster 3
- Centroids (préca k-means) → ...Cluster K
- Cluster 2 * → Top-K (FLAT inside cluster choisi)
Recall : 90-99 % selon combien de clusters tu sondes (paramètre n_probe, typiquement 1 à 10). Plus tu en sondes, plus c’est précis et lent. Variante prod : IVF-PQ (Product Quantization) qui compresse les vecteurs ×8 à ×32 en mémoire — utile au-delà de 10 millions de vecteurs.
HNSW — Hierarchical Navigable Small World, le défaut moderne
L’algorithme dominant en 2026 (Malkov & Yashunin 2018). Idée : un graphe à plusieurs niveaux où les vecteurs sont reliés à leurs voisins proches. Au sommet, peu de nœuds, longues arêtes (vue d’avion). En bas, tous les nœuds, courtes arêtes (vue de détail). Pour une requête, tu pars du sommet, tu suis l’arête vers le voisin le plus proche, tu descends d’un niveau, tu recommences. En quelques sauts, tu arrives sur les vrais voisins.
Analogie infra : c’est l’organisation d’un CDN multi-niveaux. Le routing DNS te trouve d’abord la région (sommet), puis le serveur edge (intermédiaire), puis le contenu (base). Tu n’as jamais à connaître l’adresse exacte du serveur final — tu descends par approximations successives.
Complexité empirique O(log N) jusqu’à ~100 millions de vecteurs. Coût : build 5-10× plus lent qu’IVF, mémoire ~1.5× le brut.
| Algorithme | Recall@10 | Search | Mémoire | Cas d’usage |
|---|---|---|---|---|
| FLAT | 100 % | O(N) | ×1 | < 50 000 vecteurs, latence > 100 ms |
| IVF | 90-99 % | O(√N) | ×1.05 | 100 000 - 1 million, RAM serrée |
| IVF-PQ | 85-95 % | O(√N) | ×0.05-0.25 | > 10 millions, RAM très serrée |
| HNSW | 95-99 % | O(log N) | ×1.5 | 10 000 - 100 millions, latence prio |
Les trois algos ne sont pas concurrents — ils couvrent des zones différentes. FLAT : démarrage. HNSW : production standard. IVF / IVF-PQ : volumes très gros avec contrainte mémoire. Quand un store annonce « ANN search » sans préciser, c’est presque toujours HNSW.
Cinq vector stores comparés — la matrice 2026
Cinq stores méritent d’être nommés en 2026. Les autres (Weaviate, Milvus, Pinecone) restent valides dans des cas spécifiques (multi-modal enterprise, milliards de vecteurs, SaaS managé) hors scope d’un copilote interne.
Le schéma en texte
- Embedded (1 process) : sqlite-vec extension SQLite, FAISS lib C++/Python, Chroma embedded ou C/S
- Service réseau : Qdrant service Rust, pgvector extension Postgres
- Hors liens : sqlite-vec extension SQLite, FAISS lib C++/Python, Chroma embedded ou C/S, Qdrant service Rust, pgvector extension Postgres
sqlite-vec — l’extension SQLite single-file
Extension SQLite qui ajoute une table virtuelle vec0 pour stocker des embeddings et faire du k-NN par opérateur SQL MATCH. Mode FLAT par défaut (IVF en preview). Toute ta donnée — texte, vecteur, BM25 lexical (FTS5) — vit dans un seul fichier .db. Zéro infra, backup cp file.db, transactionnel. Limite : 1 writer concurrent max (héritage SQLite), dégradation au-delà de ~100 000 vecteurs en FLAT. Le bon défaut pour un projet RAG démarrage.
FAISS — la bibliothèque pure de Facebook AI
C’est une librairie, pas une DB. Tu instancies un index (IndexFlatIP, IndexHNSWFlat…), tu add() des vecteurs, tu search(). Pas de persistance native (tu sérialises toi-même), pas de filtrage metadata sans code custom. Le plus rapide sur les benchmarks pure-search. Cas d’usage : pipelines ML serving custom, batch offline, recherche académique. Pas le bon choix pour une stack RAG prête à l’emploi.
Qdrant — le service standalone en Rust
Service séparé en Rust, exposé en REST + gRPC. HNSW custom optimisé, filtrage riche (conditions metadata appliquées pendant la recherche, pas après — voir AP-3), multi-tenant via collections, snapshots de backup. Setup : docker run -p 6333:6333 qdrant/qdrant. Latence k=20 sur 100 000 vecteurs : ~10 ms. Idéal multi-tenant, filtrage complexe, scaling au-delà de 1 million de vecteurs.
pgvector — l’extension PostgreSQL
Extension qui ajoute un type colonne vector(N) à PostgreSQL, et trois opérateurs de distance (<-> L2, <=> cosine, <#> produit scalaire). Index HNSW depuis 0.5.0, IVFFlat avant. Le bon choix si tu as déjà Postgres : tu joins ta table chunks_vec à documents et tags dans une seule requête SQL avec push-down des filtres metadata, et tu réutilises pg_dump, transactions, monitoring existants. Latence ~5-20 ms sur 100 000 vecteurs en HNSW.
Chroma — l’embedded pythonic
Embedded ou client/serveur, HNSW via hnswlib sous le capot, API très pythonic (add(), query()). Limite : couche Python obligatoire, metadata séparées du vecteur dans le format de stockage. Apprentissage moins « bare-metal » que sqlite-vec.
| Store | Type | Algo défaut | Latence k=20 @100k | Setup | Filtrage | Backup | Quand l’utiliser |
|---|---|---|---|---|---|---|---|
| sqlite-vec | extension SQLite | FLAT | ~250 ms | pip install sqlite-vec | SQL WHERE | cp file.db | < 100 000 vecteurs, single-user |
| FAISS | lib C++/Python | au choix | ~20 ms (HNSW) | lib import | non (custom) | sérialisation manuelle | pipeline ML, latence dure, in-memory pur |
| Qdrant | service Rust | HNSW custom | ~10 ms | docker run | riche, push-down | snapshot API | prod multi-tenant |
| pgvector | extension Postgres | HNSW (0.5+) | ~20 ms | CREATE EXTENSION | SQL WHERE | pg_dump | Postgres déjà déployé |
| Chroma | embedded ou C/S | HNSW (hnswlib) | ~15 ms | pip install | partiel | cp dir | quick local ML pythonic |
Quatre axes te font choisir entre ces stores : volume du corpus, latence cible, stack existante (Postgres ou pas), et compétence DevOps que tu acceptes d’investir. Le tableau ci-dessous donne les recommandations directes.
Matrice de décision — quel store pour quel contexte
| Volume corpus | Latence cible | Stack existante | Recommandation | Pourquoi |
|---|---|---|---|---|
| < 10 000 chunks | < 1 s | Aucune | sqlite-vec FLAT | Zéro infra, single-file, suffit largement. |
| 10 000 - 100 000 | < 500 ms | Aucune | sqlite-vec ou Chroma | Toujours embedded, FLAT acceptable < 50k. |
| 100 000 - 1 million | < 200 ms | Postgres | pgvector | Réutilise infra, transactions, backups. |
| 100 000 - 1 million | < 200 ms | Agnostique | Qdrant | Filtering riche, multi-tenant, scaling natif. |
| > 1 million | < 100 ms | ML pipeline | FAISS ou Qdrant | FAISS si in-memory pur acceptable, sinon Qdrant. |
| Latence < 10 ms | dur | ML serving | FAISS in-memory | Pas d’overhead réseau, indices custom. |
Cas pratique labo (le mien) : ~1 800 chunks aujourd’hui, projection 18 000 à 5 ans, latence p95 < 10 s tolérée, single-user. Verdict : sqlite-vec FLAT — confortable jusqu’en 2030. Plan de migration vers pgvector si une de ces trois conditions devient vraie : (a) multi-utilisateurs concurrents, (b) intégration avec un schéma SQL riche, (c) volume > 100 000 chunks avec contrainte latence < 200 ms.
Heuristiques transverses :
- Démarre toujours en FLAT. C’est gratuit (pas de tuning), tu mesures recall et latence sur ton corpus, tu passes à HNSW seulement quand la latence dépasse ton budget.
- Pousse les filtres metadata dans la requête, pas en post-traitement Python (cf. AP-3).
- Stocke la version du modèle d’embedding dans les metadata. Si tu changes d’embedder, tu dois rebuilder l’index — la version te dit lesquels.
Code minimal — sqlite-vec en pratique
Voici le squelette d’un système RAG single-file complet en Python, avec sqlite-vec, qui marche en moins de 30 lignes utiles.
import sqlite3, sqlite_vec, struct
# 1. Connexion + chargement de l'extension sqlite-vec
db = sqlite3.connect("rag.db")
db.enable_load_extension(True)
sqlite_vec.load(db)
db.enable_load_extension(False)
# 2. Schéma — table classique pour le texte+metadata, table virtuelle vec0 pour les vecteurs
db.executescript("""
CREATE TABLE IF NOT EXISTS chunks (
chunk_id INTEGER PRIMARY KEY,
text TEXT NOT NULL,
category TEXT
);
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(
chunk_id INTEGER PRIMARY KEY,
embedding FLOAT[1024]
);
""")
# 3. Sérialisation float32 little-endian (format attendu par vec0)
def to_blob(vec): return struct.pack(f"{len(vec)}f", *vec)
# 4. Recherche top-k avec filtre metadata push-down (rapide, dans la requête SQL)
def search_topk(query_emb, k=5, category=None):
sql = """SELECT c.chunk_id, c.text, distance FROM chunks_vec v
JOIN chunks c USING (chunk_id)
WHERE v.embedding MATCH ? AND k = ?"""
params = [to_blob(query_emb), k]
if category:
sql += " AND c.category = ?"; params.append(category)
return [dict(zip(["id", "text", "score"], r))
for r in db.execute(sql + " ORDER BY distance", params)]
Trace exécution sur 1 800 chunks (i5-12500, BGE-M3 1024 dim) : insertion batch ~3 s, build index nul (FLAT), search top-k ~12 ms par requête. Production sur ton poste de travail demain matin sans Docker.
Pièges et anti-patterns
AP-1 — Confondre vector DB et index ANN. Quand tu compares Qdrant et pgvector, tu compares deux implémentations de HNSW. Le « secret sauce » est dans le tuning des paramètres (M, ef_construction, ef_search), pas dans le nom du produit.
AP-2 — Choisir HNSW sur 5 000 chunks. HNSW est conçu pour > 100 000 vecteurs. Sur petit corpus, FLAT bat HNSW : pas de build, latence comparable, recall 100 %. Démarre toujours en FLAT et migre quand la latence dépasse 200 ms.
AP-3 — Filtrer metadata après le retrieval. Si tu fais search(k=20) puis tu filtres category == 'fundamentals' en Python post-hoc, tu rates les chunks pertinents qui n’étaient pas dans le top-20 dense. Pousse toujours le filtre dans la requête (WHERE SQL ou condition Qdrant).
AP-4 — Réindexer sur changement d’embedder. Si tu passes de BGE-M3 (1024 dim) à OpenAI text-embedding-3-large (3072 dim), tu dois rebuilder l’index complet. Les espaces vectoriels ne sont pas comparables. Stocke la version du modèle dans les metadata pour détecter les drifts.
AP-5 — Mesurer recall sur un benchmark générique. SIFT-1M, GloVe-100 — datasets génériques. Ton corpus FR de conventions techniques a une distribution différente. Construis ton eval set (30 paires Q/A gold), mesure recall@5 sur ton pipeline. Si HNSW tape 0.95 sur SIFT mais 0.80 chez toi, FLAT vaut peut-être mieux.
AP-6 — Ignorer la concurrence d’écriture SQLite. sqlite-vec hérite des limites SQLite : 1 writer max simultané. Ingest concurrent + queries serving → database is locked. Solution : queue d’ingest séquentielle, ou pivot vers pgvector / Qdrant.
📌 Débat ouvert — embedding propriétaire vs vector store agnostique. La fragilité d’un système RAG est rarement le vector store (HNSW est devenu un commodity). C’est l’embedder. Si OpenAI déprécie
text-embedding-3-large, tu dois ré-embedder ton corpus entier. Le vector store, lui, accepte n’importe quel vecteur. Choisis ton embedder avec soin, ton vector store avec pragmatisme.
Récap visuel
Le schéma en texte
- Documents → chunking RAG-02
- chunking RAG-02 → embedding RAG-03
- embedding RAG-03 → Vector store (cette leçon)
- Question → embedding
- embedding → search top_k
- Vector store (cette leçon) → search top_k (lookup)
- search top_k → Chunks + scores
Le vector store est le pivot du RAG. L’indexation y dépose, la requête y lit. Tout le reste (hybrid search, reranking, query rewriting) est de l’optimisation autour de ce pivot.
Quel store changer quand le retrieval pose problème
Quand le retrieval déraille, le vector store n’est pas toujours coupable — c’est même rarement le cas. Table debug à parcourir avant de migrer.
| Symptôme observé | Première chose à challenger |
|---|---|
| Latence > budget | Algo d’index (FLAT → HNSW) ou tuning HNSW (ef_search) |
| Recall qui chute après scaling | Algo d’index (FLAT dégrade en O(N), HNSW reste log N) |
| Filtres metadata cassés ou ignorés | Vérifier le push-down du store (sinon post-filter rate des chunks) |
| RAM saturée | Passer IVF-PQ (compression ×8 à ×32) ou sharder en collections |
database is locked (sqlite-vec) | Sérialiser les writes ou pivoter vers pgvector / Qdrant |
| Recall stable mais réponses fausses | Pas le vector store — chunking (RAG-02) ou embedder (RAG-03) |
Tu démarres un projet interne : 40 000 documents, une dizaine d’utilisateurs, pas d’équipe d’exploitation dédiée. Un collègue propose de monter un cluster distribué « pour ne pas avoir à migrer plus tard ». Quel est le coût réel de ce conseil ?
Réponse
Il t’ajoute un composant à exploiter avant d’avoir prouvé que le produit sert à quelque chose. À cette échelle, un store embarqué tient sans effort — et il n’a ni cluster, ni sauvegarde, ni montée de version, ni astreinte.
L’argument « pour ne pas migrer plus tard » suppose que la migration soit coûteuse. Elle ne l’est pas tant que : tes chunks et tes métadonnées sont stockés hors du store, et que tu peux réindexer depuis la source. Si tu tiens cette discipline, changer de store est une réindexation — pas une reconstruction.
La vraie question n’est donc pas « quel store va me porter à cinq ans » mais « qu’est-ce que je garde en dehors du store pour pouvoir en changer ». Réponse : le corpus, les métadonnées, et le script qui reconstruit l’index.
À toi de jouer
Note en trois lignes ce que tu devrais garder hors de ton vector store pour pouvoir le remplacer un lundi matin. Si la liste est vide, ton index est devenu ta source de vérité — et c’est le moment de corriger ça, pas dans deux ans.
Glossaire
Format : TERME — définition courte + analogie infra quand pertinente.
- Vector store (rappel RAG-01) — Base de données spécialisée pour stocker des vecteurs et chercher rapidement les plus proches d’un vecteur cible. Analogie : Redis, mais indexé par similarité au lieu de clé.
- Index — Structure de données accessoire qui accélère la recherche au prix d’un coût de build et de stockage. Analogie : un B-tree sur une colonne SQL.
- ANN (Approximate Nearest Neighbor) — Famille d’algorithmes qui trouvent les
kvoisins les plus proches d’un point en acceptant 1-5 % d’erreur en échange de 100× à 1000× de vitesse. Analogie : un cache à hit-rate 95 % qui répond en O(log N) au lieu d’un store exhaustif en O(N). - FLAT — Recherche brute exhaustive : on calcule la similarité avec tous les vecteurs, on trie. Recall 100 %, complexité O(N), aucun index. Le défaut quand N est petit.
- IVF (Inverted File Index) — Algorithme ANN qui pré-clustérise les vecteurs en groupes (k-means), puis fait un FLAT à l’intérieur du ou des clusters les plus proches de la requête. Analogie : sharding par hash.
- HNSW (Hierarchical Navigable Small World) — Algorithme ANN à base de graphe multi-niveaux. Le défaut moderne 2026, complexité O(log N) jusqu’à 100 millions de vecteurs. Analogie : un CDN multi-niveaux où on descend par approximations successives.
- Recall@k — Pourcentage des
kvrais voisins qu’on retrouve effectivement dans le top-kretourné par l’index ANN. Mesure clé de qualité d’un index ANN. Analogie : taux de hit dans un cache. - Push-down filter — Application d’un filtre sur les metadata dans la requête (pendant le scan de l’index), par opposition à un post-filtre appliqué après. Plus rapide et plus précis : tu ne rates pas les chunks pertinents qui auraient été éjectés du top-k brut.
- Quantization — Compression des vecteurs (typiquement de float32 à int8 ou moins) pour réduire la mémoire. Variante : Product Quantization (PQ) utilisée dans IVF-PQ. Compromis : ×8 à ×32 de mémoire économisée pour 5-15 pts de recall en moins.
- Embedding (rappel RAG-01) — Représentation d’un texte sous forme de vecteur de nombres. Le vector store stocke et indexe ces vecteurs.
- Similarité cosinus (rappel RAG-01) — Mesure de proximité entre deux vecteurs basée sur l’angle entre eux. Métrique standard utilisée par la plupart des vector stores.
- top_k (rappel RAG-01) — Nombre de chunks ramenés par le retrieval. Cible typique : 5 à 20.
Pour aller plus loin
Sources principales :
- Malkov & Yashunin 2018, Efficient and robust ANN search using HNSW — arXiv:1603.09320 (article fondateur HNSW)
- Johnson, Douze & Jégou 2017, Billion-scale similarity search with GPUs (FAISS) — arXiv:1702.08734
- ANN-Benchmarks (Aumüller et al.) — ann-benchmarks.com (50+ algos comparés sur datasets standards)
- sqlite-vec (Alex Garcia) — github.com/asg017/sqlite-vec
- pgvector (Andrew Kane) — github.com/pgvector/pgvector
- Qdrant docs — qdrant.tech/documentation
Prochaine leçon dans l’axe RAG :
- RAG-05 — Retrieval avancé (à venir) : hybrid search (BM25 + dense + RRF), reranking, query rewriting. Comment combiner recherche sémantique (vector store) et recherche par mots-clés pour gagner 5-15 points de recall.