<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Mala Gupta on Medium]]></title>
        <description><![CDATA[Stories by Mala Gupta on Medium]]></description>
        <link>https://medium.com/@mala.gupta?source=rss-439aa1c2e46e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*ONZ-XRC4I3UxATMRO1N2Lg.jpeg</url>
            <title>Stories by Mala Gupta on Medium</title>
            <link>https://medium.com/@mala.gupta?source=rss-439aa1c2e46e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 21:31:28 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@mala.gupta/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Are We Ready for the Unemployment Apocalypse?]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@mala.gupta/are-we-ready-for-the-unemployment-apocalypse-e8649eceb74d?source=rss-439aa1c2e46e------2"><img src="https://cdn-images-1.medium.com/max/2450/1*iru0A4jTVfZldooY2CSYmA.png" width="2450"></a></p><p class="medium-feed-snippet">AI is taking the entry-level jobs. Here are five simple, win-win ways to keep the door open for our youth.</p><p class="medium-feed-link"><a href="https://medium.com/@mala.gupta/are-we-ready-for-the-unemployment-apocalypse-e8649eceb74d?source=rss-439aa1c2e46e------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://medium.com/@mala.gupta/are-we-ready-for-the-unemployment-apocalypse-e8649eceb74d?source=rss-439aa1c2e46e------2</link>
            <guid isPermaLink="false">https://medium.com/p/e8649eceb74d</guid>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[life]]></category>
            <category><![CDATA[work]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[lifestyle]]></category>
            <dc:creator><![CDATA[Mala Gupta]]></dc:creator>
            <pubDate>Fri, 02 Oct 2026 19:33:55 GMT</pubDate>
            <atom:updated>2026-10-03T06:49:55.488Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Reduced Memory Usage by Default in Java 27 with Compact Object Headers]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@mala.gupta/reduced-memory-usage-by-default-in-java-27-with-compact-object-headers-caab2e358e8c?source=rss-439aa1c2e46e------2"><img src="https://cdn-images-1.medium.com/max/2600/1*KasO4SSTdGRM0xnx8EStiA.png" width="2817"></a></p><p class="medium-feed-snippet">No Code Changes Needed.</p><p class="medium-feed-link"><a href="https://medium.com/@mala.gupta/reduced-memory-usage-by-default-in-java-27-with-compact-object-headers-caab2e358e8c?source=rss-439aa1c2e46e------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://medium.com/@mala.gupta/reduced-memory-usage-by-default-in-java-27-with-compact-object-headers-caab2e358e8c?source=rss-439aa1c2e46e------2</link>
            <guid isPermaLink="false">https://medium.com/p/caab2e358e8c</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[jdk27]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[Mala Gupta]]></dc:creator>
            <pubDate>Wed, 30 Sep 2026 18:39:36 GMT</pubDate>
            <atom:updated>2026-09-30T18:47:54.793Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Two pictures, ten differences, and a childhood memory]]></title>
            <link>https://medium.com/no-time/two-pictures-ten-differences-and-a-childhood-memory-0b221667abeb?source=rss-439aa1c2e46e------2</link>
            <guid isPermaLink="false">https://medium.com/p/0b221667abeb</guid>
            <category><![CDATA[memories]]></category>
            <category><![CDATA[life]]></category>
            <category><![CDATA[joy]]></category>
            <category><![CDATA[love]]></category>
            <category><![CDATA[psychology]]></category>
            <dc:creator><![CDATA[Mala Gupta]]></dc:creator>
            <pubDate>Sun, 27 Sep 2026 19:02:41 GMT</pubDate>
            <atom:updated>2026-09-27T19:02:41.559Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Play ‘Spot the Difference’ and rediscover a little childhood joy</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RGjI4-_QaRfid7_CVL7RFw.png" /></figure><p><strong>Play the game — </strong><a href="https://little-differences.mala-gupta.workers.dev/"><strong>Little Differences</strong></a></p><p>Let’s take a little break from reading about technology. Today, I’m sharing something I loved long before I wanted to become a software developer: spotting differences between two pictures.</p><p>As a child, I used to look forward to our <a href="https://www.childrensbooktrust.com/childrens-magazine.html">monthly children’s magazine</a> from <a href="https://www.childrensbooktrust.com/">Children’s Book Trust</a> in India. One of my favourite pages had two similar black-and-white pictures and a challenge: spot the ten differences between them.</p><p>I loved doing that.</p><p>Finding the differences was only part of the excitement. I would mark them all, cut out the page, and post my completed entry to the publisher’s address. Sometimes, I won prizes! Imagine the joy of finding tiny details in a picture and then receiving a prize for it.</p><p>That memory stayed with me.</p><p>So I decided to make <strong>Little Differences</strong>, a small browser game inspired by those magazine puzzles. It’s my attempt to relive that joy and share a little of it with others.</p><p>The game starts with ten black-and-white picture puzzles, each with three differences to find. Some differences take a closer look to spot. There’s one hint per puzzle if you get stuck, and a timer if you fancy challenging yourself. Once you finish, “you can try another puzzle.</p><p>You don’t to create an account to play this. And this time, no scissors, envelopes, or postage stamps either. 😊</p><p><strong>Play the game — </strong><a href="https://little-differences.mala-gupta.workers.dev/"><strong>Little Differences</strong></a></p><p>If you remember solving puzzles like these in magazines, I’d love to hear your memories. And if this is your first time, I hope you enjoy that little moment of satisfaction when you finally spot the difference.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0b221667abeb" width="1" height="1" alt=""><hr><p><a href="https://medium.com/no-time/two-pictures-ten-differences-and-a-childhood-memory-0b221667abeb">Two pictures, ten differences, and a childhood memory</a> was originally published in <a href="https://medium.com/no-time">No Time</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Java 27’s Lazy Constants: Can We Do This Later, Please?]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@mala.gupta/java-27s-lazy-constants-can-we-do-this-later-please-182296ddac0d?source=rss-439aa1c2e46e------2"><img src="https://cdn-images-1.medium.com/max/2600/1*4AJPFlxmoqsoAgjVi6rWjA.png" width="2766"></a></p><p class="medium-feed-snippet">Define constants. Choose when to compute them.</p><p class="medium-feed-link"><a href="https://medium.com/@mala.gupta/java-27s-lazy-constants-can-we-do-this-later-please-182296ddac0d?source=rss-439aa1c2e46e------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://medium.com/@mala.gupta/java-27s-lazy-constants-can-we-do-this-later-please-182296ddac0d?source=rss-439aa1c2e46e------2</link>
            <guid isPermaLink="false">https://medium.com/p/182296ddac0d</guid>
            <category><![CDATA[jdk27]]></category>
            <category><![CDATA[lazyconstant]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[constant]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[Mala Gupta]]></dc:creator>
            <pubDate>Sat, 26 Sep 2026 19:38:20 GMT</pubDate>
            <atom:updated>2026-09-27T06:06:09.060Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Java’s Vector API in Practice: Four Problems You Can Solve]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@mala.gupta/javas-vector-api-in-practice-four-problems-you-can-solve-8e1adacfc239?source=rss-439aa1c2e46e------2"><img src="https://cdn-images-1.medium.com/max/2301/1*cmtSTrSN2IbjXD8cFjgXGw.png" width="2301"></a></p><p class="medium-feed-snippet">Part 2 of the Vector API series</p><p class="medium-feed-link"><a href="https://medium.com/@mala.gupta/javas-vector-api-in-practice-four-problems-you-can-solve-8e1adacfc239?source=rss-439aa1c2e46e------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://medium.com/@mala.gupta/javas-vector-api-in-practice-four-problems-you-can-solve-8e1adacfc239?source=rss-439aa1c2e46e------2</link>
            <guid isPermaLink="false">https://medium.com/p/8e1adacfc239</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[vector]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[vector-api]]></category>
            <category><![CDATA[java27]]></category>
            <dc:creator><![CDATA[Mala Gupta]]></dc:creator>
            <pubDate>Thu, 17 Sep 2026 22:57:56 GMT</pubDate>
            <atom:updated>2026-09-27T18:10:18.273Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Before You Dive into Java’s Vector API]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@mala.gupta/before-you-dive-into-javas-vector-api-86b5655278f0?source=rss-439aa1c2e46e------2"><img src="https://cdn-images-1.medium.com/max/2538/1*7y4X7PJSRO5F9KByGEUpyw.png" width="2538"></a></p><p class="medium-feed-snippet">(Part 1 of the Vector API series)</p><p class="medium-feed-link"><a href="https://medium.com/@mala.gupta/before-you-dive-into-javas-vector-api-86b5655278f0?source=rss-439aa1c2e46e------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://medium.com/@mala.gupta/before-you-dive-into-javas-vector-api-86b5655278f0?source=rss-439aa1c2e46e------2</link>
            <guid isPermaLink="false">https://medium.com/p/86b5655278f0</guid>
            <category><![CDATA[java]]></category>
            <category><![CDATA[vector-api]]></category>
            <category><![CDATA[java27]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Mala Gupta]]></dc:creator>
            <pubDate>Fri, 11 Sep 2026 21:25:00 GMT</pubDate>
            <atom:updated>2026-09-26T20:20:37.114Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Is this thing training on my data? Decoding what happens when you talk to a LLM]]></title>
            <link>https://medium.com/@mala.gupta/is-this-thing-training-on-my-data-decoding-what-happens-when-you-talk-to-a-llm-13cab663ff5b?source=rss-439aa1c2e46e------2</link>
            <guid isPermaLink="false">https://medium.com/p/13cab663ff5b</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[large-language-models]]></category>
            <dc:creator><![CDATA[Mala Gupta]]></dc:creator>
            <pubDate>Thu, 10 Sep 2026 03:36:48 GMT</pubDate>
            <atom:updated>2026-09-26T20:22:31.388Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TpDbndEk7JC1jOjtqxuDJQ.png" /></figure><p>When you send a query to a hosted LLM, do you fear whether that model is learning from what you just sent it? You could have shared and queried about something sensitive, such as an employment contract details, or a half-formed idea that you were brainstorming with the model. And right after hitting the send button (believe me, it is always after clicking on send!), a voice inside you questions whether that was safe and if that information could surface in responses sent to another user (your boss or a colleague)?</p><p>This post is about that fear.</p><p>The naive version of your fear is wrong because that is not how models learn. However, your fear is justified, but for other reasons. To understand this confusing answer, you’ll need to understand how models learn, as covered in this post.</p><p>We’ll use the kitchen analogy from the <a href="https://medium.com/@mala.gupta/want-to-build-ai-agents-lets-decode-the-jargon-first-2fd4e77dee2b">first post</a>, with one change. Before any chef works in the real world, they usually go to a culinary school to learn their craft.</p><p>Let’s start with a simple (and important) question.</p><h3>Do models learn from your conversations?</h3><p>No. A released model’s weights are frozen. Nothing you say in a chat changes them.</p><p>When a model “remembers” what you said three messages ago, that is not because its learning from your conversations, it is because the messages history is being handed back to it as input. Some models send a summary of your previous chats as part of your prompt to a model.</p><p>So the naive fear that your information or data is being used to train the model you are querying, is incorrect.</p><p>But hold that thought, because the fear comes back, and the second time it’s right. To see why, you have to know what “frozen weights” actually means, which means knowing how the model got un-frozen in the first place, through three different styles of learning.</p><h4>1. Supervised learning</h4><p>Compare supervised learning with learning from examples that come with the correct answer attached. Imagine examples such as the following where each item includes a label so you could verify your learning:</p><ol><li>This photo: cat.</li><li>This email: spam.</li><li>This product: defective.</li></ol><p>The model guesses, compares it with the correct answer and adjusts to close the gap. The “supervision” is that answer key accompanying every example.</p><p>(A naming note: you’ll sometimes hear this being referred to as “classified learning”. The standard term is supervised learning, paired with unsupervised learning.)</p><h4>Analogy: a head chef tastes everything</h4><p>Picture a kitchen. Supervised learning is a trainee working next to a head chef who tastes everything they make. Trainee makes a sauce, head chef tastes it. If the chef thinks that the sauce is too thin, sour or anything else, they instruct the trainee accordingly. Every attempt (from the trainee) gets a verdict from someone (the chef) who already knows what a good sauce tastes like.</p><p>It works beautifully, but it doesn’t scale. Though extremely valuable, chef’s is the bottleneck. Every attempt has to be personally tasted. That’s the cost of the answer key.</p><h4>2. Unsupervised learning</h4><p>Compare unsupervised learning with finding the structure hiding inside data nobody labelled.</p><p>There is no answer key or labels to tell what it is. Just a pile of data and the task of finding its patterns. The model discovers that a group of customers behave alike, or a group of documents cluster, or perhaps, a transaction resembles nothing else.</p><p>Since nothing needs labelling, it scales to enormous piles. The trade-off: you get structure, not answers. It can tell you what goes together, not what to call it.</p><h4>Analogy: alone in the walk-in fridge</h4><p>Imagine a kitchen where a trainee is let loose in the walk-in fridge with no instructions. Nobody labelled anything. But after enough time they notice that a few food items go soft in a day, others are good for weeks, a group smells related, one item resembles nothing else in the room. However, the trainee still might not be able to tell you which group is “herbs” because nobody taught them the word.</p><h4>3. Reinforcement learning</h4><p>Compare reinforcement learning with learning from consequences instead of any answer keys or labels. The model acts, an outcome follows, and it was either good or bad. Based of the consequences, a model ‘learns’ to do more of what worked, less of what didn’t.</p><h4>Analogy: running your own restaurant</h4><p>Imagine you are running your own restaurant now. There are no head chef tasting your sauce, just customers. Some plates come back clean, some half-eaten, and nobody explains why. You changed four things on Tuesday, but it went badly. Which of the four? You’ll work it out over months of Tuesdays. This learning is slower, but it is also the only way to learn what nobody can demonstrate. No head chef can taste “did the customer enjoy their whole evening.” Only the consequence tells you that.</p><p>Now that you’ve had a bare minimum understanding of the three ways in which models learn, let’s look at other concepts, such as, weights of a model to understand how published models can’t continue to learn.</p><h3>What are weights of a Model? Why published models can’t continue to learn?</h3><p>Think of <strong>weights of a model as the numbers that store what the model has learned</strong>.</p><p>When an LLM is trained , it repeatedly looks at examples, makes predictions, compares them with the expected results, and adjusts these numbers. Each adjustment could be tiny, but training repeats this process billions of times.</p><p>At the beginning, the weights are essentially random, so the model produces nonsense. As training progresses, the weights gradually change to capture patterns in language: which words tend to go together, how sentences are structured, how concepts relate, and so on.</p><p>When training is complete, the resulting set of weights is saved. That set of numbers is the Model Weights.</p><p>There isn’t a little database inside the model containing every sentence it learned, or a collection of recipes filed away for later. Instead, what the model has learned is encoded in the enormous pattern of numbers called weights.</p><p>Compare training weights with the numbers inside the model that hold everything it learned.</p><h4>Analogy</h4><p>Picture a chef whose skills have been shaped by years of training: how to hold a knife, adjust the heat, and season a dish. Those learned instincts are like model weights.</p><p>Now imagine that when the chef finishes culinary school, their learned skills are locked in place. They can still cook new dishes, but what happens in the kitchen doesn’t change those skills.</p><p>That’s the key difference. Humans can learn from experience. A deployed LLM can generate new answers, but its weights don’t change as a result of those interactions.</p><p>That’s what “frozen weights” means.</p><h3>So why is everyone still worried about their query data?</h3><p>If the model in front of you can’t learn from your data, why do companies panic about their prompts being used for training? If nothing you type changes the model, where’s the risk?</p><p>This is because two different things are being confused as one.</p><p>The model you are using is frozen. It cannot learn on your data. However, the provider running it can still keep your data. And that data can be used to train the next model version.</p><p>The learning doesn’t happen for the version of the model you are using. It happens later, when the provider gathers what it collected and trains a fresh version. Your data isn’t teaching this chef. It might be teaching next year’s class.</p><h4>Analogy</h4><p>The chef in front of you won’t improve no matter what you order, and they’ll forget your table the moment you leave.</p><p>But imagine the restaurant keeps a notebook and writes down every order or conversation. The secret recipe you shared with the chef could be recorded in the notebook too. Noting changes today. Next year, the new batch of chefs might know how to make your family’s dish.</p><p><strong>The risk isn’t just that the model might learn your data. The bigger risk is what happens to your data after you send it.</strong></p><h3>The risks go far beyond training</h3><p>Once sensitive data leaves your hands, several things can happen:</p><p><strong>1. Retention &amp; breach</strong><br>Your prompt may be stored. If that storage is breached, your data is exposed whether or not it was ever used for training. The safest data is data that was never kept.</p><p><strong>2. Human review</strong><br>People may review samples of conversations for quality or safety. Your “private” prompt could therefore be seen by a human.</p><p><strong>3. Legal discovery</strong><br>Stored data can potentially be subpoenaed or provided to authorities. Data that doesn’t exist can’t be compelled.</p><p><strong>4. Subprocessors</strong><br>Your data may pass through infrastructure, storage, or annotation vendors. Every additional party adds another point of exposure.</p><p><strong>5. Regulatory exposure</strong><br>For health, financial, or other regulated data, simply sending the data to a provider without the required agreement or safeguards can itself create a compliance problem. Training doesn’t have to happen for the problem to exist.</p><p><strong>6. Policy drift</strong><br>Today’s promises may not be tomorrow’s policies. The data can outlive the terms under which you originally shared it.</p><p>There could be many more risks.</p><h3>So what actually protects you?</h3><p>Instead of asking “Does the model learn from my conversation?”, ask “What happens to my data after I send it?”</p><p>Ask three questions:</p><ul><li>Retention<strong>:</strong> Is my data stored? For how long?</li><li>Training<strong>:</strong> If stored, can it be used to train future models?</li><li>Access<strong>: </strong>Who can see it, including employees, contractors, and subprocessors?</li></ul><p>“We don’t train on your data” does not mean “we don’t store it.” And “we don’t store it” does not mean the same thing as “no one can ever see it.” A real data-governance decision has to consider all three: retention, training, and access.</p><h3>So what should you do?</h3><p>Don’t send sensitive data to an LLM casually.</p><p>A sensible policy is:</p><ul><li>Public / low-risk data<strong>:</strong> Use LLMs freely.</li><li>Internal / confidential data: Use an approved business/API deployment with appropriate retention, training, access, and security controls.</li><li>Highly sensitive or regulated data<strong>:</strong> Keep it out of the LLM unless the provider, contract, architecture, and compliance controls explicitly allow it.</li></ul><h3>Summary</h3><p>Should you fear that an LLM learns from your conversation? Not in the way you might fear. A released model has frozen weights, so it can’t <em>learn</em> from your prompt, data, or documents.</p><p>But that doesn’t make sharing sensitive data safe. Your data can still be stored, reviewed, exposed in a breach, subject to legal demands, or potentially used to train a future model.</p><p>So don’t focus only on “Is the model learning from me?” Ask the more important question: “What happens to my data after I send it?” Use LLMs with caution. Understand the difference between the model learning from your data and the provider storing your data.</p><h3>Buy me a Coffee</h3><p>Found this useful? If you’d like to support my Java articles, you can <a href="https://buymeacoffee.com/malagupta">buy me a coffee</a>. Thank you for reading!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=13cab663ff5b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Are you a human?]]></title>
            <link>https://medium.com/no-time/are-you-a-human-8b81af4b9426?source=rss-439aa1c2e46e------2</link>
            <guid isPermaLink="false">https://medium.com/p/8b81af4b9426</guid>
            <category><![CDATA[sarcasm]]></category>
            <category><![CDATA[lifestyle]]></category>
            <category><![CDATA[life]]></category>
            <category><![CDATA[humor]]></category>
            <category><![CDATA[life-lessons]]></category>
            <dc:creator><![CDATA[Mala Gupta]]></dc:creator>
            <pubDate>Wed, 15 Jul 2026 15:41:36 GMT</pubDate>
            <atom:updated>2026-09-26T20:17:32.440Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Attempt all questions. No negative marking.</em><br> <em>(Yes, the AI series is still on. This is just the tea break.)</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*psCgtYJFit91cNCodA1Dnw.jpeg" /></figure><p>You are a human, and there are some simple tests for you to attempt to prove it. And no, these tests will not go anywhere near your emotions, your empathy or your love. They will not even ask you an intelligent question like explaining the philosophy of life, because let’s be honest, AI is much better at answering that one anyway.</p><p>So let’s find out what these tests are. And <strong>if you need help preparing for them, do connect with me, I’m running a live masterclass on this topic next week.</strong></p><h3>Captchas (with dancing letters or numbers)</h3><p>Have you ever clicked a photo of a toddler or a teenager who doesn’t want their photo clicked (like at a family function)? The teenagers often look grumpy and turn their face at the exact moment, and the toddlers stand in all sorts of poses, poking fingers in their noses or sticking out their tongues, while trying all sorts of gymnastics. This is how the letters and digits in a captcha look to me.</p><p>A request to the folks creating these captchas. Could you please stop harassing the letters and numbers for photographs? Your photos make it hard for me to determine if a letter is an inverted ‘v’, or an ‘I’ bent to the left, or an ‘L’ lying down after a long day. And who has ever been able to tell the difference between a 0, an O and an o? Not me, not you. Still, we humans endure this torture to prove we are humans.</p><h3>The Audio Option</h3><p>Captchas also have an audio option, which is the button you click after you have given up on your eyes. A voice reads out the captcha, except that the voice has either a weird accent or the way of saying those letters and numbers is off. A follow up question. How many of you have been able to figure out if the audio option said 8BVN or ABDN on the first attempt?</p><p>Sometimes I wonder if this was designed by companies trying to figure out which humans have impaired hearing. I mean why do I see advertisements for hearing aids right after I fail to enter a captcha correctly :D.</p><p>So yes, I successfully proved I’m a human, perhaps with a hearing issue (with maybe a hearing aid in my shopping cart).</p><h3>Click a checkbox that says ‘You are a human’</h3><p>I love this one. This is the simplest one to prove anyone is a human. <strong>Just a click and it proves one is human, no matter how inhumane they are in real life.</strong></p><p>But here is my problem with it. As a developer advocate and a power user of an IDE, I have been promoting the use of the keyboard over the mouse for the last couple of years. And then this checkbox arrives, and I have to squirm and reach out for my mouse just to prove I’m a human. I know I could tab to it and hit the spacebar, but web pages are weird, because then they just scroll. So I seriously think this one is designed to test whether humans can recall where their mouse is.. and whether they still have the ability to use it.</p><p>And the funniest part is, <strong>the checkbox does not even care about the click. It is busy enjoying the sight of me squirming over the question</strong>.. really.. I need to check this box to prove I’m a human? I believe it loves to JUDGE humans. Are we being ranked on our grumpiness? Is this another trick to sell ‘wellness sessions’ to us :D</p><h3>Select boxes with a car/ traffic light etc.</h3><p>C’mon! First of all, <strong>don’t show those blurry images to me</strong>. It is difficult to figure out whether it has a car, or whether the brick just seems like the boot of the car. And no, my vision is not that bad. If you want to check my vision, give me another test. Don’t give me blurry images and then JUDGE me, telling me I’m not a human!</p><p>And then there is this classic example about selecting boxes with traffic lights. Is the pole a part of the traffic light or not? Nobody knows. <strong>Google does not know. That is exactly why they are asking me</strong>! So there I am at 11 in the night, taking a policy decision on behalf of a self driving car, and unpaid. And the moment I click a square, it fades out and a new square slowly fades in, and that one also has a bicycle. Everything has a bicycle. I did not come here for bicycles, I came here to reset my password.</p><h3>Summary</h3><p>Proving you are a human is much simpler in this AI era. Just a click here and there. And you are kind of fine.</p><p>My blogs on AI agents resumes from the next post with all the serious AI stuff I have been writing all this while. I just needed a break from it, and going by the state of the internet, I suspect you needed one too. Think of this post as the captcha you had to clear before the real content loads.</p><h3>Buy me a Coffee</h3><p>Found this useful? If you’d like to support my Java articles, you can <a href="https://buymeacoffee.com/malagupta">buy me a coffee</a>. Thank you for reading!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8b81af4b9426" width="1" height="1" alt=""><hr><p><a href="https://medium.com/no-time/are-you-a-human-8b81af4b9426">Are you a human?</a> was originally published in <a href="https://medium.com/no-time">No Time</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Want to build AI agents? Let’s decode the jargon first]]></title>
            <link>https://medium.com/@mala.gupta/want-to-build-ai-agents-lets-decode-the-jargon-first-2fd4e77dee2b?source=rss-439aa1c2e46e------2</link>
            <guid isPermaLink="false">https://medium.com/p/2fd4e77dee2b</guid>
            <category><![CDATA[tools]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[skills]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[mcp-server]]></category>
            <dc:creator><![CDATA[Mala Gupta]]></dc:creator>
            <pubDate>Tue, 14 Jul 2026 10:28:27 GMT</pubDate>
            <atom:updated>2026-09-26T20:23:11.074Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*258qxex75GJGHAZbUpGiJA.jpeg" /></figure><p>If you’re starting out with building AI agents, you’ve probably run into words like workflows, skills, orchestration, tools, MCP server, loop engineering, guardrails, and harnessing. The list gets overwhelming fast. Learning these terms first builds a strong foundation before you write any code.</p><p>A quick note: people use “Agents” and “AI Agents” to mean the same thing. These are fairly new terms, so the exact meaning often depends on who you ask.</p><p>In this new series, I’m learning how to create AI agents and sharing each step. This first post explains those terms using simple analogies. Most of them compare a human worker with an LLM, since both can reason and analyze to some degree (how well depends on which person, or which model, you’re talking to :D).</p><p>This isn’t an exhaustive list, just the terms worth knowing to get going. Let’s start.</p><h3>1. What is an Agent?</h3><p>This is a simple question but it could have multiple answers depending on whom you are talking with.</p><p>Compare an<strong> Agent </strong>with<strong> a worker</strong> who has thinking capabilities. Depending on how much freedom an agent has, they could be following a predefined process (such as a worker on an assembly line), or changing the course as they see fit (such as a Chef in a restaurant kitchen). In computing scenarios, every agent would include some sort of LLM to achieve its goal. It would take in information, decide, act, and produce an output.</p><h4>Analogy</h4><p>Picture a <strong>Chef</strong> in a <strong>kitchen</strong> and an <strong>assembly line</strong> worker <strong>as</strong> your <strong>agents</strong>.</p><p>A Chef might have more freedom to prepare a dish. They course correct and improvise, depending on the availability of ingredients, their quality, or special requests from customers. However, an assembly line worker might not have a lot of wiggle room. They usually follow pre-defined steps.</p><p>An assembly line worker has limited flexibility as compared to a Chef. But, this is <strong>NOT</strong> a flaw in the system. It is the intent. If every assembly line worker decides independently on how an item should be handled, the whole production system would fall apart.</p><p>Let’s talk about the ways in which agents are designed, such as workflows.</p><h3>2. What is a workflow?</h3><p>Compare a <strong>Workflow </strong>with a <strong>predefined sequence of steps followed in order</strong>. The path is defined in advance, such as, do A, then B, then C. It’s reliable and predictable, but it can’t stray from the script. If something unexpected happens, course correction steps could be pre-defined too.</p><h4>Analogy</h4><p>Picture a <strong>kitchen</strong> again. A <strong>workflow</strong> is a <strong>recipe</strong> that defines a fixed set of steps that are followed to prepare a dish. It has the same ingredients, same process, every single time. <strong>It still needs a person</strong> (an agent) to prepare this dish though. In a workflow, a cook (or an LLM) follows a path someone else laid out.</p><h4>Can a workflow’s sub task be performed by another agent?</h4><p>Most real systems are hybrids, where an agent might run inside a workflow, or a workflow might call an agent for one tricky step. The line blurs in practice.</p><p>Let’s see how tasks are performed by an agent or a workflow using tools.</p><h3>3. What are tools?</h3><p>Compare <strong>tools</strong> with the <strong>external capabilities an agent or a workflow can call on</strong>. A tool is any function the system can invoke to take an action or fetch information. A language model can reason and decide, but it can’t search your database, or send an email on your behalf. Tools give it those extra powers. Behind a tool could be a local method, an external API, or even another LLM (though that needs further coordination, as covered later in this post). The tool itself is the capability the model is handed; each time it uses one, that’s a call to the tool.</p><h4><strong>Analogy</strong></h4><p>Picture the kitchen again. The chef (an agent) has the skill and judgment, but they still need equipment to cook. A knife to chop, an oven to bake, a phone to call a supplier when an ingredient runs out. Those are the tools. The chef decides which one to grab for the task at hand, uses it, sees the result, and moves on. Without tools, the chef could plan a great meal in their head but never actually make it.</p><h3>4. What are skills?</h3><p>Compare <strong>Skills</strong> with <strong>the know-how to do a task well</strong>. A skill is a set of instructions, expertise, and sometimes helper files that teaches an agent how to do a job. It usually captures the process to follow and the expectations for the output, and it can also include information about how a business works, its rules, terminology, and edge cases. It often carries guidelines, best practices, or gotchas for the task. Where a tool is a capability the agent invokes, a skill is guidance the agent reads and follows.</p><h4>Analogy</h4><p>Picture the kitchen again. The chef’s tools are the equipment, but their skills are the techniques they’ve trained in, such as, how to fillet a fish, how to build a proper sauce, how to plate a dish so it looks right. Notice how the two work together: filleting a fish takes the knife (the tool) and the filleting technique (the skill) at the same time. When an order calls for fish, the chef draws on that filleting know-how without being taught it fresh each time. That learned, reusable expertise, ready to apply when the task calls for it, is a skill.</p><h4>A sample SKILL.md</h4><p>Skills are conventionally written as Markdown files and named SKILL.md, so that’s what you’ll see in the wild. It also usually includes a, that is, the block at the top. That’s where the description of your skill files lives, and the description is what tells a system when to reach for the skill. A good one is what makes a skill actually get used.</p><p>Here’s sample of SKILL.md:</p><pre>---<br>name: fillet-a-fish<br>description: Use when a dish calls for a boneless fish portion. Covers how to<br>  fillet a whole fish cleanly. Do NOT use for shellfish or pre-cut portions.<br>---<br><br># Skill: Filleting a fish<br><br>## Steps to follow<br>1. Lay the fish flat and locate the spine.<br>2. Cut behind the gills, then run the knife along the spine to the tail.<br>3. Flip and repeat on the other side.<br>4. Trim the belly bones and pull out any pin bones.<br><br>## Good technique (what makes it clean)<br>- Keep the blade angled toward the bones, not the flesh, to waste less.<br>- Use long, smooth strokes rather than sawing.<br><br>## Gotchas<br>- Missed pin bones ruin the dish. Always run a finger along the fillet to check.<br>- A dull knife tears the flesh. Sharpen before starting.<br><br>## Checks<br>- [ ] Are both fillets free of bones?<br>- [ ] Is the flesh smooth, not shredded?</pre><h4>SKILLS.md or SKILL.md?</h4><p>It is easy to overlook that simple difference, but it is always SKILL.md. <br>So what do you do, if you want to include more skills for your project?</p><h4>Does one project have exactly one skill.md file?</h4><p>A project can have many skills, not just one.</p><p>Each skill lives in its own folder, and inside that folder is a single file named SKILL.md. The skills are told apart by the <em>folder name</em>, not by the filename. So the filename is always the same, SKILL.md (singular, note, not SKILLS.md), while the folder gives each skill its identity. You end up with a layout like:</p><pre>skills/<br>├── fillet-a-fish/<br>│   └── SKILL.md<br>├── build-a-sauce/<br>│   └── SKILL.md<br>├── plate-a-dish/<br>│   └── SKILL.md<br>└── knead-dough/<br>    └── SKILL.md</pre><p>One folder has exactly one SKILL.md, but a project can hold as many skill folders as you like, each with its own. The folder name is effectively the skill’s name (and it usually matches the name field in the frontmatter).</p><h4>Does each skill folder exactly one file, that is, skill.md?</h4><p>The folder can hold more than just SKILL.md, with helper file. An example:</p><pre>fillet-a-fish/<br>├── SKILL.md              ← the know-how (steps, technique, gotchas)<br>├── knife-cuts.png        ← a reference sheet showing the cut angles<br>└── fish-guide.md         ← details on different fish (where bones sit, etc.)</pre><p>Now the SKILL.md could be shortened, and it could point <em>to</em> the other two files instead of cramming everything in:</p><pre>## Steps to follow<br>1. Lay the fish flat and locate the spine.<br>2. Check fish-guide.md for where the pin bones sit in this species.<br>3. Follow the cut angles shown in knife-cuts.png.<br>4. Trim the bones, then run the checks.</pre><h4>When does the line between tools and skills blurs?</h4><p>The line between a skill and a tool can blur. When an agent invokes it to get a result (a database, an API), it is a tool. If it’s guidance on how to do something (steps, conventions, domain knowledge), it is skill. The two can even live together. A skill folder might include a small script the agent runs to resize an image, and that script is a tool sitting inside a skill.</p><h3>5. What is a MCP Server?</h3><p><strong>MCP Server</strong> is a <strong>standardized way to plug tools and data into an agent</strong>.</p><p>MCP (Model Context Protocol) is an open standard that defines <em>how</em> an agent and an external capability talk to each other. An MCP server is a program that exposes a set of tools or data sources (a database, your email, a file system, a company’s internal API) in this common format. Because they all speak the same protocol, any agent that “speaks MCP” can connect to any MCP server without custom wiring for each one.</p><h4>Analogy</h4><p>Picture the kitchen again. Every appliance, the oven, the blender, the mixer, could come with its own oddly shaped plug, forcing you to rewire the kitchen for each one. An MCP server is like a <em>standard power outlet on the wall</em>. Any appliance built to that standard just plugs in and works. The chef (the agent) doesn’t care about the wiring behind the wall. They see a socket, plug in the tool they need, and start cooking.</p><h4>Why is MCP important?</h4><p>Without MCP, every provider is a new custom integration. With MCP, every provider is reached through the same three steps: connect, list tools, call a tool. You still have one server per provider, but you stop writing custom code for each one.</p><p>Since this is a bit complicated to understand, let me use an example that is for sending an email via Gmail and Outlook.</p><p>Custom code for each provider (without MCP Server):</p><pre># --- Gmail: custom integration ---<br>gmail = GmailClient(api_key = &quot;...&quot;)<br>gmail.login_with_google_oauth()<br>gmail.messages.send(to = &quot;sam@x.com&quot;, subject = &quot;Hi&quot;, body = &quot;Hello&quot;)<br><br>outlook = OutlookClient(tenant = &quot;...&quot;, secret = &quot;...&quot;)<br>outlook.authenticate_microsoft()<br>outlook.mail.post(recipient = &quot;sam@x.com&quot;, title = &quot;Hi&quot;, content = &quot;Hello&quot;)</pre><p>With MCP Server:</p><pre># Connect to any MCP server the SAME way<br>gmail   = connect_mcp(&quot;https://gmail-mcp-server&quot;)<br>outlook = connect_mcp(&quot;https://outlook-mcp-server&quot;)<br><br># Ask a server what tools it offers<br>gmail.list_tools()     # -&gt; [&quot;send_message&quot;, &quot;search&quot;, ...]<br>outlook.list_tools()   # -&gt; [&quot;send_mail&quot;, &quot;search&quot;, ...]<br><br># Call a tool through the same standard method<br>gmail.call_tool(&quot;send_message&quot;, { to: &quot;sam@x.com&quot;, body: &quot;Hello&quot; })<br>outlook.call_tool(&quot;send_mail&quot;,  { to: &quot;sam@x.com&quot;, body: &quot;Hello&quot; })</pre><p>The connection code is now identical: connect_mcp(…), list_tools(), call_tool(…). Look at the tool names: Gmail exposes send_message, Outlook exposes send_mail. The <em>way you call them</em> is the same, but the <em>names</em> can differ. So MCP makes connecting uniform, not the tools themselves identical.</p><h3>6. What is an inference API?</h3><p>As the name suggests, it is an API to talk to an LLM. You can send your query as text to an LLM, via an API, and receive text back. Depending on the API, you could also get JSON objects as a response. You could get all of the response text in one go, or as a stream (received as the chunks of the response are generated). You could also send images or other formats and receive back text, images, or other formats, such as video, sound, and documents, and many more.</p><h3>7. What is orchestration?</h3><p>Orchestration is the coordination of multiple agents, tools, and steps toward one larger goal. When a task is too big for a single agent or a single step, orchestration is the layer that decides who does what, in what order, and how the pieces hand off to each other. It routes the work, passes results between components, handles what happens when a step fails, and keeps everything moving toward the final outcome.</p><p>In short, agents do the work, and orchestration is the coordination that makes many of them work together as one system.</p><h4>Analogy</h4><p>Picture a big kitchen on a busy night. You have several chefs, one on sauces, one on grilling, one on desserts, plus ovens and prep stations all running at once. Orchestration is the head chef calling the pass, deciding which dish starts when, making sure the steak and the sides finish at the same moment, rerouting work when one station falls behind, and sending each plate out only when it’s complete. No single cook or chef sees the whole picture. The head chef’s coordination is what turns separate efforts into one smooth service.</p><h4>Does orchestration come in multiple styles?</h4><p>Yes. In <em>centralized</em> orchestration, one controller directs everyone (the head chef model above). In <em>decentralized</em> orchestration, agents coordinate among themselves without a single boss, more like experienced cooks who know the rhythm and hand off to each other directly. Most real systems lean on the centralized version first because it’s easier to reason about.</p><h3>8. What are guardrails?</h3><p><strong>Guardrails </strong>are the rules and limits that keep an agent’s behavior safe and on-track. Because an agent decides its own steps, you need boundaries on what it’s allowed to do, say, or access. Guardrails are the checks that sit around the agent: filtering harmful or off-topic outputs, blocking risky actions, keeping it within its intended scope, and validating that inputs and outputs meet your standards. They can run before the agent acts, while it acts, or after, catching problems the agent’s own judgment might miss.</p><p>In short, an agent decides freely, and guardrails are the boundaries that make sure its freedom stays safe and appropriate.</p><h4>Analogy</h4><p>Picture the kitchen again. A skilled chef has freedom to improvise, but the kitchen still has rules they can’t cross, such as, don’t serve anything with an allergen the customer flagged, don’t send out undercooked chicken, keep raw meat away from the salad station, and stay within the day’s budget. Those food-safety and house rules are the guardrails. They don’t tell the chef how to cook, but they draw hard lines the chef must not step over, no matter how creative they get.</p><h4>Is guardrails only about blocking bad outputs?</h4><p>No, they also work on the way in. An input guardrail can catch a malicious or manipulative instruction before the agent ever acts on it (for example, someone trying to trick the agent into ignoring its rules). Splitting it into input-side and output-side guardrails is a clean way to show your audience that safety wraps around the whole system, not just the final answer.</p><h3>9. What is harnessing?</h3><p><strong>Harnessing </strong>is the <strong>supporting code that turns a model into a working agent.</strong></p><p>Harnessing is the supporting code that turns a model into a working agent. An agent is the whole thing (model + harness working together). The harness is just the code part, without the model. In short, the model is the reasoning core that makes the decisions, and the harness is the machinery around it that calls the tools, and remembers past steps.</p><h4>Analogy</h4><p>Picture the kitchen again. The chef is brilliant, but a brilliant chef alone in an empty room can’t cook. The harness is the working kitchen built around them: the station where ingredients arrive, the ticket rail that hands over one order at a time, the runner who carries finished plates out and brings back the next request, and the loop that keeps the whole cycle turning. The chef supplies the judgment, but the harness is what feeds them work, carries out their instructions, and brings back results so they can keep going.</p><h3>10. What is Loop engineering?</h3><p>An agent works in a loop: think, use a tool, look at the result, then think again, until the task is done. Loop engineering is the work of making that loop run well, deciding when it should stop, keeping it from repeating itself, and controlling how much time or cost it can use.</p><h4>Analogy</h4><p>Picture the kitchen again. The chef works in a taste-adjust-taste cycle: taste the sauce, add salt, taste again, adjust, until it’s right. Loop engineering is setting the smart rules around that cycle. Know when it’s good enough to send out rather than fussing forever, don’t keep adding salt in a way that never converges, and don’t burn the whole night’s time perfecting one sauce while other orders pile up. It’s the judgment about <em>how to run the loop</em>, not the cooking itself.</p><h3>Summary</h3><p>This is a beginner-friendly guide to the jargon behind AI agents, agentic AI, workflows, tools, MCP servers, guardrails, and more, explained with simple analogies before you start building.</p><h3>Buy me a Coffee</h3><p>Found this useful? If you’d like to support my Java articles, you can <a href="https://buymeacoffee.com/malagupta">buy me a coffee</a>. Thank you for reading!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2fd4e77dee2b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[No stories. Just the minimal code to build a working RAG application]]></title>
            <link>https://medium.com/@mala.gupta/no-stories-just-the-minimal-code-to-build-a-working-rag-application-21f5f5bc8767?source=rss-439aa1c2e46e------2</link>
            <guid isPermaLink="false">https://medium.com/p/21f5f5bc8767</guid>
            <category><![CDATA[qdrant]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[rags]]></category>
            <category><![CDATA[ai-agent]]></category>
            <dc:creator><![CDATA[Mala Gupta]]></dc:creator>
            <pubDate>Sun, 12 Jul 2026 13:06:34 GMT</pubDate>
            <atom:updated>2026-07-13T17:21:33.499Z</atom:updated>
            <content:encoded><![CDATA[<h3>Building a RAG application requires understanding multiple concepts. This is my attempt to simplify them for you.</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UvjrKKMBLnE-axeY9JYxXw.jpeg" /></figure><p>This is the payoff of the series. I covered a lot of concepts related to a RAG application via my previous posts: <a href="https://medium.com/@mala.gupta/vector-databases-explained-for-java-developers-d07721b8c8ab">what a vector database is</a>, <a href="https://medium.com/@mala.gupta/how-do-we-see-a-vector-with-384-dimensions-9b7bbc3d01d9">what a 384-dimension vector looks like</a>, <a href="https://medium.com/@mala.gupta/how-vector-databases-measure-closeness-dot-product-cosine-and-euclidean-9749c4dbbbc3">how closeness is measured</a>, <a href="https://medium.com/@mala.gupta/what-football-can-teach-you-about-chunking-data-16bca9cf4000">how to chunk</a>, and <a href="https://medium.com/@mala.gupta/can-you-ask-a-pdf-why-egypt-did-not-win-the-2026-fifa-world-cup-ae2ffcfc1a91">how to ask a PDF a question</a>.</p><p>That last post stopped at retrieval. Here we add the missing step: hand the retrieved chunks to Gemini and get a written answer, with the page still attached. Same PDF, same Qdrant, same local embeddings.</p><p>One step at a time below. The complete file is at the end.</p><h3>Step 0: Keys</h3><p>Two things in your environment. Qdrant Cloud (get them as in the <a href="https://medium.com/@mala.gupta/vector-databases-explained-for-java-developers-d07721b8c8ab">vector database post</a>) and a free Gemini key from Google AI Studio (https://aistudio.google.com/apikey → <strong>Create API key</strong>).</p><pre>export QDRANT_CLUSTER_ENDPOINT=&quot;YOUR-CLUSTER.cloud.qdrant.io&quot;   <br>export QDRANT_API_KEY=&quot;...&quot;<br>export GEMINI_API_KEY=&quot;...&quot;</pre><h3>Step 1: Dependencies</h3><p>BOM manages the langchain4j modules. Only langchain4j-google-genai is new versus the PDF post.</p><pre>&lt;dependencyManagement&gt;<br>  &lt;dependencies&gt;<br>    &lt;dependency&gt;<br>      &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;<br>      &lt;artifactId&gt;langchain4j-bom&lt;/artifactId&gt;<br>      &lt;version&gt;1.17.1&lt;/version&gt;<br>      &lt;type&gt;pom&lt;/type&gt;<br>      &lt;scope&gt;import&lt;/scope&gt;<br>    &lt;/dependency&gt;<br>  &lt;/dependencies&gt;<br>&lt;/dependencyManagement&gt;<br><br>&lt;dependencies&gt;<br>  &lt;dependency&gt;&lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;&lt;artifactId&gt;langchain4j&lt;/artifactId&gt;&lt;/dependency&gt;<br>  &lt;dependency&gt;&lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;&lt;artifactId&gt;langchain4j-document-parser-apache-tika&lt;/artifactId&gt;&lt;/dependency&gt;<br>  &lt;dependency&gt;&lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;&lt;artifactId&gt;langchain4j-embeddings-all-minilm-l6-v2&lt;/artifactId&gt;&lt;/dependency&gt;<br>  &lt;dependency&gt;&lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;&lt;artifactId&gt;langchain4j-qdrant&lt;/artifactId&gt;&lt;/dependency&gt;<br>  <br>  &lt;!-- NEW: Gemini via the Google GenAI SDK --&gt;<br>  &lt;dependency&gt;&lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;&lt;artifactId&gt;langchain4j-google-genai&lt;/artifactId&gt;&lt;/dependency&gt;<br>  <br>  &lt;!-- PDF text + page rendering, OCR, logging --&gt;<br>  &lt;dependency&gt;&lt;groupId&gt;org.apache.pdfbox&lt;/groupId&gt;&lt;artifactId&gt;pdfbox&lt;/artifactId&gt;&lt;version&gt;3.0.5&lt;/version&gt;&lt;/dependency&gt;<br>  &lt;dependency&gt;&lt;groupId&gt;net.sourceforge.tess4j&lt;/groupId&gt;&lt;artifactId&gt;tess4j&lt;/artifactId&gt;&lt;version&gt;5.14.0&lt;/version&gt;&lt;/dependency&gt;<br>  &lt;dependency&gt;&lt;groupId&gt;org.slf4j&lt;/groupId&gt;&lt;artifactId&gt;slf4j-simple&lt;/artifactId&gt;&lt;version&gt;2.0.16&lt;/version&gt;&lt;/dependency&gt;<br>&lt;/dependencies&gt;</pre><p>If the BOM doesn’t resolve the dependency for Google GenAI, use the following (the versions change faster now. Check on Maven Central if this still doesn’t work):</p><pre>&lt;dependency&gt;<br>    &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;<br>    &lt;artifactId&gt;langchain4j-google-genai&lt;/artifactId&gt;<br>    &lt;version&gt;1.17.2-beta27&lt;/version&gt;<br>&lt;/dependency&gt;</pre><h3>Step 2: The embedding model</h3><p>Runs locally, no API key. Its dimension (384) is what the collection must match.</p><pre>static final EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();<br><br>int vectorDimension = embeddingModel.dimension();   // 384</pre><h3>Step 3: Connect to hosted Qdrant</h3><p>Same host, gRPC port 6334, but TLS on and the API key set — that is the only difference from a local instance (refer to section 0 to get the keys).</p><pre>static QdrantClient openQdrant() {<br>    return new QdrantClient(<br>        QdrantGrpcClient.newBuilder(qdrantHost, 6334, true)   // true = TLS, for Qdrant Cloud<br>                        .withApiKey(qdrantApiKey)<br>                        .build());<br>}</pre><h3>Step 4: Create the collection</h3><p>Size it to the embedding model, compare by Cosine. Wipe any old copy first so re-runs start clean.</p><pre>static void ensureCollection(QdrantClient qdrant, int vectorDimension) throws Exception {<br>    if (qdrant.collectionExistsAsync(collectionName).get()) {<br>        qdrant.deleteCollectionAsync(collectionName).get();<br>    }<br>    qdrant.createCollectionAsync(<br>            collectionName,<br>            VectorParams.newBuilder()<br>                        .setDistance(Distance.Cosine)<br>                        .setSize(vectorDimension)<br>                        .build()<br>    ).get();<br>}</pre><h3>Step 5: Build the embedding store</h3><p>This is the LangChain4j handle you read from and write to. It points at the collection you just made.</p><pre>static EmbeddingStore&lt;TextSegment&gt; buildStore() {<br>    var builder = QdrantEmbeddingStore.builder()<br>                                      .host(qdrantHost)<br>                                      .port(qdrantPort)<br>                                      .useTls(qdrantUseTls)<br>                                      .collectionName(collectionName);<br>    if (qdrantApiKey != null &amp;&amp; !qdrantApiKey.isBlank()) {<br>        builder.apiKey(qdrantApiKey);<br>    }<br>    return builder.build();<br>}</pre><h3>Step 6: Read the PDF page by page</h3><p>Read one page at a time so page numbers survive. If a page has almost no text, it is a scan, so render it and OCR it.</p><pre>for (int pageNumber = 1; pageNumber &lt;= totalPages; pageNumber++) {<br>    stripper.setStartPage(pageNumber);<br>    stripper.setEndPage(pageNumber);              // read just this one page<br>    String text = stripper.getText(pdf).strip();<br>    String extraction = &quot;text&quot;;<br>    if (text.length() &lt; MIN_TEXT_CHARS_PER_PAGE) {<br>        if (renderer == null) renderer = new PDFRenderer(pdf);<br>        text = ocrPage(renderer, pageNumber - 1).strip();   // the page is a scan<br>        extraction = &quot;ocr&quot;;<br>    }<br>    rawText[pageNumber - 1] = text;<br>    method[pageNumber - 1]  = extraction;<br>}</pre><h3>Step 7: Strip repeated headers and footers</h3><p>Any line that shows up on most pages is the running header/footer, not content. Detect it once, drop it everywhere.</p><pre>Set&lt;String&gt; boilerplate = detectRepeatedLines(rawText);   // lines on &gt;= 60% of pages<br>String cleaned = removeBoilerplate(raw, boilerplate);     // also drops &quot;Page 3 of 5&quot;</pre><h3>Step 8: Tag every page with where it came from</h3><p>Each page becomes a Document carrying its source file and page number. LangChain4j copies this onto every chunk, so the citation follows the text.</p><pre>Metadata metadata = new Metadata();<br>metadata.put(&quot;source&quot;, file.getFileName().toString());<br>metadata.put(&quot;page&quot;, pageNumber);<br>metadata.put(&quot;total_pages&quot;, totalPages);<br>metadata.put(&quot;extraction&quot;, method[pageNumber - 1]);<br>pages.add(Document.from(cleaned, metadata));</pre><h3>Step 9: Chunk the pages</h3><p>Split each page into overlapping segments. The metadata rides along automatically.</p><pre>DocumentSplitter splitter = DocumentSplitters.recursive(MAX_SEGMENT_CHARS, SEGMENT_OVERLAP_CHARS);<br>List&lt;TextSegment&gt; segments = new ArrayList&lt;&gt;();<br>for (Document document : documents) {<br>    segments.addAll(splitter.split(document));<br>}</pre><h3>Step 10: Embed and store, in batches</h3><p>Turn each chunk into a vector and upsert (update or insert) it. Batching keeps memory flat on big files.</p><pre>for (int start = 0; start &lt; segments.size(); start += EMBED_BATCH_SIZE) {<br>    List&lt;TextSegment&gt; batch = segments.subList(start, Math.min(start + EMBED_BATCH_SIZE, segments.size()));<br>    List&lt;Embedding&gt; vectors = embeddingModel.embedAll(batch).content();<br>    embeddingStore.addAll(vectors, batch);<br>}</pre><p>Ingestion is done. The rest is querying.</p><h3>Step 11: Retrieve the nearest chunks</h3><p>Embed the question the same way, then ask Qdrant for the closest chunks above a score threshold.</p><pre>Embedding queryVector = embeddingModel.embed(question).content();<br>EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()<br>                                                       .queryEmbedding(queryVector)<br>                                                       .maxResults(MAX_RESULTS)<br>                                                       .minScore(MIN_SCORE)<br>                                                       .build();<br>List&lt;EmbeddingMatch&lt;TextSegment&gt;&gt; matches = embeddingStore.search(request).matches();</pre><h3>Step 12: Assemble the RAG prompt</h3><p>Question + retrieved passages, each tagged with its citation, and an instruction to answer only from that context.</p><pre>StringBuilder input = new StringBuilder();<br>input.append(&quot;Answer the question using only the context below.\n&quot;);<br>input.append(&quot;Cite the source file and page for every fact you use.\n\n&quot;);<br>input.append(&quot;QUESTION:\n&quot;).append(question).append(&quot;\n\nCONTEXT:\n&quot;);<br>int n = 1;<br>for (EmbeddingMatch&lt;TextSegment&gt; hit : matches) {<br>    Metadata m = hit.embedded().metadata();<br>    String citation = m.containsKey(&quot;page&quot;) ? m.getString(&quot;source&quot;) + &quot; p.&quot; + m.getInteger(&quot;page&quot;)<br>                                            : m.getString(&quot;source&quot;);<br>    input.append(&quot;[&quot;).append(n++).append(&quot;] &quot;).append(citation).append(&quot;\n&quot;);<br>    input.append(hit.embedded().text().strip()).append(&quot;\n\n&quot;);<br>}</pre><h3>Step 13: Hand it to Gemini</h3><p>The one line the previous post left out. Build the model once, send it the prompt, print the answer.</p><pre>ChatModel llm = GoogleGenAiChatModel.builder()<br>        .apiKey(System.getenv(&quot;GEMINI_API_KEY&quot;))<br>        .modelName(&quot;gemini-3.5-flash&quot;)<br>        .build();<br><br>String answer = llm.chat(prompt);   <br>System.out.println(answer.strip());</pre><p>That is the whole loop: read → chunk → embed → store → retrieve → answer.</p><h3>The complete code</h3><pre>package com.gupta.compele_rag_appl;<br><br>import dev.langchain4j.data.document.Document;<br>import dev.langchain4j.data.document.DocumentSplitter;<br>import dev.langchain4j.data.document.Metadata;<br>import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;<br>import dev.langchain4j.data.document.parser.apache.tika.ApacheTikaDocumentParser;<br>import dev.langchain4j.data.document.splitter.DocumentSplitters;<br>import dev.langchain4j.data.embedding.Embedding;<br>import dev.langchain4j.data.segment.TextSegment;<br>import dev.langchain4j.model.chat.ChatModel;<br>import dev.langchain4j.model.embedding.EmbeddingModel;<br>import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;<br>import dev.langchain4j.model.google.genai.GoogleGenAiChatModel;<br>import dev.langchain4j.store.embedding.EmbeddingMatch;<br>import dev.langchain4j.store.embedding.EmbeddingSearchRequest;<br>import dev.langchain4j.store.embedding.EmbeddingStore;<br>import dev.langchain4j.store.embedding.qdrant.QdrantEmbeddingStore;<br>import io.qdrant.client.QdrantClient;<br>import io.qdrant.client.QdrantGrpcClient;<br>import io.qdrant.client.grpc.Collections.Distance;<br>import io.qdrant.client.grpc.Collections.VectorParams;<br>import net.sourceforge.tess4j.Tesseract;<br>import org.apache.pdfbox.Loader;<br>import org.apache.pdfbox.pdmodel.PDDocument;<br>import org.apache.pdfbox.rendering.ImageType;<br>import org.apache.pdfbox.rendering.PDFRenderer;<br>import org.apache.pdfbox.text.PDFTextStripper;<br><br>import java.awt.image.BufferedImage;<br>import java.nio.file.Files;<br>import java.nio.file.Path;<br>import java.util.*;<br><br>public class RagOverPdf {<br><br>    static final String qdrantHost = env(&quot;QDRANT_CLUSTER_ENDPOINT&quot;, &quot;localhost&quot;);<br>    static final String qdrantApiKey = System.getenv(&quot;QDRANT_API_KEY&quot;); // null for a local instance<br><br>    static final int qdrantPort = 6334;<br>    static final boolean qdrantUseTls = true;     // true for Qdrant Cloud<br>    static final String collectionName = &quot;worldcup_2026&quot;;<br>    static final Path inputFile = Path.of(&quot;C:\\code\\LangChain4jDemo\\src\\main\\resources\\pdf\\fifa-world-cup-2026-knockout-log.pdf&quot;);<br>    static final String tessdataDir = &quot;/usr/share/tesseract-ocr/5/tessdata&quot;;<br><br>    // Indexing behavior. These are constants, not config, because they decide<br>    // what gets embedded: change any of them and you must re-index.<br>    static final int MAX_SEGMENT_CHARS = 900;<br>    static final int SEGMENT_OVERLAP_CHARS = 150;<br>    static final int EMBED_BATCH_SIZE = 64;<br><br>    // Retrieval behavior.<br>    static final int MAX_RESULTS = 2;<br>    static final double MIN_SCORE = 0.30;<br><br>    // A page that yields fewer usable characters than this is treated as scanned<br>    // and sent to OCR. Raise it for dense layouts, lower it for sparse forms.<br>    static final int MIN_TEXT_CHARS_PER_PAGE = 80;<br>    static final int OCR_DPI = 300;<br>    static final String OCR_LANGUAGE = &quot;eng&quot;;<br><br>    // Reuse one embedding model instance. It is thread safe and loading it is expensive.<br>    static final EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();<br><br>    public static void main(String[] args) throws Exception {<br>        if (!Files.isRegularFile(inputFile)) {<br>            System.err.println(&quot;Input file not found: &quot; + inputFile.toAbsolutePath()<br>                               + &quot;\nPut fifa-world-cup-2026-knockout-log.pdf next to this program, or edit inputFile.&quot;);<br>            return;<br>        }<br><br>        int vectorDimension = embeddingModel.dimension();<br><br>        // The LLM. This is the step the previous post left out.<br>        ChatModel llm = GoogleGenAiChatModel.builder()<br>                .apiKey(System.getenv(&quot;GEMINI_API_KEY&quot;))<br>                .modelName(&quot;gemini-3.5-flash&quot;)<br>                .build();<br><br>        try (QdrantClient qdrant = openQdrant()) {<br>            ensureCollection(qdrant, vectorDimension);<br>            EmbeddingStore&lt;TextSegment&gt; embeddingStore = buildStore();<br><br>            // ---- Extract: one Document per PDF page (Tika for other formats) ----<br>            List&lt;Document&gt; documents;<br>            if (isPdf(inputFile)) {<br>                documents = extractPdfPages(inputFile);<br>            } else {<br>                documents = List.of(extractWithTika(inputFile));<br>            }<br>            long ocrPages = documents.stream()<br>                                     .filter(d -&gt; &quot;ocr&quot;.equals(d.metadata().getString(&quot;extraction&quot;)))<br>                                     .count();<br><br>            // ---- Split: chunk each Document. Metadata is copied onto every segment. ----<br>            DocumentSplitter splitter = DocumentSplitters.recursive(MAX_SEGMENT_CHARS, SEGMENT_OVERLAP_CHARS);<br>            List&lt;TextSegment&gt; segments = new ArrayList&lt;&gt;();<br>            for (Document document : documents) {<br>                segments.addAll(splitter.split(document));<br>            }<br>            System.out.printf(&quot;Split %d page(s) into %d chunks (max %d chars, overlap %d).%n&quot;,<br>                              documents.size(), segments.size(), MAX_SEGMENT_CHARS, SEGMENT_OVERLAP_CHARS);<br><br>            // ---- Embed and upsert in batches ----<br>            int stored = 0;<br>            for (int start = 0; start &lt; segments.size(); start += EMBED_BATCH_SIZE) {<br>                List&lt;TextSegment&gt; batch = segments.subList(start, Math.min(start + EMBED_BATCH_SIZE, segments.size()));<br>                List&lt;Embedding&gt; vectors = embeddingModel.embedAll(batch).content();<br>                embeddingStore.addAll(vectors, batch);<br>                stored += batch.size();<br>            }<br><br>            System.out.printf(&quot;Ingested %s: %d page(s) (%d via OCR), %d chunks stored in &#39;%s&#39;.%n&quot;,<br>                              inputFile.getFileName(), documents.size(), ocrPages, stored, collectionName);<br><br>            // ---- Ask a question, get an answer, repeat until &#39;exit&#39; ----<br>            System.out.println(&quot;\nAsk a question about the 2026 World Cup. Type &#39;exit&#39; to quit.&quot;);<br>            try (Scanner scanner = new Scanner(System.in)) {<br>                while (true) {<br>                    System.out.print(&quot;\n&gt; &quot;);<br>                    if (!scanner.hasNextLine()) break;<br>                    String question = scanner.nextLine().strip();<br>                    if (question.isEmpty()) continue;<br>                    if (question.equalsIgnoreCase(&quot;exit&quot;) || question.equalsIgnoreCase(&quot;quit&quot;)) break;<br>                    runQuery(embeddingStore, llm, question);<br>                }<br>            }<br>            System.out.println(&quot;Goodbye.&quot;);<br>        }<br>    }<br><br>    /**<br>     *   1. Embeds the question and retrieves the closest chunks from Qdrant,<br>     *   2. Builds the RAG payload (question + retrieved chunks with citations),<br>     *   3. Sends it to Gemini and prints the written answer,<br>     *   4. Prints the sources so the answer stays checkable.<br>     */<br>    static void runQuery(EmbeddingStore&lt;TextSegment&gt; embeddingStore, ChatModel llm, String question) {<br>        Embedding queryVector = embeddingModel.embed(question).content();<br>        EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()<br>                                                               .queryEmbedding(queryVector)<br>                                                               .maxResults(MAX_RESULTS)<br>                                                               .minScore(MIN_SCORE)<br>                                                               .build();<br>        List&lt;EmbeddingMatch&lt;TextSegment&gt;&gt; matches = embeddingStore.search(request).matches();<br><br>        if (matches.isEmpty()) {<br>            System.out.println(&quot;\nNothing in the document cleared the similarity threshold.&quot;);<br>            return;<br>        }<br><br>        String prompt = buildLlmInput(question, matches);<br>        String answer = llm.chat(prompt);<br><br>        System.out.println(&quot;\n===================== ANSWER =====================&quot;);<br>        System.out.println(answer.strip());<br>        System.out.println(&quot;==================================================&quot;);<br><br>        System.out.println(&quot;Sources:&quot;);<br>        for (EmbeddingMatch&lt;TextSegment&gt; hit : matches) {<br>            Metadata metadata = hit.embedded().metadata();<br>            String citation = metadata.containsKey(&quot;page&quot;)<br>                    ? metadata.getString(&quot;source&quot;) + &quot; p.&quot; + metadata.getInteger(&quot;page&quot;)<br>                    : metadata.getString(&quot;source&quot;);<br>            System.out.printf(&quot;  %.0f%% match  %s%n&quot;, hit.score() * 100, citation);<br>        }<br>    }<br><br>    /**<br>     * Assemble the message a RAG app hands to an LLM: a short instruction, the<br>     * user&#39;s question, and the retrieved passages, each tagged with its citation<br>     * so the model can attribute the facts it uses.<br>     */<br>    static String buildLlmInput(String question, List&lt;EmbeddingMatch&lt;TextSegment&gt;&gt; matches) {<br>        StringBuilder input = new StringBuilder();<br>        input.append(&quot;Answer the question using only the context below.\n&quot;);<br>        input.append(&quot;Cite the source file and page for every fact you use.\n\n&quot;);<br>        input.append(&quot;QUESTION:\n&quot;).append(question).append(&quot;\n\n&quot;);<br>        input.append(&quot;CONTEXT:\n&quot;);<br>        int n = 1;<br>        for (EmbeddingMatch&lt;TextSegment&gt; hit : matches) {<br>            Metadata metadata = hit.embedded().metadata();<br>            String citation = metadata.containsKey(&quot;page&quot;)<br>                    ? metadata.getString(&quot;source&quot;) + &quot; p.&quot; + metadata.getInteger(&quot;page&quot;)<br>                    : metadata.getString(&quot;source&quot;);<br>            input.append(&quot;[&quot;).append(n++).append(&quot;] &quot;).append(citation).append(&quot;\n&quot;);<br>            input.append(hit.embedded().text().strip()).append(&quot;\n\n&quot;);<br>        }<br>        return input.toString().strip() + &quot;\n&quot;;<br>    }<br><br>    /**<br>     * PDF path: extract page by page so page numbers survive, remove repeated<br>     * headers and footers, and OCR any page that has no text layer.<br>     */<br>    static List&lt;Document&gt; extractPdfPages(Path file) throws Exception {<br>        try (PDDocument pdf = Loader.loadPDF(file.toFile())) {<br>            int totalPages = pdf.getNumberOfPages();<br>            String[] rawText = new String[totalPages];<br>            String[] method = new String[totalPages];<br><br>            PDFTextStripper stripper = new PDFTextStripper();<br>            PDFRenderer renderer = null; // created lazily, only if a page needs OCR<br><br>            // Pass 1: pull the raw text of each page, with OCR fallback for scans.<br>            for (int pageNumber = 1; pageNumber &lt;= totalPages; pageNumber++) {<br>                stripper.setStartPage(pageNumber);<br>                stripper.setEndPage(pageNumber);<br>                String text = stripper.getText(pdf).strip();<br>                String extraction = &quot;text&quot;;<br>                if (text.length() &lt; MIN_TEXT_CHARS_PER_PAGE) {<br>                    if (renderer == null) renderer = new PDFRenderer(pdf);<br>                    text = ocrPage(renderer, pageNumber - 1).strip();<br>                    extraction = &quot;ocr&quot;;<br>                }<br>                rawText[pageNumber - 1] = text;<br>                method[pageNumber - 1] = extraction;<br>            }<br><br>            // Detect lines that repeat across most pages. Those are the running<br>            // header and footer, and they are noise, not content.<br>            Set&lt;String&gt; boilerplate = detectRepeatedLines(rawText);<br><br>            System.out.println(&quot;Extracting &quot; + file.getFileName() + &quot; (&quot; + totalPages + &quot; pages):&quot;);<br><br>            // Pass 2: build one clean Document per page, carrying its provenance.<br>            List&lt;Document&gt; pages = new ArrayList&lt;&gt;();<br>            for (int pageNumber = 1; pageNumber &lt;= totalPages; pageNumber++) {<br>                String raw = rawText[pageNumber - 1];<br>                String cleaned = removeBoilerplate(raw, boilerplate);<br><br>                System.out.printf(&quot;  page %d: %d chars via %s, %d chars after header/footer removal%n&quot;,<br>                                  pageNumber, raw.length(), method[pageNumber - 1], cleaned.length());<br><br>                if (cleaned.isBlank()) continue;<br><br>                Metadata metadata = new Metadata();<br>                metadata.put(&quot;source&quot;, file.getFileName().toString());<br>                metadata.put(&quot;path&quot;, file.toAbsolutePath().toString());<br>                metadata.put(&quot;page&quot;, pageNumber);<br>                metadata.put(&quot;total_pages&quot;, totalPages);<br>                metadata.put(&quot;extraction&quot;, method[pageNumber - 1]);<br>                pages.add(Document.from(cleaned, metadata));<br>            }<br>            return pages;<br>        }<br>    }<br><br>    /** Lines that appear on 60 percent or more of the pages are treated as running header/footer. */<br>    static Set&lt;String&gt; detectRepeatedLines(String[] pages) {<br>        Map&lt;String, Integer&gt; frequency = new HashMap&lt;&gt;();<br>        for (String page : pages) {<br>            Set&lt;String&gt; seenOnThisPage = new HashSet&lt;&gt;();<br>            for (String line : page.split(&quot;\\R&quot;)) {<br>                String trimmed = line.strip();<br>                if (!trimmed.isEmpty() &amp;&amp; seenOnThisPage.add(trimmed)) {<br>                    frequency.merge(trimmed, 1, Integer::sum);<br>                }<br>            }<br>        }<br>        int threshold = Math.max(2, (int) Math.ceil(pages.length * 0.6));<br>        Set&lt;String&gt; boilerplate = new HashSet&lt;&gt;();<br>        for (Map.Entry&lt;String, Integer&gt; entry : frequency.entrySet()) {<br>            if (entry.getValue() &gt;= threshold) boilerplate.add(entry.getKey());<br>        }<br>        return boilerplate;<br>    }<br><br>    /** Drop the detected boilerplate lines plus per-page markers like &quot;Page 3 of 5&quot;. */<br>    static String removeBoilerplate(String text, Set&lt;String&gt; boilerplate) {<br>        StringBuilder kept = new StringBuilder();<br>        for (String line : text.split(&quot;\\R&quot;)) {<br>            String trimmed = line.strip();<br>            if (trimmed.isEmpty()) continue;<br>            if (boilerplate.contains(trimmed)) continue;<br>            if (trimmed.matches(&quot;(?i)page \\d+ of \\d+&quot;)) continue;<br>            kept.append(trimmed).append(&quot;\n&quot;);<br>        }<br>        return kept.toString().strip();<br>    }<br><br>    /** Render one PDF page to an image and read it with Tesseract. */<br>    static String ocrPage(PDFRenderer renderer, int pageIndex) throws Exception {<br>        BufferedImage image = renderer.renderImageWithDPI(pageIndex, OCR_DPI, ImageType.RGB);<br>        Tesseract tesseract = new Tesseract();<br>        tesseract.setDatapath(tessdataDir);<br>        tesseract.setLanguage(OCR_LANGUAGE);<br>        return tesseract.doOCR(image);<br>    }<br><br>    /**<br>     * Office and text formats: Apache Tika auto detects and parses almost anything.<br>     * Tika does not expose reliable page numbers, so these are cited by source file.<br>     * Note: ApacheTikaDocumentParser is stateful, so build a fresh one per file.<br>     */<br>    static Document extractWithTika(Path file) {<br>        Document document = FileSystemDocumentLoader.loadDocument(file, new ApacheTikaDocumentParser());<br>        Metadata metadata = document.metadata();<br>        metadata.put(&quot;source&quot;, file.getFileName().toString());<br>        metadata.put(&quot;path&quot;, file.toAbsolutePath().toString());<br>        metadata.put(&quot;extraction&quot;, &quot;text&quot;);<br>        return document;<br>    }<br><br>    // ---- Qdrant plumbing ----<br><br>    static QdrantClient openQdrant() {<br>        QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder(qdrantHost,<br>                                                                           6334,<br>                                                                           true)<br>                                                               .withApiKey(qdrantApiKey)<br>                                                               .build());<br>        return client;<br>    }<br><br>    /** Create the collection sized to the embedding model. Optionally wipe it first. */<br>    static void ensureCollection(QdrantClient qdrant, int vectorDimension) throws Exception {<br>        if (qdrant.collectionExistsAsync(collectionName).get()) {<br>            qdrant.deleteCollectionAsync(collectionName).get();<br>        }<br>        qdrant.createCollectionAsync(<br>                collectionName,<br>                VectorParams.newBuilder()<br>                            .setDistance(Distance.Cosine)<br>                            .setSize(vectorDimension)<br>                            .build()<br>        ).get();<br>    }<br><br>    static EmbeddingStore&lt;TextSegment&gt; buildStore() {<br>        var builder = QdrantEmbeddingStore.builder()<br>                                          .host(qdrantHost)<br>                                          .port(qdrantPort)<br>                                          .useTls(qdrantUseTls)<br>                                          .collectionName(collectionName);<br>        if (qdrantApiKey != null &amp;&amp; !qdrantApiKey.isBlank()) {<br>            builder.apiKey(qdrantApiKey);<br>        }<br>        return builder.build();<br>    }<br><br>    // ---- helpers ----<br><br>    static boolean isPdf(Path file) {<br>        return file.getFileName().toString().toLowerCase().endsWith(&quot;.pdf&quot;);<br>    }<br><br>    static String env(String key, String fallback) {<br>        String value = System.getenv(key);<br>        return (value == null || value.isBlank()) ? fallback : value;<br>    }<br>}</pre><h3>Run it</h3><p>Point inputFile at your PDF and run RagOverPdf. Same question as the last post — now with a written answer:</p><pre>&gt; Why did Egypt not win the 2026 World Cup?<br><br>===================== ANSWER =====================<br>Egypt were knocked out in the Round of 16: they lost 3-2 to Argentina in<br>Atlanta on 7 July 2026, after beating Australia on penalties in the Round<br>of 32. So they were out before the quarterfinals.<br>[fifa-world-cup-2026-knockout-log.pdf p.5, p.3]<br>==================================================<br>Sources:<br>  71% match  fifa-world-cup-2026-knockout-log.pdf p.5<br>  69% match  fifa-world-cup-2026-knockout-log.pdf p.3</pre><h3>Summary</h3><p>Two weeks ago I set out to build a RAG app. The hard part wasn’t the code, but everything it assumed I was already aware of, such as, what a vector is, how closeness is measured, where to chunk, and how to cite the right page. So I broke it into a series, one concept at a time.</p><p>This is the final piece: no stories, just the code. Connect to hosted Qdrant, ingest a PDF page by page, embed with a local model, retrieve the nearest chunks, and hand them to Gemini for a sourced answer , each step a small block, with the full class at the end. Hope it helps you!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=21f5f5bc8767" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>