Design · not a shipped productAn interface study for oneground. No application like this exists; the tool is a set of commands and the figures here are illustrative.What is actually shipped →
oneground
support-tickets-2026q3 · seed 20260915

What are we measuring, and against what?

One file describes the sample and the requirements the decision will be judged against. Nothing else is configured anywhere.

Your corpus

./data/tickets_sample.npy 20,000 vectors · 768 dimensions · float32
./data/queries_from_logs.jsonl 412 real queries — above the 50 needed to measure ambiguity
created_at present, so drift can be measured
2,140,000 documents stratified by month — recorded, not enforced

What the decision must satisfy

≥ 0.95 at k=10
p95 ≤ 40 ms at 200 qps, concurrency 32 a load constraint — only an under-load run can answer it
≤ 2.0× the raw vectors
≤ €1,200 / month
qdrantpgvectormilvus · adapter needed
this machinea machine I rentmy existing cluster
Four steps. The first three run here; the engine step asks before it spends anything.

Your corpus, measured

Five properties of your data, computed against exact answers. These decide which architectures are even in the running — before anything is tried.

Topic separability
0.084
Low. Few documents sit clearly inside one topic.
Ambiguous queries
72.6%
Most queries sit between two regions.
Concentration
0.31
Top ten regions hold a third of the corpus.
Intrinsic dimensionality
28.4
High. Distances concentrate; partitions blur.
Drift
0.61 → 0.48
Older partitions find newer documents less well.
Truncated documents
1,204 of 20,000
Longer than your model reads. If these are documents rather than chunks, chunk them first.

What this means before anything is tried

Your topics overlap and your queries sit between them. Architectures that partition by meaning need boundaries to partition on, and this corpus has few — they will be measured anyway, and the numbers will say so.

Your answers also go stale: a partition built on last year's documents finds this year's noticeably worse. That is a fact about your corpus rather than about any database, and it is worth knowing before you choose one.

measured in 4.2 min · 4 artifacts + MANIFEST.sha256 written These are measurements of this corpus, not a comparison against anything.

Three architectures, twenty-four configurations

Each scored against the same exact answers on your sample. Nothing is judged here — these are measurements, and the constraint column is shown only so you can see where the line falls.

ArchitectureConfiguration RecallReachableStorageQueries a node sees
One index, one machineM=32 · ef=128 0.9911.0001.00×all
Split by hash3 shards · M=32 · ef=96 0.9891.0001.00×all
Split by meaning256 regions · ε=0.00 · probe 1 0.4120.4131.00×1 of 256
Split by meaning256 regions · ε=0.10 · probe 2 0.7340.7362.41×2 of 256
Split by meaning256 regions · ε=0.20 · probe 2 0.8610.8623.68×2 of 256

Where the loss is

For every configuration that split by meaning, what the index returned was almost exactly everything its routing could reach — the gap between recall and reachable is under 0.002 throughout. The deficit is not the index being imprecise; it is neighbours living in regions the query never visits, and no amount of tuning recovers them.

That matches what the corpus said before anything ran: few clear boundaries, most queries sitting between regions.

24 configurations across 3 families · 5m 51s No option is judged here. Run oneground report to compare these against your constraints.

The real database, three times each

Two engines, one machine, the same sample and the same queries, run in turn. Speed is measured three times because one measurement near a threshold is a coin flip.

qdrant 1.19.1

run 1 · p95
36.4 ms
run 2 · p95
38.9 ms
run 3 · p95
37.1 ms
Spread 2.5 ms across three runs, engine restarted between each and confirmed answering before the next began. Sustained 200.0 of 200 offered queries per second, 0 errors. Ceiling before latency broke down: 381 qps at concurrency 4.
Recall against exact answers0.9990
Ingest3,688 vectors/s
Round trip with no query behind it1.9 ms — 5% of the measurement

pgvector 0.8.6

run 1 · p95
317 ms
run 2 · p95
332 ms
run 3 · p95
317 ms
Spread 15.4 ms. Sustained 119.1 of 200 offered, 0 errors — it did not keep up with the rate it was asked for. Ceiling 121 qps at concurrency 4.
Recall against exact answers0.9984
Ingest1,196 vectors/s — index built during ingest
Settings it ran underrecorded, engine defaults
machine tzr4k9 · rented 28 min · €0.31 · terminated Both engines measured on the same host, in turn, never at the same time. A comparison between them is admissible.

One recommendation, and what it rests on

Twenty-four configurations judged against five constraints. Every verdict below names the file and field it was read from.

24 configurations 1 meets 21 fails 2 couldn't check
Recommended
one index, one machine · M=32 · efConstruction=200 · efSearch=128
built and measured on qdrant 1.19.1 · machine tzr4k9 · 2026-09-15
Recall
0.991 ≥ 0.95 simulate.json:rows[single_node_hnsw…].recall_at_10
meets
Latency
worst of three runs 38.9 ms ≤ 40 ms, at 200 qps and concurrency 32 verify.json:searches[k=10_under_load].p95_across_runs.max
meets
Storage
1.00× ≤ 2.0× simulate.json:rows[…].storage_amplification
meets
Sustained rate
200.0 of 200 offered, 0 errors — a sustain check, not the ceiling verify.json:load.completed
meets
Budget
€310 ± €78 per month; the verdict uses €388 against €1,200 report.json:costs[…].monthly_high · prices declared 2026-09-15
meets

What the report will not say

indistinguishable
Split by hash reaches 0.989 against 0.991 — closer than this tool's own calibration error, so the two are indistinguishable on recall. They separate on the number of machines a query touches, which is measured, and nothing else. calibration tolerance 0.01 · calibration/history.jsonl 2026-09-14
couldn't check
Split by hash carries no latency verdict: the engine was built as one index in one namespace, which is not a three-shard deployment. A measurement of the index that was built says nothing about the one that wasn't. verify_info.json:engine_facts.index_params
fails
Every configuration that split by meaning fails recall — best 0.861 against 0.95 — and the three with replication also fail storage, at 2.41× to 3.68×. The shortfall is routing rather than tuning: what was returned was everything the routing could reach. simulate.json:rows[…].recall_at_10, .routing_ceiling
to resolve
pgvector was measured and fails latency at 332 ms against 40 ms. It is not recommended, and it was not ranked against qdrant on anything that was not measured on the same machine. verify.json:engines[pgvector] · machine tzr4k9
Calibrated against qdrant 1.19.1 on 14 September: this tool's prediction was within 0.0019 of what the engine did. report.json · report.html · manifest.yaml