Technology stack
archAIc is a Rust engine wrapped in a PHP UI, served from Docker, optionally backed by a self-hosted LLM. Nothing in the stack is exotic, but several choices look unusual at first glance. This page explains what we picked and why.
The Rust engine
Section titled “The Rust engine”The core is a Cargo workspace of around a dozen crates. The crates that show up in the runtime hot path:
| Library | Used for |
|---|---|
axum 0.8 | HTTP server in gd-server and gd-mcp. Tower middleware stack, native async. |
utoipa | OpenAPI 3.1 schema generation from Rust types. The Swagger UI under /swagger-ui/ is generated; the spec is /api-docs/openapi.json. |
heed | Safe Rust bindings over LMDB. The lexical index uses thirteen named databases inside a single environment. |
fst 0.4 | Burntsushi’s Finite State Transducer crate. Prefix and Levenshtein search over the vocabulary. |
roaring 0.11 | RoaringBitmap. Compressed per-word page sets, allowing fast Boolean set operations over postings. |
rkyv 0.8 | Zero-copy archive format. Per-page payloads are deserialised in place, no allocator pressure on the hot path. |
rusqlite | The corpus DB (corpora.db). Bundled SQLite to keep the deploy a single artefact. |
qdrant-client | gRPC client for the Qdrant vector store. |
ort 2.0 | ONNX Runtime for embedding inference. Optional CUDA feature flag. |
rmcp | The Model Context Protocol server library. |
tokio | Async runtime everywhere outside the index builder. |
rayon | Data-parallel index builder. The build step is CPU-bound and parallel-friendly. |
The UI layer
Section titled “The UI layer”Today the deployed UI is plain PHP 8 served by Apache (the dipucoru deployment). An in-progress UI branch is modernising it toward Twig templates, typed DTOs, Pest-driven tests, Lit web components for shared widgets, and FrankenPHP as the planned application server; that branch deploys side-by-side under nginx as newdipucoru during the migration.
Choosing PHP for the UI is pragmatic. The first archAIc deployments inherited an existing PHP frontend from the C++ engine era, and the customer ops people who run archAIc already know PHP + Apache. We invested in modernising it (DTOs, Twig, Pest, FrankenPHP) instead of rewriting in a different language. The Lit components are the migration path: every interactive surface that needs to be sharable (search results, page viewer, DLU editor, IIIF mini-Mirador) becomes a web component the UI mounts and any third party can embed.
LLM (optional)
Section titled “LLM (optional)”archAIc’s natural-language Q&A surface and curator-assist autofill rely on a local LLM. The default is Ollama running qwen3:32b on the customer’s own hardware. vLLM is the graduation path when one tenant outgrows a single GPU host. Cloud LLM APIs are supported through cfg_llm_profile but explicitly not recommended for archives bound by GDPR or sovereignty constraints.
The LLM is optional. KWS, semantic search, every standards-shaped export, IIIF, and MCP search all work without it. Only the natural-language Q&A endpoints and the curator autofill require Ollama (or equivalent) to be reachable.
See LLM setup for the install and configuration steps.
Packaging
Section titled “Packaging”Everything ships as Docker images and is composed with docker-compose.yml. The compose file exposes profiles (rust, cpp, both, observability) so an operator starts only what they need. See deployment overview for the compose surface.
How a query flows
Section titled “How a query flows”A keyword search request lands on gd-server, walks through the parser, hits the engine’s query cache, runs resolution and hydration, and returns JSON. The diagram below maps each step to the crate and module that owns it.
The hydration step does as little work as the response depth allows. Depth 0–1 stays in bitmap operations and skips per-page reads entirely; depth 2 reads per-word summaries; depth 3 (Full) loads per-hit detail via rkyv and the L2 cache.
Why not Elasticsearch?
Section titled “Why not Elasticsearch?”Elasticsearch (and OpenSearch, and Solr) treats probability as a filter after retrieval: rank by BM25, optionally re-rank with a model. archAIc’s queries operate over probabilities. Every word hypothesis carries a confidence, and a useful KWS engine integrates over those probabilities at scoring time rather than discarding all but the top-ranked hypothesis. Bolting that on top of Lucene was tried in the C++ predecessor and never matched the precision of an integrated implementation. The Rust engine treats probability as first-class data, which means we can return word-level confidence with every hit and let the UI render score histograms per query.
The second reason is footprint. The Rust engine holds the RMCR corpus (around 347K pages) at roughly 50 MB resident. The C++ predecessor that used MARISA tries plus DAWG fuzzy automata sat at about 689 MB on the same data. Elasticsearch on the same corpus is in the multi-gigabyte range. For a customer running on a workstation-class box the engine footprint matters.
Why not Postgres?
Section titled “Why not Postgres?”The lexical index is shaped like a sorted-key store with many small, random reads over compressed postings. LMDB is memory-mapped, single-writer / many-reader, and lets the operating system page-cache exactly what stays resident. The FST plus RoaringBitmap layout is more compact and more cache-friendly than the equivalent B-tree shapes a relational store would build. Postgres is excellent for the corpus ledger (collections, libros, pages, DLUs, audit, config), but we use SQLite for that part because it ships as a single embedded file and removes one moving part from the deployment. Qdrant runs as a separate process because vector storage is its own beast.
Why not pure Python?
Section titled “Why not pure Python?”archAIc’s latency budget is sub-millisecond per hot query, not measured in cold-start or first-token-out time. Pure Python KWS implementations exist (lucy, whoosh, custom NumPy code) and serve fine at small scale, but the GIL and per-call allocation cost make sustained 20K queries-per-second cold-cache benchmarks impossible. Rust gives us the latency floor for the engine, and an embedding service in Python (via ort from Rust) loses too much to FFI overhead. We use Python only for ingestion-side tooling and offline scripts.