Como ya hemos comentado, los LLMs son modelos preentrenados hasta una cierta fecha y con datos públicos (se supone que no tienen acceso a datos de una empresa o de nuestros dispositivos, por ejemplo). También es popular la importancia de los tokens en aspectos como la facturación o la context-window (no podemos enviar todo lo que queramos en el input ni el output, aunque hay LLMs que tienen context-window de millones de tokens como Gemini).

El patrón RAG se enfoca en ayudarnos con estas limitaciones de los LLMs de cara a tener unos mejores resultados, con menos alucinaciones y reduciendo el coste por uso.

Antes de entrar en detalle, puedes echarle un vistazo al resto de contenidos de la serie de Spring AI en los siguientes enlaces por si te has perdido alguno de los artículos anteriores:

Algunas de las bases de RAG ya las hemos visto en el post de los Advisors, Tool-Calling o el tema de aumentar el contexto, pero en este post nos centraremos más en la esencia del propio RAG y en qué tecnologías y abstracciones está apoyado.

RAG

Retrieval Augmented Generation (RAG) es una técnica muy empleada para abordar las limitaciones que existen en los modelos lingüísticos con contenidos grandes, precisión y conocimiento del contexto. El enfoque se basa en un procesamiento batch en donde primero se leen datos desestructurados desde los documentos (u otros orígenes de datos), se transforman y luego se escriben en una Vector Database (a grandes rasgos es un proceso ETL).

Una de las transformaciones en este flujo es la división del documento original (u origen de datos) en piezas más pequeñas, existiendo dos pasos clave:

Imagen donde se ve la estructura RAG con Document ingestion - ETL offline y runtime

La siguiente fase en RAG ya sería el procesamiento del input del usuario, empleando la búsqueda por similitud para enriquecer el contexto con documentos similares a enviar al modelo.

Spring AI soporta RAG proporcionando una arquitectura modular que permite crear flujos RAG personalizados o usar los creados a través del uso del API de Advisor.

String rag() {
    return ChatClient.builder(chatModel)
        .build().prompt()
        .advisors(new QuestionAnswerAdvisor(vectorStore))
        .user("que me puedes decir de los aranceles de EEUU en el año 2025")
        .call()
        .content();
}

En el ejemplo, QuestionAnswerAdvisor realizará una búsqueda por similitud sobre los documentos en la base de datos, pudiendo filtrar los documentos por los que se busque con la clase SearchRequest (también en tiempo de ejecución).

Módulos

Spring AI implementa una arquitectura modular RAG basada en el documento Modular RAG: Transforming RAG Systems into LEGO-like Reconfigurable Frameworks. Esto todavía está en fase experimental, por lo que es susceptible a cambios. Los módulos que se pueden encontrar son:

Query query = new Query("Hvad er Danmarks hovedstad?");

QueryTransformer queryTransformer = TranslationQueryTransformer.builder()
        .chatClientBuilder(chatClientBuilder)
        .targetLanguage("english")
        .build();

Query transformedQuery = queryTransformer.transform(query);
Map<Query, List<List<Document>>> documentsForQuery = ...
DocumentJoiner documentJoiner = new ConcatenationDocumentJoiner();
List<Document> documents = documentJoiner.join(documentsForQuery);

Embeddings

Los embeddings son representaciones numéricas de texto, imágenes o vídeos que capturan la relación entre los datos de entrada. Funcionan transformando texto, imágenes o vídeos en arrays de números llamados vectores, que están diseñados para capturar el significado de estos recursos. Esto se hace calculando la distancia numérica entre dos vectores de, por ejemplo, dos textos, pudiendo así determinar su similitud.

La interfaz EmbeddingModel está diseñada para la integración con los modelos, cuya función principal es convertir los recursos en vectores numéricos que son usados para análisis semántico y clasificación de textos, entre otros. La interfaz se centra principalmente en:

API Overview

La interfaz EmbeddingModel extiende de la interfaz Model, así como EmbeddingRequest y EmbeddingResponse de sus correspondientes ModelRequest y ModelResponse. En la siguiente imagen se puede observar las relaciones entre el Embedding API, la Model API y los Embedding Models:

