<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Tim Rutana</title>
    <description>The latest articles on Forem by Tim Rutana (@reebow).</description>
    <link>https://forem.com/reebow</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2882740%2F0d9fad02-2939-4f6d-80d4-78a24155c4b0.png</url>
      <title>Forem: Tim Rutana</title>
      <link>https://forem.com/reebow</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/reebow"/>
    <language>en</language>
    <item>
      <title>From RAG to Multi-Agent AI for Job Matching</title>
      <dc:creator>Tim Rutana</dc:creator>
      <pubDate>Mon, 23 Jun 2025 10:44:11 +0000</pubDate>
      <link>https://forem.com/reebow/from-rag-to-multi-agent-ai-for-job-matching-5d66</link>
      <guid>https://forem.com/reebow/from-rag-to-multi-agent-ai-for-job-matching-5d66</guid>
      <description>&lt;p&gt;A while back, I wrote a &lt;a href="https://dev.to/reebow/using-rag-with-java-spring-boot-ai-google-vertex-ai-crafting-an-automated-resume-matcher-47lg"&gt;blog post&lt;/a&gt; about building an AI resume matcher using a RAG approach with Java and Google Vertex AI. I originally built a RAG-based AI to match resumes against job postings using a vector store. It was a fantastic project, but it got me thinking: how could this be even more intelligent and nuanced?&lt;/p&gt;

&lt;p&gt;That question led me straight to the &lt;a href="https://googlecloudmultiagents.devpost.com/?_gl=1*1wm6ow3*_ga*NjQ0Nzc3NjIxLjE3NTAxNTU4MjU.*_ga_0YHJK3Y10M*czE3NTA2NjE4MDEkbzMkZzEkdDE3NTA2NjE4OTckajYwJGwwJGgw" rel="noopener noreferrer"&gt;Google ADK Hackathon&lt;/a&gt;. The challenge was to build autonomous multi-agent systems, which felt like the perfect evolution of my previous project. So, I decided to tackle the same problem job matching, but with a completely new architecture. In this post, I'll walk you through my journey building JobMatch AI and explore why a multi-agent system was a game-changer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚀 Let's get started!&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 The Problem (Revisited)
&lt;/h2&gt;

&lt;p&gt;The core problem remains the same: finding the right job is overwhelming. Manually sifting through roles is exhausting. My goal was to create an AI assistant to do the heavy lifting, but this time, I wanted to build a system that could reason about a candidate's profile on a deeper level.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ What is the Agent Development Kit (ADK)?
&lt;/h2&gt;

&lt;p&gt;Before we dive into the specifics of my project, it's helpful to understand the core technology that made it possible. The &lt;a href="https://google.github.io/adk-docs/" rel="noopener noreferrer"&gt;Agent Development Kit (ADK)&lt;/a&gt; is an open-source framework provided by Google for building AI agents. Think of it as a toolkit that simplifies the process of creating sophisticated AI applications. Instead of just having a single AI model, the ADK helps you build and orchestrate multiple agents that can collaborate, use tools, and follow complex workflows to solve problems. It handles the tricky parts of making agents work together, allowing developers like me to focus on the logic and strategy of the agents themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚙️ Building the AI Hiring Committee: A Look Under the Hood
&lt;/h2&gt;

&lt;p&gt;In my previous RAG project, a single "generalist" AI handled everything. For the hackathon, I moved to a "team of specialists." Using the ADK, I created a SequentialAgent that acts as the team coordinator, ensuring each agent performs its task in a precise order. It’s like an automated assembly line for career matching.&lt;/p&gt;

&lt;p&gt;But why is this "team of specialists" approach better? In my experience, it leads to significantly more accurate and reliable results. A single, generalist AI trying to handle everything at once can get overwhelmed. It might miss nuances in the resume, fail to properly weigh career aspirations against experience, or get sidetracked during the scoring process. By breaking the problem down, each agent can focus on doing one thing exceptionally well. This division of labor ensures every step of the process is handled with higher precision, leading to a final list of job matches that are genuinely a better fit for the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏛️ Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Before diving into the specifics of each agent, let's take a high-level look at the system's architecture. The entire application is hosted on Google Cloud, leveraging a few key services to make the magic happen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34ax2mq12rcfk8inrfuw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34ax2mq12rcfk8inrfuw.png" alt="Architecture Overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's a breakdown of the flow:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The User uploads their resume and aspirations to the web application.&lt;/p&gt;

&lt;p&gt;The request hits the JobMatch AI App, a Java application built with the Java ADK running on Google Cloud Run.&lt;/p&gt;

&lt;p&gt;The ADK orchestrates the multi-agent workflow. For all language processing tasks, summarizing the resume, scoring jobs, and formatting the final output, the agents make calls to Vertex AI, specifically using the Gemini 2.0 model.&lt;/p&gt;

