doc2vec 2.10.3 → 2.10.5
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/content-processor.js +10 -0
- package/dist/database.js +121 -77
- package/dist/doc2vec.js +24 -6
- package/package.json +1 -1
|
@@ -576,6 +576,11 @@ class ContentProcessor {
|
|
|
576
576
|
logger.debug(`HEAD returned ${headStatus} for ${url}, skipping`);
|
|
577
577
|
if (headStatus === 404) {
|
|
578
578
|
notFoundUrls.add(url);
|
|
579
|
+
// The URL was optimistically added to visitedUrls when
|
|
580
|
+
// dequeued (before we knew it was dead). Remove it so the
|
|
581
|
+
// obsolete-chunk cleanup treats it as gone and purges its
|
|
582
|
+
// chunks — otherwise deleted pages linger forever.
|
|
583
|
+
visitedUrls.delete(normalizedUrl);
|
|
579
584
|
}
|
|
580
585
|
skippedCount++;
|
|
581
586
|
continue;
|
|
@@ -717,6 +722,11 @@ class ContentProcessor {
|
|
|
717
722
|
consecutiveProtocolErrors = 0;
|
|
718
723
|
}
|
|
719
724
|
if (status === 404) {
|
|
725
|
+
notFoundUrls.add(url);
|
|
726
|
+
// The URL was optimistically added to visitedUrls when dequeued
|
|
727
|
+
// (before this GET revealed it's a 404). Remove it so the
|
|
728
|
+
// obsolete-chunk cleanup treats it as gone and purges its chunks.
|
|
729
|
+
visitedUrls.delete(normalizedUrl);
|
|
720
730
|
const sources = referrers.get(url) ?? new Set([baseUrl]);
|
|
721
731
|
for (const source of sources) {
|
|
722
732
|
addBrokenLink(source, url);
|
package/dist/database.js
CHANGED
|
@@ -215,7 +215,13 @@ class DatabaseManager {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
catch (error) {
|
|
218
|
-
|
|
218
|
+
// Best-effort by design: a failed metadata write errs toward the
|
|
219
|
+
// safe direction (redundant reprocessing next run — etag/lastmod/
|
|
220
|
+
// watermark stay behind, never skip ahead), so we warn rather than
|
|
221
|
+
// fail the job. Genuine disk/connection failures surface earlier at
|
|
222
|
+
// the chunk-write stage (storeChunkInQdrant), which shares this
|
|
223
|
+
// collection and throws.
|
|
224
|
+
logger.warn(`Failed to update metadata value for ${key} (continuing; will retry next run):`, error);
|
|
219
225
|
}
|
|
220
226
|
}
|
|
221
227
|
static async getLastRunDate(dbConnection, repo, defaultDate, logger) {
|
|
@@ -436,7 +442,11 @@ class DatabaseManager {
|
|
|
436
442
|
});
|
|
437
443
|
}
|
|
438
444
|
catch (error) {
|
|
439
|
-
|
|
445
|
+
// Rethrow so the failure propagates to the top-level handler and the
|
|
446
|
+
// job exits non-zero. Swallowing here previously let a sync "succeed"
|
|
447
|
+
// while chunks silently failed to store (e.g. Qdrant disk full).
|
|
448
|
+
console.error(`Error storing chunk in Qdrant (collection ${collectionName}, url ${chunk.metadata.url}):`, error);
|
|
449
|
+
throw error;
|
|
440
450
|
}
|
|
441
451
|
}
|
|
442
452
|
static removeObsoleteChunksSQLite(db, visitedUrls, urlPrefix, logger) {
|
|
@@ -462,40 +472,51 @@ class DatabaseManager {
|
|
|
462
472
|
static async removeObsoleteChunksQdrant(db, visitedUrls, urlPrefix, logger) {
|
|
463
473
|
const { client, collectionName } = db;
|
|
464
474
|
try {
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
475
|
+
// Scroll through ALL points that match the URL prefix but are not
|
|
476
|
+
// metadata points. Qdrant caps a single scroll at its page limit, so
|
|
477
|
+
// we must paginate via next_page_offset — otherwise large collections
|
|
478
|
+
// (e.g. tens of thousands of chunks) would only have their first page
|
|
479
|
+
// examined and obsolete chunks beyond it would never be removed.
|
|
480
|
+
const filter = {
|
|
481
|
+
must: [
|
|
482
|
+
{
|
|
483
|
+
key: "url",
|
|
484
|
+
match: {
|
|
485
|
+
text: urlPrefix
|
|
477
486
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
487
|
+
}
|
|
488
|
+
],
|
|
489
|
+
must_not: [
|
|
490
|
+
{
|
|
491
|
+
key: "is_metadata",
|
|
492
|
+
match: {
|
|
493
|
+
value: true
|
|
485
494
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
const obsoletePointIds =
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
+
}
|
|
496
|
+
]
|
|
497
|
+
};
|
|
498
|
+
const obsoletePointIds = [];
|
|
499
|
+
let offset = undefined;
|
|
500
|
+
do {
|
|
501
|
+
const response = await client.scroll(collectionName, {
|
|
502
|
+
limit: 1000,
|
|
503
|
+
offset,
|
|
504
|
+
with_payload: true,
|
|
505
|
+
with_vector: false,
|
|
506
|
+
filter
|
|
507
|
+
});
|
|
508
|
+
for (const point of response.points) {
|
|
509
|
+
const url = point.payload?.url;
|
|
510
|
+
// Double check it's not a metadata record
|
|
511
|
+
if (point.payload?.is_metadata === true) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (url && !visitedUrls.has(url)) {
|
|
515
|
+
obsoletePointIds.push(point.id);
|
|
516
|
+
}
|
|
495
517
|
}
|
|
496
|
-
|
|
497
|
-
})
|
|
498
|
-
.map((point) => point.id);
|
|
518
|
+
offset = response.next_page_offset;
|
|
519
|
+
} while (offset !== null && offset !== undefined);
|
|
499
520
|
if (obsoletePointIds.length > 0) {
|
|
500
521
|
await client.delete(collectionName, {
|
|
501
522
|
points: obsoletePointIds,
|
|
@@ -507,7 +528,11 @@ class DatabaseManager {
|
|
|
507
528
|
}
|
|
508
529
|
}
|
|
509
530
|
catch (error) {
|
|
531
|
+
// Rethrow so a failed cleanup fails the job instead of silently
|
|
532
|
+
// leaving orphans behind. This is idempotent and recomputed each
|
|
533
|
+
// run, so a rerun retries it cleanly.
|
|
510
534
|
logger.error(`Error removing obsolete chunks from Qdrant:`, error);
|
|
535
|
+
throw error;
|
|
511
536
|
}
|
|
512
537
|
}
|
|
513
538
|
static hasColumn(db, columnName) {
|
|
@@ -550,7 +575,12 @@ class DatabaseManager {
|
|
|
550
575
|
logger.info(`Deleted chunks from Qdrant for URL ${url}`);
|
|
551
576
|
}
|
|
552
577
|
catch (error) {
|
|
578
|
+
// Rethrow so the failure propagates and the job exits non-zero.
|
|
579
|
+
// A swallowed delete leaves stale chunks behind while the sync
|
|
580
|
+
// reports success. Callers that treat deletion as best-effort
|
|
581
|
+
// (e.g. 404 cleanup) wrap this call in their own try/catch.
|
|
553
582
|
logger.error(`Error deleting chunks from Qdrant for URL ${url}:`, error);
|
|
583
|
+
throw error;
|
|
554
584
|
}
|
|
555
585
|
}
|
|
556
586
|
/**
|
|
@@ -715,53 +745,64 @@ class DatabaseManager {
|
|
|
715
745
|
urlPrefix = `file://${cleanedDirPrefix}`;
|
|
716
746
|
}
|
|
717
747
|
logger.debug(`Checking for obsolete chunks with URL prefix: ${urlPrefix}`);
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
748
|
+
// Scroll through ALL matching points via next_page_offset. A single
|
|
749
|
+
// capped scroll would only examine the first page, so obsolete
|
|
750
|
+
// chunks beyond it (in collections larger than the page limit) would
|
|
751
|
+
// never be removed.
|
|
752
|
+
const filter = {
|
|
753
|
+
must: [
|
|
754
|
+
{
|
|
755
|
+
key: "url",
|
|
756
|
+
match: {
|
|
757
|
+
text: urlPrefix
|
|
729
758
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
759
|
+
}
|
|
760
|
+
],
|
|
761
|
+
must_not: [
|
|
762
|
+
{
|
|
763
|
+
key: "is_metadata",
|
|
764
|
+
match: {
|
|
765
|
+
value: true
|
|
737
766
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
}
|
|
741
|
-
const obsoletePointIds =
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
//
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
filePath
|
|
767
|
+
}
|
|
768
|
+
]
|
|
769
|
+
};
|
|
770
|
+
const obsoletePointIds = [];
|
|
771
|
+
let offset = undefined;
|
|
772
|
+
do {
|
|
773
|
+
const response = await client.scroll(collectionName, {
|
|
774
|
+
limit: 1000,
|
|
775
|
+
offset,
|
|
776
|
+
with_payload: true,
|
|
777
|
+
with_vector: false,
|
|
778
|
+
filter
|
|
779
|
+
});
|
|
780
|
+
for (const point of response.points) {
|
|
781
|
+
const url = point.payload?.url;
|
|
782
|
+
// Double check it's not a metadata record
|
|
783
|
+
if (point.payload?.is_metadata === true) {
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (!url || !url.startsWith(urlPrefix)) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
let filePath;
|
|
790
|
+
if (isRewriteMode) {
|
|
791
|
+
// URL rewrite mode: extract relative path and construct full file path
|
|
792
|
+
const config = pathConfig;
|
|
793
|
+
const relativePath = url.substring(urlPrefix.length + 1); // +1 for the '/'
|
|
794
|
+
filePath = path.join(config.path, relativePath);
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
// Direct file path mode: remove file:// prefix to match with processedFiles
|
|
798
|
+
filePath = url.startsWith('file://') ? url.substring(7) : '';
|
|
799
|
+
}
|
|
800
|
+
if (filePath && !processedFiles.has(filePath)) {
|
|
801
|
+
obsoletePointIds.push(point.id);
|
|
802
|
+
}
|
|
761
803
|
}
|
|
762
|
-
|
|
763
|
-
})
|
|
764
|
-
.map((point) => point.id);
|
|
804
|
+
offset = response.next_page_offset;
|
|
805
|
+
} while (offset !== null && offset !== undefined);
|
|
765
806
|
if (obsoletePointIds.length > 0) {
|
|
766
807
|
await client.delete(collectionName, {
|
|
767
808
|
points: obsoletePointIds,
|
|
@@ -773,7 +814,10 @@ class DatabaseManager {
|
|
|
773
814
|
}
|
|
774
815
|
}
|
|
775
816
|
catch (error) {
|
|
817
|
+
// Rethrow so a failed cleanup fails the job instead of silently
|
|
818
|
+
// leaving orphans behind (idempotent — retried cleanly next run).
|
|
776
819
|
logger.error(`Error removing obsolete chunks from Qdrant:`, error);
|
|
820
|
+
throw error;
|
|
777
821
|
}
|
|
778
822
|
}
|
|
779
823
|
}
|
package/dist/doc2vec.js
CHANGED
|
@@ -522,15 +522,33 @@ class Doc2Vec {
|
|
|
522
522
|
this.writeBrokenLinksReport();
|
|
523
523
|
logger.info(`Found ${validChunkIds.size} valid chunks across processed pages for ${config.url}`);
|
|
524
524
|
logger.section('CLEANUP');
|
|
525
|
-
// Remove 404
|
|
526
|
-
|
|
527
|
-
|
|
525
|
+
// Remove URLs that returned 404 during the crawl. A 404 is a definitive
|
|
526
|
+
// "this page is gone" signal (network errors fall through to full
|
|
527
|
+
// processing and never land here), so we purge these unconditionally —
|
|
528
|
+
// even when hasNetworkErrors below skips the broad obsolete cleanup.
|
|
529
|
+
if (crawlResult.notFoundUrls && crawlResult.notFoundUrls.size > 0) {
|
|
530
|
+
logger.info(`Removing ${crawlResult.notFoundUrls.size} not-found (404) URLs`);
|
|
528
531
|
for (const url of crawlResult.notFoundUrls) {
|
|
532
|
+
// Postgres markdown store
|
|
533
|
+
if (useMarkdownStore) {
|
|
534
|
+
try {
|
|
535
|
+
await this.markdownStore.deleteMarkdown(url);
|
|
536
|
+
}
|
|
537
|
+
catch (pgError) {
|
|
538
|
+
logger.error(`Failed to delete markdown from Postgres for ${url}:`, pgError);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Vector DB chunks
|
|
529
542
|
try {
|
|
530
|
-
|
|
543
|
+
if (dbConnection.type === 'sqlite') {
|
|
544
|
+
database_1.DatabaseManager.removeChunksByUrlSQLite(dbConnection.db, url, logger);
|
|
545
|
+
}
|
|
546
|
+
else if (dbConnection.type === 'qdrant') {
|
|
547
|
+
await database_1.DatabaseManager.removeChunksByUrlQdrant(dbConnection, url, logger);
|
|
548
|
+
}
|
|
531
549
|
}
|
|
532
|
-
catch (
|
|
533
|
-
logger.error(`Failed to delete
|
|
550
|
+
catch (dbError) {
|
|
551
|
+
logger.error(`Failed to delete chunks for 404 URL ${url}:`, dbError);
|
|
534
552
|
}
|
|
535
553
|
}
|
|
536
554
|
}
|