Self-hosting guide
Install, secure, operate, and scale a self-hosted engine — compose or bare-metal.
The engine is two moving parts: a Postgres 16 database and one Python process (FastAPI + background tasks in the same event loop). No queue, no cache layer, no worker fleet.
Prerequisites
- Docker + docker compose (the easy path), or
- Python 3.12 and Postgres 16 for a bare install.
Compose install
What happens on first start:
- Alembic migrates the schema (
migrations/versions/0001_initial.py). - The entrypoint loads the bundled Wikipedia sample corpus
(
data/sample_corpus.jsonl.gz, ~1,000 articles) so the demo works immediately. Skip withNOPLAG_LOAD_SAMPLE_CORPUS=0. - uvicorn serves the API + demo page on port 8000.
Configuration is environment variables only — see
Configuration for the complete annotated
list. Compose reads a .env file next to docker-compose.yml.
Bare install
Security model
There is none, on purpose: the API is an unauthenticated single-tenant backend service. Anyone who can reach the port can run checks and manage the corpus. Deploy it:
- on a private network / behind a VPN, or
- behind a reverse proxy that enforces your own auth (basic auth, OAuth2 proxy, mTLS — anything), or
- as an internal service your own application calls server-to-server.
Do not expose it raw to the public internet.
Operating at scale
Up to a few million chunks, the defaults just work. Past that, in order:
- ANALYZE after every bulk load. The L1
&&probe lives and dies by planner statistics; a freshly bulk-loaded (or restored) table is quiescent and autoanalyze may never fire.python -m noplag_engine.maintenance.analyze. - Build the document-frequency table once retrieval latency starts to
matter (tens of millions of chunks):
python scripts/compute_fingerprint_df.py --floor 200. With it present, retrieval probes only each chunk's rarest fingerprints, the stop-list filter activates, and non-discriminative all-common-phrase chunks are skipped. Without it, the engine transparently falls back to full-union retrieval — correct, just slower at scale. Re-run after each major ingestion. - Tune the knobs (
NOPLAG_L1_DF_BUDGET,NOPLAG_L1_POOL_CAP,NOPLAG_L1_NONDISCRIM_DF,NOPLAG_STOP_LIST_N) against your own corpus — the defaults were tuned on a web-scale English corpus. All are read at query time; changing them needs a restart but no re-index. - Per-check time budget (
NOPLAG_CHECK_BUDGET_S, default 60): a huge document that exceeds it returnscoverage: "partial"with every match found so far, visited in a spread order so partial coverage is uniform across the document rather than front-biased.
Changing NOPLAG_FP_K / NOPLAG_FP_W / chunking parameters invalidates the
corpus — fingerprints only match when the corpus and query sides used
identical parameters. Re-ingest after changing them.
Backups
Everything lives in Postgres (the raw upload bytes under
CORPUS_STORAGE_DIR are a convenience copy for future re-extraction, not
required for matching). Standard Postgres backup practice applies; after any
restore, run the ANALYZE step above.
Scaling the process
The single process handles the API and runs checks on its own event loop,
with L0 alignment spread over a process pool sized to the machine. CPU is
the ceiling: alignment dominates a check's wall clock. Give the container
cores before RAM. If you need more than one node, note that checks are
tracked in Postgres but SSE progress events are in-process — sticky routing
per check, or polling GET /v1/checks/{id} (which reads the persisted
stage), keeps multi-node deployments honest.