doc2vec 1.1.1 → 1.2.0

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.
@@ -0,0 +1,507 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.DatabaseManager = void 0;
40
+ const path = __importStar(require("path"));
41
+ const crypto_1 = __importDefault(require("crypto"));
42
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
43
+ const sqliteVec = __importStar(require("sqlite-vec"));
44
+ const js_client_rest_1 = require("@qdrant/js-client-rest");
45
+ const utils_1 = require("./utils");
46
+ class DatabaseManager {
47
+ static async initDatabase(config, parentLogger) {
48
+ const logger = parentLogger.child('database');
49
+ const dbConfig = config.database_config;
50
+ if (dbConfig.type === 'sqlite') {
51
+ const params = dbConfig.params;
52
+ const dbPath = params.db_path || path.join(process.cwd(), `${config.product_name.replace(/\s+/g, '_')}-${config.version}.db`);
53
+ logger.info(`Opening SQLite database at ${dbPath}`);
54
+ const db = new better_sqlite3_1.default(dbPath, { allowExtension: true });
55
+ sqliteVec.load(db);
56
+ logger.debug(`Creating vec_items table if it doesn't exist`);
57
+ db.exec(`
58
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_items USING vec0(
59
+ embedding FLOAT[3072],
60
+ product_name TEXT,
61
+ version TEXT,
62
+ heading_hierarchy TEXT,
63
+ section TEXT,
64
+ chunk_id TEXT UNIQUE,
65
+ content TEXT,
66
+ url TEXT,
67
+ hash TEXT
68
+ );
69
+ `);
70
+ logger.info(`SQLite database initialized successfully`);
71
+ return { db, type: 'sqlite' };
72
+ }
73
+ else if (dbConfig.type === 'qdrant') {
74
+ const params = dbConfig.params;
75
+ const qdrantUrl = params.qdrant_url || 'http://localhost:6333';
76
+ const qdrantPort = params.qdrant_port || 443;
77
+ const collectionName = params.collection_name || `${config.product_name.toLowerCase().replace(/\s+/g, '_')}_${config.version}`;
78
+ logger.info(`Connecting to Qdrant at ${qdrantUrl}:${qdrantPort}, collection: ${collectionName}`);
79
+ const qdrantClient = new js_client_rest_1.QdrantClient({ url: qdrantUrl, apiKey: process.env.QDRANT_API_KEY, port: qdrantPort });
80
+ await this.createCollectionQdrant(qdrantClient, collectionName, logger);
81
+ logger.info(`Qdrant connection established successfully`);
82
+ return { client: qdrantClient, collectionName, type: 'qdrant' };
83
+ }
84
+ else {
85
+ const errMsg = `Unsupported database type: ${dbConfig.type}`;
86
+ logger.error(errMsg);
87
+ throw new Error(errMsg);
88
+ }
89
+ }
90
+ static async createCollectionQdrant(qdrantClient, collectionName, logger) {
91
+ try {
92
+ logger.debug(`Checking if collection ${collectionName} exists`);
93
+ const collections = await qdrantClient.getCollections();
94
+ const collectionExists = collections.collections.some((collection) => collection.name === collectionName);
95
+ if (collectionExists) {
96
+ logger.info(`Collection ${collectionName} already exists`);
97
+ return;
98
+ }
99
+ logger.info(`Creating new collection ${collectionName}`);
100
+ await qdrantClient.createCollection(collectionName, {
101
+ vectors: {
102
+ size: 3072,
103
+ distance: "Cosine",
104
+ },
105
+ });
106
+ logger.info(`Collection ${collectionName} created successfully`);
107
+ }
108
+ catch (error) {
109
+ if (error instanceof Error) {
110
+ const errorMsg = error.message.toLowerCase();
111
+ const errorString = JSON.stringify(error).toLowerCase();
112
+ if (errorMsg.includes("already exists") ||
113
+ errorString.includes("already exists") ||
114
+ error?.status === 409 ||
115
+ errorString.includes("conflict")) {
116
+ logger.info(`Collection ${collectionName} already exists (from error response)`);
117
+ return;
118
+ }
119
+ }
120
+ logger.error(`Error creating Qdrant collection:`, error);
121
+ logger.warn(`Continuing with existing collection...`);
122
+ }
123
+ }
124
+ static async initDatabaseMetadata(dbConnection, logger) {
125
+ if (dbConnection.type === 'sqlite') {
126
+ const db = dbConnection.db;
127
+ logger.debug('Creating metadata table if it doesn\'t exist');
128
+ db.exec(`
129
+ CREATE TABLE IF NOT EXISTS vec_metadata (
130
+ key TEXT PRIMARY KEY,
131
+ value TEXT
132
+ );
133
+ `);
134
+ logger.info('SQLite metadata table initialized');
135
+ }
136
+ else if (dbConnection.type === 'qdrant') {
137
+ // For Qdrant, we'll use the same collection but verify it exists
138
+ logger.info(`Using existing Qdrant collection for metadata: ${dbConnection.collectionName}`);
139
+ // Nothing special to initialize as we'll use the same collection
140
+ }
141
+ }
142
+ static async getLastRunDate(dbConnection, repo, defaultDate, logger) {
143
+ const metadataKey = `last_run_${repo.replace('/', '_')}`;
144
+ try {
145
+ if (dbConnection.type === 'sqlite') {
146
+ const stmt = dbConnection.db.prepare('SELECT value FROM vec_metadata WHERE key = ?');
147
+ const result = stmt.get(metadataKey);
148
+ if (result) {
149
+ logger.info(`Retrieved last run date for ${repo}: ${result.value}`);
150
+ return result.value;
151
+ }
152
+ }
153
+ else if (dbConnection.type === 'qdrant') {
154
+ // Generate a UUID for this repo's metadata
155
+ const metadataUUID = utils_1.Utils.generateMetadataUUID(repo);
156
+ logger.debug(`Looking up metadata with UUID: ${metadataUUID}`);
157
+ try {
158
+ // Try to retrieve the metadata point for this repo
159
+ const response = await dbConnection.client.retrieve(dbConnection.collectionName, {
160
+ ids: [metadataUUID],
161
+ with_payload: true,
162
+ with_vector: false
163
+ });
164
+ if (response.length > 0 && response[0].payload?.metadata_value) {
165
+ const lastRunDate = response[0].payload.metadata_value;
166
+ logger.info(`Retrieved last run date for ${repo}: ${lastRunDate}`);
167
+ return lastRunDate;
168
+ }
169
+ }
170
+ catch (error) {
171
+ logger.warn(`Failed to retrieve metadata for ${repo}:`, error);
172
+ }
173
+ }
174
+ }
175
+ catch (error) {
176
+ logger.warn(`Error retrieving last run date:`, error);
177
+ }
178
+ logger.info(`No saved run date found for ${repo}, using default: ${defaultDate}`);
179
+ return defaultDate;
180
+ }
181
+ static async updateLastRunDate(dbConnection, repo, logger) {
182
+ const now = new Date().toISOString();
183
+ try {
184
+ if (dbConnection.type === 'sqlite') {
185
+ const metadataKey = `last_run_${repo.replace('/', '_')}`;
186
+ const stmt = dbConnection.db.prepare(`
187
+ INSERT INTO vec_metadata (key, value) VALUES (?, ?)
188
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
189
+ `);
190
+ stmt.run(metadataKey, now);
191
+ logger.info(`Updated last run date for ${repo} to ${now}`);
192
+ }
193
+ else if (dbConnection.type === 'qdrant') {
194
+ // Generate UUID for this repo's metadata
195
+ const metadataUUID = utils_1.Utils.generateMetadataUUID(repo);
196
+ const metadataKey = `last_run_${repo.replace('/', '_')}`;
197
+ logger.debug(`Using UUID: ${metadataUUID} for metadata`);
198
+ // Generate a dummy embedding (all zeros)
199
+ const dummyEmbeddingSize = 3072; // Same size as your content embeddings
200
+ const dummyEmbedding = new Array(dummyEmbeddingSize).fill(0);
201
+ // Create a point with special metadata payload
202
+ const metadataPoint = {
203
+ id: metadataUUID,
204
+ vector: dummyEmbedding,
205
+ payload: {
206
+ metadata_key: metadataKey,
207
+ metadata_value: now,
208
+ is_metadata: true, // Flag to identify metadata points
209
+ content: `Metadata: Last run date for ${repo}`,
210
+ product_name: 'system',
211
+ version: 'metadata',
212
+ url: 'metadata://' + repo
213
+ }
214
+ };
215
+ await dbConnection.client.upsert(dbConnection.collectionName, {
216
+ wait: true,
217
+ points: [metadataPoint]
218
+ });
219
+ logger.info(`Updated last run date for ${repo} to ${now}`);
220
+ }
221
+ }
222
+ catch (error) {
223
+ logger.error(`Failed to update last run date for ${repo}:`, error);
224
+ }
225
+ }
226
+ static prepareSQLiteStatements(db) {
227
+ return {
228
+ insertStmt: db.prepare(`
229
+ INSERT INTO vec_items (embedding, product_name, version, heading_hierarchy, section, chunk_id, content, url, hash)
230
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
231
+ `),
232
+ checkHashStmt: db.prepare(`SELECT hash FROM vec_items WHERE chunk_id = ?`),
233
+ updateStmt: db.prepare(`
234
+ UPDATE vec_items SET embedding = ?, product_name = ?, version = ?, heading_hierarchy = ?, section = ?, content = ?, url = ?, hash = ?
235
+ WHERE chunk_id = ?
236
+ `),
237
+ getAllChunkIdsStmt: db.prepare(`SELECT chunk_id FROM vec_items`),
238
+ deleteChunkStmt: db.prepare(`DELETE FROM vec_items WHERE chunk_id = ?`)
239
+ };
240
+ }
241
+ static insertVectorsSQLite(db, chunk, embedding, logger, chunkHash) {
242
+ const { insertStmt, updateStmt } = this.prepareSQLiteStatements(db);
243
+ const hash = chunkHash || utils_1.Utils.generateHash(chunk.content);
244
+ const transaction = db.transaction(() => {
245
+ const params = [
246
+ new Float32Array(embedding),
247
+ chunk.metadata.product_name,
248
+ chunk.metadata.version,
249
+ JSON.stringify(chunk.metadata.heading_hierarchy),
250
+ chunk.metadata.section,
251
+ chunk.metadata.chunk_id,
252
+ chunk.content,
253
+ chunk.metadata.url,
254
+ hash
255
+ ];
256
+ try {
257
+ insertStmt.run(params);
258
+ }
259
+ catch (error) {
260
+ updateStmt.run([...params.slice(0, 8), chunk.metadata.chunk_id]);
261
+ }
262
+ });
263
+ transaction();
264
+ }
265
+ static async storeChunkInQdrant(db, chunk, embedding, chunkHash) {
266
+ const { client, collectionName } = db;
267
+ try {
268
+ let pointId;
269
+ try {
270
+ pointId = chunk.metadata.chunk_id;
271
+ if (!utils_1.Utils.isValidUuid(pointId)) {
272
+ pointId = utils_1.Utils.hashToUuid(chunk.metadata.chunk_id);
273
+ }
274
+ }
275
+ catch (e) {
276
+ pointId = crypto_1.default.randomUUID();
277
+ }
278
+ const hash = chunkHash || utils_1.Utils.generateHash(chunk.content);
279
+ const pointItem = {
280
+ id: pointId,
281
+ vector: embedding,
282
+ payload: {
283
+ content: chunk.content,
284
+ product_name: chunk.metadata.product_name,
285
+ version: chunk.metadata.version,
286
+ heading_hierarchy: chunk.metadata.heading_hierarchy,
287
+ section: chunk.metadata.section,
288
+ url: chunk.metadata.url,
289
+ hash: hash,
290
+ original_chunk_id: chunk.metadata.chunk_id,
291
+ },
292
+ };
293
+ await client.upsert(collectionName, {
294
+ wait: true,
295
+ points: [pointItem],
296
+ });
297
+ }
298
+ catch (error) {
299
+ console.error("Error storing chunk in Qdrant:", error);
300
+ }
301
+ }
302
+ static removeObsoleteChunksSQLite(db, visitedUrls, urlPrefix, logger) {
303
+ const getChunksForUrlStmt = db.prepare(`
304
+ SELECT chunk_id, url FROM vec_items
305
+ WHERE url LIKE ? || '%'
306
+ `);
307
+ const deleteChunkStmt = db.prepare(`DELETE FROM vec_items WHERE chunk_id = ?`);
308
+ const existingChunks = getChunksForUrlStmt.all(urlPrefix);
309
+ let deletedCount = 0;
310
+ const transaction = db.transaction(() => {
311
+ for (const { chunk_id, url } of existingChunks) {
312
+ if (!visitedUrls.has(url)) {
313
+ logger.debug(`Deleting obsolete chunk from SQLite: ${chunk_id.substring(0, 8)}... (URL not visited)`);
314
+ deleteChunkStmt.run(chunk_id);
315
+ deletedCount++;
316
+ }
317
+ }
318
+ });
319
+ transaction();
320
+ logger.info(`Deleted ${deletedCount} obsolete chunks from SQLite for URL ${urlPrefix}`);
321
+ }
322
+ static async removeObsoleteChunksQdrant(db, visitedUrls, urlPrefix, logger) {
323
+ const { client, collectionName } = db;
324
+ try {
325
+ // Get all points that match the URL prefix but are not metadata points
326
+ const response = await client.scroll(collectionName, {
327
+ limit: 10000,
328
+ with_payload: true,
329
+ with_vector: false,
330
+ filter: {
331
+ must: [
332
+ {
333
+ key: "url",
334
+ match: {
335
+ text: urlPrefix
336
+ }
337
+ }
338
+ ],
339
+ must_not: [
340
+ {
341
+ key: "is_metadata",
342
+ match: {
343
+ value: true
344
+ }
345
+ }
346
+ ]
347
+ }
348
+ });
349
+ const obsoletePointIds = response.points
350
+ .filter((point) => {
351
+ const url = point.payload?.url;
352
+ // Double check it's not a metadata record
353
+ if (point.payload?.is_metadata === true) {
354
+ return false;
355
+ }
356
+ return url && !visitedUrls.has(url);
357
+ })
358
+ .map((point) => point.id);
359
+ if (obsoletePointIds.length > 0) {
360
+ await client.delete(collectionName, {
361
+ points: obsoletePointIds,
362
+ });
363
+ logger.info(`Deleted ${obsoletePointIds.length} obsolete chunks from Qdrant for URL ${urlPrefix}`);
364
+ }
365
+ else {
366
+ logger.info(`No obsolete chunks to delete from Qdrant for URL ${urlPrefix}`);
367
+ }
368
+ }
369
+ catch (error) {
370
+ logger.error(`Error removing obsolete chunks from Qdrant:`, error);
371
+ }
372
+ }
373
+ static removeObsoleteFilesSQLite(db, processedFiles, pathConfig, logger) {
374
+ const getChunksForPathStmt = db.prepare(`
375
+ SELECT chunk_id, url FROM vec_items
376
+ WHERE url LIKE ? || '%'
377
+ `);
378
+ const deleteChunkStmt = db.prepare(`DELETE FROM vec_items WHERE chunk_id = ?`);
379
+ // Determine if we're using URL rewriting or direct file paths
380
+ const isRewriteMode = typeof pathConfig === 'object' && pathConfig.url_rewrite_prefix;
381
+ // Set up the URL prefix for searching
382
+ let urlPrefix;
383
+ if (isRewriteMode) {
384
+ // Handle URL rewriting case
385
+ urlPrefix = pathConfig.url_rewrite_prefix || '';
386
+ urlPrefix = urlPrefix.endsWith('/') ? urlPrefix.slice(0, -1) : urlPrefix;
387
+ }
388
+ else {
389
+ // Handle direct file path case
390
+ const dirPrefix = typeof pathConfig === 'string' ? pathConfig : pathConfig.path;
391
+ const cleanedDirPrefix = dirPrefix.replace(/^\.\/+/, '');
392
+ urlPrefix = `file://${cleanedDirPrefix}`;
393
+ }
394
+ logger.debug(`Searching for chunks with URL prefix: ${urlPrefix}`);
395
+ const existingChunks = getChunksForPathStmt.all(urlPrefix);
396
+ let deletedCount = 0;
397
+ const transaction = db.transaction(() => {
398
+ for (const { chunk_id, url } of existingChunks) {
399
+ // Skip if it's not from our URL prefix (safety check)
400
+ if (!url.startsWith(urlPrefix))
401
+ continue;
402
+ let filePath;
403
+ let shouldDelete = false;
404
+ if (isRewriteMode) {
405
+ // URL rewrite mode: extract relative path and construct full file path
406
+ const config = pathConfig;
407
+ const relativePath = url.substring(urlPrefix.length + 1); // +1 for the '/'
408
+ filePath = path.join(config.path, relativePath);
409
+ shouldDelete = !processedFiles.has(filePath);
410
+ }
411
+ else {
412
+ // Direct file path mode: remove file:// prefix to match with processedFiles
413
+ filePath = url.substring(7); // Remove 'file://' prefix
414
+ shouldDelete = !processedFiles.has(filePath);
415
+ }
416
+ if (shouldDelete) {
417
+ logger.debug(`Deleting obsolete chunk from SQLite: ${chunk_id.substring(0, 8)}... (File not processed: ${filePath})`);
418
+ deleteChunkStmt.run(chunk_id);
419
+ deletedCount++;
420
+ }
421
+ }
422
+ });
423
+ transaction();
424
+ logger.info(`Deleted ${deletedCount} obsolete chunks from SQLite for URL prefix ${urlPrefix}`);
425
+ }
426
+ static async removeObsoleteFilesQdrant(db, processedFiles, pathConfig, logger) {
427
+ const { client, collectionName } = db;
428
+ try {
429
+ // Determine if we're using URL rewriting or direct file paths
430
+ const isRewriteMode = typeof pathConfig === 'object' && pathConfig.url_rewrite_prefix;
431
+ // Set up the URL prefix for searching
432
+ let urlPrefix;
433
+ if (isRewriteMode) {
434
+ // Handle URL rewriting case
435
+ urlPrefix = pathConfig.url_rewrite_prefix || '';
436
+ urlPrefix = urlPrefix.endsWith('/') ? urlPrefix.slice(0, -1) : urlPrefix;
437
+ }
438
+ else {
439
+ // Handle direct file path case
440
+ const dirPrefix = typeof pathConfig === 'string' ? pathConfig : pathConfig.path;
441
+ const cleanedDirPrefix = dirPrefix.replace(/^\.\/+/, '');
442
+ urlPrefix = `file://${cleanedDirPrefix}`;
443
+ }
444
+ logger.debug(`Checking for obsolete chunks with URL prefix: ${urlPrefix}`);
445
+ const response = await client.scroll(collectionName, {
446
+ limit: 10000,
447
+ with_payload: true,
448
+ with_vector: false,
449
+ filter: {
450
+ must: [
451
+ {
452
+ key: "url",
453
+ match: {
454
+ text: urlPrefix
455
+ }
456
+ }
457
+ ],
458
+ must_not: [
459
+ {
460
+ key: "is_metadata",
461
+ match: {
462
+ value: true
463
+ }
464
+ }
465
+ ]
466
+ }
467
+ });
468
+ const obsoletePointIds = response.points
469
+ .filter((point) => {
470
+ const url = point.payload?.url;
471
+ // Double check it's not a metadata record
472
+ if (point.payload?.is_metadata === true) {
473
+ return false;
474
+ }
475
+ if (!url || !url.startsWith(urlPrefix)) {
476
+ return false;
477
+ }
478
+ let filePath;
479
+ if (isRewriteMode) {
480
+ // URL rewrite mode: extract relative path and construct full file path
481
+ const config = pathConfig;
482
+ const relativePath = url.substring(urlPrefix.length + 1); // +1 for the '/'
483
+ filePath = path.join(config.path, relativePath);
484
+ }
485
+ else {
486
+ // Direct file path mode: remove file:// prefix to match with processedFiles
487
+ filePath = url.startsWith('file://') ? url.substring(7) : '';
488
+ }
489
+ return filePath && !processedFiles.has(filePath);
490
+ })
491
+ .map((point) => point.id);
492
+ if (obsoletePointIds.length > 0) {
493
+ await client.delete(collectionName, {
494
+ points: obsoletePointIds,
495
+ });
496
+ logger.info(`Deleted ${obsoletePointIds.length} obsolete chunks from Qdrant for URL prefix ${urlPrefix}`);
497
+ }
498
+ else {
499
+ logger.info(`No obsolete chunks to delete from Qdrant for URL prefix ${urlPrefix}`);
500
+ }
501
+ }
502
+ catch (error) {
503
+ logger.error(`Error removing obsolete chunks from Qdrant:`, error);
504
+ }
505
+ }
506
+ }
507
+ exports.DatabaseManager = DatabaseManager;