A Multimodal Retrieval-Augmented Generation (RAG) system that indexes The Batch news articles together with their associated images and lets you ask natural-language questions via a Gradio UI. The stack is container-first: clone, fill-in a few environment variables, and spin everything up with a single docker compose command.
- Prerequisites
- Quick Start
- Environment Variables
- Running with Docker Compose
- Data Ingestion Pipeline
- Technical Documentation
- Development Workflow (without Docker)
- Troubleshooting
- Clean Up
- License
| Requirement | Minimum Version | Purpose |
|---|---|---|
| Docker | 24.0 (Engine v2) | Containers for the API, UI & Qdrant |
| Docker Compose | v2.x (built-in to Docker Desktop / CLI) | Or docker compose plugin |
| Git | any | To clone this repository |
Note for Windows users: Enable WSL 2 for best performance.
# Clone the repository
git clone https://github.com/Kusm0/Multimodal_RAG.git
cd Multimodal_RAG
# Create your local environment file
cp .env.example .env
# Then open .env in your editor and fill in the placeholders (see next section)
# Start the full stack (API + UI + Qdrant)
docker compose up -d --build
# Open the Gradio UI
# Wait until the containers report "UI running on [http://0.0.0.0:7860]"
# On macOS:
open http://localhost:7860
# Or simply visit the URL in your browserAll configuration is managed in the .env file. Duplicate the template and replace the <> placeholders with your own values:
cp .env.example .env| Variable | Description |
|---|---|
OPENAI_API_KEY |
Your OpenAI API key (for embeddings & LLM answers). |
QDRANT_URL |
URL of the Qdrant instance (by defult LEAVE IT EMPTY!). |
Running Qdrant in Docker vs. on a separate VM
- Same machine (default): Leave
USE_EXTERNAL_QDRANTblank or set tofalse. Docker Compose will build and start aqdrantservice. - Separate VM: Set
USE_EXTERNAL_QDRANT=true, fill inQDRANT_IP(andQDRANT_API_KEYif needed), then comment out or remove theqdrant:service block indocker-compose.override.yml.
# Build images (on first run)
docker compose build
# Start in detached mode
docker compose up -d
# View logs
docker compose logs -f --tail=100
# Stop containers
docker compose downServices defined in docker-compose.yml:
| Service | Responsibility |
|---|---|
ui |
Gradio web UI. |
qdrant |
Vector database (omitted when using an external instance). |
The script:
- Crawl and refresh cached HTML under
data/cached_html/. - Extract text chunks & image captions into
data/raw_jsonl/. - Generate embeddings with OpenAI.
- Upsert all data into Qdrant.
While scraping The Batch website, it was observed that nearly every image carries a meaningful accessibility caption (the alt attribute). These captions provide short, descriptive text closely tied to the surrounding article narrative. By pairing these captions with article chunks, we can build a multimodal retrieval-augmented generation (MRAG) pipeline that returns both relevant text passages and the exact images referenced in context.
- Crawler (asynchronous Python): Downloads raw HTML pages to
cached_html/. - Parser: Implements two code paths—one for
issue_*pages and one for regular posts—to extract the article title, cleaned body text, and each<img>source URL with itsaltcaption. - Image Downloader: Stores image files in
cached_images/, skipping inline base64 data. - JSONL Builder: Writes a single
data/batch_multimodal.jsonlfile. Each record is eithertype: textwith a text chunk, ortype: image_captionwith a caption and image metadata. Both record types share the samearticle_idandchunk_indexto maintain their linkage.
| Modality | Encoder | Rationale |
|---|---|---|
| Text & Captions | text-embedding-3-small (OpenAI) | A single, universal text model simplifies retrieval and keeps vector dimensions consistent. |
| Visual (for future) | CLIP ViT-L/14 | Reserved for future experiments where the signal from captions may be weak or absent. |
All vectors are created via the OpenAI Python SDK and stored together. Each payload includes the type, source_url, and the local image_path when applicable.
Qdrant was selected for its:
- Typed payload filtering, which enables fetching images belonging to the same article as a given text hit.
- HNSW and quantization for sub-second similarity search across over 100,000 vectors.
- Flexible deployment: a single Docker container for development, or the managed Qdrant Cloud for production without code changes.
- Simple REST and gRPC APIs that can be consumed directly from custom Python code without requiring extra frameworks.
- The user query is embedded using an OpenAI model.
- Primary Search: The top-k text chunks are retrieved from Qdrant using cosine distance.
- Paired Image Retrieval: For each returned text chunk, a secondary, filtered search retrieves nearby
image_captionvectors that share the samearticle_id. - Answer Synthesis: The GPT-4o-mini model receives the user's question, the retrieved text contexts, and the selected images and their captions (as tool messages) to produce an answer that references specific images.
- The API returns a JSON object containing the
answer, a list ofimages(with their paths and captions), and a list ofsources.
Pipeline:
- Question Generation: For each text chunk, GPT-4o-mini creates one or two question-answer pairs. Results are cached to
qa_cache.jsonl. - The MRAG pipeline answers the generated questions.
ragas.evaluatecomputescontext_recall,context_precision,answer_relevancy, andfaithfulness.
Latest Results (10,000 QA pairs):
| Metric | Score |
|---|---|
| Context Recall | 0.925 |
| Context Precision | 0.95 |
| Answer Relevancy | 0.905 |
| Faithfulness | 0.944 |
- Captions alone are often sufficient to identify the correct image, making expensive visual embeddings unnecessary in most cases.
- Qdrant's payload filtering makes text-image synchronization trivial to implement.
- RAGAS analysis highlighted a subset of images with generic captions; these are earmarked for future enhancement using CLIP-based methods.
- Future Work: Includes auto-captioning and visual embeddings for images, implementing a cross-encoder for re-ranking to boost answer relevancy, and building a Streamlit UI with a RAGAS metrics dashboard.
For developers who prefer to work directly on a local machine (macOS/Linux), a virtual environment setup is available:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Load environment variables from .env file
export $(grep -v '^#' .env | xargs)
# Run the API locally
python app/main.py # This launches the Gradio Blocks interfaceLinting, formatting, and type-checking can be run with make:
make lint # ruff + isort
make format # black
make type # mypy| Symptom | Potential Fix |
|---|---|
ImportError: No module named … |
Ensure the virtual environment is activated, or rebuild the Docker images. |
Connection refused :6333 |
Check that the Qdrant container is running (docker compose ps). |
| UI shows "No relevant context found" | Run the data ingestion pipeline to populate the vector store with embeddings. |
# Stop containers and remove volumes
docker compose down -v
# Remove all Docker images (optional)
docker image prune -aDistributed under the MIT License. See LICENSE for more information.
