doc2vec 2.3.0 → 2.5.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/README.md +76 -8
- package/dist/content-processor.js +545 -33
- package/dist/database.js +102 -14
- package/dist/doc2vec.js +171 -262
- package/dist/utils.js +7 -2
- package/package.json +1 -1
package/dist/database.js
CHANGED
|
@@ -44,7 +44,7 @@ const sqliteVec = __importStar(require("sqlite-vec"));
|
|
|
44
44
|
const js_client_rest_1 = require("@qdrant/js-client-rest");
|
|
45
45
|
const utils_1 = require("./utils");
|
|
46
46
|
class DatabaseManager {
|
|
47
|
-
static async initDatabase(config, parentLogger) {
|
|
47
|
+
static async initDatabase(config, parentLogger, embeddingDimension) {
|
|
48
48
|
const logger = parentLogger.child('database');
|
|
49
49
|
const dbConfig = config.database_config;
|
|
50
50
|
if (dbConfig.type === 'sqlite') {
|
|
@@ -53,10 +53,10 @@ class DatabaseManager {
|
|
|
53
53
|
logger.info(`Opening SQLite database at ${dbPath}`);
|
|
54
54
|
const db = new better_sqlite3_1.default(dbPath, { allowExtension: true });
|
|
55
55
|
sqliteVec.load(db);
|
|
56
|
-
logger.debug(`Creating vec_items table if it doesn't exist`);
|
|
56
|
+
logger.debug(`Creating vec_items table if it doesn't exist (dimension: ${embeddingDimension})`);
|
|
57
57
|
db.exec(`
|
|
58
58
|
CREATE VIRTUAL TABLE IF NOT EXISTS vec_items USING vec0(
|
|
59
|
-
embedding FLOAT[
|
|
59
|
+
embedding FLOAT[${embeddingDimension}],
|
|
60
60
|
product_name TEXT,
|
|
61
61
|
version TEXT,
|
|
62
62
|
branch TEXT,
|
|
@@ -81,7 +81,7 @@ class DatabaseManager {
|
|
|
81
81
|
const collectionName = params.collection_name || `${config.product_name.toLowerCase().replace(/\s+/g, '_')}_${config.version}`;
|
|
82
82
|
logger.info(`Connecting to Qdrant at ${qdrantUrl}:${qdrantPort}, collection: ${collectionName}`);
|
|
83
83
|
const qdrantClient = new js_client_rest_1.QdrantClient({ url: qdrantUrl, apiKey: process.env.QDRANT_API_KEY, port: qdrantPort });
|
|
84
|
-
await this.createCollectionQdrant(qdrantClient, collectionName, logger);
|
|
84
|
+
await this.createCollectionQdrant(qdrantClient, collectionName, logger, embeddingDimension);
|
|
85
85
|
logger.info(`Qdrant connection established successfully`);
|
|
86
86
|
return { client: qdrantClient, collectionName, type: 'qdrant' };
|
|
87
87
|
}
|
|
@@ -91,7 +91,7 @@ class DatabaseManager {
|
|
|
91
91
|
throw new Error(errMsg);
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
-
static async createCollectionQdrant(qdrantClient, collectionName, logger) {
|
|
94
|
+
static async createCollectionQdrant(qdrantClient, collectionName, logger, embeddingDimension) {
|
|
95
95
|
try {
|
|
96
96
|
logger.debug(`Checking if collection ${collectionName} exists`);
|
|
97
97
|
const collections = await qdrantClient.getCollections();
|
|
@@ -100,10 +100,10 @@ class DatabaseManager {
|
|
|
100
100
|
logger.info(`Collection ${collectionName} already exists`);
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
|
-
logger.info(`Creating new collection ${collectionName}`);
|
|
103
|
+
logger.info(`Creating new collection ${collectionName} with dimension ${embeddingDimension}`);
|
|
104
104
|
await qdrantClient.createCollection(collectionName, {
|
|
105
105
|
vectors: {
|
|
106
|
-
size:
|
|
106
|
+
size: embeddingDimension,
|
|
107
107
|
distance: "Cosine",
|
|
108
108
|
},
|
|
109
109
|
});
|
|
@@ -181,7 +181,7 @@ class DatabaseManager {
|
|
|
181
181
|
}
|
|
182
182
|
return defaultValue;
|
|
183
183
|
}
|
|
184
|
-
static async setMetadataValue(dbConnection, key, value, logger) {
|
|
184
|
+
static async setMetadataValue(dbConnection, key, value, logger, embeddingDimension) {
|
|
185
185
|
try {
|
|
186
186
|
if (dbConnection.type === 'sqlite') {
|
|
187
187
|
const stmt = dbConnection.db.prepare(`
|
|
@@ -193,8 +193,7 @@ class DatabaseManager {
|
|
|
193
193
|
}
|
|
194
194
|
else if (dbConnection.type === 'qdrant') {
|
|
195
195
|
const metadataUUID = utils_1.Utils.generateMetadataUUID(key);
|
|
196
|
-
const
|
|
197
|
-
const dummyEmbedding = new Array(dummyEmbeddingSize).fill(0);
|
|
196
|
+
const dummyEmbedding = new Array(embeddingDimension).fill(0);
|
|
198
197
|
const metadataPoint = {
|
|
199
198
|
id: metadataUUID,
|
|
200
199
|
vector: dummyEmbedding,
|
|
@@ -258,7 +257,7 @@ class DatabaseManager {
|
|
|
258
257
|
logger.info(`No saved run date found for ${repo}, using default: ${defaultDate}`);
|
|
259
258
|
return defaultDate;
|
|
260
259
|
}
|
|
261
|
-
static async updateLastRunDate(dbConnection, repo, logger) {
|
|
260
|
+
static async updateLastRunDate(dbConnection, repo, logger, embeddingDimension) {
|
|
262
261
|
const now = new Date().toISOString();
|
|
263
262
|
try {
|
|
264
263
|
if (dbConnection.type === 'sqlite') {
|
|
@@ -276,8 +275,7 @@ class DatabaseManager {
|
|
|
276
275
|
const metadataKey = `last_run_${repo.replace('/', '_')}`;
|
|
277
276
|
logger.debug(`Using UUID: ${metadataUUID} for metadata`);
|
|
278
277
|
// Generate a dummy embedding (all zeros)
|
|
279
|
-
const
|
|
280
|
-
const dummyEmbedding = new Array(dummyEmbeddingSize).fill(0);
|
|
278
|
+
const dummyEmbedding = new Array(embeddingDimension).fill(0);
|
|
281
279
|
// Create a point with special metadata payload
|
|
282
280
|
const metadataPoint = {
|
|
283
281
|
id: metadataUUID,
|
|
@@ -535,7 +533,7 @@ class DatabaseManager {
|
|
|
535
533
|
{
|
|
536
534
|
key: 'url',
|
|
537
535
|
match: {
|
|
538
|
-
|
|
536
|
+
value: url // Exact match — must match getChunkHashesByUrlQdrant's filter
|
|
539
537
|
}
|
|
540
538
|
}
|
|
541
539
|
],
|
|
@@ -555,6 +553,96 @@ class DatabaseManager {
|
|
|
555
553
|
logger.error(`Error deleting chunks from Qdrant for URL ${url}:`, error);
|
|
556
554
|
}
|
|
557
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* Fetch all distinct URLs stored under a given URL prefix.
|
|
558
|
+
* Used to pre-seed the crawl queue on re-runs so link discovery isn't lost
|
|
559
|
+
* when pages are skipped via ETag matching.
|
|
560
|
+
*/
|
|
561
|
+
static getStoredUrlsByPrefixSQLite(db, urlPrefix) {
|
|
562
|
+
const stmt = db.prepare('SELECT DISTINCT url FROM vec_items WHERE url LIKE ? || \'%\'');
|
|
563
|
+
const rows = stmt.all(urlPrefix);
|
|
564
|
+
return rows.map(r => r.url);
|
|
565
|
+
}
|
|
566
|
+
static async getStoredUrlsByPrefixQdrant(db, urlPrefix) {
|
|
567
|
+
const { client, collectionName } = db;
|
|
568
|
+
try {
|
|
569
|
+
const response = await client.scroll(collectionName, {
|
|
570
|
+
limit: 10000,
|
|
571
|
+
with_payload: { include: ['url'] },
|
|
572
|
+
with_vector: false,
|
|
573
|
+
filter: {
|
|
574
|
+
must: [
|
|
575
|
+
{
|
|
576
|
+
key: 'url',
|
|
577
|
+
match: { text: urlPrefix }
|
|
578
|
+
}
|
|
579
|
+
],
|
|
580
|
+
must_not: [
|
|
581
|
+
{
|
|
582
|
+
key: 'is_metadata',
|
|
583
|
+
match: { value: true }
|
|
584
|
+
}
|
|
585
|
+
]
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
const urls = new Set();
|
|
589
|
+
for (const point of response.points) {
|
|
590
|
+
const url = point.payload?.url;
|
|
591
|
+
if (url && url.startsWith(urlPrefix)) {
|
|
592
|
+
urls.add(url);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return Array.from(urls);
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
return [];
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Fetch all stored chunk content hashes for a given URL.
|
|
603
|
+
* Returns a sorted array of hash strings for comparison.
|
|
604
|
+
*/
|
|
605
|
+
static getChunkHashesByUrlSQLite(db, url) {
|
|
606
|
+
const stmt = db.prepare('SELECT hash FROM vec_items WHERE url = ?');
|
|
607
|
+
const rows = stmt.all(url);
|
|
608
|
+
return rows.map(r => r.hash).sort();
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Fetch all stored chunk content hashes for a given URL from Qdrant.
|
|
612
|
+
* Returns a sorted array of hash strings for comparison.
|
|
613
|
+
*/
|
|
614
|
+
static async getChunkHashesByUrlQdrant(db, url) {
|
|
615
|
+
const { client, collectionName } = db;
|
|
616
|
+
try {
|
|
617
|
+
const response = await client.scroll(collectionName, {
|
|
618
|
+
limit: 10000,
|
|
619
|
+
with_payload: { include: ['hash'] },
|
|
620
|
+
with_vector: false,
|
|
621
|
+
filter: {
|
|
622
|
+
must: [
|
|
623
|
+
{
|
|
624
|
+
key: 'url',
|
|
625
|
+
match: { value: url }
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
must_not: [
|
|
629
|
+
{
|
|
630
|
+
key: 'is_metadata',
|
|
631
|
+
match: { value: true }
|
|
632
|
+
}
|
|
633
|
+
]
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
return response.points
|
|
637
|
+
.map((point) => point.payload?.hash)
|
|
638
|
+
.filter(Boolean)
|
|
639
|
+
.sort();
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
// On error, return empty array so the caller treats it as "changed" (safe default)
|
|
643
|
+
return [];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
558
646
|
static removeObsoleteFilesSQLite(db, processedFiles, pathConfig, logger) {
|
|
559
647
|
const getChunksForPathStmt = db.prepare(`
|
|
560
648
|
SELECT chunk_id, url FROM vec_items
|