relaciones entre el Embedding API, la Model API y los Embedding Models

Ya más en profundidad:

public interface EmbeddingModel extends Model<EmbeddingRequest, EmbeddingResponse> {

    @Override
    EmbeddingResponse call(EmbeddingRequest request);
    float[] embed(Document document);
    default float[] embed(String text) {...}
    default List<float[]> embed(List<String> texts) {...}
    default EmbeddingResponse embedForResponse(List<String> texts) {...}
    default int dimensions() {...}
}
public class EmbeddingRequest implements ModelRequest<List<String>> {
    private final List<String> inputs;
    private final EmbeddingOptions options;
    ...
}
public class EmbeddingResponse implements ModelResponse<Embedding> {
    private List<Embedding> embeddings;
    private EmbeddingResponseMetadata metadata = new EmbeddingResponseMetadata();
    ...
}
public class Embedding implements ModelResult<float[]> {
    private float[] embedding;
    private Integer index;
    private EmbeddingResultMetadata metadata;
    ...
}

Implementaciones disponibles

En esta página puedes encontrar las implementaciones disponibles. Siguiendo las premisas de posts anteriores nos centraremos en la implementación de Ollama.

Propiedades

De cara a la autoconfiguración, se proporcionan una serie de propiedades muy similares a las propiedades existentes en el modelo de chat, habilitando la autoconfiguración con la propiedad:

spring.ai.model.embedding

Implementación

A continuación mostramos distintos ejemplos de cómo hacer uso del OllamaEmbeddingModel y EmbeddingModel:

@Autowired
private EmbeddingModel autoEmbeddingModel;
...
@GetMapping("/auto")
public EmbeddingResponse embed() {
    return autoEmbeddingModel.embedForResponse(List.of("Tendencias tecnologicas 2025", "Tendencia tecnologica IA", "Tendencia tecnologica accesibilidad"));
}
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-ollama</artifactId>
</dependency>

Para, posteriormente, crear la instancia correspondiente:

private OllamaApi ollamaApi;
private OllamaEmbeddingModel embeddingModel;

public EmbeddingController() {
    ollamaApi = new OllamaApi();
    embeddingModel = 
OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(OllamaOptions.builder().model(OllamaModel.LLAMA3_2_1B).build()).build();
}

@GetMapping("/plain")
public EmbeddingResponse embedd() {
    return embeddingModel.call(new EmbeddingRequest(List.of("Tendencias tecnologicas 2025", "Tendencia tecnologica IA", "Tendencia tecnologica accesibilidad"), 
        OllamaOptions.builder()
            .model(OllamaModel.LLAMA3_2_1B)
            .truncate(false)
            .build()));
}

Vector Databases

Son un tipo de bases de datos especializadas que, en vez de realizar coincidencias exactas, se apoyan en las búsquedas por similitud, es decir, cuando se indica como entrada un vector, el resultado son vectores “similares”. En primer lugar, se cargan los datos en la base de datos vectorial. Posteriormente, cuando se envía una petición al modelo, primero se recuperan unos vectores similares que luego son usados como contexto para la pregunta del usuario y enviados también al modelo.

API Overview

Para trabajar con las bases de datos vectoriales se hace uso de la interfaz Vector Store:

public interface VectorStore extends DocumentWriter {
    default String getName() {...}
    void add(List<Document> documents);
    void delete(List<String> idList);
    void delete(Filter.Expression filterExpression);
    default void delete(String filterExpression) {...};
    List<Document> similaritySearch(String query);
    List<Document> similaritySearch(SearchRequest request);
    default <T> Optional<T> getNativeClient() {...}
}

Además de la clase SearchRequest:

public class SearchRequest {
    public static final double SIMILARITY_THRESHOLD_ACCEPT_ALL = 0.0;
    public static final int DEFAULT_TOP_K = 4;
    private String query = "";
    private int topK = DEFAULT_TOP_K;
    private double similarityThreshold = SIMILARITY_THRESHOLD_ACCEPT_ALL;
    @Nullable
    private Filter.Expression filterExpression;
    public static Builder from(SearchRequest originalSearchRequest) {...}
    public static class Builder {...}
    public String getQuery() {...}
    public int getTopK() {...}
    public double getSimilarityThreshold() {...}
    public Filter.Expression getFilterExpression() {...}
}

