Spring AI 2.0 RAG: ingest PDFs, retrieve, rerank, answer, check
A retrieval-augmented-generation service on Spring Boot 4.1.1, Spring AI 2.0.1, Java 25 and PostgreSQL with
pgvector. It is the companion code for the two articles on ankurm.com
(the explanation and
the code tour), and it replaces the 1.x code those articles used to carry.
For upgrading an existing Spring AI 1.x project, see the
migration guide.
Every number in the articles and in docs/ comes from a file in docs/output/, and the tests that write those files
also assert them. The one big caveat is what is not real: the chat and embedding models in the tests are scripted stand-ins (next section).
What is real and what is scripted
| Piece |
In the tests |
In the shipped application |
PDF reading, chunking, PgVectorStore, PostgreSQL + pgvector, metadata filters |
real |
real |
RetrievalAugmentationAdvisor, QuestionAnswerAdvisor, FactCheckingEvaluator, ChatClient, HTTP layer, Micrometer |
real |
real |
| Embedding model |
HashingEmbeddingModel: words hashed into a vector |
OpenAI text-embedding-3-small |
| Chat model (answers, reranker ratings, judge verdicts) |
FakeChatModel: scripted from the prompt |
OpenAI gpt-4o |
So the tests show what Spring AI sends to the model and how this code handles each kind of reply. They do not show how a real model
answers, what similarity scores or thresholds suit a real embedding model, whether reranking improves answers, or how well a real judge catches
an invented claim. There is no relevance benchmark here. The application itself was never run against the OpenAI API in authoring.
Versions
|
|
| Java |
25 |
| Spring Boot |
4.1.1 |
| Spring AI |
2.0.1 (spring-ai-bom) |
| PostgreSQL / pgvector |
16 / 0.6.0 used for every transcript (Debian package). docker-compose.yml uses pgvector/pgvector:pg16, which was not run here |
Run it
| Endpoint |
What it does |
POST /api/ingest (multipart: file, tenantId, docType) |
reads, chunks, embeds and stores a PDF; returns ingested, updated or skipped |
POST /api/query ({"question": "...", "tenantId": "acme"}) |
answer, the chunks used with their rerank scores, grounded, and status (answered, ungrounded, no_context) |
GET /actuator/prometheus |
rag_* metrics |
Chapters
|
|
| 1. The shape of a RAG pipeline |
stages, the smallest thing that works, what is not tested |
| 2. Chunking |
TokenTextSplitter measured, a recursive chunker with overlap, a semantic chunker |
| 3. Ingestion |
PDF pages, whitespace, metadata, uploading twice |
| 4. Retrieval and reranking |
threshold, empty context, tenant filter and injection, LLM reranking |
| 5. Generation and the faithfulness check |
the judge, its four outcomes |
| 6. Observability and a production checklist |
metrics, alerts, checklist |
Captured output
Written by scripts/run-all.sh. Files 01-11 and 15 are written by the tests (the same test asserts the numbers); 12-14 by the scripts.
| File |
Shows |
Written by |
| 01-pipeline-beans.txt |
the beans of the running application |
EndToEndTest |
| 02-token-text-splitter.txt |
what TokenTextSplitter does to a fixed document |
ChunkingTest |
| 03-recursive-and-semantic-chunkers.txt |
the two chunkers of ours |
ChunkingTest |
| 04-pdf-pages-and-metadata.txt |
a PDF page as read, and tidied |
IngestionTest |
| 05-ingestion-idempotency.txt |
upload twice, edit, re-export, naive add() (PostgreSQL) |
IngestionTest |
| 06-retrieval-threshold-and-prompt.txt |
threshold, augmented prompt, empty context |
RetrievalTest |
| 07-tenant-filter-and-injection.txt |
tenant filter, filter-string injection, both stores |
TenantFilterTest |
| 08-reranking.txt |
reranker calls, latency, parse failure |
RerankTest |
| 09-faithfulness-check.txt |
the judge and the four outcomes |
FaithfulnessTest |
| 10-end-to-end.txt |
HTTP ingest and query, rows in PostgreSQL, metrics |
EndToEndTest |
| 11-question-answer-advisor.txt |
the smallest RAG and its prompts |
SimpleAdvisorTest |
| 12-api-facts.txt |
javap of the Spring AI classes used, and a count of any SemanticSearchCache class on the classpath |
scripts/capture-javap.sh |
| 13-dependencies.txt |
which artifact brings what |
scripts/capture-dependencies.sh |
| 14-legacy-1x.txt |
the 1.x article's code and configuration keys against 1.1.0 and 2.0.1 |
scripts/capture-legacy-compile.sh |
| 15-config-keys.txt |
every spring.ai.* key against the jars' metadata |
ConfigKeysTest |
Layout
Known limits
- The ingestion tracker is in memory (chapter 3).
- The reranker parses a bare integer and degrades silently when the model does not reply with one (chapter 4).
- Filtered queries were not tested at a size where pgvector's approximate index matters (chapter 4).
- The application has no authentication;
tenantId in the request body is trusted, which is a demonstration, not a design.
- Not run against a real model, real embeddings, or the
pgvector/pgvector Docker image.