Skip to main content

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:

  1. You type a question in plain English
  2. The system converts your question into a mathematical representation of its meaning
  3. It compares your question against all stored documents
  4. 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:

  1. Rate limit. One scan per 300 s across all workers, claimed with a self-expiring Redis key (pub:v1:{publication}:orphan-scan-claim).
  2. Redis rules out owned generations. From the operation journal it skips any generation whose operation is FENCED or ACTIVATED, holds the global or Client active_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 log publication_orphan_scan_failed.
  3. 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.
  4. Report every orphan. Each detection logs the identity, generation, owning operation, quiet time, lane counts, and deterministic cleanup_id for an operator. The latest successful scan's orphan count is persisted as orphan_generations in publication Redis global state and exposed by /metrics as infoconnect_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 eventLevelMeaning
publication_orphan_detectedwarningOne 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_skippedwarningInvalid operation evidence was skipped, with operation_id and reason (invalid_state, invalid_generation, invalid_registry_selection, or invalid_activity_timestamp).
publication_orphan_scan_completedinfoScan totals: candidates and detected; the detected count updates the publication runtime gauge's orphan_generations metric.
publication_orphan_scan_failedwarningThe scan stopped (error, error_type); the drain still replayed intents and re-armed. The next interval retries.
publication_generation_cleanup_finishedinfoOutcome 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 triggers classify_document_task; failed or retrying batches do not signal the barrier. A delayed fallback and reconciliation task recover a missed final signal or eventually record failed.
  • 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 → completed ownership 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_K representative 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​

StageQueueDescription
ValidationAPIFile size, type, auth
ParsingPreprocessingExtract 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
ChunkingPreprocessingSplit into overlapping chunks
Entity ExtractionEmbeddingspaCy NER for persons, orgs, dates, locations
Dense EmbeddingEmbeddingBAAI/bge-small-en-v1.5 (384-dim)
Sparse EmbeddingEmbeddingQdrant/bm25 sparse vectors
StorageEmbeddingUpsert chunk vectors and metadata to Qdrant
Chunk SelectionEmbeddingLegacy mode reads stored vectors; publication mode selects from staged node batches before generation embedding
ClassificationPreprocessingLegacy 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​

QueuePurposeProduction replicasConcurrencyContainer memoryChild recycle policy
preprocessingI/O-bound parsing/chunking + ModernBERT classification scoring11–2, host-planned2–4GB500 tasks or 1.5–3.5GiB RSS
embeddingCPU-bound NER/embeddings1–8, host-planned1 per replica2–2.5GB per replica250 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 /search fields, and upstream failures map to safe client messages

API Layer​

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
  • Embedding Queue: CPU/GPU-bound tasks

    • 1–8 host-planned replicas in Docker Compose (prefork pool, --concurrency=1 each)
    • 2–2.5GB memory limit per replica
    • Tasks: NER extraction, dense embedding, sparse embedding, Qdrant upsert, and publication representative-chunk selection

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, and publication ACL users; the default user is disabled
  • Strict append-only persistence (appendfsync everysec, truncated AOF loading disabled) and noeviction
  • Production maxmemory and 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: documents with text-dense (384-dim cosine) and text-sparse (BM25) vectors
  • Companion collection: documents-fuzzy-terms with client- and chunk-scoped text-fuzzy spelling-vocabulary vectors
  • Publication lanes: documents_publication_content, documents_publication_fuzzy, documents_metadata_v2, and documents_publication_manifest; each lane is a dedicated collection, isolated from the documents, documents-fuzzy-terms, and sparse-only documents_metadata_v1 collections. 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​

ServicePurposeTechnology
EmbedderServiceDense/sparse embedding generationFastEmbed (BAAI/bge-small-en-v1.5)
QdrantServiceVector storage and hybrid searchQdrant client with raw-score gates and RRF fusion
EntityExtractorNamed entity recognitionspaCy (en_core_web_sm)
SearchServiceHybrid retrieval + rerankingFastEmbed + cross-encoder
ModernBERTClassifierServiceDocument classificationonnx-community/ModernBERT-base-nli-ONNX

Hybrid Retrieval Architecture​

The system uses a hybrid retrieval approach combining multiple signals:

MethodStrengthsBest For
Dense (FastEmbed)Semantic understandingConceptual queries, paraphrases
Sparse (BM25)Exact keyword matchingTechnical terms, proper nouns
Hybrid (Fusion)Best of both worldsGeneral search, high recall
+ RerankingPrecision at top-kWhen result quality matters most

Retrieval Components​

  1. 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_eligible checks before returning evidence.
  2. Content embeddings:

    • Dense vectors use BAAI/bge-small-en-v1.5 (384 dimensions).
    • Sparse vectors use Qdrant/bm25.
    • alpha weights semantic and lexical retrieval; a hybrid request retains candidates admitted by the active lanes.
  3. 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.5 is 0.75.
    • Value eligibility runs before path context; path-only matches never qualify.
  4. 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_deleted affect lifecycle visibility without bypassing Client or generation checks.
  5. Lane contributions and reranking:

    • Content and Metadata produce lane-local ranks.
    • Both merges 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:

ServiceImageEnforced production memoryHealth Check
apiCustom multi-stage2–4GBHTTP /health
consoleCustom Next.js128–192MBHTTP /
worker-embedding replicasembedding-runtime2–2.5GB per replica-
worker-preprocessing-1api-runtime2–4GB-
redisredis:7.4.10@sha256:e7723ff73d963f5cc6d9c4643ea3d989527a402a319239054e9472a7fb9219a2256MB–2GB container; maxmemory retains 25% headroomREDISCLI_AUTH="$REDIS_APP_PASSWORD" redis-cli --user app --no-auth-warning ping
qdrantqdrant/v1.17.0Runtime planner budgetHTTP /healthz
host reserveHost OS/runtime512MB–1GB-

Shared Volumes:

  • ./models: Model cache for FastEmbed, spaCy, and ONNX artifacts
  • redis_data: Redis persistence
  • qdrant_data: Qdrant storage

Scaling Considerations​

BottleneckSolution
Search latencyAdd Qdrant replicas
Document backlogScale embedding workers (1 per GPU)
Memory pressureReduce batch size, restart workers frequently
API throughputRun multiple API instances behind load balancer

Data Flow Summary​

OperationData Flow
UploadClient → API → Redis → Celery → Qdrant
Browser searchBrowser → Next.js server proxy → API → FastEmbed → Qdrant → browser
Browser chatBrowser → Next.js chat route → selected LLM provider; search tool → API → Qdrant; streamed cited answer → browser
Direct API searchAPI client → API → FastEmbed → Qdrant → API client
StatusClient → API → Redis → Client
DeleteClient → API → Qdrant

Security & Reliability​

Rate Limiting​

  • Algorithm: Sliding window (60-second window)
  • Default limit: 30 requests/minute per resolved client id
  • Configuration: RATE_LIMIT environment variable
  • Scope: Applied to protected routes per client id; POST /search also has a search-specific per-client counter

Client Isolation (Multi-Tenancy)​

  • API key → client mapping: API_KEY_CLIENTS is a dict of api_key → client_id. Auth is resolved by resolve_client(x_api_key) in app/utils/security.py; a missing key or an empty client value returns None → 401.
  • Server-injected filter: Every Qdrant read and write passes a mandatory client_id filter. There is a single shared documents collection; the filter is applied server-side and cannot be bypassed by the caller.
  • Cross-client access returns 404: GET /status/{job_id} checks a jobmeta:{job_id} Redis binding. If the job belongs to a different client, the response is 404 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_CLIENTS is 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: