Exploration des bases de données vectorielles: Google Cloud Vertex AI Vector Search

Exploration Google Cloud Vertex AI Vector Search, un moteur de recherche de similarité managé.

2025-12-03

gcpAI

Nous allons continuer dans cet article notre exploration des bases de données vectorielles pour notre projet d'agent IA de documentation.

Structure:

Dans l'article précédent avec Google Cloud Vertex AI RAG engine, nous avons mis en place le corpus de RAG engine, uploadé nos fichiers et nous avons fait des requêtes de recherche. Le problème que nous avons rencontré dans ce cas se trouve au niveau du filtrage des fichiers. Nous avons plusieurs types et pour la plupart des requêtes, nous ne voulons que des recherches dans un type de fichier spécifique (comme les fichiers terraform par exemple). L'idée maintenant c'est d'implémenter cette approche avec Google Cloud Vector Search.

C'est quoi Vertex AI Vector Search?

Vertex AI Vector Search est un moteur de recherche de similarité managé conçu pour les applications d'IA modernes, notamment les architectures RAG et les systèmes de recommandation à grande échelle. Comme nous l'avons spécifié dans la partie précédente, le but c'est de prendre nos fichiers, les chunker, faire l'embedding, les charger dans Vector Search et faire des requêtes de recherche.

Partie 1: Chunking

Contrairement à RAG Engine où nous avons laissé le RAG faire le chunking, avec Vector Search, c'est à nous de le faire. Ceci nous permettra d'ajouter des métadonnées si on le veut.

Dans mon cas, avec un script j'ai réussi à faire du chunking sur tous mes fichiers pour avoir un résultat qui ressemble à ceci


  {
    "content": "",
    "metadata": {
      "filename": "readme.md",
      "chunk_index": 0,
      "headers": [
        "Introduction"
      ],
      "has_table": false,
      "has_code": false,
      "chunk_type": "text",
      "section_depth": 1,
      "word_count": 351,
      "char_count": 2422,
      "file_tag": []
    }
  },
  ...
  {
    "content": "...",
    "metadata": {
      "word_count": 270,
      "has_code": true,
      "char_count": 2624,
      "chunk_type": "terraform_blocks",
      "has_table": false,
      "headers": [
        "configuration",
        "variable"
      ],
      "chunk_index": 1,
      "filename": "variables.tf",
      "file_tag": [
        "schema"
      ]
    }
  }

Le plus important dans mon cas c'est d'avoir content et des métadonnées comme filename ou chunk_type.

Pour l'embedding, on le spécifiera après.

Partie 2: Charger les données dans Vector Search

Vector Search fonctionne avec des index de recherche. Pour cela, il faut que nous créions un index


vs_index = aiplatform.MatchingEngineIndex.create_tree_ah_index(
    display_name=VERTEX_SEARCH_DISPLAY_NAME,
    description=VERTEX_SEARCH_DISPLAY_DESCRIPTION,
    dimensions=VERTEX_SEARCH_DIMENSION,
    approximate_neighbors_count=10,
    leaf_node_embedding_count=500,
    leaf_nodes_to_search_percent=7,
    distance_measure_type="DOT_PRODUCT_DISTANCE",
    feature_norm_type="UNIT_L2_NORM",
    index_update_method="STREAM_UPDATE",
    contents_delta_uri=f"gs://{STAGING_BUCKET}/vs-contents-delta-uri"
)

Voici une petite explication de ce code:

  • dimensions, c'est la taille des vecteurs (embeddings)
  • approximate_neighbors_count=10 signifie qu’on veut environ 10 résultats similaires à la requête.
  • leaf_node_embedding_count indique combien de vecteurs on stocke en moyenne dans chaque “feuille” de cet arbre.
  • leaf_nodes_to_search_percent: Pourcentage par défaut de nœuds feuilles sur lesquels toute requête peut être effectuée. Doit être compris entre 1 et 100 inclus.
  • distance_measure_type indique comment Vertex AI mesure la similarité entre deux vecteurs. distance_measure_type="DOT_PRODUCT_DISTANCE" On utilise le produit scalaire (dot product), très courant pour les embeddings issus de modèles de langage.
  • feature_norm_type précise si Vertex AI doit normaliser les vecteurs avant de les comparer.
  • index_update_method décrit comment l’index sera mis à jour au fil du temps.
  • contents_delta_uri est le chemin dans Google Cloud Storage où vous allez déposer les fichiers de mise à jour de l’index. Voir https://docs.cloud.google.com/vertex-ai/docs/vector-search/create-manage-index#create_index-python_vertex_ai_sdk

