This article is published in English.
Practical notes: Spring AI Recipe: Filtering RAG Results with Metadata
Operable walkthrough of Practical notes: Spring AI Recipe: Filtering RAG Results with Metadata: contracts, checks, and drop-in code slots for teams shipping this pattern.
This walkthrough rebuilds the path from raw materials to a working system for: Spring AI Recipe: Filtering RAG Results with Metadata. The focus is operable steps, explicit checks, and code that you can drop into a repo without guessing intent. For the Overview stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.
On each player's turn, they place a dog, food bowl, or toy into
one of the three play yards. The game ends when a player has
placed their last dog into a yard. Then each player who hasn't
played in that round gets to place one more item into a yard
until the last player so that all players have an equal number
of turns.
On their turn, a player starts by moving any flies on the board
to the same space as an adjacent frog (effectively the frog has
eaten that fly). Then the player can either place 1 fly adjacent
to a frog on the board as a distraction so that the frog won't
jump or they may place a frog of their player color onto any
empty space. If the frog is adjacent to another frog (even if it
is their own), the adjacent frog will jump two spaces in any
direction away from the frog that was just placed. The direction
of the jump is decided by the current player. If the frog jumps
off the board, then that frog is out of play.
The game ends when all players have placed all of their frogs
(not counting frogs that have jumped off the board). The player
with the most frogs remaining on the board wins.
When it is a player's turn, they will draw a card from the
ingredients deck and add it to their hand. Then they may play
any ingredient card from their hand to add that ingredient to
one of up to three tacos in progress. Or, if they have a crunch
card in their hand, then they may play it to remove one
ingredient from any one opponent's taco. The first player to
completely build 3 tacos wins.
@Configuration
public class RagIngestionConfig {
private static final Logger logger =
LoggerFactory.getLogger(RagIngestionConfig.class);
@Value("${rag.documents}")
Resource[] documentResources;
@Bean
@Order(-1)
ApplicationRunner load(VectorStore vectorStore) {
return args -> {
for (Resource documentResource : documentResources) {
var filename = documentResource.getFilename();
logger.info("Loading document from {}.", filename);
var reader = new TikaDocumentReader(documentResource);
var splitter = TokenTextSplitter.builder().build();
vectorStore.accept(
splitter.apply(
reader.get()));
}
logger.info("Document loading complete.");
};
}
}
How can I help?
> What can I do on my turn?
- Draw a card from the ingredients deck and add it to your hand.
- Then you may either:
- Play any ingredient card from your hand to add that ingredient
to one of up to three tacos in progress, or
- Play a crunch card (if you have one) to remove one ingredient
from any one opponent's taco.
- On your turn you place a dog, a food bowl, or a toy into one of
the three play yards.
- The game ends when a player has placed their last dog; then players
who haven't yet played in that round get one more placement so all
players have equal turns.
Adding Metadata to the Documents
When working through the Adding Metadata to the stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Measure recall on a fixed question set before tuning prompts. Prompt churn rarely fixes a weak retrieval surface.
@Bean
@Order(-1)
ApplicationRunner load(VectorStore vectorStore) {
return args -> {
for (Resource documentResource : documentResources) {
var filename = documentResource.getFilename();
logger.info("Loading document from {}.", filename);
var reader = new TikaDocumentReader(documentResource);
var splitter = TokenTextSplitter.builder().build();
var titleTag =
filename.substring(0, filename.lastIndexOf('.'));
vectorStore.accept(
splitter.apply(
reader.get().stream()
.peek(document ->
document.getMetadata()
.put("title", titleTag))
.toList()));
}
logger.info("Document loading complete.");
};
}
title = camp-bowwow
Determining Which Game the User Is Asking About
When working through the Determining Which Game the stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Measure recall on a fixed question set before tuning prompts. Prompt churn rarely fixes a weak retrieval surface.
@Service
public class TitleHelper {
private final ChatClient chatClient;
public TitleHelper(ChatModel chatModel) {
this.chatClient = ChatClient.builder(chatModel).build();
}
public String determineGameTitle(String question) {
var title = chatClient.prompt()
.user(userSpec -> userSpec
.text("""
Your job is to try to determine the title of a game from
the question asked.
The game choices are:
- camp-bowwow
- frog-panic
- taco-truck
- unknown
If the game's title isn't explicitly mentioned in the
question, or you don't recognize the game's title, then
say "unknown".
The question is:
{question}
""")
.param("question", question))
.call()
.content();
return title.equals("unknown") ? null : title;
}
}
Filtering the Vector Search
When working through the Filtering the Vector Search stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Measure recall on a fixed question set before tuning prompts. Prompt churn rarely fixes a weak retrieval surface. When working through the Filtering the Vector Search stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.
@Bean
ApplicationRunner go(
ChatClient chatClient,
TitleHelper titleHelper) {
return args -> {
System.out.println("How can I help?\n");
try (Scanner scanner = new Scanner(System.in)) {
while (true) {
System.out.print("> ");
if (!scanner.hasNextLine()) break;
var input = scanner.nextLine();
if (input.isBlank()) continue;
var requestSpec = chatClient.prompt(input);
var gameTitle =
titleHelper.determineGameTitle(input);
var answer = requestSpec
.advisors(spec -> {
spec.param(
ChatMemory.CONVERSATION_ID,
"DEMO");
if (gameTitle != null) {
spec.param(
QuestionAnswerAdvisor.FILTER_EXPRESSION,
String.format(
"title == '%s'",
gameTitle));
}
})
.call()
.content();
System.out.println("\n - " + answer);
}
}
};
}
title == 'camp-bowwow'
Trying It Out
The Trying It Out stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Separate chunking policy from retrieval policy. Changing one should not force a rewrite of the other when quality metrics move.
How can I help?
> I'm playing Camp Bowwow. What do I do when it's my turn?
- On your turn you place one item - either a dog, a food bowl,
or a toy - into one of the three play yards.
(For reference: the game ends when a player has placed their last
dog; then any players who haven't yet played in that round each get
one more item turn in order until the last player, so all players
have equal turns.)
> I'm playing Frog Panic. What do I do when it's my turn?
- On your turn:
1. First, move any flies on the board onto the same space as an
adjacent frog (the frog eats that fly).
2. Then choose one action:
- Place 1 fly adjacent to a frog on the board as a distraction
(so that frog won't jump), or
- Place a frog of your color onto any empty space.
If the frog you place is adjacent to another frog (even your own),
that adjacent frog jumps two spaces away in any direction you choose.
If a frog jumps off the board it is out of play.
(For reference: the game ends when all players have placed all their
frogs; the player with the most frogs remaining on the board wins.)
More Than Similarity
The More Than Similarity stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Separate chunking policy from retrieval policy. Changing one should not force a rewrite of the other when quality metrics move.
Operational checklist
The Operational checklist stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope.
Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.
Separate chunking policy from retrieval policy. Changing one should not force a rewrite of the other when quality metrics move.
Add a smoke test that exercises the critical path in CI with fixtures, not live paid APIs, whenever budgets allow.
Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.
Separate chunking policy from retrieval policy. Changing one should not force a rewrite of the other when quality metrics move.
Before promoting the stack, freeze versions, capture a golden transcript for the critical path, and confirm rollback steps. Shared environments need rate limits, tenancy checks, and a clear owner for secret rotation. Prefer boring reliability over clever one-off demos.
Batch note for bef2f8a7cb72: keep provider keys out of the repo, set a per-session token ceiling, and store transcripts next to the eval fixtures so later model swaps stay comparable.
For the hardening note 0 stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish.
Hardening detail 0/963: measure wall time, error class, and token spend for this note, then decide whether to keep the change based on a fixed question set rather than anecdote.
When working through the hardening note 1 stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.
Hardening detail 1/963: measure wall time, error class, and token spend for this note, then decide whether to keep the change based on a fixed question set rather than anecdote.