
Quand (ne pas) utiliser la base de données vectorielle
. Ils résolvent un problème réel et, dans de nombreux cas, constituent le bon choix pour les systèmes RAG. Mais voici le problème : ce n’est pas parce que vous utilisez des intégrations que vous besoin une base de données vectorielles.
Nous avons constaté une tendance croissante selon laquelle chaque implémentation de RAG commence par le branchement d’une base de données vectorielle. Cela peut avoir du sens pour les bases de connaissances persistantes à grande échelle, mais ce n’est pas toujours la voie la plus efficace, en particulier lorsque votre cas d’utilisation est plus dynamique ou sensible au facteur temps.
Chez Planck, nous utilisons des intégrations pour améliorer les systèmes basés sur LLM. Cependant, dans l’une de nos applications réelles, nous avons choisi de éviter une base de données vectorielles et a plutôt utilisé un magasin clé-valeur simplequi s’est avéré être une bien meilleure solution.
Avant de plonger dans le vif du sujet, explorons une version simple et généralisée de notre scénario pour expliquer pourquoi.
Exemple de Foo
Imaginons un système simple de style RAG. Un utilisateur télécharge quelques fichiers texte, peut-être des rapports ou des notes de réunion. Nous divisons ces fichiers en morceaux, générons des intégrations pour chaque morceau et utilisons ces intégrations pour répondre aux questions. L’utilisateur pose une poignée de questions au cours des prochaines minutes, puis s’en va. À ce stade, les fichiers et leurs intégrations sont inutiles et peuvent être supprimés en toute sécurité.
En d’autres termes, les données sont éphémèrel’utilisateur ne demandera qu’un quelques questionset nous voulons y répondre le plus vite possible.
Maintenant, faites une pause une seconde et demandez-vous :
Où dois-je stocker ces intégrations ?
L’instinct de la plupart des gens est : « J’ai des intégrations, j’ai donc besoin d’une base de données vectorielles », mais faites une pause une seconde et réfléchissez à ce qui se passe réellement derrière cette abstraction. Lorsque vous envoyez des intégrations à une base de données vectorielle, elle ne se contente pas de les « stocker ». Il crée un index qui accélère les recherches de similarité. C’est de ce travail d’indexation que vient une grande partie de la magie, et aussi de là où réside une grande partie des coûts.
Dans une base de connaissances à grande échelle et de longue durée, ce compromis est parfaitement logique : vous payez un coût d’indexation une fois (ou progressivement à mesure que les données changent), puis répartissez ce coût sur des millions de requêtes. Dans notre exemple Foo, ce n’est pas ce qui se passe. Nous faisons le contraire : ajouter constamment de petits lots ponctuels d’intégrations, répondre à un petit nombre de requêtes par lot, puis tout jeter.
La vraie question n’est donc pas « dois-je utiliser une base de données vectorielles ? mais « le travail d’indexation en vaut-il la peine ?« Pour répondre à cela, on peut s’appuyer sur un simple benchmark.
Analyse comparative : récupération sans index et récupération indexée

