درس 3 — Construire pas à pas

VECTOR_SEARCH, filtres SQL et index

5 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

VECTOR_SEARCH, filtres SQL et index

1. La requête

VECTOR_SEARCH(table_de_base, 'colonne', table_requête, top_k, distance_type) compare chaque vecteur de la requête à ceux de la table et renvoie les top_k plus proches.

files/bq_rag.py
def build_query(categorie=None, annee_min=None, top_k=4) -> tuple[str, dict]:
    where, params = [], {}
    if categorie:
        if categorie not in CATEGORIES:                     # ("rh", "finance", "securite")
            raise ValueError(f"catégorie inconnue : {categorie!r} (attendu : {', '.join(CATEGORIES)})")
        where.append("categorie = @categorie")
        params["categorie"] = categorie
    if annee_min:
        where.append("annee >= @annee_min")
        params["annee_min"] = int(annee_min)
    base = f"(SELECT * FROM `{TABLE}`" + (f" WHERE {' AND '.join(where)})" if where else ")")
    sql = f"""
SELECT base.doc, base.titre, base.annee, base.texte, distance
FROM VECTOR_SEARCH(
  {base}, 'embedding',
  (SELECT @query_embedding AS embedding),
  top_k => {int(top_k)}, distance_type => 'COSINE')
ORDER BY distance"""
    return sql, params

Le résultat expose deux structures, query et base (la ligne trouvée), et la colonne distance. Avec COSINE, 0 = même direction, plus c'est petit plus c'est proche.

flowchart LR
  P["@query_embedding (768 nombres)"] --> VS["VECTOR_SEARCH"]
  W["Sous-requête de base : WHERE categorie = @categorie AND annee >= @annee_min"] --> VS
  VS --> R["top_k lignes : base.*, distance"]

2. Les paramètres, pas la concaténation

files/bq_rag.py
qp = [bigquery.ArrayQueryParameter("query_embedding", "FLOAT64", embed([requete], "RETRIEVAL_QUERY")[0])]
qp += [bigquery.ScalarQueryParameter(k, "INT64" if isinstance(v, int) else "STRING", v) for k, v in params.items()]
job = bigquery.Client(project=PROJECT, location=LOCATION).query(sql, job_config=bigquery.QueryJobConfig(
    query_parameters=qp, maximum_bytes_billed=MAX_BYTES))   # 100 Mo : au-delà, BigQuery refuse

Les valeurs ('rh', 2026, le vecteur) voyagent à côté du SQL : même si le modèle invente rh' OR 1=1 --, ce texte ne peut pas devenir du SQL. Et ici il n'arrive même pas jusqu'à BigQuery : la liste fermée CATEGORIES le refuse avant.

3. Pré-filtre ou post-filtre ?

Où est le WHEREEffet
dans la sous-requête de base (notre cas)on cherche les top_k parmi les lignes filtrées : on obtient toujours top_k résultats s'il y en a assez
après VECTOR_SEARCHon cherche les top_k dans toute la table, puis on filtre : on peut en obtenir moins, voire zéro

4. Faut-il un index vectoriel ?

Sans index, VECTOR_SEARCH fait une recherche exacte (brute force) : il compare la requête à toutes les lignes. Pour 19 morceaux, c'est instantané et parfaitement exact.

SituationRecommandation
table < 10 Mopas d'index : la doc indique qu'il n'est pas rempli et que la recherche repasse en force brute
des centaines de milliers de lignes, requêtes fréquentesCREATE VECTOR INDEX … OPTIONS(index_type = 'IVF')
lots de centaines de vecteurs de requêteTreeAH, optimisé pour les requêtes par lots
filtres fréquents avec indexdéclarer les colonnes filtrées dans STORING, sinon le filtre devient un post-filtre
exemple (non exécuté dans ce lab)
CREATE VECTOR INDEX chunks_idx ON lab_rag_bq.chunks(embedding)
STORING (categorie, annee)
OPTIONS (index_type = 'IVF', distance_type = 'COSINE');

Un index rend la recherche approchée (le rappel peut être inférieur à 100 %) et son stockage est facturé tant qu'il existe. À 19 lignes, il n'apporterait que des inconvénients.

Avec un pré-filtre categorie = 'finance' et top_k = 4, combien de lignes reçoit-on si la catégorie compte 4 morceaux ou plus ?

Quand créer un index vectoriel sur la table du lab ?

Exercice : un filtre « hors ancienne version »

Ajoutez à build_query un paramètre exclure_doc (identifiant d'un document à exclure, par exemple politique-teletravail-2024) sans ouvrir de faille d'injection.

Voir la solution
if exclure_doc:
    if exclure_doc not in METADATA:
        raise ValueError(f"document inconnu : {exclure_doc!r}")
    where.append("doc != @exclure_doc")
    params["exclure_doc"] = exclure_doc

Deux protections : la valeur doit exister dans la liste connue (METADATA), et elle passe en paramètre @exclure_doc (le code existant transforme toute valeur texte en ScalarQueryParameter de type STRING). Ajoutez un test qui vérifie "doc != @exclure_doc" in sql.

Résultat obtenu

Exécutions réelles du 2026-10-15 :

$ python bq_rag.py search "nombre de jours de télétravail par semaine"
  [BigQuery] 124107 octets lus, filtre=aucun
  0.227  Politique de télétravail (v3) (2026) : - Pendant les 4 premières semaines d'un nouvel arrivant, …
  0.237  Politique de télétravail (v2, ancienne) (2024) : - Le lundi et le jeudi sont des jours de présence obligatoire.
  …
$ python bq_rag.py search "nombre de jours de télétravail par semaine" rh 2026
  [BigQuery] 124227 octets lus, filtre={'categorie': 'rh', 'annee_min': 2026}
  0.227  Politique de télétravail (v3) (2026) : - Pendant les 4 premières semaines d'un nouvel arrivant, …
  0.254  Politique de télétravail (v3) (2026) : Les alternants et stagiaires peuvent télétravailler 1 jour …
$ bq query --dry_run 'SELECT doc, texte FROM `<projet>.lab_rag_bq.chunks` WHERE categorie = "rh"'
Query successfully validated. […] running this query will process 6772 bytes of data.

Sans filtre, la v2 de 2024 arrive en 2e position ; avec le filtre, elle disparaît.

Je sais faire

  • Écrire un VECTOR_SEARCH avec distance cosinus et top_k
  • Passer filtres et vecteur en paramètres de requête
  • Distinguer pré-filtre et post-filtre
  • Décider quand créer un index vectoriel et lequel
التالي