@sparkleideas/plugins 3.0.0-alpha.10
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.
- package/README.md +401 -0
- package/__tests__/collection-manager.test.ts +332 -0
- package/__tests__/dependency-graph.test.ts +434 -0
- package/__tests__/enhanced-plugin-registry.test.ts +488 -0
- package/__tests__/plugin-registry.test.ts +368 -0
- package/__tests__/ruvector-bridge.test.ts +2429 -0
- package/__tests__/ruvector-integration.test.ts +1602 -0
- package/__tests__/ruvector-migrations.test.ts +1099 -0
- package/__tests__/ruvector-quantization.test.ts +846 -0
- package/__tests__/ruvector-streaming.test.ts +1088 -0
- package/__tests__/sdk.test.ts +325 -0
- package/__tests__/security.test.ts +348 -0
- package/__tests__/utils/ruvector-test-utils.ts +860 -0
- package/examples/plugin-creator/index.ts +636 -0
- package/examples/plugin-creator/plugin-creator.test.ts +312 -0
- package/examples/ruvector/README.md +288 -0
- package/examples/ruvector/attention-patterns.ts +394 -0
- package/examples/ruvector/basic-usage.ts +288 -0
- package/examples/ruvector/docker-compose.yml +75 -0
- package/examples/ruvector/gnn-analysis.ts +501 -0
- package/examples/ruvector/hyperbolic-hierarchies.ts +557 -0
- package/examples/ruvector/init-db.sql +119 -0
- package/examples/ruvector/quantization.ts +680 -0
- package/examples/ruvector/self-learning.ts +447 -0
- package/examples/ruvector/semantic-search.ts +576 -0
- package/examples/ruvector/streaming-large-data.ts +507 -0
- package/examples/ruvector/transactions.ts +594 -0
- package/examples/ruvector-plugins/hook-pattern-library.ts +486 -0
- package/examples/ruvector-plugins/index.ts +79 -0
- package/examples/ruvector-plugins/intent-router.ts +354 -0
- package/examples/ruvector-plugins/mcp-tool-optimizer.ts +424 -0
- package/examples/ruvector-plugins/reasoning-bank.ts +657 -0
- package/examples/ruvector-plugins/ruvector-plugins.test.ts +518 -0
- package/examples/ruvector-plugins/semantic-code-search.ts +498 -0
- package/examples/ruvector-plugins/shared/index.ts +20 -0
- package/examples/ruvector-plugins/shared/vector-utils.ts +257 -0
- package/examples/ruvector-plugins/sona-learning.ts +445 -0
- package/package.json +97 -0
- package/src/collections/collection-manager.ts +661 -0
- package/src/collections/index.ts +56 -0
- package/src/collections/official/index.ts +1040 -0
- package/src/core/base-plugin.ts +416 -0
- package/src/core/plugin-interface.ts +215 -0
- package/src/hooks/index.ts +685 -0
- package/src/index.ts +378 -0
- package/src/integrations/agentic-flow.ts +743 -0
- package/src/integrations/index.ts +88 -0
- package/src/integrations/ruvector/ARCHITECTURE.md +1245 -0
- package/src/integrations/ruvector/attention-advanced.ts +1040 -0
- package/src/integrations/ruvector/attention-executor.ts +782 -0
- package/src/integrations/ruvector/attention-mechanisms.ts +757 -0
- package/src/integrations/ruvector/attention.ts +1063 -0
- package/src/integrations/ruvector/gnn.ts +3050 -0
- package/src/integrations/ruvector/hyperbolic.ts +1948 -0
- package/src/integrations/ruvector/index.ts +394 -0
- package/src/integrations/ruvector/migrations/001_create_extension.sql +135 -0
- package/src/integrations/ruvector/migrations/002_create_vector_tables.sql +259 -0
- package/src/integrations/ruvector/migrations/003_create_indices.sql +328 -0
- package/src/integrations/ruvector/migrations/004_create_functions.sql +598 -0
- package/src/integrations/ruvector/migrations/005_create_attention_functions.sql +654 -0
- package/src/integrations/ruvector/migrations/006_create_gnn_functions.sql +728 -0
- package/src/integrations/ruvector/migrations/007_create_hyperbolic_functions.sql +762 -0
- package/src/integrations/ruvector/migrations/index.ts +35 -0
- package/src/integrations/ruvector/migrations/migrations.ts +647 -0
- package/src/integrations/ruvector/quantization.ts +2036 -0
- package/src/integrations/ruvector/ruvector-bridge.ts +2000 -0
- package/src/integrations/ruvector/self-learning.ts +2376 -0
- package/src/integrations/ruvector/streaming.ts +1737 -0
- package/src/integrations/ruvector/types.ts +1945 -0
- package/src/providers/index.ts +643 -0
- package/src/registry/dependency-graph.ts +568 -0
- package/src/registry/enhanced-plugin-registry.ts +994 -0
- package/src/registry/plugin-registry.ts +604 -0
- package/src/sdk/index.ts +563 -0
- package/src/security/index.ts +594 -0
- package/src/types/index.ts +446 -0
- package/src/workers/index.ts +700 -0
- package/tmp.json +0 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +23 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuVector PostgreSQL Bridge - Quantization Example
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* - Comparing different quantization methods
|
|
6
|
+
* - Measuring recall vs compression trade-offs
|
|
7
|
+
* - Production configuration recommendations
|
|
8
|
+
* - Memory optimization strategies
|
|
9
|
+
*
|
|
10
|
+
* Run with: npx ts-node examples/ruvector/quantization.ts
|
|
11
|
+
*
|
|
12
|
+
* @module @sparkleideas/plugins/examples/ruvector/quantization
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createRuVectorBridge,
|
|
17
|
+
type RuVectorBridge,
|
|
18
|
+
type VectorRecord,
|
|
19
|
+
} from '../../src/integrations/ruvector/index.js';
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Configuration
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
const config = {
|
|
26
|
+
connection: {
|
|
27
|
+
host: process.env.POSTGRES_HOST || 'localhost',
|
|
28
|
+
port: parseInt(process.env.POSTGRES_PORT || '5432', 10),
|
|
29
|
+
database: process.env.POSTGRES_DB || 'vectors',
|
|
30
|
+
user: process.env.POSTGRES_USER || 'postgres',
|
|
31
|
+
password: process.env.POSTGRES_PASSWORD || 'postgres',
|
|
32
|
+
},
|
|
33
|
+
dimensions: 768, // Typical embedding dimension
|
|
34
|
+
testVectors: 10000, // Number of test vectors
|
|
35
|
+
queryVectors: 100, // Number of query vectors
|
|
36
|
+
k: 10, // Top-k for recall calculation
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Quantization Types
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
type QuantizationMethod = 'none' | 'int8' | 'int4' | 'binary' | 'pq';
|
|
44
|
+
|
|
45
|
+
interface QuantizationConfig {
|
|
46
|
+
method: QuantizationMethod;
|
|
47
|
+
name: string;
|
|
48
|
+
bitsPerComponent: number;
|
|
49
|
+
description: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const quantizationMethods: QuantizationConfig[] = [
|
|
53
|
+
{
|
|
54
|
+
method: 'none',
|
|
55
|
+
name: 'Float32 (No Quantization)',
|
|
56
|
+
bitsPerComponent: 32,
|
|
57
|
+
description: 'Full precision floating point',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
method: 'int8',
|
|
61
|
+
name: 'Int8 Scalar Quantization',
|
|
62
|
+
bitsPerComponent: 8,
|
|
63
|
+
description: '4x compression, ~99% recall',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
method: 'int4',
|
|
67
|
+
name: 'Int4 Scalar Quantization',
|
|
68
|
+
bitsPerComponent: 4,
|
|
69
|
+
description: '8x compression, ~95% recall',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
method: 'binary',
|
|
73
|
+
name: 'Binary Quantization',
|
|
74
|
+
bitsPerComponent: 1,
|
|
75
|
+
description: '32x compression, ~85% recall',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
method: 'pq',
|
|
79
|
+
name: 'Product Quantization (PQ)',
|
|
80
|
+
bitsPerComponent: 8, // per subvector
|
|
81
|
+
description: 'Adaptive compression, good for high dimensions',
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Quantization Implementation
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Scalar quantization to Int8.
|
|
91
|
+
*/
|
|
92
|
+
function quantizeInt8(vector: number[]): { quantized: Int8Array; scale: number; offset: number } {
|
|
93
|
+
const min = Math.min(...vector);
|
|
94
|
+
const max = Math.max(...vector);
|
|
95
|
+
const scale = (max - min) / 255;
|
|
96
|
+
const offset = min;
|
|
97
|
+
|
|
98
|
+
const quantized = new Int8Array(vector.length);
|
|
99
|
+
for (let i = 0; i < vector.length; i++) {
|
|
100
|
+
quantized[i] = Math.round((vector[i] - offset) / scale) - 128;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { quantized, scale, offset };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Dequantize Int8 back to float.
|
|
108
|
+
*/
|
|
109
|
+
function dequantizeInt8(data: { quantized: Int8Array; scale: number; offset: number }): number[] {
|
|
110
|
+
const result = new Array(data.quantized.length);
|
|
111
|
+
for (let i = 0; i < data.quantized.length; i++) {
|
|
112
|
+
result[i] = (data.quantized[i] + 128) * data.scale + data.offset;
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Scalar quantization to Int4 (packed).
|
|
119
|
+
*/
|
|
120
|
+
function quantizeInt4(vector: number[]): { quantized: Uint8Array; scale: number; offset: number } {
|
|
121
|
+
const min = Math.min(...vector);
|
|
122
|
+
const max = Math.max(...vector);
|
|
123
|
+
const scale = (max - min) / 15;
|
|
124
|
+
const offset = min;
|
|
125
|
+
|
|
126
|
+
// Pack two Int4 values into one byte
|
|
127
|
+
const packedLength = Math.ceil(vector.length / 2);
|
|
128
|
+
const quantized = new Uint8Array(packedLength);
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < vector.length; i += 2) {
|
|
131
|
+
const v1 = Math.round((vector[i] - offset) / scale) & 0x0F;
|
|
132
|
+
const v2 = i + 1 < vector.length
|
|
133
|
+
? Math.round((vector[i + 1] - offset) / scale) & 0x0F
|
|
134
|
+
: 0;
|
|
135
|
+
quantized[i / 2] = (v1 << 4) | v2;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { quantized, scale, offset };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Dequantize Int4 back to float.
|
|
143
|
+
*/
|
|
144
|
+
function dequantizeInt4(
|
|
145
|
+
data: { quantized: Uint8Array; scale: number; offset: number },
|
|
146
|
+
originalLength: number
|
|
147
|
+
): number[] {
|
|
148
|
+
const result = new Array(originalLength);
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < originalLength; i += 2) {
|
|
151
|
+
const packed = data.quantized[i / 2];
|
|
152
|
+
result[i] = ((packed >> 4) & 0x0F) * data.scale + data.offset;
|
|
153
|
+
if (i + 1 < originalLength) {
|
|
154
|
+
result[i + 1] = (packed & 0x0F) * data.scale + data.offset;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Binary quantization (sign bit only).
|
|
163
|
+
*/
|
|
164
|
+
function quantizeBinary(vector: number[]): Uint8Array {
|
|
165
|
+
const packedLength = Math.ceil(vector.length / 8);
|
|
166
|
+
const quantized = new Uint8Array(packedLength);
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < vector.length; i++) {
|
|
169
|
+
if (vector[i] >= 0) {
|
|
170
|
+
quantized[Math.floor(i / 8)] |= (1 << (7 - (i % 8)));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return quantized;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Compute Hamming distance for binary vectors.
|
|
179
|
+
*/
|
|
180
|
+
function hammingDistance(a: Uint8Array, b: Uint8Array): number {
|
|
181
|
+
let distance = 0;
|
|
182
|
+
for (let i = 0; i < a.length; i++) {
|
|
183
|
+
// Count differing bits
|
|
184
|
+
let xor = a[i] ^ b[i];
|
|
185
|
+
while (xor) {
|
|
186
|
+
distance += xor & 1;
|
|
187
|
+
xor >>= 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return distance;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Product Quantization (simplified).
|
|
195
|
+
*/
|
|
196
|
+
class ProductQuantizer {
|
|
197
|
+
private numSubvectors: number;
|
|
198
|
+
private subvectorDim: number;
|
|
199
|
+
private codebooks: number[][][]; // [subvector][centroid][dimension]
|
|
200
|
+
private numCentroids: number = 256;
|
|
201
|
+
|
|
202
|
+
constructor(dimension: number, numSubvectors: number = 8) {
|
|
203
|
+
this.numSubvectors = numSubvectors;
|
|
204
|
+
this.subvectorDim = Math.ceil(dimension / numSubvectors);
|
|
205
|
+
this.codebooks = [];
|
|
206
|
+
|
|
207
|
+
// Initialize random codebooks (in production, train on data)
|
|
208
|
+
for (let m = 0; m < numSubvectors; m++) {
|
|
209
|
+
const codebook: number[][] = [];
|
|
210
|
+
for (let c = 0; c < this.numCentroids; c++) {
|
|
211
|
+
const centroid = Array.from(
|
|
212
|
+
{ length: this.subvectorDim },
|
|
213
|
+
() => Math.random() * 2 - 1
|
|
214
|
+
);
|
|
215
|
+
codebook.push(centroid);
|
|
216
|
+
}
|
|
217
|
+
this.codebooks.push(codebook);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
encode(vector: number[]): Uint8Array {
|
|
222
|
+
const codes = new Uint8Array(this.numSubvectors);
|
|
223
|
+
|
|
224
|
+
for (let m = 0; m < this.numSubvectors; m++) {
|
|
225
|
+
const start = m * this.subvectorDim;
|
|
226
|
+
const end = Math.min(start + this.subvectorDim, vector.length);
|
|
227
|
+
const subvector = vector.slice(start, end);
|
|
228
|
+
|
|
229
|
+
// Pad if necessary
|
|
230
|
+
while (subvector.length < this.subvectorDim) {
|
|
231
|
+
subvector.push(0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Find nearest centroid
|
|
235
|
+
let minDist = Infinity;
|
|
236
|
+
let minIdx = 0;
|
|
237
|
+
|
|
238
|
+
for (let c = 0; c < this.numCentroids; c++) {
|
|
239
|
+
const dist = this.euclideanDistance(subvector, this.codebooks[m][c]);
|
|
240
|
+
if (dist < minDist) {
|
|
241
|
+
minDist = dist;
|
|
242
|
+
minIdx = c;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
codes[m] = minIdx;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return codes;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
decode(codes: Uint8Array): number[] {
|
|
253
|
+
const result: number[] = [];
|
|
254
|
+
|
|
255
|
+
for (let m = 0; m < this.numSubvectors; m++) {
|
|
256
|
+
const centroid = this.codebooks[m][codes[m]];
|
|
257
|
+
result.push(...centroid);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return result.slice(0, this.numSubvectors * this.subvectorDim);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private euclideanDistance(a: number[], b: number[]): number {
|
|
264
|
+
let sum = 0;
|
|
265
|
+
for (let i = 0; i < a.length; i++) {
|
|
266
|
+
const diff = a[i] - b[i];
|
|
267
|
+
sum += diff * diff;
|
|
268
|
+
}
|
|
269
|
+
return Math.sqrt(sum);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============================================================================
|
|
274
|
+
// Evaluation Functions
|
|
275
|
+
// ============================================================================
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Compute cosine similarity.
|
|
279
|
+
*/
|
|
280
|
+
function cosineSimilarity(a: number[], b: number[]): number {
|
|
281
|
+
let dot = 0, magA = 0, magB = 0;
|
|
282
|
+
for (let i = 0; i < a.length; i++) {
|
|
283
|
+
dot += a[i] * b[i];
|
|
284
|
+
magA += a[i] * a[i];
|
|
285
|
+
magB += b[i] * b[i];
|
|
286
|
+
}
|
|
287
|
+
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Generate random normalized vectors.
|
|
292
|
+
*/
|
|
293
|
+
function generateVectors(count: number, dim: number): number[][] {
|
|
294
|
+
const vectors: number[][] = [];
|
|
295
|
+
for (let i = 0; i < count; i++) {
|
|
296
|
+
const vec = Array.from({ length: dim }, () => Math.random() * 2 - 1);
|
|
297
|
+
const mag = Math.sqrt(vec.reduce((s, v) => s + v * v, 0));
|
|
298
|
+
vectors.push(vec.map(v => v / mag));
|
|
299
|
+
}
|
|
300
|
+
return vectors;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Find ground truth top-k by exact search.
|
|
305
|
+
*/
|
|
306
|
+
function exactTopK(query: number[], vectors: number[][], k: number): number[] {
|
|
307
|
+
const distances = vectors.map((v, i) => ({
|
|
308
|
+
index: i,
|
|
309
|
+
similarity: cosineSimilarity(query, v),
|
|
310
|
+
}));
|
|
311
|
+
distances.sort((a, b) => b.similarity - a.similarity);
|
|
312
|
+
return distances.slice(0, k).map(d => d.index);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Calculate recall@k.
|
|
317
|
+
*/
|
|
318
|
+
function calculateRecall(groundTruth: number[], predicted: number[]): number {
|
|
319
|
+
const gtSet = new Set(groundTruth);
|
|
320
|
+
const overlap = predicted.filter(p => gtSet.has(p)).length;
|
|
321
|
+
return overlap / groundTruth.length;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ============================================================================
|
|
325
|
+
// Main Example
|
|
326
|
+
// ============================================================================
|
|
327
|
+
|
|
328
|
+
async function main(): Promise<void> {
|
|
329
|
+
console.log('RuVector PostgreSQL Bridge - Quantization Example');
|
|
330
|
+
console.log('===================================================\n');
|
|
331
|
+
|
|
332
|
+
const bridge: RuVectorBridge = createRuVectorBridge({
|
|
333
|
+
connectionString: `postgresql://${config.connection.user}:${config.connection.password}@${config.connection.host}:${config.connection.port}/${config.connection.database}`,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
await bridge.connect();
|
|
338
|
+
console.log('Connected to PostgreSQL\n');
|
|
339
|
+
|
|
340
|
+
// ========================================================================
|
|
341
|
+
// 1. Generate Test Data
|
|
342
|
+
// ========================================================================
|
|
343
|
+
console.log('1. Generating test data...');
|
|
344
|
+
console.log(' ' + '-'.repeat(50));
|
|
345
|
+
|
|
346
|
+
const vectors = generateVectors(config.testVectors, config.dimensions);
|
|
347
|
+
const queries = generateVectors(config.queryVectors, config.dimensions);
|
|
348
|
+
|
|
349
|
+
console.log(` Generated ${config.testVectors.toLocaleString()} test vectors`);
|
|
350
|
+
console.log(` Generated ${config.queryVectors} query vectors`);
|
|
351
|
+
console.log(` Dimensions: ${config.dimensions}`);
|
|
352
|
+
console.log();
|
|
353
|
+
|
|
354
|
+
// ========================================================================
|
|
355
|
+
// 2. Compute Ground Truth
|
|
356
|
+
// ========================================================================
|
|
357
|
+
console.log('2. Computing ground truth (exact search)...');
|
|
358
|
+
console.log(' ' + '-'.repeat(50));
|
|
359
|
+
|
|
360
|
+
const startGT = performance.now();
|
|
361
|
+
const groundTruths = queries.map(q => exactTopK(q, vectors, config.k));
|
|
362
|
+
const gtTime = performance.now() - startGT;
|
|
363
|
+
|
|
364
|
+
console.log(` Ground truth computed in ${gtTime.toFixed(2)}ms`);
|
|
365
|
+
console.log(` Average time per query: ${(gtTime / config.queryVectors).toFixed(2)}ms`);
|
|
366
|
+
console.log();
|
|
367
|
+
|
|
368
|
+
// ========================================================================
|
|
369
|
+
// 3. Compare Quantization Methods
|
|
370
|
+
// ========================================================================
|
|
371
|
+
console.log('3. Comparing Quantization Methods');
|
|
372
|
+
console.log(' ' + '-'.repeat(70));
|
|
373
|
+
console.log(' Method | Compression | Recall@10 | Query Time | Mem/Vector');
|
|
374
|
+
console.log(' ' + '-'.repeat(70));
|
|
375
|
+
|
|
376
|
+
const results: Array<{
|
|
377
|
+
method: string;
|
|
378
|
+
compression: number;
|
|
379
|
+
recall: number;
|
|
380
|
+
queryTimeMs: number;
|
|
381
|
+
bytesPerVector: number;
|
|
382
|
+
}> = [];
|
|
383
|
+
|
|
384
|
+
// Test each quantization method
|
|
385
|
+
for (const qConfig of quantizationMethods) {
|
|
386
|
+
let quantizedVectors: any[];
|
|
387
|
+
let queryFn: (query: number[], vectors: any[], k: number) => number[];
|
|
388
|
+
let bytesPerVector: number;
|
|
389
|
+
|
|
390
|
+
switch (qConfig.method) {
|
|
391
|
+
case 'none':
|
|
392
|
+
quantizedVectors = vectors;
|
|
393
|
+
queryFn = (q, vecs, k) => exactTopK(q, vecs, k);
|
|
394
|
+
bytesPerVector = config.dimensions * 4;
|
|
395
|
+
break;
|
|
396
|
+
|
|
397
|
+
case 'int8':
|
|
398
|
+
quantizedVectors = vectors.map(v => ({
|
|
399
|
+
original: v,
|
|
400
|
+
...quantizeInt8(v),
|
|
401
|
+
}));
|
|
402
|
+
queryFn = (q, vecs, k) => {
|
|
403
|
+
const queryQ = quantizeInt8(q);
|
|
404
|
+
const distances = vecs.map((v: any, i: number) => ({
|
|
405
|
+
index: i,
|
|
406
|
+
similarity: cosineSimilarity(
|
|
407
|
+
dequantizeInt8({ quantized: v.quantized, scale: v.scale, offset: v.offset }),
|
|
408
|
+
dequantizeInt8(queryQ)
|
|
409
|
+
),
|
|
410
|
+
}));
|
|
411
|
+
distances.sort((a: any, b: any) => b.similarity - a.similarity);
|
|
412
|
+
return distances.slice(0, k).map((d: any) => d.index);
|
|
413
|
+
};
|
|
414
|
+
bytesPerVector = config.dimensions * 1 + 8; // quantized + scale + offset
|
|
415
|
+
break;
|
|
416
|
+
|
|
417
|
+
case 'int4':
|
|
418
|
+
quantizedVectors = vectors.map(v => ({
|
|
419
|
+
original: v,
|
|
420
|
+
...quantizeInt4(v),
|
|
421
|
+
originalLength: v.length,
|
|
422
|
+
}));
|
|
423
|
+
queryFn = (q, vecs, k) => {
|
|
424
|
+
const queryQ = quantizeInt4(q);
|
|
425
|
+
const distances = vecs.map((v: any, i: number) => ({
|
|
426
|
+
index: i,
|
|
427
|
+
similarity: cosineSimilarity(
|
|
428
|
+
dequantizeInt4(
|
|
429
|
+
{ quantized: v.quantized, scale: v.scale, offset: v.offset },
|
|
430
|
+
v.originalLength
|
|
431
|
+
),
|
|
432
|
+
dequantizeInt4(queryQ, q.length)
|
|
433
|
+
),
|
|
434
|
+
}));
|
|
435
|
+
distances.sort((a: any, b: any) => b.similarity - a.similarity);
|
|
436
|
+
return distances.slice(0, k).map((d: any) => d.index);
|
|
437
|
+
};
|
|
438
|
+
bytesPerVector = Math.ceil(config.dimensions / 2) + 8;
|
|
439
|
+
break;
|
|
440
|
+
|
|
441
|
+
case 'binary':
|
|
442
|
+
quantizedVectors = vectors.map(v => ({
|
|
443
|
+
original: v,
|
|
444
|
+
binary: quantizeBinary(v),
|
|
445
|
+
}));
|
|
446
|
+
queryFn = (q, vecs, k) => {
|
|
447
|
+
const queryB = quantizeBinary(q);
|
|
448
|
+
const distances = vecs.map((v: any, i: number) => ({
|
|
449
|
+
index: i,
|
|
450
|
+
// Lower Hamming distance = more similar
|
|
451
|
+
distance: hammingDistance(v.binary, queryB),
|
|
452
|
+
}));
|
|
453
|
+
distances.sort((a: any, b: any) => a.distance - b.distance);
|
|
454
|
+
return distances.slice(0, k).map((d: any) => d.index);
|
|
455
|
+
};
|
|
456
|
+
bytesPerVector = Math.ceil(config.dimensions / 8);
|
|
457
|
+
break;
|
|
458
|
+
|
|
459
|
+
case 'pq':
|
|
460
|
+
const pq = new ProductQuantizer(config.dimensions, 8);
|
|
461
|
+
quantizedVectors = vectors.map(v => ({
|
|
462
|
+
original: v,
|
|
463
|
+
codes: pq.encode(v),
|
|
464
|
+
pq,
|
|
465
|
+
}));
|
|
466
|
+
queryFn = (q, vecs, k) => {
|
|
467
|
+
const distances = vecs.map((v: any, i: number) => ({
|
|
468
|
+
index: i,
|
|
469
|
+
similarity: cosineSimilarity(v.pq.decode(v.codes), q),
|
|
470
|
+
}));
|
|
471
|
+
distances.sort((a: any, b: any) => b.similarity - a.similarity);
|
|
472
|
+
return distances.slice(0, k).map((d: any) => d.index);
|
|
473
|
+
};
|
|
474
|
+
bytesPerVector = 8; // 8 subvectors, 1 byte each
|
|
475
|
+
break;
|
|
476
|
+
|
|
477
|
+
default:
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Measure recall and query time
|
|
482
|
+
const recalls: number[] = [];
|
|
483
|
+
const startQuery = performance.now();
|
|
484
|
+
|
|
485
|
+
for (let i = 0; i < queries.length; i++) {
|
|
486
|
+
const predicted = queryFn(queries[i], quantizedVectors, config.k);
|
|
487
|
+
recalls.push(calculateRecall(groundTruths[i], predicted));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const queryTime = (performance.now() - startQuery) / queries.length;
|
|
491
|
+
const avgRecall = recalls.reduce((a, b) => a + b, 0) / recalls.length;
|
|
492
|
+
const compression = (config.dimensions * 4) / bytesPerVector;
|
|
493
|
+
|
|
494
|
+
results.push({
|
|
495
|
+
method: qConfig.name,
|
|
496
|
+
compression,
|
|
497
|
+
recall: avgRecall,
|
|
498
|
+
queryTimeMs: queryTime,
|
|
499
|
+
bytesPerVector,
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
console.log(
|
|
503
|
+
` ${qConfig.name.padEnd(30)} | ` +
|
|
504
|
+
`${compression.toFixed(1).padStart(6)}x | ` +
|
|
505
|
+
`${(avgRecall * 100).toFixed(1).padStart(6)}% | ` +
|
|
506
|
+
`${queryTime.toFixed(2).padStart(8)}ms | ` +
|
|
507
|
+
`${bytesPerVector.toString().padStart(5)} B`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
console.log();
|
|
511
|
+
|
|
512
|
+
// ========================================================================
|
|
513
|
+
// 4. Memory Savings Analysis
|
|
514
|
+
// ========================================================================
|
|
515
|
+
console.log('4. Memory Savings Analysis');
|
|
516
|
+
console.log(' ' + '-'.repeat(50));
|
|
517
|
+
|
|
518
|
+
const baseMemory = config.testVectors * config.dimensions * 4 / (1024 * 1024);
|
|
519
|
+
console.log(` Base memory (Float32): ${baseMemory.toFixed(2)} MB`);
|
|
520
|
+
|
|
521
|
+
console.log('\n Memory usage by method:');
|
|
522
|
+
results.forEach(r => {
|
|
523
|
+
const memory = config.testVectors * r.bytesPerVector / (1024 * 1024);
|
|
524
|
+
const savings = ((baseMemory - memory) / baseMemory * 100);
|
|
525
|
+
console.log(
|
|
526
|
+
` ${r.method.padEnd(30)}: ${memory.toFixed(2).padStart(6)} MB ` +
|
|
527
|
+
`(${savings.toFixed(1)}% reduction)`
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
console.log();
|
|
531
|
+
|
|
532
|
+
// ========================================================================
|
|
533
|
+
// 5. Recall vs Compression Trade-off
|
|
534
|
+
// ========================================================================
|
|
535
|
+
console.log('5. Recall vs Compression Trade-off');
|
|
536
|
+
console.log(' ' + '-'.repeat(50));
|
|
537
|
+
|
|
538
|
+
console.log(' Visual representation (Compression -> Recall):');
|
|
539
|
+
console.log();
|
|
540
|
+
|
|
541
|
+
results.forEach(r => {
|
|
542
|
+
const compressionBar = '='.repeat(Math.floor(r.compression * 2));
|
|
543
|
+
const recallBar = '*'.repeat(Math.floor(r.recall * 50));
|
|
544
|
+
console.log(` ${r.method.slice(0, 20).padEnd(20)}`);
|
|
545
|
+
console.log(` Compression: ${compressionBar} ${r.compression.toFixed(1)}x`);
|
|
546
|
+
console.log(` Recall: ${recallBar} ${(r.recall * 100).toFixed(1)}%`);
|
|
547
|
+
console.log();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// ========================================================================
|
|
551
|
+
// 6. Production Recommendations
|
|
552
|
+
// ========================================================================
|
|
553
|
+
console.log('6. Production Recommendations');
|
|
554
|
+
console.log(' ' + '-'.repeat(50));
|
|
555
|
+
|
|
556
|
+
console.log('\n Use Case Recommendations:');
|
|
557
|
+
|
|
558
|
+
console.log('\n High Accuracy (recall > 99%):');
|
|
559
|
+
console.log(' - Method: Int8 Scalar Quantization');
|
|
560
|
+
console.log(' - Compression: 4x');
|
|
561
|
+
console.log(' - Best for: RAG, semantic search, recommendations');
|
|
562
|
+
|
|
563
|
+
console.log('\n Balanced (recall > 95%):');
|
|
564
|
+
console.log(' - Method: Int4 Scalar Quantization');
|
|
565
|
+
console.log(' - Compression: 8x');
|
|
566
|
+
console.log(' - Best for: Large-scale similarity search');
|
|
567
|
+
|
|
568
|
+
console.log('\n Maximum Compression (recall > 85%):');
|
|
569
|
+
console.log(' - Method: Binary Quantization');
|
|
570
|
+
console.log(' - Compression: 32x');
|
|
571
|
+
console.log(' - Best for: Candidate generation, first-pass filtering');
|
|
572
|
+
|
|
573
|
+
console.log('\n High-Dimensional Data:');
|
|
574
|
+
console.log(' - Method: Product Quantization (PQ)');
|
|
575
|
+
console.log(' - Compression: Variable (8-64x typical)');
|
|
576
|
+
console.log(' - Best for: Embeddings > 512 dimensions');
|
|
577
|
+
|
|
578
|
+
// ========================================================================
|
|
579
|
+
// 7. PostgreSQL Integration Notes
|
|
580
|
+
// ========================================================================
|
|
581
|
+
console.log('\n7. PostgreSQL Integration Notes');
|
|
582
|
+
console.log(' ' + '-'.repeat(50));
|
|
583
|
+
|
|
584
|
+
console.log('\n pgvector supports:');
|
|
585
|
+
console.log(' - halfvec (Float16): 2x compression, ~99.9% recall');
|
|
586
|
+
console.log(' - sparsevec: For sparse vectors');
|
|
587
|
+
console.log(' - HNSW with quantization: Index-level compression');
|
|
588
|
+
|
|
589
|
+
console.log('\n Example SQL for halfvec:');
|
|
590
|
+
console.log(' CREATE TABLE items (');
|
|
591
|
+
console.log(' id bigserial PRIMARY KEY,');
|
|
592
|
+
console.log(' embedding halfvec(768) -- Float16 storage');
|
|
593
|
+
console.log(' );');
|
|
594
|
+
|
|
595
|
+
console.log('\n Example SQL for quantized index:');
|
|
596
|
+
console.log(' CREATE INDEX ON items USING hnsw (');
|
|
597
|
+
console.log(' (embedding::halfvec(768)) halfvec_l2_ops');
|
|
598
|
+
console.log(' );');
|
|
599
|
+
|
|
600
|
+
// ========================================================================
|
|
601
|
+
// 8. Store Quantized Vectors (Demo)
|
|
602
|
+
// ========================================================================
|
|
603
|
+
console.log('\n8. Storing Vectors with Different Precisions');
|
|
604
|
+
console.log(' ' + '-'.repeat(50));
|
|
605
|
+
|
|
606
|
+
// Create collections for different precisions
|
|
607
|
+
const collections = ['vectors_float32', 'vectors_int8_sim'];
|
|
608
|
+
|
|
609
|
+
for (const collection of collections) {
|
|
610
|
+
await bridge.createCollection(collection, {
|
|
611
|
+
dimensions: config.dimensions,
|
|
612
|
+
distanceMetric: 'cosine',
|
|
613
|
+
indexType: 'hnsw',
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Insert sample vectors
|
|
618
|
+
const sampleSize = 1000;
|
|
619
|
+
console.log(`\n Inserting ${sampleSize} vectors to each collection...`);
|
|
620
|
+
|
|
621
|
+
// Float32 (original)
|
|
622
|
+
const float32Start = performance.now();
|
|
623
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
624
|
+
await bridge.insert('vectors_float32', {
|
|
625
|
+
id: `float32_${i}`,
|
|
626
|
+
embedding: vectors[i],
|
|
627
|
+
metadata: { precision: 'float32' },
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const float32Time = performance.now() - float32Start;
|
|
631
|
+
|
|
632
|
+
// Simulated Int8 (stored as float but simulating quantization overhead)
|
|
633
|
+
const int8Start = performance.now();
|
|
634
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
635
|
+
const q = quantizeInt8(vectors[i]);
|
|
636
|
+
const dequantized = dequantizeInt8(q);
|
|
637
|
+
await bridge.insert('vectors_int8_sim', {
|
|
638
|
+
id: `int8_${i}`,
|
|
639
|
+
embedding: dequantized,
|
|
640
|
+
metadata: { precision: 'int8_simulated', scale: q.scale, offset: q.offset },
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
const int8Time = performance.now() - int8Start;
|
|
644
|
+
|
|
645
|
+
console.log(` Float32 insert time: ${float32Time.toFixed(2)}ms`);
|
|
646
|
+
console.log(` Int8 (simulated) insert time: ${int8Time.toFixed(2)}ms`);
|
|
647
|
+
|
|
648
|
+
// Compare search results
|
|
649
|
+
const testQuery = queries[0];
|
|
650
|
+
const float32Results = await bridge.search('vectors_float32', testQuery, {
|
|
651
|
+
k: 10,
|
|
652
|
+
includeDistance: true,
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
const int8Results = await bridge.search('vectors_int8_sim', testQuery, {
|
|
656
|
+
k: 10,
|
|
657
|
+
includeDistance: true,
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
console.log('\n Search result comparison (first 5):');
|
|
661
|
+
console.log(' Float32 IDs: ' + float32Results.slice(0, 5).map(r => r.id).join(', '));
|
|
662
|
+
console.log(' Int8 (sim) IDs: ' + int8Results.slice(0, 5).map(r => r.id.replace('int8', 'float32')).join(', '));
|
|
663
|
+
|
|
664
|
+
// ========================================================================
|
|
665
|
+
// Done
|
|
666
|
+
// ========================================================================
|
|
667
|
+
console.log('\n' + '='.repeat(55));
|
|
668
|
+
console.log('Quantization example completed!');
|
|
669
|
+
console.log('='.repeat(55));
|
|
670
|
+
|
|
671
|
+
} catch (error) {
|
|
672
|
+
console.error('Error:', error);
|
|
673
|
+
throw error;
|
|
674
|
+
} finally {
|
|
675
|
+
await bridge.disconnect();
|
|
676
|
+
console.log('\nDisconnected from PostgreSQL.');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
main().catch(console.error);
|