|
@@ -4,6 +4,7 @@ import { ShopDatabase, ShopContent } from '../database/Database';
|
|
|
import { QdrantService } from './QdrantService';
|
|
import { QdrantService } from './QdrantService';
|
|
|
import { EmbeddingService } from './EmbeddingService';
|
|
import { EmbeddingService } from './EmbeddingService';
|
|
|
import { QdrantCleanupService } from './QdrantCleanupService';
|
|
import { QdrantCleanupService } from './QdrantCleanupService';
|
|
|
|
|
+import { ChunkingService } from './ChunkingService';
|
|
|
import { WebhookManager } from '../webhooks/WebhookManager';
|
|
import { WebhookManager } from '../webhooks/WebhookManager';
|
|
|
import type { AppConfig } from '../config';
|
|
import type { AppConfig } from '../config';
|
|
|
|
|
|
|
@@ -19,6 +20,7 @@ export class QdrantEmbeddingWorkflow {
|
|
|
private qdrantService: QdrantService;
|
|
private qdrantService: QdrantService;
|
|
|
private embeddingService: EmbeddingService;
|
|
private embeddingService: EmbeddingService;
|
|
|
private cleanupService: QdrantCleanupService;
|
|
private cleanupService: QdrantCleanupService;
|
|
|
|
|
+ private chunkingService: ChunkingService;
|
|
|
private webhookManager: WebhookManager;
|
|
private webhookManager: WebhookManager;
|
|
|
|
|
|
|
|
constructor(
|
|
constructor(
|
|
@@ -28,6 +30,7 @@ export class QdrantEmbeddingWorkflow {
|
|
|
this.qdrantService = new QdrantService(config);
|
|
this.qdrantService = new QdrantService(config);
|
|
|
this.embeddingService = new EmbeddingService(config);
|
|
this.embeddingService = new EmbeddingService(config);
|
|
|
this.cleanupService = new QdrantCleanupService(config, database);
|
|
this.cleanupService = new QdrantCleanupService(config, database);
|
|
|
|
|
+ this.chunkingService = new ChunkingService();
|
|
|
this.webhookManager = new WebhookManager(database);
|
|
this.webhookManager = new WebhookManager(database);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -75,34 +78,11 @@ export class QdrantEmbeddingWorkflow {
|
|
|
return { contentId, embedded: false, error: 'Content unchanged and embedding exists' };
|
|
return { contentId, embedded: false, error: 'Content unchanged and embedding exists' };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Prepare content for embedding
|
|
|
|
|
- const processedContent = this.embeddingService.preprocessContent(title, content);
|
|
|
|
|
|
|
+ // Split content into chunks
|
|
|
|
|
+ const chunks = this.chunkingService.chunkContent(content, title);
|
|
|
|
|
+ const chunkStats = this.chunkingService.getChunkingStats(chunks);
|
|
|
|
|
|
|
|
- // Check if content is too long
|
|
|
|
|
- if (this.embeddingService.isTextTooLong(processedContent)) {
|
|
|
|
|
- const truncatedContent = this.embeddingService.truncateText(processedContent);
|
|
|
|
|
- logger.warn(`Content truncated for embedding: ${contentId}`);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Generate embedding
|
|
|
|
|
- const embeddingResult = await this.embeddingService.generateEmbedding({
|
|
|
|
|
- text: this.embeddingService.isTextTooLong(processedContent)
|
|
|
|
|
- ? this.embeddingService.truncateText(processedContent)
|
|
|
|
|
- : processedContent,
|
|
|
|
|
- contentId,
|
|
|
|
|
- shopId,
|
|
|
|
|
- contentType
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- if (!embeddingResult.success) {
|
|
|
|
|
- await this.cleanupService.logQdrantError(
|
|
|
|
|
- shopId,
|
|
|
|
|
- 'embedding',
|
|
|
|
|
- embeddingResult.error || 'Embedding generation failed',
|
|
|
|
|
- { contentId, contentType, url }
|
|
|
|
|
- );
|
|
|
|
|
- return { contentId, embedded: false, error: embeddingResult.error };
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ logger.info(`Content ${contentId} split into ${chunks.length} chunks (avg: ${chunkStats.avgWordCount} words/chunk)`);
|
|
|
|
|
|
|
|
// Get iterator for collection naming
|
|
// Get iterator for collection naming
|
|
|
const iterator = await this.qdrantService.getNextIterator(shop.custom_id, contentType);
|
|
const iterator = await this.qdrantService.getNextIterator(shop.custom_id, contentType);
|
|
@@ -114,58 +94,112 @@ export class QdrantEmbeddingWorkflow {
|
|
|
logger.info(`Created Qdrant collection: ${collectionName}`);
|
|
logger.info(`Created Qdrant collection: ${collectionName}`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Create point
|
|
|
|
|
- const pointId = uuidv4();
|
|
|
|
|
- const point = {
|
|
|
|
|
- id: pointId,
|
|
|
|
|
- vector: embeddingResult.embedding,
|
|
|
|
|
- payload: {
|
|
|
|
|
- shop_id: shop.custom_id,
|
|
|
|
|
- content_id: contentId,
|
|
|
|
|
- content_type: contentType,
|
|
|
|
|
- url: url,
|
|
|
|
|
- title: title || undefined,
|
|
|
|
|
- content_hash: contentHash,
|
|
|
|
|
- created_at: new Date().toISOString(),
|
|
|
|
|
|
|
+ // Process each chunk
|
|
|
|
|
+ let successfulChunks = 0;
|
|
|
|
|
+ const pointIds: string[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ for (const chunk of chunks) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Preprocess chunk content
|
|
|
|
|
+ const processedChunk = this.embeddingService.preprocessContent(null, chunk.text);
|
|
|
|
|
+
|
|
|
|
|
+ // Generate embedding for this chunk
|
|
|
|
|
+ const embeddingResult = await this.embeddingService.generateEmbedding({
|
|
|
|
|
+ text: processedChunk,
|
|
|
|
|
+ contentId: `${contentId}-chunk-${chunk.index}`,
|
|
|
|
|
+ shopId,
|
|
|
|
|
+ contentType
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!embeddingResult.success) {
|
|
|
|
|
+ logger.error(`Failed to embed chunk ${chunk.index} of content ${contentId}:`, embeddingResult.error);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Create point with chunk data
|
|
|
|
|
+ const pointId = uuidv4();
|
|
|
|
|
+ const point = {
|
|
|
|
|
+ id: pointId,
|
|
|
|
|
+ vector: embeddingResult.embedding,
|
|
|
|
|
+ payload: {
|
|
|
|
|
+ shop_id: shop.custom_id,
|
|
|
|
|
+ content_id: contentId,
|
|
|
|
|
+ chunk_id: `${contentId}-chunk-${chunk.index}`,
|
|
|
|
|
+ chunk_index: chunk.index,
|
|
|
|
|
+ total_chunks: chunks.length,
|
|
|
|
|
+ chunk_text: chunk.text,
|
|
|
|
|
+ content_type: contentType,
|
|
|
|
|
+ url: url,
|
|
|
|
|
+ title: title || undefined,
|
|
|
|
|
+ content_hash: contentHash,
|
|
|
|
|
+ created_at: new Date().toISOString(),
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Insert point into Qdrant
|
|
|
|
|
+ await this.qdrantService.upsertPoint(collectionName, point);
|
|
|
|
|
+ pointIds.push(pointId);
|
|
|
|
|
+ successfulChunks++;
|
|
|
|
|
+
|
|
|
|
|
+ // Small delay between chunks to avoid rate limiting
|
|
|
|
|
+ if (chunks.length > 1) {
|
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ logger.error(`Error embedding chunk ${chunk.index} of content ${contentId}:`, error);
|
|
|
}
|
|
}
|
|
|
- };
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Save embedding record to database (for the first/main point)
|
|
|
|
|
+ if (pointIds.length > 0) {
|
|
|
|
|
+ const now = new Date().toISOString();
|
|
|
|
|
+ const embeddingId = uuidv4();
|
|
|
|
|
|
|
|
- // Insert point into Qdrant
|
|
|
|
|
- await this.qdrantService.upsertPoint(collectionName, point);
|
|
|
|
|
|
|
+ this.database.createShopEmbedding({
|
|
|
|
|
+ id: embeddingId,
|
|
|
|
|
+ shop_content_id: contentId,
|
|
|
|
|
+ collection_name: collectionName,
|
|
|
|
|
+ point_id: pointIds[0], // Store first chunk's point ID
|
|
|
|
|
+ embedding_status: successfulChunks === chunks.length ? 'completed' : 'failed',
|
|
|
|
|
+ created_at: now,
|
|
|
|
|
+ updated_at: now
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Save embedding record to database
|
|
|
|
|
- const now = new Date().toISOString();
|
|
|
|
|
- const embeddingId = uuidv4();
|
|
|
|
|
|
|
+ if (successfulChunks < chunks.length) {
|
|
|
|
|
+ const errorMsg = `Only ${successfulChunks}/${chunks.length} chunks embedded successfully`;
|
|
|
|
|
+ logger.warn(errorMsg);
|
|
|
|
|
|
|
|
- this.database.createShopEmbedding({
|
|
|
|
|
- id: embeddingId,
|
|
|
|
|
- shop_content_id: contentId,
|
|
|
|
|
- collection_name: collectionName,
|
|
|
|
|
- point_id: pointId,
|
|
|
|
|
- embedding_status: 'completed',
|
|
|
|
|
- created_at: now,
|
|
|
|
|
- updated_at: now
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ await this.cleanupService.logQdrantError(
|
|
|
|
|
+ shopId,
|
|
|
|
|
+ 'embedding',
|
|
|
|
|
+ errorMsg,
|
|
|
|
|
+ { contentId, contentType, url, chunks: chunks.length, successful: successfulChunks }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- logger.info(`Successfully embedded content ${contentId} in collection ${collectionName}`);
|
|
|
|
|
|
|
+ logger.info(`Successfully embedded ${successfulChunks}/${chunks.length} chunks of content ${contentId} in collection ${collectionName}`);
|
|
|
|
|
|
|
|
// Trigger webhook for successful embedding
|
|
// Trigger webhook for successful embedding
|
|
|
- try {
|
|
|
|
|
- await this.webhookManager.notifyQdrantEmbeddingCompleted(shopId, {
|
|
|
|
|
- content_id: contentId,
|
|
|
|
|
- content_type: contentType,
|
|
|
|
|
- collection_name: collectionName,
|
|
|
|
|
- point_id: pointId
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Failed to send embedding completed webhook:', error);
|
|
|
|
|
|
|
+ if (successfulChunks > 0) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.webhookManager.notifyQdrantEmbeddingCompleted(shopId, {
|
|
|
|
|
+ content_id: contentId,
|
|
|
|
|
+ content_type: contentType,
|
|
|
|
|
+ collection_name: collectionName,
|
|
|
|
|
+ point_id: pointIds[0] // First chunk's point ID
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Failed to send embedding completed webhook:', error);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
contentId,
|
|
contentId,
|
|
|
- embedded: true,
|
|
|
|
|
|
|
+ embedded: successfulChunks > 0,
|
|
|
collectionName,
|
|
collectionName,
|
|
|
- pointId
|
|
|
|
|
|
|
+ pointId: pointIds[0]
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|