Para insertar datos en la base de datos vectorial es necesario encapsular dichos datos en un objeto Document. Al insertarse en la bbdd, el texto se transforma en un array de números conocido como embedding vector (la función de la bbdd vectorial es realizar búsquedas por similitud, no para generar los embeddings en sí). Los métodos similaritySearch permiten obtener documentos similares a una petición, pudiendo ajustarse con los siguientes parámetros:

FilterExpressionBuilder builder = new FilterExpressionBuilder();
Expression expression = builder.eq("tendencia", "cloud").build();

Schema Initialization

Algunas bases de datos necesitan que se inicialice el esquema antes de usarlo. Con Spring Boot se puede configurar la propiedad …initialize-schema a true, aunque lo adecuado es contrastar esta información con cada una de las implementaciones.

Batching Strategy

Es común cuando se trabaja con este tipo de bbdd tener que embeber muchos documentos.

Aunque la primera idea que se nos ocurre es intentar embeber todos estos documentos al mismo tiempo, puede llevar a ciertos problemas. Esto se debe sobre todo a los límites de tokens en los modelos (window size), lo que provocaría errores o embedings truncados.

Para eso precisamente se usa la estrategia batch, en donde se dividen conjuntos grandes de documentos en conjuntos más pequeños que cuadren con la window size. Esto no solo soluciona el problema de los límites de tokens, sino que además puede mejorar el rendimiento y los límites de peticiones existentes en las distintas APIs.

Spring ofrece esta funcionalidad a través de la interfaz BatchingStrategy:

public interface BatchingStrategy {
    List<List<Document>> batch(List<Document> documents);
}

La implementación por defecto es TokenCountBatchingStrategy que basa la división según el número de tokens existentes en cada batch, asegurándose que no se supere el límite de input de tokens. Los puntos importantes de esta implementación serían:

La estrategia estima el número de tokens por documento, los agrupa en batch sin exceder el límite de tokens, lanzando una excepción si alguno de los documentos lo excediera. También se puede personalizar la estrategia creando una nueva instancia a través de una clase @Configuration:

@Configuration
public class EmbeddingConfig {
    @Bean
    public BatchingStrategy customTokenCountBatchingStrategy() {
        return new TokenCountBatchingStrategy(
            EncodingType.CL100K_BASE,  // Specify the encoding type
            8000,                      // Set the maximum input token count
            0.1                        // Set the reserve percentage
        );
    }
}

Una vez definido este bean, será usado automáticamente por las implementaciones del EmbeddingModel en vez de la estrategia por defecto. Adicionalmente, se permite incluir las implementaciones propias de TokenCountEstimator (clase que calcula los tokens del documento), además de parámetros para formatear contenido y metadatos o incluso nuestra propia implementación custom. Y, como en otras ocasiones, además se permite crear la implementación completamente personalizada:

@Configuration
public class EmbeddingConfig {
    @Bean
    public BatchingStrategy customBatchingStrategy() {
        return new CustomBatchingStrategy();
    }
}

Implementaciones disponibles

En esta página se pueden encontrar las implementaciones disponibles. En este caso, nos centraremos en la implementación PGvector para bases de datos PostgreSQL, que no es más que una extensión open source para PostgreSQL que permite guardar y buscar sobre embeddings.

Prerrequisitos

En primer lugar, se necesita acceso a PostgreSQL con las extensiones vector, hstore y uuid-ossp. Al iniciarse PgVectorStore intentará instalar las extensiones necesarias sobre la bbdd y crear la tabla vector_store con un índice. Se puede hacer lo mismo de forma manual con:

CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS hstore;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE IF NOT EXISTS vector_store (
    id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
    content text,
    metadata json,
    embedding vector(1536) // 1536 is the default embedding dimension
);

CREATE INDEX ON vector_store USING HNSW (embedding vector_cosine_ops);

Configuración

Empezamos por incluir la dependencia correspondiente:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>

La implementación de vector store puede inicializar el schema automáticamente, pero se debe indicar el initializeSchema en el constructor correspondiente o mediante la propiedad …​initialize-schema=true.