&lt;p&gt;When the SearchAgent needs to find jobs, it uses a static tool to invoke a query against the job postings stored in a Firebase DB.&lt;/p&gt;

&lt;p&gt;Finally, the scored and formatted job matches are returned to the user through the web app.&lt;/p&gt;

&lt;p&gt;This setup creates a robust and scalable system where the application logic on Cloud Run coordinates the powerful AI capabilities of Vertex AI and the data storage of Firebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  🤝 Here’s how the four-agent team works together
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: The Profile &amp;amp; Goal Synthesizer (SummarizeAgent)
&lt;/h4&gt;

&lt;p&gt;Everything starts here. This first agent is a specialist in understanding people. It takes the raw text of a user's resume and their career aspirations and gets to work. Its most critical instruction is to prioritize aspirations over experience. If a software developer wants to move into product management, this agent focuses on that goal. It intelligently extracts transferable skills from their past to support their future. It then synthesizes all this information into a detailed user profile, creating a rich summary for later analysis and a lean set of search terms for the next agent in the chain.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: The Job Query Specialist (SearchAgent)
&lt;/h4&gt;

&lt;p&gt;This agent has one job, and it’s strictly enforced: query the job database. During early tests, I ran into the classic LLM problem of "hallucination," where the model would invent jobs. The solution was to build a highly constrained agent. The SearchAgent is only permitted to use a single tool, getAllJobPostings, which connects to my project's Firebase database. (I chose Firebase for its flexibility, which was perfect for rapid prototyping during a hackathon). The agent is explicitly forbidden from inventing, modifying, or filtering jobs. It takes the search profile from the first agent, calls the tool, and passes the exact, unmodified results to the next agent. This completely solved the hallucination issue by separating the tasks of searching and thinking.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: The Analytical Scorer (ScoringAgent)
&lt;/h4&gt;

&lt;p&gt;Now that we have a user profile and a list of potential jobs, it's time for the heavy analysis. This agent acts as a highly analytical hiring manager. For every single job, it performs a multi-dimensional analysis, scoring the fit based on three key areas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aspiration Fit (The Most Important Factor):&lt;/strong&gt; How well does this job align with the user's stated career goals?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skill Fit:&lt;/strong&gt; Do the user's skills match what the job requires?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experience Fit:&lt;/strong&gt; Is the seniority and type of experience a good match?&lt;/p&gt;

&lt;p&gt;It then generates a comprehensive analysis for each job, containing the original job posting, a breakdown of the scores, an overall score from 0-100, and a brief, data-driven reasoning for its decision.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 4: The Encouraging Matchmaker (MatchmakerAgent)
&lt;/h4&gt;

&lt;p&gt;This is the final agent in the sequence, and it's the one responsible for communicating with the user. It takes the list of scored jobs from the ScoringAgent and first filters out anything with a score below 70, we only want to show high-quality matches. It then sorts the best jobs and crafts a personalized, encouraging report in Markdown. It doesn't just list the facts; it builds a narrative. It rephrases the reasoning in a friendly tone and, most importantly, explicitly connects the job back to the user's aspirations (e.g., "This is a fantastic opportunity because it directly aligns with your goal of moving into Product Management."). This final, human-centric touch is what turns a list of data into actionable career advice.&lt;/p&gt;

&lt;p&gt;This is just a high-level overview of how the agents collaborate. If you're interested in diving deeper into the prompts and the specific implementation of each agent, you can find the complete source code on my &lt;a href="https://github.com/reebow/jobmatchai" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎨 From Programmer Art to Polished UI: A Visual Journey
&lt;/h2&gt;

&lt;p&gt;While the agents were the core of the project, the user interface is what brings it to life. My initial UI was classic "programmer art" functional, but not exactly beautiful. It had the basic elements needed to upload a file and see the results, but it lacked any design polish.&lt;br&gt;
&lt;strong&gt;The prototype:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3w8yowh3xvm7byy8w1sm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3w8yowh3xvm7byy8w1sm.png" alt="programmer art prototype ui"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is where I turned to some incredible Google tools that felt like a superpower. Using &lt;a href="https://stitch.withgoogle.com/" rel="noopener noreferrer"&gt;Google Stitch&lt;/a&gt;, I was able to quickly create a clean, professional mockup of what I wanted the final application to look like.&lt;br&gt;
&lt;strong&gt;The mockup:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Farbsphiwrt8yio6typub.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Farbsphiwrt8yio6typub.png" alt="Stitch Mockup"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I fed the visual mockup from Stitch directly to Gemini, and it generated the frontend code to bring that design to life. This workflow was a complete gamechanger, allowing me to go from a basic mockup to a polished, user friendly interface without spending days on frontend development and UI designing.&lt;br&gt;
&lt;strong&gt;The final result:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbc0c3o2fce0w8mtkalfw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbc0c3o2fce0w8mtkalfw.png" alt="final result"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🎯 The Result
&lt;/h2&gt;

