A domain-specific RAG application for pet health Q&A, built as a full backend product rather than a single notebook demo.
This project combines document ingestion, hybrid retrieval, reranking, history-aware query rewriting, local LLM answering, user authentication, and evaluation into one end-to-end system.
This repository demonstrates that I can build more than a prompt wrapper.
- I implemented a full RAG pipeline around real product constraints: retrieval quality, answer grounding, auth, sessions, admin operations, and evaluation.
- I built history-aware query rewriting to handle follow-up questions such as "Is it dangerous?" or "What about vomiting?" instead of only supporting isolated search queries.
- I integrated Milvus hybrid search with dense + sparse retrieval, then added cross-encoder reranking to improve final context quality.
- I included offline ingestion and evaluation scripts so the system can be measured, not just manually demoed.
Pet Doctor is a pet-health assistant that answers questions from a domain corpus rather than from the LLM's parametric memory alone.
The current system:
- loads local veterinary reference documents
- splits them into chunks and enriches them with chapter metadata
- stores dense and sparse vectors in Milvus
- rewrites ambiguous follow-up questions using conversation history when needed
- retrieves relevant chunks with hybrid search
- reranks candidates with a cross-encoder
- generates grounded answers with a local Ollama-hosted model
- exposes the workflow through a FastAPI app with user login, profile pages, and an admin console
This is intentionally designed as an application system, not just a model experiment.
From an interview perspective, this project shows practical backend and LLM engineering skills across:
- API design with FastAPI
- vector search and retrieval engineering
- ranking and prompt-grounding design
- evaluation with retrieval metrics and RAGAS
- Docker-based local infrastructure
- authentication and role-based access control
- project organization across ingestion, online serving, and offline evaluation
flowchart TD
A[Local Documents\nPDF / DOCX / TXT / MD] --> B[Ingestion Pipeline]
B --> C[Chunking + Metadata Enrichment]
C --> D[BGEM3 Dense + Sparse Embeddings]
D --> E[Milvus Collection\nDense Vector + Sparse Vector]
U[User Question] --> V[Session Auth]
V --> W[Query Rewrite Decision]
W --> X[History-Aware Query Rewrite]
X --> Y[Milvus Hybrid Retrieval]
Y --> Z[Cross-Encoder Reranker]
Z --> AA[Prompt Assembly]
AA --> AB[Ollama LLM]
AB --> AC[Grounded Answer]
AC --> AD[Conversation History Store]
AD --> X
| Layer | Tools | Why It Is Used |
|---|---|---|
| Backend API | FastAPI, Uvicorn | Lightweight and fast Python API framework for serving the app and static frontend |
| Retrieval Framework | LangChain, LangChain Milvus | Orchestration of embeddings, retrieval, and prompt pipeline |
| Vector Database | Milvus, etcd, MinIO | Production-style vector DB stack with explicit index and search parameter control |
| Embeddings | BAAI bge-m3, pymilvus[model], FlagEmbedding |
Supports hybrid dense + sparse retrieval in one retrieval pipeline |
| Reranking | bge-reranker-v2-m3 via sentence-transformers CrossEncoder |
Improves precision after initial retrieval |
| LLM Serving | Ollama with llama3.2 |
Local-first generation without relying on a paid online inference path |
| Data Layer | SQLAlchemy, SQLite | User and admin persistence with simple local setup |
| Auth | Session cookies, bcrypt | Practical stateful auth with password hashing and role separation |
| Evaluation | RAGAS, OpenAI embeddings, custom retrieval metrics | Measures both retrieval quality and downstream answer quality |
| Infra | Docker Compose | Runs Milvus stack and app services locally in a reproducible way |
The retrieval layer is not limited to semantic dense embeddings.
BGE-M3produces both dense and sparse representations- Milvus stores
dense_vectorandsparse_vectorfields in the same collection - the system supports weighted hybrid ranking and RRF-style fusion
- retrieval parameters are configurable through environment variables rather than hard-coded
Why this matters:
- dense search is good at semantic similarity
- sparse search is better at exact term matching and rare keywords
- hybrid retrieval is a practical compromise for domain QA systems where both matter
A common failure mode in RAG apps is that users ask short follow-up questions that only make sense in conversation context.
Examples:
- "Is it dangerous?"
- "What about vomiting?"
- "He seems weak and sleepy too."
This project addresses that with a two-stage rewrite design:
- a rule-based scoring step decides whether rewrite is necessary
- an LLM rewrite step only runs when the query is ambiguous and usable history exists
This reduces unnecessary rewrite calls while still improving retrieval for follow-up queries.
The system does not trust first-pass vector retrieval as the final answer context.
- Milvus retrieves a broader candidate set
- a cross-encoder reranker rescored the candidate chunks against the user query
- only the strongest contexts are passed into generation
This is important because many practical RAG failures come from "almost right" retrieval that needs better final ordering.
The ingestion pipeline does more than split raw text.
- supports
pdf,docx,txt, andmd - maps PDF pages to printed page numbers
- adds chapter metadata during ingestion
- keeps ingestion separate from online serving
This makes the corpus more explainable, easier to evaluate, and easier to debug.
This project includes user management because a usable assistant is more than a model endpoint.
- public registration creates regular users
- admins are stored separately from users
- session cookies are used for server-side auth
- admin-only routes exist for user management
- static pages are served for landing, chat, profile, and admin views
The repo includes both retrieval and answer-quality evaluation:
eval/retrieval_metrics.pyfor retrieval-focused scoringeval/ragas_eval.pyfor downstream RAG quality checkseval/test_set.jsonas a reproducible test set
That matters because it turns the project into an engineering system with measurable iteration, not just a demo.
- Documents are loaded from the local
data/directory. - Documents are chunked with overlap for retrieval.
- Chunks are embedded with
bge-m3. - Dense and sparse vectors are stored in Milvus.
- A user submits a question through
/ask. - The system decides whether the query needs rewriting.
- If needed, the query is rewritten using conversation history.
- Milvus runs hybrid retrieval across dense and sparse vector fields.
- Retrieved chunks are reranked with a cross-encoder.
- The top contexts are inserted into a constrained prompt.
- Ollama generates a short grounded answer.
- The turn is added to conversation history for future rewrites.
Current repository snapshot includes both retrieval and RAG evaluation outputs.
On the current 55-question test set using chapter-level relevance:
| Metric | Value |
|---|---|
| Macro Precision@4 | 0.7409 |
| Macro Recall@4 | 1.0000 |
| Macro F1@4 | 0.8336 |
Current eval/ragas_results.json snapshot:
| Metric | Value |
|---|---|
| Faithfulness | 0.9125 |
| Answer Relevancy | 0.7516 |
| Context Precision | 0.9197 |
| Context Recall | 0.7679 |
These numbers should be read as an evaluation baseline, not as a final production claim.
.
├── app
│ ├── core # settings and device selection
│ ├── db # SQLAlchemy engines and models
│ ├── deps # auth, container wiring, hybrid Milvus helpers
│ ├── pipeline # query rewrite logic
│ ├── routes # auth, QA, user, health endpoints
│ ├── services # RAG, retrieval, history, user services
│ └── static # landing page, chat UI, profile UI, admin UI
├── data # local domain documents
├── eval # test set and evaluation scripts
├── ingest.py # offline ingestion script
├── docker-compose.yml # Milvus stack + app service
└── requirements.txt
- Python 3.11+ recommended
- Docker and Docker Compose
- Ollama installed locally
- optional OpenAI API key for RAGAS evaluation
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtRecommended development workflow:
- run the application locally
- run Milvus infrastructure in Docker
docker compose up -d etcd minio milvusIf you want the app container too:
docker compose up --build -dollama pull llama3.2
ollama serveCreate a .env file in the project root if needed.
Common variables:
SESSION_SECRET_KEY=change-me
ADMIN_SEED_USERNAME=admin
ADMIN_SEED_EMAIL=admin@petdoctor.com
ADMIN_SEED_PASSWORD=admin123456
MILVUS_URI=http://localhost:19530
OLLAMA_BASE_URL=http://localhost:11434
HF_EMBED_MODEL=BAAI/bge-m3
RAG_HYBRID_ENABLED=true
OPENAI_API_KEY=your-key-for-ragas-onlypython ingest.pyuvicorn app.main:app --reloadThen open:
http://localhost:8000/http://localhost:8000/chathttp://localhost:8000/profilehttp://localhost:8000/admin
| Endpoint | Method | Purpose |
|---|---|---|
/register |
POST |
Create a user account |
/login |
POST |
User login |
/admin/login |
POST |
Admin login |
/me |
GET |
Return current authenticated user |
/ask |
POST |
Main question-answering endpoint |
/users/ |
GET |
Admin-only user listing |
/users/{id} |
GET/PUT/DELETE |
User profile management with role checks |
python eval/retrieval_metrics.py --k 4python eval/ragas_eval.py --k 4Notes:
- RAGAS evaluation requires
OPENAI_API_KEY - retrieval evaluation runs against the same local retrieval stack used by the app
I wanted explicit control over:
- vector fields
- hybrid retrieval
- indexing strategy
- search parameters
That makes the project closer to a production retrieval system than a toy notebook built around a default in-memory vector store.
For this project, server-side sessions were the simpler and more practical choice.
- easier local development
- straightforward role checks
- easy logout and session invalidation
JWT would make more sense for a distributed multi-service deployment, but it would add complexity that this project did not need yet.
Using Ollama keeps the online serving path local-first:
- lower cost during development
- easier offline experimentation
- clearer separation between retrieval quality and model API spend
I still use OpenAI in the evaluation pipeline because RAGAS metrics benefit from a stronger judge model.
I consider these important to state clearly in an interview setting.
- Conversation history is currently process-local and in-memory, not persisted in a database.
- Neo4j is provisioned in Docker but is not yet integrated into the online retrieval path.
- Retrieval evaluation is currently chapter-level; section-level or span-level relevance would be a stronger benchmark.
- The system is a pet-health information assistant, not a veterinary diagnosis system.
- Prompting is safety-aware, but this is still not a replacement for professional medical advice.
- persist conversation memory in a database instead of process memory
- add citation-aware responses with explicit source snippets and metadata
- compare dense-only, hybrid, and rerank configurations with stricter offline A/B evaluation
- add section-level retrieval labels for more sensitive retrieval measurement
- integrate Neo4j as an optional graph augmentation path for structured relationships
- add streaming responses and more robust observability
The app seeds an admin account on startup.
username: admin
email: admin@petdoctor.com
password: admin123456
Change these values before any real deployment.
This project is best understood as a complete domain-RAG backend application with:
- hybrid retrieval
- reranking
- history-aware query understanding
- grounded generation
- authentication and admin operations
- offline evaluation
That combination is the main point of the repository. The value is not just that it answers questions, but that it does so through a measurable and extensible retrieval system.