Cette section est plus technique. Nous examinerons le code Python et expliquerons les algorithmes sous-jacents. Si les détails exacts de la mise en œuvre ne vous concernent pas, n’hésitez pas à passer directement à la Résultats section.
Nous souhaitons comparer deux systèmes :
- Pas d’indexation du tout, conserve simplement les intégrations en mémoire et les analyse directement.
- Une base de données vectoriellesoù nous payons un coût d’indexation à l’avance pour rendre chaque requête plus rapide.
Tout d’abord, considérons l’approche « sans base de données vectorielle ». Lorsqu’une requête arrive, nous calculons les similitudes entre l’intégration de la requête et toutes les intégrations stockées, puis sélectionnons le top-k. C’est juste K-Voisins les plus proches sans aucun indice.
import numpy as np
def run_knn(embeddings: np.ndarray, query_embedding: np.ndarray, top_k: int) -> np.ndarray:
sims = embeddings @ query_embedding
return sims.argsort()[-top_k:][::-1]
Le code utilise le produit scalaire comme proxy pour similarité cosinus (en supposant des vecteurs normalisés) et trie les scores pour trouver les meilleures correspondances. C’est littéralement juste scanne tous les vecteurs et sélectionne les plus proches.
Voyons maintenant ce que fait généralement une base de données vectorielle. Sous le capot, la plupart des bases de données vectorielles reposent sur un Indice approximatif du voisin le plus proche (ANN). Les méthodes ANN échangent un peu de précision contre une augmentation importante de la vitesse de recherche, et l’un des algorithmes les plus largement utilisés à cet effet est HNSW. Nous utiliserons le hnswlib bibliothèque pour simuler le comportement de l’index.
import numpy as np
import hnswlib
def create_hnsw_index(embeddings: np.ndarray, num_dims: int) -> hnswlib.Index:
index = hnswlib.Index(space='cosine', dim=num_dims)
index.init_index(max_elements=embeddings.shape[0])
index.add_items(embeddings)
return index
def query_hnsw(index: hnswlib.Index, query_embedding: np.ndarray, top_k: int) -> np.ndarray:
labels, distances = index.knn_query(query_embedding, k=top_k)
return labels[0]
Pour voir où se situe le compromis, nous pouvons générer des intégrations aléatoires, les normaliser et mesurer la durée de chaque étape :
import time
import numpy as np
import hnswlib
from tqdm import tqdm
def run_benchmark(num_embeddings: int, num_dims: int, top_k: int, num_iterations: int) -> None:
print(f"Benchmarking with {num_embeddings} embeddings of dimension {num_dims}, retrieving top-{top_k} nearest neighbors.")
knn_times: list[float] = []
index_times: list[float] = []
hnsw_query_times: list[float] = []
for _ in tqdm(range(num_iterations), desc="Running benchmark"):
embeddings = np.random.rand(num_embeddings, num_dims).astype('float32')
embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
query_embedding = np.random.rand(num_dims).astype('float32')
query_embedding = query_embedding / np.linalg.norm(query_embedding)
start_time = time.time()
run_knn(embeddings, query_embedding, top_k)
knn_times.append((time.time() - start_time) * 1e3)
start_time = time.time()
vector_db_index = create_hnsw_index(embeddings, num_dims)
index_times.append((time.time() - start_time) * 1e3)
start_time = time.time()
query_hnsw(vector_db_index, query_embedding, top_k)
hnsw_query_times.append((time.time() - start_time) * 1e3)
print(f"BENCHMARK RESULTS (averaged over {num_iterations} iterations)")
print(f"[Naive KNN] Average search time without indexing: {np.mean(knn_times):.2f} ms")
print(f"[HNSW Index] Average index construction time: {np.mean(index_times):.2f} ms")
print(f"[HNSW Index] Average query time with indexing: {np.mean(hnsw_query_times):.2f} ms")
run_benchmark(num_embeddings=50000, num_dims=1536, top_k=5, num_iterations=20)
Résultats
Dans cet exemple, nous utilisons 50 000 intégrations avec 1 536 dimensions (correspondant à celles d’OpenAI). text-embedding-3-small) et récupérez les 5 premiers voisins. Les résultats exacts varient selon les configurations, mais le modèle qui nous intéresse est le même.
Je vous encourage à exécuter le benchmark avec vos propres chiffres, c’est le meilleur moyen de voir comment les compromis se déroulent dans votre cas d’utilisation spécifique.
En moyenne, la recherche naïve KNN prend 24,54 millisecondes par requête. La création de l’index HNSW pour les mêmes intégrations prend environ 277 secondes. Une fois l’index généré, chaque requête prend environ 0,47 milliseconde.
À partir de là, nous pouvons estimer le seuil de rentabilité. La différence entre les requêtes KNN naïves et indexées est de 24,07 ms par requête. Cela implique que vous avez besoin 11 510 requêtes avant que le temps gagné sur chaque requête ne compense le temps passé à construire l’index.