Evidentemente, también será necesario un EmbeddingModel, debiendo incluir su correspondiente dependencia en el proyecto. Y, como en muchas ocasiones, también habrá que indicar los valores para la conexión a través de properties:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: postgres
  ai:
    vectorstore:
      pgvector:
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1024
        max-document-batch-size: 10000
        initialize-schema: true

Además, existen otras properties para mayor personalización de la Vector Store. Recordemos que tal y como nos tiene acostumbrados Spring, también se puede crear una configuración manual.

Ejecución local

Se puede ejecutar una instancia de PGVector a través del siguiente comando Docker:

docker run -it --rm --name postgres -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres pgvector/pgvector:pg16

Y conectándose a la instancia con el comando:

psql -U postgres -h localhost -p 5432

Ejemplos de uso

Como es de esperar, se debe seleccionar un modelo embedding en conjunción con el modelo general empleado. Un ejemplo de carga de Documents en la Vector Store (esta operación de carga realmente se ejecutaría de forma batch) sería:

Document document1 = new Document("Tendencia tecnologica 2025: La voz y los vídeos en la IA", Map.of("tendencia", "ia"));
...
List<Document> documents = Arrays.asList(document1, document2, document3, document4, document5, document6, document7, document8, document9, document10);
vectorStore.add(documents);

Más tarde, cuando un usuario envíe una pregunta, se realizará una búsqueda por similitud para obtener documentos similares que se pasarán como contexto del prompt.

List<Document> similarDocuments = vectorStore.similaritySearch("Dime las tendencias tecnologicas actuales");

Eliminar documentos

Se proporcionan múltiples métodos para eliminar documentos:

Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.EQ, new Filter.Key("tendencia"), new Filter.Value("dragdrop"));
vectorStore.delete(filterExpression);

FilterExpressionBuilder builder = new FilterExpressionBuilder();
Expression expression = builder.eq("tendencia", "cloud").build();

try {
    vectorStore.delete(filterExpression);
    vectorStore.delete(expression);
} catch (Exception e) {
    log.error("Invalid filter expression", e);
}

Nota: según la documentación, se recomienda envolver estas llamadas de delete en bloques try-catch.

Además, también hay que tener en cuenta las siguientes consideraciones en cuanto a rendimiento:

Demo

Para ver en acción los componentes que hemos visto en este post, creamos una app. Una vez iniciamos la aplicación con la propiedad …initialize-schema: true se puede ver cómo se ha creado la tabla vector_store con un cliente de base de datos:

Tabla creada en vector_store con las columnas id, content, metadata, embedding

Para comprobar la funcionalidad contamos con los siguientes endpoints:

"metadata":  "model": "1lama3.2:1b", "usage": { "empty": true "result": { "index": 0, "metadata" : { "output": 0.008033557, 0. 05002698, 0.008954761, 0.013158767, 0.012868241, -0.023812277, 0.018066008, 0.05619622, -0.010666855, 0.017404212, -0.028421931,
Imagen donde se muestra la tabla anteriormente creada con los campos id, content, metadata y embedding rellenos en una lista de 10 items
"id": "bb767982-5564-4791-9649-b508700{3367", "text": "Tendencia tecnologica 2025: Transformación impulsada por evidencias", "media": null, "metadata": { "distance": 0.20401171, "tendencia": "transformacion" "score": 0.7959882915019989 "id": "048ad20b-d24c-4545-82cb-94bf9c67dcac" "text": "Tendencia tecnologica 2025: Liderazgo Sistémico Humanista", "media": null, "metadata": { "distance": 0.22337313, "tendencia": "humanista" "score": 0.7766268700361252
"id": "bb767982-5564-4791-9649-b508700f3367" , "text": "Tendencia tecnologica 2025: Transformación impulsada por evidencias" "media": null, "metadata": { "distance": 0.20401171, "tendencia": "transformacion" "score": 0.7959882915019989
Recuperamos la tabla anteriormente creada con los 10 items y vemos que ahora tiene 8 items. Se han borrado 2
o.s.ai.reader.pdt.PagePdfDocumentReader : Processing PDF page: 1 o.s.ai.reader.pdf.PagePdfDocumentReader : Processing : Processing PDF page: 2 o.s.ai.reader.pdf.PagePdfDocumentReader : Processing PDF page: 3 o.s.ai.reader.pdf.PagePdfDocumentReader : Processing PDF page: 4 o.s.ai.reader.pdf.PagePdfDocumentReader : Processing PDF page: 5 o.s.ai.reader.pdf.PagePdfDocumentReader : Processing PDF page: 6 o.s.ai.reader.pdf.PagePdfDocumentReader : Processing PDF page: 7 o.s.ai.reader.pdf.PagePdfDocumentReader : Processing 7 pages o.s.a.transformer.splitter.TextSplitter : Splitting up document into 2 chunks. o.s.a.transformer.splitter.TextSplitter : Splitting up document into 2 chunks. o.s.a.transformer.splitter.TextSplitter : Splitting up document into 2 chunks. o.s.a.transformer.splitter.TextSplitter : Splitting up document into 2 chunks. c.e.s.d.s.application.IngestionService : Data loaded in vectorStore
Lo siento, pero no puedo proporcionar información actualizada sobre los aranceles de EE. UU. hacia países o productos específicos hasta el año 2023. Los aranceles son un tema político complejo y su regulación puede cambiar rápidamente debido a cambios en las políticas de comercio internacional, decisiones gubernamentales y la evolución del mercado. Sin embargo, puedo ofrecerte una idea general sobre cómo funcionan los aranceles y qué factores pueden influir en su implementación: 1. Exención de importaciones: Los productos que no se consideran "destruyentes" o "detrimentiosales" para la seguridad nacional del país pueden ser exentos de aranceles. 2. Aranceles por contenido: Dependiendo del contenido del producto, puede haber aranceles aplicados. Por ejemplo, si un producto contiene ciertos artículos prohibidos o restricciones, puede ser objeto de aranceles. 3. Aranceles por protección agricola: Los productos de protección agropecuaria, como certificados de origen y semillas genéticamente modificadas, pueden tener aranceles para proteger intereses comerciales nacionales. 4. Aranceles por salud y seguridad alimentaria: Aranceles pueden aplicarse a productos que violen regulaciones sobre impurezas, contaminación o seguridad alimentaria. Para obtener información actualizada y específica sobre los aranceles de EE. UU. hacia un país o producto en 2025, te recomendaria consultar las siguientes fuentes:
Estimado usuario, En cuanto a la información sobre los presupuestos iniciales de las Asesorías para la Evaluación y el Monitoreo (AA. PP.) en 2025, según el informe "Informe sobre los presupuestos iniciales de las AA. PP. 2025" proporcionado por AIReF, se puede resumir a continuación: * Los presupuestos iniciales para 2025 son de $500 millones. * Se espera que el crecimiento del PIB en Estados Unidos reduzca en torno al medio punto porcentual en 2025, respecto a un escenario sin incertidumbre. * En 2024, las exportaciones españolas a Estados Unidos representaron 15.6% del peso total de las exportaciones españolas y 11.6% del peso total de las importaciones españolas. En cuanto a los principales destinos de las exportaciones y orígenes de las importaciones de España en 2024, se puede ver que: * Las exportaciones españolas representan 15.6% del PIB de Francia (1,1%), 11.6% del PIB de Alemania (3.7%) y 8,9% del PIB de Italia (3.43%). * La importación principal de España es el paío sudamericano. Si deseas obtener más información sobre los presupuestos iniciales o cualquier otro tema relacionado con las Asesorías para la...

En este enlace se puede descargar el código de la aplicación de ejemplo.

Conclusión

En esta ocasión hemos visto en qué consiste y cómo se implementa el patrón RAG en Spring AI, además de las funcionalidades que lo soporta (embeddings y las bbdd vectoriales), así como los ejemplos de las implementaciones correspondientes (Ollama y PGVector).

En el siguiente capítulo abordaremos la parte offline del patrón RAG (fase ETL) además de centrarnos en el auge de MCP y cómo lo resuelve Spring AI. ¡Te leo en comentarios!

Referencias

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete