A small, complete Retrieval-Augmented Generation (RAG) system in Rust β document loader, chunker, vector store, LLM service, gRPC API, and an interactive terminal UI.
Built with Qdrant for vector search, Rig for the AI application layer, Tonic for gRPC, ratatui for the terminal UI, and OpenRouter for embeddings and completion.
If you've ever wondered what's underneath
langchain.create_retrieval_chain(...)β this project is the answer, written in readable Rust you can follow end to end.
- π Loads Markdown, plain text, and PDF documents from a local folder
- πͺ Word-based chunking (~120 words per chunk) with UUID-tracked sources
- π§ Embeddings via OpenAI's
text-embedding-3-small(1536-dim) through OpenRouter - π Vector search powered by Qdrant with cosine similarity
- π¬ Grounded answers with explicit source attribution
- β‘ Streaming answers β replies type out token-by-token
- π₯οΈ Interactive ratatui TUI β chat + live sources panel, spinner and timer
- π Auto-indexes
./docson startup when the collection is empty - π§ Configurable LLM model via the
MODELenv var - π gRPC service exposing
AskQuestionandReindexendpoints - π§© Cargo workspace β the RAG engine is a reusable
qrag-corelibrary - π³ One-command Qdrant via Docker Compose
Two phases share a single RagEngine:
Indexing (runs automatically on startup when the collection is empty; or on demand via Reindex):
./docs β load β chunk β embed (OpenRouter) β store (Qdrant)
Query (every time someone asks):
question β embed β search Qdrant β top-k chunks β prompt β LLM (streamed) β answer + sources
The same embedding model is used on both sides β that's what makes vector distances meaningful.
A Cargo workspace: the engine is a reusable library (qrag-core); the
gRPC server + chat client live in qrag-server; the terminal UI is qrag-tui.
qrag-rust/
βββ Cargo.toml # workspace root + shared dependency versions
βββ docker-compose.yaml # Qdrant container
βββ proto/
β βββ rag.proto # gRPC service definition
βββ docs/ # your knowledge base lives here
βββ crates/
βββ qrag-core/ # reusable RAG engine (library)
β βββ src/
β βββ lib.rs # re-exports the public API
β βββ config.rs # env-var configuration
β βββ document_loader.rs # reads .md, .txt, .pdf
β βββ chunker.rs # splits into ~120-word chunks
β βββ qdrant_store.rs # embeddings + vector storage
β βββ llm.rs # prompt + streamed completion
β βββ rag.rs # orchestration (+ auto-index, streaming)
βββ qrag-server/ # gRPC server + chat client
β βββ build.rs # compiles .proto β Rust at build time
β βββ src/
β βββ main.rs # boots the gRPC server
β βββ grpc_service.rs # tonic handlers
β βββ bin/chat.rs # simple terminal chat client
βββ qrag-tui/ # interactive ratatui UI (embeds qrag-core)
βββ src/
βββ main.rs # boots engine + opens the UI
βββ app.rs # state + async event loop
βββ ui.rs # layout / rendering
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
rustc --version # needs a recent toolchain (edition 2024)sudo apt update
sudo apt install -y \
build-essential pkg-config libssl-dev \
clang cmake protobuf-compiler poppler-utils| Package | Why |
|---|---|
build-essential |
GCC/G++ for native crate compilation |
pkg-config |
Locates system libraries |
libssl-dev |
OpenSSL headers for HTTPS crates |
clang |
LLVM toolchain (bindgen) |
cmake |
Used by several native libraries |
protobuf-compiler |
The protoc binary for gRPC code generation |
poppler-utils |
Provides pdftotext, used for PDF ingestion |
sudo apt install -y docker.io docker-compose-plugin
sudo systemctl enable --now dockerbrew install rustup protobuf poppler
brew install --cask docker
rustup-initgit clone https://github.com/Parikalp-Bhardwaj/qrag-rust
cd qrag-rustcat > .env << 'EOF'
# Required β get one at https://openrouter.ai/keys
OPENROUTER_API_KEY=sk-or-v1-paste-your-key-here
# Optional β defaults shown
SERVER_ADDR=127.0.0.1
PORT=50051
QDRANT_URL=http://127.0.0.1:6334
QDRANT_COLLECTION=question
MODEL=openai/gpt-4o-mini
EOF| Variable | Default | What it controls |
|---|---|---|
OPENROUTER_API_KEY |
(required) | Authenticates embedding and LLM calls |
SERVER_ADDR |
127.0.0.1 |
Host the gRPC server binds to |
PORT |
50051 |
gRPC port for clients |
QDRANT_URL |
http://127.0.0.1:6334 |
Qdrant's gRPC endpoint (not 6333) |
QDRANT_COLLECTION |
question |
Name of the vector collection |
MODEL |
openai/gpt-4o-mini |
OpenRouter model used for answers |
.env. Make sure it's in .gitignore.
docker compose up -dDrop .md, .txt, or .pdf files into ./docs/.
The terminal UI embeds the engine β no separate server needed:
cargo run -p qrag-tuiOn first run it auto-indexes ./docs, then opens a two-panel interface: a
Chat panel on the left and a Sources panel (file Β· similarity score Β·
preview) on the right. Type a question, press Enter, and the answer streams
in live while the header shows a spinner and elapsed timer.
Keys: Enter send Β· β/β scroll Β· Esc / Ctrl-C quit.
Run the server, then talk to it from the simple chat client or grpcurl:
# terminal 1 β server (auto-indexes on first run)
cargo run -p qrag-server --bin qrag-server
# terminal 2 β chat client
cargo run -p qrag-server --bin chatOr query directly:
grpcurl -plaintext -d '{"question":"What is tokio?"}' \
-import-path proto -proto rag.proto \
127.0.0.1:50051 rag.RagService/AskQuestionAuto-index only runs when the collection is empty, so after adding new files
call the Reindex RPC:
grpcurl -plaintext -d '{}' \
-import-path proto -proto rag.proto \
127.0.0.1:50051 rag.RagService/ReindexDefined in proto/rag.proto:
service RagService {
rpc AskQuestion(AskQuestionRequest) returns (AskQuestionResponse);
rpc Reindex(ReindexRequest) returns (ReindexResponse);
}AskQuestion returns an answer plus the source chunks (file path, preview,
similarity score). Reindex rebuilds the index from ./docs/ and returns the
chunk count; the server also runs it automatically on startup when the
collection is empty.
Configuration is environment-driven (crates/qrag-core/src/config.rs). To
change chunking or the embedding model, edit the constants in code:
| What | Where |
|---|---|
| Embedding model | EMBEDDING_MODEL in crates/qrag-core/src/qdrant_store.rs |
| Vector dimension | VectorParamsBuilder::new(1536, ...) in crates/qrag-core/src/qdrant_store.rs |
| LLM model | MODEL env var (or the default in config.rs) |
| Chunk size | chunk_retrieve(load, 120) in crates/qrag-core/src/rag.rs |
| Top-K retrieval | self.qdrant_store.search(&question, 3) in crates/qrag-core/src/rag.rs |
If you change the embedding model, also update the vector dimension and run docker compose down -v to clear the old collection.
- Vector dimension mismatch.
text-embedding-3-smallis 1536-dim. Change one without the other and every upsert fails; wipe the volume (docker compose down -v) to recover. - REST vs gRPC port. Qdrant exposes REST on 6333 and gRPC on 6334. The Rust app needs 6334.
- PDFs need
pdftotext. Withoutpoppler-utils, PDF documents fail to load and startup auto-index logs a warning. - Auto-index only runs when empty. After adding new docs, call the
ReindexRPC β a restart alone won't pick them up if the collection already has data. .envis loaded from the current directory. Runcargofrom the workspace root.
- Cargo workspace with reusable
qrag-core - Auto-index on startup
- Interactive ratatui TUI
- Streaming responses
- Configurable LLM model
- File-watcher to auto-reindex when docs change
- Reranking (top-20 β cross-encoder β top-3)
- Hybrid retrieval (vector + BM25)
- Per-tenant collections
- HTTP/REST endpoint
- Tauri desktop app
| Layer | Choice |
|---|---|
| Language | Rust (edition 2024) |
| Async runtime | Tokio |
| gRPC | Tonic + Prost |
| Terminal UI | ratatui + crossterm |
| LLM framework | Rig |
| Vector DB | Qdrant (via qdrant-client) |
| Vector store glue | rig-qdrant |
| LLM provider | OpenRouter (embedding + completion) |
| PDF text extraction | pdftotext (poppler) |
Built to learn. Read the source, break it, fork it. π¦
