Como Usar Inteligência Artificial com Spring Boot e Bancos de Dados Vetoriais

Por Gaspar Barancelli Junior em 03 de janeiro de 2025
Imagem ilustrativa sobre o post Como Usar Inteligência Artificial com Spring Boot e Bancos de Dados Vetoriais

Nos últimos anos, a Inteligência Artificial (IA) tem ganhado cada vez mais destaque em diversas áreas de negócio, desde o atendimento ao cliente até a análise de dados. Na prática, o maior desafio costuma ser lidar com grandes volumes de informações, fazer a indexação e, principalmente, resgatar esses dados de maneira assertiva para responder perguntas ou realizar buscas mais complexas.

Neste artigo, veremos como utilizar Spring Boot AI junto a um banco de dados de vetores – no caso, o TypeSense – para indexar conteúdos e permitir pesquisas semânticas eficientes. Vamos explorar:

1) Por que utilizar um banco de dados de vetor?

2) Benefícios de indexar dados em formato vetorial.

3) Exemplo de implementação com Spring Boot AI e TypeSense.

4) Exemplos de chamadas cURL para testes.

5) Link para o repositório no GitHub.

1. Por que utilizar um banco de dados de vetor?

Com a evolução dos modelos de IA, principalmente os modelos de linguagem natural, surgiu a necessidade de lidar com consultas que não são mais apenas uma simples comparação de texto literal (palavra exata), mas também de semelhança semântica.

Imagine que um usuário procure por um trecho de um livro, um conceito ou algo relacionado a um documento. Muitas vezes, ele não saberá a frase exata nem mesmo palavras-chave específicas. Um banco de dados tradicional (relacional ou NoSQL) tem limitações em buscas semânticas, pois trabalha essencialmente com indexações textuais ou chaves primárias.

Já um banco de dados de vetor faz a indexação de conteúdo transformando textos em vetores num espaço matemático de alta dimensão (vector embeddings). Esses vetores representam a “essência” semântica das frases. Então, quando a IA converte a consulta do usuário em um vetor, o banco de dados realiza uma busca por vetores semelhantes (similaridade coseno ou outra métrica). O resultado é uma busca muito mais próxima do contexto e do significado, em vez de só uma comparação direta de strings.

2. Benefícios de indexar dados em formato vetorial

1) Busca Semântica

Permite encontrar resultados relevantes mesmo que as palavras exatas não batam. Por exemplo, consultas do tipo “Quem pode julgar a alma?” podem associar-se a qualquer citação de ‘julgar a alma’, ‘dúvida sobre a aparência da alma’, etc.

2) Alta Precisão em Consultas Complexas

Quando há grandes volumes de dados (livros, artigos, documentos), consultas semânticas evitam a perda de informações relevantes, aumentando a precisão.

3) Melhor Experiência do Usuário

O usuário final não precisa se preocupar com termos específicos ou exatos. Ele pode escrever perguntas em linguagem natural, e o sistema compreende o contexto.

4) Escalabilidade

Bancos de dados de vetor modernos, como o TypeSense, Milvus, Pinecone e outros, foram projetados para lidar com grandes quantidades de vetores, mantendo uma performance de pesquisa rápida.

3. Exemplo de implementação com Spring Boot AI e TypeSense

A seguir, segue um exemplo de como você pode criar uma API que:

1) Receba documentos para adicionar ao banco vetorial.

2) Faça buscas semânticas usando o Spring AI e o TypeSense.

3) Forneça um endpoint de chat que utilize a IA (no caso, via OpenAI) para gerar respostas com base nos documentos armazenados.

Estrutura do Projeto

No pom.xml, destacamos as dependências:

<dependencies>
    <!-- Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI OpenAI -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    </dependency>

    <!-- Spring AI TypeSense Vector Store -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-typesense-store-spring-boot-starter</artifactId>
    </dependency>
</dependencies>

Configurações no application.yaml

spring:
  application:
    name: demo-spring-ai-vector-database
  ai:
    openai:
      api-key: ${API_KEY}
      chat:
        options:
          model: gpt-3.5-turbo
    vectorstore:
      typesense:
        initialize-schema: true
        collection-name: vector_store
        embedding-dimension: 1536
        client:
          protocol: http
          host: localhost
          port: 8108
          api-key: xyz
  • api-key: sua chave de acesso ao OpenAI.

  • embedding-dimension: dimensão do vetor usada pelo modelo para representar o texto.

  • host e port: endereço e porta onde o TypeSense está rodando.

  • client.api-key: chave de API para o TypeSense.

