Inicio / Artículos

RAG sin una base de datos vectorial: un pipeline en BigQuery SQL y dónde falla

Construya una generación mejorada por recuperación de información exclusivamente con SQL de BigQuery, desde los embeddings hasta las respuestas fundamentadas, y luego consulte los costos, cuotas y límites de versionado de prompts.

Editor: REACTAPP.TOP11 min de lectura2592 palabras

Fuentes: SQL Is All You Need: A Complete RAG Pipeline in SQL with BigQuery

Una base de datos vectorial, un orquestador, un servicio de embedding, claves API dispersas en todos ellos, y tres contenedores solo para responder preguntas sobre sus propios documentos. Esa es la forma en que se presentó la generación mejorada con recuperación de información en la mayoría de los tutoriales, y para una amplia gama de casos de uso es posible que ya no sea necesaria.

Las funciones de IA de BigQuery permiten que el LLM se comporte como cualquier otro operador SQL, al igual que JOIN o WHERE. El embedding, la búsqueda vectorial y la generación estructurada tienen lugar donde ya se encuentran los datos, bajo los controles de IAM, registro de auditoría y gestión de costos que ya existen. Tampoco se trata de un entorno cerrado basado en el modelo de un único proveedor; se pueden utilizar Claude, Llama, Mistral y modelos de código abierto de Hugging Face.

Las secciones siguientes construyen el mismo pipeline de dos maneras sobre un conjunto de datos público al que cualquiera puede acceder, el corpus de noticias de la BBC en bigquery-public-data.bbc_news.fulltext. La primera es una versión por lotes de un solo uso que utiliza funciones generalmente disponibles. La segunda es una versión en fase de prueba, donde el corpus se actualiza continuamente a medida que llegan nuevas filas. A continuación se presenta un análisis honesto de los puntos en los que este enfoque falla y por qué Python sigue siendo la mejor opción.

La tecnología que asumen la mayoría de los tutoriales

Una configuración estándar de RAG suele incluir:

  1. Algo para generar embeddings: una API alojada o un endpoint de modelo que se implementa y se mantiene uno mismo
  2. Una base de datos vectorial, lo que añade otro proveedor, otra factura y otro pipeline de sincronización para mantener la consistencia con la fuente de datos original
  3. Un orquestador como LangChain, LlamaIndex o código personalizado para integrar todo
  4. Una API de LLM con sus propios límites de tasa y cuotas
  5. Hosting para todo ello, generalmente unos pocos contenedores que alguien debe empacar y mantener actualizados

Sume las partes: cinco sistemas y al menos tres claves API. Luego está el costo del cual nadie habla, que es el hecho de que sus datos salen del almacén. Cada regla de gobernanza, política de seguridad a nivel de fila y requisito de auditoría integrados en BigQuery deja de aplicarse en el momento en que los datos se exportan a un almacenamiento vectorial externo.

Esa arquitectura existía porque no había alternativa. La premisa ha cambiado.

El LLM como operador SQL

La familia de funciones AI.* en BigQuery ya está disponible para todos. Tres elementos son importantes para RAG:

  • AI.GENERATE_EMBEDDING crea embeddings en masa a partir de cualquier tabla.
  • VECTOR_SEARCH realiza búsquedas semánticas como función integrada de GoogleSQL. Puedes agregar un índice IVF o TreeAH, y TreeAH se basa en el algoritmo ScaNN de Google.
  • AI.GENERATE es una función escalar por fila que puede devolver resultados estructurados a través de un output_schema, y se puede utilizar en cualquier lugar donde se use un valor, incluyendo condiciones SELECT, WHERE e incluso JOIN.

Ninguna clave aparece en tu código. La autenticación se realiza a través de una conexión de BigQuery, una cuenta de servicio gestionada a la que le otorgas un rol una sola vez. Ahora existe una conexión predeterminada, lo que elimina la mayor parte de los pasos de configuración.

No estás atado a Gemini

Una preocupación común es que esto te vincule a Gemini. No es así, aunque a menudo se malentienden los detalles. Las funciones existen en dos tipos.

