Contents
- pgContext vs. pgvector vs. Qdrant benchmark
- Recognized-dataset comparison — GloVe-100-angular, 1.18M, matched Docker (2026-07-20)
- Latency vs. recall Pareto sweep (SciFact 5,183 x 384, cosine)
- Three-trial matched-recall run (SciFact)
- Filtered ANN across selectivity (SciFact)
- 100k synthetic scale lane
- Filtered ANN at 100k across selectivity — pgContext leads pgvector at every band
- Concurrency lane (100k corpus)
- 1M synthetic scale lane
- Update-churn lane (100k corpus) — sustained high-churn writes
- Cold-cache lane (100k corpus)
- Methodology
- Reproduce
- Not yet measured
pgContext vs. pgvector vs. Qdrant benchmark
All numbers on this page come from clean committed trees (git_dirty: false
in the archived result JSONs) on the same machine: Apple M4 Pro, macOS 26.5
arm64, PostgreSQL 17.9, Python 3.12.9, pgContext 0.1.0, pgvector 0.8.5, and
Qdrant 1.18.2 in the official Docker image over its recommended local gRPC
transport.
Headline: on the SciFact warm-cache single-client workload, pgContext’s HNSW
latency/recall curve Pareto-dominates pgvector’s — at every measured
ef_search, pgContext is faster, and at equal recall targets it is faster at
equal or better recall. Qdrant’s curve sits above both PostgreSQL extensions
because every Qdrant query crosses a client/service gRPC boundary that
in-process extensions do not pay; Qdrant also returns perfect recall on this
small corpus. This page also reports the lanes pgContext does not win:
index build time, low-selectivity raw filtered ANN, and the masked filter
path’s function-boundary overhead.
Recognized-dataset comparison — GloVe-100-angular, 1.18M, matched Docker (2026-07-20)
This is the most directly comparable lane: a standard public dataset
(ann-benchmarks glove-100-angular,
1,183,514 x 100, cosine), recall measured against the dataset’s own
precomputed ground-truth neighbors (not a self-generated corpus or each
system’s own exact scan), with all three systems in matched Docker
deployment so the client/transport boundary is identical, and the same
8-way parallel build budget for both PostgreSQL engines. Rendered reports:
pgContext vs pgvector and
pgContext vs pgvector vs Qdrant.
| ef_search | pgContext recall / p50 | pgvector recall / p50 | Qdrant recall / p50 |
|---|---|---|---|
| 64 | 0.731 / 0.67 ms | 0.750 / 2.56 ms | 0.839 / 1.30 ms |
| 128 | 0.810 / 0.94 ms | 0.820 / 4.14 ms | 0.893 / 1.48 ms |
| 256 | 0.868 / 1.46 ms | 0.870 / 7.09 ms | 0.934 / 1.91 ms |
| 512 | 0.910 / 2.44 ms | 0.910 / 12.97 ms | 0.963 / 2.38 ms |
| build | 88.0 s | 50.3 s | 19.9 s (7 segments) |
Two honest readings.
- Versus pgvector, pgContext wins cleanly. Recall matches at every
ef_search(both trace the same 0.73 → 0.91 curve), and pgContext serves it 3.8–5.3x faster — 2.44 ms versus 12.97 ms at the top of the sweep. pgvector builds faster (50 s versus 88 s). - Versus Qdrant, the comparison is honest and Qdrant leads at high recall.
With the transport asymmetry removed (earlier SciFact lanes ran native
PostgreSQL against a Dockerized Qdrant crossing the Docker Desktop VM
boundary; here every system is Dockerized), Qdrant’s real strength shows:
0.963 recall at 2.38 ms, where pgContext needs more
ef_searchto approach 0.96. Qdrant ran 7 segments, so it applieshnsw_efper segment and its per-query effective candidates areef x 7. Effort-matched, pgContext’s single graph is at least as accurate per candidate, but Qdrant’s segment fan-out delivers a latency-at-recall a single-threaded scan cannot. This is the per-query-parallelism gap the roadmap’s segmented-serving work targets — it is not a graph-quality deficit.
Caveats: single trial, warm cache, single client, on an Apple M4 Pro with the
NEON distance kernels active (x86 AVX2 kernels exist but are not yet
performance-measured). The harness and archived result JSON are in
benchmarks/pgvector_comparison/.
Latency vs. recall Pareto sweep (SciFact 5,183 x 384, cosine)
Three-system ef_search sweep, 200 queries, 20 warmups, top-10, m=16,
ef_construction=64. Archived:
sweep JSON
at commit 21eda7ec.
| ef | pgContext p50 / recall@10 | pgvector p50 / recall@10 | Qdrant p50 / recall@10 |
|---|---|---|---|
| 16 | 0.095 ms / 0.9620 | 0.111 ms / 0.9700 | 0.993 ms / 1.0000 |
| 24 | 0.094 ms / 0.9760 | 0.133 ms / 0.9830 | 1.123 ms / 1.0000 |
| 32 | 0.107 ms / 0.9815 | 0.146 ms / 0.9875 | 0.838 ms / 1.0000 |
| 48 | 0.126 ms / 0.9900 | 0.170 ms / 0.9925 | 0.929 ms / 1.0000 |
| 64 | 0.139 ms / 0.9950 | 0.205 ms / 0.9950 | 0.702 ms / 1.0000 |
| 96 | 0.167 ms / 0.9975 | 0.253 ms / 0.9965 | 0.730 ms / 1.0000 |
At ef=64 the two PostgreSQL systems land on the same 0.9950 recall; pgContext
serves it 32% faster at p50. Read whole curves, not single points: this
retires any single-ef comparison, including our own earlier ones.
Qdrant caveat, stated plainly: the Qdrant column measures a local gRPC client round trip against a service; the PostgreSQL columns measure in-process index scans reached over local libpq. That difference is real for application latency but it is not a distance-kernel comparison, and Qdrant’s 1.0000 recall on this small corpus reflects its optimizer choosing effectively exact behavior at this scale. Qdrant remains the more mature distributed vector service; this harness measures the local single-node case only.
Three-trial matched-recall run (SciFact)
Rotating trial order, median/combined distribution across three trials,
pgContext ef=48 vs pgvector/Qdrant ef=40. Archived:
three-trial JSON
at commit 74f083c2.
| Measurement | pgContext | pgvector | Qdrant |
|---|---|---|---|
| Load 5,183 vectors, median | 0.734 s | 0.710 s | 0.233 s |
| Build HNSW, median | 1.018 s | 0.397 s | 0.553 s |
| HNSW index size | 10.44 MiB | 10.13 MiB | not captured |
| Exact cosine p50 | 0.784 ms | 0.686 ms | 0.683 ms |
| HNSW cosine p50 | 0.157 ms | 0.179 ms | 0.690 ms |
| HNSW cosine p95 | 0.284 ms | 0.300 ms | 0.982 ms |
| HNSW recall@10 | 0.9900 | 0.9902 | 1.0000 |
| Filtered ANN p50, 10% | 0.533 ms | 0.378 ms | 0.678 ms |
| Filtered ANN recall@10 | 0.9935 | 0.9658 | 1.0000 |
| Filtered full-result rate | 100% | 100% | 100% |
| pgContext masked ANN p50 | 1.823 ms | — | — |
| pgContext masked recall@10 | 1.0000 | — | — |
pgContext loses the build-time lane (2.6x pgvector) and the raw 10% filtered latency lane (1.4x, at meaningfully higher recall). Those are open engineering items, not footnotes.
Filtered ANN across selectivity (SciFact)
filtered-sweep lane, 1% / 10% / 50% selectivity predicates, matched
settings as above. Archived:
filtered-sweep JSON
at commit 21eda7ec.
| Selectivity | Strategy | p50 | recall@10 |
|---|---|---|---|
| 1% | pgContext exact scan | 0.312 ms | 1.0000 |
| 1% | pgContext raw filtered ANN | 3.683 ms | 1.0000 |
| 1% | pgContext masked traversal | 2.222 ms | 1.0000 |
| 1% | pgvector iterative ANN | 2.051 ms | 0.9860 |
| 1% | Qdrant filtered ANN | 0.640 ms | 1.0000 |
| 10% | pgContext raw filtered ANN | 0.545 ms | 0.9935 |
| 10% | pgvector iterative ANN | 0.524 ms | 0.9630 |
| 10% | Qdrant filtered ANN | 0.677 ms | 1.0000 |
| 50% | pgContext raw filtered ANN | 0.195 ms | 0.9810 |
| 50% | pgvector iterative ANN | 0.225 ms | 0.9775 |
| 50% | Qdrant filtered ANN | 0.772 ms | 1.0000 |
What this says, honestly: at 1% selectivity every graph strategy loses to a plain exact scan — the right system behavior is choosing exact automatically, which pgContext’s registered-collection API can do and the raw index path cannot yet. At 10% the raw lanes are a latency wash while pgContext returns substantially more of the true top-10. At 50% pgContext wins latency and recall outright. Qdrant’s filter-aware index gives it the flattest profile across selectivity — that flatness at every selectivity level is the bar for pgContext’s planned adaptive filtered execution.
100k synthetic scale lane
Seeded synthetic clustered corpus (100,000 x 384, generator in the harness),
single trial, same settings, maintenance_work_mem=2GB for both PostgreSQL
systems. Archived:
100k JSON
at commit 86330dcd.
| Measurement | pgContext | pgvector | Qdrant |
|---|---|---|---|
| Load 100,000 vectors | 14.2 s | 16.4 s | 7.4 s |
| Build HNSW | 48.8 s | 13.8 s | 2.5 s |
| HNSW index size | 200.8 MiB | 195.3 MiB | not captured |
| ANN p50 (pgContext ef=48, others ef=40) | 0.494 ms | 0.886 ms | 0.983 ms |
| ANN recall@10 at those settings | 0.6920 | 0.3945 | 0.7745 |
| Filtered ANN p50, 10% | 1.192 ms | 1.515 ms | 1.836 ms |
| Filtered ANN recall@10 | 0.8925 | 0.4225 | 1.0000 |
Two findings this lane exists to surface, stated plainly:
- Build time is pgContext’s worst scale lane. The gap versus pgvector widens from 2.6x at 5k rows to 3.5x at 100k, and Qdrant builds the same graph roughly 20x faster. This fails the project’s own build-time gate and is open engineering work, not a caveat.
- Fixed small-ef points are meaningless at this scale. On this harder clustered corpus, every system’s recall collapses at the SciFact-tuned settings (all below 0.78). No latency claim at these settings is a win for anyone; the matched-recall sweep below is the only comparison we stand behind at 100k.
A third finding from bringing the lane up: pgContext refuses HNSW builds
whose estimated memory exceeds maintenance_work_mem instead of degrading
the way pgvector does; under PostgreSQL’s default 64MB budget, that refusal
triggers near this corpus size. The harness now sets an explicit equal budget
for both systems, and the graceful-build gap is tracked as follow-up work.
Matched-recall sweep at 100k
Because fixed small-ef points are meaningless here, the 100k comparison that
matters is the high-ef sweep (m=16, ef_construction=64 for all systems).
Archived:
100k sweep JSON
at commit 531360f5.
| ef | pgContext p50 / recall@10 | pgvector p50 / recall@10 | Qdrant p50 / recall@10 |
|---|---|---|---|
| 64 | 0.541 ms / 0.7705 | 1.072 ms / 0.5130 | 1.625 ms / 0.8495 |
| 96 | 0.579 ms / 0.8675 | 1.149 ms / 0.5675 | 1.831 ms / 0.8875 |
| 128 | 0.665 ms / 0.9160 | 1.246 ms / 0.6055 | 1.996 ms / 0.9115 |
| 192 | 0.749 ms / 0.9655 | 1.678 ms / 0.6385 | 1.997 ms / 0.9445 |
| 256 | 0.864 ms / 0.9820 | 2.035 ms / 0.6685 | 2.031 ms / 0.9770 |
Single-client, pgContext’s graph is decisively better on this corpus: it reaches 0.982 recall at 0.86 ms while pgvector’s graph, built with the same nominal parameters, does not exceed 0.67 recall at any measured ef. pgContext’s longer build (3.5x) buys real graph quality here, which is part of the build-time tradeoff. Qdrant tracks pgContext’s recall curve at roughly 2.3x the latency, consistent with its service boundary. Read this together with the concurrency lane below before concluding anything about production throughput.
Filtered ANN at 100k across selectivity — pgContext leads pgvector at every band
Filtered ANN across selectivity on the 100k synthetic corpus (200 queries,
ef_search=48). Archived as
apple-m4-pro-pg17.9-2026-07-17-filtered-sweep-100k-1a70f2eb.json.
| Selectivity | pgContext adaptive (collection API) | pgContext operator filter | pgvector | Qdrant |
|---|---|---|---|---|
| 1% | 21.9 ms @ 1.000 recall | 5.5 ms @ 0.997 | 4.8 ms @ 0.489 | 0.7 ms @ 1.000 |
| 10% | 33.6 ms @ 1.000 recall | 1.2 ms @ 0.887 | 1.4 ms @ 0.368 | 2.3 ms @ 1.000 |
| 50% | 71.0 ms @ 1.000 recall | 0.4 ms @ 0.608 | 0.7 ms @ 0.338 | 1.0 ms @ 0.660 |
Reading, honestly:
- pgContext’s adaptive path beats pgvector’s filtered recall outright. pgvector’s post-filtered scan cannot exceed ~0.34-0.49 recall at any measured selectivity on this corpus — filter starvation — while the collection API returns full-recall results at every selectivity, and even our raw operator-level filter dominates pgvector’s recall at comparable latency (0.997 vs 0.489 at 1%).
- The open gap is Qdrant’s filtered latency, not recall: their payload-aware traversal reaches full recall in ~1-2 ms where our full-recall path costs 22-71 ms (mask construction and exact-scan fallbacks dominate; the 50% lane is an exact scan by strategy). The planned statistics-driven selectivity estimation, packed-bitmap masks, and iterative widening target exactly this overhead.
- The masked-filter candidate budget is configurable via
pgcontext.hnsw_mask_candidate_limit.
Concurrency lane (100k corpus)
1, 8, and 32 client processes, each running the full 200-query set (pgContext ef=48, pgvector ef=40 — the same settings as the fixed-point lane above, so read latency alongside that lane’s recall figures).
pgContext serves concurrent backends from a shared packed-base registry (a
GetNamedDSMSegment-backed registry: one backend publishes its packed
generation, other backends attach it instead of rebuilding). Archived:
concurrency-100k-3ea49bb0.json
at commit 3ea49bb0.
| Clients | pgContext agg QPS / backend RSS | pgvector agg QPS / backend RSS |
|---|---|---|
| 1 | 140 / 734 MiB | 299 / 139 MiB |
| 8 | 2,347 / 1.5 GiB | 1,747 / 1.1 GiB |
| 32 | 2,621 / 6.2 GiB | 2,480 / 4.4 GiB |
Aggregate QPS scales with concurrency (140 → 2,347 → 2,621), and pgContext edges out pgvector’s throughput at both 8 and 32 clients. One shared image serves many backends rather than each backend holding a private copy. Backend memory is higher than pgvector’s shared-buffer footprint (6.2 GiB vs 4.4 GiB at 32 clients), because the registry holds one full copy plus per-backend fallback state; memory parity is a known limitation.
1M synthetic scale lane
Same generator at 1,000,000 x 384, single trial,
maintenance_work_mem=2GB. Archived:
1M JSON
at commit f29edac4.
| Measurement | pgContext | pgvector | Qdrant |
|---|---|---|---|
| Load 1,000,000 vectors | 138 s | 161 s | 105 s |
| Build HNSW | 774 s | 346 s | 84 s |
| HNSW index size | 1.84 GiB | 1.91 GiB | not captured |
| ANN p50 (fixed benchmark ef) | 2.54 ms | 79.07 ms | 1.77 ms |
| ANN recall@10 at those settings | 0.1400 | 0.0875 | 0.5430 |
| Filtered ANN p50, 10% | 6.75 ms | 254.98 ms | 1.84 ms |
| Filtered ANN recall@10 | 0.2960 | 0.0765 | 0.7365 |
What we claim from this lane, and what we do not:
- Build time. With
pgcontext.hnsw_build_parallel_workers(a per-node-locking parallel builder), the 1M x 384 build measures (maintenance_work_mem=4GB): 638 s at 1 worker, 437 s at 2, 347 s at 4, 181 s at 8 workers (3.5x speedup, 0.52x of pgvector’s 346 s serial build), still behind Qdrant’s 84 s. The speedup is consistent across 20k/100k/1M. (The 774 s figure in the table above is a single-worker build.) High-ef 1M sweep (archived as
apple-m4-pro-pg17.9-2026-07-17-sweep-1m-fdbbf527.json): pgContext strictly Pareto-dominates pgvector.ef_search pgContext pgvector Qdrant 128 2.80 ms @ 0.305 83.03 ms @ 0.168 3.74 ms @ 0.859 256 3.55 ms @ 0.462 48.16 ms @ 0.249 4.66 ms @ 0.934 384 4.16 ms @ 0.574 21.87 ms @ 0.292 4.77 ms @ 0.955 Our worst point beats pgvector’s best point on both axes simultaneously — 5-30x lower latency at higher recall across the entire measured range.
Reading the Qdrant column. The fixed-ef rows above compare unlike quantities. Qdrant applies
hnsw_efper segment and merges results across every segment of the collection, and its default segment count is the machine’s CPU count — so on this 14-core machine its “ef=384” row explores on the order of ten times more candidates per query than one ef=384 search of a single graph. A direct probe of the same 1M corpus at HEAD (40 queries, single trial, ef_construction=200 build, index plan verified) shows the monolithic graph has no recall ceiling:ef_search recall@10 p50 384 0.6525 4.63 ms 768 0.8325 4.85 ms 1536 0.9225 6.30 ms 3072 0.9650 8.95 ms At matched total effort (pgContext ef=3072 against Qdrant’s ~10 segments x ef=384) the graphs are equivalent: 0.965 vs 0.955. What is genuinely better on the Qdrant side is latency at high recall (4.77 ms vs 8.95 ms), because segment fan-out parallelizes one query across cores while a single graph is searched on one thread. Closing that is architecture work (segmented serving with per-query fan-out, on the roadmap), not graph-construction work. These probe figures are preliminary — 40 queries, single trial. Sweep reports record Qdrant’s segment count and per-ef effective candidate totals (
qdrant_effortin sweep.json) so the comparison can be read effort-matched.- The masked-filter candidate ceiling is configurable via
pgcontext.hnsw_mask_candidate_limit(up to 5M points); the filtered lane’s large-mask economics remain a scale consideration.
Update-churn lane (100k corpus) — sustained high-churn writes
The harness’s churn lane rewrites 5% of vectors per round, VACUUMs, and
re-measures. Two rounds were measured (archived as
apple-m4-pro-pg17.9-2026-07-17-churn-partial-100k-c9ff4d76.json); the trend
was already clear:
| Round | update throughput | first post-churn query | index size | recall @ ef=48 |
|---|---|---|---|---|
| 0 (baseline) | — | — | 201 MiB | 0.692 |
| 1 | 2 rows/s (2,463 s for 5,000) | 1,211 ms | 541 MiB | 0.695 |
| 2 | 1 row/s (3,677 s) | 1,727 ms | 885 MiB | 0.686 |
Without the segmented write path, sustained high-churn writes degrade on every dimension: write throughput (1-2 rows/s, degrading per round), post-churn first-query latency (1.2-1.7 s vs sub-ms steady), and index growth (4.4x baseline by round 2). The cause is O(graph-size) page reconstruction per incremental insert. The segmented write path addresses this:
Re-measurement with delta segment plus threshold compaction (2026-07-20)
The delta segment and threshold-triggered compaction target exactly this
sustained-write pattern. Measured on the same 100k 384-dimension corpus,
maintenance_work_mem = 2GB, single-row inserts in a plpgsql loop:
| Measurement | Value |
|---|---|
| 12,000 single-row inserts, wall clock | 45.6 s |
| Sustained throughput | 263 rows/s |
| Slowest single insert | 42.9 s (the compaction) |
| Inserts exceeding 1 s | 1 of 12,000 |
| Delta-path appends excluding that insert | ~4,440 rows/s |
Explicit pgcontext.compact() on the same index |
59.5 s, drained 10,261 delta records |
| Index size after compaction | 439 MB (201 MB baseline) |
Sustained throughput is 263 rows/s — roughly two orders of magnitude above the 1-2 rows/s of pure inline splicing — and 11,999 of 12,000 inserts are sub-millisecond. Three limitations remain:
- Sustained throughput misses the 500 rows/s bar. The figure is amortized:
cheap delta appends plus one full rebuild per drained segment. Compaction
cost is fixed per run, so raising
pgcontext.hnsw_delta_segment_limitamortizes it further and can clear 500 rows/s, at the cost of query latency (scans exact-scan the whole delta). - Index size misses the ≤1.3x bar at 2.2x. Compaction deliberately leaves
superseded pages in place — it reclaims write throughput, not disk.
REINDEXreclaims the space. - The tail is the main remaining limitation. One insert blocking for 42.9 s is
not acceptable as a steady-state property, and it grows with the index.
pgcontext.hnsw_compact_on_threshold_max_mb(default 1GB of projected vectors, tunable) bounds which indexes an insert will compact by itself, so the stall is capped rather than unbounded, but it is still paid by a user statement.
The planned improvement is running compaction in a background worker so no statement pays for it (see the roadmap); the synchronous path plus the size bound is what ships today.
Note on measurement method: the rounds table uses the harness’s churn lane;
the single-row insert figures use a direct insert loop, because the harness
lane rewrites rows via UPDATE and reports per-round aggregates that hide the
single-statement tail. Both are reported.
Cold-cache lane (100k corpus)
The cold-cache lane restarts PostgreSQL, then measures 20 queries, each
opened on a fresh connection (a fresh PostgreSQL backend process), before
measuring steady state on a warm, reused connection.
With the shared packed-base registry, only the first backend after a restart
rebuilds the packed generation; every later backend attaches the published
image. Archived:
cold-cache-100k-0c8129e6.json
at commit 0c8129e6.
| Measurement | pgContext | pgvector |
|---|---|---|
| First query after restart | 640.0 ms | 3.2 ms |
| p50 of first 20 cold (fresh-backend) queries | 5.3 ms | 1.8 ms |
| Steady-state p50 | 1.626 ms | 0.552 ms |
The registry is empty immediately after a restart, so the very first backend pays the full pack cost (640 ms). Every backend after the first attaches the published image, so the cold-start cost is paid once per server lifetime, not once per backend — the median of the first 20 cold queries is 5.3 ms. pgContext does not match pgvector’s cold path, which serves cold reads straight from shared buffers with no pack step; pgContext needs one first-ever pack per server start. This is a known limitation.
Methodology
- Dataset: public MTEB SciFact,
pinned to revision
cf10ab6856b15b0e670ef8ae5dae4e266c12d035; scale lane uses the harness’s seeded synthetic generator (seed20260715). - Model: sentence-transformers/all-MiniLM-L6-v2 through FastEmbed’s ONNX export; 384 dimensions, cosine.
- Query sample: 200 queries selected without replacement with seed
20260715; 20 warmups; top 10. - Timing: single-client warm-cache request latency unless the lane states otherwise. Embedding generation, dataset download, and client-side vector serialization are outside the timed window.
- PostgreSQL isolation: pgContext and pgvector use separate databases in the same cluster with identical rows and server settings.
- PostgreSQL exact oracle: HNSW, bitmap, and ordinary index scans disabled; ANN trials fail if the plan does not select the HNSW index.
- Qdrant: Query API with
exact=truefor the oracle,exact=falsewith the collection fully indexed for ANN; pinnedqdrant/qdrant:v1.18.2; client prefers gRPC; integer payload indexes created before HNSW construction. - Every archived JSON records the git SHA and a clean/dirty flag; only
git_dirty: falseartifacts are cited on this page. Excluding dirty-tree results is an editorial discipline, not enforced automatically by the harness.
Not yet measured: a matched-recall 1M curve (fixed-ef 1M evidence exists and is reported above), the complete multi-round churn artifact (round-1 evidence reported above), constrained-memory serving, replication, and distributed operation. Claims on this page are limited accordingly.
Reproduce
Install pgContext and pgvector against PostgreSQL 17:
cargo pgrx install -p context-pg --release \
--pg-config /opt/homebrew/opt/postgresql@17/bin/pg_config \
--no-default-features --features pg17
PG_CONFIG=/opt/homebrew/opt/postgresql@17/bin/pg_config \
benchmarks/pgvector_comparison/install_pgvector.sh
Start the pinned Qdrant service:
docker run --rm --name pgcontext-bench-qdrant \
-p 6333:6333 -p 6334:6334 \
qdrant/qdrant:v1.18.2
Run the lanes (never two invocations concurrently against one server):
benchmarks/pgvector_comparison/run.sh test
benchmarks/pgvector_comparison/run.sh prepare
# Three-trial matched-recall comparison
PGCONTEXT_HNSW_EF_SEARCH=48 PGVECTOR_HNSW_EF_SEARCH=40 QDRANT_HNSW_EF_SEARCH=40 \
benchmarks/pgvector_comparison/run.sh run \
--dsn "host=/tmp port=5432 dbname=postgres" \
--queries 200 --warmup 20 --trials 3
# Pareto sweep and filtered selectivity sweep
benchmarks/pgvector_comparison/run.sh sweep --ef-values 16,24,32,48,64,96
benchmarks/pgvector_comparison/run.sh filtered-sweep
# 100k scale + concurrency lanes
benchmarks/pgvector_comparison/run.sh prepare --synthetic 100000 \
--output-dir target/pgvector-comparison-100k
benchmarks/pgvector_comparison/run.sh run \
--output-dir target/pgvector-comparison-100k --trials 1
benchmarks/pgvector_comparison/run.sh concurrency \
--output-dir target/pgvector-comparison-100k --workers 1,8,32
The harness pins pgvector 0.8.5, Qdrant server 1.18.2, qdrant-client 1.18.0, FastEmbed 0.7.3, psycopg 3.2.9, and certifi 2025.8.3.
Not yet measured
Areas this page does not yet cover, so no claim is made about them:
- A matched-recall 1M curve against Qdrant. On the 1M sweep pgContext strictly Pareto-dominates pgvector; against Qdrant, the open item is latency at high recall, which comes from per-query segment parallelism (see the 1M reading above), not graph quality.
- Sustained high-churn writes: throughput is 263 rows/s, with a single-statement compaction tail and index growth as the remaining limitations (see the churn section). Background-worker compaction over segmented units is the planned improvement.
- Memory parity with pgvector under concurrency (6.2 GiB vs 4.4 GiB at 32
clients) and the first-backend cold-start cost after a restart (640 ms,
which
pgcontext.hnsw_pack_on_first_use=offpage-native serving avoids). - Constrained-memory serving and WAL-volume comparisons.