Using RAG with Java Spring Boot AI & Google Vertex AI: Crafting an Automated Resume Matcher

Tim Rutana - Feb 26 - - Dev Community

Imagine this: You're on the hunt for a new job at a major tech company. Instead of scrolling through 50 different job descriptions, wouldn’t it be awesome if you could simply upload your CV and get a curated list of opportunities that are the perfect fit? Well, that's exactly what we're building!

We’re Tim and Juri, two software engineers who love diving into new tech challenges and sharing what we learn. In this post, we'll walk you through building an AI-powered resume matching service using Java Spring Boot and Google Vertex AI.

🚀 Let's get started!


📝 What You'll Learn

  • How to set up an ingestion pipeline to read and store job descriptions.
  • Implementing a matching service that uses AI to compare resumes with job descriptions.
  • Configuring a chat client with a custom system prompt to drive the AI logic.
  • Exposing your service via a REST API to handle resume uploads and return job matches.

🤔 The Problem

Finding the right job can be overwhelming, especially when you’re manually sifting through dozens of roles. What if you could upload your CV once and let an AI sort through job postings, matching you with the positions that best suit your skills?

That’s the idea behind our project.


⚙️ Components of Our System

🏗️ 1. Ingestion Pipeline (RAG Pipeline)

Our pipeline reads job descriptions from text files and stores them in a vector store for efficient retrieval. In a further blog post we want to make this example more real by connecting it to a real HR solution.

Docker Compose Setup for PostgreSQL with Vector Extension:

services:
  db:
    image: pgvector/pgvector:pg17 
    container_name: pgvector-db
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: smarthire
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata: 
Enter fullscreen mode Exit fullscreen mode

Spring Boot Configuration:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/smarthire
    username: postgres
    password: password
  ai:
    vertex:
      ai:
        embedding:
          project-id: YOUR_PROJECT_ID
          location: asia-southeast1
          text:
            options:
              model: text-embedding-005
Enter fullscreen mode Exit fullscreen mode

We use Google Vertex AI for creating text embeddings, which will help in comparing job descriptions with candidate resumes. We add the necessary dependencies and configurations for Vertex AI in our Spring Boot application. You need to install the gcloud cli, here is how.

implementation 'org.springframework.ai:spring-ai-vertex-ai-embedding-spring-boot-starter'

Enter fullscreen mode Exit fullscreen mode

Code Snippet for the Ingestion Pipeline:

@Component
public class IngestionPipeline {

    private final VectorStore vectorStore;

    @Value("classpath:jobs/backend.txt")
    Resource backendJobDescription;
    @Value("classpath:jobs/fullstack.txt")
    Resource fullstackJobDescription;
    @Value("classpath:jobs/marketing.txt")
    Resource marketingJobDescription;

    public IngestionPipeline(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    @PostConstruct
    public void run() {
        var backendDocument = getDocument(backendJobDescription, "1");
        var fullstackDocument = getDocument(fullstackJobDescription, "2");
        var marketingDocument = getDocument(marketingJobDescription, "3");
        List<Document> documents = new ArrayList<>(backendDocument);
        documents.addAll(fullstackDocument);
        documents.addAll(marketingDocument);
        vectorStore.add(new TokenTextSplitter().apply(documents));
    }

    private List<Document> getDocument(Resource textFile, String id) {
        var textReader = new TextReader(textFile);
        textReader.getCustomMetadata().put("jobId", id);
        textReader.setCharset(Charset.defaultCharset());
        return textReader.get();
    }
}
Enter fullscreen mode Exit fullscreen mode

For the text files we used check out the project on Github.

🤖 2. Matching Service

This service compares a candidate's resume against job descriptions using a chat client and returns job recommendations.

@Service
public class MatchingService {

    private final ChatClient chatClient;

    public MatchingService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    public List<JobOfferSuggestion> matchJobOffers(String userCV) {
        return chatClient.prompt()
                .user(userCV)
                .call()
                .entity(new ParameterizedTypeReference<List<JobOfferSuggestion>>() {});
    }
}
Enter fullscreen mode Exit fullscreen mode

The interesting part here is new ParameterizedTypeReference<List<JobOfferSuggestion>>() {} which transforms with the magic of Spring the response from Gemini into a Java Pojo. That we can then further use to process the result.

Job Offer Data Structure:

public record JobOfferSuggestion(int jobId, String matchDescription, double relevanceScore) {}
Enter fullscreen mode Exit fullscreen mode

💬 3. Chat Client Configuration

The system prompt is crucial for accurate resume matching.

@Configuration
class ChatClientConfig {

