raggit

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.

Project status

raggit is in early development. The core ingestion, retrieval, and CLI flows are functional and tested. API server mode and analytics are on the roadmap.

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.

Quick Start

You need Docker Desktop (or Docker Engine + Compose) and uv installed.

  1. Start PostgreSQL and Qdrant

    docker compose up -d postgres qdrant

    PostgreSQL is mapped to host port 5433 to avoid conflicts with a local postgres on 5432.

  2. Install dependencies

    uv sync
  3. Run database migrations

    uv run alembic upgrade head
  4. 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_KEY

    raggit setup writes ~/.config/raggit/raggit.env with 0600 permissions and bootstraps the system by checking Postgres, running migrations, checking Qdrant, and creating the local document directory.

  5. Add documents

    mkdir -p data/documents
    cp my-docs/*.pdf data/documents/
  6. Run the service for continuous indexing

    uv run raggit serve

    raggit will perform an initial sync and then watch for new, modified, and deleted files. Local changes are reflected almost instantly.

  7. Ask questions

    uv run raggit query "What is raggit?"

See the storage backends section for S3, GCS, and Azure setup. The legacy raggit watch command is still available if you prefer live event indicators in the terminal.

Architecture

raggit is organized into clear layers: storage, ingestion, data, retrieval, LLM, audit, and interface. 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

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.

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_id and 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

FieldTypeDescription
idUUIDPrimary key.
source_typeenumlocal, s3, gcs, azure_blob.
source_uristringStable URI such as a local path or s3://bucket/key.
filenamestringRelative filename.
content_hashstringHash of file contents used to skip unchanged files.
file_sizeintegerCached file size for fast sync decisions.
file_modified_attimestamptzCached modification time for fast sync decisions.
statusenumpending, indexing, parsed, chunked, embedded, completed, failed, deleted.
error_messagetextFailure reason when status is failed.
tenant_idstringOptional tenant identifier.
tagsarrayOptional filter tags.
created_attimestamptzRow creation time.
updated_attimestamptzRow update time.
deleted_attimestamptzSoft-deletion timestamp.

chunks

FieldTypeDescription
idUUIDPrimary key.
document_idUUIDForeign key to documents.
chunk_indexintegerSequential index within the document.
raw_contenttextOriginal chunk text.
cleaned_contenttextNormalized chunk text used for embedding and BM25.
word_countintegerWord count for diagnostics.
embedding_modelstringModel that produced the embedding.
vector_idUUIDQdrant point ID.
fts_vectortsvectorPostgreSQL full-text search vector.
parent_chunk_indexintegerParent chunk for hierarchical chunking.
prev_chunk_idUUIDPrevious sibling chunk.
next_chunk_idUUIDNext sibling chunk.
section_titlestringSection or heading, if detected.
page_numberintegerPDF page number, if detected.
start_offsetintegerCharacter offset in source text.
end_offsetintegerCharacter offset in source text.
content_hashstringHash 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.

1
Watch local filesystem or remote object storage for add, modify, and delete events.
2
Parse PDF, DOCX, HTML, Markdown, and plain text. PDF pages are marked explicitly so page numbers survive chunking.
3
Chunk in a format-aware way: Markdown by headers, code by top-level definitions, PDF by page markers, and fallback recursive character splitting.
4
Size chunks by tokens using tiktoken cl100k_base when available, with configurable overlap.
5
Dedup near-identical chunks using content hash plus Jaccard word-set similarity.
6
Clean chunks by normalizing Unicode, collapsing whitespace, and fixing hyphenation.
7
Redact PII (optional) and harden against prompt-injection patterns before embedding.
8
Embed chunks using local sentence-transformers or an OpenAI-compatible API.
9
Store vectors in a model-scoped Qdrant collection and metadata/links in PostgreSQL.
10
Audit every ingestion input and output to the PostgreSQL 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.

1
Sanitize the query to extract keywords.
2
Rewrite the query when configured: multi_query generates alternative phrasings; hyde generates a hypothetical answer passage to embed.
3
Filter BM25 and semantic searches by metadata: source URI prefix, filename prefix, tenant, tags, document IDs, date range.
4
BM25 keyword search via PostgreSQL full-text search with a tsvector GIN index.
5
Semantic similarity search via Qdrant cosine distance.
6
Fuse ranked lists with weighted Reciprocal Rank Fusion with tunable BM25/semantic weights.
7
Rerank top-N candidates with an optional cross-encoder such as BAAI/bge-reranker-base.
8
Threshold low-confidence chunks with min_score and refuse empty or low-score retrieval.
9
Expand parent-document windows around each hit when parent_window > 0.
10
Cite every chunk with ID, source URI, filename, page, section, offsets, score, and excerpt.
11
Augment the prompt with isolated untrusted-context wrappers and system instructions.
12
Generate an answer via an OpenAI-compatible API or Ollama.
13
Check groundedness and warn if the answer is not supported by retrieved context.
14
Audit the query, retrieval result, and final answer to the PostgreSQL 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.

raggit watch (legacy mode)

raggit watch is preserved for users who want an explicit command with live +/- event indicators in the terminal. It uses the same WatcherService internally.

Deployment tip

The included docker-compose.yml runs raggit serve by default, so the container starts watching and indexing as soon as it comes up.

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. Watches storage and indexes changes automatically.
raggit ingest <path> One-time ingestion with a progress bar. Path is optional for cloud storage.
raggit watch <path> Continuously watch and index with live event indicators.
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.

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: --log-level, --tenant, --tag (repeatable).

ingest

uv run raggit ingest [PATH] [OPTIONS]

Options: --chunk-size, --chunk-overlap, --preserve-sections/--split-sections, --embedding-provider, --embedding-model, --log-level, --tenant, --tag.

watch

uv run raggit watch [PATH] [OPTIONS]

Options: --poll-interval, --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.

status

uv run raggit status

Shows a table of indexed documents and active embedding collections.

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 watcher

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_URLpostgresql+asyncpg://raggit:raggit@localhost:5433/raggitPostgreSQL connection string.
QDRANT_URLhttp://localhost:6333Qdrant URL.
QDRANT_COLLECTIONraggit_chunksBase Qdrant collection name. Actual collection is model-scoped.
QDRANT_API_KEYNoneQdrant API key, if required.
LOG_LEVELINFOLog level for console output.
CHUNK_SIZE1024Target chunk size in tokens.
CHUNK_OVERLAP0Overlap between chunks in tokens.
CHUNKING_DEDUP_ENABLEDtrueRemove near-duplicate chunks.
CHUNKING_DEDUP_SIMILARITY0.92Jaccard similarity threshold for dedup.
CHUNKING_FORMAT_AWAREtrueUse format-aware chunk boundaries.
CHUNKING_PRESERVE_SECTIONStrueKeep detected sections whole when possible.
MIN_TOP_K5Minimum retrieved chunks.
MAX_TOP_K50Maximum retrieved chunks.
TOP_K_RATIO0.01Fraction of total chunks used to scale top-k.
RRF_K60Reciprocal rank fusion constant.
RETRIEVAL_PARENT_WINDOW0Expand hits by +/- N sibling chunks.
RETRIEVAL_MIN_SCORENoneDrop chunks below this score.
RETRIEVAL_QUERY_REWRITEnoneQuery rewrite: none, multi_query, hyde.
RETRIEVAL_MULTI_QUERY_COUNT3Variants for multi_query.
RETRIEVAL_TRAVERSAL_ENABLEDtrueRelevance-chain traversal.
RETRIEVAL_TRAVERSAL_MAX_STEPS10Max traversal steps.
RETRIEVAL_TRAVERSAL_MIN_SCORE0.01Min score to continue traversal.
RETRIEVAL_TRAVERSAL_DROP_RATIO0.5Score ratio that stops traversal.
RERANKER_ENABLEDfalseCross-encoder reranking.
RERANKER_MODELBAAI/bge-reranker-baseReranker model name.
RERANKER_TOP_N20Candidates to rerank.
EMBEDDING_PROVIDERsentence-transformersEmbedding provider.
EMBEDDING_MODELBAAI/bge-small-en-v1.5Embedding model name.
EMBEDDING_API_KEYNoneAPI key for remote embedding provider.
EMBEDDING_BASE_URLNoneOpenAI-compatible embedding base URL.
EMBEDDING_BATCH_SIZE32Texts per embedding batch.
LLM_PROVIDERopenaiLLM provider: openai or ollama.
LLM_MODELgpt-4o-miniModel name.
LLM_BASE_URLNoneOpenAI-compatible LLM base URL.
LLM_API_KEYNoneLLM API key.
LLM_TEMPERATURE0.1Sampling temperature.
LLM_MAX_TOKENS2048Max response tokens.
STORAGE_SOURCE_TYPElocalStorage backend: local, s3, gcs, azure_blob.
STORAGE_URI./data/documentsStorage URI or local path.
STORAGE_BUCKETNoneS3/GCS bucket name.
STORAGE_CONTAINERNoneAzure container name.
STORAGE_PREFIXNoneObject prefix or folder.
STORAGE_REGIONNoneS3 region.
STORAGE_AWS_ACCESS_KEY_IDNoneAWS access key ID.
STORAGE_AWS_SECRET_ACCESS_KEYNoneAWS secret access key.
STORAGE_GCS_SERVICE_ACCOUNT_PATHNoneGCS service account JSON path.
STORAGE_AZURE_CONNECTION_STRINGNoneAzure Blob connection string.
STORAGE_POLL_INTERVAL_SECONDS30Poll interval for cloud watchers.
SAFETY_REFUSE_ON_EMPTYtrueRefuse when no chunks retrieved.
SAFETY_REFUSE_ON_LOW_SCOREtrueRefuse when scores are low.
SAFETY_MIN_ANSWER_SCORE0.01Minimum answer score.
SAFETY_GROUNDEDNESS_CHECKtrueGroundedness check.
SAFETY_PII_REDACTIONfalseRedact PII before embedding.
SAFETY_PROMPT_INJECTION_HARDENINGtrueHarden chunks against prompt injection.
DEFAULT_TENANT_IDNoneDefault tenant id.

Docker Deployment

Build and run the entire stack with one command:

docker compose up -d

This starts:

  • raggit-postgres on port 5433
  • raggit-qdrant on ports 6333 and 6334
  • raggit-app running raggit serve (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?"

# 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 and public API types
  cli/            # Typer CLI commands
  core/           # Configuration, logging, audit, watcher service
  db/             # SQLAlchemy models, repositories, sessions, vector store
  ingestion/      # Parsing, chunking, cleaning, embedding, indexer
  llm/            # LLM providers and answer augmentation
  retrieval/      # Search, RRF, reranker, safety
  storage/        # Storage backends and factory

Adding a storage backend

  1. Subclass raggit.storage.base.Storage.
  2. Implement list_files, read_file, file_exists, compute_hash, watch, and close.
  3. Register the backend in raggit.storage.factory.create_storage.
  4. Add corresponding fields to raggit.api.models.StorageConfig if needed.
  5. Add tests mirroring tests/test_storage_s3.py.

Troubleshooting

Watcher is not detecting local file changes

  • Verify you are running raggit serve or raggit watch.
  • Ensure the watched path is the exact directory where files are created.
  • 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 status to confirm documents are completed.
  • Verify the active embedding collection in embedding_collections matches the model you queried with.
  • Try lowering --min-score or disabling --refuse-on-empty temporarily.

Cloud watcher misses events

  • Cloud watchers poll by default. Decrease STORAGE_POLL_INTERVAL_SECONDS if 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 setup writes ~/.config/raggit/raggit.env with 0600 permissions. If you created it manually, ensure it is readable by your user.

License

raggit is released under the MIT License.

Back to top