raggit
A plug-and-play production-grade RAG system that connects to local and remote object storage, automatically indexes documents, and answers questions using hybrid retrieval with reranking and LLM augmentation.
What raggit does
raggit is built for teams that need reliable retrieval-augmented generation without managing a patchwork of vector databases, parsers, and rerankers. It handles the full document lifecycle: watching storage, parsing common formats, chunking intelligently, embedding, searching, and generating cited answers.
Documents live in storage you already control. Point raggit at a local directory, S3 bucket, GCS bucket, or Azure container and it keeps an index in sync automatically. Query it through the CLI and receive an answer grounded in your documents, with citations.
Features
Ingestion
- Automatic watching via
raggit serve. Local filesystem changes are detected instantly through OS-native events (fsevents/inotify). Cloud storage is polled and diffed. - Format-aware parsing for PDF, DOCX, HTML, Markdown, and plain text. PDF page numbers are preserved through chunking.
- Smart chunking respects Markdown headers, code boundaries, and PDF pages, with recursive character splitting as a fallback.
- Deduplication removes exact and near-duplicate chunks using content hashes and Jaccard word-set similarity.
- Cleaning & normalization fixes Unicode, hyphenation, and whitespace before embedding.
- Optional safety steps: PII redaction and prompt-injection hardening.
- Model-scoped collections in Qdrant so changing embedding models never corrupts an existing index.
Retrieval
- Hybrid search combines BM25 keyword search with dense semantic search.
- Weighted Reciprocal Rank Fusion merges heterogeneous rank lists.
- Query rewriting with multi-query expansion and HyDE.
- Optional cross-encoder reranking for better top-N ordering.
- Dynamic top-k scales with corpus size while staying bounded.
- Parent-window expansion provides surrounding context for each hit.
- Structured citations include source URI, filename, page, section, offsets, score, and excerpt.
Operations
- Multi-tenancy & filtering by tenant, tags, source prefix, filename prefix, document IDs, and date range.
- Structured audit logs persisted to PostgreSQL for every ingestion, query, and answer.
- Document lifecycle status tracks pending, indexing, parsed, chunked, embedded, completed, failed, and deleted states.
- Async-first architecture using asyncpg, async Qdrant client, and async cloud SDKs.
- FastAPI HTTP API with interactive
/docsSwagger UI for every operation: query, manage documents, inspect chunks and logs, update configuration, trigger ingestion, and control the watcher. - Three-tier evaluation framework – isolated component suites (parser, chunker, cleaner, PII, injection, sanitizer, RRF, reranker, safety, embedder, storage), pipeline suites (ingestion & retrieval chains), and system end-to-end (22+ system metrics including citations, hallucination, tenant/tag/prefix filters, p50/p95 latency, audit). 69+ metrics total, `kind: all` runs every tier.
- Optional MCP server exposing the same core operations to MCP clients over stdio or SSE. Install with
raggit[mcp].
Quick Start
You need Docker Desktop (or Docker Engine + Compose). For local development you also need uv.
Option 1: Docker (recommended)
Build and run the entire stack with one command:
docker compose up -d
This starts:
raggit-postgreson port5433raggit-qdranton ports6333and6334raggit-apprunningraggit servewith the FastAPI server on port8000and the watcher enabled
Place documents in ./data/documents for local storage ingestion. The container entrypoint applies Alembic migrations before starting the service.
If you rebuild the image after Dockerfile changes, recreate the app container:
docker compose down raggit
docker compose build --no-cache raggit
docker compose up -d raggit
Useful commands
# View logs
docker compose logs -f raggit
# Run one-time ingestion inside the container
docker compose exec raggit raggit ingest
# Run a query
docker compose exec raggit raggit query "What is raggit?"
# Stop everything
docker compose down
Option 2: Local development
-
Start PostgreSQL and Qdrant
docker compose up -d postgres qdrantPostgreSQL is mapped to host port
5433to avoid conflicts with a local postgres on5432. -
Install dependencies
uv sync -
Run database migrations
uv run alembic upgrade head -
Configure raggit
uv run raggit setup \ --database-url postgresql+asyncpg://raggit:raggit@localhost:5433/raggit \ --qdrant-url http://localhost:6333 \ --storage-source-type local \ --storage-uri ./data/documents \ --llm-provider openai \ --llm-model gpt-4o-mini \ --llm-api-key $OPENAI_API_KEYraggit setupwrites~/.config/raggit/raggit.envwith0600permissions and bootstraps the system by checking Postgres, running migrations, checking Qdrant, and creating the local document directory. -
Add documents
mkdir -p data/documents cp my-docs/*.pdf data/documents/ -
Run the service for continuous indexing
uv run raggit serveraggit will start the FastAPI server on
http://localhost:8000and perform an initial sync, then watch for new, modified, and deleted files. Local changes are reflected almost instantly. Open/docsfor interactive Swagger UI. -
Ask questions
uv run raggit query "What is raggit?"Or use the HTTP API:
curl -X POST http://localhost:8000/query \ -H "Content-Type: application/json" \ -d '{"query": "What is raggit?"}'
See the HTTP API and storage backends sections for more. Live +/- file event indicators are now shown directly by raggit serve.
Architecture
raggit is organized into clear layers: storage, ingestion, data, retrieval, LLM, audit, and interface. The HTTP API is built on FastAPI and exposes every major operation. All I/O is async, and every significant input and output is logged to PostgreSQL.
flowchart TB
subgraph Storage
LS[Local Filesystem]
RS[S3 / GCS / Azure Blob]
end
subgraph Ingestion
W[Watcher Service]
I[Indexing Service]
P[Parser Registry]
FA[Format-Aware Chunker]
DED[Dedup]
CL[Chunk Cleaner]
PI[PII Redaction]
IH[Injection Hardening]
E[Embedder]
end
subgraph PostgreSQL
D[documents]
CH[chunks]
EC[embedding_collections]
LG[logs]
end
VS[Qdrant Vector Store]
LS --> W
RS --> W
W --> I
I --> P
P --> FA
FA --> DED
DED --> CL
CL --> PI
PI --> IH
IH --> E
E --> VS
I --> D
IH --> CH
E --> CH
I --> EC
IH --> LG
E --> LG
subgraph Retrieval
Q[User Query]
QR[Query Rewriter]
MQ[Multi-Query]
HY[HyDE]
BM25[Postgres FTS / BM25]
SEM[Qdrant Semantic Search]
RRF[Weighted RRF]
RR[Cross-Encoder Reranker]
TH[Score Threshold]
PW[Parent-Window Expansion]
AUG[Augmenter]
LLM[LLM Provider]
GRD[Groundedness Check]
OUT[Answer + Citations]
end
Q --> QR
QR --> BM25
QR --> SEM
QR --> MQ
QR --> HY
BM25 --> RRF
SEM --> RRF
RRF --> RR
RR --> TH
TH --> PW
PW --> AUG
AUG --> LLM
LLM --> GRD
GRD --> OUT
RRF --> LG
AUG --> LG
LLM --> LG
subgraph Interface
CLI[Typer CLI]
API[FastAPI HTTP API]
end
API --> Q
CLI --> Q
API --> W
CLI --> W
Component responsibilities
- Storage layer abstracts local directories and cloud object stores behind a common interface. Local backends emit OS-native events; cloud backends poll and diff.
- WatcherService starts storage watching, runs an initial sync, and dispatches file events to the indexer with lightweight debouncing.
- Indexer orchestrates hash checks, parsing, chunking, cleaning, embedding, and persistence.
- Parser registry selects the right parser by file extension and extracts text plus structural hints like page numbers.
- Chunker produces format-aware pieces and links sequential siblings for relevance-chain retrieval.
- Embedder generates dense vectors using sentence-transformers or an OpenAI-compatible API.
- Vector store manages Qdrant collections scoped to the embedding model and version.
- Retrieval engine sanitizes queries, optionally rewrites them, runs BM25 and semantic search in parallel, fuses results, reranks, thresholds, and expands parent windows.
- LLM layer augments prompts with isolated untrusted context and generates cited answers.
- Audit logger persists structured events to PostgreSQL.
- HTTP API exposes every operation via FastAPI with interactive OpenAPI documentation at
/docs.
Design goals
- Accuracy through hybrid retrieval, cross-encoder reranking, query rewriting, and parent-document expansion.
- Safety by isolating untrusted document text from system instructions and optionally redacting PII.
- Observability with lifecycle statuses, model-scoped collections, and structured audit logs.
- Multi-tenancy via
tenant_idand tags applied consistently across BM25 and vector search. - Evolvability via model-scoped Qdrant collections so changing embedding models does not corrupt existing indexes.
Data Model
PostgreSQL is the source of truth for metadata, chunks, audit logs, and collection tracking. Qdrant holds the dense vectors with filterable payloads.
documents
| Field | Type | Description |
|---|---|---|
id | UUID | Primary key. |
source_type | enum | local, s3, gcs, azure_blob. |
source_uri | string | Stable URI such as a local path or s3://bucket/key. |
filename | string | Relative filename. |
content_hash | string | Hash of file contents used to skip unchanged files. |
file_size | integer | Cached file size for fast sync decisions. |
file_modified_at | timestamptz | Cached modification time for fast sync decisions. |
status | enum | pending, indexing, parsed, chunked, embedded, completed, failed, deleted. |
error_message | text | Failure reason when status is failed. |
tenant_id | string | Optional tenant identifier. |
tags | array | Optional filter tags. |
created_at | timestamptz | Row creation time. |
updated_at | timestamptz | Row update time. |
deleted_at | timestamptz | Soft-deletion timestamp. |
chunks
| Field | Type | Description |
|---|---|---|
id | UUID | Primary key. |
document_id | UUID | Foreign key to documents. |
chunk_index | integer | Sequential index within the document. |
raw_content | text | Original chunk text. |
cleaned_content | text | Normalized chunk text used for embedding and BM25. |
word_count | integer | Word count for diagnostics. |
embedding_model | string | Model that produced the embedding. |
vector_id | UUID | Qdrant point ID. |
fts_vector | tsvector | PostgreSQL full-text search vector. |
parent_chunk_index | integer | Parent chunk for hierarchical chunking. |
prev_chunk_id | UUID | Previous sibling chunk. |
next_chunk_id | UUID | Next sibling chunk. |
section_title | string | Section or heading, if detected. |
page_number | integer | PDF page number, if detected. |
start_offset | integer | Character offset in source text. |
end_offset | integer | Character offset in source text. |
content_hash | string | Hash of chunk content. |
embedding_collections
Tracks which Qdrant collection is active for the current embedding model. Changing models creates a new collection and activates it automatically.
logs
Structured audit log with level, component, message, and JSON extra field. Captures ingestion inputs/outputs, queries, retrieval results, and generated answers.
Ingestion Pipeline
When a file is added or changed, the watcher triggers the indexer, which runs the document through parsing, format-aware chunking, cleaning, optional safety steps, embedding, and persistence.
tiktoken cl100k_base when available, with configurable overlap.logs table.Retrieval Pipeline
Queries are sanitized, optionally rewritten, run through BM25 and semantic search in parallel, fused, reranked, expanded, and then passed to the LLM with citations.
multi_query generates alternative phrasings; hyde generates a hypothetical answer passage to embed.tsvector GIN index.BAAI/bge-reranker-base.min_score and refuse empty or low-score retrieval.parent_window > 0.logs table.Watcher & Continuous Indexing
raggit keeps your index in sync with storage automatically. The recommended way to run it is raggit serve, a lightweight long-running process that starts a WatcherService.
raggit serve
raggit serve is the default long-running mode. It:
- Loads configuration from environment variables and
~/.config/raggit/raggit.env. - Runs an initial full sync of all existing files.
- Starts the storage-specific watcher.
- Dispatches add/modify/delete events to the indexer with per-path debouncing.
- Handles SIGINT and SIGTERM gracefully.
uv run raggit serve
uv run raggit serve ./data/documents
uv run raggit serve --log-level DEBUG --tenant acme --tag finance
Event-driven local watching
For local directories, raggit uses watchdog to receive OS-native filesystem events (FSEvents on macOS, inotify on Linux). This means:
- New or modified files are detected almost instantly.
- There is no periodic polling loop waking the CPU.
- Idle resource usage is negligible.
Git-like stat snapshot
On startup and during sync, raggit stores a cheap stat snapshot (file_size and file_modified_at) for each document. When a file is seen again, the indexer first compares this metadata. Only when size or modification time differs does it read the file and compute a content hash. This makes initial sync fast when most files have not changed.
Cloud storage watching
S3, GCS, and Azure backends do not have a universal push-notification mechanism, so they use lightweight polling and snapshot diffing. The interval is configurable with STORAGE_POLL_INTERVAL_SECONDS (default 30 seconds). You can make cloud watching near-instant by routing provider events (S3 Event Notifications, GCS Pub/Sub, Azure Event Grid) into a queue that a future extension can consume.
Automatic & independent operation
The watcher is now a single canonical implementation inside WatcherService. It starts automatically with raggit serve and via the FastAPI lifespan when running uvicorn raggit.api.server:app – no manual POST /watcher/start needed. The legacy raggit watch command has been removed; raggit serve now prints live +/- indicators itself. Disable the watcher with raggit serve --no-watcher or RAGGIT_NO_AUTO_WATCHER=1.
The included docker-compose.yml runs raggit serve by default, so the container starts the FastAPI server and watcher as soon as it comes up.
HTTP API
raggit exposes a FastAPI HTTP server on port 8000 (configurable via --port). Every major operation is available over HTTP, and interactive documentation is served at /docs (Swagger UI) and /redoc (ReDoc).
Running the API
uv run raggit serve
uv run raggit serve --host 0.0.0.0 --port 8000
uv run raggit serve --no-watcher # API only, no file watcher
Endpoints
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check. |
GET |
/config |
Get current runtime configuration. |
POST |
/config |
Update and persist configuration to the env file. |
GET |
/status |
Overall status: document counts and active collections. |
GET |
/documents |
List indexed documents with optional filters. |
GET |
/documents/{id} |
Get a single document. |
DELETE |
/documents/{id} |
Hard-delete a document and its chunks/vectors. |
GET |
/documents/{id}/chunks |
List chunks for a document. |
GET |
/chunks/{id} |
Get a single chunk. |
GET |
/logs |
Structured audit log entries with optional filters. |
POST |
/query |
Run a hybrid retrieval query and optionally generate an answer. |
POST |
/ingest |
Trigger a one-time ingestion run. |
GET |
/watcher/status |
Check whether the watcher is running. |
POST |
/watcher/start |
Start the storage watcher. |
POST |
/watcher/stop |
Stop the storage watcher. |
Examples
Query
curl -X POST http://localhost:8000/query \
-H "Content-Type: application/json" \
-d '{"query": "What is raggit?"}'
Filtered query
curl -X POST http://localhost:8000/query \
-H "Content-Type: application/json" \
-d '{
"query": "What is raggit?",
"tenant_id": "acme",
"tags": ["finance"],
"top_k": 10,
"generate_answer": false
}'
Update configuration
curl -X POST http://localhost:8000/config \
-H "Content-Type: application/json" \
-d '{
"config": {
"retrieval": {
"min_top_k": 10,
"max_top_k": 50,
"query_rewrite": "multi_query"
},
"safety": {
"groundedness_check": true
}
}
}'
Trigger ingestion
curl -X POST http://localhost:8000/ingest \
-H "Content-Type: application/json" \
-d '{"path": "./data/documents", "tenant_id": "acme"}'
List logs
curl "http://localhost:8000/logs?level=INFO&limit=50"
Delete a document
curl -X DELETE http://localhost:8000/documents/{document_id}
Watcher control
curl -X POST http://localhost:8000/watcher/start
# ... later ...
curl -X POST http://localhost:8000/watcher/stop
POST /config writes changes to ~/.config/raggit/raggit.env. If database_url changes, the SQLAlchemy engine is recreated automatically. Other runtime services (such as the watcher) use the new config on the next operation; restart the watcher to apply storage changes immediately.
Evaluation
raggit ships a three-tier evaluation framework so every feature is measurable in isolation, as a pipeline, and end-to-end. Run it from the CLI or HTTP API and export reports as JSON or Markdown. All 69+ metrics are available via --list-metrics.
- Component – isolated primitives (parser, chunker, cleaner, PII, injection, sanitizer, embedder, RRF, reranker, safety, storage, watcher, retriever). Synthetic data, no DB needed, fast.
- Pipeline – ingestion chain (parse→chunk→clean→embed) and retrieval chain (sanitize→rewrite→BM25/semantic→RRF→rerank→threshold→parent→traversal) with real DB/vector store.
- System – end-to-end ingestion + retrieval + LLM augmentation, citations, filtering, safety, MCP. The original
EvalRunnertier. - All – runs component + pipeline + system sequentially from a single
kind: alldataset.
Dataset format by tier
Datasets are JSON/YAML with a kind discriminator. Old datasets without kind default to system for backwards compatibility.
Component (kind: component)
name: chunker-eval
kind: component
component: chunker # parser|chunker|cleaner|pii|injection|sanitizer|embedder|rrf|reranker|safety|storage|watcher|retriever
metrics: [chunker_section_preservation, chunker_dedup_effectiveness]
k_values: [5]
component_tests:
- id: chunker-md-1
component: chunker
input: {text: "# Title\nContent", path: "doc.md"}
expected: {expected_titles: ["Title"]}
tags: [markdown]
Pipeline (kind: pipeline)
name: ingestion-pipeline
kind: pipeline
pipeline: ingestion # ingestion|retrieval|e2e
metrics: [pipeline_ingestion_success_rate, pipeline_retrieval_success_rate]
pipeline_tests:
- id: ingest-1
pipeline: ingestion
documents: [{path: "note.md", text: "# Hello\nWorld"}]
- id: retrieve-1
pipeline: retrieval
query: "What is hello?"
expected_chunk_ids: ["11111111-1111-1111-1111-111111111111"]
System (kind: system)
name: my-eval
kind: system
metrics:
- retrieval_recall@k
- retrieval_mrr
- answer_contains
- answer_semantic_similarity
- system_citation_precision
- filter_tenant_accuracy
k_values: [5, 10]
tests:
- id: q1
query: "What is raggit?"
filters: {tenant_id: "acme", tags: ["finance"]}
expected_chunk_ids:
- 11111111-1111-1111-1111-111111111111
expected_answer: "raggit is a production-grade RAG system"
tags: [basic]
All tiers (kind: all)
name: raggit-all-tiers
kind: all
metrics: [chunker_section_preservation, pipeline_ingestion_success_rate, retrieval_recall@k, system_citation_precision]
component_tests: [...] # every component
pipeline_tests: [...] # ingestion + retrieval + e2e
tests: [...] # system end-to-end
Metrics – every feature covered
| Metric | Tier | Feature |
|---|---|---|
parser_parse_success | component | Parser: any supported extension loads |
parser_text_fidelity | component | Parser: extracted text 3-gram fidelity |
parser_page_preservation | component | PDF: --- Page N --- markers |
parser_html_stripping | component | HTML: script/style/nav removal |
chunker_section_preservation | component | Markdown headers, headings |
chunker_page_preservation | component | PDF pages kept as chunks |
chunker_function_boundary | component | Code: def/class/function split |
chunker_dedup_effectiveness | component | Jaccard 0.92 dedup |
chunker_word_count_accuracy | component | Word count within tolerance |
chunker_overlap_correctness | component | Sliding window overlap |
chunker_format_aware | component | Format-aware vs fallback |
cleaner_effectiveness | component | NFKC, hyphenation, whitespace |
cleaner_unicode_normalization | component | Unicode NFKC |
cleaner_hyphenation_fix | component | word-\\nword → wordword |
cleaner_whitespace_collapse | component | Collapse spaces / blank lines |
pii_redaction_recall | component | PII: email/phone/SSN/card/IP recall |
pii_redaction_precision | component | PII precision |
pii_redaction_f1 | component | PII F1 |
injection_hardening_recall | component | Injection: ignore/system/override |
injection_hardening_precision | component | Injection false-positive |
sanitizer_keyword_recall | component | Keywords vs expected |
sanitizer_stopword_removal_rate | component | Stopwords removed |
rrf_fusion_quality | component | Weighted RRF nDCG |
reranker_gain | component | Cross-encoder MRR gain |
threshold_retention_rate | pipeline | min_score retention |
parent_window_gain | pipeline | Parent-window recall gain |
traversal_precision | pipeline | Relevance-chain traversal |
safety_groundedness_accuracy | component | Groundedness heuristic |
safety_refusal_f1 | component | Refusal on empty/low-score |
embedder_cosine_accuracy | component | Cosine ≈ expected |
embedder_latency | component | Embedding ms |
storage_path_traversal_block_rate | component | Path traversal blocked |
watcher_debounce_accuracy | component | Debounce per-path |
retrieval_recall@k | system/pipeline | Recall of relevant chunks |
retrieval_precision@k | system/pipeline | Precision in top-k |
retrieval_mrr | system/pipeline | Reciprocal rank |
retrieval_ndcg@k | system/pipeline | nDCG |
retrieval_hit_rate@k | system/pipeline | Hit rate |
bm25_recall@k | pipeline | BM25 only |
semantic_recall@k | pipeline | Semantic only |
hybrid_recall@k | pipeline | Hybrid fused |
answer_exact_match | system | Exact string match |
answer_contains | system | Expected in answer |
answer_semantic_similarity | system | Embed cosine |
answer_llm_judge | system | LLM 0-5 → 0-1 |
groundedness | system | Answer grounded in context |
latency_ms | system/pipeline | End-to-end ms |
system_latency_p50 | system | p50 latency |
system_latency_p95 | system | p95 latency |
refusal_accuracy | system | Refusal matches expected |
system_citation_precision | system | Citations vs expected |
system_citation_recall | system | Citation recall |
system_hallucination_rate | system | 1 - groundedness |
filter_tenant_accuracy | system | tenant_id filter |
filter_tag_accuracy | system | tag filter |
filter_prefix_accuracy | system | source_uri/filename prefix |
system_audit_coverage | system | Audit logs emitted |
system_e2e_success | system | Overall pass (not refused when not expected + grounded) |
pipeline_ingestion_success_rate | pipeline | Parse→chunk→clean success |
pipeline_retrieval_success_rate | pipeline | Retrieval not refused |
mcp_tool_success | system | MCP tools (synthetic) |
CLI – every tier
# Component – isolated primitive (no DB needed for many)
uv run raggit eval --generate --kind component --component chunker --name chunker-suite
uv run raggit eval chunker-suite.yaml
# All components at once
uv run raggit eval --generate --kind component --component retriever --name retriever-suite
# Pipeline – ingestion & retrieval chains (needs DB/vector store)
uv run raggit eval --generate --kind pipeline --pipeline ingestion --name ingest-pipe
uv run raggit eval --generate --kind pipeline --pipeline retrieval --name retrieve-pipe
uv run raggit eval ingest-pipe.yaml
uv run raggit eval retrieve-pipe.yaml
# System – end-to-end with LLM (default)
uv run raggit eval --generate --name my-eval
uv run raggit eval my-eval.yaml --output report.json
uv run raggit eval my-eval.yaml --output report.md
# Comprehensive: every feature in one file
uv run raggit eval --comprehensive --name full-suite # kind: system, 12 tests, 22 metrics
uv run raggit eval --generate --kind all --name all-tiers # component+pipeline+system
# Explore metrics
uv run raggit eval --list-metrics
uv run raggit eval --generate --kind component --component pii --name pii-suite
Golden datasets – built-in & custom
raggit ships golden datasets under eval_datasets/ – curated ground-truth for every tier, committed to git so CI and teammates share the same baseline:
eval_datasets/golden.yaml/golden-all-tiers.yaml– all tiers, every component + pipeline + system (27+13 tests, 53 metrics)eval_datasets/golden-system.yaml– comprehensive system (12 tests, 22 metrics covering retrieval, filters, safety, citations, latency, MCP)eval_datasets/golden-component-<name>.yaml– per-primitive (parser 3 tests, chunker 3, cleaner 3, pii 3, injection 3, sanitizer 2, etc.)eval_datasets/golden-pipeline-<ingestion|retrieval|e2e>.yaml– ingestion and retrieval chains
Run them directly:
uv run raggit eval eval_datasets/golden.yaml
uv run raggit eval eval_datasets/golden-component-chunker.yaml
uv run raggit eval eval_datasets/golden-pipeline-ingestion.yaml
uv run raggit eval eval_datasets/golden-system.yaml --output report.json
Custom golden dataset: add your own ground-truth and merge or diff:
# Create your own golden dataset (edit it with your queries & expected chunk IDs/answers)
uv run raggit eval --generate --kind system --name my-golden
# → my-golden.yaml (fill in expected_chunk_ids, expected_answer, filters, tags)
# Run your dataset merged with the built-in golden
uv run raggit eval my-custom.yaml --golden-dataset eval_datasets/golden-system.yaml
# Or use your custom golden as the sole dataset
uv run raggit eval --golden-dataset ./my-golden.yaml
# Use any dataset as golden
uv run raggit eval eval_datasets/golden-component-pii.yaml --golden-dataset ./my-pii-golden.yaml
# Compare current run against a previous JSON report (shows Δ in terminal)
uv run raggit eval eval_datasets/golden-component-cleaner.yaml --output report.json
uv run raggit eval eval_datasets/golden-component-cleaner.yaml --golden-report report.json
The terminal report now renders a detailed per-tier breakdown (header with kind, summary with pass rate, aggregate table with Δ vs golden, per-test table with latency and key metric, component/pipeline details, answer & citation preview, and a final verdict). The same rich report is available via --output report.md and the HTTP API.
HTTP API – tier-aware
# Component
curl -X POST http://localhost:8000/eval/run \
-H "Content-Type: application/json" \
-d '{"dataset": {"name": "chunker-suite","kind": "component","component": "chunker","metrics": ["chunker_section_preservation"],"component_tests": [{"id": "c1","component": "chunker","input": {"text": "# T\nhello"},"expected": {"expected_titles": ["T"]}}]}}'
# Pipeline
curl -X POST http://localhost:8000/eval/run \
-H "Content-Type: application/json" \
-d '{"dataset": {"name": "pipe","kind": "pipeline","pipeline": "ingestion","pipeline_tests": [{"id": "p1","pipeline": "ingestion","documents": [{"path": "a.md","text": "# Hi"}]}]}}'
# System (default)
curl -X POST http://localhost:8000/eval/run \
-H "Content-Type: application/json" \
-d '{
"dataset": {
"name": "my-eval",
"kind": "system",
"metrics": ["retrieval_recall@k", "retrieval_mrr"],
"k_values": [5],
"tests": [{"id": "q1","query": "What is raggit?","expected_chunk_ids": ["11111111-1111-1111-1111-111111111111"]}]
}
}'
# All tiers in one call
curl -X POST http://localhost:8000/eval/run -d '{"dataset": {"kind": "all", ...}}'
Start with component suites when iterating on a single feature (e.g. chunker headers), then validate the pipeline chain, then run the system or all suite for release confidence. Use comprehensive nightly.
MCP Server (optional)
raggit can expose its operations through the Model Context Protocol (MCP). This lets MCP clients such as Claude Desktop, Cursor, or any other MCP host query documents, inspect status, run ingestion, and trigger evaluation runs.
Installation
MCP support is an optional extra so raggit still works when mcp is not installed.
uv pip install 'raggit[mcp]'
CLI: stdio transport
Run the MCP server over stdio for local MCP clients:
uv run raggit mcp
HTTP API: SSE transport
When the mcp extra is installed and raggit serve is running, the MCP server is mounted at /mcp using Server-Sent Events:
curl -N http://localhost:8000/mcp
Exposed tools
| Tool | Description |
|---|---|
query | Ask a question with optional filters (tenant, tags, date range, document IDs). |
get_status | Fetch runtime status and configuration. |
list_documents | List indexed documents with optional filters. |
get_document | Get a single document by UUID. |
list_chunks | List chunks for a document. |
get_chunk | Get a single chunk by UUID. |
list_logs | List audit logs with filters. |
get_config | Read the current configuration. |
ingest | Trigger ingestion of a path or cloud prefix. |
run_eval | Run an evaluation dataset and return metrics. |
Every MCP operation goes through the same services as the HTTP API and CLI, so configuration, audit logging, and multi-tenancy work identically across all interfaces.
CLI Reference
The raggit CLI is the primary interface for setup, ingestion, watching, and querying.
| Command | Description |
|---|---|
raggit setup |
Interactive configuration for local, S3, GCS, and Azure backends. |
raggit serve [path] |
Run the long-running service. Starts the FastAPI server and watches storage, indexing changes automatically. The optional MCP SSE endpoint is available at /mcp when the mcp extra is installed. |
raggit mcp [--transport stdio|sse] [--host ...] [--port ...] |
Run the MCP server. Requires raggit[mcp]. |
raggit ingest <path> |
One-time ingestion with a progress bar. Path is optional for cloud storage. |
raggit query "<question>" |
Ask a question; shows status spinners, chunk table, answer panel, and citation tree. |
raggit status |
Show indexed document status and active embedding collections. |
raggit chunks <document> |
List chunks for a document by UUID or filename. |
raggit eval <dataset> |
Run an evaluation dataset (component, pipeline, system, or all) and print or save a report. 69+ metrics. |
setup
Writes ~/.config/raggit/raggit.env and bootstraps the system. Exposes every configuration parameter as a CLI option.
uv run raggit setup --help
serve
uv run raggit serve [PATH] [OPTIONS]
Options: --host, --port, --poll-interval, --no-watcher, --log-level, --tenant, --tag (repeatable). The watcher runs automatically; no separate raggit watch needed. Removed raggit watch now shows a deprecation message directing to raggit serve.
ingest
uv run raggit ingest [PATH] [OPTIONS]
Options: --chunk-size, --chunk-overlap, --preserve-sections/--split-sections, --embedding-provider, --embedding-model, --log-level, --tenant, --tag.
query
uv run raggit query "<question>" [OPTIONS]
Options include --top-k, --min-top-k, --max-top-k, --top-k-ratio, --rrf-k, --source-prefix, --filename-prefix, --tenant, --tag, --document-id, --created-after, --created-before, --min-score, --rewrite, --multi-query-count, --parent-window, --reranker/--no-reranker, --reranker-model, --reranker-top-n, --refuse-on-empty/--no-refuse-on-empty, --refuse-on-low-score/--no-refuse-on-low-score, --min-answer-score, --groundedness-check/--no-groundedness-check, --pii-redaction/--no-pii-redaction, --prompt-injection-hardening/--no-prompt-injection-hardening, and --no-llm.
chunks
uv run raggit chunks <document-id> [OPTIONS]
Options: --filename to look up by filename, --full to show full content.
status
uv run raggit status
Shows a table of indexed documents and active embedding collections.
eval
uv run raggit eval <dataset.yaml> [OPTIONS]
uv run raggit eval --generate --kind component --component chunker --name my-chunker
uv run raggit eval --generate --kind pipeline --pipeline ingestion --name pipe
uv run raggit eval --comprehensive --name full
uv run raggit eval --generate --kind all --name all-tiers
Options: --generate, --comprehensive, --list-metrics, --kind component|pipeline|system|all, --component parser|chunker|..., --pipeline ingestion|retrieval|e2e, --name, --description, --metric (repeatable, 69+ available), --k (repeatable), --output, --output-format, --log-level.
Tier mapping: component isolates primitives (14 types) with synthetic data; pipeline tests ingestion (parse→chunk→clean→embed) and retrieval (sanitize→RRF→rerank→traversal) with DB; system end-to-end with LLM, citations, filters, p50/p95; all runs every tier sequentially.
Storage Backends
raggit supports local filesystem, S3, Google Cloud Storage, and Azure Blob Storage. Remote backends are installed as optional extras to keep the base install small.
Local filesystem
The default backend. Set STORAGE_SOURCE_TYPE=local and STORAGE_URI to a directory path. The directory is created automatically if it does not exist. Changes are detected instantly via OS-native events.
AWS S3
Install the S3 extra:
uv pip install 'raggit[s3]'
Then run setup with S3 options:
uv run raggit setup \
--database-url postgresql+asyncpg://raggit:raggit@localhost:5433/raggit \
--qdrant-url http://localhost:6333 \
--storage-source-type s3 \
--storage-uri s3://my-bucket/documents \
--storage-bucket my-bucket \
--storage-prefix documents \
--storage-region us-east-1 \
--aws-access-key-id $AWS_ACCESS_KEY_ID \
--aws-secret-access-key $AWS_SECRET_ACCESS_KEY \
--llm-provider openai \
--llm-model gpt-4o-mini \
--llm-api-key $OPENAI_API_KEY
Google Cloud Storage
Install the GCS extra:
uv pip install 'raggit[gcs]'
uv run raggit setup \
--database-url postgresql+asyncpg://raggit:raggit@localhost:5433/raggit \
--qdrant-url http://localhost:6333 \
--storage-source-type gcs \
--storage-uri gs://my-bucket/documents \
--storage-bucket my-bucket \
--storage-prefix documents \
--gcs-service-account-path /path/to/service-account.json \
--llm-provider openai \
--llm-model gpt-4o-mini \
--llm-api-key $OPENAI_API_KEY
Azure Blob Storage
Install the Azure extra:
uv pip install 'raggit[azure]'
uv run raggit setup \
--database-url postgresql+asyncpg://raggit:raggit@localhost:5433/raggit \
--qdrant-url http://localhost:6333 \
--storage-source-type azure_blob \
--storage-uri azure://my-container/documents \
--storage-container my-container \
--storage-prefix documents \
--azure-connection-string $AZURE_STORAGE_CONNECTION_STRING \
--llm-provider openai \
--llm-model gpt-4o-mini \
--llm-api-key $OPENAI_API_KEY
Cloud storage watchers use periodic polling and snapshot diffing instead of native push notifications. This keeps the implementation portable across providers without requiring SQS, Pub/Sub, or Event Grid.
Configuration
Configuration is loaded from environment variables and a ~/.config/raggit/raggit.env file generated by raggit setup. The setup command writes the file with 0600 permissions.
Environment variables
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | postgresql+asyncpg://raggit:raggit@localhost:5433/raggit | PostgreSQL connection string. |
QDRANT_URL | http://localhost:6333 | Qdrant URL. |
QDRANT_COLLECTION | raggit_chunks | Base Qdrant collection name. Actual collection is model-scoped. |
QDRANT_API_KEY | None | Qdrant API key, if required. |
LOG_LEVEL | INFO | Log level for console output. |
CHUNK_SIZE | 1024 | Target chunk size in tokens. |
CHUNK_OVERLAP | 0 | Overlap between chunks in tokens. |
CHUNKING_DEDUP_ENABLED | true | Remove near-duplicate chunks. |
CHUNKING_DEDUP_SIMILARITY | 0.92 | Jaccard similarity threshold for dedup. |
CHUNKING_FORMAT_AWARE | true | Use format-aware chunk boundaries. |
CHUNKING_PRESERVE_SECTIONS | true | Keep detected sections whole when possible. |
MIN_TOP_K | 5 | Minimum retrieved chunks. |
MAX_TOP_K | 50 | Maximum retrieved chunks. |
TOP_K_RATIO | 0.01 | Fraction of total chunks used to scale top-k. |
RRF_K | 60 | Reciprocal rank fusion constant. |
RETRIEVAL_PARENT_WINDOW | 0 | Expand hits by +/- N sibling chunks. |
RETRIEVAL_MIN_SCORE | None | Drop chunks below this score. |
RETRIEVAL_QUERY_REWRITE | none | Query rewrite: none, multi_query, hyde. |
RETRIEVAL_MULTI_QUERY_COUNT | 3 | Variants for multi_query. |
RETRIEVAL_TRAVERSAL_ENABLED | true | Relevance-chain traversal. |
RETRIEVAL_TRAVERSAL_MAX_STEPS | 10 | Max traversal steps. |
RETRIEVAL_TRAVERSAL_MIN_SCORE | 0.01 | Min score to continue traversal. |
RETRIEVAL_TRAVERSAL_DROP_RATIO | 0.5 | Score ratio that stops traversal. |
RERANKER_ENABLED | false | Cross-encoder reranking. |
RERANKER_MODEL | BAAI/bge-reranker-base | Reranker model name. |
RERANKER_TOP_N | 20 | Candidates to rerank. |
EMBEDDING_PROVIDER | sentence-transformers | Embedding provider. |
EMBEDDING_MODEL | BAAI/bge-small-en-v1.5 | Embedding model name. |
EMBEDDING_API_KEY | None | API key for remote embedding provider. |
EMBEDDING_BASE_URL | None | OpenAI-compatible embedding base URL. |
EMBEDDING_BATCH_SIZE | 32 | Texts per embedding batch. |
LLM_PROVIDER | openai | LLM provider: openai or ollama. |
LLM_MODEL | gpt-4o-mini | Model name. |
LLM_BASE_URL | None | OpenAI-compatible LLM base URL. |
LLM_API_KEY | None | LLM API key. |
LLM_TEMPERATURE | 0.1 | Sampling temperature. |
LLM_MAX_TOKENS | 2048 | Max response tokens. |
STORAGE_SOURCE_TYPE | local | Storage backend: local, s3, gcs, azure_blob. |
STORAGE_URI | ./data/documents | Storage URI or local path. |
STORAGE_BUCKET | None | S3/GCS bucket name. |
STORAGE_CONTAINER | None | Azure container name. |
STORAGE_PREFIX | None | Object prefix or folder. |
STORAGE_REGION | None | S3 region. |
STORAGE_AWS_ACCESS_KEY_ID | None | AWS access key ID. |
STORAGE_AWS_SECRET_ACCESS_KEY | None | AWS secret access key. |
STORAGE_GCS_SERVICE_ACCOUNT_PATH | None | GCS service account JSON path. |
STORAGE_AZURE_CONNECTION_STRING | None | Azure Blob connection string. |
STORAGE_POLL_INTERVAL_SECONDS | 30 | Poll interval for cloud watchers. |
SAFETY_REFUSE_ON_EMPTY | true | Refuse when no chunks retrieved. |
SAFETY_REFUSE_ON_LOW_SCORE | true | Refuse when scores are low. |
SAFETY_MIN_ANSWER_SCORE | 0.01 | Minimum answer score. |
SAFETY_GROUNDEDNESS_CHECK | true | Groundedness check. |
SAFETY_PII_REDACTION | false | Redact PII before embedding. |
SAFETY_PROMPT_INJECTION_HARDENING | true | Harden chunks against prompt injection. |
DEFAULT_TENANT_ID | None | Default tenant id. |
Docker Deployment
Build and run the entire stack with one command:
docker compose up -d
This starts:
raggit-postgreson port5433raggit-qdranton ports6333and6334raggit-apprunningraggit servewith the FastAPI server on port8000and the watcher enabled by default
Mount your documents into ./data/documents. The container entrypoint applies Alembic migrations before starting the service.
Useful commands
# View logs
docker compose logs -f raggit
# Run one-time ingestion inside the container
docker compose exec raggit raggit ingest
# Run a query
docker compose exec raggit raggit query "What is raggit?"
# Use the HTTP API from the host
curl http://localhost:8000/health
curl -X POST http://localhost:8000/query \
-H "Content-Type: application/json" \
-d '{"query": "What is raggit?"}'
# Stop everything
docker compose down
Development
raggit uses uv for dependency management and pytest for testing. The default dependency group includes dev tools and all cloud SDKs.
# Install dependencies
uv sync
# Run linting
uv run ruff check .
# Run type checking
uv run mypy raggit
# Run tests
uv run pytest
Project structure
raggit/
api/ # Pydantic models, public API types, and FastAPI server
cli/ # Typer CLI commands
core/ # Configuration, logging, audit, watcher service
db/ # SQLAlchemy models, repositories, sessions, vector store
eval/ # Evaluation: component (14 types) / pipeline (ingestion|retrieval|e2e) / system (E2E) / all; 69+ metrics
ingestion/ # Parsing, chunking, cleaning, PII, injection, embedding, indexer
llm/ # LLM providers and answer augmentation
retrieval/ # Sanitizer, rewrite, BM25, semantic, RRF, reranker, threshold, parent-window, traversal, safety
storage/ # Storage backends and factory
Adding a storage backend
- Subclass
raggit.storage.base.Storage. - Implement
list_files,read_file,file_exists,compute_hash,watch, andclose. - Register the backend in
raggit.storage.factory.create_storage. - Add corresponding fields to
raggit.api.models.StorageConfigif needed. - Add tests mirroring
tests/test_storage_s3.py.
Troubleshooting
Watcher is not detecting local file changes
- Verify you are running
raggit serve(the watcher is automatic – no separateraggit watchneeded). - Ensure the watched path is the exact directory where files are created and that the watcher is not disabled via
--no-watcherorRAGGIT_NO_AUTO_WATCHER=1. - Check
/watcher/statusto confirm the watcher is running; if not, start it viaPOST /watcher/startor restartraggit serve. - Network filesystems (NFS, some Docker volumes) may not emit reliable filesystem events. Use host paths when possible.
Initial sync is slow
- The first sync must read, parse, chunk, and embed every file. Subsequent syncs use the stat snapshot and skip unchanged files.
- Enable the sentence-transformers cache and ensure you have enough CPU/GPU for embedding.
Queries return no results
- Check
raggit statusto confirm documents arecompleted. - Verify the active embedding collection in
embedding_collectionsmatches the model you queried with. - Try lowering
--min-scoreor disabling--refuse-on-emptytemporarily.
Cloud watcher misses events
- Cloud watchers poll by default. Decrease
STORAGE_POLL_INTERVAL_SECONDSif you need faster detection. - For true real-time cloud watching, route provider events to a message queue and extend the watcher to consume it.
Permission denied on config file
raggit setupwrites~/.config/raggit/raggit.envwith0600permissions. If you created it manually, ensure it is readable by your user.
License
raggit is released under the MIT License.