Dans mon article précédent, nous avons exploré la création d’un système RAG local pour interroger nos documents personnels. Dans cet article je vous propose d’améliorer ce système en intégrant ChromaDB de manière plus approfondie pour une meilleure gestion des embeddings et des index.

Pourquoi ChromaDB ?

La première implémentation utilisait déjà ChromaDB via LlamaIndex, mais de manière assez basique. ChromaDB mérite qu’on s’y attarde car c’est un composant crucial de notre architecture qui offre :

  • La persistance des embeddings (pas besoin de recalculer à chaque démarrage)
  • Une recherche vectorielle optimisée
  • Une gestion efficace des métadonnées
  • Une excellente intégration avec l’écosystème Python

Index ? Kézako ?

Avant d’aller plus loin dans le code, il faut que l’on cause des index (vectoriels).

Un index vectoriel est comme une bibliothèque organisée où chaque “morceau” de vos documents est transformé en vecteur (une liste de nombres) et stocké de manière structurée. Dans mon programme cela se déroule ainsi :

  1. Découpage des documents :
  • Les documents Markdown sont d’abord découpés en morceaux (chunks) par markdown_processor.py
  • Dans mon programme, chaque chunk fait CHUNK_SIZE = 2048 caractères, avec un chevauchement de CHUNK_OVERLAP = 128
  1. Création des embeddings :
  • Chaque chunk est converti en un vecteur d’embedding par le modèle EMBEDDING_MODEL (all-mpnet-base-v2)
  • Un embedding est un vecteur de nombres (par exemple 384 ou 768 dimensions) qui capture le “sens” du texte
  • Par exemple, des chunks parlant de “réseaux de neurones” (extraits du document example-md.md) auront des vecteurs similaires
  1. Structure de l’index : Dans ChromaDB, l’index contient pour chaque chunk :
  • L’embedding vectoriel
  • Le texte original du chunk
  • Des métadonnées (comme le nom du fichier source, la section)

Un façon de se représenter cela serait (de façon simplifiée) :


Index dans ChromaDB
├── Chunk 1
│ ├── Vecteur: [0.1, 0.3, -0.2, ..., 0.7]
│ ├── Texte: "Les réseaux de neurones sont..."
│ └── Métadonnées: {source: "example-md.md", section: "Introduction"}
├── Chunk 2
│ ├── Vecteur: [-0.2, 0.5, 0.1, ..., -0.3]
│ ├── Texte: "Pour éviter le surapprentissage..."
│ └── Métadonnées: {source: "example-md.md", section: "Bonnes Pratiques"}
...

Quand on pose une question :

  1. La question est convertie en vecteur d’embedding
  2. ChromaDB cherche les chunks dont les vecteurs sont les plus proches (similaires) de la question posée
  3. Ces chunks sont utilisés par le LLM pour générer la réponse

Intégration Améliorée de ChromaDB

Configuration Initiale

Voici comment nous initialisons maintenant ChromaDB de manière plus robuste :

# Configuration dans config.py
from pathlib import Path
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb

CHROMA_PERSIST_DIR = "./chroma_db"
COLLECTION_NAME = "markdown_docs"

def init_chroma_storage():
    """Initialize ChromaDB storage"""
    # On s'assure que le répertoire pour chroma existe
    Path(CHROMA_PERSIST_DIR).mkdir(parents=True, exist_ok=True)

    # Création d'un client ChromaDB avec persistance des données
    chroma_client = chromadb.PersistentClient(path=CHROMA_PERSIST_DIR)

    # Récupère ou créé une collection
    chroma_collection = chroma_client.get_or_create_collection(name=COLLECTION_NAME)

    # Création stockage vecteur
    vector_store = ChromaVectorStore(chroma_collection=chroma_collection)

    return StorageContext.from_defaults(vector_store=vector_store)

Gestion Intelligente des Index

Un écueil à éviter est la recréation d’index qui existent déjà. En effet, à chaque lancement de ce petit programme, celui-ci va retraiter les documents présents dans le répertoire docs et créer de nouveaux index. On ne veut pas de doublons. La solution “basique” mise en oeuvre consiste à vérifier l’existance d’index dans notre collection. :

def get_or_create_index(storage_context):
    """Get existing index or create a new one if needed"""
    # On récupère la collection de ChromaDB
    chroma_collection = storage_context.vector_store._collection

    # Vérification que la collection contient déjà des données.
    try:
        count = chroma_collection.count()
        if count > 0:
            print(f"Index existant chargé depuis ChromaDB ({count} éléments).")
            return VectorStoreIndex.from_vector_store(storage_context.vector_store)
        else:
            print("Création d'un nouvel index...")
            documents = load_markdown_files("docs")
            return VectorStoreIndex.from_documents(
                documents,
                storage_context=storage_context
            )
    except Exception as e:
        print(f"Erreur lors de la vérification de l'index : {e}")
        return create_new_index(storage_context)

Dans ce bout de code on vérifie explicitement le nombre d’éléments dans notre collection avec collection.count(). Si elle est vide on créé de nouveaux index sinon on récupère les données existantes de ChromaDB.

Un peu bourrin comme méthode : elle ne gère pas l’ajout de nouveaux documents. Si l’on souhaite indexer de nouveaux documents il faut recréer les index entièrement avec l’ensemble des documents. Ce qui peut se faire en supprimant le dossier chroma_db et en relançant le programme.

Cette réindexation complète à chaque modification n’est pas efficace.

Je proposerai une méthode plus propre dans un prochain article.

Optimisations et Bonnes Pratiques

1. Gestion de la Mémoire

ChromaDB charge les embeddings en mémoire. Pour optimiser cela :

CHUNK_SIZE = 2048  # Taille des chunks
CHUNK_OVERLAP = 128  # Chevauchement minimal

Ces paramètres permettent un bon équilibre entre contexte et utilisation mémoire.

2. Structure des Métadonnées

ChromaDB permet d’associer des métadonnées à chaque embedding :

documents.append(
    Document(
        text=section_text,
        metadata={
            "source": str(source_path),
            "section": current_title,
            "last_updated": timestamp
        }
    )
)

Prochaines Étapes

  1. Indexation Incrémentale

    • Implémenter la détection des modifications par document
    • Mettre à jour uniquement les chunks modifiés
  2. Migration des Données

    • Gérer les changements de schéma de métadonnées
    • Permettre la réindexation sélective
  3. Interface de Gestion

    • Ajouter des commandes pour la maintenance de l’index
    • Visualiser l’état de l’index

Conclusion

L’intégration approfondie de ChromaDB a significativement amélioré notre système RAG :

  • Meilleure persistance des données
  • Gestion plus intelligente des index
  • Base solide pour des fonctionnalités futures

Dans le prochain article, j’aborderai la gestion efficace des index lors de l’ajout de nouveaux documents mais aussi la question du prompt engineering.


Cet article fait partie d’une série sur l’IA pratique. Retrouvez le code complet sur GitHub.