Course topics

By WebNest Studio

Spring Boot Tutorial

Retrieval Augmented Generation (RAG)

A general-purpose LLM does not know your company's refund policy, your internal wiki, or the PDF manual for your product — and if asked, it may invent a plausible but wrong answer. Fine-tuning a model on your data is expensive and goes out of date quickly. Retrieval Augmented Generation takes a simpler approach: before asking the model, search your own documents for the most relevant passages and include them in the prompt, with instructions to answer only from that context.

RAG is the most widely deployed pattern for enterprise AI. In this lesson you will build a full pipeline with Spring AI: ingest PDFs and web pages (ETL), split them into chunks, store them in a vector store, answer questions with QuestionAnswerAdvisor and RetrievalAugmentationAdvisor, cite sources, and handle questions the documents cannot answer.

The Two Phases of RAG

Ingestion (offline): read source documents, split them into chunks of a few hundred tokens, embed each chunk, and store it in a vector store with metadata. Retrieval and generation (per question): embed the user's question, retrieve the most similar chunks, insert them into the prompt as context, and let the model write the answer. The model's job changes from "remember facts" to "read and summarise the provided context", which it does far more reliably.

ETL: Readers, Transformers, Writers

Spring AI's ETL pipeline has three stages. DocumentReaders load content: PagePdfDocumentReader (spring-ai-pdf-document-reader), TikaDocumentReader for Word, HTML, PowerPoint and more (spring-ai-tika-document-reader), TextReader and JsonReader. DocumentTransformers such as TokenTextSplitter split long text into chunks. The VectorStore acts as the writer.

Chunk size matters: too large and the context is diluted with irrelevant text; too small and chunks lose meaning. 300–800 tokens with some overlap is a common starting point.

QuestionAnswerAdvisor

The quickest way to add RAG to a ChatClient is QuestionAnswerAdvisor (module spring-ai-advisors-vector-store). It runs a similarity search for the user's message and appends the results to the prompt with instructions to use them. Configure topK, similarityThreshold and filter expressions through a SearchRequest. You can change the filter per request, for example to restrict to the current user's tenant.

Modular RAG with RetrievalAugmentationAdvisor

For more control, RetrievalAugmentationAdvisor (module spring-ai-rag) lets you compose the pipeline: query transformers such as RewriteQueryTransformer (rewrite vague questions) and CompressionQueryTransformer (fold chat history into a standalone question), query expanders like MultiQueryExpander, a document retriever (VectorStoreDocumentRetriever), and a query augmenter (ContextualQueryAugmenter) that controls what happens when no relevant documents are found.

Grounding, Citations and "I don't know"

A good RAG system admits when it has no answer. Instruct the model to answer only from the context, and configure the augmenter to refuse when retrieval returns nothing, rather than letting the model fall back to guessing. Retrieved documents are available in the response metadata, so you can show "Sources: KB-2, Refund Policy p.3" under each answer — users trust answers they can verify.

Keeping the Index Fresh

Documents change. Store a stable id and a version or hash in metadata, re-ingest changed documents on a schedule or when they are edited, and delete chunks of removed documents with vectorStore.delete(filterExpression). Stale chunks produce confidently outdated answers.

Examples

Dependencies for a RAG application

Java
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-rag</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
Output
(All versions come from spring-ai-bom.)

Ingesting a folder of PDFs into the vector store

Java
@Service
public class PolicyIngestionService {

    private static final Logger log = LoggerFactory.getLogger(PolicyIngestionService.class);
    private final VectorStore vectorStore;

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

    public void ingest(Resource pdf, String docId) {
        // 1. Read: one Document per PDF page, with page number metadata
        List<Document> pages = new PagePdfDocumentReader(pdf).get();

        // 2. Transform: split pages into token-sized chunks
        List<Document> chunks = new TokenTextSplitter().apply(pages);
        chunks.forEach(c -> c.getMetadata().put("docId", docId));

        // 3. Write: remove the old version, then embed and store the new chunks
        vectorStore.delete(new FilterExpressionBuilder().eq("docId", docId).build());
        vectorStore.add(chunks);
        log.info("Ingested {} pages as {} chunks from {}", pages.size(), chunks.size(), docId);
    }
}

@Component
class IngestOnStartup implements ApplicationRunner {

    private final PolicyIngestionService ingestion;

    IngestOnStartup(PolicyIngestionService ingestion) {
        this.ingestion = ingestion;
    }

    @Override
    public void run(ApplicationArguments args) {
        ingestion.ingest(new ClassPathResource("docs/refund-policy.pdf"), "refund-policy");
        ingestion.ingest(new ClassPathResource("docs/student-handbook.pdf"), "student-handbook");
    }
}
Output
Ingested 4 pages as 11 chunks from refund-policy
Ingested 23 pages as 67 chunks from student-handbook

Answering questions with QuestionAnswerAdvisor

Java
@RestController
public class PolicyQaController {

    private final ChatClient chatClient;

    public PolicyQaController(ChatClient.Builder builder, VectorStore vectorStore) {
        this.chatClient = builder
            .defaultSystem("You answer questions for Webnest students using only the provided context. "
                + "If the context does not contain the answer, say you don't know.")
            .defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore)
                .searchRequest(SearchRequest.builder().topK(4).similarityThreshold(0.6).build())
                .build())
            .build();
    }

    @GetMapping("/policy/ask")
    public String ask(@RequestParam String q) {
        return chatClient.prompt().user(q).call().content();
    }
}
Output
GET /policy/ask?q=Can I get a refund after 10 days?
No. Refunds are available only within 7 days of purchase, and only if you have completed
less than 20% of the course.

GET /policy/ask?q=Who won the cricket world cup?
I don't know — that isn't covered in the Webnest policy documents.

Modular RAG with query rewriting, a no-answer policy and source citations

Java
@Service
public class HandbookAssistant {

    private final ChatClient chatClient;

    public HandbookAssistant(ChatClient.Builder builder, VectorStore vectorStore) {
        Advisor rag = RetrievalAugmentationAdvisor.builder()
            .queryTransformers(RewriteQueryTransformer.builder()
                .chatClientBuilder(builder.build().mutate())
                .build())
            .documentRetriever(VectorStoreDocumentRetriever.builder()
                .vectorStore(vectorStore)
                .similarityThreshold(0.55)
                .topK(5)
                .build())
            .queryAugmenter(ContextualQueryAugmenter.builder()
                .allowEmptyContext(false)   // refuse instead of guessing when nothing relevant is found
                .build())
            .build();

        this.chatClient = builder.defaultAdvisors(rag).build();
    }

    public record Answer(String text, List<String> sources) {}

    public Answer ask(String question) {
        ChatResponse response = chatClient.prompt()
            .user(question)
            .call()
            .chatResponse();

        // The advisor stores the retrieved documents in the response metadata
        List<Document> docs = response.getMetadata().get(RetrievalAugmentationAdvisor.DOCUMENT_CONTEXT);
        List<String> sources = docs == null ? List.of() : docs.stream()
            .map(d -> d.getMetadata().get("docId") + " p." + d.getMetadata().get("page_number"))
            .distinct()
            .toList();

        return new Answer(response.getResult().getOutput().getText(), sources);
    }
}
Output
ask("hey how long do i have to finish stuff before cert?")
(rewritten query: "What is the time limit to complete a course to receive a certificate?")
Answer[text=You must complete all lessons and quizzes within 12 months of enrolling to receive a certificate.,
       sources=[student-handbook p.7, student-handbook p.8]]

Common Mistakes

  • Embedding whole documents as one vector; retrieval then returns huge, unfocused context. Split into chunks.
  • Not instructing the model to answer only from context, so it mixes in invented facts.
  • Re-ingesting documents without deleting old chunks, leaving outdated and duplicated content in the index.
  • Ignoring multi-tenancy: without a tenant filter, one customer's documents can appear in another customer's answers.
  • Judging RAG quality by a few manual questions; build an evaluation set (see the testing lesson) and measure.

Key Points to Remember

  • RAG retrieves relevant chunks of your data and gives them to the model as context for each question.
  • Ingestion = read (PDF/Tika readers) → split (TokenTextSplitter) → embed and store (VectorStore).
  • QuestionAnswerAdvisor is the quick start; RetrievalAugmentationAdvisor composes rewriting, retrieval and augmentation.
  • Refuse to answer when nothing relevant is retrieved, and show sources from document metadata.
  • Keep the index fresh with stable document ids, deletion of old chunks and tenant filters.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.