A production-grade Retrieval-Augmented Generation system that answers Java developer questions using the Oracle Java SE 25 / JDK 25 API documentation as its sole source of truth — with inline citations back to the exact Javadoc section.
Question: "What does ArrayList.add(E) return?"
→ Answer: "add(E) returns true (as specified by Collection.add) [1]"
→ Cites: [1] https://docs.oracle.com/.../ArrayList.html#add(E)
→ Related: java.util.ArrayList#add(E), java.util.ArrayList#add(int,E)
Ask a question in plain English or with a precise symbol (ArrayList#add(int,E)) and get a grounded answer with citations — no hallucinated APIs, every claim traceable to official docs.
LLMs are confidently wrong about API contracts: return values, thrown exceptions, null-handling, since versions, and overload behavior. This system constrains the model to only answer from retrieved JDK 25 Javadoc, attaches a citation to every statement, and routes precise symbol queries straight to the source — so the answer is auditable.
- Hybrid retrieval — BM25 (exact identifiers) + dense embeddings (semantics), fused with Reciprocal Rank Fusion, then re-ranked by a cross-encoder.
- Symbol-aware routing — a query parser detects fully-qualified names, camelCase members, and method signatures, and does a direct DB lookup before falling back to search — eliminating retrieval noise on precise queries.
- Structure-aware chunking — one chunk = one Javadoc section (e.g. a single method-detail block), which matches how the docs are actually organized.
- Grounded generation with citations — Claude answers only from retrieved context and emits inline
[n]citations linking to the exact Javadoc URL. - Multimodal — optional CLIP-based image ingestion/search and a
/query-with-imageendpoint for diagram-augmented questions. - PDF ingestion — pull in supplementary PDFs (e.g. PostgreSQL manuals) alongside Javadoc.
- Web UI + REST API — a single-page chat front-end served at
/, plus a documented FastAPI backend with type-ahead suggestions. - Evaluation suite — gold-standard regression questions covering return values, exceptions, null contracts, inheritance,
sincequeries, and overload disambiguation.
Python 3.11 · FastAPI · FAISS · sentence-transformers (BGE) · rank_bm25 · cross-encoder (MS MARCO MiniLM) · CLIP · Anthropic Claude · SQLite · BeautifulSoup · PyMuPDF
┌────────────────────── ingest pipeline ──────────────────────┐
│ Crawler ─→ Parser ─→ Chunker ─→ Embedder ─→ Index │
│ (async, (Javadoc (1 chunk = (BGE) (FAISS + │
│ rate-ltd) HTML) 1 section) BM25 + SQLite)│
└──────────────────────────────────────────────────────────────┘
│
▼
POST /query ──→ QueryParser ──→ symbol lookup? ──→ BM25 ─┐
├─ RRF fusion ─→ cross-encoder rerank ─→ top-k
dense search ───────┘ │
▼
Claude (answer + inline citations)
| Concern | Decision | Rationale |
|---|---|---|
| Chunking | 1 chunk = 1 Javadoc section (not fixed token windows) | Javadoc is already structured; method-detail sections are the ideal retrieval unit |
| Retrieval | Hybrid BM25 + dense + RRF + cross-encoder | BM25 wins on exact identifiers; dense wins on semantics; the reranker arbitrates |
| Symbol routing | Parser detects FQN/member → direct DB lookup before hybrid search | Eliminates retrieval noise for precise queries like ArrayList#add(int,E) |
| Storage | SQLite (metadata) + FAISS (vectors) + pickle (BM25) | Zero-infrastructure; runs locally; swap to Postgres + pgvector for production |
| Embeddings | BAAI/bge-*-en-v1.5 |
Strong on technical text; BGE instruction-tuning improves recall |
| Reranker | cross-encoder/ms-marco-MiniLM-L-6-v2 |
Fast cross-encoder; good quality/latency trade-off |
| LLM | Claude Sonnet | Excellent instruction-following; accurate on API docs |
JavaDocRAG/
├── config.py # All tunable knobs (reads .env)
├── requirements.txt
├── .env.example
├── static/
│ └── index.html # Single-page chat web UI (served at /)
├── resource/ # Sample PDFs for PDF ingestion demos
├── src/
│ ├── cli/
│ │ ├── ingest.py # Crawl → parse → chunk → embed → index
│ │ ├── serve.py # FastAPI server
│ │ └── check_db.py # Inspect the indexed SQLite DB
│ ├── crawler/
│ │ ├── crawler.py # Async crawler with rate limiting
│ │ ├── parser.py # Javadoc HTML → RawSection objects
│ │ └── pdf_parser.py # PDF → text sections (PyMuPDF)
│ ├── chunker/
│ │ └── chunker.py # RawSection → token-aware Chunk
│ ├── indexer/
│ │ ├── db_store.py # SQLite read/write for chunks
│ │ ├── embedder.py # sentence-transformers wrapper
│ │ ├── image_embedder.py # CLIP image embeddings
│ │ ├── bm25_index.py # rank_bm25 wrapper
│ │ ├── faiss_store.py # FAISS wrapper
│ │ └── hybrid_retriever.py # BM25 + dense → RRF → rerank pipeline
│ ├── query/
│ │ └── query_parser.py # Symbol detection + intent classification
│ └── generator/
│ └── generator.py # Claude answer generation + citation building
└── tests/
├── eval.py # Regression evaluation suite
└── test_multimodal.py # Multimodal retrieval tests
# Python 3.11+ recommended
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtcp .env.example .env
# Edit .env and set ANTHROPIC_API_KEY# Quick smoke-test with the first 100 pages
python src/cli/ingest.py --max-pages 100
# Full ingest of all JDK 25 API pages (~4,000 pages)
python src/cli/ingest.py
# Re-build indexes without re-crawling (pages already in DB)
python src/cli/ingest.py --skip-crawl
# Ingest a supplementary PDF
python src/cli/ingest.py --pdf resource/postgresql-16-A4.pdfpython src/cli/serve.py
# Web UI: http://localhost:8000/
# Docs: http://localhost:8000/docscurl -s -X POST http://localhost:8000/query \
-H "Content-Type: application/json" \
-d '{"question": "What does ArrayList ensure about nulls?"}' \
| python -m json.toolExample response:
{
"question": "What does ArrayList ensure about nulls?",
"answer": "ArrayList permits null elements [1]...",
"citations": [
{
"index": 1,
"url": "https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/ArrayList.html#class-description",
"type_fqn": "java.util.ArrayList",
"section_title": "ArrayList – Description"
}
],
"related_apis": ["java.util.ArrayList", "java.util.List"],
"latency_ms": 1240.5
}python tests/eval.py --server http://localhost:8000 # against running server
python tests/eval.py --inline # in-process, no server| Method & path | Purpose |
|---|---|
POST /query |
Main Q&A endpoint (text) |
POST /query-with-image |
Q&A augmented with an uploaded image |
POST /chat |
Conversational endpoint |
GET /suggest?q=<partial> |
Type-ahead FQN/member suggestions |
GET /stats |
Index statistics |
GET /health |
Liveness / readiness check |
GET / |
Web UI |
POST /query body: { "question": string, "top_k": 8, "debug": false }
Response: { answer, citations[], related_apis[], latency_ms, debug_chunks? }
Key settings (full list in config.py / .env.example):
| Variable | Default | Description |
|---|---|---|
ANTHROPIC_API_KEY |
— | Required. Your Anthropic API key |
CLAUDE_MODEL |
claude-sonnet-4-6 |
Generation model |
EMBEDDING_MODEL |
BAAI/bge-small-en-v1.5 |
Embedding model |
RERANKER_MODEL |
cross-encoder/ms-marco-MiniLM-L-6-v2 |
Cross-encoder reranker |
BM25_TOP_K / DENSE_TOP_K |
50 / 50 |
Candidates before fusion |
RERANK_TOP_K |
8 |
Final chunks after reranking |
MAX_CONCURRENT / REQUEST_DELAY |
5 / 1.0 |
Crawler concurrency & politeness |
- JS-rendered content — the crawler fetches raw HTML only; standard Oracle Javadoc pages are static, so this is rarely an issue in practice.
- Crawl politeness — default 1 req/s avoids Oracle rate limits; raise
REQUEST_DELAYif you see 429s. - No incremental re-index — re-running ingest re-embeds all chunks; diff-by-content-hash is a planned improvement.
- Overload disambiguation — for heavily overloaded methods (e.g.
String.valueOf), all overloads are returned and answered with subheadings.
MIT — see LICENSE.