Subindo o container do TypeSense

Para rodar o TypeSense localmente via Docker:

docker run --name demo-vector-database \
        -p 8108:8108 -v/tmp:/data \
        typesense/typesense:27.1 \
          --data-dir /data --api-key=xyz

Com isso, teremos o TypeSense escutando na porta 8108, pronto para receber os dados.

Código Fonte de Exemplo

package com.gasparbarancelli.demo_spring_ai_vector_database;

import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@SpringBootApplication
@RestController
@RequestMapping("/")
public class DemoSpringAiVectorDatabaseApplication {

	private final VectorStore vectorStore;
	private final OpenAiChatModel openAiChatClient;

    public DemoSpringAiVectorDatabaseApplication(VectorStore vectorStore, OpenAiChatModel openAiChatClient) {
        this.vectorStore = vectorStore;
        this.openAiChatClient = openAiChatClient;
    }

    public static void main(String[] args) {
		SpringApplication.run(DemoSpringAiVectorDatabaseApplication.class, args);
	}

	@GetMapping("/add")
	public String addDocuments() {
		var documents = List.of(
				new Document("A felicidade é um bem que se goza às escondidas. Se alguém nos vê, cresce o desejo, mas também cresce a inveja, e o invejoso, na falta de meios para nos destruir a alegria, procura nos mostrar que ela é menor do que pensamos.", Map.of("autor", "Machado de Assis", "titulo", "Memórias Póstumas de Brás Cubas", "anoPublicacao", 1881)),
				new Document("A vida é cheia de obrigações que a gente cumpre por mais vontade que tenha de as infringir. É como andar por um campo de batalha, desviando-se dos tiros para salvar o que se pode, mas sempre ferido no final.", Map.of("autor", "Machado de Assis", "titulo", "Quincas Borba", "anoPublicacao", 1891)),
				new Document("Capitu era dissimulada, capaz de enganar com um sorriso ou com um olhar. Mas quem pode julgar a alma apenas pelas suas aparências? A dúvida é uma sombra que nunca nos abandona completamente.", Map.of("autor", "Machado de Assis", "titulo", "Dom Casmurro", "anoPublicacao", 1899)),
				new Document("Os destinos são como linhas que se cruzam no infinito. Cada um traça seu próprio caminho, mas, às vezes, é o acaso que decide se eles se encontrarão de forma harmoniosa ou conflituosa.", Map.of("autor", "Machado de Assis", "titulo", "Esaú e Jacó", "anoPublicacao", 1904)),
				new Document("A vida, meu caro, não passa de um teatro onde todos representam papéis. Uns são reis, outros palhaços, mas, no final, todos recolhem a máscara e caem no esquecimento do público.", Map.of("autor", "Machado de Assis", "titulo", "Memorial de Aires", "anoPublicacao", 1908)),
				new Document("Há momentos na vida em que a verdade pesa mais que qualquer mentira. O silêncio, por sua vez, é uma espada de dois gumes: pode tanto proteger quanto ferir quem tenta usá-lo.", Map.of("autor", "Machado de Assis", "titulo", "Helena", "anoPublicacao", 1876)),
				new Document("O amor, muitas vezes, não é mais do que uma ilusão que criamos para suavizar a solidão. É uma peça do destino que, mesmo quando nos engana, nos faz acreditar na sua força.", Map.of("autor", "Machado de Assis", "titulo", "Iaiá Garcia", "anoPublicacao", 1878)),
				new Document("O tempo é o senhor de todas as coisas. Ele apaga memórias, cura feridas, mas também arrasta consigo as esperanças, deixando apenas a saudade como prova de que um dia vivemos.", Map.of("autor", "Machado de Assis", "titulo", "Ressurreição", "anoPublicacao", 1872)),
				new Document("O casamento, dizia ele, é como uma dança a dois. Quando os passos se sincronizam, há harmonia; quando não, cada um pisa no pé do outro, e a música se torna um suplício.", Map.of("autor", "Machado de Assis", "titulo", "A Mão e a Luva", "anoPublicacao", 1874)),
				new Document("Simão Bacamarte dizia que a ciência não tem pátria nem fronteiras. No entanto, quando começou a trancar seus concidadãos no hospício, provou que a lógica nem sempre caminha de mãos dadas com a humanidade.", Map.of("autor", "Machado de Assis", "titulo", "O Alienista", "anoPublicacao", 1882)),
				new Document("Iracema era como a natureza selvagem e pura de sua terra natal, cheia de encantos e mistérios. Cada gesto seu parecia entoar uma melodia que falava ao coração de Martim.", Map.of("autor", "José de Alencar", "titulo", "Iracema", "anoPublicacao", 1865)),
				new Document("A voz do sertanejo é o canto da liberdade. É o hino do campo, onde o homem é senhor apenas de si mesmo e vive em harmonia com a natureza ao seu redor.", Map.of("autor", "José de Alencar", "titulo", "O Sertanejo", "anoPublicacao", 1875)),
				new Document("A cidade parecia adormecida sob o calor do meio-dia, mas o coração de Lucíola estava em chamas. Ela buscava redenção em cada palavra de Paulo, mas sabia que o amor muitas vezes era uma estrada sem volta.", Map.of("autor", "José de Alencar", "titulo", "Lucíola", "anoPublicacao", 1862)),
				new Document("O Guarani não era apenas uma história de amor entre Peri e Ceci, mas também um retrato de duas culturas tentando coexistir em um mundo em constante transformação.", Map.of("autor", "José de Alencar", "titulo", "O Guarani", "anoPublicacao", 1857)),
				new Document("O tempo passa como as águas de um rio, e o amor que nasce no coração de Lúcia não é diferente. Ele cresce como uma planta, encontrando solo fértil mesmo em meio às dificuldades.", Map.of("autor", "José de Alencar", "titulo", "Diva", "anoPublicacao", 1864))
		);

		vectorStore.add(documents);
		return "Documentos adicionados";
	}

