# Three-engine Koha benchmark — targeted tuning follow-up

Measured: Fri Oct 30 18:46:02 2026 UTC

Host: Koha 26.05.04, 2-vCPU ARM64 Neoverse-N1, approximately 7.6 GiB RAM.
RustSearch 0.6.0 / Tantivy 0.26.2, native Zebra, Elasticsearch 8.19.23 with ICU.
Elasticsearch uses a fixed 1 GiB heap, one primary shard and no replicas per index.
All three services are co-resident; phases are not separate dedicated machines.

Warm native Koha adapters with shared Koha result formatting; two warmups and twenty measured trials per supported workload/engine, with rotating engine order. Three co-resident services on the same host. All engines use a common catalog subset. Zebra uses local-number exclusions; Elasticsearch uses ID exclusions. Native field mappings, ranking, normalization and facets may differ. Counts and top-page overlap are recorded. Browser rendering, Plack, network travel and cold-cache acceptance are outside this scope.

Shared corpus: 70,410 titles. Full Koha catalog: 70,422.
Workloads: 5. Measured requests: 300, excluding warmups
and concurrency probes. Each supported workload has 20 samples per engine.

Times labeled Search adapter include query building, engine execution and MARC
retrieval. Search + formatting adds the same C4::Search::searchResults formatter.
These timings are not raw engine-only latency or a promise about browser speed.
No aggregate winner is calculated for workloads with different match sets.
Not measured means no latency is assigned for that engine/workload combination.
The legacy Zebra-style all-records probe is scored only when it returns the whole
shared corpus. A zero-match response to that syntax is not an all-records timing
and does not establish whether the engine supports other match-all interfaces.

The page checks for newly published results every 60 seconds. Static published
measurements remain associated with the measured timestamp. No public visitor
triggers a live search or benchmark against the Koha test platform.

Concurrency uses 40 mixed requests per engine at one and four clients. Warm persistent processes, separate engine phases, and startup excluded. This is a bounded check, not sustained production capacity.

This five-workload follow-up has 300 sequential samples plus 240 bounded concurrent samples (540 total). The five cases were selected for tuning investigation, not as a representative universal performance score.

RustSearch now caches immutable facet dictionary topology by segment and prefetches current bibliographic metadata per page. Query counts, rankings and holdings are not cached across requests. One search thread is retained after a two-thread trial reduced mixed concurrent throughput.
