---
title: "Spring AI Modular RAG and TypeSafe Jev: Retrieve More, Keep Only What Answers - Angryecho"
description: "Spring AI: RAG Doc | Jev Doc | Demo In the previous article we introduced Spring AI TypeSafe and its Jev model: typed questions in, calibrated numbers out, in …"
canonical_url: "https://www.angryecho.com/c/Spring/p/P2TBBkCFpNuEP6OXiurPo"
meta:
  "og:description": "Spring AI: RAG Doc | Jev Doc | Demo In the previous article we introduced Spring AI TypeSafe and its Jev model: typed questions in, calibrated numbers out, in …"
  "og:image": "https://www.angryecho.com/_og/d/c_Post%2Ctitle_~5ZCQ5qe9572R%2Cdescription_~5ZCQ5qe95LiA5LiL77yM5bCx5LiK5ZCQ5qe9572R44CC%2CpostId_P2TBBkCFpNuEP6OXiurPo%2Crevision_~MjAyNi0xMC0yOFQwMjo1NjowMC4yNzc5NDRa%2Cp_Ii9jL1NwcmluZy9wL1AyVEJCa0NGcE51RVA2T1hpdXJQbyI%2Cs_QcuKJZZ2ILPAv-sg.png"
  "og:image:height": "630"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Spring AI Modular RAG and TypeSafe Jev: Retrieve More, Keep Only What Answers"
---