&lt;p&gt;The final application is simple and powerful. You visit the site, upload your resume (and optionally, your career aspirations), and within moments, the AI hiring committee gets to work, delivering a personalized report of your top matches.&lt;/p&gt;

&lt;p&gt;Watch the submission video here:&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/FP5ZEdeW2T0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This hackathon was an incredible learning experience. Adopting a multi-agent architecture proved to be a game-changer for accuracy, showing that a team of specialist AIs can outperform a single generalist by breaking a complex problem down into manageable, high-precision tasks. To complete my submission, I even used Veo to generate the demo video, another example of how AI can be a massive force multiplier in a time crunched solo person project.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔮 What's Next for JobMatch AI?
&lt;/h2&gt;

&lt;p&gt;This project is currently a prototype, but there's so much potential. I'd love to integrate more job sources and build pre-filtering tools for the search agent. The ultimate dream? An agent that, with your permission, could even auto-apply to your top matches!&lt;/p&gt;

&lt;p&gt;Let me know if you try it out! What improvements would you make?&lt;/p&gt;

&lt;p&gt;👨‍💻 Happy coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>adk</category>
      <category>java</category>
      <category>learning</category>
    </item>
    <item>
      <title>From Prototype to Production - Local Kubernetes: Our AI Resume Matcher's Journey</title>
      <dc:creator>Tim Rutana</dc:creator>
      <pubDate>Mon, 02 Jun 2025 06:19:46 +0000</pubDate>
      <link>https://forem.com/reebow/from-prototype-to-production-local-kubernetes-our-ai-resume-matchers-journey-1hh7</link>
      <guid>https://forem.com/reebow/from-prototype-to-production-local-kubernetes-our-ai-resume-matchers-journey-1hh7</guid>
      <description>&lt;p&gt;Hey everyone, Tim and Juri here! 👋&lt;/p&gt;

&lt;p&gt;Remember that awesome AI Resume Matcher we built together in our last post? (If you missed it or need a refresher, you can check it out right &lt;a href="https://dev.to/reebow/using-rag-with-java-spring-boot-ai-google-vertex-ai-crafting-an-automated-resume-matcher-47lg"&gt;here&lt;/a&gt;). We got our Java Spring Boot application, with Google Vertex AI (Gemini), to scan CVs and match them with job descriptions, all neatly packaged with Docker Compose. 🐳&lt;/p&gt;

&lt;p&gt;It was a fantastic first step, turning an idea into a working prototype. But what comes next on its journey from prototype to production? How do we make it more robust, scalable, and ready for bigger things? That's where Local Kubernetes enters the scene!&lt;/p&gt;




&lt;h2&gt;
  
  
  📝 What You'll Learn
&lt;/h2&gt;

