Architecture
Executive Summary
The InfoConnect Hybrid Search Engine is like a smart filing system that reads your documents, understands what they mean, and finds them when you need them.
Think of it like a restaurant: you place an order (upload a document), the kitchen prepares it (workers process it), and everything is stored in the pantry (database) ready to serve when requested.
The system has four internal parts. The console's optional Chat view also calls the operator-selected LLM provider as an external model service:
- The Operator Console (Web UI): Gives trusted operators cited chat, search, and document-management workflows. Backend calls use a server-only key; Chat uses an operator-supplied provider key per request. The console must remain behind an external auth boundary
- The Front Desk (API): Takes your requests and gives you answers
- The Kitchen (Workers): Processes documents behind the scenes
- The Pantry (Storage): Keeps everything organized and ready to find
How a Document Moves Through the System
When you upload a document, it goes through four stages before it is ready to search:
Step 1: Upload
You send a file to the system through the API. The front desk checks that the file is valid (right type, not too big) and gives you a ticket number to track progress.
Step 2: Parse
Workers read the document and pull out the text. If it is a PDF, Word doc, or plain text, they extract the words. If it is an image or scanned page, they use AI to read it like a person would.
Step 3: Understand
The system breaks the text into small chunks and, during embedding, uses AI to:
- Identify names, dates, and organizations mentioned in the text
- Turn each chunk into vectors that capture both meaning and keywords
Step 4: Store and Categorize
After all chunks are embedded, the system selects representative chunks and classifies the document. Then all the information is saved in a special database that understands meaning, not just exact words. This lets the system find documents even when you search using different words than what is written.
Upload → Parse → Understand → Store & Categorize
↓ ↓ ↓ ↓
API Workers Workers Database
How Search Works
Searching works like asking a librarian who knows every book in the library:
- You type a question in plain English
- The system converts your question into a mathematical representation of its meaning
- It compares your question against all stored documents
- Returns the best matches ranked by relevance, showing you the most useful documents first
The system uses two methods at once:
- Semantic search: Finds documents about the same concept, even with different words
- Keyword search: Finds exact word matches for technical terms or names
Results are combined so you get the best of both approaches.
Atomic generation publication
When PUBLICATION_ENABLED=true, a processed Document Version is staged as one immutable
generation across Content, fuzzy-term, Metadata, and manifest lanes in Qdrant.
Unsealed upload data stays invisible to Document lists and search. The staging
and verification contract is owned by ADR-0011.
Redis DB 3 then captures the live worker roster, fences every captured incarnation, atomically changes the four-lane active
registry under one global publication mutex, and waits for every captured worker to converge
before marking the Client ready. While the Client is fenced, activation selects exactly one
authoritative Qdrant Manifest for the Document Version with strong, waited writes. Candidate
lane evidence remains immutable and carries generation tags without an activation flag. Redis retains the
journal, fence, and cached registry, but only the Qdrant Manifest can admit a generation at read
time. The detailed ordering and recovery
contract is owned by ADR-0011.
Each search or document read uses one local immutable snapshot. Qdrant retrieves raw candidates under the exact Client, request-filter, publication-eligibility, and lifecycle scope, then batch reads their deterministic generation Manifest IDs. Non-authoritative generations are discarded before Document Version ranking; missing, malformed, unreadable, or conflicting Manifest authority fails the whole request under the publication error contract. Adaptive widening prevents sealed but non-authoritative or superseded generations in an initial raw window from starving authoritative results.
Orphan generation reconciler
An orphan is a generation with Content, Fuzzy, or Metadata points but no Manifest point, no registry selection, and no live publication operation. Newly staged orphan lanes remain dark and do not disrupt reads; the sweep still counts them for hygiene. Pre-existing eligible orphan points or corrupted eligibility can still fail reads closed under the publication error contract. Stage-dark does not repair those records automatically.
The reconciler only detects and alerts. It never records cleanup intents, enqueues cleanup,
or deletes points. The cleanup-intent drain (drain_publication_cleanup_intents, every
60 s on the preprocessing queue) runs an orphan scan after replaying pending cleanup intents:
- Rate limit. One scan per 300 s across all workers, claimed with a self-expiring
Redis key (
pub:v1:{publication}:orphan-scan-claim). - Redis rules out owned generations. From the operation journal it skips any
generation whose operation is
FENCEDorACTIVATED, holds the global or Clientactive_op, or recorded activity (creation, an embedding batch claim, or a fence) within the publication delivery window (CELERY_EMBED_TIME_LIMIT_SEC × (retries + 1)plus the retry backoff; 87 min by default). It also skips the registry-selected generation, generations at or below a Hard Delete purge watermark, and identities the authority no longer knows. An operation an upload outbox still names is live however quiet its journal is; an outbox the scan cannot attribute to an operation, or a failure to enumerate the upload directory, stops the scan. Unknown operation states and nonpositive generations, registry selections, or activity timestamps are skipped and logged. Malformed idempotency records, nonnumeric generation or activity fields, and invalid identity catalogue records stop the scan and logpublication_orphan_scan_failed. - Qdrant proves the orphan. For each remaining generation, exact per-lane counts: a Manifest point, or no Content/Fuzzy/Metadata points, means it is not an orphan.
- Report every orphan. Each detection logs the identity, generation, owning operation,
quiet time, lane counts, and deterministic
cleanup_idfor an operator. The latest successful scan's orphan count is persisted asorphan_generationsin publication Redis global state and exposed by/metricsasinfoconnect_publication_runtime{metric="orphan_generations"}. Alert when this gauge is greater than zero. Failed or throttled scans preserve the previous count.
Detection starts once an upload outbox is gone and its operation has been quiet for the publication delivery window, within one scan interval after that. Repeated scans report an unresolved orphan again. A scan costs four exact Qdrant counts per quiet, unselected generation in the journal.
Cleanup remains manual. Follow the generation recovery procedure, including its quiescence requirement when writer admission is retained. The next successful scan reports zero once no orphan lane evidence remains.
| Log event | Level | Meaning |
|---|---|---|
publication_orphan_detected | warning | One reported orphan; no cleanup is scheduled. Carries cleanup_id, client_id, document_id, version_id, identity_id, generation, operation_id, operation_state, quiet_seconds, and content_points/fuzzy_points/metadata_points. |
publication_orphan_operation_skipped | warning | Invalid operation evidence was skipped, with operation_id and reason (invalid_state, invalid_generation, invalid_registry_selection, or invalid_activity_timestamp). |
publication_orphan_scan_completed | info | Scan totals: candidates and detected; the detected count updates the publication runtime gauge's orphan_generations metric. |
publication_orphan_scan_failed | warning | The scan stopped (error, error_type); the drain still replayed intents and re-armed. The next interval retries. |
publication_generation_cleanup_finished | info | Outcome of a manual or existing generation cleanup, keyed by cleanup_id: status is cleaned, already_cleaned, or preserved_active, with deleted_points. |
Main Services
API Service (The Front Desk)
The API is the public face of the system. It handles:
- Receiving document uploads
- Answering search queries
- Checking processing status
- Managing your documents (list, delete)
- Health monitoring
The API sits on port 8000 and speaks HTTP, the same language web browsers use.
Preprocessing Workers (The Prep Cooks)
These workers handle the first phase of document processing and ModernBERT scoring:
- Read different file formats (PDF, Word, text, images)
- Split long documents into manageable chunks
- Score representative chunks after legacy embedding or before publication embedding
Docker Compose runs one preprocessing worker by default (worker-preprocessing-1) with host-planned concurrency and a 2–4GB container memory limit, while local development (just dev) runs one preprocessing worker with --concurrency=4. This work is I/O bound, so higher concurrency is useful because workers spend time waiting for disk/network operations.
Embedding Workers (The Sous Chefs)
These workers handle the heavy AI processing:
- Extract named entities (people, organizations, dates, places)
- Create dense vector embeddings (mathematical meaning representations)
- Create sparse vector embeddings (keyword importance scores)
- Save everything to the database
Embedding workers process embedding tasks with concurrency 1 per replica. The host-wide production plan scales replicas and assigns 2–2.5GB to each one.
Post-Embedding Classification (The Specialist)
ModernBERT ONNX handles document classification around the embedding stage.
- Legacy trigger: With publication disabled, preprocessing sets a version-scoped Redis
atomic counter at
classification_pending:{client_id}:{document_id}:{version_id}(TTL: 1 hour). Each successfully completed embedding batch decrements it, and the last successful batch triggersclassify_document_task; failed or retrying batches do not signal the barrier. A delayed fallback and reconciliation task recover a missed final signal or eventually recordfailed. - Publication trigger: With publication enabled, classification completes before the immutable generation is embedded and activated. The embedding queue selects representative chunks from the staged node batches; the preprocessing queue scores them, then dispatches the generation's embedding chord and publication finalizer.
- Publication dispatch recovery: A fsynced outbox beside the staged upload records each
selector, classification-completion, and publication dispatch before it reaches Celery. One
atomic
accepted→running→completedownership boundary and stable task IDs make replay safe when the broker accepted a dispatch but its reply was lost. Retried parent or downstream tasks resolve the same outbox entry instead of creating a second classification or publication flow. - Terminal failures: Classification retries retryable storage and model failures. Legacy mode
writes
classification.status: "failed"after bounded recovery; publication mode leaves the unclassified generation inactive and fails closed. - Chunk selection: Legacy mode reads chunk embeddings back from Qdrant. Publication mode
computes selection embeddings from a bounded, evenly-strided sample of at most 256 staged
chunks before generation embedding, keeping selector work bounded on CPU-only workers. Both
use centroid + MMR to select
CLASSIFICATION_SELECT_TOP_Krepresentative chunks (default: 3). - Scoring: ModernBERT ONNX NLI scores the selected chunks against the configured category list.
- Latency: End-to-end classification typically takes 2-4 seconds per document.
The classifier still runs locally with ONNX Runtime, so no separate model-serving tier is required.
Storage Services
Redis: ACL-isolated logical databases hold application runtime data, Celery broker and result state, and the publication journal, fences, worker roster, and cached registry. Qdrant Manifests own read-time generation eligibility.
Qdrant: The vector database that stores documents in a way that supports semantic search. It keeps both the mathematical meaning (dense vectors) and keyword information (sparse vectors) for each document chunk.
Why There Are Two Worker Queues
Imagine a kitchen with two stations: one for quick prep work and one for careful cooking.
Preprocessing Queue: Like washing and chopping vegetables. It is quick work that happens in bursts while waiting for the fridge to open. Multiple people can chop vegetables at the same station without getting in each other"s way.
Embedding Queue: Like carefully preparing a sauce that needs constant attention. It requires focus and resources. If too many chefs try to make sauces at once, they will run out of burners and pots.
Separating them lets us run preprocessing and embedding with different concurrency profiles (Docker default: preprocessing 1×2, embedding 2×1; local dev: preprocessing 1×4, embedding 2×1), matching their resource needs.
Storage at a Glance
Documents live in Qdrant, organized like a card catalog that understands meaning:
- Each document is split into chunks (about 512 words each)
- Every chunk gets stored with:
- A dense vector (captures semantic meaning)
- A sparse vector (captures keyword importance)
- Metadata (document ID, page number, categories)
- Entities (people, organizations, dates found in the text)
The database supports hybrid search, combining semantic and keyword matching to find the best results.
Redis separates application, Celery, and publication state across ACL-isolated logical databases. It keeps track of:
- Jobs waiting to be processed
- Jobs currently being worked on
- Recent results for quick repeat access
- The durable publication journal and cached active-generation registry used for fencing and recovery
Advanced Technical Details
Click to expand technical specifications and architecture diagrams
System Overview
The InfoConnect Hybrid Search Engine consists of the following components:
- Operator Console (Next.js): Internal-only search, cited BYOK chat, and document operations through same-origin server routes
- API Service (FastAPI): HTTP endpoints for document upload, search, status, and management
- Task Queue (Redis + Celery): Asynchronous document processing with dedicated queues
- Vector Store (Qdrant): Hybrid vector database with dense and sparse indices
- Workers: Prefork-based Celery workers for preprocessing and embedding
- Embedding Service (FastEmbed): Generates dense and sparse embeddings
- RapidOCR Service:
RAPIDOCR_MODEL_NAME(rapidocr-onnxruntime) extracts text from images and low-text PDF pages - Classification: ModernBERT ONNX runs after legacy embedding or before publication embedding
- Entity Extractor (spaCy): NER for persons, organizations, dates, locations
Architecture Diagram
Document Processing Pipeline
When a document is uploaded via POST /embed:
Processing Stages
| Stage | Queue | Description |
|---|---|---|
| Validation | API | File size, type, auth |
| Parsing | Preprocessing | Extract text from PDF/DOCX/PPTX/XLSX/TXT/Images; OCR fallback uses RAPIDOCR_MODEL_NAME (rapidocr-onnxruntime) for images and low-text PDF pages. Legacy .ppt/.xls converted via LibreOffice when OFFICE_LEGACY_CONVERSION_ENABLED=true |
| Chunking | Preprocessing | Split into overlapping chunks |
| Entity Extraction | Embedding | spaCy NER for persons, orgs, dates, locations |
| Dense Embedding | Embedding | BAAI/bge-small-en-v1.5 (384-dim) |
| Sparse Embedding | Embedding | Qdrant/bm25 sparse vectors |
| Storage | Embedding | Upsert chunk vectors and metadata to Qdrant |
| Chunk Selection | Embedding | Legacy mode reads stored vectors; publication mode selects from staged node batches before generation embedding |
| Classification | Preprocessing | Legacy mode classifies after embedding; publication mode scores before dispatching the generation embedding chord. See Post-Embedding Classification for recovery behavior |
Search Pipeline
When a query arrives at POST /search, the server resolves the Client from the API key,
applies the requested scope (Content, Metadata, or Both), and takes one immutable
active-generation snapshot. Content is the default scope; Metadata and Both are explicit
opt-ins.
Content retrieval combines dense semantic and sparse lexical candidates; alpha controls
their weighting. Metadata uses the typed eligibility contract described in the
metadata-search guide across exact, lexical, and semantic
branches, including paged evidence hydration. Metadata checks the value before path context,
and a path-only match never qualifies. Exact filters constrain the candidate set without
changing rank. Every returned row is a Document Version;
pagination and totals count Document Versions rather than chunks or Metadata Leaves.
Celery Task Queues
| Queue | Purpose | Production replicas | Concurrency | Container memory | Child recycle policy |
|---|---|---|---|---|---|
preprocessing | I/O-bound parsing/chunking + ModernBERT classification scoring | 1 | 1–2, host-planned | 2–4GB | 500 tasks or 1.5–3.5GiB RSS |
embedding | CPU-bound NER/embeddings | 1–8, host-planned | 1 per replica | 2–2.5GB per replica | 250 tasks or 1.5–2GiB RSS |
Note: The host-wide runtime plan emits container limits and separate Celery child recycle thresholds. Classification scoring runs on preprocessing; legacy reconciliation also runs there, while publication selection runs on embedding. See Post-Embedding Classification for the authoritative trigger and recovery behavior.
Task Routing
# preprocessing tasks
process_document.apply_async(queue='preprocessing')
classify_document.apply_async(queue='preprocessing')
# embedding tasks (automatic from preprocessing chain)
extract_entities_and_embed.apply_async(queue='embedding')
Qdrant Collection Schema
Collection: documents
Vectors:
- text-dense: 384-dim float32 (cosine similarity)
- text-sparse: BM25 sparse vector
Payload:
- document_id: string
- chunk_id: string (format: "doc_id:index")
- chunk_text: string
- chunk_index: integer
- page_number: integer
- categories: string[] (classification results)
- metadata: object (user-provided + classification)
- entities: object (NER results)
- persons: string[]
- organizations: string[]
- dates: string[]
- locations: string[]
- monetary_amounts: string[]
- account_numbers: string[]
- transaction_refs: string[]
- account_types: string[]
System Components
Operator Console
- Next.js App Router application under
frontend/ - Scope: Trusted-operator cited chat, search, document management, upload, job status, health, and metrics; destructive operations are intentionally available
- Access boundary: Must not be public; deployment requires external SSO, a gateway, or an internal-only network because the app adds no application-level authentication
- Credential boundary: Backend proxy routes add the backend API key from server-only environment variables. Chat instead accepts an operator-supplied key for the selected LLM provider per request, keeps no server-side history, and never passes that key to the Python API
- Backend contract: Shared proxy transport fails closed, search forwards an allow-list of
POST /searchfields, and upstream failures map to safe client messages
API Layer
- FastAPI application on port 8000
- Endpoints: See the search, ingestion, document management, status, health, and metadata update references
- Rate limiting: 30 requests/minute per resolved client id for protected routes, with an additional
/searchcounter (configurable viaRATE_LIMIT) - Health checks: See the Health API component status
- Lifecycle management: Async context managers for proper service startup/shutdown
Task Processing Layer
Celery Workers: Two separate worker pools for different workload types:
-
Preprocessing Queue: I/O-bound tasks
- 1 worker in Docker Compose (prefork pool, host-planned
--concurrency=1–2) - 1 worker in local dev with
--concurrency=4 - 2–4GB host-planned Docker container memory limit
- Tasks: PDF parsing, DOCX parsing, text chunking, OCR fallback, classification scoring, and publication finalization
- 1 worker in Docker Compose (prefork pool, host-planned
-
Embedding Queue: CPU/GPU-bound tasks
- 1–8 host-planned replicas in Docker Compose (prefork pool,
--concurrency=1each) - 2–2.5GB memory limit per replica
- Tasks: NER extraction, dense embedding, sparse embedding, Qdrant upsert, and publication representative-chunk selection
- 1–8 host-planned replicas in Docker Compose (prefork pool,
Legacy task chain (historical — PUBLICATION_ENABLED=false no longer boots; the API fails fast at startup when publication is disabled):
The publication-enabled classification and dispatch contract is owned by Post-Embedding Classification above.
Reliability:
- Exponential backoff retry (max 3 retries)
- Embedding workers recycle child processes after 250 tasks or 1.5–2GiB RSS; preprocessing workers recycle after 500 tasks or 1.5–3.5GiB RSS
- Docker container limits are independently enforced from the shared host-wide plan: 2–2.5GB per embedding replica and 2–4GB for preprocessing
- PyTorch cache cleared on worker startup
- Model warm-up on service initialization
Data Layer
Redis (redis:7.4.10@sha256:e7723ff73d963f5cc6d9c4643ea3d989527a402a319239054e9472a7fb9219a2):
- DB 0: Application runtime data, including rate limits, classification counters, and metrics
- DB 1: Celery task broker
- DB 2: Celery result backend (24h TTL)
- DB 3: Durable publication journal, fencing state, worker roster, and cached registry
- Prefix-limited
app,celery, andpublicationACL users; the default user is disabled - Strict append-only persistence (
appendfsync everysec, truncated AOF loading disabled) andnoeviction - Production
maxmemoryand the Redis container limit come from the shared host-wide runtime plan; direct development runs retain capacity-based derivation
Qdrant (v1.17.0):
- Primary collection:
documentswithtext-dense(384-dim cosine) andtext-sparse(BM25) vectors - Companion collection:
documents-fuzzy-termswith client- and chunk-scopedtext-fuzzyspelling-vocabulary vectors - Publication lanes:
documents_publication_content,documents_publication_fuzzy,documents_metadata_v2, anddocuments_publication_manifest; each lane is a dedicated collection, isolated from thedocuments,documents-fuzzy-terms, and sparse-onlydocuments_metadata_v1collections. The Metadata V2 lane stores dense and sparse vectors of each rendered value. Candidate records are immutable and generation-tagged; the Manifest lane is the sole read-time authority. - HTTP port: 6333, GRPC port: 6334
- Persistent volume:
qdrant_data
Core Services
| Service | Purpose | Technology |
|---|---|---|
| EmbedderService | Dense/sparse embedding generation | FastEmbed (BAAI/bge-small-en-v1.5) |
| QdrantService | Vector storage and hybrid search | Qdrant client with raw-score gates and RRF fusion |
| EntityExtractor | Named entity recognition | spaCy (en_core_web_sm) |
| SearchService | Hybrid retrieval + reranking | FastEmbed + cross-encoder |
| ModernBERTClassifierService | Document classification | onnx-community/ModernBERT-base-nli-ONNX |
Hybrid Retrieval Architecture
The system uses a hybrid retrieval approach combining multiple signals:
| Method | Strengths | Best For |
|---|---|---|
| Dense (FastEmbed) | Semantic understanding | Conceptual queries, paraphrases |
| Sparse (BM25) | Exact keyword matching | Technical terms, proper nouns |
| Hybrid (Fusion) | Best of both worlds | General search, high recall |
| + Reranking | Precision at top-k | When result quality matters most |
Retrieval Components
-
Scope and Client resolution:
- Resolve the Client from
X-API-Key; never accept caller-supplied tenancy. - Run Content by default, with Metadata and Both as explicit opt-ins.
- Require active-generation and
publication_eligiblechecks before returning evidence.
- Resolve the Client from
-
Content embeddings:
- Dense vectors use
BAAI/bge-small-en-v1.5(384 dimensions). - Sparse vectors use
Qdrant/bm25. alphaweights semantic and lexical retrieval; a hybrid request retains candidates admitted by the active lanes.
- Dense vectors use
-
Metadata eligibility:
- The typed contract governs structured whole-value identities, mixed hard components, and descriptive lexical plus independent semantic candidates; see the metadata-search guide.
- The immutable Metadata dense semantic floor for
BAAI/bge-small-en-v1.5is0.75. - Value eligibility runs before path context; path-only matches never qualify.
-
Exact request filters:
- Filters constrain candidate eligibility and use AND across keys with OR within list values.
- Filters do not change rank.
- Soft Delete and
include_deletedaffect lifecycle visibility without bypassing Client or generation checks.
-
Lane contributions and reranking:
- Content and Metadata produce lane-local ranks.
Bothmerges the lanes at one Document Version identity and returns those contributions alongside bounded Content and Metadata Evidence Sets.- Optional Content reranking (
Xenova/ms-marco-MiniLM-L-6-v2) can reorder Content evidence; it does not change the Evidence Result contract.
Deployment Options
Running the System
# Start all services (API, workers, Redis, Qdrant)
just prod-up
# Verify everything is running
just status
# Stop when done
just prod-down
Production Docker Compose
All services containerized with health checks and resource limits:
| Service | Image | Enforced production memory | Health Check |
|---|---|---|---|
| api | Custom multi-stage | 2–4GB | HTTP /health |
| console | Custom Next.js | 128–192MB | HTTP / |
| worker-embedding replicas | embedding-runtime | 2–2.5GB per replica | - |
| worker-preprocessing-1 | api-runtime | 2–4GB | - |
| redis | redis:7.4.10@sha256:e7723ff73d963f5cc6d9c4643ea3d989527a402a319239054e9472a7fb9219a2 | 256MB–2GB container; maxmemory retains 25% headroom | REDISCLI_AUTH="$REDIS_APP_PASSWORD" redis-cli --user app --no-auth-warning ping |
| qdrant | qdrant/v1.17.0 | Runtime planner budget | HTTP /healthz |
| host reserve | Host OS/runtime | 512MB–1GB | - |
Shared Volumes:
./models: Model cache for FastEmbed, spaCy, and ONNX artifactsredis_data: Redis persistenceqdrant_data: Qdrant storage
Scaling Considerations
| Bottleneck | Solution |
|---|---|
| Search latency | Add Qdrant replicas |
| Document backlog | Scale embedding workers (1 per GPU) |
| Memory pressure | Reduce batch size, restart workers frequently |
| API throughput | Run multiple API instances behind load balancer |
Data Flow Summary
| Operation | Data Flow |
|---|---|
| Upload | Client → API → Redis → Celery → Qdrant |
| Browser search | Browser → Next.js server proxy → API → FastEmbed → Qdrant → browser |
| Browser chat | Browser → Next.js chat route → selected LLM provider; search tool → API → Qdrant; streamed cited answer → browser |
| Direct API search | API client → API → FastEmbed → Qdrant → API client |
| Status | Client → API → Redis → Client |
| Delete | Client → API → Qdrant |
Security & Reliability
Rate Limiting
- Algorithm: Sliding window (60-second window)
- Default limit: 30 requests/minute per resolved client id
- Configuration:
RATE_LIMITenvironment variable - Scope: Applied to protected routes per client id;
POST /searchalso has a search-specific per-client counter
Client Isolation (Multi-Tenancy)
- API key → client mapping:
API_KEY_CLIENTSis a dict ofapi_key → client_id. Auth is resolved byresolve_client(x_api_key)inapp/utils/security.py; a missing key or an empty client value returnsNone→ 401. - Server-injected filter: Every Qdrant read and write passes a mandatory
client_idfilter. There is a single shareddocumentscollection; the filter is applied server-side and cannot be bypassed by the caller. - Cross-client access returns 404:
GET /status/{job_id}checks ajobmeta:{job_id}Redis binding. If the job belongs to a different client, the response is404 Job not found— not 403 — to avoid leaking the existence of other clients' jobs. The same principle applies to delete and update operations on another client's document. - Fail-closed when unconfigured: If
API_KEY_CLIENTSis empty at startup, the application logs a CRITICAL banner and every request to a protected endpoint returns 401 until keys are configured.
Error Handling
- Celery retries: Exponential backoff with max 3 retries
- Rerank timeout: Graceful degradation (returns unranked results after 2 seconds)
- Memory protection: Host-planned limits cap embedding replicas at 2–2.5GB and preprocessing at 2–4GB; child recycle thresholds remain below each container ceiling
- Structured logging: All failures logged with
structlog
Observability
- Logging: Structured JSON logs via
structlog - Health checks: See the Health API response codes for serving and dependency failure signals
- Job tracking: Celery result backend tracks job status
- Performance metrics: Query timing included in search responses
- Container health: Docker health checks for all services
What's Next
Now that you understand how the system works, you might want to:
- Set up the system: Follow the Installation Guide to get running
- Process your first document: Check out the First Document Guide
- Configure the system: See Configuration for customization options
- Learn the API: Reference the Embed API or Search API for endpoint details
- Explore retrieval settings: Visit Embeddings and Retrieval for search tuning