De plus, même avec des valeurs différentes pour le nombre d’intégrations et le top-k, le seuil de rentabilité reste dans les milliers de requêtes et reste dans une plage assez étroite. Vous n’obtenez pas de scénario dans lequel l’indexation commence à porter ses fruits après seulement quelques dizaines de requêtes.

Comparez maintenant cela à l’exemple de Foo. Un utilisateur télécharge un petit ensemble de fichiers et pose quelques questions, pas des milliers. Le système n’atteint jamais le point où l’indice porte ses fruits. Au lieu de cela, l’étape d’indexation retarde simplement le moment où le système peut répondre à la première question et ajoute une complexité opérationnelle.
Pour ce type de contexte éphémère par utilisateur, l’approche KNN simple en mémoire est non seulement plus facile à mettre en œuvre et à exploiter, mais elle est également plus rapide de bout en bout.
Si le stockage en mémoire n’est pas une option, soit parce que le système est distribué, soit parce que nous devons conserver l’état de l’utilisateur pendant quelques minutes, nous pouvons utiliser un magasin clé-valeur comme Rédis. Nous pouvons stocker un identifiant unique pour la demande de l’utilisateur en tant que clé et stocker toutes les intégrations en tant que valeur.
Cela nous donne une solution légère et peu complexe, bien adaptée à notre cas d’utilisation de contextes de courte durée et peu de requêtes.
Exemple concret : pourquoi nous avons choisi un magasin à valeur clé

À Plancknous répondons aux questions liées à l’assurance des entreprises. Une demande typique commence par un nom et une adresse d’entreprise, puis nous récupérons données en temps réel sur cette entreprise spécifique, y compris sa présence en ligne, ses enregistrements et autres documents publics. Ces données deviennent notre contexte et nous utilisons des LLM et des algorithmes pour répondre aux questions basées sur celles-ci.
L’important est que chaque fois que nous recevons une demande, nous générons un nouveau contexte. Nous ne réutilisons pas les données existantes, elles sont récupérées à la demande et restent pertinentes pour quelques minutes tout au plus.
Si vous repensez au benchmark précédent, ce modèle devrait déjà déclencher votre capteur « ce n’est pas un cas d’utilisation de base de données vectorielle ».
Chaque fois que nous recevons une demande, nous générons de nouvelles intégrations pour des données de courte durée que nous n’interrogerons probablement que quelques centaines de fois. L’indexation de ces intégrations dans une base de données vectorielle ajoute une latence inutile. En revanche, avec Redis, nous pouvons stocker immédiatement les intégrations et lancer une recherche rapide de similarité dans le code de l’application avec presque aucun délai d’indexation.
C’est pourquoi nous avons choisi Rédis au lieu d’une base de données vectorielle. Bien que les bases de données vectorielles soient excellentes pour gérer de grands volumes d’intégrations et prendre en charge les requêtes rapides du voisin le plus proche, elles introduisent surcharge d’indexationet dans notre cas, ces frais généraux n’en valent pas la peine.
En conclusion
Si vous avez besoin de stocker des millions d’intégrations et de prendre en charge des charges de travail nécessitant de nombreuses requêtes sur un corpus partagé, une base de données vectorielle serait mieux adaptée. Et oui, il existe certainement des cas d’utilisation qui ont vraiment besoin et bénéficient d’une base de données vectorielle.
Mais juste parce que vous utilisez des intégrations ou construire un système RAG ne signifie pas que vous devez utiliser par défaut une base de données vectorielle.
Chaque technologie de base de données possède ses atouts et ses inconvénients. Le meilleur choix commence par une compréhension approfondie de vos données et de votre cas d’utilisation, plutôt que de suivre inconsidérément la tendance.
Alors, la prochaine fois que vous aurez besoin de choisir une base de données, faites une pause et demandez-vous : est-ce que je choisis la bonne en fonction de compromis objectifs, ou est-ce que je choisis simplement le choix le plus tendance et le plus brillant ?



