doc2vec 2.11.0 → 2.12.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.
- package/dist/controller/chunk-inspector.js +209 -0
- package/dist/controller/server.js +29 -0
- package/dist/database.js +64 -19
- package/dist/doc2vec.js +84 -19
- package/dist/types.js +12 -0
- package/dist/ui/assets/index-CZSZsL3G.css +1 -0
- package/dist/ui/assets/{index-D-QOw3rk.js → index-vglJgkwy.js} +78 -78
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BVg6n7Si.css +0 -1
|
@@ -0,0 +1,209 @@
|
|
|
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.lookupChunks = lookupChunks;
|
|
40
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const sqliteVec = __importStar(require("sqlite-vec"));
|
|
43
|
+
const yaml = __importStar(require("js-yaml"));
|
|
44
|
+
const js_client_rest_1 = require("@qdrant/js-client-rest");
|
|
45
|
+
const types_1 = require("./types");
|
|
46
|
+
const MAX_CHUNKS = 500;
|
|
47
|
+
/**
|
|
48
|
+
* Read-only lookup of the chunks stored for a URL in a source's vector store.
|
|
49
|
+
* Resolves the store the same way a sync job would (same defaults as
|
|
50
|
+
* DatabaseManager.initDatabase, same ${ENV_VAR} substitution as Doc2Vec's
|
|
51
|
+
* config loader), but never creates tables or collections.
|
|
52
|
+
*/
|
|
53
|
+
async function lookupChunks(configContent, selector, url) {
|
|
54
|
+
const source = findSource(configContent, selector);
|
|
55
|
+
const dbConfig = source.database_config ?? {};
|
|
56
|
+
const params = dbConfig.params ?? {};
|
|
57
|
+
if (dbConfig.type === 'sqlite') {
|
|
58
|
+
const dbPath = params.db_path
|
|
59
|
+
|| path.join(process.cwd(), `${String(source.product_name).replace(/\s+/g, '_')}-${source.version}.db`);
|
|
60
|
+
return {
|
|
61
|
+
source: { product_name: source.product_name, type: source.type, version: source.version },
|
|
62
|
+
database: { type: 'sqlite', path: dbPath },
|
|
63
|
+
chunks: lookupSqlite(dbPath, url),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (dbConfig.type === 'qdrant') {
|
|
67
|
+
const qdrantUrl = params.qdrant_url || 'http://localhost:6333';
|
|
68
|
+
const qdrantPort = params.qdrant_port || 443;
|
|
69
|
+
const collectionName = params.collection_name
|
|
70
|
+
|| `${String(source.product_name).toLowerCase().replace(/\s+/g, '_')}_${source.version}`;
|
|
71
|
+
const client = new js_client_rest_1.QdrantClient({ url: qdrantUrl, apiKey: process.env.QDRANT_API_KEY, port: qdrantPort });
|
|
72
|
+
return {
|
|
73
|
+
source: { product_name: source.product_name, type: source.type, version: source.version },
|
|
74
|
+
database: { type: 'qdrant', url: qdrantUrl, collection: collectionName },
|
|
75
|
+
chunks: await lookupQdrant(client, collectionName, url),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
throw new types_1.ValidationError(`source has unsupported database type: ${dbConfig.type}`);
|
|
79
|
+
}
|
|
80
|
+
function findSource(configContent, selector) {
|
|
81
|
+
// Same ${VAR} substitution the sync job applies when it loads the config
|
|
82
|
+
const substituted = configContent.replace(/\$\{([^}]+)\}/g, (match, varName) => process.env[varName] !== undefined ? process.env[varName] : match);
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = yaml.load(substituted);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
throw new types_1.ValidationError(`config does not parse: ${err instanceof Error ? err.message : String(err)}`);
|
|
89
|
+
}
|
|
90
|
+
const sources = Array.isArray(parsed?.sources) ? parsed.sources : [];
|
|
91
|
+
const source = sources.find(s => s?.product_name === selector.product_name
|
|
92
|
+
&& (selector.type === undefined || s?.type === selector.type)
|
|
93
|
+
&& (selector.version === undefined || String(effectiveVersion(s)) === selector.version));
|
|
94
|
+
if (!source) {
|
|
95
|
+
throw new types_1.NotFoundError(`source '${selector.product_name}' not found in config`);
|
|
96
|
+
}
|
|
97
|
+
return { ...source, version: effectiveVersion(source) };
|
|
98
|
+
}
|
|
99
|
+
// Mirrors the version defaulting in Doc2Vec.loadConfig for code sources
|
|
100
|
+
function effectiveVersion(source) {
|
|
101
|
+
if (source?.version && String(source.version).trim().length > 0)
|
|
102
|
+
return String(source.version);
|
|
103
|
+
if (source?.type === 'code') {
|
|
104
|
+
if (source.branch && String(source.branch).trim().length > 0)
|
|
105
|
+
return String(source.branch);
|
|
106
|
+
return 'local';
|
|
107
|
+
}
|
|
108
|
+
return String(source?.version ?? '');
|
|
109
|
+
}
|
|
110
|
+
function lookupSqlite(dbPath, url) {
|
|
111
|
+
let db;
|
|
112
|
+
try {
|
|
113
|
+
db = new better_sqlite3_1.default(dbPath, { readonly: true, fileMustExist: true, allowExtension: true });
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new types_1.NotFoundError(`SQLite database not found at ${dbPath} (has this source ever been synced?)`);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
sqliteVec.load(db);
|
|
120
|
+
const hasDates = db
|
|
121
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'vec_chunk_dates'`)
|
|
122
|
+
.get() !== undefined;
|
|
123
|
+
const rows = db.prepare(`
|
|
124
|
+
SELECT v.chunk_id, v.url, v.product_name, v.version, v.section, v.heading_hierarchy,
|
|
125
|
+
v.chunk_index, v.total_chunks, v.hash, v.content
|
|
126
|
+
${hasDates ? ', d.created_at AS created_at' : ', NULL AS created_at'}
|
|
127
|
+
FROM vec_items v
|
|
128
|
+
${hasDates ? 'LEFT JOIN vec_chunk_dates d ON d.chunk_id = v.chunk_id' : ''}
|
|
129
|
+
WHERE v.url = ?
|
|
130
|
+
LIMIT ${MAX_CHUNKS}
|
|
131
|
+
`).all(url);
|
|
132
|
+
return sortChunks(rows.map(row => ({
|
|
133
|
+
chunk_id: String(row.chunk_id),
|
|
134
|
+
url: String(row.url),
|
|
135
|
+
product_name: row.product_name ?? null,
|
|
136
|
+
version: row.version ?? null,
|
|
137
|
+
section: row.section ?? null,
|
|
138
|
+
heading_hierarchy: parseHierarchy(row.heading_hierarchy),
|
|
139
|
+
chunk_index: row.chunk_index === null || row.chunk_index === undefined ? null : Number(row.chunk_index),
|
|
140
|
+
total_chunks: row.total_chunks === null || row.total_chunks === undefined ? null : Number(row.total_chunks),
|
|
141
|
+
hash: row.hash ?? null,
|
|
142
|
+
created_at: row.created_at ?? null,
|
|
143
|
+
content: String(row.content ?? ''),
|
|
144
|
+
})));
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
db.close();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function lookupQdrant(client, collectionName, url) {
|
|
151
|
+
const chunks = [];
|
|
152
|
+
let offset = undefined;
|
|
153
|
+
do {
|
|
154
|
+
const response = await client.scroll(collectionName, {
|
|
155
|
+
limit: 200,
|
|
156
|
+
offset,
|
|
157
|
+
with_payload: true,
|
|
158
|
+
with_vector: false,
|
|
159
|
+
filter: {
|
|
160
|
+
must: [{ key: 'url', match: { value: url } }],
|
|
161
|
+
must_not: [{ key: 'is_metadata', match: { value: true } }],
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
for (const point of response.points) {
|
|
165
|
+
const p = point.payload ?? {};
|
|
166
|
+
chunks.push({
|
|
167
|
+
chunk_id: String(p.original_chunk_id ?? point.id),
|
|
168
|
+
url: String(p.url ?? url),
|
|
169
|
+
product_name: p.product_name ?? null,
|
|
170
|
+
version: p.version ?? null,
|
|
171
|
+
section: p.section ?? null,
|
|
172
|
+
heading_hierarchy: parseHierarchy(p.heading_hierarchy),
|
|
173
|
+
chunk_index: p.chunk_index === null || p.chunk_index === undefined ? null : Number(p.chunk_index),
|
|
174
|
+
total_chunks: p.total_chunks === null || p.total_chunks === undefined ? null : Number(p.total_chunks),
|
|
175
|
+
hash: p.hash ?? null,
|
|
176
|
+
created_at: p.created_at ?? null,
|
|
177
|
+
content: String(p.content ?? ''),
|
|
178
|
+
});
|
|
179
|
+
if (chunks.length >= MAX_CHUNKS)
|
|
180
|
+
return sortChunks(chunks);
|
|
181
|
+
}
|
|
182
|
+
offset = response.next_page_offset;
|
|
183
|
+
} while (offset !== null && offset !== undefined);
|
|
184
|
+
return sortChunks(chunks);
|
|
185
|
+
}
|
|
186
|
+
function parseHierarchy(value) {
|
|
187
|
+
if (Array.isArray(value))
|
|
188
|
+
return value.map(String);
|
|
189
|
+
if (typeof value === 'string' && value.trim().startsWith('[')) {
|
|
190
|
+
try {
|
|
191
|
+
const parsed = JSON.parse(value);
|
|
192
|
+
if (Array.isArray(parsed))
|
|
193
|
+
return parsed.map(String);
|
|
194
|
+
}
|
|
195
|
+
catch { /* fall through */ }
|
|
196
|
+
}
|
|
197
|
+
return value ? [String(value)] : [];
|
|
198
|
+
}
|
|
199
|
+
// Default order: position within the page. The UI offers per-column sorting
|
|
200
|
+
// (including by creation date) on top of this.
|
|
201
|
+
function sortChunks(chunks) {
|
|
202
|
+
return chunks.sort((a, b) => {
|
|
203
|
+
const ai = a.chunk_index ?? Number.MAX_SAFE_INTEGER;
|
|
204
|
+
const bi = b.chunk_index ?? Number.MAX_SAFE_INTEGER;
|
|
205
|
+
if (ai !== bi)
|
|
206
|
+
return ai - bi;
|
|
207
|
+
return a.chunk_id.localeCompare(b.chunk_id);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -40,6 +40,7 @@ exports.createServer = createServer;
|
|
|
40
40
|
const express_1 = __importDefault(require("express"));
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
|
+
const chunk_inspector_1 = require("./chunk-inspector");
|
|
43
44
|
const config_registry_1 = require("./config-registry");
|
|
44
45
|
const types_1 = require("./types");
|
|
45
46
|
const SSE_HEARTBEAT_MS = 15000;
|
|
@@ -151,6 +152,34 @@ function createServer(deps) {
|
|
|
151
152
|
const days = Number(req.query.days) || 30;
|
|
152
153
|
res.json(await store.getConfigStats(id, days));
|
|
153
154
|
});
|
|
155
|
+
// Inspect the chunks currently stored for a URL in one source's vector store.
|
|
156
|
+
// This reads the live store, so results reflect the present state, not a
|
|
157
|
+
// historical run.
|
|
158
|
+
app.get('/api/configs/:id/chunks', async (req, res) => {
|
|
159
|
+
const config = registry.get(parseId(String(req.params.id)));
|
|
160
|
+
if (!config)
|
|
161
|
+
throw new types_1.NotFoundError('config not found');
|
|
162
|
+
const url = typeof req.query.url === 'string' ? req.query.url.trim() : '';
|
|
163
|
+
const productName = typeof req.query.product_name === 'string' ? req.query.product_name : '';
|
|
164
|
+
if (!url)
|
|
165
|
+
throw new types_1.ValidationError('url query parameter is required');
|
|
166
|
+
if (!productName)
|
|
167
|
+
throw new types_1.ValidationError('product_name query parameter is required');
|
|
168
|
+
try {
|
|
169
|
+
res.json(await (0, chunk_inspector_1.lookupChunks)(config.content, {
|
|
170
|
+
product_name: productName,
|
|
171
|
+
type: typeof req.query.type === 'string' && req.query.type ? req.query.type : undefined,
|
|
172
|
+
version: typeof req.query.version === 'string' && req.query.version ? req.query.version : undefined,
|
|
173
|
+
}, url));
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
if (err instanceof types_1.NotFoundError || err instanceof types_1.ValidationError)
|
|
177
|
+
throw err;
|
|
178
|
+
// Vector store unreachable (e.g. Qdrant down) — surface the reason
|
|
179
|
+
logger.error('Chunk lookup failed:', err);
|
|
180
|
+
res.status(502).json({ error: `chunk lookup failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
154
183
|
// ------------------------------------------------------------------ runs
|
|
155
184
|
app.get('/api/runs', async (req, res) => {
|
|
156
185
|
res.json(await store.listRuns({
|
package/dist/database.js
CHANGED
|
@@ -71,6 +71,7 @@ class DatabaseManager {
|
|
|
71
71
|
total_chunks INTEGER
|
|
72
72
|
);
|
|
73
73
|
`);
|
|
74
|
+
this.ensureChunkDatesTable(db);
|
|
74
75
|
logger.info(`SQLite database initialized successfully`);
|
|
75
76
|
return { db, type: 'sqlite' };
|
|
76
77
|
}
|
|
@@ -307,9 +308,24 @@ class DatabaseManager {
|
|
|
307
308
|
logger.error(`Failed to update last run date for ${repo}:`, error);
|
|
308
309
|
}
|
|
309
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Chunk creation timestamps live in a companion table because vec_items is
|
|
313
|
+
* a vec0 virtual table that cannot be ALTERed to add a column. Rows are
|
|
314
|
+
* written on every chunk insert/update; stale rows for deleted chunks are
|
|
315
|
+
* harmless (lookups always join against vec_items).
|
|
316
|
+
*/
|
|
317
|
+
static ensureChunkDatesTable(db) {
|
|
318
|
+
db.exec(`
|
|
319
|
+
CREATE TABLE IF NOT EXISTS vec_chunk_dates (
|
|
320
|
+
chunk_id TEXT PRIMARY KEY,
|
|
321
|
+
created_at TEXT NOT NULL
|
|
322
|
+
);
|
|
323
|
+
`);
|
|
324
|
+
}
|
|
310
325
|
static prepareSQLiteStatements(db) {
|
|
311
326
|
let cached = this.columnCache.get(db);
|
|
312
327
|
if (!cached) {
|
|
328
|
+
this.ensureChunkDatesTable(db);
|
|
313
329
|
cached = {
|
|
314
330
|
hasBranch: this.hasColumn(db, 'branch'),
|
|
315
331
|
hasRepo: this.hasColumn(db, 'repo')
|
|
@@ -360,12 +376,16 @@ class DatabaseManager {
|
|
|
360
376
|
`),
|
|
361
377
|
getAllChunkIdsStmt: db.prepare(`SELECT chunk_id FROM vec_items`),
|
|
362
378
|
deleteChunkStmt: db.prepare(`DELETE FROM vec_items WHERE chunk_id = ?`),
|
|
379
|
+
touchDateStmt: db.prepare(`
|
|
380
|
+
INSERT INTO vec_chunk_dates (chunk_id, created_at) VALUES (?, ?)
|
|
381
|
+
ON CONFLICT(chunk_id) DO UPDATE SET created_at = excluded.created_at
|
|
382
|
+
`),
|
|
363
383
|
hasBranchColumn,
|
|
364
384
|
hasRepoColumn
|
|
365
385
|
};
|
|
366
386
|
}
|
|
367
387
|
static insertVectorsSQLite(db, chunk, embedding, logger, chunkHash) {
|
|
368
|
-
const { insertStmt, updateStmt, hasBranchColumn, hasRepoColumn } = this.prepareSQLiteStatements(db);
|
|
388
|
+
const { insertStmt, updateStmt, touchDateStmt, hasBranchColumn, hasRepoColumn } = this.prepareSQLiteStatements(db);
|
|
369
389
|
const hash = chunkHash || utils_1.Utils.generateHash(chunk.content);
|
|
370
390
|
const transaction = db.transaction(() => {
|
|
371
391
|
// Use BigInt for true integer representation in SQLite vec0
|
|
@@ -401,6 +421,7 @@ class DatabaseManager {
|
|
|
401
421
|
updateValues.push(JSON.stringify(chunk.metadata.heading_hierarchy), chunk.metadata.section, chunk.content, chunk.metadata.url, hash, chunkIndex, totalChunks, chunk.metadata.chunk_id);
|
|
402
422
|
updateStmt.run(...updateValues);
|
|
403
423
|
}
|
|
424
|
+
touchDateStmt.run(chunk.metadata.chunk_id, new Date().toISOString());
|
|
404
425
|
});
|
|
405
426
|
transaction();
|
|
406
427
|
}
|
|
@@ -434,6 +455,7 @@ class DatabaseManager {
|
|
|
434
455
|
original_chunk_id: chunk.metadata.chunk_id,
|
|
435
456
|
chunk_index: chunk.metadata.chunk_index,
|
|
436
457
|
total_chunks: chunk.metadata.total_chunks,
|
|
458
|
+
created_at: new Date().toISOString(),
|
|
437
459
|
},
|
|
438
460
|
};
|
|
439
461
|
await client.upsert(collectionName, {
|
|
@@ -457,17 +479,20 @@ class DatabaseManager {
|
|
|
457
479
|
const deleteChunkStmt = db.prepare(`DELETE FROM vec_items WHERE chunk_id = ?`);
|
|
458
480
|
const existingChunks = getChunksForUrlStmt.all(urlPrefix);
|
|
459
481
|
let deletedCount = 0;
|
|
482
|
+
const deletedUrls = new Set();
|
|
460
483
|
const transaction = db.transaction(() => {
|
|
461
484
|
for (const { chunk_id, url } of existingChunks) {
|
|
462
485
|
if (!visitedUrls.has(url)) {
|
|
463
486
|
logger.debug(`Deleting obsolete chunk from SQLite: ${chunk_id.substring(0, 8)}... (URL not visited)`);
|
|
464
487
|
deleteChunkStmt.run(chunk_id);
|
|
465
488
|
deletedCount++;
|
|
489
|
+
deletedUrls.add(url);
|
|
466
490
|
}
|
|
467
491
|
}
|
|
468
492
|
});
|
|
469
493
|
transaction();
|
|
470
494
|
logger.info(`Deleted ${deletedCount} obsolete chunks from SQLite for URL ${urlPrefix}`);
|
|
495
|
+
return { items: deletedUrls.size, chunks: deletedCount };
|
|
471
496
|
}
|
|
472
497
|
static async removeObsoleteChunksQdrant(db, visitedUrls, urlPrefix, logger) {
|
|
473
498
|
const { client, collectionName } = db;
|
|
@@ -496,6 +521,7 @@ class DatabaseManager {
|
|
|
496
521
|
]
|
|
497
522
|
};
|
|
498
523
|
const obsoletePointIds = [];
|
|
524
|
+
const obsoleteUrls = new Set();
|
|
499
525
|
let offset = undefined;
|
|
500
526
|
do {
|
|
501
527
|
const response = await client.scroll(collectionName, {
|
|
@@ -513,6 +539,7 @@ class DatabaseManager {
|
|
|
513
539
|
}
|
|
514
540
|
if (url && !visitedUrls.has(url)) {
|
|
515
541
|
obsoletePointIds.push(point.id);
|
|
542
|
+
obsoleteUrls.add(url);
|
|
516
543
|
}
|
|
517
544
|
}
|
|
518
545
|
offset = response.next_page_offset;
|
|
@@ -526,6 +553,7 @@ class DatabaseManager {
|
|
|
526
553
|
else {
|
|
527
554
|
logger.info(`No obsolete chunks to delete from Qdrant for URL ${urlPrefix}`);
|
|
528
555
|
}
|
|
556
|
+
return { items: obsoleteUrls.size, chunks: obsoletePointIds.length };
|
|
529
557
|
}
|
|
530
558
|
catch (error) {
|
|
531
559
|
// Rethrow so a failed cleanup fails the job instead of silently
|
|
@@ -548,31 +576,42 @@ class DatabaseManager {
|
|
|
548
576
|
const deleteStmt = db.prepare(`DELETE FROM vec_items WHERE url = ?`);
|
|
549
577
|
const result = deleteStmt.run(url);
|
|
550
578
|
logger.info(`Deleted ${result.changes} chunks from SQLite for URL ${url}`);
|
|
579
|
+
return result.changes;
|
|
551
580
|
}
|
|
552
581
|
static async removeChunksByUrlQdrant(db, url, logger) {
|
|
553
582
|
const { client, collectionName } = db;
|
|
554
583
|
try {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
value: url // Exact match — must match getChunkHashesByUrlQdrant's filter
|
|
562
|
-
}
|
|
584
|
+
const filter = {
|
|
585
|
+
must: [
|
|
586
|
+
{
|
|
587
|
+
key: 'url',
|
|
588
|
+
match: {
|
|
589
|
+
value: url // Exact match — must match getChunkHashesByUrlQdrant's filter
|
|
563
590
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
591
|
+
}
|
|
592
|
+
],
|
|
593
|
+
must_not: [
|
|
594
|
+
{
|
|
595
|
+
key: 'is_metadata',
|
|
596
|
+
match: {
|
|
597
|
+
value: true
|
|
571
598
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
599
|
+
}
|
|
600
|
+
]
|
|
601
|
+
};
|
|
602
|
+
// Count first so callers can report how many chunks the delete removed
|
|
603
|
+
// (Qdrant's delete-by-filter response doesn't include a count).
|
|
604
|
+
let deletedCount = 0;
|
|
605
|
+
try {
|
|
606
|
+
const countResult = await client.count(collectionName, { filter, exact: true });
|
|
607
|
+
deletedCount = countResult?.count ?? 0;
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
// Count is informational only — never fail the delete over it
|
|
611
|
+
}
|
|
612
|
+
await client.delete(collectionName, { filter });
|
|
575
613
|
logger.info(`Deleted chunks from Qdrant for URL ${url}`);
|
|
614
|
+
return deletedCount;
|
|
576
615
|
}
|
|
577
616
|
catch (error) {
|
|
578
617
|
// Rethrow so the failure propagates and the job exits non-zero.
|
|
@@ -697,6 +736,7 @@ class DatabaseManager {
|
|
|
697
736
|
logger.debug(`Searching for chunks with URL prefix: ${urlPrefix}`);
|
|
698
737
|
const existingChunks = getChunksForPathStmt.all(urlPrefix);
|
|
699
738
|
let deletedCount = 0;
|
|
739
|
+
const deletedUrls = new Set();
|
|
700
740
|
const transaction = db.transaction(() => {
|
|
701
741
|
for (const { chunk_id, url } of existingChunks) {
|
|
702
742
|
// Skip if it's not from our URL prefix (safety check)
|
|
@@ -720,11 +760,13 @@ class DatabaseManager {
|
|
|
720
760
|
logger.debug(`Deleting obsolete chunk from SQLite: ${chunk_id.substring(0, 8)}... (File not processed: ${filePath})`);
|
|
721
761
|
deleteChunkStmt.run(chunk_id);
|
|
722
762
|
deletedCount++;
|
|
763
|
+
deletedUrls.add(url);
|
|
723
764
|
}
|
|
724
765
|
}
|
|
725
766
|
});
|
|
726
767
|
transaction();
|
|
727
768
|
logger.info(`Deleted ${deletedCount} obsolete chunks from SQLite for URL prefix ${urlPrefix}`);
|
|
769
|
+
return { items: deletedUrls.size, chunks: deletedCount };
|
|
728
770
|
}
|
|
729
771
|
static async removeObsoleteFilesQdrant(db, processedFiles, pathConfig, logger) {
|
|
730
772
|
const { client, collectionName } = db;
|
|
@@ -768,6 +810,7 @@ class DatabaseManager {
|
|
|
768
810
|
]
|
|
769
811
|
};
|
|
770
812
|
const obsoletePointIds = [];
|
|
813
|
+
const obsoleteUrls = new Set();
|
|
771
814
|
let offset = undefined;
|
|
772
815
|
do {
|
|
773
816
|
const response = await client.scroll(collectionName, {
|
|
@@ -799,6 +842,7 @@ class DatabaseManager {
|
|
|
799
842
|
}
|
|
800
843
|
if (filePath && !processedFiles.has(filePath)) {
|
|
801
844
|
obsoletePointIds.push(point.id);
|
|
845
|
+
obsoleteUrls.add(url);
|
|
802
846
|
}
|
|
803
847
|
}
|
|
804
848
|
offset = response.next_page_offset;
|
|
@@ -812,6 +856,7 @@ class DatabaseManager {
|
|
|
812
856
|
else {
|
|
813
857
|
logger.info(`No obsolete chunks to delete from Qdrant for URL prefix ${urlPrefix}`);
|
|
814
858
|
}
|
|
859
|
+
return { items: obsoleteUrls.size, chunks: obsoletePointIds.length };
|
|
815
860
|
}
|
|
816
861
|
catch (error) {
|
|
817
862
|
// Rethrow so a failed cleanup fails the job instead of silently
|