A complete, working Retrieval-Augmented Generation (RAG) application in Java. It answers customer-support questions from a folder of Markdown help articles, shows the sources behind every answer, and politely says "I don't know" when the answer is not in its knowledge base.
Built with Spring Boot 4, Spring AI, OpenAI, and PostgreSQL + pgvector — no Python and no separate vector database.
📖 Full step-by-step tutorial: Spring AI in Production: Building a RAG Application with Spring Boot 4 and PostgreSQL pgvector
- Loads a knowledge base of Markdown articles into a vector store on first start.
- Retrieves the most relevant chunks for each question and answers using only that context.
- Returns citations (which articles the answer came from) with every response.
- Refuses to guess: out-of-scope questions get an honest "I don't have that information."
- Ships a simple web chat UI (Thymeleaf + plain JavaScript) — no build step, no Node.
| Framework | Spring Boot 4.0.6 |
| AI library | Spring AI 2.0.0-M8 |
| Vector store | PostgreSQL + pgvector |
| Chat + embeddings | OpenAI (gpt-4o-mini, text-embedding-3-small) |
| UI | Thymeleaf + vanilla JS |
| Build / Java | Gradle, Java 25 (Java 17+ works) |
⚠️ Spring AI 2.0.0-M8 is a milestone (pre-GA) release. It is the latest version that targets Spring Boot 4. Versions are pinned inbuild.gradle; move to2.0.0GA when it ships.
- Java 17 or newer (tested on Java 25):
java -version - PostgreSQL with the
pgvectorextension. The quickest way is the official Docker image, which already includes the extension:(Using your own Postgres? Install pgvector for it and rundocker run --name pgvector -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_DB=spring-ai-rag -p 5432:5432 -d pgvector/pgvector:pg17
CREATE EXTENSION vector;in the database.) - An OpenAI API key with a little credit (the whole demo costs a few cents).
-
Provide your OpenAI key. Copy the example env file and paste your key into it:
cp .env.example .env # then edit .env and set OPENAI_API_KEY=sk-....envis git-ignored, so your key is never committed. -
Run the app, loading the key from
.env:set -a && source .env && set +a && ./gradlew bootRun
On the first start it ingests the knowledge base (embeds the chunks via OpenAI and stores them in pgvector). This runs only once — restarts are fast and do not duplicate data.
-
Open http://localhost:8080 and ask a question, e.g. "What are the API rate limits on the Pro plan?" or "How do I reset my password?"
If port 8080 is taken, run on another port:
./gradlew bootRun --args='--server.port=8081'.
- Ingest —
KnowledgeBaseLoaderreadssrc/main/resources/kb/*.md, splits each file into chunks, and stores them. Spring AI embeds each chunk automatically. - Retrieve + Augment — a
QuestionAnswerAdvisoron theChatClientsearches pgvector for the chunks most similar to the question and adds them to the prompt as context. - Generate — the model answers using only that context. A strict system prompt keeps it honest.
- Cite — the retrieved documents are read back from the response metadata and shown as sources.
See the tutorial for the full explanation.
src/main/java/com/tucanoo/supportrag/
├── config/ChatClientConfig.java # ChatClient + QuestionAnswerAdvisor + system prompt
├── ingestion/KnowledgeBaseLoader.java # reads, chunks, embeds and stores the knowledge base
├── rag/RagService.java # asks the question, returns the answer + sources
└── web/ # ChatController + request/response records
src/main/resources/
├── kb/*.md # the knowledge base (12 sample articles)
├── templates/chat.html # the chat page
├── static/ # css + js
└── application.yaml # configuration
Key settings in src/main/resources/application.yaml:
| Property | Meaning |
|---|---|
spring.datasource.* |
your PostgreSQL connection |
spring.ai.openai.chat.options.model |
the chat model |
spring.ai.openai.embedding.options.model |
the embedding model |
spring.ai.vectorstore.pgvector.dimensions |
must match the embedding model (1536 for text-embedding-3-small) |
app.rag.top-k |
how many chunks to retrieve per question |
app.rag.similarity-threshold |
ignore matches weaker than this score |
Prefer to run everything locally with no API key? Swap the OpenAI starter for the Ollama starter and
update the models — remember to set dimensions: 768 for nomic-embed-text. The tutorial has the
full steps.
- The sample
application.yamlusespostgres/postgresas the database credentials — fine for a local demo, but change them for anything real. - This is example code for a tutorial. It is intentionally simple; see the tutorial's "Taking it to production" section for what to harden before shipping.