Funciones de uso general como AI.GENERATE_TEXT, AI.GENERATE_TABLE y AI.GENERATE_EMBEDDING, que incluyen un argumento MODEL, le brindan control total sobre qué modelo se ejecutará. Los modelos remotos compatibles incluyen la familia Gemini, además de Anthropic Claude, Meta Llama y Mistral AI como endpoints gestionados, así como modelos de código abierto de Hugging Face o del Model Garden en la plataforma de agentes de Google (anteriormente Vertex AI). En el caso de los modelos abiertos, CREATE MODEL incluso puede desplegar el modelo automáticamente.

Funciones gestionadas (AI.IF, AI.CLASSIFY, AI.SCORE y AI.GENERATE en formato escalar cuando se proporciona un endpoint) son atajos respaldados por Gemini. BigQuery elige el modelo y se encarga de formular las instrucciones necesarias.

Se indica que el cumplimiento requiere embeddings provenientes de un modelo multilingüe E5 o Qwen autohospedado, siendo Claude quien genera las respuestas. Cada uno comienza con una única instrucción CREATE MODEL. En el caso de los embeddings, ese es el cambio total, ya que nada más en la cadena de procesamiento se modifica. La generación de respuestas tiene una salvedad.

Ruta uno: un pipeline de un solo uso (disponible de forma general)

Este patrón es adecuado para trabajos en lotes y analíticos, donde un corpus se procesa con embeddings una sola vez o según un horario establecido y luego se consultan los resultados.

Paso 0: configuración única

La conexión es el paso que la mayoría de los tutoriales orientados a almacenes de datos omiten. Se trata de la cuenta de servicio gestionada a través de la cual BigQuery accede a los modelos, y se crea una sola vez.

La ruta más rápida, si tienes el rol de administrador de BigQuery, es la conexión predeterminada. Escribe WITH CONNECTION DEFAULT en la instrucción y BigQuery la preparará y otorgará los roles correspondientes al usarla por primera vez.

Si prefieres tener el control de la conexión, que en el segundo método se requiere con el nombre us.my_connection, se necesitan dos comandos: crear una conexión de recurso en la nube y luego asignar a su cuenta de servicio el rol denominado Agent Platform User (roles/aiplatform.user, anteriormente Vertex AI User).

PROJECT_ID=my-poject-id

# 1. Create a Cloud resource connection (its location must match your dataset)
bq mk --connection --location=US --project_id=$PROJECT_ID \
  --connection_type=CLOUD_RESOURCE my_connection

# 2. Grant the connection's service account the Agent Platform User role
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$(bq show --format=prettyjson \
    --connection $PROJECT_ID.US.my_connection | jq -r .cloudResource.serviceAccountId)" \
  --role=roles/aiplatform.user

El lugar donde se asigna el rol depende de cómo se hace referencia al modelo: en el proyecto que posee el modelo cuando se proporciona un nombre, o en el proyecto indicado en la URL del endpoint cuando se proporciona una URL completa. La autenticación termina allí, con un único rol y sin claves.

A continuación viene el modelo de embedding, reutilizando la conexión predeterminada:

CREATE OR REPLACE MODEL `rag_demo_us.emb_model`
  REMOTE WITH CONNECTION DEFAULT
  OPTIONS (ENDPOINT = 'gemini-embedding-001');

Esa declaración representa toda la implementación del servicio de incrustación. No hay contenedor, ningún punto de extremo que escalar ni clave que rotar. Si se implementó un modelo de código abierto, sustituya su URL de punto de extremo.

Paso 1: incrustar el corpus

Una declaración incrusta los artículos de la BBC:

CREATE OR REPLACE TABLE `rag_demo_us.news_emb` AS
SELECT *
FROM AI.GENERATE_EMBEDDING(
  MODEL `rag_demo_us.emb_model`,
  (
    SELECT title, body AS content
    FROM `bigquery-public-data.bbc_news.fulltext`
    WHERE category = 'tech'
  ),
  STRUCT('RETRIEVAL_DOCUMENT' AS task_type)
);