	@GetMapping("chat")
	public String chat(@RequestParam("message") String message) {
		Prompt prompt = getPrompt(message);
		ChatResponse chatResponse = openAiChatClient.call(prompt);
		return chatResponse.getResults().stream()
				.map(generation -> generation.getOutput().getContent())
				.collect(Collectors.joining("/n"));
	}

	private Prompt getPrompt(String message) {
		String systemMessage = """
            Você é um bibliotecário e deve usar informações de documentos selecionados para fornecer uma resposta assertiva aos usuários.

			Quando você encontrar uma resposta válida nos documentos com base num possivel trecho do livro que o usuário digitar, deve retornar as seguintes informações: autor, título, ano de publicação e uma frase do livro.

			As informações devem ser retornadas no formato JSON, conforme o exemplo abaixo:

            \\{
            	"autor": string,
            	"titulo": string,
            	"anoPublicacao": number,
            	"frase": string
            \\}

            Regras:

            Se houver um documento relevante:
            Retorne os dados completos no formato especificado.

            Caso nenhum documento forneça informações suficientes ou nenhum documento seja relevante:
            Retorne uma mensagem indicando que você não sabe, como no exemplo abaixo:

            \\{
			 	"mensagem": string
			\\}

            Documentos:
            {documents}
        """;

		var documents = searchDocuments(message);

		var systemTemplate = new SystemPromptTemplate(systemMessage).createMessage(Map.of("documents", documents));

		var testUserMessage = new UserMessage("quem pode julgar a alma");
		var assistantMessage = new AssistantMessage("""
                {
					"autor": "Machado de Assis",
					"titulo": "Dom Casmurro",
					"anoPublicacao": 1899,
					"frase": "Capitu era dissimulada, capaz de enganar com um sorriso ou com um olhar. Mas quem pode julgar a alma apenas pelas suas aparências? A dúvida é uma sombra que nunca nos abandona completamente."
				 }
        """);


		UserMessage userMessage = new UserMessage(message);
		return new Prompt(List.of(systemTemplate, testUserMessage, assistantMessage, userMessage));
	}

