@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,2000 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuVector PostgreSQL Bridge Plugin
|
|
3
|
+
*
|
|
4
|
+
* Production-ready plugin for RuVector PostgreSQL integration providing:
|
|
5
|
+
* - Connection management with pooling
|
|
6
|
+
* - Vector similarity search (HNSW, IVF)
|
|
7
|
+
* - Batch operations
|
|
8
|
+
* - Index management
|
|
9
|
+
* - MCP tool integration
|
|
10
|
+
* - Event emission and metrics
|
|
11
|
+
*
|
|
12
|
+
* @module @sparkleideas/plugins/integrations/ruvector
|
|
13
|
+
* @version 1.0.0
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { EventEmitter } from 'events';
|
|
17
|
+
import { BasePlugin } from '../../core/base-plugin.js';
|
|
18
|
+
import type { MCPToolDefinition, MCPToolResult, HealthCheckResult } from '../../types/index.js';
|
|
19
|
+
import type {
|
|
20
|
+
RuVectorConfig,
|
|
21
|
+
VectorSearchOptions,
|
|
22
|
+
VectorSearchResult,
|
|
23
|
+
VectorInsertOptions,
|
|
24
|
+
VectorUpdateOptions,
|
|
25
|
+
VectorIndexOptions,
|
|
26
|
+
BatchVectorOptions,
|
|
27
|
+
DistanceMetric,
|
|
28
|
+
VectorIndexType,
|
|
29
|
+
IndexStats,
|
|
30
|
+
QueryResult,
|
|
31
|
+
BatchResult,
|
|
32
|
+
RuVectorEventType,
|
|
33
|
+
ConnectionResult,
|
|
34
|
+
PoolConfig,
|
|
35
|
+
RetryConfig,
|
|
36
|
+
BulkSearchResult,
|
|
37
|
+
HealthStatus,
|
|
38
|
+
RuVectorStats,
|
|
39
|
+
} from './types.js';
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Type Definitions for pg (node-postgres)
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* PostgreSQL Pool interface (from pg package).
|
|
47
|
+
* Using interface to avoid direct dependency issues.
|
|
48
|
+
*/
|
|
49
|
+
interface Pool {
|
|
50
|
+
connect(): Promise<PoolClient>;
|
|
51
|
+
query<T = unknown>(text: string, values?: unknown[]): Promise<PgQueryResult<T>>;
|
|
52
|
+
end(): Promise<void>;
|
|
53
|
+
on(event: string, callback: (...args: unknown[]) => void): this;
|
|
54
|
+
totalCount: number;
|
|
55
|
+
idleCount: number;
|
|
56
|
+
waitingCount: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface PoolClient {
|
|
60
|
+
query<T = unknown>(text: string, values?: unknown[]): Promise<PgQueryResult<T>>;
|
|
61
|
+
release(err?: Error): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface PgQueryResult<T> {
|
|
65
|
+
rows: T[];
|
|
66
|
+
rowCount: number | null;
|
|
67
|
+
command: string;
|
|
68
|
+
fields?: Array<{ name: string; dataTypeID: number }>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface PoolFactory {
|
|
72
|
+
Pool: new (config: PgPoolConfig) => Pool;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface PgPoolConfig {
|
|
76
|
+
host: string;
|
|
77
|
+
port: number;
|
|
78
|
+
database: string;
|
|
79
|
+
user: string;
|
|
80
|
+
password: string;
|
|
81
|
+
ssl?: boolean | { rejectUnauthorized?: boolean };
|
|
82
|
+
min?: number;
|
|
83
|
+
max?: number;
|
|
84
|
+
idleTimeoutMillis?: number;
|
|
85
|
+
connectionTimeoutMillis?: number;
|
|
86
|
+
application_name?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Constants
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
const PLUGIN_NAME = 'ruvector-postgres';
|
|
94
|
+
const PLUGIN_VERSION = '1.0.0';
|
|
95
|
+
const DEFAULT_POOL_MIN = 2;
|
|
96
|
+
const DEFAULT_POOL_MAX = 10;
|
|
97
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
98
|
+
const DEFAULT_CONNECTION_TIMEOUT_MS = 10000;
|
|
99
|
+
const DEFAULT_QUERY_TIMEOUT_MS = 30000;
|
|
100
|
+
const DEFAULT_VECTOR_COLUMN = 'embedding';
|
|
101
|
+
const DEFAULT_DIMENSIONS = 1536;
|
|
102
|
+
const SLOW_QUERY_THRESHOLD_MS = 1000;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Distance metric to pgvector operator mapping.
|
|
106
|
+
*/
|
|
107
|
+
const DISTANCE_OPERATORS: Record<DistanceMetric, string> = {
|
|
108
|
+
cosine: '<=>',
|
|
109
|
+
euclidean: '<->',
|
|
110
|
+
dot: '<#>',
|
|
111
|
+
hamming: '<~>',
|
|
112
|
+
manhattan: '<+>',
|
|
113
|
+
chebyshev: '<+>', // Not directly supported, fallback
|
|
114
|
+
jaccard: '<~>', // Binary similarity
|
|
115
|
+
minkowski: '<->', // Fallback to L2
|
|
116
|
+
bray_curtis: '<->', // Fallback
|
|
117
|
+
canberra: '<->', // Fallback
|
|
118
|
+
mahalanobis: '<->', // Fallback
|
|
119
|
+
correlation: '<=>', // Similar to cosine
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Index type to SQL mapping.
|
|
124
|
+
*/
|
|
125
|
+
const INDEX_TYPE_SQL: Record<VectorIndexType, string> = {
|
|
126
|
+
hnsw: 'hnsw',
|
|
127
|
+
ivfflat: 'ivfflat',
|
|
128
|
+
ivfpq: 'ivfflat', // IVF with PQ uses similar syntax
|
|
129
|
+
flat: '', // No index (brute force)
|
|
130
|
+
diskann: 'hnsw', // Fallback to HNSW
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Metrics Interface
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Metrics collected by the RuVector Bridge.
|
|
139
|
+
*/
|
|
140
|
+
interface RuVectorMetrics {
|
|
141
|
+
queriesTotal: number;
|
|
142
|
+
queriesSucceeded: number;
|
|
143
|
+
queriesFailed: number;
|
|
144
|
+
slowQueries: number;
|
|
145
|
+
avgQueryTimeMs: number;
|
|
146
|
+
vectorsInserted: number;
|
|
147
|
+
vectorsUpdated: number;
|
|
148
|
+
vectorsDeleted: number;
|
|
149
|
+
searchesPerformed: number;
|
|
150
|
+
cacheHits: number;
|
|
151
|
+
cacheMisses: number;
|
|
152
|
+
connectionAcquires: number;
|
|
153
|
+
connectionReleases: number;
|
|
154
|
+
connectionErrors: number;
|
|
155
|
+
lastQueryTime: number;
|
|
156
|
+
uptime: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// Connection Manager
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Manages PostgreSQL connection pooling with automatic retry and health monitoring.
|
|
165
|
+
*/
|
|
166
|
+
class ConnectionManager extends EventEmitter {
|
|
167
|
+
private pool: Pool | null = null;
|
|
168
|
+
private readonly config: RuVectorConfig;
|
|
169
|
+
private readonly retryConfig: RetryConfig;
|
|
170
|
+
private connectionId = 0;
|
|
171
|
+
private isConnected = false;
|
|
172
|
+
private lastHealthCheck: Date | null = null;
|
|
173
|
+
|
|
174
|
+
constructor(config: RuVectorConfig) {
|
|
175
|
+
super();
|
|
176
|
+
this.config = config;
|
|
177
|
+
this.retryConfig = config.retry ?? {
|
|
178
|
+
maxAttempts: 3,
|
|
179
|
+
initialDelayMs: 1000,
|
|
180
|
+
maxDelayMs: 30000,
|
|
181
|
+
backoffMultiplier: 2,
|
|
182
|
+
jitter: true,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Initialize the connection pool.
|
|
188
|
+
*/
|
|
189
|
+
async initialize(): Promise<ConnectionResult> {
|
|
190
|
+
if (this.pool) {
|
|
191
|
+
throw new Error('Connection pool already initialized');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const poolConfig = this.buildPoolConfig();
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
// Dynamically import pg to avoid bundling issues
|
|
198
|
+
const pg = await this.loadPg();
|
|
199
|
+
this.pool = new pg.Pool(poolConfig);
|
|
200
|
+
|
|
201
|
+
// Set up event handlers
|
|
202
|
+
this.pool.on('connect', () => {
|
|
203
|
+
this.connectionId++;
|
|
204
|
+
this.emit('connection:open', {
|
|
205
|
+
connectionId: `conn-${this.connectionId}`,
|
|
206
|
+
host: this.config.host,
|
|
207
|
+
port: this.config.port,
|
|
208
|
+
database: this.config.database,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
this.pool.on('error', (...args: unknown[]) => {
|
|
213
|
+
const err = args[0] as Error;
|
|
214
|
+
this.emit('connection:error', {
|
|
215
|
+
error: err,
|
|
216
|
+
code: (err as { code?: string }).code,
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Test connection
|
|
221
|
+
const client = await this.pool.connect();
|
|
222
|
+
const result = await client.query<{ version: string; ruvector_version?: string }>(
|
|
223
|
+
"SELECT version() as version, COALESCE(ruvector.version(), 'N/A') as ruvector_version"
|
|
224
|
+
);
|
|
225
|
+
client.release();
|
|
226
|
+
|
|
227
|
+
this.isConnected = true;
|
|
228
|
+
this.lastHealthCheck = new Date();
|
|
229
|
+
|
|
230
|
+
const connectionResult: ConnectionResult = {
|
|
231
|
+
connectionId: `conn-${this.connectionId}`,
|
|
232
|
+
ready: true,
|
|
233
|
+
serverVersion: result.rows[0]?.version ?? 'unknown',
|
|
234
|
+
ruVectorVersion: result.rows[0]?.ruvector_version ?? 'N/A',
|
|
235
|
+
parameters: {
|
|
236
|
+
host: this.config.host,
|
|
237
|
+
port: String(this.config.port),
|
|
238
|
+
database: this.config.database,
|
|
239
|
+
ssl: String(!!this.config.ssl),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return connectionResult;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
this.isConnected = false;
|
|
246
|
+
throw new Error(`Failed to initialize connection pool: ${(error as Error).message}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Load pg module dynamically.
|
|
252
|
+
*/
|
|
253
|
+
private async loadPg(): Promise<PoolFactory> {
|
|
254
|
+
try {
|
|
255
|
+
// Try to import pg
|
|
256
|
+
const pg = await import('pg');
|
|
257
|
+
return pg.default ?? pg;
|
|
258
|
+
} catch {
|
|
259
|
+
throw new Error(
|
|
260
|
+
'pg (node-postgres) package not found. Install it with: npm install pg'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Build pool configuration from RuVector config.
|
|
267
|
+
*/
|
|
268
|
+
private buildPoolConfig(): PgPoolConfig {
|
|
269
|
+
const poolSettings = (this.config.pool ?? {}) as Partial<PoolConfig>;
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
host: this.config.host,
|
|
273
|
+
port: this.config.port,
|
|
274
|
+
database: this.config.database,
|
|
275
|
+
user: this.config.user,
|
|
276
|
+
password: this.config.password,
|
|
277
|
+
ssl: this.config.ssl
|
|
278
|
+
? typeof this.config.ssl === 'boolean'
|
|
279
|
+
? { rejectUnauthorized: false }
|
|
280
|
+
: { rejectUnauthorized: this.config.ssl.rejectUnauthorized ?? true }
|
|
281
|
+
: undefined,
|
|
282
|
+
min: poolSettings.min ?? DEFAULT_POOL_MIN,
|
|
283
|
+
max: poolSettings.max ?? this.config.poolSize ?? DEFAULT_POOL_MAX,
|
|
284
|
+
idleTimeoutMillis: poolSettings.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
|
|
285
|
+
connectionTimeoutMillis: this.config.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS,
|
|
286
|
+
application_name: this.config.applicationName ?? 'claude-flow-ruvector',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Execute a query with timeout and retry logic.
|
|
292
|
+
*/
|
|
293
|
+
async query<T = Record<string, unknown>>(
|
|
294
|
+
sql: string,
|
|
295
|
+
params?: unknown[],
|
|
296
|
+
timeoutMs?: number
|
|
297
|
+
): Promise<QueryResult<T>> {
|
|
298
|
+
if (!this.pool) {
|
|
299
|
+
throw new Error('Connection pool not initialized');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const startTime = Date.now();
|
|
303
|
+
const queryId = `query-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
304
|
+
const timeout = timeoutMs ?? this.config.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS;
|
|
305
|
+
|
|
306
|
+
this.emit('query:start', { queryId, sql, params });
|
|
307
|
+
|
|
308
|
+
let lastError: Error | null = null;
|
|
309
|
+
let attempt = 0;
|
|
310
|
+
|
|
311
|
+
while (attempt < this.retryConfig.maxAttempts) {
|
|
312
|
+
attempt++;
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const result = await this.executeWithTimeout<T>(sql, params, timeout);
|
|
316
|
+
const durationMs = Date.now() - startTime;
|
|
317
|
+
|
|
318
|
+
const queryResult: QueryResult<T> = {
|
|
319
|
+
rows: result.rows,
|
|
320
|
+
rowCount: result.rowCount ?? 0,
|
|
321
|
+
affectedRows: result.rowCount ?? undefined,
|
|
322
|
+
durationMs,
|
|
323
|
+
command: result.command,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
this.emit('query:complete', {
|
|
327
|
+
queryId,
|
|
328
|
+
durationMs,
|
|
329
|
+
rowCount: queryResult.rowCount,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
|
|
333
|
+
this.emit('query:slow', {
|
|
334
|
+
queryId,
|
|
335
|
+
durationMs,
|
|
336
|
+
rowCount: queryResult.rowCount,
|
|
337
|
+
threshold: SLOW_QUERY_THRESHOLD_MS,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return queryResult;
|
|
342
|
+
} catch (error) {
|
|
343
|
+
lastError = error as Error;
|
|
344
|
+
const isRetryable = this.isRetryableError(lastError);
|
|
345
|
+
|
|
346
|
+
if (!isRetryable || attempt >= this.retryConfig.maxAttempts) {
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Calculate delay with exponential backoff and optional jitter
|
|
351
|
+
let delay = this.retryConfig.initialDelayMs * Math.pow(this.retryConfig.backoffMultiplier, attempt - 1);
|
|
352
|
+
delay = Math.min(delay, this.retryConfig.maxDelayMs);
|
|
353
|
+
|
|
354
|
+
if (this.retryConfig.jitter) {
|
|
355
|
+
delay = delay * (0.5 + Math.random());
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
await this.sleep(delay);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const durationMs = Date.now() - startTime;
|
|
363
|
+
this.emit('query:error', {
|
|
364
|
+
queryId,
|
|
365
|
+
sql,
|
|
366
|
+
params,
|
|
367
|
+
error: lastError!,
|
|
368
|
+
durationMs,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
throw lastError!;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Execute query with timeout.
|
|
376
|
+
*/
|
|
377
|
+
private async executeWithTimeout<T>(
|
|
378
|
+
sql: string,
|
|
379
|
+
params: unknown[] | undefined,
|
|
380
|
+
timeoutMs: number
|
|
381
|
+
): Promise<PgQueryResult<T>> {
|
|
382
|
+
return new Promise(async (resolve, reject) => {
|
|
383
|
+
const timer = setTimeout(() => {
|
|
384
|
+
reject(new Error(`Query timed out after ${timeoutMs}ms`));
|
|
385
|
+
}, timeoutMs);
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const result = await this.pool!.query<T>(sql, params);
|
|
389
|
+
clearTimeout(timer);
|
|
390
|
+
resolve(result);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
clearTimeout(timer);
|
|
393
|
+
reject(error);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Check if error is retryable.
|
|
400
|
+
*/
|
|
401
|
+
private isRetryableError(error: Error): boolean {
|
|
402
|
+
const code = (error as { code?: string }).code;
|
|
403
|
+
const retryableCodes = this.retryConfig.retryableErrors ?? [
|
|
404
|
+
'ECONNREFUSED',
|
|
405
|
+
'ECONNRESET',
|
|
406
|
+
'ETIMEDOUT',
|
|
407
|
+
'57P01', // admin_shutdown
|
|
408
|
+
'57P02', // crash_shutdown
|
|
409
|
+
'57P03', // cannot_connect_now
|
|
410
|
+
'40001', // serialization_failure
|
|
411
|
+
'40P01', // deadlock_detected
|
|
412
|
+
];
|
|
413
|
+
return code !== undefined && retryableCodes.includes(code);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Get a client from the pool.
|
|
418
|
+
*/
|
|
419
|
+
async getClient(): Promise<PoolClient> {
|
|
420
|
+
if (!this.pool) {
|
|
421
|
+
throw new Error('Connection pool not initialized');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
this.emit('connection:pool_acquired', this.getPoolStats());
|
|
425
|
+
return this.pool.connect();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Release a client back to the pool.
|
|
430
|
+
*/
|
|
431
|
+
releaseClient(client: PoolClient, error?: Error): void {
|
|
432
|
+
client.release(error);
|
|
433
|
+
this.emit('connection:pool_released', this.getPoolStats());
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get pool statistics.
|
|
438
|
+
*/
|
|
439
|
+
getPoolStats(): {
|
|
440
|
+
connectionId: string;
|
|
441
|
+
poolSize: number;
|
|
442
|
+
availableConnections: number;
|
|
443
|
+
waitingClients: number;
|
|
444
|
+
} {
|
|
445
|
+
return {
|
|
446
|
+
connectionId: `pool-${this.connectionId}`,
|
|
447
|
+
poolSize: this.pool?.totalCount ?? 0,
|
|
448
|
+
availableConnections: this.pool?.idleCount ?? 0,
|
|
449
|
+
waitingClients: this.pool?.waitingCount ?? 0,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Check if connected.
|
|
455
|
+
*/
|
|
456
|
+
isHealthy(): boolean {
|
|
457
|
+
return this.isConnected && this.pool !== null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Shutdown the connection pool.
|
|
462
|
+
*/
|
|
463
|
+
async shutdown(): Promise<void> {
|
|
464
|
+
if (this.pool) {
|
|
465
|
+
await this.pool.end();
|
|
466
|
+
this.pool = null;
|
|
467
|
+
this.isConnected = false;
|
|
468
|
+
this.emit('connection:close', {
|
|
469
|
+
connectionId: `conn-${this.connectionId}`,
|
|
470
|
+
host: this.config.host,
|
|
471
|
+
port: this.config.port,
|
|
472
|
+
database: this.config.database,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Sleep utility.
|
|
479
|
+
*/
|
|
480
|
+
private sleep(ms: number): Promise<void> {
|
|
481
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ============================================================================
|
|
486
|
+
// Vector Operations
|
|
487
|
+
// ============================================================================
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Provides vector operation methods for search, insert, update, and delete.
|
|
491
|
+
*/
|
|
492
|
+
class VectorOps {
|
|
493
|
+
private readonly connectionManager: ConnectionManager;
|
|
494
|
+
private readonly config: RuVectorConfig;
|
|
495
|
+
|
|
496
|
+
constructor(connectionManager: ConnectionManager, config: RuVectorConfig) {
|
|
497
|
+
this.connectionManager = connectionManager;
|
|
498
|
+
this.config = config;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Perform vector similarity search.
|
|
503
|
+
*/
|
|
504
|
+
async search(options: VectorSearchOptions): Promise<VectorSearchResult[]> {
|
|
505
|
+
const tableName = options.tableName ?? 'vectors';
|
|
506
|
+
const vectorColumn = options.vectorColumn ?? DEFAULT_VECTOR_COLUMN;
|
|
507
|
+
const metric = options.metric ?? 'cosine';
|
|
508
|
+
const operator = DISTANCE_OPERATORS[metric] ?? '<=>';
|
|
509
|
+
|
|
510
|
+
// Build query vector string
|
|
511
|
+
const queryVector = this.formatVector(options.query);
|
|
512
|
+
|
|
513
|
+
// Set HNSW parameters if specified
|
|
514
|
+
if (options.efSearch) {
|
|
515
|
+
await this.connectionManager.query(
|
|
516
|
+
`SET LOCAL hnsw.ef_search = ${options.efSearch}`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Set IVF probes if specified
|
|
521
|
+
if (options.probes) {
|
|
522
|
+
await this.connectionManager.query(
|
|
523
|
+
`SET LOCAL ivfflat.probes = ${options.probes}`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Build SELECT clause
|
|
528
|
+
const selectColumns = options.selectColumns ?? ['id'];
|
|
529
|
+
const columnList = [...selectColumns];
|
|
530
|
+
|
|
531
|
+
if (options.includeVector) {
|
|
532
|
+
columnList.push(vectorColumn);
|
|
533
|
+
}
|
|
534
|
+
if (options.includeMetadata) {
|
|
535
|
+
columnList.push('metadata');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Add distance/similarity calculation
|
|
539
|
+
const distanceExpr = `${vectorColumn} ${operator} '${queryVector}'::vector`;
|
|
540
|
+
columnList.push(`(${distanceExpr}) as distance`);
|
|
541
|
+
|
|
542
|
+
// Build WHERE clause
|
|
543
|
+
const whereClauses: string[] = [];
|
|
544
|
+
const params: unknown[] = [];
|
|
545
|
+
let paramIndex = 1;
|
|
546
|
+
|
|
547
|
+
if (options.threshold !== undefined) {
|
|
548
|
+
if (metric === 'cosine' || metric === 'dot') {
|
|
549
|
+
// For similarity metrics, higher is better
|
|
550
|
+
whereClauses.push(`(1 - (${distanceExpr})) >= $${paramIndex++}`);
|
|
551
|
+
params.push(options.threshold);
|
|
552
|
+
} else {
|
|
553
|
+
// For distance metrics, lower is better
|
|
554
|
+
whereClauses.push(`(${distanceExpr}) <= $${paramIndex++}`);
|
|
555
|
+
params.push(options.threshold);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (options.maxDistance !== undefined) {
|
|
560
|
+
whereClauses.push(`(${distanceExpr}) <= $${paramIndex++}`);
|
|
561
|
+
params.push(options.maxDistance);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (options.filter) {
|
|
565
|
+
for (const [key, value] of Object.entries(options.filter)) {
|
|
566
|
+
if (key === 'metadata') {
|
|
567
|
+
// JSONB containment
|
|
568
|
+
whereClauses.push(`metadata @> $${paramIndex++}::jsonb`);
|
|
569
|
+
params.push(JSON.stringify(value));
|
|
570
|
+
} else {
|
|
571
|
+
whereClauses.push(`${this.escapeIdentifier(key)} = $${paramIndex++}`);
|
|
572
|
+
params.push(value);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (options.whereClause) {
|
|
578
|
+
whereClauses.push(`(${options.whereClause})`);
|
|
579
|
+
if (options.whereParams) {
|
|
580
|
+
// Re-index parameters in the custom WHERE clause
|
|
581
|
+
const reindexed = options.whereParams.map(() => `$${paramIndex++}`);
|
|
582
|
+
params.push(...options.whereParams);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Build final query
|
|
587
|
+
const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
|
|
588
|
+
let sql = `SELECT ${columnList.join(', ')} FROM ${schemaPrefix}${this.escapeIdentifier(tableName)}`;
|
|
589
|
+
|
|
590
|
+
if (whereClauses.length > 0) {
|
|
591
|
+
sql += ` WHERE ${whereClauses.join(' AND ')}`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
sql += ` ORDER BY ${distanceExpr} ASC`;
|
|
595
|
+
sql += ` LIMIT ${options.k}`;
|
|
596
|
+
|
|
597
|
+
const result = await this.connectionManager.query<{
|
|
598
|
+
id: string | number;
|
|
599
|
+
distance: number;
|
|
600
|
+
[key: string]: unknown;
|
|
601
|
+
}>(sql, params, options.timeoutMs);
|
|
602
|
+
|
|
603
|
+
// Transform results
|
|
604
|
+
return result.rows.map((row, index) => {
|
|
605
|
+
const score = metric === 'cosine' || metric === 'dot'
|
|
606
|
+
? 1 - (row.distance as number)
|
|
607
|
+
: 1 / (1 + (row.distance as number));
|
|
608
|
+
|
|
609
|
+
const searchResult: VectorSearchResult = {
|
|
610
|
+
id: row.id,
|
|
611
|
+
score,
|
|
612
|
+
distance: row.distance as number,
|
|
613
|
+
rank: index + 1,
|
|
614
|
+
retrievedAt: new Date(),
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
if (options.includeVector && row[vectorColumn]) {
|
|
618
|
+
(searchResult as { vector?: number[] }).vector = this.parseVector(row[vectorColumn] as string);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (options.includeMetadata && row.metadata) {
|
|
622
|
+
(searchResult as { metadata?: Record<string, unknown> }).metadata = row.metadata as Record<string, unknown>;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return searchResult;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Perform batch vector search.
|
|
631
|
+
*/
|
|
632
|
+
async batchSearch(options: BatchVectorOptions): Promise<BulkSearchResult> {
|
|
633
|
+
const startTime = Date.now();
|
|
634
|
+
const results: VectorSearchResult[][] = [];
|
|
635
|
+
let cacheHits = 0;
|
|
636
|
+
let cacheMisses = 0;
|
|
637
|
+
|
|
638
|
+
const concurrency = options.concurrency ?? 4;
|
|
639
|
+
const queries = options.queries;
|
|
640
|
+
|
|
641
|
+
// Process queries in parallel batches
|
|
642
|
+
for (let i = 0; i < queries.length; i += concurrency) {
|
|
643
|
+
const batch = queries.slice(i, i + concurrency);
|
|
644
|
+
const batchResults = await Promise.all(
|
|
645
|
+
batch.map(query =>
|
|
646
|
+
this.search({
|
|
647
|
+
query,
|
|
648
|
+
k: options.k,
|
|
649
|
+
metric: options.metric,
|
|
650
|
+
filter: options.filter,
|
|
651
|
+
tableName: options.tableName,
|
|
652
|
+
vectorColumn: options.vectorColumn,
|
|
653
|
+
})
|
|
654
|
+
)
|
|
655
|
+
);
|
|
656
|
+
results.push(...batchResults);
|
|
657
|
+
cacheMisses += batch.length; // No caching implemented yet
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const totalDurationMs = Date.now() - startTime;
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
results,
|
|
664
|
+
totalDurationMs,
|
|
665
|
+
avgDurationMs: totalDurationMs / queries.length,
|
|
666
|
+
cacheStats: {
|
|
667
|
+
hits: cacheHits,
|
|
668
|
+
misses: cacheMisses,
|
|
669
|
+
hitRate: cacheHits / (cacheHits + cacheMisses) || 0,
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Insert vectors.
|
|
676
|
+
*/
|
|
677
|
+
async insert(options: VectorInsertOptions): Promise<BatchResult<string>> {
|
|
678
|
+
const startTime = Date.now();
|
|
679
|
+
const tableName = options.tableName;
|
|
680
|
+
const vectorColumn = options.vectorColumn ?? DEFAULT_VECTOR_COLUMN;
|
|
681
|
+
const batchSize = options.batchSize ?? 100;
|
|
682
|
+
|
|
683
|
+
const successful: string[] = [];
|
|
684
|
+
const errors: Array<{ index: number; message: string; input?: unknown }> = [];
|
|
685
|
+
let insertedCount = 0;
|
|
686
|
+
|
|
687
|
+
const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
|
|
688
|
+
|
|
689
|
+
// Process in batches
|
|
690
|
+
for (let i = 0; i < options.vectors.length; i += batchSize) {
|
|
691
|
+
const batch = options.vectors.slice(i, i + batchSize);
|
|
692
|
+
|
|
693
|
+
try {
|
|
694
|
+
// Build multi-row INSERT
|
|
695
|
+
const values: string[] = [];
|
|
696
|
+
const params: unknown[] = [];
|
|
697
|
+
let paramIndex = 1;
|
|
698
|
+
|
|
699
|
+
for (const item of batch) {
|
|
700
|
+
const vector = this.formatVector(item.vector);
|
|
701
|
+
const metadata = item.metadata ? JSON.stringify(item.metadata) : null;
|
|
702
|
+
|
|
703
|
+
if (item.id !== undefined) {
|
|
704
|
+
values.push(`($${paramIndex++}, '${vector}'::vector, $${paramIndex++}::jsonb)`);
|
|
705
|
+
params.push(item.id, metadata);
|
|
706
|
+
} else {
|
|
707
|
+
values.push(`(gen_random_uuid(), '${vector}'::vector, $${paramIndex++}::jsonb)`);
|
|
708
|
+
params.push(metadata);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
let sql = `INSERT INTO ${schemaPrefix}${this.escapeIdentifier(tableName)} `;
|
|
713
|
+
sql += `(id, ${this.escapeIdentifier(vectorColumn)}, metadata) VALUES ${values.join(', ')}`;
|
|
714
|
+
|
|
715
|
+
if (options.upsert) {
|
|
716
|
+
const conflictCols = options.conflictColumns ?? ['id'];
|
|
717
|
+
sql += ` ON CONFLICT (${conflictCols.join(', ')}) DO UPDATE SET `;
|
|
718
|
+
sql += `${this.escapeIdentifier(vectorColumn)} = EXCLUDED.${this.escapeIdentifier(vectorColumn)}, `;
|
|
719
|
+
sql += `metadata = EXCLUDED.metadata`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (options.returning) {
|
|
723
|
+
sql += ' RETURNING id';
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const result = await this.connectionManager.query<{ id: string }>(sql, params);
|
|
727
|
+
|
|
728
|
+
if (options.returning && result.rows) {
|
|
729
|
+
successful.push(...result.rows.map(r => String(r.id)));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
insertedCount += batch.length;
|
|
733
|
+
} catch (error) {
|
|
734
|
+
if (options.skipInvalid) {
|
|
735
|
+
// Try inserting individually
|
|
736
|
+
for (let j = 0; j < batch.length; j++) {
|
|
737
|
+
try {
|
|
738
|
+
const item = batch[j];
|
|
739
|
+
const vector = this.formatVector(item.vector);
|
|
740
|
+
const metadata = item.metadata ? JSON.stringify(item.metadata) : null;
|
|
741
|
+
|
|
742
|
+
const sql = `INSERT INTO ${schemaPrefix}${this.escapeIdentifier(tableName)} ` +
|
|
743
|
+
`(id, ${this.escapeIdentifier(vectorColumn)}, metadata) VALUES ` +
|
|
744
|
+
`($1, '${vector}'::vector, $2::jsonb)` +
|
|
745
|
+
(options.returning ? ' RETURNING id' : '');
|
|
746
|
+
|
|
747
|
+
const result = await this.connectionManager.query<{ id: string }>(
|
|
748
|
+
sql,
|
|
749
|
+
[item.id ?? null, metadata]
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
if (options.returning && result.rows.length > 0) {
|
|
753
|
+
successful.push(String(result.rows[0].id));
|
|
754
|
+
}
|
|
755
|
+
insertedCount++;
|
|
756
|
+
} catch (itemError) {
|
|
757
|
+
errors.push({
|
|
758
|
+
index: i + j,
|
|
759
|
+
message: (itemError as Error).message,
|
|
760
|
+
input: batch[j],
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
errors.push({
|
|
766
|
+
index: i,
|
|
767
|
+
message: (error as Error).message,
|
|
768
|
+
});
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const durationMs = Date.now() - startTime;
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
total: options.vectors.length,
|
|
778
|
+
successful: insertedCount,
|
|
779
|
+
failed: options.vectors.length - insertedCount,
|
|
780
|
+
results: options.returning ? successful : undefined,
|
|
781
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
782
|
+
durationMs,
|
|
783
|
+
throughput: insertedCount / (durationMs / 1000),
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Update a vector.
|
|
789
|
+
*/
|
|
790
|
+
async update(options: VectorUpdateOptions): Promise<boolean> {
|
|
791
|
+
const tableName = options.tableName;
|
|
792
|
+
const vectorColumn = options.vectorColumn ?? DEFAULT_VECTOR_COLUMN;
|
|
793
|
+
const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
|
|
794
|
+
|
|
795
|
+
const setClauses: string[] = [];
|
|
796
|
+
const params: unknown[] = [];
|
|
797
|
+
let paramIndex = 1;
|
|
798
|
+
|
|
799
|
+
if (options.vector) {
|
|
800
|
+
const vector = this.formatVector(options.vector);
|
|
801
|
+
setClauses.push(`${this.escapeIdentifier(vectorColumn)} = '${vector}'::vector`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (options.metadata) {
|
|
805
|
+
if (options.mergeMetadata) {
|
|
806
|
+
setClauses.push(`metadata = metadata || $${paramIndex++}::jsonb`);
|
|
807
|
+
} else {
|
|
808
|
+
setClauses.push(`metadata = $${paramIndex++}::jsonb`);
|
|
809
|
+
}
|
|
810
|
+
params.push(JSON.stringify(options.metadata));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (setClauses.length === 0) {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
params.push(options.id);
|
|
818
|
+
const sql = `UPDATE ${schemaPrefix}${this.escapeIdentifier(tableName)} ` +
|
|
819
|
+
`SET ${setClauses.join(', ')} WHERE id = $${paramIndex}`;
|
|
820
|
+
|
|
821
|
+
const result = await this.connectionManager.query(sql, params);
|
|
822
|
+
return (result.affectedRows ?? 0) > 0;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Delete a vector.
|
|
827
|
+
*/
|
|
828
|
+
async delete(tableName: string, id: string | number): Promise<boolean> {
|
|
829
|
+
const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
|
|
830
|
+
const sql = `DELETE FROM ${schemaPrefix}${this.escapeIdentifier(tableName)} WHERE id = $1`;
|
|
831
|
+
const result = await this.connectionManager.query(sql, [id]);
|
|
832
|
+
return (result.affectedRows ?? 0) > 0;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Bulk delete vectors.
|
|
837
|
+
*/
|
|
838
|
+
async bulkDelete(tableName: string, ids: Array<string | number>): Promise<BatchResult> {
|
|
839
|
+
const startTime = Date.now();
|
|
840
|
+
const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
|
|
841
|
+
|
|
842
|
+
const placeholders = ids.map((_, i) => `$${i + 1}`).join(', ');
|
|
843
|
+
const sql = `DELETE FROM ${schemaPrefix}${this.escapeIdentifier(tableName)} WHERE id IN (${placeholders})`;
|
|
844
|
+
|
|
845
|
+
const result = await this.connectionManager.query(sql, ids);
|
|
846
|
+
const durationMs = Date.now() - startTime;
|
|
847
|
+
const deleted = result.affectedRows ?? 0;
|
|
848
|
+
|
|
849
|
+
return {
|
|
850
|
+
total: ids.length,
|
|
851
|
+
successful: deleted,
|
|
852
|
+
failed: ids.length - deleted,
|
|
853
|
+
durationMs,
|
|
854
|
+
throughput: deleted / (durationMs / 1000),
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Create a vector index.
|
|
860
|
+
*/
|
|
861
|
+
async createIndex(options: VectorIndexOptions): Promise<void> {
|
|
862
|
+
const indexType = INDEX_TYPE_SQL[options.indexType];
|
|
863
|
+
if (!indexType && options.indexType !== 'flat') {
|
|
864
|
+
throw new Error(`Unsupported index type: ${options.indexType}`);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const indexName = options.indexName ??
|
|
868
|
+
`idx_${options.tableName}_${options.columnName}_${options.indexType}`;
|
|
869
|
+
const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
|
|
870
|
+
|
|
871
|
+
if (options.replace) {
|
|
872
|
+
await this.connectionManager.query(
|
|
873
|
+
`DROP INDEX IF EXISTS ${schemaPrefix}${this.escapeIdentifier(indexName)}`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (options.indexType === 'flat') {
|
|
878
|
+
return; // No index needed for brute force
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Build operator class based on metric
|
|
882
|
+
const opClass = this.getOperatorClass(options.metric ?? 'cosine', options.indexType);
|
|
883
|
+
|
|
884
|
+
// Build WITH clause for index parameters
|
|
885
|
+
const withParams: string[] = [];
|
|
886
|
+
if (options.m !== undefined) {
|
|
887
|
+
withParams.push(`m = ${options.m}`);
|
|
888
|
+
}
|
|
889
|
+
if (options.efConstruction !== undefined) {
|
|
890
|
+
withParams.push(`ef_construction = ${options.efConstruction}`);
|
|
891
|
+
}
|
|
892
|
+
if (options.lists !== undefined) {
|
|
893
|
+
withParams.push(`lists = ${options.lists}`);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const withClause = withParams.length > 0 ? ` WITH (${withParams.join(', ')})` : '';
|
|
897
|
+
const concurrent = options.concurrent ? 'CONCURRENTLY ' : '';
|
|
898
|
+
|
|
899
|
+
const sql = `CREATE INDEX ${concurrent}${this.escapeIdentifier(indexName)} ` +
|
|
900
|
+
`ON ${schemaPrefix}${this.escapeIdentifier(options.tableName)} ` +
|
|
901
|
+
`USING ${indexType} (${this.escapeIdentifier(options.columnName)} ${opClass})${withClause}`;
|
|
902
|
+
|
|
903
|
+
await this.connectionManager.query(sql);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Drop an index.
|
|
908
|
+
*/
|
|
909
|
+
async dropIndex(indexName: string): Promise<void> {
|
|
910
|
+
const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
|
|
911
|
+
await this.connectionManager.query(
|
|
912
|
+
`DROP INDEX IF EXISTS ${schemaPrefix}${this.escapeIdentifier(indexName)}`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Rebuild an index.
|
|
918
|
+
*/
|
|
919
|
+
async rebuildIndex(indexName: string): Promise<void> {
|
|
920
|
+
await this.connectionManager.query(`REINDEX INDEX ${this.escapeIdentifier(indexName)}`);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Get index statistics.
|
|
925
|
+
*/
|
|
926
|
+
async getIndexStats(indexName: string): Promise<IndexStats> {
|
|
927
|
+
const result = await this.connectionManager.query<{
|
|
928
|
+
indexrelname: string;
|
|
929
|
+
idx_scan: number;
|
|
930
|
+
idx_tup_read: number;
|
|
931
|
+
idx_tup_fetch: number;
|
|
932
|
+
pg_relation_size: number;
|
|
933
|
+
}>(
|
|
934
|
+
`SELECT
|
|
935
|
+
indexrelname,
|
|
936
|
+
idx_scan,
|
|
937
|
+
idx_tup_read,
|
|
938
|
+
idx_tup_fetch,
|
|
939
|
+
pg_relation_size(indexrelid) as pg_relation_size
|
|
940
|
+
FROM pg_stat_user_indexes
|
|
941
|
+
WHERE indexrelname = $1`,
|
|
942
|
+
[indexName]
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
if (result.rows.length === 0) {
|
|
946
|
+
throw new Error(`Index ${indexName} not found`);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const row = result.rows[0];
|
|
950
|
+
return {
|
|
951
|
+
indexName: row.indexrelname,
|
|
952
|
+
indexType: 'hnsw', // Would need additional query to determine
|
|
953
|
+
numVectors: row.idx_tup_read,
|
|
954
|
+
sizeBytes: row.pg_relation_size,
|
|
955
|
+
buildTimeMs: 0, // Not available from stats
|
|
956
|
+
lastRebuild: new Date(),
|
|
957
|
+
params: {
|
|
958
|
+
scans: row.idx_scan,
|
|
959
|
+
tuplesRead: row.idx_tup_read,
|
|
960
|
+
tuplesFetched: row.idx_tup_fetch,
|
|
961
|
+
},
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* List all indices for a table.
|
|
967
|
+
*/
|
|
968
|
+
async listIndices(tableName?: string): Promise<IndexStats[]> {
|
|
969
|
+
let sql = `SELECT
|
|
970
|
+
indexrelname,
|
|
971
|
+
idx_scan,
|
|
972
|
+
idx_tup_read,
|
|
973
|
+
idx_tup_fetch,
|
|
974
|
+
pg_relation_size(indexrelid) as pg_relation_size
|
|
975
|
+
FROM pg_stat_user_indexes`;
|
|
976
|
+
|
|
977
|
+
const params: unknown[] = [];
|
|
978
|
+
if (tableName) {
|
|
979
|
+
sql += ` WHERE relname = $1`;
|
|
980
|
+
params.push(tableName);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const result = await this.connectionManager.query<{
|
|
984
|
+
indexrelname: string;
|
|
985
|
+
idx_scan: number;
|
|
986
|
+
idx_tup_read: number;
|
|
987
|
+
idx_tup_fetch: number;
|
|
988
|
+
pg_relation_size: number;
|
|
989
|
+
}>(sql, params);
|
|
990
|
+
|
|
991
|
+
return result.rows.map(row => ({
|
|
992
|
+
indexName: row.indexrelname,
|
|
993
|
+
indexType: 'hnsw' as VectorIndexType,
|
|
994
|
+
numVectors: row.idx_tup_read,
|
|
995
|
+
sizeBytes: row.pg_relation_size,
|
|
996
|
+
buildTimeMs: 0,
|
|
997
|
+
lastRebuild: new Date(),
|
|
998
|
+
params: {
|
|
999
|
+
scans: row.idx_scan,
|
|
1000
|
+
tuplesRead: row.idx_tup_read,
|
|
1001
|
+
tuplesFetched: row.idx_tup_fetch,
|
|
1002
|
+
},
|
|
1003
|
+
}));
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Get operator class for index creation.
|
|
1008
|
+
*/
|
|
1009
|
+
private getOperatorClass(metric: DistanceMetric, indexType: VectorIndexType): string {
|
|
1010
|
+
const opClasses: Record<string, Record<string, string>> = {
|
|
1011
|
+
hnsw: {
|
|
1012
|
+
cosine: 'vector_cosine_ops',
|
|
1013
|
+
euclidean: 'vector_l2_ops',
|
|
1014
|
+
dot: 'vector_ip_ops',
|
|
1015
|
+
},
|
|
1016
|
+
ivfflat: {
|
|
1017
|
+
cosine: 'vector_cosine_ops',
|
|
1018
|
+
euclidean: 'vector_l2_ops',
|
|
1019
|
+
dot: 'vector_ip_ops',
|
|
1020
|
+
},
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
return opClasses[indexType]?.[metric] ?? 'vector_cosine_ops';
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Format vector for SQL.
|
|
1028
|
+
*/
|
|
1029
|
+
private formatVector(vector: number[] | Float32Array): string {
|
|
1030
|
+
const arr = Array.isArray(vector) ? vector : Array.from(vector);
|
|
1031
|
+
return `[${arr.join(',')}]`;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Parse vector from SQL result.
|
|
1036
|
+
*/
|
|
1037
|
+
private parseVector(vectorStr: string): number[] {
|
|
1038
|
+
// Handle pgvector format: [1,2,3] or {1,2,3}
|
|
1039
|
+
const cleaned = vectorStr.replace(/[\[\]{}]/g, '');
|
|
1040
|
+
return cleaned.split(',').map(Number);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Escape SQL identifier.
|
|
1045
|
+
*/
|
|
1046
|
+
private escapeIdentifier(identifier: string): string {
|
|
1047
|
+
// Basic SQL injection prevention
|
|
1048
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// ============================================================================
|
|
1053
|
+
// RuVector Bridge Plugin
|
|
1054
|
+
// ============================================================================
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* RuVector PostgreSQL Bridge Plugin for Claude-Flow v3.
|
|
1058
|
+
*
|
|
1059
|
+
* Provides comprehensive vector database integration with:
|
|
1060
|
+
* - Connection pooling and management
|
|
1061
|
+
* - Vector similarity search (HNSW, IVF)
|
|
1062
|
+
* - Batch operations for high throughput
|
|
1063
|
+
* - Index creation and management
|
|
1064
|
+
* - MCP tool integration
|
|
1065
|
+
* - Event-driven architecture
|
|
1066
|
+
* - Production-ready error handling and metrics
|
|
1067
|
+
*
|
|
1068
|
+
* @example
|
|
1069
|
+
* ```typescript
|
|
1070
|
+
* const bridge = new RuVectorBridge({
|
|
1071
|
+
* host: 'localhost',
|
|
1072
|
+
* port: 5432,
|
|
1073
|
+
* database: 'vectors',
|
|
1074
|
+
* user: 'postgres',
|
|
1075
|
+
* password: 'password',
|
|
1076
|
+
* poolSize: 10,
|
|
1077
|
+
* });
|
|
1078
|
+
*
|
|
1079
|
+
* await bridge.initialize(context);
|
|
1080
|
+
*
|
|
1081
|
+
* const results = await bridge.vectorSearch({
|
|
1082
|
+
* query: [0.1, 0.2, 0.3, ...],
|
|
1083
|
+
* k: 10,
|
|
1084
|
+
* metric: 'cosine',
|
|
1085
|
+
* tableName: 'embeddings',
|
|
1086
|
+
* });
|
|
1087
|
+
* ```
|
|
1088
|
+
*/
|
|
1089
|
+
export class RuVectorBridge extends BasePlugin {
|
|
1090
|
+
private readonly ruVectorConfig: RuVectorConfig;
|
|
1091
|
+
private connectionManager: ConnectionManager | null = null;
|
|
1092
|
+
private vectorOps: VectorOps | null = null;
|
|
1093
|
+
private metrics: RuVectorMetrics;
|
|
1094
|
+
private initTime: Date | null = null;
|
|
1095
|
+
|
|
1096
|
+
constructor(config: RuVectorConfig) {
|
|
1097
|
+
super({
|
|
1098
|
+
name: PLUGIN_NAME,
|
|
1099
|
+
version: PLUGIN_VERSION,
|
|
1100
|
+
description: 'RuVector PostgreSQL Bridge for Claude-Flow v3 - Advanced vector database integration',
|
|
1101
|
+
tags: ['database', 'vector', 'postgresql', 'search', 'embeddings'],
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
this.ruVectorConfig = config;
|
|
1105
|
+
this.metrics = this.createInitialMetrics();
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// ===========================================================================
|
|
1109
|
+
// Lifecycle Methods
|
|
1110
|
+
// ===========================================================================
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Initialize the plugin and establish database connection.
|
|
1114
|
+
*/
|
|
1115
|
+
protected async onInitialize(): Promise<void> {
|
|
1116
|
+
this.logger.info('Initializing RuVector PostgreSQL Bridge...');
|
|
1117
|
+
this.initTime = new Date();
|
|
1118
|
+
|
|
1119
|
+
// Create connection manager
|
|
1120
|
+
this.connectionManager = new ConnectionManager(this.ruVectorConfig);
|
|
1121
|
+
|
|
1122
|
+
// Forward connection events
|
|
1123
|
+
this.forwardConnectionEvents();
|
|
1124
|
+
|
|
1125
|
+
// Initialize connection pool
|
|
1126
|
+
try {
|
|
1127
|
+
const connectionResult = await this.connectionManager.initialize();
|
|
1128
|
+
this.logger.info(`Connected to PostgreSQL: ${connectionResult.serverVersion}`);
|
|
1129
|
+
this.logger.info(`RuVector extension version: ${connectionResult.ruVectorVersion}`);
|
|
1130
|
+
|
|
1131
|
+
// Initialize vector operations
|
|
1132
|
+
this.vectorOps = new VectorOps(this.connectionManager, this.ruVectorConfig);
|
|
1133
|
+
|
|
1134
|
+
// Ensure pgvector extension is available
|
|
1135
|
+
await this.ensureExtension();
|
|
1136
|
+
|
|
1137
|
+
this.eventBus.emit('ruvector:initialized', {
|
|
1138
|
+
connectionResult,
|
|
1139
|
+
timestamp: new Date(),
|
|
1140
|
+
});
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
this.logger.error('Failed to initialize RuVector Bridge', error);
|
|
1143
|
+
throw error;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Shutdown the plugin and close database connections.
|
|
1149
|
+
*/
|
|
1150
|
+
protected async onShutdown(): Promise<void> {
|
|
1151
|
+
this.logger.info('Shutting down RuVector PostgreSQL Bridge...');
|
|
1152
|
+
|
|
1153
|
+
if (this.connectionManager) {
|
|
1154
|
+
await this.connectionManager.shutdown();
|
|
1155
|
+
this.connectionManager = null;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
this.vectorOps = null;
|
|
1159
|
+
|
|
1160
|
+
this.eventBus.emit('ruvector:shutdown', {
|
|
1161
|
+
uptime: this.getUptime(),
|
|
1162
|
+
metrics: this.metrics,
|
|
1163
|
+
timestamp: new Date(),
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Perform health check.
|
|
1169
|
+
*/
|
|
1170
|
+
protected async onHealthCheck(): Promise<Record<string, { healthy: boolean; message?: string; latencyMs?: number }>> {
|
|
1171
|
+
const checks: Record<string, { healthy: boolean; message?: string; latencyMs?: number }> = {};
|
|
1172
|
+
|
|
1173
|
+
// Check connection pool
|
|
1174
|
+
if (this.connectionManager?.isHealthy()) {
|
|
1175
|
+
const poolStats = this.connectionManager.getPoolStats();
|
|
1176
|
+
checks['connection_pool'] = {
|
|
1177
|
+
healthy: true,
|
|
1178
|
+
message: `Pool size: ${poolStats.poolSize}, available: ${poolStats.availableConnections}`,
|
|
1179
|
+
};
|
|
1180
|
+
} else {
|
|
1181
|
+
checks['connection_pool'] = {
|
|
1182
|
+
healthy: false,
|
|
1183
|
+
message: 'Connection pool not initialized or unhealthy',
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Check database connectivity with a simple query
|
|
1188
|
+
if (this.connectionManager) {
|
|
1189
|
+
const startTime = Date.now();
|
|
1190
|
+
try {
|
|
1191
|
+
await this.connectionManager.query('SELECT 1');
|
|
1192
|
+
checks['database'] = {
|
|
1193
|
+
healthy: true,
|
|
1194
|
+
message: 'Database responding',
|
|
1195
|
+
latencyMs: Date.now() - startTime,
|
|
1196
|
+
};
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
checks['database'] = {
|
|
1199
|
+
healthy: false,
|
|
1200
|
+
message: `Database error: ${(error as Error).message}`,
|
|
1201
|
+
latencyMs: Date.now() - startTime,
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Check pgvector extension
|
|
1207
|
+
if (this.connectionManager) {
|
|
1208
|
+
try {
|
|
1209
|
+
const result = await this.connectionManager.query<{ extversion: string }>(
|
|
1210
|
+
"SELECT extversion FROM pg_extension WHERE extname = 'vector'"
|
|
1211
|
+
);
|
|
1212
|
+
checks['pgvector'] = {
|
|
1213
|
+
healthy: result.rows.length > 0,
|
|
1214
|
+
message: result.rows.length > 0
|
|
1215
|
+
? `pgvector version: ${result.rows[0].extversion}`
|
|
1216
|
+
: 'pgvector extension not found',
|
|
1217
|
+
};
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
checks['pgvector'] = {
|
|
1220
|
+
healthy: false,
|
|
1221
|
+
message: `Error checking pgvector: ${(error as Error).message}`,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
return checks;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// ===========================================================================
|
|
1230
|
+
// MCP Tools Registration
|
|
1231
|
+
// ===========================================================================
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Register MCP tools for vector operations.
|
|
1235
|
+
*/
|
|
1236
|
+
override registerMCPTools(): MCPToolDefinition[] {
|
|
1237
|
+
return [
|
|
1238
|
+
// Vector Search Tool
|
|
1239
|
+
{
|
|
1240
|
+
name: 'ruvector_search',
|
|
1241
|
+
description: 'Search for similar vectors using HNSW or IVF indexing. Supports cosine, euclidean, and dot product distance metrics.',
|
|
1242
|
+
inputSchema: {
|
|
1243
|
+
type: 'object',
|
|
1244
|
+
properties: {
|
|
1245
|
+
query: {
|
|
1246
|
+
type: 'array',
|
|
1247
|
+
items: { type: 'number' },
|
|
1248
|
+
description: 'Query vector for similarity search',
|
|
1249
|
+
},
|
|
1250
|
+
k: {
|
|
1251
|
+
type: 'number',
|
|
1252
|
+
description: 'Number of nearest neighbors to return',
|
|
1253
|
+
default: 10,
|
|
1254
|
+
},
|
|
1255
|
+
metric: {
|
|
1256
|
+
type: 'string',
|
|
1257
|
+
enum: ['cosine', 'euclidean', 'dot'],
|
|
1258
|
+
description: 'Distance metric to use',
|
|
1259
|
+
default: 'cosine',
|
|
1260
|
+
},
|
|
1261
|
+
tableName: {
|
|
1262
|
+
type: 'string',
|
|
1263
|
+
description: 'Table to search in',
|
|
1264
|
+
default: 'vectors',
|
|
1265
|
+
},
|
|
1266
|
+
filter: {
|
|
1267
|
+
type: 'object',
|
|
1268
|
+
description: 'Metadata filters',
|
|
1269
|
+
},
|
|
1270
|
+
threshold: {
|
|
1271
|
+
type: 'number',
|
|
1272
|
+
description: 'Minimum similarity threshold',
|
|
1273
|
+
},
|
|
1274
|
+
includeMetadata: {
|
|
1275
|
+
type: 'boolean',
|
|
1276
|
+
description: 'Include metadata in results',
|
|
1277
|
+
default: true,
|
|
1278
|
+
},
|
|
1279
|
+
},
|
|
1280
|
+
required: ['query', 'k'],
|
|
1281
|
+
},
|
|
1282
|
+
handler: async (input): Promise<MCPToolResult> => {
|
|
1283
|
+
try {
|
|
1284
|
+
const results = await this.vectorSearch(input as unknown as VectorSearchOptions);
|
|
1285
|
+
this.metrics.searchesPerformed++;
|
|
1286
|
+
return {
|
|
1287
|
+
content: [{
|
|
1288
|
+
type: 'text',
|
|
1289
|
+
text: JSON.stringify({ success: true, results, count: results.length }, null, 2),
|
|
1290
|
+
}],
|
|
1291
|
+
};
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
return this.createErrorResult(error as Error);
|
|
1294
|
+
}
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
|
|
1298
|
+
// Vector Insert Tool
|
|
1299
|
+
{
|
|
1300
|
+
name: 'ruvector_insert',
|
|
1301
|
+
description: 'Insert vectors into a table. Supports batch insertion and upsert.',
|
|
1302
|
+
inputSchema: {
|
|
1303
|
+
type: 'object',
|
|
1304
|
+
properties: {
|
|
1305
|
+
tableName: {
|
|
1306
|
+
type: 'string',
|
|
1307
|
+
description: 'Target table name',
|
|
1308
|
+
},
|
|
1309
|
+
vectors: {
|
|
1310
|
+
type: 'array',
|
|
1311
|
+
items: {
|
|
1312
|
+
type: 'object',
|
|
1313
|
+
properties: {
|
|
1314
|
+
id: { type: 'string' },
|
|
1315
|
+
vector: { type: 'array', items: { type: 'number' } },
|
|
1316
|
+
metadata: { type: 'object' },
|
|
1317
|
+
},
|
|
1318
|
+
required: ['vector'],
|
|
1319
|
+
},
|
|
1320
|
+
description: 'Vectors to insert',
|
|
1321
|
+
},
|
|
1322
|
+
upsert: {
|
|
1323
|
+
type: 'boolean',
|
|
1324
|
+
description: 'Update on conflict',
|
|
1325
|
+
default: false,
|
|
1326
|
+
},
|
|
1327
|
+
},
|
|
1328
|
+
required: ['tableName', 'vectors'],
|
|
1329
|
+
},
|
|
1330
|
+
handler: async (input): Promise<MCPToolResult> => {
|
|
1331
|
+
try {
|
|
1332
|
+
const result = await this.vectorInsert(input as unknown as VectorInsertOptions);
|
|
1333
|
+
this.metrics.vectorsInserted += result.successful;
|
|
1334
|
+
return {
|
|
1335
|
+
content: [{
|
|
1336
|
+
type: 'text',
|
|
1337
|
+
text: JSON.stringify({ success: true, ...result }, null, 2),
|
|
1338
|
+
}],
|
|
1339
|
+
};
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
return this.createErrorResult(error as Error);
|
|
1342
|
+
}
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
|
|
1346
|
+
// Vector Update Tool
|
|
1347
|
+
{
|
|
1348
|
+
name: 'ruvector_update',
|
|
1349
|
+
description: 'Update an existing vector and/or its metadata.',
|
|
1350
|
+
inputSchema: {
|
|
1351
|
+
type: 'object',
|
|
1352
|
+
properties: {
|
|
1353
|
+
tableName: {
|
|
1354
|
+
type: 'string',
|
|
1355
|
+
description: 'Table name',
|
|
1356
|
+
},
|
|
1357
|
+
id: {
|
|
1358
|
+
oneOf: [{ type: 'string' }, { type: 'number' }],
|
|
1359
|
+
description: 'Vector ID to update',
|
|
1360
|
+
},
|
|
1361
|
+
vector: {
|
|
1362
|
+
type: 'array',
|
|
1363
|
+
items: { type: 'number' },
|
|
1364
|
+
description: 'New vector value',
|
|
1365
|
+
},
|
|
1366
|
+
metadata: {
|
|
1367
|
+
type: 'object',
|
|
1368
|
+
description: 'New or updated metadata',
|
|
1369
|
+
},
|
|
1370
|
+
mergeMetadata: {
|
|
1371
|
+
type: 'boolean',
|
|
1372
|
+
description: 'Merge with existing metadata',
|
|
1373
|
+
default: false,
|
|
1374
|
+
},
|
|
1375
|
+
},
|
|
1376
|
+
required: ['tableName', 'id'],
|
|
1377
|
+
},
|
|
1378
|
+
handler: async (input): Promise<MCPToolResult> => {
|
|
1379
|
+
try {
|
|
1380
|
+
const updated = await this.vectorUpdate(input as unknown as VectorUpdateOptions);
|
|
1381
|
+
if (updated) this.metrics.vectorsUpdated++;
|
|
1382
|
+
return {
|
|
1383
|
+
content: [{
|
|
1384
|
+
type: 'text',
|
|
1385
|
+
text: JSON.stringify({ success: true, updated }, null, 2),
|
|
1386
|
+
}],
|
|
1387
|
+
};
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
return this.createErrorResult(error as Error);
|
|
1390
|
+
}
|
|
1391
|
+
},
|
|
1392
|
+
},
|
|
1393
|
+
|
|
1394
|
+
// Vector Delete Tool
|
|
1395
|
+
{
|
|
1396
|
+
name: 'ruvector_delete',
|
|
1397
|
+
description: 'Delete vectors by ID.',
|
|
1398
|
+
inputSchema: {
|
|
1399
|
+
type: 'object',
|
|
1400
|
+
properties: {
|
|
1401
|
+
tableName: {
|
|
1402
|
+
type: 'string',
|
|
1403
|
+
description: 'Table name',
|
|
1404
|
+
},
|
|
1405
|
+
id: {
|
|
1406
|
+
oneOf: [{ type: 'string' }, { type: 'number' }],
|
|
1407
|
+
description: 'Vector ID to delete',
|
|
1408
|
+
},
|
|
1409
|
+
ids: {
|
|
1410
|
+
type: 'array',
|
|
1411
|
+
items: { oneOf: [{ type: 'string' }, { type: 'number' }] },
|
|
1412
|
+
description: 'Multiple vector IDs to delete',
|
|
1413
|
+
},
|
|
1414
|
+
},
|
|
1415
|
+
required: ['tableName'],
|
|
1416
|
+
},
|
|
1417
|
+
handler: async (input): Promise<MCPToolResult> => {
|
|
1418
|
+
try {
|
|
1419
|
+
const { tableName, id, ids } = input as { tableName: string; id?: string | number; ids?: Array<string | number> };
|
|
1420
|
+
|
|
1421
|
+
if (ids && ids.length > 0) {
|
|
1422
|
+
const result = await this.vectorBulkDelete(tableName, ids);
|
|
1423
|
+
this.metrics.vectorsDeleted += result.successful;
|
|
1424
|
+
return {
|
|
1425
|
+
content: [{
|
|
1426
|
+
type: 'text',
|
|
1427
|
+
text: JSON.stringify({ success: true, ...result }, null, 2),
|
|
1428
|
+
}],
|
|
1429
|
+
};
|
|
1430
|
+
} else if (id !== undefined) {
|
|
1431
|
+
const deleted = await this.vectorDelete(tableName, id);
|
|
1432
|
+
if (deleted) this.metrics.vectorsDeleted++;
|
|
1433
|
+
return {
|
|
1434
|
+
content: [{
|
|
1435
|
+
type: 'text',
|
|
1436
|
+
text: JSON.stringify({ success: true, deleted }, null, 2),
|
|
1437
|
+
}],
|
|
1438
|
+
};
|
|
1439
|
+
} else {
|
|
1440
|
+
return this.createErrorResult(new Error('Either id or ids must be provided'));
|
|
1441
|
+
}
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
return this.createErrorResult(error as Error);
|
|
1444
|
+
}
|
|
1445
|
+
},
|
|
1446
|
+
},
|
|
1447
|
+
|
|
1448
|
+
// Index Create Tool
|
|
1449
|
+
{
|
|
1450
|
+
name: 'ruvector_create_index',
|
|
1451
|
+
description: 'Create a vector index (HNSW or IVF) for faster similarity search.',
|
|
1452
|
+
inputSchema: {
|
|
1453
|
+
type: 'object',
|
|
1454
|
+
properties: {
|
|
1455
|
+
tableName: {
|
|
1456
|
+
type: 'string',
|
|
1457
|
+
description: 'Table name',
|
|
1458
|
+
},
|
|
1459
|
+
columnName: {
|
|
1460
|
+
type: 'string',
|
|
1461
|
+
description: 'Vector column name',
|
|
1462
|
+
default: 'embedding',
|
|
1463
|
+
},
|
|
1464
|
+
indexType: {
|
|
1465
|
+
type: 'string',
|
|
1466
|
+
enum: ['hnsw', 'ivfflat'],
|
|
1467
|
+
description: 'Index type',
|
|
1468
|
+
default: 'hnsw',
|
|
1469
|
+
},
|
|
1470
|
+
metric: {
|
|
1471
|
+
type: 'string',
|
|
1472
|
+
enum: ['cosine', 'euclidean', 'dot'],
|
|
1473
|
+
description: 'Distance metric',
|
|
1474
|
+
default: 'cosine',
|
|
1475
|
+
},
|
|
1476
|
+
m: {
|
|
1477
|
+
type: 'number',
|
|
1478
|
+
description: 'HNSW M parameter (max connections per layer)',
|
|
1479
|
+
default: 16,
|
|
1480
|
+
},
|
|
1481
|
+
efConstruction: {
|
|
1482
|
+
type: 'number',
|
|
1483
|
+
description: 'HNSW ef_construction parameter',
|
|
1484
|
+
default: 200,
|
|
1485
|
+
},
|
|
1486
|
+
lists: {
|
|
1487
|
+
type: 'number',
|
|
1488
|
+
description: 'IVF lists parameter',
|
|
1489
|
+
},
|
|
1490
|
+
concurrent: {
|
|
1491
|
+
type: 'boolean',
|
|
1492
|
+
description: 'Create index concurrently (non-blocking)',
|
|
1493
|
+
default: true,
|
|
1494
|
+
},
|
|
1495
|
+
},
|
|
1496
|
+
required: ['tableName', 'columnName', 'indexType'],
|
|
1497
|
+
},
|
|
1498
|
+
handler: async (input): Promise<MCPToolResult> => {
|
|
1499
|
+
try {
|
|
1500
|
+
await this.createIndex(input as unknown as VectorIndexOptions);
|
|
1501
|
+
return {
|
|
1502
|
+
content: [{
|
|
1503
|
+
type: 'text',
|
|
1504
|
+
text: JSON.stringify({ success: true, message: 'Index created successfully' }, null, 2),
|
|
1505
|
+
}],
|
|
1506
|
+
};
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
return this.createErrorResult(error as Error);
|
|
1509
|
+
}
|
|
1510
|
+
},
|
|
1511
|
+
},
|
|
1512
|
+
|
|
1513
|
+
// Index Stats Tool
|
|
1514
|
+
{
|
|
1515
|
+
name: 'ruvector_index_stats',
|
|
1516
|
+
description: 'Get statistics for vector indices.',
|
|
1517
|
+
inputSchema: {
|
|
1518
|
+
type: 'object',
|
|
1519
|
+
properties: {
|
|
1520
|
+
indexName: {
|
|
1521
|
+
type: 'string',
|
|
1522
|
+
description: 'Specific index name (optional)',
|
|
1523
|
+
},
|
|
1524
|
+
tableName: {
|
|
1525
|
+
type: 'string',
|
|
1526
|
+
description: 'Filter by table name (optional)',
|
|
1527
|
+
},
|
|
1528
|
+
},
|
|
1529
|
+
},
|
|
1530
|
+
handler: async (input): Promise<MCPToolResult> => {
|
|
1531
|
+
try {
|
|
1532
|
+
const { indexName, tableName } = input as { indexName?: string; tableName?: string };
|
|
1533
|
+
|
|
1534
|
+
if (indexName) {
|
|
1535
|
+
const stats = await this.getIndexStats(indexName);
|
|
1536
|
+
return {
|
|
1537
|
+
content: [{
|
|
1538
|
+
type: 'text',
|
|
1539
|
+
text: JSON.stringify({ success: true, stats }, null, 2),
|
|
1540
|
+
}],
|
|
1541
|
+
};
|
|
1542
|
+
} else {
|
|
1543
|
+
const indices = await this.listIndices(tableName);
|
|
1544
|
+
return {
|
|
1545
|
+
content: [{
|
|
1546
|
+
type: 'text',
|
|
1547
|
+
text: JSON.stringify({ success: true, indices, count: indices.length }, null, 2),
|
|
1548
|
+
}],
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
} catch (error) {
|
|
1552
|
+
return this.createErrorResult(error as Error);
|
|
1553
|
+
}
|
|
1554
|
+
},
|
|
1555
|
+
},
|
|
1556
|
+
|
|
1557
|
+
// Health Check Tool
|
|
1558
|
+
{
|
|
1559
|
+
name: 'ruvector_health',
|
|
1560
|
+
description: 'Check the health status of the RuVector connection and database.',
|
|
1561
|
+
inputSchema: {
|
|
1562
|
+
type: 'object',
|
|
1563
|
+
properties: {},
|
|
1564
|
+
},
|
|
1565
|
+
handler: async (): Promise<MCPToolResult> => {
|
|
1566
|
+
try {
|
|
1567
|
+
const health = await this.healthCheck();
|
|
1568
|
+
return {
|
|
1569
|
+
content: [{
|
|
1570
|
+
type: 'text',
|
|
1571
|
+
text: JSON.stringify({ success: true, health }, null, 2),
|
|
1572
|
+
}],
|
|
1573
|
+
};
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
return this.createErrorResult(error as Error);
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1578
|
+
},
|
|
1579
|
+
|
|
1580
|
+
// Metrics Tool
|
|
1581
|
+
{
|
|
1582
|
+
name: 'ruvector_metrics',
|
|
1583
|
+
description: 'Get performance metrics and statistics.',
|
|
1584
|
+
inputSchema: {
|
|
1585
|
+
type: 'object',
|
|
1586
|
+
properties: {},
|
|
1587
|
+
},
|
|
1588
|
+
handler: async (): Promise<MCPToolResult> => {
|
|
1589
|
+
try {
|
|
1590
|
+
const stats = await this.getStats();
|
|
1591
|
+
return {
|
|
1592
|
+
content: [{
|
|
1593
|
+
type: 'text',
|
|
1594
|
+
text: JSON.stringify({ success: true, metrics: this.metrics, stats }, null, 2),
|
|
1595
|
+
}],
|
|
1596
|
+
};
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
return this.createErrorResult(error as Error);
|
|
1599
|
+
}
|
|
1600
|
+
},
|
|
1601
|
+
},
|
|
1602
|
+
];
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// ===========================================================================
|
|
1606
|
+
// Public Vector Operation Methods
|
|
1607
|
+
// ===========================================================================
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* Perform vector similarity search.
|
|
1611
|
+
*/
|
|
1612
|
+
async vectorSearch(options: VectorSearchOptions): Promise<VectorSearchResult[]> {
|
|
1613
|
+
this.ensureInitialized();
|
|
1614
|
+
const startTime = Date.now();
|
|
1615
|
+
|
|
1616
|
+
try {
|
|
1617
|
+
const results = await this.vectorOps!.search(options);
|
|
1618
|
+
this.updateQueryMetrics(true, Date.now() - startTime);
|
|
1619
|
+
|
|
1620
|
+
this.emit('search:complete', {
|
|
1621
|
+
searchId: `search-${Date.now()}`,
|
|
1622
|
+
durationMs: Date.now() - startTime,
|
|
1623
|
+
resultCount: results.length,
|
|
1624
|
+
scannedCount: results.length,
|
|
1625
|
+
cacheHit: false,
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
return results;
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
this.updateQueryMetrics(false, Date.now() - startTime);
|
|
1631
|
+
throw error;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* Perform batch vector search.
|
|
1637
|
+
*/
|
|
1638
|
+
async vectorBatchSearch(options: BatchVectorOptions): Promise<BulkSearchResult> {
|
|
1639
|
+
this.ensureInitialized();
|
|
1640
|
+
return this.vectorOps!.batchSearch(options);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Insert vectors.
|
|
1645
|
+
*/
|
|
1646
|
+
async vectorInsert(options: VectorInsertOptions): Promise<BatchResult<string>> {
|
|
1647
|
+
this.ensureInitialized();
|
|
1648
|
+
const result = await this.vectorOps!.insert(options);
|
|
1649
|
+
|
|
1650
|
+
this.emit('vector:batch_complete', {
|
|
1651
|
+
tableName: options.tableName,
|
|
1652
|
+
count: result.total,
|
|
1653
|
+
durationMs: result.durationMs,
|
|
1654
|
+
successCount: result.successful,
|
|
1655
|
+
failedCount: result.failed,
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
return result;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Update a vector.
|
|
1663
|
+
*/
|
|
1664
|
+
async vectorUpdate(options: VectorUpdateOptions): Promise<boolean> {
|
|
1665
|
+
this.ensureInitialized();
|
|
1666
|
+
const updated = await this.vectorOps!.update(options);
|
|
1667
|
+
|
|
1668
|
+
if (updated) {
|
|
1669
|
+
this.emit('vector:updated', {
|
|
1670
|
+
tableName: options.tableName,
|
|
1671
|
+
vectorId: options.id,
|
|
1672
|
+
dimensions: options.vector?.length ?? 0,
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
return updated;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* Delete a vector.
|
|
1681
|
+
*/
|
|
1682
|
+
async vectorDelete(tableName: string, id: string | number): Promise<boolean> {
|
|
1683
|
+
this.ensureInitialized();
|
|
1684
|
+
const deleted = await this.vectorOps!.delete(tableName, id);
|
|
1685
|
+
|
|
1686
|
+
if (deleted) {
|
|
1687
|
+
this.emit('vector:deleted', {
|
|
1688
|
+
tableName,
|
|
1689
|
+
vectorId: id,
|
|
1690
|
+
dimensions: 0,
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
return deleted;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Bulk delete vectors.
|
|
1699
|
+
*/
|
|
1700
|
+
async vectorBulkDelete(tableName: string, ids: Array<string | number>): Promise<BatchResult> {
|
|
1701
|
+
this.ensureInitialized();
|
|
1702
|
+
return this.vectorOps!.bulkDelete(tableName, ids);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* Create a vector index.
|
|
1707
|
+
*/
|
|
1708
|
+
async createIndex(options: VectorIndexOptions): Promise<void> {
|
|
1709
|
+
this.ensureInitialized();
|
|
1710
|
+
await this.vectorOps!.createIndex(options);
|
|
1711
|
+
|
|
1712
|
+
this.emit('index:created', {
|
|
1713
|
+
indexName: options.indexName ?? `idx_${options.tableName}_${options.columnName}`,
|
|
1714
|
+
tableName: options.tableName,
|
|
1715
|
+
columnName: options.columnName,
|
|
1716
|
+
indexType: options.indexType,
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
/**
|
|
1721
|
+
* Drop an index.
|
|
1722
|
+
*/
|
|
1723
|
+
async dropIndex(indexName: string): Promise<void> {
|
|
1724
|
+
this.ensureInitialized();
|
|
1725
|
+
await this.vectorOps!.dropIndex(indexName);
|
|
1726
|
+
|
|
1727
|
+
this.emit('index:dropped', {
|
|
1728
|
+
indexName,
|
|
1729
|
+
tableName: '',
|
|
1730
|
+
columnName: '',
|
|
1731
|
+
indexType: 'hnsw' as VectorIndexType,
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
/**
|
|
1736
|
+
* Rebuild an index.
|
|
1737
|
+
*/
|
|
1738
|
+
async rebuildIndex(indexName: string): Promise<void> {
|
|
1739
|
+
this.ensureInitialized();
|
|
1740
|
+
await this.vectorOps!.rebuildIndex(indexName);
|
|
1741
|
+
|
|
1742
|
+
this.emit('index:rebuilt', {
|
|
1743
|
+
indexName,
|
|
1744
|
+
tableName: '',
|
|
1745
|
+
columnName: '',
|
|
1746
|
+
indexType: 'hnsw' as VectorIndexType,
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* Get index statistics.
|
|
1752
|
+
*/
|
|
1753
|
+
async getIndexStats(indexName: string): Promise<IndexStats> {
|
|
1754
|
+
this.ensureInitialized();
|
|
1755
|
+
return this.vectorOps!.getIndexStats(indexName);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* List all indices.
|
|
1760
|
+
*/
|
|
1761
|
+
async listIndices(tableName?: string): Promise<IndexStats[]> {
|
|
1762
|
+
this.ensureInitialized();
|
|
1763
|
+
return this.vectorOps!.listIndices(tableName);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
/**
|
|
1767
|
+
* Get RuVector statistics.
|
|
1768
|
+
*/
|
|
1769
|
+
async getStats(): Promise<RuVectorStats> {
|
|
1770
|
+
this.ensureInitialized();
|
|
1771
|
+
|
|
1772
|
+
const poolStats = this.connectionManager!.getPoolStats();
|
|
1773
|
+
|
|
1774
|
+
// Query for vector statistics
|
|
1775
|
+
const result = await this.connectionManager!.query<{
|
|
1776
|
+
table_count: number;
|
|
1777
|
+
total_vectors: number;
|
|
1778
|
+
total_size: number;
|
|
1779
|
+
index_count: number;
|
|
1780
|
+
}>(`
|
|
1781
|
+
SELECT
|
|
1782
|
+
COUNT(DISTINCT c.relname) as table_count,
|
|
1783
|
+
COALESCE(SUM(c.reltuples), 0)::bigint as total_vectors,
|
|
1784
|
+
COALESCE(SUM(pg_total_relation_size(c.oid)), 0)::bigint as total_size,
|
|
1785
|
+
COUNT(DISTINCT i.indexrelid) as index_count
|
|
1786
|
+
FROM pg_class c
|
|
1787
|
+
JOIN pg_attribute a ON a.attrelid = c.oid
|
|
1788
|
+
LEFT JOIN pg_index i ON i.indrelid = c.oid
|
|
1789
|
+
WHERE a.atttypid = 'vector'::regtype
|
|
1790
|
+
AND c.relkind = 'r'
|
|
1791
|
+
`);
|
|
1792
|
+
|
|
1793
|
+
const stats = result.rows[0] ?? {
|
|
1794
|
+
table_count: 0,
|
|
1795
|
+
total_vectors: 0,
|
|
1796
|
+
total_size: 0,
|
|
1797
|
+
index_count: 0,
|
|
1798
|
+
};
|
|
1799
|
+
|
|
1800
|
+
return {
|
|
1801
|
+
version: PLUGIN_VERSION,
|
|
1802
|
+
totalVectors: Number(stats.total_vectors),
|
|
1803
|
+
totalSizeBytes: Number(stats.total_size),
|
|
1804
|
+
numIndices: Number(stats.index_count),
|
|
1805
|
+
numTables: Number(stats.table_count),
|
|
1806
|
+
queryStats: {
|
|
1807
|
+
totalQueries: this.metrics.queriesTotal,
|
|
1808
|
+
avgQueryTimeMs: this.metrics.avgQueryTimeMs,
|
|
1809
|
+
p95QueryTimeMs: this.metrics.avgQueryTimeMs * 1.5, // Approximation
|
|
1810
|
+
p99QueryTimeMs: this.metrics.avgQueryTimeMs * 2, // Approximation
|
|
1811
|
+
cacheHitRate: this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses) || 0,
|
|
1812
|
+
},
|
|
1813
|
+
memoryStats: {
|
|
1814
|
+
usedBytes: 0, // Would need OS-level access
|
|
1815
|
+
peakBytes: 0,
|
|
1816
|
+
indexBytes: Number(stats.total_size),
|
|
1817
|
+
cacheBytes: 0,
|
|
1818
|
+
},
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// ===========================================================================
|
|
1823
|
+
// Private Helper Methods
|
|
1824
|
+
// ===========================================================================
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* Ensure the plugin is initialized.
|
|
1828
|
+
*/
|
|
1829
|
+
private ensureInitialized(): void {
|
|
1830
|
+
if (!this.vectorOps || !this.connectionManager) {
|
|
1831
|
+
throw new Error('RuVector Bridge not initialized. Call initialize() first.');
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Ensure pgvector extension is installed.
|
|
1837
|
+
*/
|
|
1838
|
+
private async ensureExtension(): Promise<void> {
|
|
1839
|
+
try {
|
|
1840
|
+
await this.connectionManager!.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
1841
|
+
this.logger.debug('pgvector extension ensured');
|
|
1842
|
+
} catch (error) {
|
|
1843
|
+
this.logger.warn('Could not create pgvector extension (may require superuser privileges)', error);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/**
|
|
1848
|
+
* Forward connection manager events to plugin event bus.
|
|
1849
|
+
*/
|
|
1850
|
+
private forwardConnectionEvents(): void {
|
|
1851
|
+
const events: RuVectorEventType[] = [
|
|
1852
|
+
'connection:open',
|
|
1853
|
+
'connection:close',
|
|
1854
|
+
'connection:error',
|
|
1855
|
+
'connection:pool_acquired',
|
|
1856
|
+
'connection:pool_released',
|
|
1857
|
+
'query:start',
|
|
1858
|
+
'query:complete',
|
|
1859
|
+
'query:error',
|
|
1860
|
+
'query:slow',
|
|
1861
|
+
];
|
|
1862
|
+
|
|
1863
|
+
for (const event of events) {
|
|
1864
|
+
this.connectionManager!.on(event, (data) => {
|
|
1865
|
+
this.eventBus.emit(`ruvector:${event}`, data);
|
|
1866
|
+
this.emit(event, data);
|
|
1867
|
+
this.updateMetricsFromEvent(event, data);
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/**
|
|
1873
|
+
* Update metrics from events.
|
|
1874
|
+
*/
|
|
1875
|
+
private updateMetricsFromEvent(event: string, _data: unknown): void {
|
|
1876
|
+
switch (event) {
|
|
1877
|
+
case 'connection:pool_acquired':
|
|
1878
|
+
this.metrics.connectionAcquires++;
|
|
1879
|
+
break;
|
|
1880
|
+
case 'connection:pool_released':
|
|
1881
|
+
this.metrics.connectionReleases++;
|
|
1882
|
+
break;
|
|
1883
|
+
case 'connection:error':
|
|
1884
|
+
this.metrics.connectionErrors++;
|
|
1885
|
+
break;
|
|
1886
|
+
case 'query:slow':
|
|
1887
|
+
this.metrics.slowQueries++;
|
|
1888
|
+
break;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* Update query metrics.
|
|
1894
|
+
*/
|
|
1895
|
+
private updateQueryMetrics(success: boolean, durationMs: number): void {
|
|
1896
|
+
this.metrics.queriesTotal++;
|
|
1897
|
+
if (success) {
|
|
1898
|
+
this.metrics.queriesSucceeded++;
|
|
1899
|
+
} else {
|
|
1900
|
+
this.metrics.queriesFailed++;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// Update running average
|
|
1904
|
+
const prevAvg = this.metrics.avgQueryTimeMs;
|
|
1905
|
+
const n = this.metrics.queriesTotal;
|
|
1906
|
+
this.metrics.avgQueryTimeMs = prevAvg + (durationMs - prevAvg) / n;
|
|
1907
|
+
this.metrics.lastQueryTime = durationMs;
|
|
1908
|
+
|
|
1909
|
+
if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
|
|
1910
|
+
this.metrics.slowQueries++;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* Create initial metrics object.
|
|
1916
|
+
*/
|
|
1917
|
+
private createInitialMetrics(): RuVectorMetrics {
|
|
1918
|
+
return {
|
|
1919
|
+
queriesTotal: 0,
|
|
1920
|
+
queriesSucceeded: 0,
|
|
1921
|
+
queriesFailed: 0,
|
|
1922
|
+
slowQueries: 0,
|
|
1923
|
+
avgQueryTimeMs: 0,
|
|
1924
|
+
vectorsInserted: 0,
|
|
1925
|
+
vectorsUpdated: 0,
|
|
1926
|
+
vectorsDeleted: 0,
|
|
1927
|
+
searchesPerformed: 0,
|
|
1928
|
+
cacheHits: 0,
|
|
1929
|
+
cacheMisses: 0,
|
|
1930
|
+
connectionAcquires: 0,
|
|
1931
|
+
connectionReleases: 0,
|
|
1932
|
+
connectionErrors: 0,
|
|
1933
|
+
lastQueryTime: 0,
|
|
1934
|
+
uptime: 0,
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
/**
|
|
1939
|
+
* Create error result for MCP tools.
|
|
1940
|
+
*/
|
|
1941
|
+
private createErrorResult(error: Error): MCPToolResult {
|
|
1942
|
+
return {
|
|
1943
|
+
content: [{
|
|
1944
|
+
type: 'text',
|
|
1945
|
+
text: JSON.stringify({
|
|
1946
|
+
success: false,
|
|
1947
|
+
error: error.message,
|
|
1948
|
+
code: (error as { code?: string }).code,
|
|
1949
|
+
}, null, 2),
|
|
1950
|
+
}],
|
|
1951
|
+
isError: true,
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
/**
|
|
1956
|
+
* Get plugin uptime.
|
|
1957
|
+
*/
|
|
1958
|
+
override getUptime(): number {
|
|
1959
|
+
if (!this.initTime) return 0;
|
|
1960
|
+
return Date.now() - this.initTime.getTime();
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
/**
|
|
1964
|
+
* Get current metrics.
|
|
1965
|
+
*/
|
|
1966
|
+
getMetrics(): RuVectorMetrics {
|
|
1967
|
+
return {
|
|
1968
|
+
...this.metrics,
|
|
1969
|
+
uptime: this.getUptime(),
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// ============================================================================
|
|
1975
|
+
// Factory Function
|
|
1976
|
+
// ============================================================================
|
|
1977
|
+
|
|
1978
|
+
/**
|
|
1979
|
+
* Create a new RuVector Bridge plugin instance.
|
|
1980
|
+
*
|
|
1981
|
+
* @example
|
|
1982
|
+
* ```typescript
|
|
1983
|
+
* const bridge = createRuVectorBridge({
|
|
1984
|
+
* host: 'localhost',
|
|
1985
|
+
* port: 5432,
|
|
1986
|
+
* database: 'vectors',
|
|
1987
|
+
* user: 'postgres',
|
|
1988
|
+
* password: 'password',
|
|
1989
|
+
* });
|
|
1990
|
+
* ```
|
|
1991
|
+
*/
|
|
1992
|
+
export function createRuVectorBridge(config: RuVectorConfig): RuVectorBridge {
|
|
1993
|
+
return new RuVectorBridge(config);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// ============================================================================
|
|
1997
|
+
// Default Export
|
|
1998
|
+
// ============================================================================
|
|
1999
|
+
|
|
2000
|
+
export default RuVectorBridge;
|