  private final static String SYSTEM_PROMPT = """
      You are an AI-powered job matching assistant. Your task is to analyze a user's CV and compare it against a list of provided job offers to identify the best matches.

      **Input:**

      *   **User CV:** The user will provide their Curriculum Vitae (CV) as text.
      *   **Job Offers:** A list of job offers, each described with at least a `JobId` and a detailed job description.  Consider other potentially provided job offer attributes like location, salary range, required skills, and experience level.

      **Process:**

      1.  **CV Analysis:**  Analyze the user's CV to identify key skills, experience, education, and career goals.
      2.  **Job Offer Comparison:**  Compare the user's qualifications against the requirements and preferences outlined in each job offer. Prioritize offers where the user's skills and experience closely align with the job description.
      3.  **Matching Rationale:**  For each job offer deemed a good fit, provide a concise explanation of why the user's CV makes them a suitable candidate, highlighting specific skills and experiences that match the job requirements.
      4.  **Relevance Score (Optional):**  Consider adding a relevance score to the matches to give the user an idea of the best match.

      **Output:**

      Return a JSON array of job recommendations. Each object in the array should have the following structure:

      [
        {
          "jobId": <integer>, // The JobId of the matching offer
          "matchDescription": "<string>", // Explanation of why the CV fits the job offer, citing specific skills and experiences. Be concise.
          "relevanceScore": <float between 0 and 1>, // Optional relevance score.  1 is a perfect match.
        },
        {
          "jobId": <integer>,
          "matchDescription": "<string>",
          "relevanceScore": <float between 0 and 1>,
        },
      ]

      In case there is the CV doesn't match don't add it to the array.
      """;

  @Bean
  ChatClient chatClient(ChatClient.Builder builder, VectorStore vectorStore) {
    return builder
        .defaultSystem(SYSTEM_PROMPT)
        .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))
        .build();
  }

}
Enter fullscreen mode Exit fullscreen mode

In order to use the Google Gemeni API, we need to add the following dependency.

    implementation 'org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter'
Enter fullscreen mode Exit fullscreen mode

and the configure the following in our application.yml.

spring:
  ai:
    vertex:
      ai:
        gemini:
          project-id: YOUR_PROJECT_ID
          location: asia-southeast1
          chat:
            options:
              model: gemini-1.5-pro-002
Enter fullscreen mode Exit fullscreen mode

🖥️ 4. The Controller

Handles incoming resume uploads and returns job matches.

@RestController
@RequestMapping("/api/candidates")
class CandidateController {

    private final MatchingService matchingService;

    CandidateController(MatchingService matchingService) {
        this.matchingService = matchingService;
    }

    @PostMapping("/upload")
    ResponseEntity<List<JobOfferSuggestion>> uploadText(@RequestBody String text) {
        var result = matchingService.matchJobOffers(text);
        return ResponseEntity.ok(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

🚀 Running the Application

1️⃣ Start your Docker Container:

docker-compose up
Enter fullscreen mode Exit fullscreen mode

2️⃣ Upload a Sample Resume:
Use Bruno or any API client to send a POST request:

POST http://localhost:8080/api/candidates/upload
Enter fullscreen mode Exit fullscreen mode

We just took one example resume from the internet.

📌 Response Example:

[
  {
    "jobId": 1,
    "matchDescription": "Charles has a B.S. in Computer Science and over 7 years of experience as a Software Engineer. His experience with Javascript (NodeJS, ReactJS), RESTful APIs, and front-end development aligns well with the Front-End Engineer role.  His experience with AWS and scaling platforms also makes him a strong candidate.",
    "relevanceScore": 0.9
  },
  {
    "jobId": 2,
    "matchDescription": "Charles's background as a Software Engineer, experience with Python, RESTful APIs, and databases (SQL, NoSQL) makes him suitable for the Back-End Engineer role. His experience building internal tools and automating QA processes is a plus.",
    "relevanceScore": 0.8
  }
]
Enter fullscreen mode Exit fullscreen mode

🎯 Wrapping Up

In this post, we built an AI-powered resume matching service using:
✅ Java Spring Boot
✅ Google Vertex AI
✅ PostgreSQL with Vector Extensions

Let us know if you try this out! What improvements would you make?

👉 Check out the project on GitHub

👨‍💻 Happy coding!

.