	public String searchDocuments(@RequestParam("query") String query) {
		return vectorStore.similaritySearch(SearchRequest.defaults().withQuery(query).withTopK(5))
				.stream()
				.map(document -> {
					var metadata = document.getMetadata();

					return String.format(
							"Autor: %s, título: %s, ano publicação: %d, frase: %s",
							metadata.get("autor"),
							metadata.get("titulo"),
							(Integer) metadata.get("anoPublicacao"),
							document.getContent());
				})
				.collect(Collectors.joining(System.lineSeparator()));
	}

}

Como funciona:

  1. Endpoint /add: Adiciona uma lista de documentos ao seu VectorStore. Cada documento contém:

    • O conteúdo (no exemplo, um trecho de livro).

    • Metadados como autor, título, ano de publicação.

  2. Endpoint /chat?message=…​:

    • Recebe a entrada do usuário.

    • Monta um Prompt que inclui:

    • Uma System Message informando que o sistema é um “bibliotecário”.

    • Exemplos de mensagens anteriores (opcional).

    • A mensagem do usuário.

    • Chama a API do OpenAI via openAiChatClient.call(prompt), usando como contexto os resultados de searchDocuments().

  3. Método searchDocuments(query):

    • Realiza uma busca de similaridade no VectorStore, usando a consulta do usuário convertida para um embedding e pesquisando vetores mais próximos.

    • Retorna as informações relevantes dos documentos encontrados.

4. Exemplos de requisições cURL

Podemos testar o endpoint /chat com algumas consultas para verificar como a aplicação responde. Abaixo, seguem exemplos de chamadas e seus retornos esperados:

Exemplo para adicionar documentos

curl -X GET "http://localhost:8080/add"

Resposta esperada:

Documentos adicionados

Consulta retornando um documento específico

curl -X GET "http://localhost:8080/chat?message=uns%20sao%20reis%20outros%20palhacos"

Resposta esperada:

{
  "autor": "Machado de Assis",
  "titulo": "Memorial de Aires",
  "anoPublicacao": 1908,
  "frase": "A vida, meu caro, não passa de um teatro onde todos representam papéis. Uns são reis, outros palhaços, mas, no final, todos recolhem a máscara e caem no esquecimento do público."
}

Outra consulta encontrada em outro documento

curl -X GET "http://localhost:8080/chat?message=capitu%20era%20dissimulada"

Resposta esperada:

{
  "autor": "Machado de Assis",
  "titulo": "Dom Casmurro",
  "anoPublicacao": 1899,
  "frase": "Capitu era dissimulada, capaz de enganar com um sorriso ou com um olhar. Mas quem pode julgar a alma apenas pelas suas aparências? A dúvida é uma sombra que nunca nos abandona completamente."
}

Consulta que não encontra nenhum documento relevante

curl -X GET "http://localhost:8080/chat?message=a%20saudade%20e%20a%20memoria%20do%20coracao"

Resposta esperada:

{
  "mensagem": "Não sei"
}

Você pode clonar o seguinte repositório git contendo o código fonte desta aplicação de exemplo: https://github.com/gasparbarancelli/demo-spring-ai-vector-database

Conclusão

Indexar dados em um banco de vetores faz toda a diferença quando falamos de buscas semânticas e consultas em linguagem natural. Ferramentas como o TypeSense permitem armazenar, pesquisar e gerenciar vetores de maneira eficiente, simplificando a integração com soluções de IA.

Ao usar o Spring AI com TypeSense, conseguimos:

  • Criar endpoints de indexação de documentos facilmente.

  • Realizar buscas por similaridade para encontrar trechos de texto relevantes em meio a um grande volume de informações.

  • Integrar com modelos de linguagem (OpenAI) para fornecer respostas enriquecidas e contextualizadas.

Se você tem um projeto que requer consultas mais avançadas do que as buscas convencionais, vale a pena experimentar um banco de dados vetorial. O ganho de relevância e a experiência do usuário tendem a melhorar muito, especialmente em projetos de chatbot, recomendação, análise de sentimentos e sistemas de QA (Questions & Answers).

// Compartilhe esse Post

💫
🔥 NOVO APP

Domine o Inglês em 30 dias!

Inteligência Artificial + Repetição Espaçada • Método cientificamente comprovado

✅ Grátis para começar 🚀 Resultados rápidos
×