Hay dos detalles que son fáciles de pasar por alto. La consulta de entrada debe incluir una columna llamada content, con alias si es necesario, y agregar una columna title mejora la calidad de los embeddings para RETRIEVAL_DOCUMENT. Además, task_type es importante: utilice RETRIEVAL_DOCUMENT para el corpus y RETRIEVAL_QUERY para la pregunta del usuario, ya que los embeddings asimétricos mejoran notablemente la calidad de la recuperación.

La tabla resultante contiene sus columnas originales y agrega tres columnas generadas. embedding alberga el vector, statistics es un valor JSON con token_count y truncated, y status indica los errores. Tenga en cuenta que la versión más reciente AI.GENERATE_EMBEDDING devuelve embedding, y no ml_generate_embedding_result como ocurría en las guías antiguas de ML.GENERATE_EMBEDDING. Mezclar estos dos formatos es un error típico causado por copiar y pegar incorrectamente.

Primera trampa: fallo parcial silencioso. Un trabajo por lotes puede tener éxito en general mientras que las filas individuales fallan debido a la presión de cuotas o problemas en el servicio. BigQuery informa esto por fila en la columna status, donde una cadena vacía significa éxito; por lo tanto, siempre filtre con WHERE status = ''. Si los lotes paralelos presentan errores por agotamiento de recursos, Google ofrece scripts de reintentos de inferencia remota y un paquete de Dataform que iteran hasta que se procese cada fila.

Segunda trampa: truncamiento silencioso. Las statistics de cada fila muestran su conteo de tokens y una marca de truncamiento. Un artículo de noticias tiene un par de cientos de tokens, pero los documentos más largos que el límite de entrada del modelo se incluyen de forma truncada sin generar ningún error. Para documentos largos, monitoree la marca de truncamiento y divídalos en partes con antelación.

Paso 2: añadir un índice, opcionalmente

Los corpus pequeños pueden omitirlo, ya que VECTOR_SEARCH recurre a una búsqueda bruta exacta. A gran escala, es necesario crear uno:

CREATE OR REPLACE VECTOR INDEX news_idx
ON `rag_demo_us.news_emb`(embedding)
OPTIONS (index_type = 'IVF', distance_type = 'COSINE');

Algunos comportamientos a esperar: el mantenimiento del índice se gestiona en segundo plano por usted; las filas añadidas posteriormente pueden buscarse de inmediato (BigQuery escanea el resto no indexado por fuerza bruta); las tablas de menos de 10 MB no cuentan con índice poblado, y la edición Standard no admite índices.

Paso 3: recuperación y generación en una sola consulta

Incorpore la pregunta, obtenga los cinco artículos principales, base el prompt y genere una respuesta escrita:

SELECT
  AI.GENERATE(
    (
      'Answer using only this context:\n',
      STRING_AGG(base.content, '\n---\n'),
      '\nQuestion: what were the major browser security stories?'
    ),
    endpoint      => 'gemini-2.5-flash',
    output_schema => 'answer STRING, confidence FLOAT64, sources ARRAY<STRING>'
  ).*
FROM VECTOR_SEARCH(
  TABLE `rag_demo_us.news_emb`, 'embedding',
  (
    SELECT embedding
    FROM AI.GENERATE_EMBEDDING(
      MODEL `rag_demo_us.emb_model`,
      (SELECT 'major browser security stories' AS content),
      STRUCT('RETRIEVAL_QUERY' AS task_type)
    )
  ),
  top_k => 5
);

El pipeline principal consta de unas doce líneas, sin LangChain, sin almacén vectorial externo y sin nada que desplegar. Además, output_schema genera columnas tipadas para la respuesta, una puntuación de confianza y las fuentes, en lugar de un bloque JSON que hay que analizar y del cual hay que depender.

No obstante, la limitación de la versión de un solo uso es grave: los datos no permanecen inmóviles. Llegan nuevos artículos, las descripciones cambian y los tickets se actualizan. Cada inserción o edición implica volver a ejecutar la instrucción de incrustación, detectar cambios, gestionar intentos de reintentos y vigilar si la información está desactualizada.

Segundo enfoque: un corpus que se mantiene por sí mismo (vista previa)

La generación autónoma de incrustaciones es la solución. Se declara la incrustación como una columna generada y BigQuery se encarga de mantenerla actualizada:

CREATE TABLE `rag_demo_us.news_live` (
  title STRING,
  body  STRING,
  body_embedding STRUCT<result ARRAY<FLOAT64>, status STRING>
    GENERATED ALWAYS AS (
      AI.EMBED(
        body,
        connection_id => 'us.my_connection',
        endpoint      => 'gemini-embedding-001')
    ) STORED OPTIONS (asynchronous = TRUE)
);

Luego inserte datos desde cualquier lugar:

INSERT INTO `rag_demo_us.news_live` (title, body)
SELECT title, body
FROM `bigquery-public-data.bbc_news.fulltext`
WHERE category = 'tech';

El diseño merece ser leído dos veces. Con GENERATED ALWAYS AS AI.EMBED(...), el embedding se convierte en metadatos de la tabla y no en código del pipeline. Al insertar o actualizar la columna de origen, BigQuery crea o actualiza el vector en segundo plano. Con asynchronous = TRUE, las inserciones no esperan a las llamadas al modelo, y el proceso de embedding se ejecuta en slots en segundo plano, cuyas tareas muestran un prefijo gc_ en el historial de tareas. La columna es de tipo STRUCT<result, status>, por lo que los fallos se muestran por fila y no se ignoran.

En cuanto a la elección del modelo, el ejemplo utiliza gemini-embedding-001 porque estaba ampliamente disponible y mostraba un buen rendimiento en la tabla de clasificación multilingüe MTEB en ese momento. El ejemplo canónico de la documentación emplea text-embedding-005, el cual funciona igual de bien. Hay un aspecto a tener en cuenta: la salida por defecto tiene 3072 dimensiones, frente a las 768 de los modelos text-embedding, lo que aumenta los costos de almacenamiento e índice. Para evitarlo, solicite un tamaño menor mediante model_params en AI.EMBED. El modelo se entrena al estilo Matryoshka, por lo que se puede truncar de manera limpia a 768 o 256.

También es más sencillo realizar consultas. AI.SEARCH determina con qué modelo se creó la tabla e incrusta automáticamente la cadena de búsqueda:

SELECT base.title, base.body, distance
FROM AI.SEARCH(
  TABLE `rag_demo_us.news_live`, 'body',
  'major browser security stories',
  top_k => 5
);

El texto se busca en una columna de texto, y el almacén maneja los vectores en segundo plano. También se admiten índices vectoriales: crea uno en la columna de origen, y el entrenamiento comienza una vez que el 80 % de las filas contengan embeddings.

Para cualquier cosa destinada a producción, esta es la versión que importa. El enfoque de un solo uso es una demostración, mientras que un corpus autogestionado es un diseño que se puede mantener durante años.

Hay una condición importante: esta sección está en versión de previsualización, bajo términos previos a la versión generalizada que pueden cambiar. También hay que tener en cuenta que cada tabla cuenta con una columna de incrustación generada automáticamente, que las inserciones deben realizarse en lotes y no de forma continua mediante operaciones DML frecuentes, ya que las operaciones DML concurrentes pueden retrasar la generación, y que las copias, clones y instantáneas almacenan los datos sin la configuración de generación. Las notas de lanzamiento indican que se puede activar en tablas nuevas o existentes mediante CREATE TABLE o ALTER TABLE. Las notas de lanzamiento de BigQuery son la fuente autorizada sobre el estado de las funcionalidades, así que véalas antes de comenzar a desarrollar.

Qué obtiene sin esfuerzo adicional

Esta es la parte que más debería preocupar a los equipos de datos, pero es la que recibe menos atención.

  • La seguridad a nivel de fila y columna se aplica también a VECTOR_SEARCH. Una fila que el usuario no tiene permiso para ver nunca llega al sistema como contexto RAG. Es difícil lograr esa misma garantía mediante una base de datos vectorial externa sincronizada.
  • IAM es el único modelo de autenticación. No existen claves en las variables de entorno ni procedimientos de rotación para el pipeline.
  • Los registros de auditoría, la atribución de costos y las cuotas provienen todos de la infraestructura que ya gestiona el equipo de la plataforma. Incluso se pueden rastrear los costos por tarea en Cloud Billing mediante la etiqueta bigquery_job_id_prefix.
  • No hay tareas de sincronización que ejecutar. Los vectores se encuentran junto a las filas que describen, por lo que un almacén de vectores no puede desviarse del almacén principal cuando solo existe un almacén. Con los embeddings autónomos, tampoco se necesita un responsable para las tareas de actualización.

