1. Visión general
Con las bases de datos tradicionales, normalmente dependemos de la coincidencia exacta de palabras clave o de coincidencias de patrones básicos 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.
Vector stores abordan esta limitación al almacenar los datos como vectores numéricos que capturan su significado. Palabras similares terminan cerca una de la otra, lo que permite la búsqueda semántica, donde los resultados relevantes se devuelven incluso si no contienen las palabras clave exactas utilizadas en la consulta.
En este tutorial, exploraremos cómo integrar ChromaDB, un vector store de código abierto, con Spring AI.
Para convertir nuestros datos de texto en vectores que ChromaDB pueda almacenar y buscar, necesitaremos un modelo de embeddings. Usaremos Ollama para ejecutar un modelo de embeddings localmente.
2. Dependencias
Empecemos añadiendo las dependencias necesarias al archivo pom.xml de nuestro proyecto:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-chroma-store-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
La dependencia del iniciador de ChromaDB nos permite establecer una conexión con nuestro vector store ChromaDB e interactuar con él.
Además, importamos la dependencia del iniciador de Ollama, que utilizaremos para ejecutar nuestro modelo de embeddings.
Dado que la versión actual, 1.0.0-M6, es una versión de hito, también necesitaremos agregar el repositorio de Spring Milestones a nuestro pom.xml:
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
Este repositorio es donde se publican las versiones de hito, a diferencia del repositorio Maven Central estándar.
Como estamos utilizando múltiples iniciadores de Spring AI en nuestro proyecto, también incluyamos 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-M6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Con esta adición, podemos eliminar la etiqueta version de ambas dependencias del iniciador.
El BOM elimina el riesgo de conflictos de versiones y garantiza que nuestras dependencias de Spring AI sean compatibles entre sí.
3. Configuración del entorno de prueba local con Testcontainers
Para facilitar el desarrollo local y las pruebas, utilizaremos Testcontainers para configurar nuestro vector store ChromaDB y el servicio Ollama.
El prerequisito para ejecutar los servicios requeridos a través de Testcontainers es una instancia activa de Docker.
3.1. Dependencias de prueba
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>chromadb</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>ollama</artifactId>
<scope>test</scope>
</dependency>
Estas dependencias nos proporcionan las clases necesarias para crear instancias de Docker efímeras para ambos servicios externos.
3.2. Definiendo beans de Testcontainers
A continuación, creemos una clase @TestConfiguration que defina nuestros beans de Testcontainers:
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
@ServiceConnection
public ChromaDBContainer chromaDB() {
return new ChromaDBContainer("chromadb/chroma:0.5.20");
}
@Bean
@ServiceConnection
public OllamaContainer ollama() {
return new OllamaContainer("ollama/ollama:0.4.5");
}
}
Especificamos las versiones estables más recientes para nuestros contenedores.
También anotamos nuestros métodos de bean con @ServiceConnection. Esto registra dinámicamente todas las propiedades necesarias para configurar una conexión con ambos servicios externos.
Incluso cuando no se utilice el soporte de Testcontainers, Spring AI se conecta automáticamente a ChromaDB y Ollama cuando se ejecuta localmente en sus puertos predeterminados de 8000 y 11434, respectivamente.
Sin embargo, en producción, podemos anular los detalles de conexión usando las propiedades correspondientes de Spring AI:
spring:
ai:
vectorstore:
chroma:
client:
host: ${CHROMADB_HOST}
port: ${CHROMADB_PORT}
ollama:
base-url: ${OLLAMA_BASE_URL}
Una vez que los detalles de conexión estén configurados correctamente, Spring AI crea automáticamente beans de tipo VectorStore y EmbeddingModel para nosotros, lo que nos permite interactuar con nuestro vector store y modelo de embeddings, respectivamente. Veremos cómo usar estos beans más adelante en el tutorial.
Aunque @ServiceConnection define automáticamente los detalles de conexión necesarios, todavía necesitaremos configurar algunas propiedades adicionales en nuestro archivo application.yml:
spring:
ai:
vectorstore:
chroma:
initialize-schema: true
ollama:
embedding:
options:
model: nomic-embed-text
init:
chat:
include: false
pull-model-strategy: WHEN_MISSING
Aquí, habilitamos la inicialización del esquema para ChromaDB. Luego, configuramos nomic-embed-text como nuestro modelo de embeddings e indicamos a Ollama que descargue el modelo si no está presente en nuestro sistema.
Alternativamente, podemos usar un modelo de embeddings diferente de Ollama o un modelo de Hugging Face según sea necesario.
3.3. Uso de Testcontainers durante el desarrollo
Si bien Testcontainers se utiliza principalmente para pruebas de integración, también podemos usarlo durante nuestro desarrollo local.
Para lograr esto, crearemos una clase principal separada en nuestro directorio src/test/java:
class TestApplication {
public static void main(String[] args) {
SpringApplication.from(Application::main)
.with(TestcontainersConfiguration.class)
.run(args);
}
}
Creamos una clase TestApplication y, dentro de su método main, iniciamos nuestra clase principal Application con nuestra clase TestcontainersConfiguration.
Esta configuración nos ayuda a configurar y administrar nuestros servicios externos localmente. Podemos ejecutar nuestra aplicación Spring Boot y que se conecte a nuestros servicios externos, los cuales se inician mediante Testcontainers.
4. Población de ChromaDB al iniciar la aplicación
Ahora que tenemos nuestro entorno local configurado, vamos a poblar nuestro vector store ChromaDB con algunos datos de muestra durante el inicio de la aplicación.
4.1. Recuperación de registros de Poetry desde PoetryDB
Para nuestra demostración, usaremos la API de PoetryDB para obtener poemas.
Creemos una clase de utilidad PoetryFetcher para esto:
class PoetryFetcher {
private static final String BASE_URL = "https://poetrydb.org/author/";
private static final String DEFAULT_AUTHOR_NAME = "Shakespeare";
public static List<Poem> fetch() {
return fetch(DEFAULT_AUTHOR_NAME);
}
public static List<Poem> fetch(String authorName) {
return RestClient
.create()
.get()
.uri(URI.create(BASE_URL + authorName))
.retrieve()
.body(new ParameterizedTypeReference<>() {});
}
}
record Poem(String title, List<String> lines) {}
Usamos RestClient para invocar la API de PoetryDB con el authorName especificado. Para deserializar la respuesta de la API en una lista de registros Poem, utilizamos ParameterizedTypeReference sin especificar explícitamente el tipo genérico de respuesta, y Java inferirá el tipo por nosotros.
También sobrecargamos nuestro método fetch() sin ningún parámetro para recuperar poemas del autor Shakespeare. Utilizaremos este método en nuestra siguiente sección.
4.2. Almacenamiento de Documents en el vector store de ChromaDB
Ahora, para poblar nuestro vector store ChromaDB con poemas 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;
// constructor estándar
@Override
public void run(ApplicationArguments args) {
List<Document> documents = PoetryFetcher
.fetch()
.stream()
.map(poem -> {
Map<String, Object> metadata = Map.of("title", poem.title());
String content = String.join("\n", poem.lines());
return new Document(content, metadata);
})
.toList();
vectorStore.add(documents);
}
}
En nuestra VectorStoreInitializer, inyectamos una instancia de VectorStore.
Dentro del método run(), utilizamos nuestra clase de utilidad PoetryFetcher para recuperar una lista de registros Poem. Luego, convertimos cada poem en un Document con las lines como content y el title como metadata.
Finalmente, almacenamos todos los documents en nuestro vector store. 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 nuestro vector store. No necesitamos convertirlo explícitamente utilizando el bean EmbeddingModel.
Por defecto, Spring AI usa SpringAiCollection como el nombre de la colección para almacenar datos en nuestro vector store, pero podemos sobrescribirlo utilizando la propiedad spring.ai.vectorstore.chroma.collection-name.
5. Prueba de búsqueda semántica
Con nuestro vector store ChromaDB poblado, validemos nuestra funcionalidad de búsqueda semántica:
private static final int MAX_RESULTS = 3;
@ParameterizedTest
@ValueSource(strings = {"Love and Romance", "Time and Mortality", "Jealousy and Betrayal"})
void whenSearchingShakespeareTheme_thenRelevantPoemsReturned(String theme) {
SearchRequest searchRequest = SearchRequest
.builder()
.query(theme)
.topK(MAX_RESULTS)
.build();
List<Document> documents = vectorStore.similaritySearch(searchRequest);
assertThat(documents)
.hasSizeLessThanOrEqualTo(MAX_RESULTS)
.allSatisfy(document -> {
String title = String.valueOf(document.getMetadata().get("title"));
assertThat(title)
.isNotBlank();
});
}
Aquí, pasamos algunos temas comunes de Shakespeare a nuestro método de prueba usando @ValueSource. Luego creamos un objeto SearchRequest con el theme como query y MAX_RESULTS como el número de resultados deseados.
A continuación, llamamos al método similaritySearch() del bean vectorStore, con nuestro searchRequest. Similar al método add() del VectorStore, Spring AI convierte nuestro query en su representación vectorial antes de consultar nuestro vector store.
Los documents devueltos contendrán poemas que están semánticamente relacionados con el theme dado, incluso si no contienen la palabra clave exacta.
6. Conclusión
En este artículo, exploramos cómo integrar el vector store ChromaDB con Spring AI.
Con Testcontainers, iniciamos contenedores Docker para nuestros servicios ChromaDB y Ollama, creando un entorno de prueba local.
Revisamos cómo poblar nuestro vector store con poemas de la API PoetryDB durante el inicio de la aplicación. Luego, utilizamos temas poéticos comunes para validar nuestra funcionalidad de búsqueda semántica.
El código que respalda este artículo está disponible en GitHub. Una vez que hayas iniciado sesión como Miembro Pro de Baeldung, comienza a aprender y codificar 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.