&lt;p&gt;🐳 &lt;strong&gt;Containerizing our Java Spring Boot AI application&lt;/strong&gt; with Docker by writing a Dockerfile.&lt;br&gt;
⚙️ &lt;strong&gt;Setting up a local Kubernetes environment&lt;/strong&gt; right on your machine (we'll look at options like Minikube and Docker Desktop).&lt;br&gt;
📜 &lt;strong&gt;Crafting Kubernetes deployment manifests&lt;/strong&gt; – the YAML files that tell Kubernetes how to run our AI Resume Matcher app and its PostgreSQL (pgvector) database.&lt;br&gt;
🔑 &lt;strong&gt;Securely configuring Google Cloud authentication for Vertex AI&lt;/strong&gt; so our application can access it from within the Kubernetes cluster.&lt;br&gt;
🌐 &lt;strong&gt;Exposing your application&lt;/strong&gt; to be accessible from your browser using a Kubernetes Service.&lt;/p&gt;


&lt;h2&gt;
  
  
  🤔 Our Motivation: Why Go Local K8s?
&lt;/h2&gt;

&lt;p&gt;So, why Local Kubernetes if Docker Compose worked? Great question! While Docker Compose is fantastic, leveling up to K8s locally offers key benefits for our "journey to production":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mimics Production:&lt;/strong&gt; Get a feel for how apps run in real-world, production-like clusters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handles Growth:&lt;/strong&gt; Better equipped to manage more complex applications and future microservices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud-Ready Skills:&lt;/strong&gt; Smooths your transition to cloud platforms like Google Cloud and SAP BTP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standardized Deployments:&lt;/strong&gt; Promotes more consistent and reliable application rollouts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Essentially, using local K8s helps us build production-ready skills and prepares our AI Resume Matcher for its future in the cloud! ☁️&lt;/p&gt;


&lt;h2&gt;
  
  
  ⚙️ Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Our AI Resume Matcher Project:&lt;/strong&gt; You'll need the application code from our previous blog post. If you haven't got it yet, you can grab it from our &lt;a href="https://github.com/reebow/smarthire-blog" rel="noopener noreferrer"&gt;repository&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Desktop:&lt;/strong&gt; includes one click local Kubernetes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Cloud Account &amp;amp; Project:&lt;/strong&gt; Since our application relies on Google Vertex AI:

&lt;ul&gt;
&lt;li&gt;A Google Cloud Platform (GCP) account.&lt;/li&gt;
&lt;li&gt;A GCP Project where you've enabled the Vertex AI API (as covered in the previous post).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  📦 Step 1: Containerizing Our AI Resume Matcher
&lt;/h2&gt;

&lt;p&gt;First things first, we need to package our Spring Boot application into a Docker container. This makes it portable and ensures it runs the same way everywhere, from our local machine to a Kubernetes cluster.&lt;/p&gt;
&lt;h3&gt;
  
  
  Building the Application with Gradle
&lt;/h3&gt;

&lt;p&gt;Since our project uses Gradle, the first step is to build our application to produce an executable JAR file. Open your terminal in the root directory of the smarthire-blog project and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will compile your code, run tests and package everything into a JAR file. You'll find this JAR in the build/libs/ directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crafting the Dockerfile
&lt;/h3&gt;

&lt;p&gt;Next, we need to create a file named &lt;code&gt;Dockerfile&lt;/code&gt; (no extension) in the root of our project. This file contains the instructions Docker uses to build our image. Here’s the content for our Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; eclipse-temurin:21-jre-jammy&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;groupadd &lt;span class="nt"&gt;--system&lt;/span&gt; spring &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; useradd &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--gid&lt;/span&gt; spring spring
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; spring:spring&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; build/libs/*.jar app.jar&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["java", "-jar", "/app.jar"]&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down what each line does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FROM eclipse-temurin:21-jre-jammy:&lt;/strong&gt; This tells Docker to use the official Eclipse Temurin Java 21 JRE (Java Runtime Environment) image based on Ubuntu Jammy as our base. Using a JRE image is great because it's smaller than a full JDK image, making our container more lightweight as it only contains what's needed to run the compiled Java code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RUN groupadd --system spring &amp;amp;&amp;amp; useradd --system --gid spring spring:&lt;/strong&gt; Here, we create a system group named spring and then a system user also named spring, assigning it to that group. This dedicated, unprivileged user will run our application, enhancing security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;USER spring:spring:&lt;/strong&gt; This instruction switches the active user inside the Docker image to our newly created spring user. All subsequent commands, including our ENTRYPOINT, will run as this user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;COPY build/libs/*.jar app.jar:&lt;/strong&gt; This line copies the JAR file created by the Gradle build (from build/libs/ – the *.jar helps grab the versioned JAR file without needing to specify the exact name) into the container's filesystem and names it app.jar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ENTRYPOINT ["java", "-jar", "/app.jar"]:&lt;/strong&gt; This specifies the command that will be run when the container starts. It executes our Spring Boot application using java -jar. The /app.jar path refers to the JAR we copied in the previous step (it's placed in the root of the container's filesystem by default if no WORKDIR is specified).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EXPOSE 8080:&lt;/strong&gt; This line informs Docker that the application inside the container will listen on port 8080 at runtime. This doesn't actually publish the port; it's more of a documentation for users and a hint for tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Building the Docker Image
&lt;/h3&gt;

&lt;p&gt;With our Dockerfile in place and the application JAR built, we can now build the Docker image. In your terminal (still in the root directory of your project), run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; smarthire-app:v1 &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's dissect this command:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker build&lt;/code&gt;: The command to build an image from a Dockerfile.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-t smarthire-app:v1&lt;/code&gt;: The -t flag tags our image. Here, smarthire-app is the name of the image, and :v1 is the tag. Use a specific tag like v1, not :latest. Kubernetes default imagePullPolicy for :latest is Always (tries pulling remotely), while for v1 it's IfNotPresent (uses your local image if available).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.&lt;/code&gt;: This dot at the end tells Docker to look for the Dockerfile in the current directory.
After this command finishes, you'll have a Docker image ready to be run locally! You can check your list of images with docker images.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⚙️ Step 2: Launching Kubernetes with Docker Desktop
&lt;/h2&gt;

&lt;p&gt;Now that we have our application containerized, it's time to set up our local Kubernetes environment. For this guide, we'll use the Kubernetes cluster that comes built into Docker Desktop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enabling Kubernetes in Docker Desktop
&lt;/h3&gt;

&lt;p&gt;If you haven't enabled Kubernetes in Docker Desktop yet, it's just a few clicks away:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Settings (usually the gear icon ⚙️).&lt;/li&gt;
&lt;li&gt;Navigate to the Kubernetes section in the sidebar.&lt;/li&gt;
&lt;li&gt;Make sure the Enable Kubernetes checkbox is ticked.&lt;/li&gt;
&lt;li&gt;Click Apply &amp;amp; Restart. Docker Desktop will then download the necessary Kubernetes components and start your single-node cluster. This might take a few minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Verifying Your Kubernetes Cluster
&lt;/h3&gt;

&lt;p&gt;To interact with Kubernetes, we use a command-line tool called &lt;a href="https://kubernetes.io/docs/reference/kubectl/" rel="noopener noreferrer"&gt;kubectl&lt;/a&gt; (Gets installed with enabling Kubernetes in Docker Desktop).&lt;/p&gt;

&lt;p&gt;Let's verify that your cluster is up and running. Open your terminal and type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since Docker Desktop runs a single-node cluster, you should see one node listed, typically named docker-desktop, with a status of Ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Congratulations&lt;/strong&gt;, you have a local Kubernetes cluster running and ready for our AI Resume Matcher. 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ Step 3: Orchestrating with Kubernetes - Writing Our Manifests
&lt;/h2&gt;

&lt;p&gt;With our Kubernetes cluster ready and image built, it's time to define how our application and database will run using Kubernetes manifest files. These YAML blueprints describe our desired setup. We'll create two main files for this.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Database Blueprint (PostgreSQL + pgvector)
&lt;/h3&gt;

&lt;p&gt;First, let's define our PostgreSQL database, including data persistence and internal access. Create &lt;code&gt;k8s/postgres-k8s.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgvector/pgvector:pg17&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smarthire"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_USER"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_PASSWORD"&lt;/span&gt; &lt;span class="c1"&gt;# ❗ Use K8s Secrets in production!&lt;/span&gt;
          &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-data&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-data&lt;/span&gt;
          &lt;span class="na"&gt;persistentVolumeClaim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;claimName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-pvc&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-pvc&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ReadWriteOnce&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1Gi&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIP&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key parts of &lt;code&gt;postgres-k8s.yaml&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deployment:&lt;/strong&gt; Runs the pgvector/pgvector:pg17 image. We've set crucial environment variables like POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD directly for simplicity (though for passwords, Kubernetes Secrets are recommended in production). It also mounts a persistent volume for data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PersistentVolumeClaim (PVC):&lt;/strong&gt; Named postgres-pvc, this requests 1Gi of storage, ensuring our database data isn't lost if the pod restarts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service:&lt;/strong&gt; Named postgres and of type: ClusterIP, this gives our database an internal, stable IP address and DNS name (postgres:5432) so our application can reach it from within the cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The AI Resume Matcher App Blueprint (app-k8s.yaml)
&lt;/h3&gt;

&lt;p&gt;Next, the manifest for our Spring Boot AI Resume Matcher application. Create &lt;code&gt;k8s/app-k8s.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smarthire-app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smarthire-app&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smarthire-app&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smarthire-app&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smarthire-app:v1&lt;/span&gt; &lt;span class="c1"&gt;# Our image from Step 1&lt;/span&gt;
          &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt; &lt;span class="c1"&gt;# Use local image if present&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SPRING_DATASOURCE_URL&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jdbc:postgresql://postgres:5432/smarthire"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SPRING_DATASOURCE_USERNAME&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_USER"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SPRING_DATASOURCE_PASSWORD&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_PASSWORD"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/gcp-auth/key.json&lt;/span&gt;
          &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcp-sa-key-volume&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/gcp-auth&lt;/span&gt;
              &lt;span class="na"&gt;readOnly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcp-sa-key-volume&lt;/span&gt;
          &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcp-sa-key&lt;/span&gt; &lt;span class="c1"&gt;# GCP Secret (created in Step 4)&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smarthire-app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LoadBalancer&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8090&lt;/span&gt; &lt;span class="c1"&gt;# External port&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt; &lt;span class="c1"&gt;# App's internal port&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smarthire-app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key parts of &lt;code&gt;app-k8s.yaml&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deployment:&lt;/strong&gt; Runs our smarthire-app:v1 image (with imagePullPolicy: IfNotPresent to favor local images). It sets environment variables for the database connection (SPRING_DATASOURCE_URL, username, password) and for Google Cloud credentials (GOOGLE_APPLICATION_CREDENTIALS). The Google Cloud credentials will be mounted from a Secret named gcp-sa-key (which we'll create in Step 4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service:&lt;/strong&gt; Named smarthire-app and of type: LoadBalancer, this makes our application accessible externally (e.g., via localhost:8090 in Docker Desktop). It maps the external port 8090 to the application's internal port 8080.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these manifest files defining our application's structure in Kubernetes, we're ready to move on to the crucial step of handling Google Cloud authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔑 Step 4: Google Cloud Authentication from Kubernetes
&lt;/h2&gt;

&lt;p&gt;Our AI Resume Matcher application needs to communicate with Google Vertex AI services. When running locally (not in a container), it picks up your user credentials from the gcloud CLI. However, inside a Kubernetes pod, it's a different environment and needs its own explicit way to authenticate securely.&lt;/p&gt;

&lt;p&gt;For this setup, we'll use a Google Cloud Service Account and its JSON key. We'll then store this key as a Kubernetes Secret and mount it into our application's pod. &lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites for GCP Authentication
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Cloud Service Account&lt;/strong&gt; with appropriate permissions to use Vertex AI (e.g., the "Vertex AI User" role). &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON key file&lt;/strong&gt; for this service account downloaded to your local machine. Keep this file secure!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/iam/docs/service-accounts-create" rel="noopener noreferrer"&gt;Here is the documentation on how to create a service account.&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the Kubernetes Secret
&lt;/h3&gt;

&lt;p&gt;Now, let's take that downloaded JSON key file and create a Kubernetes Secret from it. This secret will securely store the key within your Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;Open your terminal and run the following &lt;code&gt;kubectl&lt;/code&gt; command. Make sure to replace &lt;code&gt;/path/to/your-downloaded-service-account-key.json&lt;/code&gt; with the actual path to your key file. The name of the key file itself doesn't matter as much as its content and the path you provide in the command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret generic gcp-sa-key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;key.json&lt;span class="o"&gt;=&lt;/span&gt;/path/to/your-downloaded-service-account-key.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break this command down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kubectl create secret generic gcp-sa-key&lt;/code&gt;: This tells Kubernetes to create a generic secret named gcp-sa-key. This is the exact name (gcp-sa-key) our application's deployment manifest (app-k8s.yaml) expects for the secret.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--from-file=&lt;/code&gt;: This flag tells &lt;code&gt;kubectl&lt;/code&gt; to create the secret from the contents of one or more files.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;key.json&lt;/code&gt;: This will be the filename inside the secret data. Our application's deployment is configured to look for this specific filename (key.json) when the secret is mounted as a volume.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/path/to/your-downloaded-service-account-key.json&lt;/code&gt;: The actual path to the JSON key file on your local system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After running this, Kubernetes will store the contents of your JSON key in the gcp-sa-key secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Step 5: Deploy! Applying Our Manifests
&lt;/h2&gt;

&lt;p&gt;We'll use &lt;code&gt;kubectl&lt;/code&gt; apply to deploy our application using the YAML files from Step 3. Make sure your terminal is in your project's root directory, where your k8s folder is located.&lt;/p&gt;

&lt;h3&gt;
  
  
  Applying the Manifests
&lt;/h3&gt;

&lt;p&gt;Apply the database manifest first, then the application manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; k8s/postgres-k8s.yaml
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; k8s/app-k8s.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These commands instruct Kubernetes to create the deployments, services, and other resources we defined.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying Your Deployment
&lt;/h3&gt;

&lt;p&gt;Let's quickly check if everything is running correctly:&lt;/p&gt;

&lt;h4&gt;
  
  
  Check Pod Status:
&lt;/h4&gt;

&lt;p&gt;To see if our application and database pods are up and running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see pods for &lt;strong&gt;postgres&lt;/strong&gt; and &lt;strong&gt;smarthire-app&lt;/strong&gt; eventually reach a STATUS of Running. This might take a moment. &lt;/p&gt;

&lt;h4&gt;
  
  
  Check the Application Service:
&lt;/h4&gt;

&lt;p&gt;To find out how to access your deployed application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the smarthire-app service in the list. If you're using Docker Desktop, its TYPE will be LoadBalancer, and the EXTERNAL-IP should be localhost. Note the PORT(S) column – it will show something like 8090:XXXXX/TCP. This means your application should be accessible at &lt;code&gt;http://localhost:8090&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Inspect Application Logs:
&lt;/h4&gt;

&lt;p&gt;If the &lt;strong&gt;smarthire-app&lt;/strong&gt; pod isn't behaving as expected (or you just want to see its output, such as the Spring Boot startup messages), you can check its logs.&lt;br&gt;
First, get the exact pod name (it will start with smarthire-app-):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, display the logs for that specific pod (replace  with the actual name from the command above):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs &amp;lt;smarthire-app-pod-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To follow the logs in real-time (useful for seeing live requests or errors), use the -f flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs &lt;span class="nt"&gt;-f&lt;/span&gt; &amp;lt;smarthire-app-pod-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your &lt;strong&gt;smarthire-app&lt;/strong&gt; pod is Running and the logs show a successful startup (like the Spring Boot ASCII art and "Tomcat started on port(s): 8080" message), then congratulations! Your AI Resume Matcher should now be running on your local Kubernetes cluster! 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  🧪 Step 6: Test Drive Time! Checking Our K8s-Powered App
&lt;/h2&gt;

&lt;p&gt;It's time to make sure it's working as expected by sending a test request.&lt;/p&gt;

&lt;p&gt;First, remember that our application should be accessible at &lt;code&gt;http://localhost:8090&lt;/code&gt; (based on the &lt;strong&gt;smarthire-app&lt;/strong&gt; Service of type &lt;strong&gt;LoadBalancer&lt;/strong&gt; we checked with &lt;code&gt;kubectl get services&lt;/code&gt; in Step 5). The endpoint for uploading a resume is still &lt;code&gt;/api/candidates/upload&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, using &lt;a href="https://www.usebruno.com/" rel="noopener noreferrer"&gt;Bruno&lt;/a&gt; (or your preferred API client), send a POST request to &lt;code&gt;http://localhost:8090/api/candidates/upload&lt;/code&gt;. Make sure the request body contains the plain text of a sample resume from our &lt;a href="https://dev.to/reebow/using-rag-with-java-spring-boot-ai-google-vertex-ai-crafting-an-automated-resume-matcher-47lg"&gt;previous post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You should receive a successful JSON response. This response will list the job offers that best match the CV you provided, just like the example response we saw in our initial Docker Compose setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 Wrapping Up: Our AI Resume Matcher on Kubernetes!
&lt;/h2&gt;

&lt;p&gt;We've successfully taken our AI Resume Matcher application from a Docker Compose setup and deployed it onto a local Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;Together, we've walked through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🐳 &lt;strong&gt;Containerizing&lt;/strong&gt; our Java Spring Boot application with Docker.&lt;/li&gt;
&lt;li&gt;📜 &lt;strong&gt;Crafting&lt;/strong&gt; the necessary Kubernetes manifest files for both our application and its PostgreSQL database.&lt;/li&gt;
&lt;li&gt;🔑 &lt;strong&gt;Configuring&lt;/strong&gt; Google Cloud authentication from within Kubernetes using a Service Account and Secrets.&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Deploying&lt;/strong&gt; all components to the cluster.&lt;/li&gt;
&lt;li&gt;🧪 &lt;strong&gt;Testing&lt;/strong&gt; our application to ensure it's running correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By working through these steps, you've not only got our AI Resume Matcher running in a more robust, orchestrated environment but also gained hands-on experience with key DevOps practices and cloud-native technologies.&lt;/p&gt;

&lt;p&gt;Did you run into any challenges, or do you have suggestions for improvements? Let us know in the comments below!&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/reebow/smarthire-blog/tree/local-k8s" rel="noopener noreferrer"&gt;Check out the project on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👨‍💻 Happy coding!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>kubernetes</category>
      <category>java</category>
      <category>learning</category>
    </item>
    <item>
      <title>Using RAG with Java Spring Boot AI &amp; Google Vertex AI: Crafting an Automated Resume Matcher</title>
      <dc:creator>Tim Rutana</dc:creator>
      <pubDate>Wed, 26 Feb 2025 16:04:21 +0000</pubDate>
      <link>https://forem.com/reebow/using-rag-with-java-spring-boot-ai-google-vertex-ai-crafting-an-automated-resume-matcher-47lg</link>
      <guid>https://forem.com/reebow/using-rag-with-java-spring-boot-ai-google-vertex-ai-crafting-an-automated-resume-matcher-47lg</guid>
      <description>&lt;p&gt;&lt;strong&gt;Imagine this:&lt;/strong&gt; 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!  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We’re Tim and Juri&lt;/strong&gt;, 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 &lt;strong&gt;Java Spring Boot and Google Vertex AI&lt;/strong&gt;.  &lt;/p&gt;

&lt;p&gt;🚀 &lt;strong&gt;Let's get started!&lt;/strong&gt;  &lt;/p&gt;




&lt;h2&gt;
  
  
  📝 What You'll Learn
&lt;/h2&gt;

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




&lt;h2&gt;
  
  
  🤔 The Problem
&lt;/h2&gt;

&lt;p&gt;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?  &lt;/p&gt;

&lt;p&gt;That’s the idea behind our project.  &lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Components of Our System
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🏗️ 1. Ingestion Pipeline (RAG Pipeline)
&lt;/h3&gt;

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

&lt;p&gt;Docker Compose Setup for PostgreSQL with Vector Extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgvector/pgvector:pg17&lt;/span&gt; 
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgvector-db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smarthire&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring Boot Configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;datasource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jdbc:postgresql://localhost:5432/smarthire&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
  &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;vertex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;embedding&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;project-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_PROJECT_ID&lt;/span&gt;
          &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;asia-southeast1&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text-embedding-005&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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, &lt;a href="https://cloud.google.com/sdk/docs/install" rel="noopener noreferrer"&gt;here is how&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'org.springframework.ai:spring-ai-vertex-ai-embedding-spring-boot-starter'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code Snippet for the Ingestion Pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IngestionPipeline&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;VectorStore&lt;/span&gt; &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:jobs/backend.txt"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Resource&lt;/span&gt; &lt;span class="n"&gt;backendJobDescription&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:jobs/fullstack.txt"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Resource&lt;/span&gt; &lt;span class="n"&gt;fullstackJobDescription&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:jobs/marketing.txt"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Resource&lt;/span&gt; &lt;span class="n"&gt;marketingJobDescription&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;IngestionPipeline&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VectorStore&lt;/span&gt; &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vectorStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@PostConstruct&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;backendDocument&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getDocument&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backendJobDescription&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fullstackDocument&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getDocument&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fullstackJobDescription&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;marketingDocument&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getDocument&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;marketingJobDescription&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"3"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;backendDocument&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fullstackDocument&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;marketingDocument&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TokenTextSplitter&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getDocument&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Resource&lt;/span&gt; &lt;span class="n"&gt;textFile&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;textReader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;textFile&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;textReader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCustomMetadata&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"jobId"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;textReader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCharset&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Charset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultCharset&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;textReader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the text files we used check out the project on &lt;a href="https://github.com/reebow/smarthire-blog" rel="noopener noreferrer"&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  🤖 2. Matching Service
&lt;/h3&gt;

&lt;p&gt;This service compares a candidate's resume against job descriptions using a chat client and returns job recommendations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MatchingService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;MatchingService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JobOfferSuggestion&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;matchJobOffers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userCV&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userCV&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ParameterizedTypeReference&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JobOfferSuggestion&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;()&lt;/span&gt; &lt;span class="o"&gt;{});&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part here is &lt;code&gt;new ParameterizedTypeReference&amp;lt;List&amp;lt;JobOfferSuggestion&amp;gt;&amp;gt;() {}&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Job Offer Data Structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;JobOfferSuggestion&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;jobId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;matchDescription&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;relevanceScore&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  💬 3. Chat Client Configuration
&lt;/h3&gt;

&lt;p&gt;The system prompt is crucial for accurate resume matching.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatClientConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""
      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": &amp;lt;integer&amp;gt;, // The JobId of the matching offer
          "matchDescription": "&amp;lt;string&amp;gt;", // Explanation of why the CV fits the job offer, citing specific skills and experiences. Be concise.
          "relevanceScore": &amp;lt;float between 0 and 1&amp;gt;, // Optional relevance score.  1 is a perfect match.
        },
        {
          "jobId": &amp;lt;integer&amp;gt;,
          "matchDescription": "&amp;lt;string&amp;gt;",
          "relevanceScore": &amp;lt;float between 0 and 1&amp;gt;,
        },
      ]

      In case there is the CV doesn't match don't add it to the array.
      """&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Bean&lt;/span&gt;
  &lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="nf"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;VectorStore&lt;/span&gt; &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultSystem&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultAdvisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QuestionAnswerAdvisor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to use the Google Gemeni API, we need to add the following dependency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the configure the following in our application.yml.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;vertex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;gemini&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;project-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_PROJECT_ID&lt;/span&gt;
          &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;asia-southeast1&lt;/span&gt;
          &lt;span class="na"&gt;chat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini-1.5-pro-002&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🖥️ 4. The Controller
&lt;/h3&gt;

&lt;p&gt;Handles incoming resume uploads and returns job matches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/candidates"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CandidateController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;MatchingService&lt;/span&gt; &lt;span class="n"&gt;matchingService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nc"&gt;CandidateController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MatchingService&lt;/span&gt; &lt;span class="n"&gt;matchingService&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;matchingService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;matchingService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/upload"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JobOfferSuggestion&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;uploadText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;matchingService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;matchJobOffers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 Running the Application
&lt;/h2&gt;

&lt;p&gt;1️⃣ Start your Docker Container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2️⃣ Upload a Sample Resume:&lt;br&gt;
Use &lt;a href="https://www.usebruno.com/" rel="noopener noreferrer"&gt;Bruno &lt;/a&gt; or any API client to send a POST request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;POST http://localhost:8080/api/candidates/upload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We just took one example resume from the &lt;a href="https://www.beamjobs.com/resumes/software-engineer-resume-examples" rel="noopener noreferrer"&gt;internet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;📌 Response Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"matchDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"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."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"relevanceScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"matchDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"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."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"relevanceScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎯 Wrapping Up
&lt;/h2&gt;

&lt;p&gt;In this post, we built an AI-powered resume matching service using: &lt;br&gt;
✅ Java Spring Boot&lt;br&gt;
✅ Google Vertex AI&lt;br&gt;
✅ PostgreSQL with Vector Extensions&lt;/p&gt;

&lt;p&gt;Let us know if you try this out! What improvements would you make?&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/reebow/smarthire-blog" rel="noopener noreferrer"&gt;Check out the project on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👨‍💻 Happy coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>springboot</category>
      <category>java</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
