Accueil / Articles

À l’intérieur des bases de données vectorielles pour RAG : stockage des fragments, HNSW, IVF et réclassement

Comment les bases de données vectorielles RAG stockent les fragments, recherchent des millions de vecteurs à l’aide de HNSW et IVF, ajoutent une recherche hybride et un réclassement, et comparent les mesures de similarité.

Éditeur : REACTAPP.TOP9 min de lecture1968 mots

Sources: Inside Vector Databases for RAG: From Chunk Storage to HNSW & IVF Search

Un système de génération augmentée par la récupération doit répondre à une question pratique précoce : où se trouvent les fragments de texte ? La réponse la plus courante, et celle utilisée par la plupart des systèmes en production, consiste à placer chaque fragment directement dans la base de données vectorielle, à côté de son embedding.

À quoi ressemble un enregistrement stocké

Dans les configurations basées sur FAISS, Pinecone ou Weaviate, chaque enregistrement regroupe trois éléments : le vecteur d’embedding, le texte du fragment et des métadonnées.

{
        "embedding": [0.123, 0.456, ...],
        "text": "Transformer models are powerful...",
        "metadata": {
        "doc_id": "doc1",
        "page": 5
        }
}

Conserver le fragment de texte et son vecteur associé présente trois avantages. La recherche est rapide, car le texte correspondant est retourné dans la même réponse. L’architecture reste simple, sans nécessité de consulter une seconde base de données. De plus, la latence reste faible, ce qui est crucial lorsque un LLM attend les résultats en temps réel.

Le processus de récupération s’effectue alors en cinq étapes : un utilisateur pose une question, celle-ci est transformée en embedding, la base de données trouve les embeddings stockés les plus proches, le texte du fragment correspondant est retourné directement, et l’LLM rédige sa réponse à partir de ce texte. Aucune recherche supplémentaire n’est nécessaire.

Comment la recherche reste rapide

La description habituelle en une phrase est que une base de données vectorielle trouve des embeddings similaires en utilisant la similarité cosinus, le produit scalaire ou la distance euclidienne. C’est exact, mais cela omet les aspects techniques intéressants. Avec des millions ou des milliards de vecteurs, comment une requête peut-elle être traitée en quelques millisecondes ?

La méthode naïve consiste en une force brute : on compare la requête avec chaque vecteur stocké. Elle offre une précision parfaite mais une vitesse exécrable. Un million de vecteurs signifie un million de comparaisons pour chaque requête, ce qui n’est pas scalable.

Les systèmes réels utilisent une recherche d’adjacent le plus proche approximative (ANN). L’idée est d’abord de réduire l’espace de recherche, puis d’examiner uniquement un petit sous-ensemble de vecteurs, en acceptant une légère perte de précision en échange d’une grande amélioration de la vitesse. La recherche ANN s’appuie sur des structures d’index spécialisées. FAISS, par exemple, fournit des index de ce type, tout comme HNSW et IVF.

HNSW, un graphe en couches

HNSW signifie Hierarchical Navigable Small World. Il s’agit d’une méthode basée sur des graphes, utilisée en interne par Pinecone et Weaviate. Chaque vecteur devient un nœud, et chaque nœud est relié à des voisins similaires. Une recherche consiste alors en une exploration de ce graphe.

Le graphe comporte plusieurs couches. La couche supérieure contient très peu de nœuds et offre une vue grossière, tandis que la couche inférieure présente tous les nœuds en détail. Une comparaison entre autoroutes et rues locales convient bien ici.

Sa construction se fait de la manière suivante : lorsqu’un embedding est inséré, il reçoit un niveau aléatoire, de sorte que la plupart des nœuds se trouvent uniquement dans la couche inférieure, tandis que seuls quelques-uns atteignent la couche supérieure. Au sein de chaque niveau, le nœud est connecté à ses voisins les plus proches, mesurés par le cosinus, le produit scalaire ou la distance euclidienne. Le résultat est un réseau où les vecteurs proches sont étroitement connectés et ceux qui sont éloignés ne le sont que de manière plus lâche.

La recherche commence au niveau le plus élevé. La requête y trouve le nœud qui correspond le mieux, descend au niveau suivant à partir de ce nœud et répète ce processus. Cela se poursuit jusqu’au niveau le plus bas. Aux niveaux supérieurs et intermédiaires, la recherche est strictement opportuniste : on se déplace vers le meilleur voisin et on s’arrête lorsque rien ne s’améliore, avant de descendre. Le niveau le plus bas est plus exploratoire. Une file d’attente par priorité de candidats part du nœud d’entrée, et la recherche continue d’ajouter de bonnes correspondances tout en examinant plusieurs voisins en même temps.

