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.
@@ -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
- logger.error(`Failed to update metadata value for ${key}:`, error);
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
- console.error("Error storing chunk in Qdrant:", error);
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
- // Get all points that match the URL prefix but are not metadata points
466
- const response = await client.scroll(collectionName, {
467
- limit: 10000,
468
- with_payload: true,
469
- with_vector: false,
470
- filter: {
471
- must: [
472
- {
473
- key: "url",
474
- match: {
475
- text: urlPrefix
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
- must_not: [
480
- {
481
- key: "is_metadata",
482
- match: {
483
- value: true
484
- }
487
+ }
488
+ ],
489
+ must_not: [
490
+ {
491
+ key: "is_metadata",
492
+ match: {
493
+ value: true
485
494
  }
486
- ]
487
- }
488
- });
489
- const obsoletePointIds = response.points
490
- .filter((point) => {
491
- const url = point.payload?.url;
492
- // Double check it's not a metadata record
493
- if (point.payload?.is_metadata === true) {
494
- return false;
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
- return url && !visitedUrls.has(url);
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
- const response = await client.scroll(collectionName, {
719
- limit: 10000,
720
- with_payload: true,
721
- with_vector: false,
722
- filter: {
723
- must: [
724
- {
725
- key: "url",
726
- match: {
727
- text: urlPrefix
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
- must_not: [
732
- {
733
- key: "is_metadata",
734
- match: {
735
- value: true
736
- }
759
+ }
760
+ ],
761
+ must_not: [
762
+ {
763
+ key: "is_metadata",
764
+ match: {
765
+ value: true
737
766
  }
738
- ]
739
- }
740
- });
741
- const obsoletePointIds = response.points
742
- .filter((point) => {
743
- const url = point.payload?.url;
744
- // Double check it's not a metadata record
745
- if (point.payload?.is_metadata === true) {
746
- return false;
747
- }
748
- if (!url || !url.startsWith(urlPrefix)) {
749
- return false;
750
- }
751
- let filePath;
752
- if (isRewriteMode) {
753
- // URL rewrite mode: extract relative path and construct full file path
754
- const config = pathConfig;
755
- const relativePath = url.substring(urlPrefix.length + 1); // +1 for the '/'
756
- filePath = path.join(config.path, relativePath);
757
- }
758
- else {
759
- // Direct file path mode: remove file:// prefix to match with processedFiles
760
- filePath = url.startsWith('file://') ? url.substring(7) : '';
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
- return filePath && !processedFiles.has(filePath);
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 URLs from the Postgres markdown store
526
- if (useMarkdownStore && crawlResult.notFoundUrls && crawlResult.notFoundUrls.size > 0) {
527
- logger.info(`Removing ${crawlResult.notFoundUrls.size} not-found URLs from markdown store`);
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
- await this.markdownStore.deleteMarkdown(url);
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 (pgError) {
533
- logger.error(`Failed to delete markdown from Postgres for ${url}:`, pgError);
550
+ catch (dbError) {
551
+ logger.error(`Failed to delete chunks for 404 URL ${url}:`, dbError);
534
552
  }
535
553
  }
536
554
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc2vec",
3
- "version": "2.10.3",
3
+ "version": "2.10.5",
4
4
  "type": "commonjs",
5
5
  "description": "",
6
6
  "main": "dist/doc2vec.js",