On peut faire cette commande pour voir les indexes

gcloud ai indexes list --region ...

Pour utiliser notre index, il va nous falloir un endpoint. Les endpoints peuvent être publics, privés (accessibles uniquement depuis un VPC) ou privés avec Private Service Connect.


vs_endpoint = aiplatform.MatchingEngineIndexEndpoint.create(
    display_name=f"{VERTEX_SEARCH_DISPLAY_NAME}-public-endpoint",
    public_endpoint_enabled=True
)

L'étape suivante sera de déployer l'index vers un endpoint.

vs_endpoint.deploy_index(
    index=vs,
    deployed_index_id=VERTEX_SEARCH_DEPLOYED_ID,
    min_replica_count=1,
    max_replica_count=1,
    machine_type="e2-standard-2"
)

Il est recommandé que l'index ait au moins deux réplicas.

Pour charger les chunks dans notre index, nous allons utiliser le vector store de langchain. Un peu inhabituel mais c'est le moyen le plus simple et le plus propre que j'ai trouvé pour le faire.

from langchain_google_vertexai import VectorSearchVectorStore
vector_store = VectorSearchVectorStore.from_components(
    project_id=PROJECT_ID,
    region=LOCATION,
    gcs_bucket_name=STAGING_BUCKET,
    index_id=vs.resource_name,
    endpoint_id=vs_endpoint.resource_name,
    embedding=embedding_model,
    stream_update=True,
)
 vector_store.add_texts(
    texts=[
        item["page_content"]
        for item in docs
    ],
    metadatas=[
        item["metadata"]
        for item in docs
    ]
)

De là, nous aurons la capacité de faire des requêtes de recherche sur notre index.

Partie 3: Requêtes de recherche

Voici un exemple de requête de recherche

from google.cloud.aiplatform.matching_engine.matching_engine_index_endpoint import (
    Namespace,
)

rr = vector_store.similarity_search(
    query="Samples",
    k=100,
    filter=[Namespace(name="filename", allow_tokens=["readme.md"])],
)

for doc in rr:
    print(doc.metadata)
    headers = doc.metadata.get("headers", "")

    if "sample" in headers.lower():
        print(doc.metadata)

Dans ce code, on utilise la méthode similarity_search de notre vector_store pour faire une recherche de similarité avec la requête "Samples". On utilise le paramètre filter pour filtrer les résultats en fonction du nom de fichier. Dans ce cas, on ne veut que les résultats qui ont "readme.md" comme nom de fichier.

Partie 4: Batch vs Stream, et les opérations CRUD

Vector Search a deux modes de mise à jour d'index : Batch et Stream. Le choix se fait à la création (index_update_method, Partie 2) et n'est pas réversible.

En Batch, les datapoints sont traités par gros paquets : je dépose les fichiers d'embeddings sur GCS (contents_delta_uri), puis je déclenche une mise à jour qui relit tout le delta d'un coup. Ça convient à de gros volumes ou des mises à jour planifiées (job nocturne), quand un délai de propagation est acceptable.

Dans mon cas, ça ne convenait pas : à chaque modification d'un fichier source, je veux que le chunk correspondant soit à jour tout de suite. J'ai donc créé l'index directement avec index_update_method="STREAM_UPDATE". En Stream, chaque datapoint est traité dès réception et visible en recherche en quelques secondes, ce qui permet par exemple de vérifier une image à l'upload contre l'index existant pour détecter les doublons en temps réel.

