1. Visión general
Con las bases de datos tradicionales, normalmente confiamos en coincidencias exactas de palabras clave o coincidencia básica de patrones para implementar nuestra funcionalidad de búsqueda. Si bien es suficiente para aplicaciones simples, este enfoque no logra comprender completamente el significado y el contexto detrás de las consultas en lenguaje natural.
Los almacenes vectoriales abordan esta limitación almacenando los datos como vectores numéricos que capturan su significado. Palabras similares se agrupan juntas, lo que permite realizar búsquedas por similitud, donde la base de datos devuelve resultados relevantes incluso si no contienen las palabras clave exactas utilizadas en la consulta.
Oracle Database 23ai integra esta capacidad de almacenamiento vectorial en su ecosistema existente, lo que nos permite crear aplicaciones de IA sin necesitar un almacén vectorial separado. Usando la misma base de datos, podemos crear soluciones que utilizan tanto la gestión de datos estructurados tradicionales como la búsqueda por similitud vectorial.
En este tutorial, exploraremos la integración de la base de datos vectorial de Oracle con Spring AI. Implementaremos una búsqueda por similitud nativa para encontrar contenido semánticamente relacionado. Luego, aprovecharemos esta capacidad para implementar un chatbot de Generación Aumentada por Recuperación (RAG).
2. Configuración del proyecto
Antes de sumergirnos en la implementación, necesitaremos incluir las dependencias necesarias y configurar correctamente nuestra aplicación.
2.1. Dependencias
Comencemos añadiendo las dependencias necesarias al archivo pom.xml de nuestro proyecto:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-oracle</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.0.0</version>
</dependency>
La dependencia starter de Oracle vector store nos permite establecer una conexión con la base de datos vectorial de Oracle e interactuar con ella. Además, importamos la dependencia de asesores de vector store para nuestra implementación de RAG.
Finalmente, importamos la dependencia starter de OpenAI de Spring AI, que utilizaremos para interactuar con los modelos de completado de chat y de incrustación.
Dado que estamos utilizando múltiples starters de Spring AI en nuestro proyecto, también incluiremos el Bill of Materials (BOM) de Spring AI en nuestro pom.xml:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Con esta adición, ahora podemos eliminar la etiqueta version de nuestras dependencias starter. El BOM elimina el riesgo de conflictos de versiones y garantiza que las dependencias de Spring AI sean compatibles entre sí.
2.2. Configuración de modelos de IA y propiedades del vector store
Para convertir nuestros datos de texto en vectores que la base de datos vectorial de Oracle pueda almacenar y buscar, necesitaremos un modelo de incrustación. Además, para nuestro chatbot RAG también necesitaremos un modelo de completado de chat.
Para nuestra demostración, utilizaremos los modelos proporcionados por OpenAI. Configuraremos la clave API de OpenAI y los modelos en nuestro archivo application.yaml:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
embedding:
options:
model: text-embedding-3-large
chat:
options:
model: gpt-4o
Utilizamos el marcador de posición de propiedad ${} para cargar el valor de nuestra clave API desde una variable de entorno.
Además, especificamos text-embedding-3-large y gpt-4o como nuestros modelos de incrustación y de completado de chat, respectivamente. Al configurar estas propiedades, Spring AI crea automáticamente un bean del tipo ChatModel, que usaremos más adelante en el tutorial.
Alternativamente, podemos usar diferentes modelos, ya que el modelo de IA o proveedor específico es irrelevante para esta demostración.
A continuación, para almacenar y buscar datos en nuestra base de datos vectorial, primero debemos inicializar su esquema:
spring:
ai:
vectorstore:
oracle:
initialize-schema: true
Aquí, configuramos spring.ai.vectorstore.oracle.initialize-schema a true.
Esto indica a Spring AI que cree automáticamente el esquema de almacén vectorial predeterminado al iniciar la aplicación, lo que resulta conveniente para el desarrollo y las pruebas locales. Sin embargo, para aplicaciones de producción, deberíamos definir el esquema manualmente utilizando una herramienta de migración de bases de datos, como Flyway.
3. Poblar la base de datos vectorial de Oracle
Con nuestras configuraciones en su lugar, configuraremos un flujo de trabajo para poblar nuestra base de datos vectorial de Oracle con algunos datos de muestra durante el inicio de la aplicación.
3.1. Recuperar registros de Quote desde una API externa
Para nuestra demostración, utilizaremos la API de citas de Breaking Bad para obtener citas.
Creemos una clase utilitaria QuoteFetcher para esto:
class QuoteFetcher {
private static final String BASE_URL = "https://api.breakingbadquotes.xyz/v1/quotes/";
private static final int DEFAULT_COUNT = 150;
static List<Quote> fetch() {
return fetch(DEFAULT_COUNT);
}
static List<Quote> fetch(int count) {
return RestClient
.create()
.get()
.uri(URI.create(BASE_URL + count))
.retrieve()
.body(new ParameterizedTypeReference<>() {});
}
}
record Quote(String quote, String author) {
}
Utilizando RestClient, invocamos la API externa con la cantidad predeterminada de 150 y usamos ParameterizedTypeReference para deserializar la respuesta de la API a una lista de registros Quote records.
3.2. Almacenar Documents en la base de datos vectorial
Ahora, para poblar nuestra base de datos vectorial de Oracle con citas durante el inicio de la aplicación, crearemos una clase VectorStoreInitializer que implemente la interfaz ApplicationRunner:
@Component
class VectorStoreInitializer implements ApplicationRunner {
private final VectorStore vectorStore;
// standard constructor
@Override
public void run(ApplicationArguments args) {
List<Document> documents = QuoteFetcher
.fetch()
.stream()
.map(quote -> {
Map<String, Object> metadata = Map.of("author", quote.author());
return new Document(quote.quote(), metadata);
})
.toList();
vectorStore.add(documents);
}
}
En nuestra clase VectorStoreInitializer, inyectamos una instancia de VectorStore, que Spring AI crea automáticamente para nosotros.
Dentro del método run(), utilizamos nuestra clase utilitaria QuoteFetcher para recuperar una lista de registros Quote. Luego, mapeamos cada quote en un Document y configuramos el campo author como metadata.
Finalmente, almacenamos todos los documents en nuestra base de datos. Cuando invocamos el método add(), Spring AI convierte automáticamente nuestro contenido de texto plano en una representación vectorial antes de almacenarlo en la base de datos.
4. Configuración del entorno de pruebas local con Testcontainers
Para facilitar el desarrollo y las pruebas locales, utilizaremos Testcontainers para configurar la base de datos vectorial de Oracle, cuya condición previa es una instancia activa de Docker.
Primero, agreguemos las dependencias de prueba necesarias a nuestro pom.xml:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>oracle-free</artifactId>
<scope>test</scope>
</dependency>
Importamos la dependencia de Testcontainers de Spring AI para Spring Boot y el módulo Oracle Database de Testcontainers.
Estas dependencias proporcionan las clases necesarias para lanzar una instancia efímera de Docker para la base de datos vectorial de Oracle.
A continuación, creemos una clase @TestConfiguration para definir nuestro bean Testcontainers:
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
@ServiceConnection
OracleContainer oracleContainer() {
return new OracleContainer("gvenzl/oracle-free:23-slim");
}
}
Especificamos la última versión estable slim de la imagen de la base de datos Oracle al crear el bean OracleContainer.
Además, anotamos nuestro método bean con @ServiceConnection. Esto registra dinámicamente todas las propiedades de la fuente de datos necesarias para configurar una conexión con el contenedor Docker.
Ahora, **podemos usar esta configuración en nuestras pruebas de integración anotando nuestras clases de prueba con la anotación @Import(TestcontainersConfiguration.class).
5. Realizando búsquedas por similitud
Ahora que hemos configurado nuestro entorno de pruebas local y poblado nuestra base de datos vectorial de Oracle con citas de Breaking Bad, exploremos cómo realizar búsquedas por similitud.
5.1. Búsqueda por similitud básica
Comencemos realizando operaciones básicas de búsqueda por similitud para encontrar citas que coincidan con diversos temas de Breaking Bad:
private static final int MAX_RESULTS = 5;
@Autowired
private VectorStore vectorStore;
@ParameterizedTest
@ValueSource(strings = { "Sarcasm", "Regret", "Violence and Threats", "Greed, Power, and Money" })
void whenSearchingBreakingBadTheme_thenRelevantQuotesReturned(String theme) {
SearchRequest searchRequest = SearchRequest
.builder()
.query(theme)
.topK(MAX_RESULTS)
.build();
List<Document> documents = vectorStore.similaritySearch(searchRequest);
assertThat(documents)
.hasSizeGreaterThan(0)
.hasSizeLessThanOrEqualTo(MAX_RESULTS)
.allSatisfy(document -> {
assertThat(document.getText())
.isNotBlank();
assertThat(String.valueOf(document.getMetadata().get("author")))
.isNotBlank();
});
}
Aquí, pasamos los principales temas de la serie Breaking Bad a nuestro método de prueba usando @ValueSource. Luego, creamos un objeto SearchRequest con el theme como consulta. Además, limitamos los resultados a las cinco citas más similares al pasar MAX_RESULTS al método topK().
A continuación, llamamos al método similaritySearch() de nuestro bean vectorStore, con nuestro searchRequest. Al igual que el método add() del VectorStore, Spring AI convierte nuestra consulta a su representación vectorial antes de consultar la base de datos.
Los documentos devueltos contendrán citas que están semánticamente relacionadas con el tema dado, incluso si no incluyen la palabra clave exacta.
5.2. Filtrado usando metadatos
Además de realizar búsquedas por similitud básicas, la base de datos vectorial de Oracle también admite el filtrado de resultados de búsqueda en función del metadato guardado. Esto es útil cuando necesitamos reducir nuestra búsqueda y realizar búsquedas semánticas dentro de un subconjunto de datos.
Busquemos nuevamente citas relacionadas con un tema dado, pero filtrémoslas por un autor específico:
@ParameterizedTest
@CsvSource({
"Walter White, Pride",
"Walter White, Control",
"Jesse Pinkman, Abuse and foul language",
"Mike Ehrmantraut, Wisdom",
"Saul Goodman, Law"
})
void whenSearchingCharacterTheme_thenRelevantQuotesReturned(String author, String theme) {
SearchRequest searchRequest = SearchRequest
.builder()
.query(theme)
.topK(MAX_RESULTS)
.filterExpression(String.format("author == '%s'", author))
.build();
List<Document> documents = vectorStore.similaritySearch(searchRequest);
assertThat(documents)
.hasSizeGreaterThan(0)
.hasSizeLessThanOrEqualTo(MAX_RESULTS)
.allSatisfy(document -> {
assertThat(document.getText())
.isNotBlank();
assertThat(String.valueOf(document.getMetadata().get("author")))
.contains(author);
});
}
Aquí, usamos la anotación @CsvSource para encontrar citas utilizando varias combinaciones de personaje-tema.
Construimos nuestro SearchRequest como antes, pero esta vez, utilizamos el método filterExpression() para restringir los resultados a citas de un autor específico.
6. Construcción de un chatbot RAG
Si bien la búsqueda por similitud nativa es poderosa por sí sola, podemos construir sobre esta capacidad para crear un chatbot RAG inteligente y contextualmente consciente.
6.1. Definición de una plantilla de prompt
Para guiar mejor el comportamiento del LLM, definiremos una plantilla de prompt personalizada. Creamos un nuevo archivo prompt-template.st en el directorio src/main/resources:
You are a chatbot built for analyzing quotes from the 'Breaking Bad' television series.
Given the quotes in the CONTEXT section, answer the query in the USER_QUESTION section.
The response should follow the guidelines listed in the GUIDELINES section.
CONTEXT:
<question_answer_context>
USER_QUESTION:
<query>
GUIDELINES:
- Base your answer solely on the information found in the provided quotes.
- Provide concise, direct answers without mentioning "based on the context" or similar phrases.
- When referencing specific quotes, mention the character who said them.
- If the question cannot be answered using the context, respond with "The provided quotes do not contain information to answer this question."
- If the question is unrelated to the Breaking Bad show or the quotes provided, respond with "This question is outside the scope of the available Breaking Bad quotes."
Aquí definimos claramente la personalidad del chatbot y le proporcionamos un conjunto de directrices a seguir.
En nuestra plantilla, utilizamos dos marcadores de posición encerrados entre corchetes angulares. Spring AI reemplazará automáticamente los marcadores question_answer_context y query con el contexto recuperado de la base de datos vectorial y la pregunta del usuario, respectivamente.
6.2. Configuración de un bean ChatClient
Luego, definimos un bean del tipo ChatClient, que actúa como punto de entrada principal para interactuar con el modelo de completado de chat configurado:
private static final int MAX_RESULTS = 10;
@Bean
PromptTemplate promptTemplate(
@Value("classpath:system-prompt.st") Resource promptTemplate) {
String template = promptTemplate.getContentAsString(StandardCharsets.UTF_8);
return PromptTemplate
.builder()
.renderer(StTemplateRenderer
.builder()
.startDelimiterToken('<')
.endDelimiterToken('>')
.build())
.template(template)
.build();
}
@Bean
ChatClient chatClient(
ChatModel chatModel,
VectorStore vectorStore,
PromptTemplate promptTemplate) {
return ChatClient
.builder(chatModel)
.defaultAdvisors(
QuestionAnswerAdvisor
.builder(vectorStore)
.promptTemplate(promptTemplate)
.searchRequest(SearchRequest
.builder()
.topK(MAX_RESULTS)
.build())
.build()
)
.build();
}
Aquí, primero recuperamos el contenido de nuestra plantilla de prompt usando la anotación @Value y la usamos para definir un bean PromptTemplate. También lo configuramos para usar los corchetes angulares como delimitadores.
Luego, utilizamos el bean PromptTemplate, junto con los beans ChatModel y VectorStore, para definir nuestro bean ChatClient. Usamos el método defaultAdvisors() para registrar un QuestionAnswerAdvisor, que es el componente que implementa el patrón RAG.
Además, dentro del asesor, configuramos un SearchRequest para recuperar las 10 citas más relevantes. Spring AI las inyectará en la plantilla de prompt antes de realizar la llamada al LLM.
6.3. Realización de la operación RAG
Ahora, con el bean ChatClient configurado, veamos cómo podemos interactuar con él para hacer preguntas en lenguaje natural:
@Autowired
private ChatClient chatClient;
@ParameterizedTest
@ValueSource(strings = {
"How does the show portray the mentor-student dynamic?",
"Which characters in the show portray insecurity through their quotes?",
"Does the show contain quotes with mature themes inappropriate for young viewers?"
})
void whenQuestionsRelatedToBreakingBadAsked_thenRelevantAnswerReturned(String userQuery) {
String response = chatClient
.prompt(userQuery)
.call()
.content();
assertThat(response)
.isNotBlank();
.doesNotContain(OUT_OF_SCOPE_MESSAGE, NO_INFORMATION_MESSAGE);
}
Aquí, cuando pasamos la userQuery al método prompt(), nuestro QuestionAnswerAdvisor configurado realiza el flujo de trabajo RAG en segundo plano. El asesor consulta la base de datos vectorial de Oracle por citas relacionadas con la pregunta del usuario, las inyecta en la plantilla de prompt y envía el prompt combinado al LLM configurado para obtener una respuesta.
Verificamos que la respuesta no esté vacía y no contenga los mensajes de respaldo que definimos en nuestra plantilla.
7. Conclusión
En este artículo, exploramos cómo integrar la base de datos vectorial de Oracle con Spring AI.
Recorrimos las configuraciones necesarias e implementamos dos capacidades clave del almacén vectorial: búsqueda por similitud y RAG. Usando Testcontainers, configuramos la base de datos vectorial de Oracle, creando un entorno de pruebas local.
Primero, obtuvimos citas de la API de citas de Breaking Bad para poblar nuestro almacén vectorial durante el inicio de la aplicación. Luego, implementamos una búsqueda por similitud sobre los datos almacenados para recuperar citas que coincidan con los temas comunes de la serie.
Finalmente, implementamos un chatbot RAG que utiliza las citas recuperadas de la búsqueda por similitud como contexto para responder a las preguntas de los usuarios.
El código que respalda este artículo está disponible en GitHub. Una vez que estés conectado como un Miembro Baeldung Pro, comienza a aprender y programar en el proyecto.
Newsletter Semanal de Java
Cada viernes recibe lo más nuevo del ecosistema Java: frameworks, herramientas y mejores prácticas.
Sin spam. Cancela cuando quieras.