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 +11 -0
- package/dist/content-processor.js +10 -0
- package/dist/database.js +42 -31
- package/dist/doc2vec.js +30 -8
- package/package.json +1 -1
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
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
477
|
+
}
|
|
478
|
+
],
|
|
479
|
+
must_not: [
|
|
480
|
+
{
|
|
481
|
+
key: "is_metadata",
|
|
482
|
+
match: {
|
|
483
|
+
value: true
|
|
485
484
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
const obsoletePointIds =
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
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 (
|
|
529
|
-
logger.error(`Failed to delete
|
|
550
|
+
catch (dbError) {
|
|
551
|
+
logger.error(`Failed to delete chunks for 404 URL ${url}:`, dbError);
|
|
530
552
|
}
|
|
531
553
|
}
|
|
532
554
|
}
|