< Architecture | Deep Dive | Quick Start
This document covers operational procedures for monitoring, debugging, and maintaining the Self-Hosted RAG system.
The chunks.duckdb database (in $DEFAULT_DOC_INGEST_ROOT) is the primary source of truth for all document state.
duckdb $DEFAULT_DOC_INGEST_ROOT/chunks.duckdbSELECT original_filename, status, worker_id, new_at
FROM ingestion_lifecycle
ORDER BY new_at DESC;SELECT id, original_filename, status
FROM ingestion_lifecycle
WHERE status NOT LIKE '%SUCCESS%'
AND status NOT LIKE '%FAILED%'
AND new_at < (CURRENT_TIMESTAMP - INTERVAL 1 HOUR);SELECT
original_filename,
preprocessing_complete_at - preprocessing_at AS normalization_time,
ingesting_at - preprocessing_complete_at AS chunking_time,
consuming_at - ingesting_at AS queue_time,
finalized_at - consuming_at AS persistence_time,
finalized_at - new_at AS total_turnaround
FROM ingestion_lifecycle
WHERE status = 'INGEST_SUCCESS';SELECT original_filename, error_log
FROM ingestion_lifecycle
WHERE status = 'INGEST_FAILED'
ORDER BY finalized_at DESC;SELECT status, pdf_path, md_path
FROM ingestion_lifecycle
WHERE original_filename LIKE '%my_document%';SELECT slug, status, error
FROM gatekeeper_history
WHERE status = 'FAILURE'
ORDER BY timestamp DESC;SELECT type, count(*) AS chunk_count
FROM parquet_chunks
GROUP BY type;SELECT source_file, count(*) AS chunks
FROM parquet_chunks
GROUP BY source_file
ORDER BY chunks DESC
LIMIT 10;SELECT page, count(*) AS chunks_per_page
FROM parquet_chunks
WHERE source_file = 'my_document.pdf'
GROUP BY page
ORDER BY page ASC;The staged_chunks table acts as the Write-Ahead Log for chunks before Qdrant persistence.
SELECT count(*) AS enqueued_chunks, count(DISTINCT source_file) AS active_files
FROM staged_chunks;SELECT id, length(chunk) AS chars
FROM staged_chunks
ORDER BY chars DESC
LIMIT 10;SELECT id, source_file
FROM parquet_chunks
WHERE id NOT LIKE 'DOC_%';docker exec -it doc-ingest-chat-redis-1 redis-cli> LLEN chunk_ingest_queue:0
> LLEN chunk_ingest_queue:1
> LLEN ocr_job_queue
> LLEN whisper_job_queue
Note: When USE_TEMPORAL_WHISPER=true, the whisper_job_queue is bypassed — transcription jobs are dispatched to the Temporal task queue instead. Monitor those via the Temporal Web UI (http://localhost:8233) or tctl CLI.
The whisper.cpp server must be started with --convert to handle non-WAV formats (MP4, MP3, M4A, etc.). Without it, the server returns 400 Bad Request and logs failed to decode audio data from memory buffer.
curl http://<whisper-host>:1145/Should return an HTML page with the API documentation.
curl http://<whisper-host>:1145/inference \
-F "file=@/path/to/test.wav" \
-F "temperature=0.0" \
-F "response_format=json"curl http://<whisper-host>:1145/inference \
-F "file=@/path/to/test.mp4" \
-F "temperature=0.0" \
-F "temperature_inc=0.2" \
-F "no_speech_thold=0.6" \
-F "response_format=json"ffmpeg -i /path/to/test.mp4 -vn -acodec pcm_s16le -ar 16000 -ac 1 /tmp/test.wav| Symptom | Cause | Fix |
|---|---|---|
400 Bad Request + failed to decode audio data |
Server missing --convert flag |
Restart server with --convert |
Connection refused |
Server not running or wrong host/port | Check WHISPER_MODEL_ENDPOINTS env var |
| Transcription empty | Audio too quiet or no speech | Check no_speech_thold parameter |
When multi-endpoint *_ENDPOINTS env vars are set, HAProxy containers handle request distribution across backends.
| Service | URL |
|---|---|
| Supervisor LLM | http://localhost:8404/stats |
| Embedding | http://localhost:8405/stats |
| Whisper | http://localhost:8406/stats |
| OCR | http://localhost:8407/stats |
docker logs haproxy_supervisor 2>&1 | grep "POST\|GET" | tail -20
docker logs haproxy_embd 2>&1 | grep "POST\|GET" | tail -20
docker logs haproxy_whisper 2>&1 | grep "POST\|GET" | tail -20
docker logs haproxy_ocr 2>&1 | grep "POST\|GET" | tail -20Requests should alternate between backends (srv0, srv1, etc.):
docker logs haproxy_supervisor 2>&1 | grep "be_supervisor/" | tail -10Expected output shows alternating backends:
be_supervisor/srv0 ... "POST /v1/chat/completions HTTP/1.1"
be_supervisor/srv1 ... "POST /v1/chat/completions HTTP/1.1"
be_supervisor/srv0 ... "POST /v1/chat/completions HTTP/1.1"
HAProxy health-checks backends every 2s via GET /models (or GET /health). A backend is marked down after 3 consecutive failures and up after 2 successes.
docker exec haproxy_supervisor cat /tmp/haproxy.cfg | grep "server srv"| Symptom | Cause | Fix |
|---|---|---|
| All traffic to one backend | Keep-alive connection pinning | option httpclose is set — check if client is reusing connections |
| Backend marked DOWN | Health check failing | Check if the backend's /models or /health endpoint responds |
| 503 from HAProxy | 0 endpoints configured | Set *_ENDPOINTS env var or point *_PATH directly to backend |
When USE_TEMPORAL_WHISPER=true, the system connects to a remote Temporal server.
Access the Temporal Web UI provided by your remote deployment (URL varies by infrastructure).
# List running workflows
tctl --namespace default workflow list
# Describe a specific workflow
tctl --namespace default workflow describe --workflow_id <workflow-id>
# View workflow execution history
tctl --namespace default workflow show --workflow_id <workflow-id>| Symptom | Cause | Fix |
|---|---|---|
| Workflows stuck in PENDING | Temporal server unreachable | Verify TEMPORAL_HOST and TEMPORAL_PORT are correct and the remote server is reachable |
| Activities timing out | Whisper server down or slow | Check WHISPER_MODEL_ENDPOINTS and whisper server health |
| Transcription not starting | USE_TEMPORAL_WHISPER not set |
Set USE_TEMPORAL_WHISPER=true and restart workers |
curl -X POST http://<vector-db-host>:6333/collections/vector_base_collection/points/count \
-H "Content-Type: application/json" \
-d '{
"filter": {
"must": [{"key": "source_file", "match": {"text": "my_document"}}]
}
}'Replace <vector-db-host> with your Qdrant REST API endpoint (default port 6333, or as configured via VECTOR_DB_URL).
curl -X POST http://<vector-db-host>:6333/collections/vector_base_collection/points/scroll \
-H "Content-Type: application/json" \
-d '{"limit": 3, "with_payload": true, "with_vector": false}'Visit http://<vector-db-host>:6333/dashboard for the built-in web UI.
Metrics are recorded in $DEFAULT_DOC_INGEST_ROOT/metrics.jsonl.
jq -r 'select(.event == "file_processing_complete") | .metrics.total_processing_time_ms' \
$DEFAULT_DOC_INGEST_ROOT/metrics.jsonl | \
awk '{sum+=$1; count+=1} END {print "Avg: " sum/count " ms"}'The canonical schema is in doc-ingest-chat/sql/schema.sql. Update that file first, then apply changes:
ALTER TABLE ingestion_lifecycle ADD COLUMN language VARCHAR DEFAULT 'en';CREATE INDEX idx_source_file ON parquet_chunks (source_file);Warning: This permanently deletes all ingestion state and chunk data. Back up chunks.duckdb before running these commands. Qdrant vectors are not affected by DuckDB deletes and must be cleaned separately.
DELETE FROM ingestion_lifecycle;
DELETE FROM parquet_chunks;
DELETE FROM staged_chunks;
DELETE FROM file_ingestion_jobs;
DELETE FROM gatekeeper_history;SELECT status, error_log, pdf_path, md_path
FROM ingestion_lifecycle
WHERE original_filename = 'missing_file.pdf';-- Find chunks in DuckDB without deterministic IDs
SELECT id, source_file
FROM parquet_chunks
WHERE id NOT LIKE 'DOC_%';Check for lock contention using the query in Lock Contention Audit above. Also check Redis queue lengths — if LLEN chunk_ingest_queue:N is growing without bound, the Consumer may have crashed or be blocked.