Aller au contenu principal

Vector Store avec FAISS

Mis à jour le 29 juillet 2026

FAISS : recherche vectorielle haute performance

Vos embeddings sont calculés et sauvegardés, mais une matrice NumPy n’est pas un moteur de recherche : comparer une requête à cent mille vecteurs par boucle Python prendrait plusieurs secondes par question. FAISS (Facebook AI Similarity Search) répond exactement à ce besoin. Cette bibliothèque open source de Meta est conçue pour la recherche de similarité dans des ensembles de vecteurs de grande taille ; elle est rapide, efficace en mémoire, et fonctionne aussi bien avec quelques centaines qu’avec des millions de vecteurs. L’installation se fait en une commande, faiss-cpu suffisant dans l’immense majorité des cas, faiss-gpu ne se justifiant que sur des volumes où le calcul devient le goulot d’étranglement.

# pip install faiss-cpu
# ou faiss-gpu pour l'accélération GPU

import faiss
import numpy as np

Créer un premier index

L’index le plus simple est IndexFlatL2, qui effectue une recherche exhaustive par distance L2, c’est-à-dire euclidienne. « Exhaustive » signifie qu’il compare réellement la requête à tous les vecteurs stockés : aucune approximation, donc aucun résultat manqué. On l’initialise avec la dimension des vecteurs — 1024 pour mistral-embed, valeur que l’on lit directement dans la forme de la matrice plutôt que de la coder en dur. La conversion en float32 n’est pas facultative : FAISS refuse le float64 que NumPy produit par défaut, et l’oubli de ce détail est la première erreur que rencontre tout débutant.

# Supposons que nos embeddings sont une matrice NumPy
# embeddings.shape = (n_chunks, 1024)

# Créer l'index
dimension = embeddings.shape[1]  # 1024 pour mistral-embed
index = faiss.IndexFlatL2(dimension)

# Ajouter les vecteurs
index.add(embeddings.astype(np.float32))  # FAISS requiert float32

print(f"Index créé: {index.ntotal} vecteurs de dimension {dimension}")

Choisir le bon type d’index

FAISS ne propose pas un index mais une famille, et le choix se fait sur un arbitrage entre précision et vitesse. Les deux premiers, IndexFlatL2 et IndexFlatIP, sont exacts : ils ne se distinguent que par la mesure employée, distance euclidienne pour l’un, produit scalaire pour l’autre. IndexIVFFlat change de logique en partitionnant l’espace en clusters et en ne fouillant que les plus prometteurs, ce qui accélère fortement la recherche au prix d’une légère perte de rappel — et l’oblige à un entraînement préalable sur vos données. IndexHNSWFlat, enfin, construit un graphe de voisins et se révèle très rapide en interrogation, le paramètre 32 fixant ici le nombre de voisins retenus par noeud.

dimension = 1024

# 1. IndexFlatL2 : recherche exacte, distance L2
# Le plus précis, mais le plus lent pour de gros volumes
index_flat_l2 = faiss.IndexFlatL2(dimension)

# 2. IndexFlatIP : recherche exacte, produit scalaire (inner product)
# Utilisez avec des vecteurs normalisés pour la similarité cosinus
index_flat_ip = faiss.IndexFlatIP(dimension)

# 3. IndexIVFFlat : recherche approximative avec partitionnement
# Plus rapide pour de gros volumes, légère perte de précision
nlist = 100  # Nombre de partitions (clusters)
quantizer = faiss.IndexFlatL2(dimension)
index_ivf = faiss.IndexIVFFlat(quantizer, dimension, nlist)
# Nécessite un entraînement avant utilisation :
# index_ivf.train(embeddings)
# index_ivf.add(embeddings)

# 4. IndexHNSWFlat : graphe de voisins (très rapide en recherche)
index_hnsw = faiss.IndexHNSWFlat(dimension, 32)  # 32 = nombre de voisins

Interroger l’index

Rechercher se fait en trois temps. La question de l’utilisateur est d’abord encodée avec le même modèle que le corpus — une requête encodée par un autre modèle produirait des distances dénuées de sens. FAISS renvoie ensuite deux tableaux parallèles, les distances et les indices des k voisins les plus proches. Ces indices sont des positions dans votre liste de chunks : c’est là que se paie la discipline de la leçon précédente, puisque l’alignement entre la matrice et les métadonnées est ce qui permet de remonter au texte et à sa source. Le test if idx >= 0 couvre le cas où l’index contient moins de k vecteurs, FAISS remplissant alors les places manquantes par -1.

from mistralai import Mistral

client = Mistral(api_key=api_key)

