Desbloqueando a pesquisa semântica: crie um mecanismo de pesquisa de filmes baseado em Java com o Atlas Vector Search e o Spring Boot
Avalie esse Tutorial
No mundo da tecnologia em rápida evolução, a busca por resultados de pesquisa mais relevantes, personalizados e intuitivos levou ao aumento da popularidade da pesquisa semântica.
O Vector Search do MongoDB permite que você pesquise seus dados relacionados semanticamente, tornando possível pesquisar seus dados por significado, não apenas por correspondência de palavras-chave.
Neste tutorial, analisaremos como podemos criar um aplicativo Spring Boot que possa executar uma pesquisa semântica em uma collection de filmes por suas descrições de trama.
Antes de começar, você precisará de algumas coisas.
Visite o dashboard doMongoDB Atlas e configure seu cluster. Para aproveitar o operador
$vectorSearch
em um pipeline de agregação, você precisa executar o MongoDB Atlas 6.0.11 ou superior.A seleção de sua versão do MongoDB Atlas está disponível na parte inferior da tela ao configurar seu cluster em "Configurações adicionais".
Ao configurar sua implantação, você será solicitado a configurar um usuário de banco de dados e regras para sua conexão de rede.
Para este projeto, usaremos os dados de amostra fornecidos pelo MongoDB. Quando você fizer login pela primeira vez no painel, verá uma opção para carregar dados de exemplo em seu banco de dados.
Se você procurar no banco de dados
sample_mflix
, verá uma collection chamada embedded_movies
. Esses documentos contêm um campo chamado plot_embeddings
que contém uma matriz de números de ponto flutuante (nossos vetores). Esses números são sem sentido para nós quando vistos diretamente com o olho humano, mas nos permitirão realizar nossa busca semântica por filmes com base em seu enredo. Se você quiser incorporar seus próprios dados, poderá usar os métodos deste tutorial para fazer isso ou pode conferir o tutorial que mostra como configurar um trigger em seu banco de dados para incorporar automaticamente seus dados.Para usar o operador
$vectorSearch
em nossos dados, precisamos configurar um índice de Atlas Search apropriado. Selecione a aba "Atlas Search" no seu cluster e clique em "Criar Índice do Atlas Search".Queremos escolher a "Opção JSON Editor" e clicar em "Avançar".
Nesta página, vamos selecionar nosso banco de dados de destino,
sample_mflix
, e embedded_movies
collection, para este tutorial.O nome não é muito importante, mas nomeie o índice -
PlotVectorSearch
, por exemplo - e copie o seguinte JSON.1 { 2 "fields": [{ 3 "type": "vector", 4 "path": "plot_embedding", 5 "numDimensions": 1536, 6 "similarity": "euclidean" 7 }] 8 }
Os campos especificam o nome do campo de incorporação em nossos documentos,
plot_embedding
, as dimensões do modelo usado para incorporar, 1536
, e a função de similaridade a ser usada para encontrar os K-vizinhos mais próximos, dotProduct
. É muito importante que as dimensões no índice correspondam às do modelo usado para incorporação. Esses dados foram incorporados usando o mesmo modelo que usaremos, mas outros modelos estão disponíveis e podem usar dimensões diferentes.Confira nossa documentação do Vector Atlas Search para obter mais informações sobre essas definições de configuração.
Para configurar nosso projeto, vamos usar o Spring Initializr. Isso gerará o arquivopom.xml, que conterá as dependências do nosso projeto.
Para este projeto, você deseja selecionar as opções na captura de tela abaixo e criar um JAR:
- Projeto: Maven
- Linguagem: Java
- Dependências: Web reativa do Spring e MongoDB reativo dos dados do Spring
- Gerar: JAR
Abra o projeto Maven no IDE de sua escolha e vamos escrever código!
Antes de fazer qualquer coisa, abra seu arquivo pom.xml. Na seção de propriedades, você precisará adicionar o seguinte:
1 <mongodb.version>4.11.0</mongodb.version>
Isso forçará sua API do Spring Boot a usar o 4.11. Versão 0 dos drivers do MongoDB Java. Sinta-se livre para usar uma versão mais atualizada para usar alguns dos recursos mais atualizados, como o método
vectorSearch()
. Você também notará que, em todo este aplicativo, usamos os Reactive Streams do MongoDB Java. Isso ocorre porque estamos criando uma API assíncrona. Operações de AI, como gerar incorporações, podem ser intensivas em computação e consumir muito tempo. Uma API assíncrona permite que essas tarefas sejam processadas em segundo plano, liberando o sistema para lidar com outras solicitações ou operações simultaneamente. Agora, vamos codificar!Para representar nosso documento em Java, usaremos objetos Java antigos simples(POJOs). Os dados que vamos manipular são os documentos dos dados de exemplo que você acabou de carregar em seu cluster. Para cada documento e subdocumento, precisamos de um POJO. Os documentos do MongoDB já têm muitas semelhanças com POJOs e são simples de configurar usando o driver MongoDB.
No documento principal, temos três subdocumentos:
Imdb
, Tomatoes
e Viewer
. Assim, precisaremos de quatro POJOs para nosso documentoMovie
.Primeiro precisamos criar um pacote chamado
com.example.mdbvectorsearch.model
e adicionar nossa classe Movie.java
.Usamos o
@BsonProperty("_id")
para atribuir nosso campo_id
no JSON para ser mapeado para o nosso campoId
no Java, de modo a não violar as convenções de nomenclatura do Java.1 public class Movie { 2 3 4 private ObjectId Id; 5 private String title; 6 private int year; 7 private int runtime; 8 private Date released; 9 private String poster; 10 private String plot; 11 private String fullplot; 12 private String lastupdated; 13 private String type; 14 private List<String> directors; 15 private Imdb imdb; 16 private List<String> cast; 17 private List<String> countries; 18 private List<String> genres; 19 private Tomatoes tomatoes; 20 private int num_mflix_comments; 21 private String plot_embeddings; 22 23 // Getters and setters for Movie fields 24 25 }
Adicione outra classe chamada
Imdb
.1 public static class Imdb { 2 3 private double rating; 4 private int votes; 5 private int id; 6 7 // Getters and setters for Imdb fields 8 9 }
Ainda outro chamado
Tomatoes
.1 public static class Tomatoes { 2 3 private Viewer viewer; 4 private Date lastUpdated; 5 6 // Getters and setters for Tomatoes fields 7 8 }
E, finalmente,
Viewer
.1 public static class Viewer { 2 3 private double rating; 4 private int numReviews; 5 6 // Getters and setters for Viewer fields 7 8 }
Dica: Para criar os getters e setters, muitos IDEs têm atalhos.
Em seu arquivo principal, configure um pacote
com.example.mdbvectorsearch.config
e adicione uma classe, MongodbConfig.java
. É aqui que nos conectaremos ao nosso banco de dados e criaremos e configuraremos nosso cliente. Se você está usado para usar o Spring Data MongoDB, muito disso geralmente é ofuscado. Estamos fazendo isso dessa forma para aproveitar alguns dos recursos mais recentes do driver Java do MongoDB para suportar vetores.A partir da interface do MongoDB Atlas, obteremos nossa connection string e a adicionaremos ao nosso arquivo
application.properties
. Também especificaremos o nome do nosso banco de dados aqui.1 mongodb.uri=mongodb+srv://<username>:<password>@<cluster>.mongodb.net/ 2 mongodb.database=sample_mflix
Agora, em sua classe
MongodbConfig
, importe esses valores e denote isso como uma classe de configuração com a anotação @Configuration
.1 2 public class MongodbConfig { 3 4 5 private String MONGODB_URI; 6 7 8 private String MONGODB_DATABASE;
Em seguida, precisamos criar um cliente e configurá-lo para lidar com a tradução de e para BSON para nossos POJOs. Aqui, configuramos um
CodecRegistry
para lidar com essas conversões e usamos um codec padrão, pois eles são capazes de lidar com os principais tipos de dados Java. Em seguida, envolvemos estes em um MongoClientSettings
e criamos nosso MongoClient
.1 2 public MongoClient mongoClient() { 3 CodecRegistry pojoCodecRegistry = CodecRegistries.fromRegistries( 4 MongoClientSettings.getDefaultCodecRegistry(), 5 CodecRegistries.fromProviders( 6 PojoCodecProvider.builder().automatic(true).build() 7 ) 8 ); 9 10 MongoClientSettings settings = MongoClientSettings.builder() 11 .applyConnectionString(new ConnectionString(MONGODB_URI)) 12 .codecRegistry(pojoCodecRegistry) 13 .build(); 14 15 return MongoClients.create(settings); 16 }
Nossa última etapa será então obter nosso banco de dados, e terminamos esta classe.
1 2 public MongoDatabase mongoDatabase(MongoClient mongoClient) { 3 return mongoClient.getDatabase(MONGODB_DATABASE); 4 } 5 }
Vamos enviar a solicitação fornecida do usuário para a API OpenAI para ser incorporada. Uma incorporação é uma série (vetor) de números de ponto flutuante. A distância entre dois vetores mede seu relacionamento. Pequenas distâncias sugerem alto parentesco e grandes distâncias sugerem baixo parentesco.
Isso transformará nosso prompt de linguagem natural, como
"Toys that come to life when no one is looking"
, em uma grande array de números de ponto flutuante que se parecerão com esta [-0.012670076, -0.008900887, ..., 0.0060262447, -0.031987168]
.Para fazer isso, precisamos criar alguns arquivos. Todo o nosso código para interagir com o OpenAI estará contido em nossa classe
OpenAIService.java
e Go para com.example.mdbvectorsearch.service
. O @Service
no topo de nossa classe determina ao Spring Boot que ela pertence a essa camada de serviço e contém lógica de negócios.1 2 public class OpenAIService { 3 4 private static final String OPENAI_API_URL = "https://api.openai.com"; 5 6 7 8 private String OPENAI_API_KEY; 9 10 private WebClient webClient; 11 12 13 void init() { 14 this.webClient = WebClient.builder() 15 .clientConnector(new ReactorClientHttpConnector()) 16 .baseUrl(OPENAI_API_URL) 17 .defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) 18 .defaultHeader("Authorization", "Bearer " + OPENAI_API_KEY) 19 .build(); 20 } 21 22 23 public Mono<List<Double>> createEmbedding(String text) { 24 Map<String, Object> body = Map.of( 25 "model", "text-embedding-ada-002", 26 "input", text 27 ); 28 29 return webClient.post() 30 .uri("/v1/embeddings") 31 .bodyValue(body) 32 .retrieve() 33 .bodyToMono(EmbeddingResponse.class) 34 .map(EmbeddingResponse::getEmbedding); 35 } 36 }
Usamos o Spring WebClient para fazer as chamadas para a API OpenAI. Em seguida, criamos as incorporações. Para fazer isso, passamos em nosso texto e especificamos nosso modelo de incorporação (por exemplo,
text-embedding-ada-002
). Você pode ler mais sobre as opções de parâmetro da API OpenAI em seus Docs.Para passar e receber os dados da API Open AI, precisamos especificar nossos modelos para os dados que estão sendo recebidos. Vamos adicionar dois modelos ao nosso pacote
com.example.mdbvectorsearch.model
, EmbeddingData.java
e EmbeddingResponse.java
.1 public class EmbeddingData { 2 private List<Double> embedding; 3 4 public List<Double> getEmbedding() { 5 return embedding; 6 } 7 8 public void setEmbedding(List<Double> embedding) { 9 this.embedding = embedding; 10 } 11 }
1 public class EmbeddingResponse { 2 private List<EmbeddingData> data; 3 4 public List<Double> getEmbedding() { 5 return data.get(0).getEmbedding(); 6 } 7 8 public List<EmbeddingData> getData() { 9 return data; 10 } 11 12 public void setData(List<EmbeddingData> data) { 13 this.data = data; 14 } 15 }
Nós temos nosso banco de dados. Podemos incorporar nossos dados. Estamos prontos para enviar e receber nossos documentos de cinema. Como realmente realizamos nossa semântica Atlas Search?
A camada de acesso a dados da implementação de nossa API ocorre no repositório. Crie um pacote
com.example.mdbvectorsearch.repository
e adicione a interface MovieRepository.java
.1 public interface MovieRepository { 2 Flux<Movie> findMoviesByVector(List<Double> embedding); 3 }
Agora, implementamos a lógica do nosso método
findMoviesByVector
na implementação desta interface. Adicione uma classe MovieRepositoryImpl.java
ao pacote. Esse método implementa a lógica de dados de nosso aplicativo e faz a incorporação do texto inserido pelo usuário, incorporado usando a API OpenAI, em seguida, usa o estágio de agregação $vectorSearch
em relação à nossa coleçãoembedded_movies
, usando o índice que configuramos anteriormente.1 2 public class MovieRepositoryImpl implements MovieRepository { 3 4 private final MongoDatabase mongoDatabase; 5 6 public MovieRepositoryImpl(MongoDatabase mongoDatabase) { 7 this.mongoDatabase = mongoDatabase; 8 } 9 10 private MongoCollection<Movie> getMovieCollection() { 11 return mongoDatabase.getCollection("embedded_movies", Movie.class); 12 } 13 14 15 public Flux<Movie> findMoviesByVector(List<Double> embedding) { 16 String indexName = "PlotVectorSearch"; 17 int numCandidates = 100; 18 int limit = 5; 19 20 List<Bson> pipeline = asList( 21 vectorSearch( 22 fieldPath("plot_embedding"), 23 embedding, 24 indexName, 25 numCandidates, 26 limit)); 27 28 return Flux.from(getMovieCollection().aggregate(pipeline, Movie.class)); 29 } 30 }
Para a lógica de negócios do nosso aplicativo, precisamos criar uma classe de serviço. Crie uma classe chamada
MovieService.java
em nosso pacoteservice
.1 2 public class MovieService { 3 4 private final MovieRepository movieRepository; 5 private final OpenAIService embedder; 6 7 8 public MovieService(MovieRepository movieRepository, OpenAIService embedder) { 9 this.movieRepository = movieRepository; 10 this.embedder = embedder; 11 } 12 13 public Mono<List<Movie>> getMoviesSemanticSearch(String plotDescription) { 14 return embedder.createEmbedding(plotDescription) 15 .flatMapMany(movieRepository::findMoviesByVector) 16 .collectList(); 17 } 18 }
O método
getMoviesSemanticSearch
pegará a descrição do gráfico em linguagem natural do usuário, incorporará-a usando a API OpenAI, realizará uma pesquisa vetorial em nossa collectionembedded_movies
e retornará os cinco resultados mais semelhantes.Esse serviço pegará o texto inserido pelo usuário, incorporará-o usando a API OpenAI e, em seguida, usará o estágio de agregação
$vectorSearch
em relação à nossa coleçãoembedded_movies
, usando o índice que definimos anteriormente.Isso retorna um
Mono
envolvendo nossa lista de Movie
objetos. Tudo o que resta agora é passar alguns dados e chamar nossa função.Temos a lógica em nosso aplicativo. Agora, vamos transformá-lo em uma API! Primeiro, precisamos configurar nosso controlador. Isso nos permitirá receber a entrada do usuário para o nosso aplicativo. Vamos configurar um endpoint para receber a descrição do enredo do usuário e retornar os resultados da pesquisa semântica. Crie um pacote
com.example.mdbvectorsearch.service
e adicione a classeMovieController.java
.1 2 public class MovieController { 3 4 private final MovieService movieService; 5 6 7 public MovieController(MovieService movieService) { 8 this.movieService = movieService; 9 } 10 11 12 public Mono<List<Movie>> performSemanticSearch( String plotDescription) { 13 return movieService.getMoviesSemanticSearch(plotDescription); 14 } 15 }
Definimos um endpoint
/movies/semantic-search
que lida com solicitações de obtenção, captura plotDescription
como um parâmetro de query e delega a operação Atlas Search para MovieService
.Você pode usar sua ferramenta favorita para testar os pontos de conexão da API, mas só vamos enviar um comando cURL.
1 curl -X GET "http://localhost:8080/movies/semantic-search?plotDescription=A%20cop%20from%20china%20and%20cop%20from%20america%20save%20kidnapped%20girl"
Observação: usamos
%20
para indicar espaços em nossa URL.Aqui chamamos nossa API com a consulta
"A cop from China and a cop from America save a kidnapped girl"
. Não há título, mas acho que é uma boa descrição de um determinado filme de ação/comédia estrelado por Jackie Chan e Chris Tucker. Aqui está uma versão ligeiramente abreviada do meu resultado. Vamos verificar nossos resultados!1 Movie title: Rush Hour 2 Plot: Two cops team up to get back a kidnapped daughter. 3 4 Movie title: Police Story 3: Supercop 5 Plot: A Hong Kong detective teams up with his female Red Chinese counterpart to stop a Chinese drug czar. 6 7 Movie title: Fuk sing go jiu 8 Plot: Two Hong-Kong cops are sent to Tokyo to catch an ex-cop who stole a large amount of money in diamonds. After one is captured by the Ninja-gang protecting the rogue cop, the other one gets ... 9 10 Movie title: Motorway 11 Plot: A rookie cop takes on a veteran escape driver in a death defying final showdown on the streets of Hong Kong. 12 13 Movie title: The Corruptor 14 Plot: With the aid from a NYC policeman, a top immigrant cop tries to stop drug-trafficking and corruption by immigrant Chinese Triads, but things complicate when the Triads try to bribe the policeman.
Consideramos ahora do pico nossa melhor correspondência. Exatamente o que eu tinha em mente! Se a premissa ressoa com você, há alguns outros filmes de que você pode assistir.
Você mesmo pode testar isso alterando o
plotDescription
que temos no comando cURL.Este tutorial percorreu as etapas abrangentes de criação de um aplicativo de pesquisa semântica usando MongoDB Atlas, OpenAI e Spring Boot.
A pesquisa semântica oferece uma pluralidade de aplicativos, que vão desde consultas sofisticadas de produtos em sites de e-commerce até recomendações de filmes personalizados. Este guia foi elaborado para equipá-lo com o essencial, abrindo caminho para seu próximo projeto.
Está pensando em integrar a pesquisa vetorial em seu próximo projeto? Confira este artigo — Como modelar seus documentos para pesquisa vetorial — para aprender como projetar seus documentos para pesquisa vetorial.
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.