La mejor infraestructura es aquella que nunca se implementa.

Dónde falla

  1. La facturación se realiza por tokens, y Gemini 2.5 también cobra por el proceso de razonamiento. Esos modelos cobran por tokens de razonamiento además del resultado generado. En tareas de clasificación, establecer el presupuesto de razonamiento en cero en model_params reduce tanto la latencia como los costos. Si se omite ese paso, los costos por lotes aumentan sin que se note. Utilice AI.COUNT_TOKENS para hacer una estimación primero.
  2. Las consultas complejas pueden proporcionar al modelo una cantidad de filas diferente a la esperada. Las uniones (JOINs) y las órdenes con LIMIT suelen ser las causas. Google sugiere primero escribir la entrada en una tabla y luego ejecutar la inferencia sobre dicha tabla.
  • Las cuotas son reales. El rendimiento de inserción está limitado por las cuotas de tokens por minuto, que por defecto son unos pocos millones. Eso sirve para la mayoría de las cargas de trabajo, pero si se pretende insertar todo un lago de datos durante la noche se necesita preparación, o bien se debe utilizar el rendimiento provisto a través de request_type => 'dedicated'.
  • VECTOR_SEARCH se cobra según los bytes escaneados. La tarificación bajo demanda incluye la tabla base, el índice y la consulta. Coloque las columnas STORING en el índice para no tener que volver a unirse a la tabla base, y filtre temprano con WHERE en esas columnas almacenadas.
  • Las versiones no afectan a tus prompts. Un prompt es simplemente una cadena de texto dentro de SQL, por lo que no existe registro, prueba A/B ni herramienta de evaluación. Los equipos que modifican constantemente los prompts de los asistentes de producción cada semana lo experimentan, y es ahí donde resulta útil una pequeña capa en Python o dbt que almacene los prompts como configuraciones versionadas.
  • Verifica la etiqueta de estado

    El estado de las funcionalidades cambia, así que confírmalo. Tal como se informó inicialmente, AI.GENERATE y AI.GENERATE_TABLE llegaron a estar disponibles para todos a finales de enero de 2026, mientras que los modelos de socios (Claude, Llama, Mistral) se pusieron a disposición general en la primera mitad de 2025. Las funcionalidades en versión de prueba, como los embeddings autónomos, podrían haber cambiado desde entonces, y las notas de lanzamiento son el lugar adecuado para confirmarlo.

    ¿SQL es realmente todo lo que necesitas?

    De esto se derivan tres conclusiones.

    Para RAG por lotes y analítico, merece una consideración seria. La clasificación de documentos, la deduplicación semántica, los pipelines de enriquecimiento y las preguntas y respuestas internas sobre datos regulados favorecen el enfoque nativo del almacén en términos de gobernanza, simplicidad y costo total, además de permitirte elegir el modelo que prefieras.

    Un corpus que cambia constantemente es otro caso, y los embeddings autónomos alteran el equilibrio. La búsqueda nunca fue la parte difícil en RAG en producción; lo complicado era mantener los vectores consistentes con los datos actuales, y una columna generada elimina toda esa categoría de tareas.

    La habilidad radica en saber dónde está el límite. El chat orientado al usuario en menos de un segundo sigue requiriendo una capa de servicio dedicada, el ciclo de vida de las instrucciones sigue necesitando versionado y el costo por token sigue exigiendo monitoreo. Esas restricciones no desaparecieron; simplemente cambiaron de lugar.

    Para más información, la documentación de BigQuery de Google aborda la visión general de la IA generativa y la matriz de soporte de modelos, CREATE MODEL para modelos de socios y abiertos, AI.GENERATE, AI.GENERATE_EMBEDDING, AI.SEARCH, generación autónoma de embeddings, funciones de búsqueda, gestión de índices vectoriales y las notas de lanzamiento.

    Leer más