À noter : impossible de migrer un index batch vers stream après coup, il faut le prévoir dès la création.

Insert, Read, Delete

Hors LangChain, ces opérations se font en REST. L'insert (upsertDatapoints) et le delete (removeDatapoints) renvoient {} en cas de succès, à gérer explicitement côté application :

curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer `gcloud auth print-access-token`" \
https://${REGION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${REGION}/indexes/${INDEX_ID}:upsertDatapoints \
-d '{datapoints: [{datapoint_id: "111", feature_vector: [0.111, 0.111]}]}'

Deux pièges sur l'insert : datapoint_id accepte des chaînes de caractères, pas seulement des nombres, et l'upsert n'existence-check pas, un id déjà présent est écrasé silencieusement. Il faut faire ce contrôle soi-même en amont si on veut éviter les écrasements accidentels.

Côté lecture, en plus de similarity_search (Partie 3), on peut interroger un datapointId précis directement via findNeighbors :

curl -X POST -H "Authorization: Bearer $(gcloud auth print-access-token)" \
"https://${PUBLIC_ENDPOINT_NAME}/v1/projects/${PROJECT_NUMBER}/locations/${REGION}/indexEndpoints/${ENDPOINT_ID}:findNeighbors" \
-d '{deployedIndexId:"<DEPLOYED_INDEX_ID>", "queries":[{datapoint:{"datapointId":"<DATA_POINT_ID>"}}], returnFullDatapoint:false}'

Et pour supprimer, removeDatapoints prend une liste d'ids (suppression définitive, donc à vérifier avant l'appel) :

curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer `gcloud auth print-access-token`" \
https://${REGION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${REGION}/indexes/${INDEX_ID}:removeDatapoints \
-d '{"datapoint_ids": ["'{DATAPOINT_ID_1}'", "'{DATAPOINT_ID_2}'"]}'

Le vrai problème : il n'existe aucune commande native pour vider ou purger massivement l'index. Pour retirer tous les chunks d'un fichier supprimé, j'ai dû passer par une méthode interne non documentée de LangChain (vector_store._searcher.get_datapoints_by_filter({})) pour lister les ids, puis les supprimer un par un.

Avantages et inconvénients

Avantages

  • Base de données vectorielle managée : entièrement géré par Google Cloud, pas d'infrastructure à maintenir soi-même.
  • Recherche à grande échelle : reste performant même avec des millions de vecteurs stockés.
  • Mode stream : insertions et suppressions visibles en recherche en quelques secondes, adapté à un usage temps réel.

Inconvénients

  • Absence d'interface utilisateur : j'ai dû développer la mienne pour inspecter les chunks et vérifier le contenu de l'index.
  • Filtrage limité : les combinaisons de filtres complexes se heurtent vite aux restrictions du système de namespace, et l'intégration LangChain n'accepte pas les booléens natifs (il faut stocker "true" en string).
  • Déploiement d'endpoint lent et coûteux : jusqu'à 45 minutes, et des ressources dédiées en permanence (ex: e2-standard-2), même en dev.
  • Pas de suppression massive native : il faut passer par une méthode interne de LangChain pour lister les ids avant de les supprimer un par un.

Vector Search est une solution assez mature proposée par Google Cloud pour répondre à des besoins de recherche de similarité à grande échelle.

Cependant, cette exploration m'a montré qu'il y a des limitations à prendre en compte, notamment au niveau de la flexibilité. Dans mon cas, une des finalités de ce projet d'agent IA de documentation est d'avoir la possibilité de faire des recherches assez dynamiques : récupérer les documents, les chunker, les charger dans la base de données vectorielle et faire des recherches avec des filtres assez complexes. Avoir une bonne pipeline de bout en bout est donc crucial pour moi.

C'est pour cela que je vais continuer à explorer d'autres solutions comme ChromaDB et Milvus pour voir si elles peuvent mieux répondre à mes besoins.

J'avoue quand même n'avoir pas tout testé. Je verrai ce que Vector Search 2.0 propose dans un autre article.