Upload a PDF, ask questions about it, and get answers grounded in the document's content β with source citations. Built with Spring Boot 3.5, Spring AI 1.0.0, OpenRouter (OpenAI-compatible API) for chat + embeddings, H2 for document metadata, and a lightweight file-based in-memory vector store (no Docker, no external vector DB).
- Upload & index PDFs β pages are read, split into chunks, embedded, and stored.
- Ask questions β retrieval-augmented generation answers using only the selected document.
- Source citations β each answer shows the source file, page number, and snippet.
- H2 database β document metadata (id, filename, chunk count, timestamp) persisted in H2.
- Beautiful, responsive UI β drag-and-drop upload, document library, chat-style Q&A. Works on mobile and desktop.
- No Docker required β uses an in-memory
SimpleVectorStorepersisted to a JSON file.
| Layer | Technology |
|---|---|
| Runtime | Java 17+ (tested with JDK 21) |
| Framework | Spring Boot 3.5.0 |
| AI | Spring AI 1.0.0 (spring-ai-starter-model-openai) |
| LLM provider | OpenRouter (OpenAI-compatible) |
| PDF parsing | spring-ai-pdf-document-reader (PagePdfDocumentReader) |
| Vector store | SimpleVectorStore (in-memory, persisted to JSON) |
| Metadata DB | H2 (file-based) via Spring Data JPA |
| Frontend | Vanilla HTML/CSS/JS (no build step) |
- JDK 17 or newer (JDK 21 works fine)
- Maven 3.9+ (or use the included
mvnwwrapper) - An OpenRouter API key β https://openrouter.ai/keys
βΉοΈ Embeddings note: OpenRouter primarily serves chat models. If your account/key cannot call an embeddings model, embedding (the upload/index step) will fail. In that case use a provider that supports
/v1/embeddingsfor the embedding model (e.g. OpenAI) while keeping OpenRouter for chat. See Troubleshooting.
Windows (PowerShell):
$env:OPENROUTER_API_KEY="sk-or-..."macOS / Linux:
export OPENROUTER_API_KEY="sk-or-..."Windows (PowerShell):
mvn -U clean spring-boot:runmacOS / Linux:
./mvnw -U clean spring-boot:runOn Windows, use
mvn(ormvnw.cmd) β./mvnwis a Unix-style path and will not be recognized by PowerShell.
Visit http://localhost:8080 β upload a PDF β select it β ask questions.
- Upload a PDF in the left panel (drag-and-drop or browse). Indexing may take a few seconds depending on size.
- The document appears in Your documents and is auto-selected in the Active document dropdown.
- Type a question in the chat bar and press Ask.
- Expand sources under any answer to see the cited snippets and page numbers.
Health check.
{ "status": "ok" }Upload and index a PDF. Form field: file.
curl -F "file=@mydoc.pdf" http://localhost:8080/api/ingest-pdfResponse:
{ "documentId": "a1b2...", "filename": "mydoc.pdf", "chunkCount": 42, "status": "indexed" }List indexed documents.
[ { "documentId": "a1b2...", "filename": "mydoc.pdf", "createdAt": "2026-06-11T11:40:00Z", "chunkCount": 42 } ]Ask a question about one document.
curl -X POST http://localhost:8080/api/ask \
-H "Content-Type: application/json" \
-d '{"documentId":"a1b2...","question":"What is this document about?"}'Response:
{
"answer": "...",
"citations": [ { "source": "mydoc.pdf", "page": 3, "snippet": "..." } ],
"confidence": "medium",
"usedChunksCount": 5
}Document metadata is stored in a file-based H2 database at ./data/ragdb.mv.db.
The H2 web console is enabled for inspection:
- URL: http://localhost:8080/h2-console
- JDBC URL:
jdbc:h2:file:./data/ragdb - User:
saΒ· Password: (empty)
Table DOCUMENTS columns: document_id, filename, created_at, chunk_count.
| Key | Default | Description |
|---|---|---|
server.port |
8080 |
HTTP port |
spring.ai.openai.base-url |
https://openrouter.ai/api |
OpenRouter endpoint |
spring.ai.openai.api-key |
${OPENROUTER_API_KEY} |
Your API key (env var) |
spring.ai.openai.chat.options.model |
deepseek/deepseek-chat-v3-0324:free |
FREE chat model (runs on OpenRouter, no credits) |
spring.ai.model.chat |
openai |
Chat provider |
spring.ai.model.embedding |
transformers |
Embeddings run LOCALLY (ONNX all-MiniLM-L6-v2) β no credits |
app.rag.top-k |
5 |
Chunks used to build the answer |
app.rag.max-snippet-chars |
350 |
Max snippet length in citations |
app.vectorstore.path |
./data/vectorstore.json |
Vector store persistence file |
spring.servlet.multipart.max-file-size |
50MB |
Max PDF upload size |
The default chat model is free (deepseek/deepseek-chat-v3-0324:free), so the whole app runs with zero credits (embeddings are local, chat uses a free OpenRouter model). Free models are rate-limited (~50 requests/day on the free tier) and availability can change β browse current free ids at https://openrouter.ai/models?q=free. If you have credits and want higher quality, swap in a paid id like openai/gpt-4o-mini or anthropic/claude-3.5-sonnet.
pdf-rag-prd/
βββ pom.xml
βββ README.md
βββ src/main/
βββ java/com/example/pdfrag/
β βββ PdfRagPrdApplication.java # Spring Boot entry point
β βββ api/
β β βββ HealthController.java # GET /health
β β βββ RagController.java # /api/ingest-pdf, /api/documents, /api/ask
β β βββ GlobalExceptionHandler.java # JSON error responses
β βββ config/
β β βββ VectorStoreConfig.java # SimpleVectorStore bean (+ load on startup)
β βββ ingest/
β β βββ PdfIngestionService.java # read β split β embed β store
β βββ rag/
β β βββ RagService.java # retrieve β prompt β answer + citations
β βββ store/
β βββ DocumentEntity.java # JPA entity (H2)
β βββ DocumentRepository.java # Spring Data JPA repo
β βββ DocumentRegistry.java # metadata service
βββ resources/
βββ application.yml
βββ static/ # responsive UI
βββ index.html
βββ styles.css
βββ app.js
- Ingest β
PagePdfDocumentReaderreads the PDF (oneDocumentper page, withpage_numbermetadata).TokenTextSplitterchunks the text. Each chunk is tagged with adocumentId+source, embedded via OpenRouter, and added to the vector store (saved to JSON). - Ask β the question is embedded and used for similarity search. Because
SimpleVectorStoredoesn't support metadata filter expressions, the app retrieves a wider candidate set and filters bydocumentIdin Java, then keeps the top-K. - Answer β retrieved chunks become the context for a grounded prompt sent to the chat model. The response is returned with citations (source, page, snippet) and a simple confidence flag.
'dependencies.dependency.version' ... is missing
You're using an old Spring AI artifact name. This project uses the correct Spring AI 1.0.0 names:
spring-ai-starter-model-openai and spring-ai-pdf-document-reader. Run mvn -U clean package to refresh.
./mvnw is not recognized (Windows PowerShell)
Use mvn or mvnw.cmd instead of ./mvnw.
Embedding/index step fails (401 / model not found on embeddings)
OpenRouter may not serve an embeddings endpoint for your key. Point embeddings at a provider that does (e.g. OpenAI) while keeping OpenRouter for chat β set a separate base-url/key for the embedding client, or switch spring.ai.openai.* to OpenAI for embeddings.
Port 8080 in use
Change server.port in application.yml.
Reset everything
Stop the app and delete the ./data/ folder (removes H2 db + vector store).
- First request to a model may be slower (cold start / token setup).
- The vector store and H2 DB both live under
./data/and persist across restarts. - This is a starter/demo project β no authentication is included by design.