Deux paramètres sont importants dans la pratique :

  • M représente le nombre de connexions par nœud. Une valeur plus élevée de M améliore le taux de rappel, mais consomme plus de mémoire et augmente le temps de construction.
  • efSearch indique le nombre de candidats explorés lors d’une requête. Une valeur plus élevée améliore le taux de rappel, mais à un coût de latence plus élevée.

En termes simples, M influence la qualité de l’index, tandis que efSearch compromet la précision au profit de la vitesse lors des requêtes.

IVF, clustering en premier

L’IVF (index de fichiers inversé) est courant dans FAISS et adopte une approche complètement différente. Il ne crée pas de graphe ; il effectue un clustering en regroupant des vecteurs similaires et ne recherche que les groupes pertinents. Une bibliothèque en est un bon exemple : les livres sont classés par catégorie, de sorte que personne n’a besoin de parcourir chaque étagère.

Lors de l’insertion, un algorithme de clustering comme k-means divise les vecteurs en N clusters. Chaque cluster possède un centre de gravité, et chaque vecteur est attribué au plus proche d’entre eux. Lors d’une requête, l’embedding est comparé à tous les centres de gravité, le cluster le plus proche est sélectionné, puis la requête est évaluée pour chaque vecteur de ce cluster afin de retourner les meilleurs résultats.

Les deux méthodes diffèrent en termes de consommation mémoire. L’IVF stocke moins de connexions, ce qui lui permet d’utiliser moins de mémoire. Le HNSW conserve un graphe avec de nombreuses arêtes par nœud, ce qui entraîne une consommation mémoire plus élevée.

Où la recherche vectorielle pure manque à ses fins

Les embeddings capturent le sens, le contexte et la proximité conceptuelle. Ils sont moins efficaces pour les mots-clés exacts, les nombres et les identifiants. En recherchant « kafka error code 504 », un index vectoriel pourrait retourner « kafka network issues » ou « timeouts in distributed systems », tout en omettant le document qui contient réellement le code 504.

Les systèmes RAG en production résolvent ce problème grâce à une recherche hybride, qui combine la récupération sémantique (embeddings associés à des algorithmes tels que HNSW ou IVF) avec la récupération par mots-clés pour les correspondances exactes utilisant BM25. Dans une architecture typique, ces deux types de recherche s’exécutent sur des systèmes distincts, et leurs résultats sont fusionnés puis réclassés.

Les bases de données vectorielles plus modernes gèrent internement la recherche hybride, il n’est donc pas nécessaire d’utiliser un second système. Weaviate et Pinecone le prennent en charge nativement. Cela devient rapidement la norme pour les environnements de production, car il combine la compréhension du sens avec la correspondance à l’intention exacte.

Le mode hybride ne signifie-t-il pas stocker tout deux fois ?

Avec une configuration externe, oui : les mêmes données se trouvent souvent à la fois dans la base de données vectorielle et dans un moteur de mots-clés comme Elasticsearch. C’est intentionnel. Le stockage vectoriel conserve un index HNSW ou IVF pour la similarité sémantique, tandis que le moteur de mots-clés conserve un index inversé pour les recherches exactes. Chaque index est conçu pour un but précis, donc il ne s’agit pas de redondance.

Les équipes conservent généralement une seule source de vérité, comme une base de données ou un data lake, et la synchronisent dans les deux systèmes via des pipelines. Cela assure la cohérence entre les deux tout en maintenant des recherches rapides et précises.

Re-classement des résultats principaux

De nombreux ingénieurs pensent que le travail est terminé une fois que les K résultats principaux sont obtenus depuis la base de données vectorielle. Ce n’est pas le cas. Ces résultats sont seulement approximativement pertinents, pas parfaitement ordonnés, et une pertinence plus fine peut être manquée.

Prenons la requête « kafka error 504 fix ». Les K résultats principaux pourraient être « Kafka timeout issues », puis « Distributed system errors », puis « Error code 504 gateway timeout ». Le troisième résultat devrait clairement être en premier, mais la recherche vectorielle ne parvient pas toujours à le faire.

Le re-classement consiste à reprendre les éléments récupérés et à les réordonner en analysant plus en profondeur la requête. Les bases de données vectorielles ne le font généralement pas elles-mêmes, donc cette logique se trouve dans la couche d’application. Deux approches prédominent :

  1. Re-classement basé sur un LLM. Envoyez la requête ainsi que les fragments à un LLM et demandez-lui de les classer en fonction de leur pertinence. Cela se met en œuvre facilement et le modèle comprend bien le contexte. En revanche, c’est lent en raison de l’appel supplémentaire, et les coûts augmentent à grande échelle.
  2. Encodage croisé. C’est la méthode standard pour un re-classement de haute qualité. Le modèle prend la requête et un fragment ensemble en tant que paire et génère un score de pertinence. Vous répétez cette opération pour tous les candidats du top-K puis vous classez selon le score. Cette méthode est déterministe et nettement plus précise.

Un exemple de code compact d’encodage croisé avec LangChain :

"""Super-simple CrossEncoder reranking example.

Steps:
1. Define a query and a few short documents.
2. Build (query, doc) pairs.
3. Use a CrossEncoder to get a relevance score for each pair.
4. Print raw scores, then print documents sorted by score.
"""

from sentence_transformers import CrossEncoder


def main() -> None:
        # 1. Create model
model_name = "cross-encoder/ms-marco-MiniLM-L-6-v2"
print(f"Loading CrossEncoder model: {model_name}\n")
model = CrossEncoder(model_name)

    # 2. Query and documents
        query = "What is a vector database?"
documents = [
        "A vector database stores embeddings and allows similarity search.",
        "Relational databases store structured data in tables.",
        "FAISS is a library for efficient similarity search of vectors.",
        "Vector databases are used in AI applications like RAG.",
        ]

        # 3. Build (query, doc) pairs
pairs = [(query, doc) for doc in documents]

        # 4. Get scores
scores = model.predict(pairs)

print("Query:\n  " + query + "\n")
print("Raw scores (higher = more relevant):")
    for doc, score in zip(documents, scores):
print(f"  score={score:.4f}  |  doc={doc}")

    # 5. Sort by score (descending)
ranked = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True)

print("\nDocuments sorted by cross-encoder score:\n")
    for rank, (doc, score) in enumerate(ranked, start=1):
print(f"Rank {rank}: score={score:.4f}")
print(f"  {doc}\n")


if __name__ == "__main__":
main()

Output
****************************************************
Query:
  What is a vector database?

Raw scores (higher = more relevant):
  score=8.4591  |  doc=A vector database stores embeddings and allows similarity search.
  score=-7.1590  |  doc=Relational databases store structured data in tables.
  score=1.0106  |  doc=FAISS is a library for efficient similarity search of vectors.
  score=7.0736  |  doc=Vector databases are used in AI applications like RAG.

Documents sorted by cross-encoder score:

Rank 1: score=8.4591
  A vector database stores embeddings and allows similarity search.

Rank 2: score=7.0736
  Vector databases are used in AI applications like RAG.

Rank 3: score=1.0106
  FAISS is a library for efficient similarity search of vectors.

Rank 4: score=-7.1590
  Relational databases store structured data in tables.

Mesure de la similarité

La qualité de la récupération dépend fortement de la manière dont la proximité vectorielle est calculée. Trois mesures couvrent presque tous les cas.

Similarité cosinus : elle prend en compte l’angle entre deux vecteurs, c’est-à-dire uniquement leur direction. Deux flèches pointant dans la même direction sont très similaires, tandis que des flèches opposées sont complètement différentes, quel que soit leur longueur. La formule est cos(θ) = A·B / (‖A‖‖B‖). Pour la requête « Kafka error 504 », un document sur « Gestion des erreurs Kafka » obtiendrait un score élevé (autour de 0,9), tandis qu’un document sur « Indexation de bases de données » aurait un score bas (autour de 0,2). Cette méthode convient bien aux embeddings et constitue le choix habituel dans les systèmes RAG, car c’est le sens qui compte, et non l’amplitude. Essayez avec query = [1, 1], docA = [2, 2] et docB = [1, 0] : docA pointe dans la même direction que la requête, donc son score en similarité cosinus est parfait, même s’il est plus long.

Produit scalaire combine direction et magnitude : A·B = |A| × |B| × cos(θ). Les vecteurs plus grands sont considérés comme plus importants, et ceux qui pointent dans la même direction sont considérés comme plus similaires.

Distance euclidienne mesure l’écart en ligne droite entre des points. Une distance plus faible signifie une plus grande similarité. Elle est principalement utilisée pour les problèmes de clustering.

Comment les éléments s’assemblent

Un bon pipeline RAG va au-delà d’une base de données vectorielle simplement ajoutée à un LLM. La direction dans un embedding porte un sens, tandis que la magnitude indique la force du signal. FAISS, Pinecone et Weaviate utilisent HNSW et IVF pour effectuer des recherches à grande échelle. Par la suite, la qualité dépend de quelques décisions supplémentaires :

  • diviser soigneusement les documents afin de préserver le contexte
  • utiliser une recherche hybride pour couvrir les correspondances sémantiques et basées sur des mots-clés
  • reclasser les résultats à l’aide d’un cross-encoder ou d’un LLM
  • Écrire des prompts solides pour guider le modèle et limiter les hallucinations
  • Adapter les seuils top-K et de score
  • Toute la chaîne de traitement comprend : les données, le découpage en chunks, les embeddings, l’indexation, la récupération, le réclassement, les prompts, le LLM et la réponse.