Building a Quarkus Application to Perform MongoDB Vector Search
Aasawari Sahasrabuddhe9 min read • Published Oct 07, 2024 • Updated Oct 07, 2024
FULL APPLICATION
Rate this article
Traditional keyword-based search methods often fall short of delivering relevant results in a world inundated with data. Vector search is a revolutionary approach that allows applications to understand context and meaning, enabling smarter, more intuitive searches. In this article, we’ll explore how to harness the combined power of Quarkus, a lightweight and high-performance Java framework, and MongoDB, a flexible document database, to implement vector search seamlessly.
We will get into the fundamentals of vector search, covering how it differs from traditional search methods and why it’s essential for modern applications. We will learn about generating vector embeddings from your data, setting up a robust search engine, and optimising performance—all while leveraging the speed and efficiency of Quarkus.
Whether you’re building a recommendation system, improving content discovery, or simply enhancing the user experience, this guide will equip you with the knowledge and tools to unlock the true potential of your search capabilities. Join us on this journey to revolutionize how users find and engage with information!
In the past, traditional search methods focused primarily on exact keyword matching or, at best, incorporated fuzzy search techniques. However, the modern digital landscape demands far more than just fuzzy search capabilities. Today’s developers are seeking advanced search solutions that offer semantic understanding, full recommendation engines, and AI-driven chatbots through natural language processing (NLP).
The solution to these evolving needs lies in MongoDB's Vector Search capability. This cutting-edge technology goes beyond simple keyword matching, providing a deeper understanding of user intent and context. As the demand for smarter, more intuitive search experiences grows, adopting vector search becomes essential for developers and businesses aiming to elevate user experience and engagement. With MongoDB's vector search, you can build highly responsive and intelligent search systems that keep pace with modern application requirements.
MongoDB's vector search works on the concept of vector representations. The vector representation in AI is a method of transforming data into vectors so that machine learning algorithms can process and understand it. These vectors capture semantic relationships; for example, words with similar meanings will be positioned closer together in the vector space.
The vector search performs the similarity measures using techniques like Euclidean, Cosine Similarity, and Dot product calculations to assess how closely related two vectors are.
The vector search performs the similarity measures using techniques like Euclidean, Cosine Similarity, and Dot product calculations to assess how closely related two vectors are.
Gemini AI is a suite of next-generation artificial intelligence models developed by Google DeepMind, intended to compete with advanced AI systems like OpenAI's GPT-4. It focuses on various AI capabilities, including natural language understanding, reasoning, and generating creative outputs.
The Gemini AI models are designed to be used across different Google services and integrated into products like Google Search, Google Workspace, and Google Cloud. They are known for improving interactive AI capabilities, making tasks such as conversation, content generation, and problem-solving more intuitive and human-like.
In this tutorial, we will be utilising the Gemini AI key to generate the embeddings for the fields and also for the query being sent for performing the vector search.
Here’s what you will need to follow along:
- A free-tier Atlas cluster—if you don’t have one already, register for a free cluster now
- Java 17 or above
- Gemini AI key—create your free Gemini key
Quarkus, known for its lightweight framework and robust extensions, is an excellent choice for building high-performance applications. When paired with MongoDB, a scalable document-oriented database, it enables the creation of advanced search solutions that surpass traditional keyword-based methods, offering a more context-aware and intuitive search experience.
In this tutorial, we will work with the
listings
collection from the AirbnB database to explore generating embeddings and conducting vector searches.Specifically, we’ll cover two main functionalities:
- Generating vector embeddings for the "description" field in each document within the collection.
- Creating a vector index to perform efficient vector searches across the dataset.
The complete code for the project is available on the GitHub repository. You can start by cloning the project and adding your details to the application.properties file to run the application.*
The first step is to generate the vector embeddings for a specific field, which contains large texts and is also in different languages. To create these embeddings, we have the ListingResource.java file, which has the REST call to generate embeddings.
We are using a simple embedding model as text-embedding-004. The "text-embedding-004" model is one of the embedding models provided by Gemini AI, which is designed to handle various AI tasks, such as text embedding for semantic search, recommendation systems, and natural language processing.
The number of embeddings generated by the text-embedding-004 model in Gemini AI depends on the length of the input text and how the model is configured to handle it. Typically, embeddings are produced for each token (words or subwords) in the text. The embedding model will generate a fixed-length numerical vector for each token or sequence of tokens. In this example, the total number of embeddings generated is 768.
1 2 3 public Response generateEmbeddings() { 4 listingService.generateAndStoreEmbeddings(); 5 return Response.ok("Embeddings generated and stored").build(); 6 }
The ListingService.java class is where the embeddings are being performed in batches. This is to avoid making repetitive calls to the Gemini API to generate embeddings for each document.
The batch process is followed by BulkWrite, which first generates all description fields and then makes the API call to Gemini AI.
1 public void generateAndStoreEmbeddings() { 2 MongoCollection<Document> listingsCollection = mongoClient.getDatabase("sample_airbnb").getCollection("listingsAndReviews"); 3 4 int processedDocuments = 0; 5 long totalDocuments = listingsCollection.countDocuments(); 6 7 while (processedDocuments < totalDocuments) { 8 List<Document> documents = listingsCollection.find() 9 .skip(processedDocuments) 10 .limit(BATCH_SIZE) 11 .into(new ArrayList<>()); 12 List<UpdateOneModel<Document>> bulkUpdates = new ArrayList<>(); 13 14 for (Document document : documents) { 15 String description = document.getString("description"); 16 List<Double> embeddings = geminiClient.getEmbedding(description); 17 18 if (embeddings != null) { 19 UpdateOneModel<Document> updateModel = new UpdateOneModel<>( 20 Filters.eq("_id", document.getString("_id")), 21 Updates.set("embeddings", embeddings) 22 ); 23 bulkUpdates.add(updateModel); 24 } 25 } 26 if (!bulkUpdates.isEmpty()) { 27 listingsCollection.bulkWrite(bulkUpdates, new BulkWriteOptions().ordered(false)); 28 } 29 30 processedDocuments += documents.size(); 31 System.out.println("Processed " + processedDocuments + " out of " + totalDocuments + " documents."); 32 } 33 34 System.out.println("Embedding generation complete for all documents."); 35 }
The next step of generating the embedding is creating the Gemini request and returning the list of embeddings. This is carried through the GeminiAIGatewayImpl.java class.
1 package com.example.vectorsearch.gateway; 2 3 import com.example.vectorsearch.config.GeminiConfig; 4 import com.example.vectorsearch.request.GeminiRequest; 5 import com.fasterxml.jackson.core.JsonProcessingException; 6 import com.fasterxml.jackson.databind.ObjectMapper; 7 import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; 8 import jakarta.enterprise.context.ApplicationScoped; 9 import jakarta.inject.Inject; 10 import jakarta.ws.rs.WebApplicationException; 11 import org.slf4j.Logger; 12 import org.slf4j.LoggerFactory; 13 14 import java.net.URI; 15 import java.util.List; 16 17 18 public class GeminiAIGatewayImpl implements GeminiAIGateway { 19 20 21 GeminiConfig geminiAIConfig; 22 23 private static final Logger LOGGER = LoggerFactory.getLogger(GeminiAIGatewayImpl.class); 24 25 26 public List<Double> getEmbedding(String input) { 27 ObjectMapper objectMapper = new ObjectMapper(); 28 29 var geminiAIGateway = QuarkusRestClientBuilder.newBuilder() 30 .baseUri(URI.create*(geminiAIConfig.getGeminiAIUrl() + "?key=" + geminiAIConfig.getApiKey())) 31 .build(GeminiClient.class); 32 33 GeminiRequest request = new GeminiRequest( 34 geminiAIConfig.getModel(), 35 new GeminiRequest.Content(List.of(new GeminiRequest.Part(input))) 36 ); 37 try { 38 String jsonRequest = objectMapper.writeValueAsString(request); 39 var embedding = geminiAIGateway.embedding(jsonRequest); 40 41 return embedding.getEmbedding().getValues(); 42 } catch (WebApplicationException e) { 43 LOGGER.error("Error from Gemini AI: {}", e.getResponse().readEntity(String.class)); 44 throw e; 45 } catch (JsonProcessingException e) { 46 throw new RuntimeException(e); 47 } 48 } 49 }
To run this API method, run the below command and wait until all the embeddings are generated in the collection for the field.
1 curl -X POST http://localhost:8080/api/listings/generate-embeddings
Note: The generated embedding will take time to generate and store inside the collection. In the above method, the processing happens in batches.
After the embeddings have been generated and stored inside the collection, the next step is to create the vector index. To create this index, you can go to the created Atlas cluster and then click on Atlas Search and perform the steps below:
Step 1: Click on the Search Indexes tab of the Atlas UI screen as shown below:
Screenshot representing how to create the vector search index for the selected collection.
Screenshot representing how to create the vector search index for the selected collection.
Step 2: Click on Create Vector Search Index on the screen.
Screenshot representing how to create the vector search index for the selected collection.
Step 3: Click on JSON Editor under the Atlas Vector Search section and click Next.
Screenshot representing how to create the vector search index for the selected collection.
Step 4: Select the database and collection name and create the vector index with the following JSON.
1 { 2 "fields": [ 3 { 4 "numDimensions": 768, 5 "path": "embeddings", 6 "similarity": "euclidean", 7 "type": "vector" 8 } 9 ] 10 }
Screenshot representing how to create the vector search index for the selected collection.
After generating the embedding for the desired field and creating the vector search index, the last step is to perform the semantic search. For example, you might wish to look for:
- Hotels that are recommended for a romantic stay.
- Places that have beachside views.
- Places that one would enjoy with kids.
To perform the searches with such an example, you need to perform the vector search.
The first step to do this is creating an embedding for the text being sent as a part of the request.
The first step to do this is creating an embedding for the text being sent as a part of the request.
1 public void performVectorSearch(String query) { 2 MongoCollection<Document> collection = mongoClient.getDatabase("sample_airbnb").getCollection("listingsAndReviews"); 3 4 List<Double> queryEmbeddings = geminiClient.getEmbedding(query); 5 String indexName = "vector_index"; 6 int numCandidates = 150; 7 int limit = 10; 8 9 List<Document> pipeline = Arrays.asList( 10 new Document("$vectorSearch", 11 new Document("index", indexName) 12 .append("path", "embeddings") 13 .append("queryVector", queryEmbeddings) 14 .append("numCandidates", numCandidates) 15 .append("limit", limit) 16 ), 17 new Document("$project", 18 new Document("_id", 0) 19 .append("name", 1) 20 .append("listing_url", 1) 21 .append("description", 1) 22 .append("price", 1) 23 ), 24 new Document("$limit", 5)); 25 26 collection.aggregate(pipeline) 27 .forEach(doc -> System.out.println(doc.toJson())); 28 }
The next step is to use the $vectorsearch aggregation stage. This stage performs the semantic search on the data. It searches a vector index (a specialized index that stores vector embeddings) for the closest matching vectors based on a given query vector.
For this example, we are projecting only a few details of the hotel and limiting the response to five.
To run the API, use the below command:
1 curl -X GET "http://localhost:8080/api/listings/perform-vector-search?query=Hotels%20that%20are%20recommended%20for%20romantic%20stay" | jq
Note: “ | jq” has been used in the cURL command to display the results in the JSON format. It is recommended to download jq before using the above cURL command.
This will give you the following response:
1 [ 2 { 3 "listing_url": "https://www.airbnb.com/rooms/15266254", 4 "name": "The Manhattan Club in the heart of midtown!!!!", 5 "description": "My place is good for couples, solo adventurers, and business travelers.", 6 "price": 305.00 7 }, 8 { 9 "listing_url": "https://www.airbnb.com/rooms/14261122", 10 "name": "Cute Condo in Mile-End, sleeps 2", 11 "description": "You’ll love my place because of the location. My place is good for couples, solo adventurers, and business travellers.", 12 "price": 70.00 13 }, 14 { 15 "listing_url": "https://www.airbnb.com/rooms/13609188", 16 "name": "Habitacion al lado de Sagrada Familia", 17 "description": "Lugares de interés: La Paradeta Sagarada Familia, FCB Official Point, Basílica de la Sagrada Família, Starbucks, . Mi alojamiento es bueno para parejas y aventureros.", 18 "price": 45.00 19 }, 20 { 21 "listing_url": "https://www.airbnb.com/rooms/13748059", 22 "name": "Studio for the 2016 Olympics", 23 "description": "My space is good for couples, business travelers and individual adventures.", 24 "price": 298.00 25 }, 26 { 27 "listing_url": "https://www.airbnb.com/rooms/11130156", 28 "name": "Double Room", 29 "description": "Literally just across the street is the world famous Cagaloglu Hammam featured in 1,000 Places to See Before You Die by Patricia Schultz. The hotel is tucked away on a quiet street and is just a few minutes walk to some of the world’s famous sites,", 30 "price": 359.00 31 } 32 ]
Another example that we can try is, “Stays that the kids would enjoy.”
1 curl -X GET "http://localhost:8080/api/listings/perform-vector-search?query=Stays%20that%20the%20kids%20would%20enjoy" | jq
This would give the response:
1 [ 2 { 3 "listing_url": "https://www.airbnb.com/rooms/13997910", 4 "name": "Apartamento de luxo em Copacabana - 4 quartos", 5 "description": "Meu espaço é bom para casais, viajantes de negócios e famílias (com crianças).", 6 "price": 11190.00 7 }, 8 { 9 "listing_url": "https://www.airbnb.com/rooms/14571224", 10 "name": "Alugo Quarto Para Temporada de Olimpiadas", 11 "description": "Meu espaço é bom para casais, viajantes de negócios e famílias (com crianças).", 12 "price": 798.00 13 }, 14 { 15 "listing_url": "https://www.airbnb.com/rooms/13927230", 16 "name": "Casa completa p olimpíadas com serviços incluído", 17 "description": "Você vai amar meu espaço por causa de a área externa, da piscina, o bairro com vários bares e diversões, o condomínio seguro com porteiro, a iluminação, a cama confortável e a cozinha grande. Meu espaço é bom para famílias (com crianças) e grandes grupos. Podendo se contratar junto também serviços de cozinheira, garçons e bartender. Podendo se contratar também com alimentação. Tudo para se sentir em casa e não se preocupar com nada. Consultar valores extras.", 18 "price": 601.00 19 }, 20 { 21 "listing_url": "https://www.airbnb.com/rooms/13617872", 22 "name": "Amor do Rio", 23 "description": "Você vai amar meu espaço por causa de o aconchego e a localização. Meu espaço é bom para casais, aventuras individuais, viajantes de negócios, famílias (com crianças) e grandes grupos.", 24 "price": 116.00 25 }, 26 { 27 "listing_url": "https://www.airbnb.com/rooms/13993059", 28 "name": "Ventanas Nature Resort ambiente familiar", 29 "description": "Meu espaço é perto de transporte público, restaurantes e refeições, atividades para famílias, , praia. Você vai amar meu espaço por causa de a localização, as pessoas, o ambiente e o bairro. Meu espaço é bom para casais, famílias (com crianças), grandes grupos e amigos peludos (animais de estimação).", 30 "price": 899.00 31 } 32 ]
There are several additional examples you can explore by adjusting the query to perform a vector search tailored to your needs.
Integrating vector search with MongoDB and Quarkus provides a powerful, scalable, and efficient solution for modern search needs. By leveraging Quarkus's lightweight framework and MongoDB's advanced search capabilities, developers can easily implement vector search to provide more context-aware and meaningful search experiences. With the ability to process natural language queries and return semantically relevant results, vector search opens new possibilities for building intelligent applications—whether for recommendation engines, content discovery, or personalized user interactions. This approach not only enhances performance but also significantly improves user satisfaction by making search results more intuitive and aligned with their true intent.
If you have further questions, please reach out to the MongoDB Community Forums and visit the content on the Developer Center to learn about more such cool concepts.
Happy searching!
Top Comments in Forums
There are no comments on this article yet.