pgserve 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,14 +9,218 @@
9
9
 
10
10
  import { Database } from 'bun:sqlite';
11
11
  import { PGlite } from '@electric-sql/pglite';
12
+ import { vector } from '@electric-sql/pglite/vector';
12
13
  import { startMultiTenantServer } from '../../src/index.js';
13
14
  import fs from 'fs';
14
15
  import path from 'path';
15
16
  import os from 'os';
16
17
  import pg from 'pg';
18
+ import { loadEmbeddings, generateQueryVectors, formatPgVector, getEmbeddingsPath, getGroundTruth, calculateRecall } from './vector-generator.js';
17
19
 
18
20
  const { Pool } = pg;
19
21
 
22
+ // ============================================================================
23
+ // ANSI Colors and Visual Utilities (stress-test style)
24
+ // ============================================================================
25
+ const C = {
26
+ reset: '\x1b[0m',
27
+ bold: '\x1b[1m',
28
+ dim: '\x1b[2m',
29
+ green: '\x1b[32m',
30
+ yellow: '\x1b[33m',
31
+ cyan: '\x1b[36m',
32
+ red: '\x1b[31m',
33
+ magenta: '\x1b[35m',
34
+ blue: '\x1b[34m',
35
+ white: '\x1b[37m',
36
+ };
37
+
38
+ /**
39
+ * Print benchmark banner
40
+ */
41
+ function banner() {
42
+ console.log(`
43
+ ${C.cyan}${C.bold}╔════════════════════════════════════════════════════════════════╗
44
+ ║ pgserve UNIFIED BENCHMARK SUITE ║
45
+ ║ ║
46
+ ║ Comparing: SQLite │ PGlite │ PostgreSQL │ pgserve ║
47
+ ╚════════════════════════════════════════════════════════════════╝${C.reset}
48
+ `);
49
+ }
50
+
51
+ /**
52
+ * Print section header
53
+ */
54
+ function section(name, description) {
55
+ console.log(`
56
+ ${C.yellow}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}
57
+ ${C.bold}${C.cyan}▶ ${name}${C.reset}
58
+ ${C.dim} ${description}${C.reset}
59
+ ${C.yellow}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}
60
+ `);
61
+ }
62
+
63
+ /**
64
+ * Progress bar
65
+ */
66
+ function progressBar(current, total, width = 30) {
67
+ const pct = Math.min(1, Math.max(0, current / total || 0));
68
+ // filled is clamped to [0, width], so (width - filled) is always non-negative
69
+ const filled = Math.max(0, Math.min(width, Math.round(pct * width)));
70
+ const empty = width - filled;
71
+ return `[${C.green}${'█'.repeat(filled)}${C.dim}${'░'.repeat(empty)}${C.reset}] ${(pct * 100).toFixed(0)}%`;
72
+ }
73
+
74
+ /**
75
+ * Calculate score for an engine
76
+ * Higher is better - weighted combination of throughput and latency
77
+ */
78
+ function calculateScore(results) {
79
+ if (results.skipped) return 0;
80
+
81
+ // Score formula:
82
+ // - Base: throughput QPS (main factor)
83
+ // - Bonus: low latency (P99 < 10ms gets bonus)
84
+ // - Penalty: errors
85
+ const throughputScore = results.throughput || 0;
86
+ const latencyBonus = results.p99 > 0 ? Math.max(0, (10 - results.p99) * 10) : 0;
87
+ const errorPenalty = (results.errors || 0) * 100;
88
+
89
+ return Math.round(throughputScore + latencyBonus - errorPenalty);
90
+ }
91
+
92
+ /**
93
+ * Print final results table with scores
94
+ */
95
+ function printFinalResults(allResults, vectorResults, canUseRam) {
96
+ console.log(`
97
+ ${C.cyan}${C.bold}
98
+ ╔════════════════════════════════════════════════════════════════╗
99
+ ║ FINAL RESULTS ║
100
+ ╚════════════════════════════════════════════════════════════════╝${C.reset}
101
+ `);
102
+
103
+ // Aggregate results per engine
104
+ // Note: recallCount tracks only SEARCH scenarios (INSERT has 'N/A' recall)
105
+ const engines = {
106
+ sqlite: { name: 'SQLite', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0 },
107
+ pglite: { name: 'PGlite', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0 },
108
+ postgres: { name: 'PostgreSQL', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0, skipped: false },
109
+ pgserve: { name: 'pgserve (disk)', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0 },
110
+ pgserveRam: { name: 'pgserve (RAM)', crudQps: 0, vecQps: 0, vecRecall: 0, p50: 0, p99: 0, errors: 0, count: 0, vecCount: 0, recallCount: 0 },
111
+ };
112
+
113
+ // Aggregate CRUD results
114
+ for (const r of allResults) {
115
+ for (const [key, eng] of Object.entries(engines)) {
116
+ const data = r[key];
117
+ if (data && !data.skipped) {
118
+ eng.crudQps += data.throughput || 0;
119
+ eng.p50 += data.p50 || 0;
120
+ eng.p99 += data.p99 || 0;
121
+ eng.errors += data.errors || 0;
122
+ eng.count++;
123
+ } else if (data?.skipped) {
124
+ eng.skipped = true;
125
+ }
126
+ }
127
+ }
128
+
129
+ // Aggregate Vector results (with recall)
130
+ // Note: INSERT scenarios have recall='N/A', only SEARCH scenarios have numeric recall
131
+ for (const r of vectorResults) {
132
+ for (const [key, eng] of Object.entries(engines)) {
133
+ const data = r[key];
134
+ if (data && !data.skipped) {
135
+ eng.vecQps += data.throughput || 0;
136
+ eng.vecCount++;
137
+ // Only count recall for SEARCH scenarios (not INSERT which has 'N/A')
138
+ const recallValue = parseFloat(data.recall);
139
+ if (!isNaN(recallValue)) {
140
+ eng.vecRecall += recallValue;
141
+ eng.recallCount++;
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ // Average the results
148
+ for (const eng of Object.values(engines)) {
149
+ if (eng.count > 0) {
150
+ eng.crudQps = Math.round(eng.crudQps / eng.count);
151
+ eng.p50 = (eng.p50 / eng.count).toFixed(1);
152
+ eng.p99 = (eng.p99 / eng.count).toFixed(1);
153
+ }
154
+ if (eng.vecCount > 0) {
155
+ eng.vecQps = Math.round(eng.vecQps / eng.vecCount);
156
+ } else {
157
+ eng.vecQps = 0;
158
+ }
159
+ // Average recall only from SEARCH scenarios (recallCount), not INSERT scenarios
160
+ if (eng.recallCount > 0) {
161
+ eng.vecRecall = (eng.vecRecall / eng.recallCount).toFixed(1);
162
+ } else {
163
+ eng.vecRecall = 'N/A';
164
+ }
165
+ eng.score = Math.round(eng.crudQps * 0.6 + eng.vecQps * 0.4 - eng.errors * 10);
166
+ }
167
+
168
+ // Print table header (with Recall column)
169
+ const hasVec = vectorResults.length > 0;
170
+ if (hasVec) {
171
+ console.log(`${C.bold}Engine │ CRUD QPS │ Vec QPS │ Recall │ P50 │ P99 │ Errors │ SCORE${C.reset}`);
172
+ console.log(`${'─'.repeat(90)}`);
173
+ } else {
174
+ console.log(`${C.bold}Engine │ CRUD QPS │ P50 │ P99 │ Errors │ SCORE${C.reset}`);
175
+ console.log(`${'─'.repeat(70)}`);
176
+ }
177
+
178
+ // Print each engine row
179
+ const engineOrder = ['sqlite', 'pglite', 'postgres', 'pgserve'];
180
+ if (canUseRam) engineOrder.push('pgserveRam');
181
+
182
+ let maxScore = 0;
183
+ let winner = '';
184
+
185
+ for (const key of engineOrder) {
186
+ const eng = engines[key];
187
+ if (eng.skipped) {
188
+ if (hasVec) {
189
+ console.log(`${C.dim}${eng.name.padEnd(17)} │ ${'-'.padStart(8)} │ ${'-'.padStart(7)} │ ${'-'.padStart(6)} │ ${'-'.padStart(7)} │ ${'-'.padStart(7)} │ ${'-'.padStart(6)} │ ${'-'.padStart(7)}${C.reset}`);
190
+ } else {
191
+ console.log(`${C.dim}${eng.name.padEnd(17)} │ ${'-'.padStart(8)} │ ${'-'.padStart(7)} │ ${'-'.padStart(7)} │ ${'-'.padStart(6)} │ ${'-'.padStart(7)}${C.reset}`);
192
+ }
193
+ continue;
194
+ }
195
+
196
+ const color = eng.errors > 0 ? C.yellow : C.green;
197
+ const scoreColor = eng.score > maxScore ? C.magenta + C.bold : color;
198
+
199
+ if (eng.score > maxScore) {
200
+ maxScore = eng.score;
201
+ winner = eng.name;
202
+ }
203
+
204
+ if (hasVec) {
205
+ // vecRecall is 'N/A' for INSERT-only scenarios, or a numeric string like '100.0' for SEARCH
206
+ const recallStr = eng.vecRecall !== 'N/A' ? `${eng.vecRecall}%` : 'N/A';
207
+ const vecQpsStr = eng.vecCount > 0 ? eng.vecQps.toLocaleString() : 'N/A';
208
+ console.log(`${color}${eng.name.padEnd(17)} │ ${String(eng.crudQps.toLocaleString()).padStart(8)} │ ${vecQpsStr.padStart(7)} │ ${recallStr.padStart(6)} │ ${(eng.p50 + 'ms').padStart(7)} │ ${(eng.p99 + 'ms').padStart(7)} │ ${String(eng.errors).padStart(6)} │ ${scoreColor}${String(eng.score.toLocaleString()).padStart(7)}${C.reset}`);
209
+ } else {
210
+ console.log(`${color}${eng.name.padEnd(17)} │ ${String(eng.crudQps.toLocaleString()).padStart(8)} │ ${(eng.p50 + 'ms').padStart(7)} │ ${(eng.p99 + 'ms').padStart(7)} │ ${String(eng.errors).padStart(6)} │ ${scoreColor}${String(eng.score.toLocaleString()).padStart(7)}${C.reset}`);
211
+ }
212
+ }
213
+
214
+ console.log(`${'─'.repeat(hasVec ? 90 : 70)}`);
215
+
216
+ // Winner announcement
217
+ console.log(`
218
+ ${C.magenta}${C.bold}╔═══════════════════════════════════════════════════╗
219
+ ║ 🏆 WINNER: ${winner.padEnd(20)} SCORE: ${String(maxScore).padStart(7)} ║
220
+ ╚═══════════════════════════════════════════════════╝${C.reset}
221
+ `);
222
+ }
223
+
20
224
  // Global error handlers (suppress expected PGlite WASM ExitStatus errors)
21
225
  process.on('unhandledRejection', (reason, promise) => {
22
226
  if (reason && reason.name === 'ExitStatus') return;
@@ -34,10 +238,10 @@ const RESULTS_DIR = new URL('./results', import.meta.url).pathname;
34
238
  // PostgreSQL Server configuration (Docker with tmpfs for fair RAM-to-RAM comparison)
35
239
  const POSTGRES_CONFIG = {
36
240
  host: 'localhost',
37
- port: 15432,
241
+ port: 5433, // pgvector/pgvector:pg17 Docker container
38
242
  user: 'postgres',
39
- password: 'benchpass',
40
- database: 'bench'
243
+ password: 'postgres',
244
+ database: 'postgres'
41
245
  };
42
246
 
43
247
  /**
@@ -69,6 +273,47 @@ const scenarios = [
69
273
  }
70
274
  ];
71
275
 
276
+ /**
277
+ * Vector benchmark scenarios (pgvector)
278
+ * Requires: --include-vector flag
279
+ */
280
+ /**
281
+ * Vector benchmark scenarios
282
+ * Following industry-standard methodology (ANN-Benchmarks, Qdrant, VectorDBBench):
283
+ * - Measure Recall@k alongside QPS
284
+ * - Compare approximate results to brute-force ground truth
285
+ * - Report both metrics together (can't compare QPS without knowing recall)
286
+ */
287
+ const vectorScenarios = [
288
+ {
289
+ name: 'Vector INSERT (1000 vectors)',
290
+ description: 'Bulk insert performance - where RAM mode shows benefits',
291
+ type: 'INSERT',
292
+ dimension: 1536,
293
+ insertCount: 1000, // Insert 1000 vectors to measure write speed
294
+ },
295
+ {
296
+ name: 'k-NN Search (k=10)',
297
+ description: 'Recall@10 and QPS on 10k vectors, 100 queries',
298
+ type: 'SEARCH',
299
+ dimension: 1536,
300
+ corpusSize: 10000,
301
+ queryCount: 100,
302
+ k: 10,
303
+ warmupQueries: 20 // Warm-up before measuring
304
+ },
305
+ {
306
+ name: 'k-NN Search (k=100)',
307
+ description: 'Recall@100 and QPS on 10k vectors - harder recall target',
308
+ type: 'SEARCH',
309
+ dimension: 1536,
310
+ corpusSize: 10000,
311
+ queryCount: 100,
312
+ k: 100,
313
+ warmupQueries: 20
314
+ }
315
+ ];
316
+
72
317
  /**
73
318
  * Performance metrics
74
319
  */
@@ -367,9 +612,9 @@ async function benchmarkPostgreSQL(scenario) {
367
612
 
368
613
  return metrics.getReport();
369
614
  } catch (error) {
370
- console.error(' PostgreSQL benchmark failed:', error.message);
371
- await pool.end();
372
- return { throughput: 0, p50: 0, p99: 0, errors: 1, lockTimeouts: 0, totalOps: 0, skipped: true };
615
+ console.error(' PostgreSQL benchmark skipped:', error.message);
616
+ await pool.end().catch(() => {});
617
+ return { throughput: 0, p50: 0, p99: 0, errors: 0, lockTimeouts: 0, totalOps: 0, skipped: true };
373
618
  }
374
619
  }
375
620
 
@@ -504,13 +749,428 @@ async function benchmarkPgserve(scenario, useRam = false) {
504
749
  }
505
750
  }
506
751
 
752
+ // ============================================================================
753
+ // VECTOR BENCHMARKS (pgvector)
754
+ // ============================================================================
755
+
756
+ /**
757
+ * PGlite Vector Benchmark
758
+ * Supports both INSERT and SEARCH scenarios
759
+ */
760
+ async function benchmarkPGliteVector(scenario, embeddings, queryVectors, groundTruth) {
761
+ console.log(' 🔹 Running PGlite vector benchmark...');
762
+
763
+ const dataDir = path.join(RESULTS_DIR, 'pglite-vector-bench');
764
+ if (fs.existsSync(dataDir)) {
765
+ fs.rmSync(dataDir, { recursive: true });
766
+ }
767
+
768
+ try {
769
+ // Create PGlite with pgvector extension
770
+ const db = new PGlite(dataDir, { extensions: { vector } });
771
+ await db.exec('CREATE EXTENSION IF NOT EXISTS vector');
772
+
773
+ // Setup schema
774
+ await db.exec(`
775
+ DROP TABLE IF EXISTS embeddings;
776
+ CREATE TABLE embeddings (
777
+ id INTEGER PRIMARY KEY,
778
+ vector vector(${scenario.dimension})
779
+ )
780
+ `);
781
+
782
+ // INSERT scenario - measure insert speed
783
+ if (scenario.type === 'INSERT') {
784
+ console.log(` Inserting ${scenario.insertCount} vectors (measured)...`);
785
+ const latencies = [];
786
+
787
+ for (let i = 0; i < scenario.insertCount; i++) {
788
+ const vec = formatPgVector(embeddings.vectors[i]);
789
+ const start = performance.now();
790
+ await db.query('INSERT INTO embeddings (id, vector) VALUES ($1, $2::vector)', [i + 1, vec]);
791
+ latencies.push(performance.now() - start);
792
+ }
793
+
794
+ const totalTime = latencies.reduce((a, b) => a + b, 0);
795
+ const qps = Math.round((scenario.insertCount / totalTime) * 1000);
796
+ latencies.sort((a, b) => a - b);
797
+ const p50 = latencies[Math.floor(latencies.length * 0.5)]?.toFixed(1) || 0;
798
+ const p99 = latencies[Math.floor(latencies.length * 0.99)]?.toFixed(1) || 0;
799
+
800
+ try { await db.close(); } catch { /* ignore */ }
801
+
802
+ return {
803
+ throughput: qps,
804
+ recall: 'N/A',
805
+ p50: parseFloat(p50),
806
+ p99: parseFloat(p99),
807
+ errors: 0,
808
+ totalOps: scenario.insertCount,
809
+ skipped: false
810
+ };
811
+ }
812
+
813
+ // SEARCH scenario - measure recall and QPS
814
+ // Phase 1: Insert all vectors (not measured)
815
+ console.log(' Inserting vectors...');
816
+ for (let i = 0; i < embeddings.vectors.length; i++) {
817
+ const vec = formatPgVector(embeddings.vectors[i]);
818
+ await db.query('INSERT INTO embeddings (id, vector) VALUES ($1, $2::vector)', [i + 1, vec]);
819
+ }
820
+
821
+ // Phase 2: Warm-up queries (not measured)
822
+ console.log(' Warming up...');
823
+ for (let i = 0; i < scenario.warmupQueries; i++) {
824
+ const queryVec = formatPgVector(queryVectors[i % queryVectors.length]);
825
+ await db.query(`SELECT id FROM embeddings ORDER BY vector <-> $1::vector LIMIT $2`, [queryVec, scenario.k]);
826
+ }
827
+
828
+ // Phase 3: Measured queries with recall tracking
829
+ console.log(' Running measured queries...');
830
+ const latencies = [];
831
+ const approximateResults = [];
832
+
833
+ for (let i = 0; i < scenario.queryCount; i++) {
834
+ const queryVec = formatPgVector(queryVectors[i]);
835
+ const start = performance.now();
836
+ const result = await db.query(
837
+ `SELECT id FROM embeddings ORDER BY vector <-> $1::vector LIMIT $2`,
838
+ [queryVec, scenario.k]
839
+ );
840
+ latencies.push(performance.now() - start);
841
+
842
+ // Collect IDs for recall calculation
843
+ approximateResults.push(result.rows.map(r => r.id));
844
+ }
845
+
846
+ // Calculate recall
847
+ const { recall } = calculateRecall(approximateResults, groundTruth, scenario.k);
848
+
849
+ // Calculate metrics
850
+ const totalTime = latencies.reduce((a, b) => a + b, 0);
851
+ const qps = Math.round((scenario.queryCount / totalTime) * 1000);
852
+ latencies.sort((a, b) => a - b);
853
+ const p50 = latencies[Math.floor(latencies.length * 0.5)]?.toFixed(1) || 0;
854
+ const p99 = latencies[Math.floor(latencies.length * 0.99)]?.toFixed(1) || 0;
855
+
856
+ try { await db.close(); } catch { /* ignore */ }
857
+
858
+ return {
859
+ throughput: qps,
860
+ recall: (recall * 100).toFixed(1), // as percentage
861
+ p50: parseFloat(p50),
862
+ p99: parseFloat(p99),
863
+ errors: 0,
864
+ totalOps: scenario.queryCount,
865
+ skipped: false
866
+ };
867
+ } catch (error) {
868
+ console.error(' PGlite vector benchmark failed:', error.message);
869
+ return { throughput: 0, recall: 0, p50: 0, p99: 0, errors: 1, totalOps: 0, skipped: true };
870
+ }
871
+ }
872
+
873
+ /**
874
+ * PostgreSQL Server Vector Benchmark
875
+ * Supports both INSERT and SEARCH scenarios
876
+ */
877
+ async function benchmarkPostgreSQLVector(scenario, embeddings, queryVectors, groundTruth) {
878
+ console.log(' 🔷 Running PostgreSQL vector benchmark...');
879
+
880
+ const pool = new Pool({
881
+ ...POSTGRES_CONFIG,
882
+ max: 20
883
+ });
884
+
885
+ try {
886
+ // Test connection and check for pgvector
887
+ await pool.query('SELECT 1');
888
+
889
+ // Check if pgvector is available
890
+ try {
891
+ await pool.query('CREATE EXTENSION IF NOT EXISTS vector');
892
+ } catch (e) {
893
+ console.error(' pgvector extension not available. Use pgvector/pgvector:pg17 Docker image.');
894
+ await pool.end();
895
+ return { throughput: 0, recall: 'N/A', p50: 0, p99: 0, errors: 1, totalOps: 0, skipped: true };
896
+ }
897
+
898
+ // Setup schema
899
+ await pool.query(`
900
+ DROP TABLE IF EXISTS embeddings;
901
+ CREATE TABLE embeddings (
902
+ id INTEGER PRIMARY KEY,
903
+ vector vector(${scenario.dimension})
904
+ )
905
+ `);
906
+
907
+ // INSERT scenario - measure insert speed
908
+ if (scenario.type === 'INSERT') {
909
+ console.log(` Inserting ${scenario.insertCount} vectors (measured)...`);
910
+ const latencies = [];
911
+
912
+ for (let i = 0; i < scenario.insertCount; i++) {
913
+ const vec = formatPgVector(embeddings.vectors[i]);
914
+ const start = performance.now();
915
+ await pool.query('INSERT INTO embeddings (id, vector) VALUES ($1, $2::vector)', [i + 1, vec]);
916
+ latencies.push(performance.now() - start);
917
+ }
918
+
919
+ const totalTime = latencies.reduce((a, b) => a + b, 0);
920
+ const qps = Math.round((scenario.insertCount / totalTime) * 1000);
921
+ latencies.sort((a, b) => a - b);
922
+ const p50 = latencies[Math.floor(latencies.length * 0.5)]?.toFixed(1) || 0;
923
+ const p99 = latencies[Math.floor(latencies.length * 0.99)]?.toFixed(1) || 0;
924
+
925
+ await pool.query('DROP TABLE IF EXISTS embeddings');
926
+ await pool.end();
927
+
928
+ return {
929
+ throughput: qps,
930
+ recall: 'N/A',
931
+ p50: parseFloat(p50),
932
+ p99: parseFloat(p99),
933
+ errors: 0,
934
+ totalOps: scenario.insertCount,
935
+ skipped: false
936
+ };
937
+ }
938
+
939
+ // SEARCH scenario - Phase 1: Insert all vectors (not measured)
940
+ console.log(' Inserting vectors...');
941
+ for (let i = 0; i < embeddings.vectors.length; i++) {
942
+ const vec = formatPgVector(embeddings.vectors[i]);
943
+ await pool.query('INSERT INTO embeddings (id, vector) VALUES ($1, $2::vector)', [i + 1, vec]);
944
+ }
945
+
946
+ // Phase 2: Warm-up queries (not measured)
947
+ console.log(' Warming up...');
948
+ for (let i = 0; i < scenario.warmupQueries; i++) {
949
+ const queryVec = formatPgVector(queryVectors[i % queryVectors.length]);
950
+ await pool.query(`SELECT id FROM embeddings ORDER BY vector <-> $1::vector LIMIT $2`, [queryVec, scenario.k]);
951
+ }
952
+
953
+ // Phase 3: Measured queries with recall tracking
954
+ console.log(' Running measured queries...');
955
+ const latencies = [];
956
+ const approximateResults = [];
957
+
958
+ for (let i = 0; i < scenario.queryCount; i++) {
959
+ const queryVec = formatPgVector(queryVectors[i]);
960
+ const start = performance.now();
961
+ const result = await pool.query(
962
+ `SELECT id FROM embeddings ORDER BY vector <-> $1::vector LIMIT $2`,
963
+ [queryVec, scenario.k]
964
+ );
965
+ latencies.push(performance.now() - start);
966
+
967
+ // Collect IDs for recall calculation
968
+ approximateResults.push(result.rows.map(r => r.id));
969
+ }
970
+
971
+ // Calculate recall
972
+ const { recall } = calculateRecall(approximateResults, groundTruth, scenario.k);
973
+
974
+ // Calculate metrics
975
+ const totalTime = latencies.reduce((a, b) => a + b, 0);
976
+ const qps = Math.round((scenario.queryCount / totalTime) * 1000);
977
+ latencies.sort((a, b) => a - b);
978
+ const p50 = latencies[Math.floor(latencies.length * 0.5)]?.toFixed(1) || 0;
979
+ const p99 = latencies[Math.floor(latencies.length * 0.99)]?.toFixed(1) || 0;
980
+
981
+ // Cleanup
982
+ await pool.query('DROP TABLE IF EXISTS embeddings');
983
+ await pool.end();
984
+
985
+ return {
986
+ throughput: qps,
987
+ recall: (recall * 100).toFixed(1), // as percentage
988
+ p50: parseFloat(p50),
989
+ p99: parseFloat(p99),
990
+ errors: 0,
991
+ totalOps: scenario.queryCount,
992
+ skipped: false
993
+ };
994
+ } catch (error) {
995
+ console.error(' PostgreSQL vector benchmark skipped:', error.message);
996
+ await pool.end().catch(() => {});
997
+ return { throughput: 0, recall: 'N/A', p50: 0, p99: 0, errors: 0, totalOps: 0, skipped: true };
998
+ }
999
+ }
1000
+
1001
+ /**
1002
+ * pgserve Vector Benchmark
1003
+ * Supports both INSERT and SEARCH scenarios
1004
+ */
1005
+ async function benchmarkPgserveVector(scenario, embeddings, queryVectors, groundTruth, useRam = false) {
1006
+ const mode = useRam ? 'RAM' : 'disk';
1007
+ console.log(` 🚀 Running pgserve (${mode}) vector benchmark...`);
1008
+
1009
+ let server;
1010
+ try {
1011
+ // Start pgserve (use different ports for vector benchmarks to avoid conflicts)
1012
+ const port = useRam ? 18435 : 18434;
1013
+ server = await startMultiTenantServer({
1014
+ port,
1015
+ logLevel: 'error',
1016
+ useRam
1017
+ });
1018
+
1019
+ // Wait for server to be fully ready
1020
+ await new Promise(resolve => setTimeout(resolve, 2000));
1021
+
1022
+ const pool = new Pool({
1023
+ host: 'localhost',
1024
+ port,
1025
+ database: 'vector_bench',
1026
+ user: 'postgres',
1027
+ password: 'postgres',
1028
+ max: 20,
1029
+ connectionTimeoutMillis: 30000
1030
+ });
1031
+
1032
+ // Wait for connection with retries
1033
+ let connected = false;
1034
+ for (let i = 0; i < 10; i++) {
1035
+ try {
1036
+ await pool.query('SELECT 1');
1037
+ connected = true;
1038
+ break;
1039
+ } catch (error) {
1040
+ if (i === 9) throw error;
1041
+ await new Promise(resolve => setTimeout(resolve, 1000));
1042
+ }
1043
+ }
1044
+
1045
+ if (!connected) {
1046
+ throw new Error('Failed to connect to pgserve');
1047
+ }
1048
+
1049
+ // Enable pgvector extension
1050
+ try {
1051
+ await pool.query('CREATE EXTENSION IF NOT EXISTS vector');
1052
+ } catch (e) {
1053
+ console.error(` pgvector extension not available in pgserve. Install pgvector files to ~/.pgserve/bin/<platform>/`);
1054
+ await pool.end();
1055
+ await server.stop();
1056
+ return { throughput: 0, recall: 'N/A', p50: 0, p99: 0, errors: 0, totalOps: 0, skipped: true, reason: 'pgvector not installed' };
1057
+ }
1058
+
1059
+ // Setup schema
1060
+ await pool.query(`
1061
+ DROP TABLE IF EXISTS embeddings;
1062
+ CREATE TABLE embeddings (
1063
+ id INTEGER PRIMARY KEY,
1064
+ vector vector(${scenario.dimension})
1065
+ )
1066
+ `);
1067
+
1068
+ // INSERT scenario - measure insert speed
1069
+ if (scenario.type === 'INSERT') {
1070
+ console.log(` Inserting ${scenario.insertCount} vectors (measured)...`);
1071
+ const latencies = [];
1072
+
1073
+ for (let i = 0; i < scenario.insertCount; i++) {
1074
+ const vec = formatPgVector(embeddings.vectors[i]);
1075
+ const start = performance.now();
1076
+ await pool.query('INSERT INTO embeddings (id, vector) VALUES ($1, $2::vector)', [i + 1, vec]);
1077
+ latencies.push(performance.now() - start);
1078
+ }
1079
+
1080
+ const totalTime = latencies.reduce((a, b) => a + b, 0);
1081
+ const qps = Math.round((scenario.insertCount / totalTime) * 1000);
1082
+ latencies.sort((a, b) => a - b);
1083
+ const p50 = latencies[Math.floor(latencies.length * 0.5)]?.toFixed(1) || 0;
1084
+ const p99 = latencies[Math.floor(latencies.length * 0.99)]?.toFixed(1) || 0;
1085
+
1086
+ await pool.query('DROP TABLE IF EXISTS embeddings');
1087
+ await pool.end();
1088
+ await server.stop();
1089
+
1090
+ return {
1091
+ throughput: qps,
1092
+ recall: 'N/A',
1093
+ p50: parseFloat(p50),
1094
+ p99: parseFloat(p99),
1095
+ errors: 0,
1096
+ totalOps: scenario.insertCount,
1097
+ skipped: false
1098
+ };
1099
+ }
1100
+
1101
+ // SEARCH scenario - Phase 1: Insert all vectors (not measured)
1102
+ console.log(' Inserting vectors...');
1103
+ for (let i = 0; i < embeddings.vectors.length; i++) {
1104
+ const vec = formatPgVector(embeddings.vectors[i]);
1105
+ await pool.query('INSERT INTO embeddings (id, vector) VALUES ($1, $2::vector)', [i + 1, vec]);
1106
+ }
1107
+
1108
+ // Phase 2: Warm-up queries (not measured)
1109
+ console.log(' Warming up...');
1110
+ for (let i = 0; i < scenario.warmupQueries; i++) {
1111
+ const queryVec = formatPgVector(queryVectors[i % queryVectors.length]);
1112
+ await pool.query(`SELECT id FROM embeddings ORDER BY vector <-> $1::vector LIMIT $2`, [queryVec, scenario.k]);
1113
+ }
1114
+
1115
+ // Phase 3: Measured queries with recall tracking
1116
+ console.log(' Running measured queries...');
1117
+ const latencies = [];
1118
+ const approximateResults = [];
1119
+
1120
+ for (let i = 0; i < scenario.queryCount; i++) {
1121
+ const queryVec = formatPgVector(queryVectors[i]);
1122
+ const start = performance.now();
1123
+ const result = await pool.query(
1124
+ `SELECT id FROM embeddings ORDER BY vector <-> $1::vector LIMIT $2`,
1125
+ [queryVec, scenario.k]
1126
+ );
1127
+ latencies.push(performance.now() - start);
1128
+
1129
+ // Collect IDs for recall calculation
1130
+ approximateResults.push(result.rows.map(r => r.id));
1131
+ }
1132
+
1133
+ // Calculate recall
1134
+ const { recall } = calculateRecall(approximateResults, groundTruth, scenario.k);
1135
+
1136
+ // Calculate metrics
1137
+ const totalTime = latencies.reduce((a, b) => a + b, 0);
1138
+ const qps = Math.round((scenario.queryCount / totalTime) * 1000);
1139
+ latencies.sort((a, b) => a - b);
1140
+ const p50 = latencies[Math.floor(latencies.length * 0.5)]?.toFixed(1) || 0;
1141
+ const p99 = latencies[Math.floor(latencies.length * 0.99)]?.toFixed(1) || 0;
1142
+
1143
+ // Cleanup
1144
+ await pool.query('DROP TABLE IF EXISTS embeddings');
1145
+ await pool.end();
1146
+ await server.stop();
1147
+
1148
+ return {
1149
+ throughput: qps,
1150
+ recall: (recall * 100).toFixed(1), // as percentage
1151
+ p50: parseFloat(p50),
1152
+ p99: parseFloat(p99),
1153
+ errors: 0,
1154
+ totalOps: scenario.queryCount,
1155
+ skipped: false
1156
+ };
1157
+ } catch (error) {
1158
+ console.error(` pgserve (${mode}) vector benchmark failed:`, error.message);
1159
+ if (server) {
1160
+ try { await server.stop(); } catch { /* ignore */ }
1161
+ }
1162
+ return { throughput: 0, recall: 'N/A', p50: 0, p99: 0, errors: 0, totalOps: 0, skipped: true, reason: error.message };
1163
+ }
1164
+ }
1165
+
507
1166
  /**
508
1167
  * Generate comparison report
509
1168
  */
510
- function generateReport(results) {
1169
+ function generateReport(results, vectorResults = []) {
511
1170
  const report = {
512
1171
  timestamp: new Date().toISOString(),
513
- scenarios: results
1172
+ scenarios: results,
1173
+ vectorScenarios: vectorResults
514
1174
  };
515
1175
 
516
1176
  // Save JSON
@@ -614,6 +1274,58 @@ function generateReport(results) {
614
1274
  }
615
1275
  }
616
1276
 
1277
+ // Vector benchmark results (with Recall@k)
1278
+ if (vectorResults && vectorResults.length > 0) {
1279
+ md += '---\n\n';
1280
+ md += '## Vector Benchmarks (pgvector) - Recall@k Methodology\n\n';
1281
+ md += '*Following industry-standard ANN-Benchmarks methodology: comparing approximate results to brute-force ground truth.*\n\n';
1282
+
1283
+ for (const scenario of vectorResults) {
1284
+ md += `### ${scenario.name}\n\n`;
1285
+ md += `${scenario.description}\n\n`;
1286
+
1287
+ const { pglite, postgres, pgserve, pgserveRam, k } = scenario;
1288
+
1289
+ md += '```\n';
1290
+ md += '┌─────────────────┬──────────┬──────────┬──────────┬─────────────┐\n';
1291
+ md += '│ Metric │ PGlite │ PostgreSQL│ pgserve │ pgserve RAM │\n';
1292
+ md += '├─────────────────┼──────────┼──────────┼──────────┼─────────────┤\n';
1293
+
1294
+ const pad = (s, n) => String(s).padEnd(n);
1295
+ const val = (r, key) => r.skipped ? 'N/A' : r[key];
1296
+ const recallVal = (r) => r.skipped ? 'N/A' : `${r.recall}%`;
1297
+
1298
+ md += `│ Recall@${String(k || 10).padEnd(8)}│ ${pad(recallVal(pglite), 8)} │ ${pad(recallVal(postgres), 9)} │ ${pad(recallVal(pgserve), 8)} │ ${pad(recallVal(pgserveRam), 11)} │\n`;
1299
+ md += `│ Throughput (qps)│ ${pad(val(pglite, 'throughput'), 8)} │ ${pad(val(postgres, 'throughput'), 9)} │ ${pad(val(pgserve, 'throughput'), 8)} │ ${pad(val(pgserveRam, 'throughput'), 11)} │\n`;
1300
+ md += `│ P50 latency (ms)│ ${pad(val(pglite, 'p50'), 8)} │ ${pad(val(postgres, 'p50'), 9)} │ ${pad(val(pgserve, 'p50'), 8)} │ ${pad(val(pgserveRam, 'p50'), 11)} │\n`;
1301
+ md += `│ P99 latency (ms)│ ${pad(val(pglite, 'p99'), 8)} │ ${pad(val(postgres, 'p99'), 9)} │ ${pad(val(pgserve, 'p99'), 8)} │ ${pad(val(pgserveRam, 'p99'), 11)} │\n`;
1302
+ md += `│ Errors │ ${pad(val(pglite, 'errors'), 8)} │ ${pad(val(postgres, 'errors'), 9)} │ ${pad(val(pgserve, 'errors'), 8)} │ ${pad(val(pgserveRam, 'errors'), 11)} │\n`;
1303
+ md += '└─────────────────┴──────────┴──────────┴──────────┴─────────────┘\n';
1304
+ md += '```\n\n';
1305
+
1306
+ // Find winner among non-skipped (considering both recall and throughput)
1307
+ const candidates = {};
1308
+ if (!pglite.skipped) candidates.pglite = { recall: parseFloat(pglite.recall), qps: pglite.throughput };
1309
+ if (!postgres.skipped) candidates.postgres = { recall: parseFloat(postgres.recall), qps: postgres.throughput };
1310
+ if (!pgserve.skipped) candidates.pgserve = { recall: parseFloat(pgserve.recall), qps: pgserve.throughput };
1311
+ if (pgserveRam && !pgserveRam.skipped) candidates.pgserveRam = { recall: parseFloat(pgserveRam.recall), qps: pgserveRam.throughput };
1312
+
1313
+ if (Object.keys(candidates).length > 0) {
1314
+ const nameMap = { pglite: 'PGlite', postgres: 'PostgreSQL', pgserve: 'pgserve', pgserveRam: 'pgserve RAM' };
1315
+ // Winner = highest QPS among those with 100% recall, otherwise highest recall
1316
+ const perfect = Object.entries(candidates).filter(([, v]) => v.recall === 100);
1317
+ let winnerKey;
1318
+ if (perfect.length > 0) {
1319
+ winnerKey = perfect.reduce((a, b) => a[1].qps > b[1].qps ? a : b)[0];
1320
+ md += `**${nameMap[winnerKey]} wins** (100% recall @ ${candidates[winnerKey].qps} qps)\n\n`;
1321
+ } else {
1322
+ winnerKey = Object.entries(candidates).reduce((a, b) => a[1].recall > b[1].recall ? a : b)[0];
1323
+ md += `**${nameMap[winnerKey]} wins** (${candidates[winnerKey].recall}% recall @ ${candidates[winnerKey].qps} qps)\n\n`;
1324
+ }
1325
+ }
1326
+ }
1327
+ }
1328
+
617
1329
  md += '---\n\n';
618
1330
  md += '## Why pgserve?\n\n';
619
1331
  md += '- **TRUE Concurrency**: Native PostgreSQL process forking\n';
@@ -636,10 +1348,36 @@ function generateReport(results) {
636
1348
  * Main runner
637
1349
  */
638
1350
  async function main() {
639
- console.log('╔════════════════════════════════════════════════════════════════╗');
640
- console.log('║ pgserve Benchmark Suite ║');
641
- console.log('║ Comparing: SQLite | PGlite | PostgreSQL | pgserve ║');
642
- console.log('╚════════════════════════════════════════════════════════════════╝\n');
1351
+ // Parse CLI args
1352
+ const args = process.argv.slice(2);
1353
+ const includeVector = args.includes('--include-vector') || args.includes('--vector');
1354
+ const vectorOnly = args.includes('--vector-only');
1355
+
1356
+ if (args.includes('--help') || args.includes('-h')) {
1357
+ console.log(`
1358
+ pgserve Benchmark Suite
1359
+
1360
+ Usage:
1361
+ bun tests/benchmarks/runner.js [options]
1362
+
1363
+ Options:
1364
+ --include-vector Include vector (pgvector) benchmarks
1365
+ --vector-only Run only vector benchmarks
1366
+ --help, -h Show this help
1367
+
1368
+ Vector benchmarks require:
1369
+ - PGLite: Built-in pgvector support
1370
+ - PostgreSQL: Docker image pgvector/pgvector:pg17
1371
+ - pgserve: Not yet supported (marked as skipped)
1372
+ `);
1373
+ process.exit(0);
1374
+ }
1375
+
1376
+ // Print banner
1377
+ banner();
1378
+ if (includeVector || vectorOnly) {
1379
+ console.log(`${C.dim} + Vector benchmarks (pgvector) enabled${C.reset}\n`);
1380
+ }
643
1381
 
644
1382
  // Ensure results directory exists
645
1383
  if (!fs.existsSync(RESULTS_DIR)) {
@@ -647,54 +1385,137 @@ async function main() {
647
1385
  }
648
1386
 
649
1387
  const results = [];
1388
+ const vectorResults = [];
650
1389
 
651
1390
  // Check if RAM mode is available (Linux only with /dev/shm)
652
1391
  const canUseRam = os.platform() === 'linux' && fs.existsSync('/dev/shm');
653
1392
  if (canUseRam) {
654
- console.log('💾 RAM mode available (/dev/shm detected)\n');
1393
+ console.log(`${C.green}💾 RAM mode available (/dev/shm detected)${C.reset}\n`);
655
1394
  } else {
656
- console.log('⚠️ RAM mode not available (Linux /dev/shm required)\n');
657
- }
658
-
659
- for (const scenario of scenarios) {
660
- console.log(`\n📊 Scenario: ${scenario.name}`);
661
- console.log(` ${scenario.description}\n`);
662
-
663
- const sqlite = await benchmarkSQLite(scenario);
664
- const pglite = await benchmarkPGlite(scenario);
665
- const postgres = await benchmarkPostgreSQL(scenario);
666
- const pgserve = await benchmarkPgserve(scenario, false); // disk mode
667
- const pgserveRam = canUseRam
668
- ? await benchmarkPgserve(scenario, true) // RAM mode
669
- : { throughput: 0, p50: 0, p99: 0, errors: 0, lockTimeouts: 0, totalOps: 0, skipped: true };
670
-
671
- results.push({
672
- name: scenario.name,
673
- description: scenario.description,
674
- sqlite,
675
- pglite,
676
- postgres,
677
- pgserve,
678
- pgserveRam
679
- });
1395
+ console.log(`${C.yellow}⚠️ RAM mode not available (Linux /dev/shm required)${C.reset}\n`);
1396
+ }
680
1397
 
681
- console.log(`\n SQLite: ${sqlite.throughput} qps, P50=${sqlite.p50}ms, errors=${sqlite.errors}`);
682
- console.log(` PGlite: ${pglite.throughput} qps, P50=${pglite.p50}ms, errors=${pglite.errors}`);
683
- console.log(` PostgreSQL: ${postgres.throughput} qps, P50=${postgres.p50}ms, errors=${postgres.errors}`);
684
- console.log(` pgserve: ${pgserve.throughput} qps, P50=${pgserve.p50}ms, errors=${pgserve.errors}`);
685
- if (canUseRam) {
686
- console.log(` pgserve (RAM): ${pgserveRam.throughput} qps, P50=${pgserveRam.p50}ms, errors=${pgserveRam.errors}`);
1398
+ // Run CRUD benchmarks (unless --vector-only)
1399
+ if (!vectorOnly) {
1400
+ for (const scenario of scenarios) {
1401
+ section(scenario.name, scenario.description);
1402
+
1403
+ const sqlite = await benchmarkSQLite(scenario);
1404
+ const pglite = await benchmarkPGlite(scenario);
1405
+ const postgres = await benchmarkPostgreSQL(scenario);
1406
+ const pgserve = await benchmarkPgserve(scenario, false); // disk mode
1407
+ const pgserveRam = canUseRam
1408
+ ? await benchmarkPgserve(scenario, true) // RAM mode
1409
+ : { throughput: 0, p50: 0, p99: 0, errors: 0, lockTimeouts: 0, totalOps: 0, skipped: true };
1410
+
1411
+ results.push({
1412
+ name: scenario.name,
1413
+ description: scenario.description,
1414
+ sqlite,
1415
+ pglite,
1416
+ postgres,
1417
+ pgserve,
1418
+ pgserveRam
1419
+ });
1420
+
1421
+ // Scenario results summary
1422
+ console.log(`\n ${C.bold}Results:${C.reset}`);
1423
+ console.log(` ${C.dim}──────────────────────────────────────────${C.reset}`);
1424
+ console.log(` ${C.yellow}🔸${C.reset} SQLite: ${C.bold}${sqlite.throughput}${C.reset} qps, P50=${sqlite.p50}ms, P99=${sqlite.p99}ms`);
1425
+ console.log(` ${C.blue}🔹${C.reset} PGlite: ${C.bold}${pglite.throughput}${C.reset} qps, P50=${pglite.p50}ms, P99=${pglite.p99}ms`);
1426
+ console.log(` ${C.cyan}🔷${C.reset} PostgreSQL: ${postgres.skipped ? `${C.dim}SKIPPED${C.reset}` : `${C.bold}${postgres.throughput}${C.reset} qps, P50=${postgres.p50}ms, P99=${postgres.p99}ms`}`);
1427
+ console.log(` ${C.green}🚀${C.reset} pgserve: ${C.bold}${pgserve.throughput}${C.reset} qps, P50=${pgserve.p50}ms, P99=${pgserve.p99}ms`);
1428
+ if (canUseRam) {
1429
+ console.log(` ${C.magenta}⚡${C.reset} pgserve (RAM): ${C.bold}${pgserveRam.throughput}${C.reset} qps, P50=${pgserveRam.p50}ms, P99=${pgserveRam.p99}ms`);
1430
+ }
687
1431
  }
688
1432
  }
689
1433
 
690
- console.log('\n📄 Generating report...\n');
691
- generateReport(results);
1434
+ // Run vector benchmarks (if --include-vector or --vector-only)
1435
+ if (includeVector || vectorOnly) {
1436
+ console.log(`\n${C.cyan}${C.bold}
1437
+ ╔════════════════════════════════════════════════════════════════╗
1438
+ ║ Vector Benchmarks (pgvector) - Recall@k Methodology ║
1439
+ ╚════════════════════════════════════════════════════════════════╝${C.reset}\n`);
1440
+
1441
+ // Load or generate embeddings
1442
+ console.log(`${C.dim}📦 Loading embeddings...${C.reset}`);
1443
+ const dimension = 1536;
1444
+ const corpusSize = 10000; // 10k vectors (~60MB) to exceed buffer cache and show RAM vs disk difference
1445
+
1446
+ // Ensure embeddings file exists
1447
+ getEmbeddingsPath(corpusSize, dimension);
1448
+ const embeddings = loadEmbeddings(`embeddings-${corpusSize}-${dimension}.json`);
1449
+ const queryVectors = generateQueryVectors(100, dimension);
1450
+ console.log(`${C.dim} Loaded ${embeddings.vectors.length} corpus vectors, ${queryVectors.length} query vectors${C.reset}\n`);
1451
+
1452
+ for (const scenario of vectorScenarios) {
1453
+ section(`Vector: ${scenario.name}`, scenario.description);
1454
+
1455
+ let groundTruth = null;
1456
+
1457
+ // Only compute ground truth for SEARCH scenarios
1458
+ if (scenario.type === 'SEARCH') {
1459
+ console.log(`${C.dim} 📐 Computing ground truth (brute-force k=${scenario.k})...${C.reset}`);
1460
+ groundTruth = getGroundTruth(
1461
+ embeddings.vectors,
1462
+ queryVectors.slice(0, scenario.queryCount),
1463
+ scenario.k,
1464
+ `corpus-${corpusSize}-dim-${dimension}-queries-${scenario.queryCount}`
1465
+ );
1466
+ }
1467
+
1468
+ // Run benchmarks
1469
+ const pglite = await benchmarkPGliteVector(scenario, embeddings, queryVectors, groundTruth);
1470
+ const postgres = await benchmarkPostgreSQLVector(scenario, embeddings, queryVectors, groundTruth);
1471
+ const pgserve = await benchmarkPgserveVector(scenario, embeddings, queryVectors, groundTruth, false);
1472
+ const pgserveRam = canUseRam
1473
+ ? await benchmarkPgserveVector(scenario, embeddings, queryVectors, groundTruth, true)
1474
+ : { throughput: 0, recall: 'N/A', p50: 0, p99: 0, errors: 0, totalOps: 0, skipped: true };
1475
+
1476
+ vectorResults.push({
1477
+ name: scenario.name,
1478
+ description: scenario.description,
1479
+ type: scenario.type,
1480
+ k: scenario.k,
1481
+ pglite,
1482
+ postgres,
1483
+ pgserve,
1484
+ pgserveRam
1485
+ });
1486
+
1487
+ // Vector scenario results summary
1488
+ const isInsert = scenario.type === 'INSERT';
1489
+ console.log(`\n ${C.bold}Results (${isInsert ? 'INSERT QPS' : `Recall@${scenario.k} + QPS`}):${C.reset}`);
1490
+ console.log(` ${C.dim}──────────────────────────────────────────────────────${C.reset}`);
1491
+ const formatResult = (r, icon, color, name) => {
1492
+ if (r.skipped) return ` ${color}${icon}${C.reset} ${name.padEnd(14)} ${C.dim}SKIPPED${r.reason ? ` (${r.reason})` : ''}${C.reset}`;
1493
+ if (isInsert) {
1494
+ return ` ${color}${icon}${C.reset} ${name.padEnd(14)} ${C.bold}${r.throughput}${C.reset} inserts/sec, P50=${r.p50}ms, P99=${r.p99}ms`;
1495
+ }
1496
+ return ` ${color}${icon}${C.reset} ${name.padEnd(14)} Recall: ${C.bold}${r.recall}%${C.reset}, ${C.bold}${r.throughput}${C.reset} qps, P50=${r.p50}ms`;
1497
+ };
1498
+ console.log(formatResult(pglite, '🔹', C.blue, 'PGlite:'));
1499
+ console.log(formatResult(postgres, '🔷', C.cyan, 'PostgreSQL:'));
1500
+ console.log(formatResult(pgserve, '🚀', C.green, 'pgserve:'));
1501
+ if (canUseRam) {
1502
+ console.log(formatResult(pgserveRam, '⚡', C.magenta, 'pgserve (RAM):'));
1503
+ }
1504
+ }
1505
+ }
1506
+
1507
+ // Save detailed JSON report
1508
+ generateReport(results, vectorResults);
1509
+
1510
+ // Print final results table with scores
1511
+ printFinalResults(results, vectorResults, canUseRam);
692
1512
 
693
- console.log('╔════════════════════════════════════════════════════════════════╗');
694
- console.log('║ Benchmarks Complete! ║');
695
- console.log('║ ║');
696
- console.log('║ Try it yourself: npx pgserve ║');
697
- console.log('╚════════════════════════════════════════════════════════════════╝\n');
1513
+ console.log(`${C.cyan}${C.bold}╔════════════════════════════════════════════════════════════════╗
1514
+ ║ Benchmarks Complete! ║
1515
+ ║ ║
1516
+ ║ Try it yourself: npx pgserve ║
1517
+ ╚════════════════════════════════════════════════════════════════╝${C.reset}
1518
+ `);
698
1519
  }
699
1520
 
700
1521
  main().catch(console.error);