@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,860 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuVector Test Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for RuVector integration tests including:
|
|
5
|
+
* - Random vector generation
|
|
6
|
+
* - Mock data factories
|
|
7
|
+
* - Test database helpers
|
|
8
|
+
* - Performance measurement utilities
|
|
9
|
+
*
|
|
10
|
+
* @module @sparkleideas/plugins/__tests__/utils/ruvector-test-utils
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { vi, type Mock } from 'vitest';
|
|
14
|
+
import type {
|
|
15
|
+
RuVectorConfig,
|
|
16
|
+
RuVectorClientOptions,
|
|
17
|
+
VectorSearchOptions,
|
|
18
|
+
VectorSearchResult,
|
|
19
|
+
VectorInsertOptions,
|
|
20
|
+
VectorUpdateOptions,
|
|
21
|
+
VectorIndexOptions,
|
|
22
|
+
BatchVectorOptions,
|
|
23
|
+
GraphData,
|
|
24
|
+
GNNLayer,
|
|
25
|
+
AttentionConfig,
|
|
26
|
+
AttentionInput,
|
|
27
|
+
HyperbolicEmbedding,
|
|
28
|
+
HyperbolicInput,
|
|
29
|
+
DistanceMetric,
|
|
30
|
+
VectorIndexType,
|
|
31
|
+
IndexStats,
|
|
32
|
+
QueryResult,
|
|
33
|
+
BatchResult,
|
|
34
|
+
ConnectionResult,
|
|
35
|
+
HealthStatus,
|
|
36
|
+
RuVectorStats,
|
|
37
|
+
AnalysisResult,
|
|
38
|
+
MigrationResult,
|
|
39
|
+
} from '../../src/integrations/ruvector/types.js';
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Environment Detection
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if real database tests should be run
|
|
47
|
+
*/
|
|
48
|
+
export function useRealDatabase(): boolean {
|
|
49
|
+
return process.env.RUVECTOR_TEST_DB === 'true';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get test database configuration from environment
|
|
54
|
+
*/
|
|
55
|
+
export function getTestDatabaseConfig(): RuVectorConfig {
|
|
56
|
+
return {
|
|
57
|
+
host: process.env.RUVECTOR_TEST_HOST ?? 'localhost',
|
|
58
|
+
port: parseInt(process.env.RUVECTOR_TEST_PORT ?? '5432', 10),
|
|
59
|
+
database: process.env.RUVECTOR_TEST_DATABASE ?? 'ruvector_test',
|
|
60
|
+
user: process.env.RUVECTOR_TEST_USER ?? 'postgres',
|
|
61
|
+
password: process.env.RUVECTOR_TEST_PASSWORD ?? 'postgres',
|
|
62
|
+
poolSize: 5,
|
|
63
|
+
connectionTimeoutMs: 5000,
|
|
64
|
+
queryTimeoutMs: 30000,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Vector Generation Utilities
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate a random vector with specified dimensions
|
|
74
|
+
*/
|
|
75
|
+
export function randomVector(dimensions: number = 384): number[] {
|
|
76
|
+
return Array.from({ length: dimensions }, () => Math.random() * 2 - 1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generate a normalized random vector (unit length)
|
|
81
|
+
*/
|
|
82
|
+
export function normalizedVector(dimensions: number = 384): number[] {
|
|
83
|
+
const vec = randomVector(dimensions);
|
|
84
|
+
const magnitude = Math.sqrt(vec.reduce((sum, v) => sum + v * v, 0));
|
|
85
|
+
return vec.map(v => v / magnitude);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generate a vector within Poincare ball (norm < 1)
|
|
90
|
+
*/
|
|
91
|
+
export function poincareVector(dimensions: number = 32): number[] {
|
|
92
|
+
const vec = randomVector(dimensions);
|
|
93
|
+
const magnitude = Math.sqrt(vec.reduce((sum, v) => sum + v * v, 0));
|
|
94
|
+
const scale = Math.random() * 0.95 / Math.max(magnitude, 0.001);
|
|
95
|
+
return vec.map(v => v * scale);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generate multiple random vectors
|
|
100
|
+
*/
|
|
101
|
+
export function randomVectors(count: number, dimensions: number = 384): number[][] {
|
|
102
|
+
return Array.from({ length: count }, () => randomVector(dimensions));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generate vectors with known similarities for testing search accuracy
|
|
107
|
+
*/
|
|
108
|
+
export function generateSimilarVectors(
|
|
109
|
+
base: number[],
|
|
110
|
+
count: number,
|
|
111
|
+
noise: number = 0.1
|
|
112
|
+
): number[][] {
|
|
113
|
+
return Array.from({ length: count }, () =>
|
|
114
|
+
base.map(v => v + (Math.random() - 0.5) * noise * 2)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Generate orthogonal vectors for testing
|
|
120
|
+
*/
|
|
121
|
+
export function orthogonalVectors(dimensions: number, count: number): number[][] {
|
|
122
|
+
// Simple Gram-Schmidt orthogonalization
|
|
123
|
+
const vectors: number[][] = [];
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < count; i++) {
|
|
126
|
+
let v = randomVector(dimensions);
|
|
127
|
+
|
|
128
|
+
// Subtract projections onto previous vectors
|
|
129
|
+
for (const u of vectors) {
|
|
130
|
+
const dot = v.reduce((sum, val, idx) => sum + val * u[idx], 0);
|
|
131
|
+
v = v.map((val, idx) => val - dot * u[idx]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Normalize
|
|
135
|
+
const mag = Math.sqrt(v.reduce((sum, val) => sum + val * val, 0));
|
|
136
|
+
if (mag > 0.001) {
|
|
137
|
+
vectors.push(v.map(val => val / mag));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return vectors;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Calculate cosine similarity between two vectors
|
|
146
|
+
*/
|
|
147
|
+
export function cosineSimilarity(a: number[], b: number[]): number {
|
|
148
|
+
const dot = a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
149
|
+
const magA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0));
|
|
150
|
+
const magB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0));
|
|
151
|
+
return dot / (magA * magB);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Calculate Euclidean distance between two vectors
|
|
156
|
+
*/
|
|
157
|
+
export function euclideanDistance(a: number[], b: number[]): number {
|
|
158
|
+
return Math.sqrt(a.reduce((sum, v, i) => sum + (v - b[i]) ** 2, 0));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Calculate Poincare distance in hyperbolic space
|
|
163
|
+
*/
|
|
164
|
+
export function poincareDistance(a: number[], b: number[]): number {
|
|
165
|
+
const normA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0));
|
|
166
|
+
const normB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0));
|
|
167
|
+
const diffNorm = Math.sqrt(a.reduce((sum, v, i) => sum + (v - b[i]) ** 2, 0));
|
|
168
|
+
|
|
169
|
+
const numerator = 2 * diffNorm ** 2;
|
|
170
|
+
const denominator = (1 - normA ** 2) * (1 - normB ** 2);
|
|
171
|
+
|
|
172
|
+
return Math.acosh(1 + numerator / Math.max(denominator, 1e-10));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Mock Data Factories
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create a test configuration with optional overrides
|
|
181
|
+
*/
|
|
182
|
+
export function createTestConfig(overrides: Partial<RuVectorConfig> = {}): RuVectorConfig {
|
|
183
|
+
return {
|
|
184
|
+
host: 'localhost',
|
|
185
|
+
port: 5432,
|
|
186
|
+
database: 'test_db',
|
|
187
|
+
user: 'test_user',
|
|
188
|
+
password: 'test_password',
|
|
189
|
+
poolSize: 10,
|
|
190
|
+
connectionTimeoutMs: 5000,
|
|
191
|
+
queryTimeoutMs: 30000,
|
|
192
|
+
schema: 'public',
|
|
193
|
+
...overrides,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Create test client options
|
|
199
|
+
*/
|
|
200
|
+
export function createTestClientOptions(
|
|
201
|
+
overrides: Partial<RuVectorClientOptions> = {}
|
|
202
|
+
): RuVectorClientOptions {
|
|
203
|
+
return {
|
|
204
|
+
...createTestConfig(),
|
|
205
|
+
autoReconnect: true,
|
|
206
|
+
maxReconnectAttempts: 3,
|
|
207
|
+
...overrides,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Create mock search results
|
|
213
|
+
*/
|
|
214
|
+
export function createMockSearchResults(
|
|
215
|
+
count: number,
|
|
216
|
+
options: { includeVector?: boolean; includeMetadata?: boolean; dimensions?: number } = {}
|
|
217
|
+
): VectorSearchResult[] {
|
|
218
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
219
|
+
id: `result-${i}`,
|
|
220
|
+
score: 1 - i * (1 / count),
|
|
221
|
+
distance: i * (1 / count),
|
|
222
|
+
rank: i + 1,
|
|
223
|
+
retrievedAt: new Date(),
|
|
224
|
+
...(options.includeVector && { vector: randomVector(options.dimensions ?? 384) }),
|
|
225
|
+
...(options.includeMetadata && { metadata: { index: i, label: `item-${i}` } }),
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create mock connection result
|
|
231
|
+
*/
|
|
232
|
+
export function createMockConnectionResult(): ConnectionResult {
|
|
233
|
+
return {
|
|
234
|
+
connectionId: `conn-${Date.now()}`,
|
|
235
|
+
ready: true,
|
|
236
|
+
serverVersion: 'PostgreSQL 15.0',
|
|
237
|
+
ruVectorVersion: '1.0.0',
|
|
238
|
+
parameters: {
|
|
239
|
+
server_encoding: 'UTF8',
|
|
240
|
+
client_encoding: 'UTF8',
|
|
241
|
+
server_version: '15.0',
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Create mock index stats
|
|
248
|
+
*/
|
|
249
|
+
export function createMockIndexStats(
|
|
250
|
+
indexName: string,
|
|
251
|
+
indexType: VectorIndexType = 'hnsw'
|
|
252
|
+
): IndexStats {
|
|
253
|
+
return {
|
|
254
|
+
indexName,
|
|
255
|
+
indexType,
|
|
256
|
+
numVectors: 10000 + Math.floor(Math.random() * 90000),
|
|
257
|
+
sizeBytes: 1024 * 1024 * (50 + Math.floor(Math.random() * 200)),
|
|
258
|
+
buildTimeMs: 5000 + Math.floor(Math.random() * 10000),
|
|
259
|
+
lastRebuild: new Date(),
|
|
260
|
+
params: {
|
|
261
|
+
m: 16,
|
|
262
|
+
efConstruction: 200,
|
|
263
|
+
ef_search: 100,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Create mock health status
|
|
270
|
+
*/
|
|
271
|
+
export function createMockHealthStatus(healthy: boolean = true): HealthStatus {
|
|
272
|
+
return {
|
|
273
|
+
status: healthy ? 'healthy' : 'unhealthy',
|
|
274
|
+
components: {
|
|
275
|
+
database: {
|
|
276
|
+
name: 'PostgreSQL',
|
|
277
|
+
healthy,
|
|
278
|
+
latencyMs: healthy ? 5 : undefined,
|
|
279
|
+
error: healthy ? undefined : 'Connection failed',
|
|
280
|
+
},
|
|
281
|
+
ruvector: {
|
|
282
|
+
name: 'RuVector Extension',
|
|
283
|
+
healthy,
|
|
284
|
+
latencyMs: healthy ? 1 : undefined,
|
|
285
|
+
},
|
|
286
|
+
pool: {
|
|
287
|
+
name: 'Connection Pool',
|
|
288
|
+
healthy: true,
|
|
289
|
+
latencyMs: 0,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
lastCheck: new Date(),
|
|
293
|
+
issues: healthy ? [] : ['Database connection failed'],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create mock stats
|
|
299
|
+
*/
|
|
300
|
+
export function createMockStats(): RuVectorStats {
|
|
301
|
+
return {
|
|
302
|
+
version: '1.0.0',
|
|
303
|
+
totalVectors: 100000,
|
|
304
|
+
totalSizeBytes: 1024 * 1024 * 500,
|
|
305
|
+
numIndices: 3,
|
|
306
|
+
numTables: 5,
|
|
307
|
+
queryStats: {
|
|
308
|
+
totalQueries: 50000,
|
|
309
|
+
avgQueryTimeMs: 15,
|
|
310
|
+
p95QueryTimeMs: 50,
|
|
311
|
+
p99QueryTimeMs: 100,
|
|
312
|
+
cacheHitRate: 0.85,
|
|
313
|
+
},
|
|
314
|
+
memoryStats: {
|
|
315
|
+
usedBytes: 1024 * 1024 * 256,
|
|
316
|
+
peakBytes: 1024 * 1024 * 512,
|
|
317
|
+
indexBytes: 1024 * 1024 * 150,
|
|
318
|
+
cacheBytes: 1024 * 1024 * 50,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Create mock analysis result
|
|
325
|
+
*/
|
|
326
|
+
export function createMockAnalysisResult(tableName: string = 'vectors'): AnalysisResult {
|
|
327
|
+
return {
|
|
328
|
+
tableName,
|
|
329
|
+
numRows: 10000,
|
|
330
|
+
columnStats: [
|
|
331
|
+
{
|
|
332
|
+
columnName: 'id',
|
|
333
|
+
dataType: 'uuid',
|
|
334
|
+
nullPercent: 0,
|
|
335
|
+
distinctCount: 10000,
|
|
336
|
+
avgSizeBytes: 16,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
columnName: 'embedding',
|
|
340
|
+
dataType: 'vector(384)',
|
|
341
|
+
nullPercent: 0,
|
|
342
|
+
distinctCount: 10000,
|
|
343
|
+
avgSizeBytes: 1536,
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
columnName: 'metadata',
|
|
347
|
+
dataType: 'jsonb',
|
|
348
|
+
nullPercent: 5,
|
|
349
|
+
distinctCount: 9500,
|
|
350
|
+
avgSizeBytes: 256,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
recommendations: [
|
|
354
|
+
'Consider adding an HNSW index for faster similarity search',
|
|
355
|
+
'Metadata column could benefit from a GIN index',
|
|
356
|
+
],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Create mock migration result
|
|
362
|
+
*/
|
|
363
|
+
export function createMockMigrationResult(
|
|
364
|
+
name: string,
|
|
365
|
+
direction: 'up' | 'down' = 'up',
|
|
366
|
+
success: boolean = true
|
|
367
|
+
): MigrationResult {
|
|
368
|
+
return {
|
|
369
|
+
name,
|
|
370
|
+
success,
|
|
371
|
+
direction,
|
|
372
|
+
durationMs: 500 + Math.floor(Math.random() * 2000),
|
|
373
|
+
affectedTables: ['vectors', 'vector_indices'],
|
|
374
|
+
error: success ? undefined : 'Migration failed: table already exists',
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ============================================================================
|
|
379
|
+
// Graph Data Factories
|
|
380
|
+
// ============================================================================
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Create a random graph for GNN testing
|
|
384
|
+
*/
|
|
385
|
+
export function createRandomGraph(
|
|
386
|
+
numNodes: number,
|
|
387
|
+
numEdges: number,
|
|
388
|
+
featureDim: number
|
|
389
|
+
): GraphData {
|
|
390
|
+
const nodeFeatures = randomVectors(numNodes, featureDim);
|
|
391
|
+
const edges: [number[], number[]] = [[], []];
|
|
392
|
+
|
|
393
|
+
for (let i = 0; i < numEdges; i++) {
|
|
394
|
+
const source = Math.floor(Math.random() * numNodes);
|
|
395
|
+
const target = Math.floor(Math.random() * numNodes);
|
|
396
|
+
edges[0].push(source);
|
|
397
|
+
edges[1].push(target);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
nodeFeatures,
|
|
402
|
+
edgeIndex: edges,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Create a complete graph (all nodes connected)
|
|
408
|
+
*/
|
|
409
|
+
export function createCompleteGraph(numNodes: number, featureDim: number): GraphData {
|
|
410
|
+
const nodeFeatures = randomVectors(numNodes, featureDim);
|
|
411
|
+
const edges: [number[], number[]] = [[], []];
|
|
412
|
+
|
|
413
|
+
for (let i = 0; i < numNodes; i++) {
|
|
414
|
+
for (let j = 0; j < numNodes; j++) {
|
|
415
|
+
if (i !== j) {
|
|
416
|
+
edges[0].push(i);
|
|
417
|
+
edges[1].push(j);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
nodeFeatures,
|
|
424
|
+
edgeIndex: edges,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Create a chain graph (linear sequence)
|
|
430
|
+
*/
|
|
431
|
+
export function createChainGraph(numNodes: number, featureDim: number): GraphData {
|
|
432
|
+
const nodeFeatures = randomVectors(numNodes, featureDim);
|
|
433
|
+
const edges: [number[], number[]] = [[], []];
|
|
434
|
+
|
|
435
|
+
for (let i = 0; i < numNodes - 1; i++) {
|
|
436
|
+
edges[0].push(i);
|
|
437
|
+
edges[1].push(i + 1);
|
|
438
|
+
// Bidirectional
|
|
439
|
+
edges[0].push(i + 1);
|
|
440
|
+
edges[1].push(i);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
nodeFeatures,
|
|
445
|
+
edgeIndex: edges,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ============================================================================
|
|
450
|
+
// Mock Database Interfaces
|
|
451
|
+
// ============================================================================
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Mock PostgreSQL client interface
|
|
455
|
+
*/
|
|
456
|
+
export interface MockPgClient {
|
|
457
|
+
connect: Mock;
|
|
458
|
+
query: Mock;
|
|
459
|
+
release: Mock;
|
|
460
|
+
end: Mock;
|
|
461
|
+
on: Mock;
|
|
462
|
+
off: Mock;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Mock PostgreSQL pool interface
|
|
467
|
+
*/
|
|
468
|
+
export interface MockPgPool {
|
|
469
|
+
connect: Mock;
|
|
470
|
+
query: Mock;
|
|
471
|
+
end: Mock;
|
|
472
|
+
on: Mock;
|
|
473
|
+
totalCount: number;
|
|
474
|
+
idleCount: number;
|
|
475
|
+
waitingCount: number;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Create a mock PostgreSQL client
|
|
480
|
+
*/
|
|
481
|
+
export function createMockPgClient(): MockPgClient {
|
|
482
|
+
return {
|
|
483
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
484
|
+
query: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
|
|
485
|
+
release: vi.fn(),
|
|
486
|
+
end: vi.fn().mockResolvedValue(undefined),
|
|
487
|
+
on: vi.fn(),
|
|
488
|
+
off: vi.fn(),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Create a mock PostgreSQL pool
|
|
494
|
+
*/
|
|
495
|
+
export function createMockPgPool(): MockPgPool {
|
|
496
|
+
const mockClient = createMockPgClient();
|
|
497
|
+
return {
|
|
498
|
+
connect: vi.fn().mockResolvedValue(mockClient),
|
|
499
|
+
query: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
|
|
500
|
+
end: vi.fn().mockResolvedValue(undefined),
|
|
501
|
+
on: vi.fn(),
|
|
502
|
+
totalCount: 10,
|
|
503
|
+
idleCount: 5,
|
|
504
|
+
waitingCount: 0,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ============================================================================
|
|
509
|
+
// Performance Testing Utilities
|
|
510
|
+
// ============================================================================
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Measure execution time of an async function
|
|
514
|
+
*/
|
|
515
|
+
export async function measureAsync<T>(
|
|
516
|
+
fn: () => Promise<T>
|
|
517
|
+
): Promise<{ result: T; durationMs: number }> {
|
|
518
|
+
const start = performance.now();
|
|
519
|
+
const result = await fn();
|
|
520
|
+
const durationMs = performance.now() - start;
|
|
521
|
+
return { result, durationMs };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Run a function multiple times and return statistics
|
|
526
|
+
*/
|
|
527
|
+
export async function benchmark<T>(
|
|
528
|
+
fn: () => Promise<T>,
|
|
529
|
+
iterations: number = 100
|
|
530
|
+
): Promise<{
|
|
531
|
+
iterations: number;
|
|
532
|
+
totalMs: number;
|
|
533
|
+
avgMs: number;
|
|
534
|
+
minMs: number;
|
|
535
|
+
maxMs: number;
|
|
536
|
+
p95Ms: number;
|
|
537
|
+
p99Ms: number;
|
|
538
|
+
}> {
|
|
539
|
+
const times: number[] = [];
|
|
540
|
+
|
|
541
|
+
for (let i = 0; i < iterations; i++) {
|
|
542
|
+
const { durationMs } = await measureAsync(fn);
|
|
543
|
+
times.push(durationMs);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
times.sort((a, b) => a - b);
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
iterations,
|
|
550
|
+
totalMs: times.reduce((sum, t) => sum + t, 0),
|
|
551
|
+
avgMs: times.reduce((sum, t) => sum + t, 0) / iterations,
|
|
552
|
+
minMs: times[0],
|
|
553
|
+
maxMs: times[times.length - 1],
|
|
554
|
+
p95Ms: times[Math.floor(iterations * 0.95)],
|
|
555
|
+
p99Ms: times[Math.floor(iterations * 0.99)],
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Generate test data for throughput testing
|
|
561
|
+
*/
|
|
562
|
+
export function generateBulkInsertData(
|
|
563
|
+
count: number,
|
|
564
|
+
dimensions: number = 384
|
|
565
|
+
): VectorInsertOptions['vectors'] {
|
|
566
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
567
|
+
id: `bulk-${Date.now()}-${i}`,
|
|
568
|
+
vector: randomVector(dimensions),
|
|
569
|
+
metadata: {
|
|
570
|
+
index: i,
|
|
571
|
+
timestamp: Date.now(),
|
|
572
|
+
batch: Math.floor(i / 100),
|
|
573
|
+
},
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ============================================================================
|
|
578
|
+
// Test Data Builders
|
|
579
|
+
// ============================================================================
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Builder for VectorSearchOptions
|
|
583
|
+
*/
|
|
584
|
+
export class SearchOptionsBuilder {
|
|
585
|
+
private options: VectorSearchOptions;
|
|
586
|
+
|
|
587
|
+
constructor(dimensions: number = 384) {
|
|
588
|
+
this.options = {
|
|
589
|
+
query: randomVector(dimensions),
|
|
590
|
+
k: 10,
|
|
591
|
+
metric: 'cosine',
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
withQuery(query: number[]): this {
|
|
596
|
+
this.options = { ...this.options, query };
|
|
597
|
+
return this;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
withK(k: number): this {
|
|
601
|
+
this.options = { ...this.options, k };
|
|
602
|
+
return this;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
withMetric(metric: DistanceMetric): this {
|
|
606
|
+
this.options = { ...this.options, metric };
|
|
607
|
+
return this;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
withFilter(filter: Record<string, unknown>): this {
|
|
611
|
+
this.options = { ...this.options, filter };
|
|
612
|
+
return this;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
withThreshold(threshold: number): this {
|
|
616
|
+
this.options = { ...this.options, threshold };
|
|
617
|
+
return this;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
withTable(tableName: string): this {
|
|
621
|
+
this.options = { ...this.options, tableName };
|
|
622
|
+
return this;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
includeVector(include: boolean = true): this {
|
|
626
|
+
this.options = { ...this.options, includeVector: include };
|
|
627
|
+
return this;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
includeMetadata(include: boolean = true): this {
|
|
631
|
+
this.options = { ...this.options, includeMetadata: include };
|
|
632
|
+
return this;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
build(): VectorSearchOptions {
|
|
636
|
+
return { ...this.options };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Builder for VectorInsertOptions
|
|
642
|
+
*/
|
|
643
|
+
export class InsertOptionsBuilder {
|
|
644
|
+
private options: VectorInsertOptions;
|
|
645
|
+
|
|
646
|
+
constructor(tableName: string = 'vectors') {
|
|
647
|
+
this.options = {
|
|
648
|
+
tableName,
|
|
649
|
+
vectors: [],
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
addVector(
|
|
654
|
+
vector: number[],
|
|
655
|
+
id?: string,
|
|
656
|
+
metadata?: Record<string, unknown>
|
|
657
|
+
): this {
|
|
658
|
+
this.options = {
|
|
659
|
+
...this.options,
|
|
660
|
+
vectors: [...this.options.vectors, { id, vector, metadata }],
|
|
661
|
+
};
|
|
662
|
+
return this;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
addRandomVectors(count: number, dimensions: number = 384): this {
|
|
666
|
+
const vectors = Array.from({ length: count }, (_, i) => ({
|
|
667
|
+
id: `gen-${Date.now()}-${i}`,
|
|
668
|
+
vector: randomVector(dimensions),
|
|
669
|
+
metadata: { generated: true, index: i },
|
|
670
|
+
}));
|
|
671
|
+
this.options = {
|
|
672
|
+
...this.options,
|
|
673
|
+
vectors: [...this.options.vectors, ...vectors],
|
|
674
|
+
};
|
|
675
|
+
return this;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
withUpsert(upsert: boolean = true): this {
|
|
679
|
+
this.options = { ...this.options, upsert };
|
|
680
|
+
return this;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
withBatchSize(batchSize: number): this {
|
|
684
|
+
this.options = { ...this.options, batchSize };
|
|
685
|
+
return this;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
withReturning(returning: boolean = true): this {
|
|
689
|
+
this.options = { ...this.options, returning };
|
|
690
|
+
return this;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
build(): VectorInsertOptions {
|
|
694
|
+
return { ...this.options };
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Builder for VectorIndexOptions
|
|
700
|
+
*/
|
|
701
|
+
export class IndexOptionsBuilder {
|
|
702
|
+
private options: VectorIndexOptions;
|
|
703
|
+
|
|
704
|
+
constructor(tableName: string, columnName: string = 'embedding') {
|
|
705
|
+
this.options = {
|
|
706
|
+
tableName,
|
|
707
|
+
columnName,
|
|
708
|
+
indexType: 'hnsw',
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
withType(indexType: VectorIndexType): this {
|
|
713
|
+
this.options = { ...this.options, indexType };
|
|
714
|
+
return this;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
withName(indexName: string): this {
|
|
718
|
+
this.options = { ...this.options, indexName };
|
|
719
|
+
return this;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
withMetric(metric: DistanceMetric): this {
|
|
723
|
+
this.options = { ...this.options, metric };
|
|
724
|
+
return this;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
withHNSWParams(m: number, efConstruction: number): this {
|
|
728
|
+
this.options = { ...this.options, m, efConstruction };
|
|
729
|
+
return this;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
withIVFParams(lists: number): this {
|
|
733
|
+
this.options = { ...this.options, lists };
|
|
734
|
+
return this;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
concurrent(concurrent: boolean = true): this {
|
|
738
|
+
this.options = { ...this.options, concurrent };
|
|
739
|
+
return this;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
replace(replace: boolean = true): this {
|
|
743
|
+
this.options = { ...this.options, replace };
|
|
744
|
+
return this;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
build(): VectorIndexOptions {
|
|
748
|
+
return { ...this.options };
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// ============================================================================
|
|
753
|
+
// Assertion Helpers
|
|
754
|
+
// ============================================================================
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Assert that results are sorted by score descending
|
|
758
|
+
*/
|
|
759
|
+
export function assertSortedByScore(results: VectorSearchResult[]): void {
|
|
760
|
+
for (let i = 1; i < results.length; i++) {
|
|
761
|
+
if (results[i].score > results[i - 1].score) {
|
|
762
|
+
throw new Error(
|
|
763
|
+
`Results not sorted by score: ${results[i - 1].score} > ${results[i].score}`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Assert that results are sorted by distance ascending
|
|
771
|
+
*/
|
|
772
|
+
export function assertSortedByDistance(results: VectorSearchResult[]): void {
|
|
773
|
+
for (let i = 1; i < results.length; i++) {
|
|
774
|
+
if (
|
|
775
|
+
results[i].distance !== undefined &&
|
|
776
|
+
results[i - 1].distance !== undefined &&
|
|
777
|
+
results[i].distance! < results[i - 1].distance!
|
|
778
|
+
) {
|
|
779
|
+
throw new Error(
|
|
780
|
+
`Results not sorted by distance: ${results[i - 1].distance} < ${results[i].distance}`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Assert that all vectors are normalized (unit length)
|
|
788
|
+
*/
|
|
789
|
+
export function assertNormalized(vectors: number[][], tolerance: number = 0.001): void {
|
|
790
|
+
for (const vec of vectors) {
|
|
791
|
+
const magnitude = Math.sqrt(vec.reduce((sum, v) => sum + v * v, 0));
|
|
792
|
+
if (Math.abs(magnitude - 1) > tolerance) {
|
|
793
|
+
throw new Error(`Vector not normalized: magnitude = ${magnitude}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Assert that all vectors are inside Poincare ball
|
|
800
|
+
*/
|
|
801
|
+
export function assertInPoincareBall(
|
|
802
|
+
vectors: number[][],
|
|
803
|
+
maxNorm: number = 0.99
|
|
804
|
+
): void {
|
|
805
|
+
for (const vec of vectors) {
|
|
806
|
+
const norm = Math.sqrt(vec.reduce((sum, v) => sum + v * v, 0));
|
|
807
|
+
if (norm >= maxNorm) {
|
|
808
|
+
throw new Error(`Vector outside Poincare ball: norm = ${norm}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ============================================================================
|
|
814
|
+
// Cleanup Utilities
|
|
815
|
+
// ============================================================================
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Generate unique table name for tests
|
|
819
|
+
*/
|
|
820
|
+
export function uniqueTableName(prefix: string = 'test'): string {
|
|
821
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Generate unique index name for tests
|
|
826
|
+
*/
|
|
827
|
+
export function uniqueIndexName(tableName: string, columnName: string = 'embedding'): string {
|
|
828
|
+
return `idx_${tableName}_${columnName}_${Math.random().toString(36).slice(2, 8)}`;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ============================================================================
|
|
832
|
+
// Type Exports
|
|
833
|
+
// ============================================================================
|
|
834
|
+
|
|
835
|
+
export type {
|
|
836
|
+
RuVectorConfig,
|
|
837
|
+
RuVectorClientOptions,
|
|
838
|
+
VectorSearchOptions,
|
|
839
|
+
VectorSearchResult,
|
|
840
|
+
VectorInsertOptions,
|
|
841
|
+
VectorUpdateOptions,
|
|
842
|
+
VectorIndexOptions,
|
|
843
|
+
BatchVectorOptions,
|
|
844
|
+
GraphData,
|
|
845
|
+
GNNLayer,
|
|
846
|
+
AttentionConfig,
|
|
847
|
+
AttentionInput,
|
|
848
|
+
HyperbolicEmbedding,
|
|
849
|
+
HyperbolicInput,
|
|
850
|
+
DistanceMetric,
|
|
851
|
+
VectorIndexType,
|
|
852
|
+
IndexStats,
|
|
853
|
+
QueryResult,
|
|
854
|
+
BatchResult,
|
|
855
|
+
ConnectionResult,
|
|
856
|
+
HealthStatus,
|
|
857
|
+
RuVectorStats,
|
|
858
|
+
AnalysisResult,
|
|
859
|
+
MigrationResult,
|
|
860
|
+
};
|