A Retrieval-Augmented Generation (RAG) system that helps students query academic regulations and policies at Hanoi University of Science and Technology (HUST). The system processes Markdown-based regulation documents, stores them in a vector database, and uses a hybrid retrieval pipeline with reranking to provide accurate, context-grounded answers through a conversational chat interface.
✨ Key Features
Hybrid Search — Combines vector similarity search (ChromaDB) with BM25 keyword matching for both semantic and lexical retrieval
Reranking — Uses Qwen3-Reranker-8B via SiliconFlow API to re-score and sort retrieved documents by relevance
Small-to-Big Retrieval — Summarizes large tables with an LLM, embeds the summary for search, and returns the full original table at query time
RAGAS Evaluation — Built-in evaluation pipeline using the RAGAS framework with metrics like faithfulness, relevancy, precision, recall, and ROUGE scores
API endpoint: POST /api/chat with {"message": "your question"}
🐳 Docker Deployment
Quick Start (Docker Compose)
bash
1# 1. Make sure data/ folder exists (download first if needed)2python scripts/download_data.py
34# 2. Create .env with API keys5echo"SILICONFLOW_API_KEY=your_key"> .env
6echo"GROQ_API_KEY=your_key">> .env
78# 3. Build and run9docker compose up --build -d
1011# Access at http://localhost:8000
1# Login to ECR2aws ecr get-login-password --region ap-southeast-1 |\3docker login --username AWS --password-stdin <ACCOUNT_ID>.dkr.ecr.ap-southeast-1.amazonaws.com
45# Create repository (first time only)6aws ecr create-repository --repository-name hust-rag-api
78# Tag and push9docker tag hust-rag-api:latest <ACCOUNT_ID>.dkr.ecr.ap-southeast-1.amazonaws.com/hust-rag-api:latest
10docker push <ACCOUNT_ID>.dkr.ecr.ap-southeast-1.amazonaws.com/hust-rag-api:latest
Step 2 — Run on EC2:
bash
1# Pull image2docker pull <ACCOUNT_ID>.dkr.ecr.ap-southeast-1.amazonaws.com/hust-rag-api:latest
34# Upload data to EC25scp -r data/ ec2-user@<EC2_IP>:/home/ec2-user/data
67# Run container8docker run -d \9 -p 8000:8000 \10 -v /home/ec2-user/data:/app/data \11 -e GROQ_API_KEY=your_key \12 -e SILICONFLOW_API_KEY=your_key \13 --restart unless-stopped \14 --name hust-rag \15 hust-rag-api:latest
Docker Notes
The data/ directory is mounted as a volume — not baked into the image
API keys are passed via environment variables or .env file — never stored in the image
To update: rebuild image → push → pull on EC2 → restart container
📖 Usage Guide
Chat Interface
The Gradio chat interface supports natural language questions about HUST student regulations. Example questions:
Question
Topic
Sinh viên vi phạm quy chế thi thì bị xử lý như thế nào?
Exam violation penalties
Điều kiện để đổi ngành là gì?
Major transfer requirements
Làm thế nào để đăng ký hoãn thi?
Exam postponement registration
Debug Mode
To launch the debug interface that shows retrieved documents and relevance scores:
python core/gradio/gradio_rag.py
Building/Updating the Database
When you add, modify, or delete markdown files in data/data_process/, rebuild the database:
bash
1# Incremental update (only changed files)2python scripts/build_data.py
34# Force full rebuild5python scripts/build_data.py --force
67# Skip orphan deletion8python scripts/build_data.py --no-delete
The build script will:
Detect changed files via SHA-256 hash comparison
Delete chunks from removed files
Re-chunk and re-embed only modified files
Automatically invalidate the BM25 cache
🔧 Core Components
Chunking (core/rag/chunk.py)
Processes Markdown documents into searchable chunks:
Feature
Description
YAML Frontmatter Extraction
Parses metadata (document type, year, cohort, program) into chunk metadata
Heading-based Splitting
Uses MarkdownNodeParser to split by headings, preserving document structure
Table Extraction & Splitting
Extracts Markdown tables, splits large tables into chunks of 15 rows
Small-to-Big Pattern
Summarizes tables with LLM → embeds summary → links to parent (full table)
Small Chunk Merging
Merges chunks smaller than 200 characters with adjacent chunks
Metadata Enrichment
Extracts course names and codes from content using regex patterns
Configuration:
python
1CHUNK_SIZE =1500# Maximum chunk size in characters2CHUNK_OVERLAP =150# Overlap between consecutive chunks3MIN_CHUNK_SIZE =200# Minimum chunk size (smaller chunks get merged)4TABLE_ROWS_PER_CHUNK =15# Maximum rows per table chunk
Embedding (core/rag/embedding_model.py)
Model: Qwen3-Embedding-4B via SiliconFlow API
Dimensions: 2048
Batch processing with configurable batch size (default: 16)
Rate limit handling with exponential backoff retry
Vector Store (core/rag/vector_store.py)
Backend: ChromaDB with LangChain integration
Parent node storage: Separate JSON file for Small-to-Big parent nodes (not embedded)
Content-based document IDs: SHA-256 hash of (source_file, header_path, chunk_index, content)
Metadata flattening: Converts complex metadata types to ChromaDB-compatible formats
Batch operations:add_documents() and upsert_documents() with configurable batch size
Retrieval (core/rag/retrieval.py)
Mode
Description
vector_only
Pure vector similarity search via ChromaDB
bm25_only
Pure keyword matching via BM25 (with lazy-load and disk caching)
hybrid
Ensemble of vector + BM25 with configurable weights (default: 0.5/0.5)
hybrid_rerank
Hybrid search followed by Qwen3-Reranker-8B reranking (default)
Small-to-Big at retrieval time: When a table summary node is retrieved, it is automatically swapped with the full parent table before returning results to the user.
Configuration:
python
1rerank_model ="Qwen/Qwen3-Reranker-8B"# Reranker model2initial_k =25# Documents fetched before reranking3top_k =5# Final documents returned4vector_weight =0.5# Weight for vector search5bm25_weight =0.5# Weight for BM25 search
Constructs prompts with a Vietnamese system prompt that enforces context-grounded answers
RAGContextBuilder combines retrieval and context preparation into a single step
📊 Evaluation
The project includes a RAGAS-based evaluation pipeline.
Running Evaluation
bash
1# Evaluate with default settings (10 samples, hybrid_rerank)2python scripts/run_eval.py
34# Custom sample size and mode5python scripts/run_eval.py --samples 50 --mode hybrid_rerank
67# Run all retrieval modes for comparison8python scripts/run_eval.py --samples 20 --mode all
Metrics
Metric
Description
Faithfulness
How well the answer is grounded in the retrieved context
Answer Relevancy
How relevant the answer is to the question
Context Precision
How precise the retrieved contexts are
Context Recall
How well the retrieved contexts cover the ground truth
ROUGE-1 / ROUGE-2 / ROUGE-L
N-gram overlap with ground truth answers
Results
Benchmark on HUST student regulation Q&A dataset (200 samples):
Metric
vector_only
bm25_only
hybrid
hybrid_rerank
Answer Relevancy
0.749
0.635
0.832
0.872
Context Precision
0.678
0.538
0.795
0.861
Context Recall
0.815
0.732
0.849
0.872
Faithfulness
0.912
0.938
0.942
0.937
ROUGE-1
0.557
0.533
0.576
0.598
ROUGE-2
0.408
0.385
0.421
0.439
ROUGE-L
0.526
0.508
0.545
0.567
Key takeaways:
hybrid_rerank achieves the best scores in 6 out of 7 metrics, confirming it as the optimal default retrieval mode.
Faithfulness is consistently high (>0.91 across all modes), meaning the LLM reliably grounds its answers in the provided context with minimal hallucination.
Reranking significantly boosts Context Precision (+60% over BM25-only, +8% over hybrid), demonstrating the value of Qwen3-Reranker in filtering irrelevant documents.
Hybrid search substantially outperforms single-mode retrieval, validating the ensemble approach of combining semantic (vector) and lexical (BM25) search.
Results are saved to evaluation/results/ as both JSON and CSV files with timestamps.
🧪 Testing
bash
1# Run all tests2pytest test/ -v
34# Run specific test module5pytest test/test_chunk.py -v
6pytest test/test_retrieval.py -v
78# Run with coverage9pytest test/ --cov=core --cov-report=term-missing