Skip to content

★ Enterprise Edition — not included in the Community Edition. Compare editions

RAG / AI Search Worker

The RAG (Retrieval-Augmented Generation) worker provides semantic email search. Instead of keyword matching, it understands the meaning of a query -- "emails about the Q3 budget discussion" finds relevant messages even when they never contain those exact words.

It converts email content into vector embeddings and stores them in Qdrant, a purpose-built vector database. It is a FastAPI service composed of an organization-config API, an admin API, and a health API, with search itself exposed to tenants through the API gateway.

What It Does

  • Embeds email content via pluggable providers -- default is local sentence-transformers (all-MiniLM-L6-v2); OpenAI, Azure OpenAI, and HuggingFace are configurable
  • Stores embeddings in Qdrant with per-organization tenant isolation
  • Per-organization indexing configuration (which fields to index, reindex triggers, status)
  • Admin surface: collection management, circuit-breaker resets, performance and error introspection
  • Webhook integration for indexing events

How It Works

flowchart LR
    subgraph RAG["RAG Worker :8090"]
        OrgAPI["Organization API\n/api/v1/organizations"]
        AdminAPI["Admin API\n/admin"]
        HealthAPI["Health API\n/health"]
        Embed["Embedding Service\n(sentence-transformers default)"]
        Index["Indexing Service"]
    end

    Gateway["API Gateway\n/api/v1/rag/*"] -->|"search / index"| RAG
    Index --> Embed
    Embed -->|"vectors"| Qdrant[("Qdrant :6333")]
    OrgAPI --> MySQL[(MySQL)]

Tenant-facing search and indexing go through the API gateway (worker/api/routes/rag.py) under /api/v1/rag/* -- /search, /rag/index/status/{domain}, /rag/index/trigger, collection and document management -- with org scoping enforced at the gateway.

API Endpoints

Copied from the routers in worker/rag/app.py and worker/rag/api/:

Text Only
# app.py
GET /            -- Service info
GET /config      -- Effective (sanitized) configuration
GET /metrics     -- Prometheus metrics

# health router (prefix /health)
GET /health/             -- Basic health
GET /health/detailed     -- Detailed health
GET /health/qdrant       -- Qdrant connectivity
GET /health/embedding    -- Embedding provider health
GET /health/collections  -- Collection health
GET /health/metrics      -- Health metrics

# organization router (prefix /api/v1/organizations)
GET    /api/v1/organizations/                                -- List configured orgs
GET    /api/v1/organizations/available-fields                -- Indexable fields
GET    /api/v1/organizations/{organization_id}/config        -- Get org RAG config
POST   /api/v1/organizations/{organization_id}/config        -- Set org RAG config
DELETE /api/v1/organizations/{organization_id}/config        -- Delete org config
GET    /api/v1/organizations/{organization_id}/indexing-status
POST   /api/v1/organizations/{organization_id}/reindex

# admin router (prefix /admin)
GET  /admin/health, /admin/metrics, /admin/status/detailed
GET  /admin/performance/{operation_name}, /admin/errors/recent
POST /admin/circuit-breaker/{service_name}/reset
GET  /admin/optimization/suggestions
GET  /admin/collections, /admin/collections/{name}/info
POST /admin/collections/{name}/optimize
GET  /admin/embedding/models
POST /admin/embedding/test
GET  /admin/config/current
POST /admin/maintenance/cleanup

# gateway compatibility router (root-mounted; api/gateway_api.py) -- serves
# the paths worker/api/routes/rag.py forwards for the public /api/v1/rag/*
# contract, org-scoped via the organization_id the gateway supplies
POST /rag/search
GET  /rag/index/status/{domain}
POST /rag/index/trigger
GET  /collections/{domain}
POST /collections/{domain}
GET  /collections/{domain}/stats
GET  /organizations/{organization_id}/collections
POST /documents/{collection_id}
GET  /organizations/{organization_id}/rag/documents
GET  /rag/config/domain/{domain}
POST /rag/config/domain/{domain}

Configuration

The service has a large config surface (worker/rag/config.py); the load-bearing variables:

Variable Default Description
QDRANT_HOST / QDRANT_PORT / QDRANT_URL qdrant / 6333 / http://qdrant:6333 Qdrant connection
QDRANT_COLLECTION_PREFIX mailrag Collection name prefix
QDRANT_VECTOR_SIZE derived from the embedding model (384 for the shipped default) Vector dimensions; collection creation follows the active model and warns when an explicit setting disagrees
EMBEDDING_PROVIDER sentence_transformers sentence_transformers, openai, azure_openai, huggingface
EMBEDDING_MODEL_NAME all-MiniLM-L6-v2 Model for the default provider
OPENAI_API_KEY / AZURE_OPENAI_* / HUGGINGFACE_API_KEY -- Only needed for hosted providers
DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD mysql / 3306 / mailserver / -- / -- MySQL connection
RAG_TENANT_ISOLATION_LEVEL / RAG_ORGANIZATION_CONFIGS -- Multi-tenancy behavior
SEARCH_TOP_K / SEARCH_SCORE_THRESHOLD / SEARCH_CACHE_TTL -- Search tuning
WEBHOOK_SERVICE_URL http://webhooks:8081 (compose) Webhook integration

Docker Configuration

YAML
rag:
  build:
    context: .
    dockerfile: ./worker/rag/Dockerfile
  container_name: rag
  ports:
    - "8091:8090"     # host 8091 -> container 8090
  depends_on:
    - mysql
    - migrate
    - redis
    - qdrant

qdrant:
  image: qdrant/qdrant:latest
  container_name: qdrant
  ports:
    - "6333:6333"
  volumes:
    - ./storage/qdrant_data:/qdrant/storage

In production (docker-compose.prod.yml) both host ports are bound to 127.0.0.1 only -- Qdrant carries no authentication of its own, which is exactly why it must never be published on 0.0.0.0.

Gotchas

Embedding costs

With a hosted provider (OpenAI/Azure), every indexed email and every query costs API credits. The default is a local sentence-transformers model for exactly this reason.

Qdrant memory

Qdrant keeps vectors in memory for fast search. A million 1536-dimension vectors is roughly 6 GB of RAM.