> Index of every page on this site: [llms.txt](https://www.angryecho.com/llms.txt)

# **Spring AI Modular RAG and TypeSafe Jev: Retrieve More, Keep Only What Answers**

**Spring AI:** [RAG Doc](https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html) | [Jev Doc](https://spring-ai-community.github.io/spring-ai-typesafe/latest/) | [Demo](https://github.com/tzolov/voxxeddays2026-demo/tree/main/05-1-modular-rag)

In the [previous article](https://spring.io/blog/2026/09/21/spring-ai-typesafe-structured-judgment) we introduced [Spring AI TypeSafe](https://github.com/spring-ai-community/spring-ai-typesafe) and its Jev model: typed questions in, calibrated numbers out, in a few hundred milliseconds. One row in that article's SPI table deserves its own post. `JevDocumentFilter` and `JevDocumentReranker` are `DocumentPostProcessor`s, which means they plug straight into Spring AI's **Modular RAG** pipeline.

This article puts the two together. Two LLM calls clean up and multiply the user's question before retrieval. Jev then judges every retrieved chunk before it reaches the prompt. The whole pipeline is one advisor builder.

> **💡 Demo**: The complete example is the `[05-1-modular-rag](https://github.com/tzolov/voxxeddays2026-demo/tree/main/05-1-modular-rag)` module. It sits next to `[05-rag](https://github.com/tzolov/voxxeddays2026-demo/tree/main/05-rag)`, the naive version, so you can diff the two. Every output quoted below is from a live run.

## Why Naive RAG Falls Short

The classic Spring AI RAG demo is a `QuestionAnswerAdvisor` over a vector store. It embeds the user's text, fetches similar chunks and stuffs them into the prompt. That works on stage and gets shaky in real life, for three reasons:

1. **Users don't type search queries.** They type *"I'm a Florida resident and heard a lot about storms last fall. Did Milton actually make landfall in my state, and where?"* Embedding that verbatim drags all the noise into the search.
2. **Similar is not the same as useful.** A vector store returns chunks that are *about* the topic. A reference list that mentions "Hurricane Milton" five times scores high and answers nothing.
3. **Retrieved text is untrusted input.** Whatever sits in your documents goes straight into the prompt, including text written to hijack the model.

We will answer exactly that question over a Wikipedia PDF about Hurricane Milton, and fix each problem with one pluggable component.

## Modular RAG in Spring AI

The `RetrievalAugmentationAdvisor`, from the `spring-ai-rag` module, implements the architecture described in [Modular RAG: Transforming RAG Systems into LEGO-like Reconfigurable Frameworks](https://arxiv.org/abs/2407.21059). From the outside it is just another advisor: it sits between your prompt and the model, runs the retrieval phases, and appends the result to the user message. The system prompt and the question pass through untouched.

![图片](https://raw.githubusercontent.com/spring-io/spring-io-static/refs/heads/main/blog/tzolov/20261006/spring-ai-rag-advisor-overview.png)

Inside, each phase is a small interface you can swap:

| Phase | Spring AI interface | Job | Used in this demo |
| --- | --- | --- | --- |
| Pre-retrieval | `QueryTransformer` | rewrite one query into a better one | `RewriteQueryTransformer` |
| Pre-retrieval | `QueryExpander` | turn one query into several | `MultiQueryExpander` |
| Retrieval | `DocumentRetriever` | fetch documents for one query | `VectorStoreDocumentRetriever` |
| Retrieval | `DocumentJoiner` | merge the results of all queries | `ConcatenationDocumentJoiner` (default) |
| Post-retrieval | `DocumentPostProcessor` | filter, rerank or compress documents | `JevDocumentFilter`, `JevDocumentReranker` |
| Generation | `QueryAugmenter` | put context and question into the prompt | `ContextualQueryAugmenter` |

Here is the full flow for one call:

![图片](https://raw.githubusercontent.com/spring-io/spring-io-static/refs/heads/main/blog/tzolov/20261006/spring-ai-modular-rag-jev-flow.png)

The retrieval branches run in parallel, one per query, and are joined before post-processing. Note one subtle detail: the post-processors and the augmenter see the user's **original** question, not the rewritten one. The rewrite only exists to improve search.

## Getting Started

Add the Spring AI RAG module and the two TypeSafe artifacts from the previous article. Spring AI's BOM manages the first one:

```
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-rag</artifactId>
</dependency>

<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>spring-ai-starter-typesafe</artifactId>
<version>0.3.0</version>
</dependency>

<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>typesafe-spring-ai</artifactId>
<version>0.3.0</version>
</dependency>
```

The starter auto-configures a `TypeSafeClient` bean from your API key:

```
spring.ai.typesafe.api-key=${TYPESAFE_API_KEY}
```

The demo also uses `spring-ai-pdf-document-reader` for ingestion and `spring-ai-starter-model-transformers` for local embeddings.

## The Pipeline

Ingestion is the same as in any Spring AI RAG application: read the PDF, split it into chunks, store them.

```
vectorStore.add(
TokenTextSplitter.builder().build().split(
new PagePdfDocumentReader(hurricaneDocs).read()));
```

The interesting part is a single builder. Each numbered comment is one brick from the diagram:

```
// Separate builder for the LLM-backed stages, so they don't inherit the main client's advisors
var ragClientBuilder = chatClientBuilder.clone();

var modularRag = RetrievalAugmentationAdvisor.builder()
// 1. Pre-retrieval: rewrite the chatty question into a search-friendly query
.queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(ragClientBuilder)
.build())
// 2. Pre-retrieval: expand it into several diverse queries (original included)
.queryExpander(MultiQueryExpander.builder()
.chatClientBuilder(ragClientBuilder)
.numberOfQueries(3)
.build())
// 3. Retrieval: similarity search per query; the default joiner dedups the union
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.5)
.topK(4)
.build())
// 4. Post-retrieval: Jev drops bad passages, then reranks and keeps the top 3
.documentPostProcessors(
JevDocumentFilter.builder(typeSafeClient).build(),
JevDocumentReranker.builder(typeSafeClient).topK(3).build())
// 5. Generation: stuff the surviving context into the prompt
.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(true)
.build())
.taskExecutor(taskExecutor)   // see "Things to know" below
.build();
```

Using it looks like any other advisor:

```
String answer = chatClient.prompt()
.advisors(modularRag)
.user("I'm a Florida resident and heard a lot about storms last fall. "
+ "Did Milton actually make landfall in my state, and where?")
.call()
.content();
```

`DocumentRetriever` and `DocumentPostProcessor` are both single-method interfaces, so a lambda is enough to log what flows through the pipeline. The demo wraps the retriever and adds two printing post-processors around the Jev stages; that is where the output below comes from.

## Jev in the Post-Retrieval Phase

Both components send one Jev call per passage, with the query and the passage as the **state**, and turn the answers into a decision.

[JevDocumentFilter](https://spring-ai-community.github.io/spring-ai-typesafe/latest/rag/JevDocumentFilter/) asks four `Noul` questions about every passage and applies them in order:

| Question | Default threshold | Outcome |
| --- | --- | --- |
| `contains_prompt_injection` | above `0.70` | excluded |
| `contradicts_query_premise` | above `0.70` | kept, tagged `CONFLICTING` |
| `is_relevant` | below `0.45` | excluded |
| `contains_answer_evidence` | above `0.55` | kept, otherwise excluded |

This is the [atomic questions](https://spring.io/blog/2026/09/21/spring-ai-typesafe-structured-judgment) idea from the previous article at work: four narrow questions, four thresholds, one call. Contradicting passages survive on purpose. If the user assumes something false, the model should see the evidence that says so. The classification is stored in the document metadata under `jev.classification`, and the thresholds are a `Policy` record you can override.

[JevDocumentReranker](https://spring-ai-community.github.io/spring-ai-typesafe/latest/rag/JevDocumentReranker/) asks a single question, *could this passage answer the query?*, sorts by the score and keeps the top K. The score lands in the metadata under `jev.rerank.score`. Its focus is deliberate: whether the passage states information that answers the query, *not merely whether it covers the same subject*. That is precisely the gap between similarity and usefulness.

Order matters. Filter first, so the reranker only pays for the survivors.

（节选）

> 原文： [Spring Blog｜Spring AI Modular RAG and TypeSafe Jev: Retrieve More, Keep Only What Answers](https://spring.io/blog/2026/10/02/spring-ai-modular-rag-typesafe-jev)，2026-10-02

排序方式 :

### **成为第一条评论！**

Nobody's responded to this post yet. Add your thoughts and get the conversation going.

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://www.angryecho.com/c/Spring/p/P2TBBkCFpNuEP6OXiurPo#post","@type":"DiscussionForumPosting","commentCount":0,"dateModified":"2026-10-28T02:56:00.277Z","datePublished":"2026-10-02T04:00:00.000Z","description":"Spring AI: RAG Doc | Jev Doc | Demo In the previous article we introduced Spring AI TypeSafe and its Jev model: typed questions in, calibrated numbers out, in …","headline":"Spring AI Modular RAG and TypeSafe Jev: Retrieve More, Keep Only What Answers","isPartOf":"https://www.angryecho.com/c/Spring","mainEntityOfPage":"https://www.angryecho.com/c/Spring/p/P2TBBkCFpNuEP6OXiurPo","text":"Spring AI: RAG Doc | Jev Doc | Demo\n\nIn the previous article we introduced Spring AI TypeSafe and its Jev model: typed questions in, calibrated numbers out, in a few hundred milliseconds. One row in that article's SPI table deserves its own post. JevDocumentFilter and JevDocumentReranker are DocumentPostProcessors, which means they plug straight into Spring AI's Modular RAG pipeline.\n\nThis article puts the two together. Two LLM calls clean up and multiply the user's question before retrieval. Jev then judges every retrieved chunk before it reaches the prompt. The whole pipeline is one advisor builder.\n\n\n\n💡 Demo: The complete example is the 05-1-modular-rag module. It sits next to 05-rag, the naive version, so you can diff the two. Every output quoted below is from a live run.\n\nWhy Naive RAG Falls Short\n\nThe classic Spring AI RAG demo is a QuestionAnswerAdvisor over a vector store. It embeds the user's text, fetches similar chunks and stuffs them into the prompt. That works on stage and gets shaky in real life, for three reasons:\n\n\n\n\n\nUsers don't type search queries. They type \"I'm a Florida resident and heard a lot about storms last fall. Did Milton actually make landfall in my state, and where?\" Embedding that verbatim drags all the noise into the search.\n\n\n\nSimilar is not the same as useful. A vector store returns chunks that are about the topic. A reference list that mentions \"Hurricane Milton\" five times scores high and answers nothing.\n\n\n\nRetrieved text is untrusted input. Whatever sits in your documents goes straight into the prompt, including text written to hijack the model.\n\nWe will answer exactly that question over a Wikipedia PDF about Hurricane Milton, and fix each problem with one pluggable component.\n\nModular RAG in Spring AI\n\nThe RetrievalAugmentationAdvisor, from the spring-ai-rag module, implements the architecture described in Modular RAG: Transforming RAG Systems into LEGO-like Reconfigurable Frameworks. From the outside it is just another advisor: it sits between your prompt and the model, runs the retrieval phases, and appends the result to the user message. The system prompt and the question pass through untouched.\n\n\n\nInside, each phase is a small interface you can swap:\n\n\n\n\n\n\n\nPhase\n\n\n\nSpring AI interface\n\n\n\nJob\n\n\n\nUsed in this demo\n\n\n\n\n\nPre-retrieval\n\n\n\nQueryTransformer\n\n\n\nrewrite one query into a better one\n\n\n\nRewriteQueryTransformer\n\n\n\n\n\nPre-retrieval\n\n\n\nQueryExpander\n\n\n\nturn one query into several\n\n\n\nMultiQueryExpander\n\n\n\n\n\nRetrieval\n\n\n\nDocumentRetriever\n\n\n\nfetch documents for one query\n\n\n\nVectorStoreDocumentRetriever\n\n\n\n\n\nRetrieval\n\n\n\nDocumentJoiner\n\n\n\nmerge the results of all queries\n\n\n\nConcatenationDocumentJoiner (default)\n\n\n\n\n\nPost-retrieval\n\n\n\nDocumentPostProcessor\n\n\n\nfilter, rerank or compress documents\n\n\n\nJevDocumentFilter, JevDocumentReranker\n\n\n\n\n\nGeneration\n\n\n\nQueryAugmenter\n\n\n\nput context and question into the prompt\n\n\n\nContextualQueryAugmenter\n\nHere is the full flow for one call:\n\n\n\nThe retrieval branches run in parallel, one per query, and are joined before post-processing. Note one subtle detail: the post-processors and the augmenter see the user's original question, not the rewritten one. The rewrite only exists to improve search.\n\nGetting Started\n\nAdd the Spring AI RAG module and the two TypeSafe artifacts from the previous article. Spring AI's BOM manages the first one:\n\n\u003Cdependency>\n\u003CgroupId>org.springframework.ai\u003C/groupId>\n\u003CartifactId>spring-ai-rag\u003C/artifactId>\n\u003C/dependency>\n\n\u003Cdependency>\n\u003CgroupId>org.springaicommunity\u003C/groupId>\n\u003CartifactId>spring-ai-starter-typesafe\u003C/artifactId>\n\u003Cversion>0.3.0\u003C/version>\n\u003C/dependency>\n\n\u003Cdependency>\n\u003CgroupId>org.springaicommunity\u003C/groupId>\n\u003CartifactId>typesafe-spring-ai\u003C/artifactId>\n\u003Cversion>0.3.0\u003C/version>\n\u003C/dependency>\n\nThe starter auto-configures a TypeSafeClient bean from your API key:\n\nspring.ai.typesafe.api-key=${TYPESAFE_API_KEY}\n\nThe demo also uses spring-ai-pdf-document-reader for ingestion and spring-ai-starter-model-transformers for local embeddings.\n\nThe Pipeline\n\nIngestion is the same as in any Spring AI RAG application: read the PDF, split it into chunks, store them.\n\nvectorStore.add(\nTokenTextSplitter.builder().build().split(\nnew PagePdfDocumentReader(hurricaneDocs).read()));\n\nThe interesting part is a single builder. Each numbered comment is one brick from the diagram:\n\n// Separate builder for the LLM-backed stages, so they don't inherit the main client's advisors\nvar ragClientBuilder = chatClientBuilder.clone();\n\nvar modularRag = RetrievalAugmentationAdvisor.builder()\n// 1. Pre-retrieval: rewrite the chatty question into a search-friendly query\n.queryTransformers(RewriteQueryTransformer.builder()\n.chatClientBuilder(ragClientBuilder)\n.build())\n// 2. Pre-retrieval: expand it into several diverse queries (original included)\n.queryExpander(MultiQueryExpander.builder()\n.chatClientBuilder(ragClientBuilder)\n.numberOfQueries(3)\n.build())\n// 3. Retrieval: similarity search per query; the default joiner dedups the union\n.documentRetriever(VectorStoreDocumentRetriever.builder()\n.vectorStore(vectorStore)\n.similarityThreshold(0.5)\n.topK(4)\n.build())\n// 4. Post-retrieval: Jev drops bad passages, then reranks and keeps the top 3\n.documentPostProcessors(\nJevDocumentFilter.builder(typeSafeClient).build(),\nJevDocumentReranker.builder(typeSafeClient).topK(3).build())\n// 5. Generation: stuff the surviving context into the prompt\n.queryAugmenter(ContextualQueryAugmenter.builder()\n.allowEmptyContext(true)\n.build())\n.taskExecutor(taskExecutor)   // see \"Things to know\" below\n.build();\n\nUsing it looks like any other advisor:\n\nString answer = chatClient.prompt()\n.advisors(modularRag)\n.user(\"I'm a Florida resident and heard a lot about storms last fall. \"\n+ \"Did Milton actually make landfall in my state, and where?\")\n.call()\n.content();\n\nDocumentRetriever and DocumentPostProcessor are both single-method interfaces, so a lambda is enough to log what flows through the pipeline. The demo wraps the retriever and adds two printing post-processors around the Jev stages; that is where the output below comes from.\n\nJev in the Post-Retrieval Phase\n\nBoth components send one Jev call per passage, with the query and the passage as the state, and turn the answers into a decision.\n\nJevDocumentFilter asks four Noul questions about every passage and applies them in order:\n\n\n\n\n\n\n\nQuestion\n\n\n\nDefault threshold\n\n\n\nOutcome\n\n\n\n\n\ncontains_prompt_injection\n\n\n\nabove 0.70\n\n\n\nexcluded\n\n\n\n\n\ncontradicts_query_premise\n\n\n\nabove 0.70\n\n\n\nkept, tagged CONFLICTING\n\n\n\n\n\nis_relevant\n\n\n\nbelow 0.45\n\n\n\nexcluded\n\n\n\n\n\ncontains_answer_evidence\n\n\n\nabove 0.55\n\n\n\nkept, otherwise excluded\n\nThis is the atomic questions idea from the previous article at work: four narrow questions, four thresholds, one call. Contradicting passages survive on purpose. If the user assumes something false, the model should see the evidence that says so. The classification is stored in the document metadata under jev.classification, and the thresholds are a Policy record you can override.\n\nJevDocumentReranker asks a single question, could this passage answer the query?, sorts by the score and keeps the top K. The score lands in the metadata under jev.rerank.score. Its focus is deliberate: whether the passage states information that answers the query, not merely whether it covers the same subject. That is precisely the gap between similarity and usefulness.\n\nOrder matters. Filter first, so the reranker only pays for the survivors.\n\n（节选）\n\n\n\n原文：Spring Blog｜Spring AI Modular RAG and TypeSafe Jev: Retrieve More, Keep Only What Answers，2026-10-02","url":"https://www.angryecho.com/c/Spring/p/P2TBBkCFpNuEP6OXiurPo","author":{"@type":"Person","name":"TopHub","url":"https://www.angryecho.com/u/THTI1862908"},"image":["https://raw.githubusercontent.com/spring-io/spring-io-static/refs/heads/main/blog/tzolov/20261006/spring-ai-rag-advisor-overview.png","https://raw.githubusercontent.com/spring-io/spring-io-static/refs/heads/main/blog/tzolov/20261006/spring-ai-modular-rag-jev-flow.png"],"interactionStatistic":[{"@type":"InteractionCounter","interactionType":"https://schema.org/LikeAction","userInteractionCount":0},{"@type":"InteractionCounter","interactionType":"https://schema.org/DislikeAction","userInteractionCount":0},{"@type":"InteractionCounter","interactionType":"https://schema.org/ViewAction","userInteractionCount":0},{"@type":"InteractionCounter","interactionType":"https://schema.org/ShareAction","userInteractionCount":0}]}]}
```

## Sitemap

See the full [sitemap](https://www.angryecho.com/sitemap.md) for all pages.