def search_index(query, index, chunks, k=5):
    """Rechercher les chunks les plus proches d'une requête."""
    # 1. Encoder la requête
    response = client.embeddings.create(
        model="mistral-embed",
        inputs=[query],
    )
    query_vector = np.array([response.data[0].embedding], dtype=np.float32)

    # 2. Rechercher dans FAISS
    distances, indices = index.search(query_vector, k)

    # 3. Formater les résultats
    results = []
    for i, (dist, idx) in enumerate(zip(distances[0], indices[0])):
        if idx >= 0:  # FAISS retourne -1 si pas assez de résultats
            results.append({
                "rank": i + 1,
                "distance": float(dist),
                "text": chunks[idx]["text"],
                "source": chunks[idx].get("source", ""),
            })

    return results

# Exemple de recherche
results = search_index("Comment fonctionne le RAG ?", index, chunks)
for r in results:
    print(f"  #{r['rank']} (dist: {r['distance']:.4f}) [{r['source']}]")
    print(f"    {r['text'][:100]}...")

Passer à la similarité cosinus

FAISS n’offre pas d’index « cosinus » explicite, mais l’algèbre en fournit un équivalent gratuit : sur des vecteurs de norme 1, le produit scalaire est le cosinus. Il suffit donc de diviser chaque vecteur par sa norme, puis d’utiliser IndexFlatIP. Le changement est loin d’être théorique : la distance L2 est sensible à la longueur des vecteurs, si bien qu’un chunk très long peut se retrouver artificiellement éloigné d’une requête courte alors que leurs sujets coïncident. Le cosinus ne mesure que l’orientation, donc le sens, et c’est en général ce que vous voulez comparer.

def create_cosine_index(embeddings):
    """Créer un index FAISS avec similarité cosinus."""
    # Normaliser les vecteurs (norme L2 = 1)
    norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
    normalized = (embeddings / norms).astype(np.float32)

    # Utiliser IndexFlatIP (produit scalaire = cosinus pour vecteurs normalisés)
    dimension = normalized.shape[1]
    index = faiss.IndexFlatIP(dimension)
    index.add(normalized)

    return index

cosine_index = create_cosine_index(embeddings)

Comme pour les embeddings, l’index se persiste sur disque. faiss.write_index produit un fichier binaire que faiss.read_index recharge tel quel, structures internes comprises — ce qui compte surtout pour les index entraînés, dont la reconstruction serait autrement à refaire.

# Sauvegarder
faiss.write_index(index, "rag_faiss.index")
print("Index FAISS sauvegardé")

# Charger
index_loaded = faiss.read_index("rag_faiss.index")
print(f"Index chargé: {index_loaded.ntotal} vecteurs")

Quand le corpus devient massif

À partir de plusieurs centaines de milliers de vecteurs, la recherche exhaustive devient trop lente et l’index IVF (Inverted File) s’impose. Son cycle de vie comporte une étape que les index plats n’ont pas : l’entraînement, qui analyse la distribution de vos données pour placer les nlist centroïdes de partitions. Vient ensuite l’ajout des vecteurs, puis le réglage de nprobe, le curseur qui décide combien de partitions seront explorées à chaque requête. Augmenter nprobe améliore le rappel et ralentit la recherche ; c’est le paramètre que vous ajusterez en mesurant, sur un jeu de questions représentatif, à partir de quelle valeur les résultats cessent de s’améliorer.

def create_ivf_index(embeddings, nlist=100, nprobe=10):
    """Créer un index IVF pour de gros volumes."""
    dimension = embeddings.shape[1]
    data = embeddings.astype(np.float32)

    # Créer le quantizer et l'index IVF
    quantizer = faiss.IndexFlatL2(dimension)
    index = faiss.IndexIVFFlat(quantizer, dimension, nlist)

    # Entraîner l'index (nécessaire pour IVF)
    print("Entraînement de l'index...")
    index.train(data)

    # Ajouter les vecteurs
    index.add(data)

    # Nombre de partitions à explorer lors de la recherche
    index.nprobe = nprobe  # Plus grand = plus précis, plus lent

    print(f"Index IVF créé: {index.ntotal} vecteurs, {nlist} partitions")
    return index

# Pour 100K+ vecteurs
# large_index = create_ivf_index(embeddings, nlist=256, nprobe=20)

Points clés à retenir

  • FAISS est la référence pour la recherche vectorielle rapide
  • IndexFlatL2 pour les petits corpus (moins de 100K vecteurs), recherche exacte
  • IndexIVFFlat pour les gros corpus, recherche approximative mais rapide
  • Normalisez les vecteurs et utilisez IndexFlatIP pour la similarité cosinus
  • Sauvegardez l’index sur disque avec faiss.write_index pour éviter de reconstruire