doc2vec 2.10.2 → 2.10.4

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 CHANGED
@@ -151,6 +151,16 @@ Configuration is managed through two files:
151
151
  OPENAI_API_KEY="sk-..."
152
152
  OPENAI_MODEL="text-embedding-3-large" # Optional, defaults to text-embedding-3-large
153
153
 
154
+ # Optional: Override the OpenAI API base URL. Useful for pointing the
155
+ # OpenAI SDK at an OpenAI-compatible endpoint such as Ollama, LM Studio,
156
+ # llama.cpp, vLLM, or a proxy gateway. When unset, the SDK uses the
157
+ # default OpenAI endpoint. Equivalent to the `embedding.openai.base_url`
158
+ # field in config.yaml; the env var wins if both are set.
159
+ # Example values:
160
+ # OPENAI_BASE_URL="http://localhost:11434/v1" # Ollama
161
+ # OPENAI_BASE_URL="https://gateway.example.com/v1" # proxy / gateway
162
+ OPENAI_BASE_URL="http://localhost:11434/v1"
163
+
154
164
  # Optional: Embedding dimension size (defaults to 3072)
155
165
  EMBEDDING_DIMENSION="3072"
156
166
 
@@ -277,6 +287,7 @@ Configuration is managed through two files:
277
287
  openai:
278
288
  api_key: '${OPENAI_API_KEY}' # Optional, uses env var by default
279
289
  model: 'text-embedding-3-large' # Optional, defaults to text-embedding-3-large
290
+ # base_url: 'http://localhost:11434/v1' # Optional, override OpenAI API base URL for Ollama / other OpenAI-compatible endpoints. Falls back to OPENAI_BASE_URL env var.
280
291
  # For Azure OpenAI, use this instead:
281
292
  # azure:
282
293
  # api_key: '${AZURE_OPENAI_KEY}'
@@ -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
@@ -462,40 +462,51 @@ class DatabaseManager {
462
462
  static async removeObsoleteChunksQdrant(db, visitedUrls, urlPrefix, logger) {
463
463
  const { client, collectionName } = db;
464
464
  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
- }
465
+ // Scroll through ALL points that match the URL prefix but are not
466
+ // metadata points. Qdrant caps a single scroll at its page limit, so
467
+ // we must paginate via next_page_offset — otherwise large collections
468
+ // (e.g. tens of thousands of chunks) would only have their first page
469
+ // examined and obsolete chunks beyond it would never be removed.
470
+ const filter = {
471
+ must: [
472
+ {
473
+ key: "url",
474
+ match: {
475
+ text: urlPrefix
477
476
  }
478
- ],
479
- must_not: [
480
- {
481
- key: "is_metadata",
482
- match: {
483
- value: true
484
- }
477
+ }
478
+ ],
479
+ must_not: [
480
+ {
481
+ key: "is_metadata",
482
+ match: {
483
+ value: true
485
484
  }
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;
485
+ }
486
+ ]
487
+ };
488
+ const obsoletePointIds = [];
489
+ let offset = undefined;
490
+ do {
491
+ const response = await client.scroll(collectionName, {
492
+ limit: 1000,
493
+ offset,
494
+ with_payload: true,
495
+ with_vector: false,
496
+ filter
497
+ });
498
+ for (const point of response.points) {
499
+ const url = point.payload?.url;
500
+ // Double check it's not a metadata record
501
+ if (point.payload?.is_metadata === true) {
502
+ continue;
503
+ }
504
+ if (url && !visitedUrls.has(url)) {
505
+ obsoletePointIds.push(point.id);
506
+ }
495
507
  }
496
- return url && !visitedUrls.has(url);
497
- })
498
- .map((point) => point.id);
508
+ offset = response.next_page_offset;
509
+ } while (offset !== null && offset !== undefined);
499
510
  if (obsoletePointIds.length > 0) {
500
511
  await client.delete(collectionName, {
501
512
  points: obsoletePointIds,
package/dist/doc2vec.js CHANGED
@@ -96,13 +96,17 @@ class Doc2Vec {
96
96
  else {
97
97
  const openaiApiKey = embeddingConfig.openai?.api_key || process.env.OPENAI_API_KEY;
98
98
  const openaiModel = embeddingConfig.openai?.model || process.env.OPENAI_MODEL || 'text-embedding-3-large';
99
+ const openaiBaseURL = embeddingConfig.openai?.base_url || process.env.OPENAI_BASE_URL;
99
100
  if (!openaiApiKey) {
100
101
  this.logger.error('OpenAI requires api_key to be configured');
101
102
  process.exit(1);
102
103
  }
103
- this.openai = new openai_1.OpenAI({ apiKey: openaiApiKey });
104
+ this.openai = new openai_1.OpenAI({
105
+ apiKey: openaiApiKey,
106
+ ...(openaiBaseURL && { baseURL: openaiBaseURL }),
107
+ });
104
108
  this.embeddingModel = openaiModel;
105
- this.logger.info(`Using OpenAI with model: ${openaiModel} (${this.embeddingDimension} dimensions)`);
109
+ this.logger.info(`Using OpenAI with model: ${openaiModel} (${this.embeddingDimension} dimensions)${openaiBaseURL ? ` via ${openaiBaseURL}` : ''}`);
106
110
  }
107
111
  this.contentProcessor = new content_processor_1.ContentProcessor(this.logger);
108
112
  // Initialize Postgres markdown store if configured
@@ -518,15 +522,33 @@ class Doc2Vec {
518
522
  this.writeBrokenLinksReport();
519
523
  logger.info(`Found ${validChunkIds.size} valid chunks across processed pages for ${config.url}`);
520
524
  logger.section('CLEANUP');
521
- // Remove 404 URLs from the Postgres markdown store
522
- if (useMarkdownStore && crawlResult.notFoundUrls && crawlResult.notFoundUrls.size > 0) {
523
- 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`);
524
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
525
542
  try {
526
- 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
+ }
527
549
  }
528
- catch (pgError) {
529
- 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);
530
552
  }
531
553
  }
532
554
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc2vec",
3
- "version": "2.10.2",
3
+ "version": "2.10.4",
4
4
  "type": "commonjs",
5
5
  "description": "",
6
6
  "main": "dist/doc2vec.js",