|
@@ -78,7 +78,7 @@ export class KnowledgeBaseService {
|
|
|
processingMode: processingMode,
|
|
processingMode: processingMode,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 分類(グループ)の関連付け
|
|
|
|
|
|
|
+ // Associate groups
|
|
|
if (config?.groupIds && config.groupIds.length > 0) {
|
|
if (config?.groupIds && config.groupIds.length > 0) {
|
|
|
const groups = await this.groupRepository.find({
|
|
const groups = await this.groupRepository.find({
|
|
|
where: { id: In(config.groupIds), tenantId: tenantId }
|
|
where: { id: In(config.groupIds), tenantId: tenantId }
|
|
@@ -141,14 +141,14 @@ export class KnowledgeBaseService {
|
|
|
}
|
|
}
|
|
|
return this.kbRepository.find({
|
|
return this.kbRepository.find({
|
|
|
where,
|
|
where,
|
|
|
- relations: ['groups'], // グループリレーションをロード
|
|
|
|
|
|
|
+ relations: ['groups'], // Load group relations
|
|
|
order: { createdAt: 'DESC' },
|
|
order: { createdAt: 'DESC' },
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async searchKnowledge(userId: string, tenantId: string, query: string, topK: number = 5) {
|
|
async searchKnowledge(userId: string, tenantId: string, query: string, topK: number = 5) {
|
|
|
try {
|
|
try {
|
|
|
- // 環境変数のデフォルト次元数を使用してシミュレーションベクトルを生成
|
|
|
|
|
|
|
+ // Generate simulation vector using default dimensions from environment variable
|
|
|
const defaultDimensions = parseInt(
|
|
const defaultDimensions = parseInt(
|
|
|
process.env.DEFAULT_VECTOR_DIMENSIONS || '2560',
|
|
process.env.DEFAULT_VECTOR_DIMENSIONS || '2560',
|
|
|
);
|
|
);
|
|
@@ -246,11 +246,11 @@ export class KnowledgeBaseService {
|
|
|
`RAG search failed for user ${userId}:`,
|
|
`RAG search failed for user ${userId}:`,
|
|
|
error.stack || error.message,
|
|
error.stack || error.message,
|
|
|
);
|
|
);
|
|
|
- // エラーをスローするのではなく空の結果を返し、システムの稼働を継続させる
|
|
|
|
|
|
|
+ // Return empty result instead of throwing error to keep system running
|
|
|
return {
|
|
return {
|
|
|
searchResults: [],
|
|
searchResults: [],
|
|
|
sources: [],
|
|
sources: [],
|
|
|
- ragPrompt: query, // オリジナルのクエリを使用
|
|
|
|
|
|
|
+ ragPrompt: query, // Use original query
|
|
|
hasRelevantContent: false,
|
|
hasRelevantContent: false,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
@@ -345,18 +345,18 @@ export class KnowledgeBaseService {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // メモリ監視 - 処理前チェック
|
|
|
|
|
|
|
+ // Memory monitor - pre-processing check
|
|
|
const memBefore = this.memoryMonitor.getMemoryUsage();
|
|
const memBefore = this.memoryMonitor.getMemoryUsage();
|
|
|
- this.logger.log(`メモリ状態 - 処理前: ${memBefore.heapUsed}/${memBefore.heapTotal}MB`);
|
|
|
|
|
|
|
+ this.logger.log(`Memory state - before processing: ${memBefore.heapUsed}/${memBefore.heapTotal}MB`);
|
|
|
|
|
|
|
|
- // モードに基づいて処理フローを選択
|
|
|
|
|
|
|
+ // Select processing flow based on mode
|
|
|
const mode = config?.mode || 'fast';
|
|
const mode = config?.mode || 'fast';
|
|
|
|
|
|
|
|
if (mode === 'precise') {
|
|
if (mode === 'precise') {
|
|
|
- // 精密モード - Vision Pipeline を使用
|
|
|
|
|
|
|
+ // Precise mode - use Vision Pipeline
|
|
|
await this.processPreciseMode(kb, userId, tenantId, config);
|
|
await this.processPreciseMode(kb, userId, tenantId, config);
|
|
|
} else {
|
|
} else {
|
|
|
- // 高速モード - Tika を使用
|
|
|
|
|
|
|
+ // Fast mode - use Tika
|
|
|
await this.processFastMode(kb, userId, tenantId, config);
|
|
await this.processFastMode(kb, userId, tenantId, config);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -368,13 +368,13 @@ export class KnowledgeBaseService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 高速モード処理(既存フロー)
|
|
|
|
|
|
|
+ * Fast mode processing (existing flow)
|
|
|
*/
|
|
*/
|
|
|
private async processFastMode(kb: KnowledgeBase, userId: string, tenantId: string, config?: any) {
|
|
private async processFastMode(kb: KnowledgeBase, userId: string, tenantId: string, config?: any) {
|
|
|
- // 1. Tika を使用してテキストを抽出
|
|
|
|
|
|
|
+ // 1. Extract text using Tika
|
|
|
let text = await this.tikaService.extractText(kb.storagePath);
|
|
let text = await this.tikaService.extractText(kb.storagePath);
|
|
|
|
|
|
|
|
- // 画像ファイルの場合はビジョンモデルを使用
|
|
|
|
|
|
|
+ // Use vision model for image files
|
|
|
if (this.visionService.isImageFile(kb.mimetype)) {
|
|
if (this.visionService.isImageFile(kb.mimetype)) {
|
|
|
const settings = await this.tenantService.getSettings(tenantId || 'default');
|
|
const settings = await this.tenantService.getSettings(tenantId || 'default');
|
|
|
const visionModelId = settings?.selectedVisionId;
|
|
const visionModelId = settings?.selectedVisionId;
|
|
@@ -398,37 +398,37 @@ export class KnowledgeBaseService {
|
|
|
this.logger.warn(this.i18nService.getMessage('noTextExtracted'));
|
|
this.logger.warn(this.i18nService.getMessage('noTextExtracted'));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // テキストサイズを確認
|
|
|
|
|
|
|
+ // Check text size
|
|
|
const textSizeMB = Math.round(text.length / 1024 / 1024);
|
|
const textSizeMB = Math.round(text.length / 1024 / 1024);
|
|
|
if (textSizeMB > 50) {
|
|
if (textSizeMB > 50) {
|
|
|
this.logger.warn(this.i18nService.formatMessage('extractedTextTooLarge', { size: textSizeMB }));
|
|
this.logger.warn(this.i18nService.formatMessage('extractedTextTooLarge', { size: textSizeMB }));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // テキストをデータベースに保存
|
|
|
|
|
|
|
+ // Save text to database
|
|
|
await this.kbRepository.update(kb.id, { content: text });
|
|
await this.kbRepository.update(kb.id, { content: text });
|
|
|
await this.updateStatus(kb.id, FileStatus.EXTRACTED);
|
|
await this.updateStatus(kb.id, FileStatus.EXTRACTED);
|
|
|
|
|
|
|
|
- // 非同期ベクトル化
|
|
|
|
|
|
|
+ // Async vectorization
|
|
|
await this.vectorizeToElasticsearch(kb.id, userId, tenantId, text, config).catch((err) => {
|
|
await this.vectorizeToElasticsearch(kb.id, userId, tenantId, text, config).catch((err) => {
|
|
|
this.logger.error(`Error vectorizing file ${kb.id}`, err);
|
|
this.logger.error(`Error vectorizing file ${kb.id}`, err);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 自動タイトル生成 (非同期的に実行)
|
|
|
|
|
|
|
+ // Auto-generate title (async execution)
|
|
|
this.generateTitle(kb.id).catch((err) => {
|
|
this.generateTitle(kb.id).catch((err) => {
|
|
|
this.logger.error(`Error generating title for file ${kb.id}`, err);
|
|
this.logger.error(`Error generating title for file ${kb.id}`, err);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 非同期的に PDF 変換をトリガー(ドキュメントファイルの場合)
|
|
|
|
|
|
|
+ // Trigger PDF conversion asynchronously (for document files)
|
|
|
this.ensurePDFExists(kb.id, userId, tenantId).catch((err) => {
|
|
this.ensurePDFExists(kb.id, userId, tenantId).catch((err) => {
|
|
|
this.logger.warn(this.i18nService.formatMessage('pdfConversionFailedDetail', { id: kb.id }), err);
|
|
this.logger.warn(this.i18nService.formatMessage('pdfConversionFailedDetail', { id: kb.id }), err);
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 精密モード処理(新規フロー)
|
|
|
|
|
|
|
+ * Precise mode processing (new flow)
|
|
|
*/
|
|
*/
|
|
|
private async processPreciseMode(kb: KnowledgeBase, userId: string, tenantId: string, config?: any) {
|
|
private async processPreciseMode(kb: KnowledgeBase, userId: string, tenantId: string, config?: any) {
|
|
|
- // 精密モードがサポートされているか確認
|
|
|
|
|
|
|
+ // Check if precise mode is supported
|
|
|
const preciseFormats = ['.pdf', '.doc', '.docx', '.ppt', '.pptx'];
|
|
const preciseFormats = ['.pdf', '.doc', '.docx', '.ppt', '.pptx'];
|
|
|
const ext = kb.originalName.toLowerCase().substring(kb.originalName.lastIndexOf('.'));
|
|
const ext = kb.originalName.toLowerCase().substring(kb.originalName.lastIndexOf('.'));
|
|
|
|
|
|
|
@@ -439,7 +439,7 @@ export class KnowledgeBaseService {
|
|
|
return this.processFastMode(kb, userId, tenantId, config);
|
|
return this.processFastMode(kb, userId, tenantId, config);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Vision モデルが設定されているか確認
|
|
|
|
|
|
|
+ // Check if Vision model is configured
|
|
|
const settings = await this.tenantService.getSettings(tenantId || 'default');
|
|
const settings = await this.tenantService.getSettings(tenantId || 'default');
|
|
|
const visionModelId = settings?.selectedVisionId;
|
|
const visionModelId = settings?.selectedVisionId;
|
|
|
if (!visionModelId) {
|
|
if (!visionModelId) {
|
|
@@ -461,7 +461,7 @@ export class KnowledgeBaseService {
|
|
|
return this.processFastMode(kb, userId, tenantId, config);
|
|
return this.processFastMode(kb, userId, tenantId, config);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Vision Pipeline を呼び出し
|
|
|
|
|
|
|
+ // Call Vision Pipeline
|
|
|
try {
|
|
try {
|
|
|
const result = await this.visionPipelineService.processPreciseMode(
|
|
const result = await this.visionPipelineService.processPreciseMode(
|
|
|
kb.storagePath,
|
|
kb.storagePath,
|
|
@@ -481,7 +481,7 @@ export class KnowledgeBaseService {
|
|
|
return this.processFastMode(kb, userId, tenantId, config);
|
|
return this.processFastMode(kb, userId, tenantId, config);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // テキスト内容をデータベースに保存
|
|
|
|
|
|
|
+ // Save text content to database
|
|
|
const combinedText = result.results.map(r => r.text).join('\n\n');
|
|
const combinedText = result.results.map(r => r.text).join('\n\n');
|
|
|
const metadata = {
|
|
const metadata = {
|
|
|
processedPages: result.processedPages,
|
|
processedPages: result.processedPages,
|
|
@@ -505,18 +505,18 @@ export class KnowledgeBaseService {
|
|
|
this.i18nService.formatMessage('preciseModeComplete', { pages: result.processedPages, cost: result.cost.toFixed(2) })
|
|
this.i18nService.formatMessage('preciseModeComplete', { pages: result.processedPages, cost: result.cost.toFixed(2) })
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // 非同期でベクトル化し、Elasticsearch にインデックス
|
|
|
|
|
- // 各ページを独立したドキュメントとして作成し、メタデータを保持
|
|
|
|
|
|
|
+ // Async vectorization and Elasticsearch indexing
|
|
|
|
|
+ // Create each page as separate document with metadata
|
|
|
this.indexPreciseResults(kb, userId, tenantId, kb.embeddingModelId, result.results).catch((err) => {
|
|
this.indexPreciseResults(kb, userId, tenantId, kb.embeddingModelId, result.results).catch((err) => {
|
|
|
this.logger.error(`Error indexing precise results for ${kb.id}`, err);
|
|
this.logger.error(`Error indexing precise results for ${kb.id}`, err);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 非同期で PDF 変換をトリガー
|
|
|
|
|
|
|
+ // Trigger PDF conversion asynchronously
|
|
|
this.ensurePDFExists(kb.id, userId, tenantId).catch((err) => {
|
|
this.ensurePDFExists(kb.id, userId, tenantId).catch((err) => {
|
|
|
this.logger.warn(`Initial PDF conversion failed for ${kb.id}`, err);
|
|
this.logger.warn(`Initial PDF conversion failed for ${kb.id}`, err);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 自動タイトル生成 (非同期的に実行)
|
|
|
|
|
|
|
+ // Auto-generate title (async execution)
|
|
|
this.generateTitle(kb.id).catch((err) => {
|
|
this.generateTitle(kb.id).catch((err) => {
|
|
|
this.logger.error(`Error generating title for file ${kb.id}`, err);
|
|
this.logger.error(`Error generating title for file ${kb.id}`, err);
|
|
|
});
|
|
});
|
|
@@ -528,7 +528,7 @@ export class KnowledgeBaseService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 精密モードの結果をインデックス
|
|
|
|
|
|
|
+ * Index precise mode results
|
|
|
*/
|
|
*/
|
|
|
private async indexPreciseResults(
|
|
private async indexPreciseResults(
|
|
|
kb: KnowledgeBase,
|
|
kb: KnowledgeBase,
|
|
@@ -539,11 +539,11 @@ export class KnowledgeBaseService {
|
|
|
): Promise<void> {
|
|
): Promise<void> {
|
|
|
this.logger.log(`Indexing ${results.length} precise results for ${kb.id}`);
|
|
this.logger.log(`Indexing ${results.length} precise results for ${kb.id}`);
|
|
|
|
|
|
|
|
- // インデックスの存在を確認 - 実際のモデル次元数を取得
|
|
|
|
|
|
|
+ // Check index existence - get actual model dimensions
|
|
|
const actualDimensions = await this.getActualModelDimensions(embeddingModelId, userId, tenantId);
|
|
const actualDimensions = await this.getActualModelDimensions(embeddingModelId, userId, tenantId);
|
|
|
await this.elasticsearchService.createIndexIfNotExists(actualDimensions);
|
|
await this.elasticsearchService.createIndexIfNotExists(actualDimensions);
|
|
|
|
|
|
|
|
- // ベクトル化とインデックスをバッチ処理
|
|
|
|
|
|
|
+ // Batch vectorization and indexing
|
|
|
const batchSize = parseInt(process.env.CHUNK_BATCH_SIZE || '50');
|
|
const batchSize = parseInt(process.env.CHUNK_BATCH_SIZE || '50');
|
|
|
|
|
|
|
|
for (let i = 0; i < results.length; i += batchSize) {
|
|
for (let i = 0; i < results.length; i += batchSize) {
|
|
@@ -551,14 +551,14 @@ export class KnowledgeBaseService {
|
|
|
const texts = batch.map(r => r.text);
|
|
const texts = batch.map(r => r.text);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- // ベクトルを生成
|
|
|
|
|
|
|
+ // Generate vectors
|
|
|
const embeddings = await this.embeddingService.getEmbeddings(
|
|
const embeddings = await this.embeddingService.getEmbeddings(
|
|
|
texts,
|
|
texts,
|
|
|
userId,
|
|
userId,
|
|
|
embeddingModelId
|
|
embeddingModelId
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // 各結果をインデックス
|
|
|
|
|
|
|
+ // Index each result
|
|
|
for (let j = 0; j < batch.length; j++) {
|
|
for (let j = 0; j < batch.length; j++) {
|
|
|
const result = batch[j];
|
|
const result = batch[j];
|
|
|
const embedding = embeddings[j];
|
|
const embedding = embeddings[j];
|
|
@@ -588,30 +588,30 @@ export class KnowledgeBaseService {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this.logger.log(`バッチ ${Math.floor(i / batchSize) + 1} 完了: ${batch.length} ページ`);
|
|
|
|
|
|
|
+ this.logger.log(`Batch ${Math.floor(i / batchSize) + 1} completed: ${batch.length} pages`);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- this.logger.error(`バッチ ${Math.floor(i / batchSize) + 1} の処理に失敗しました`, error);
|
|
|
|
|
|
|
+ this.logger.error(`Batch ${Math.floor(i / batchSize) + 1} processing failed`, error);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
await this.updateStatus(kb.id, FileStatus.VECTORIZED);
|
|
await this.updateStatus(kb.id, FileStatus.VECTORIZED);
|
|
|
- this.logger.log(`精密モードのインデックス完了: ${results.length} ページ`);
|
|
|
|
|
|
|
+ this.logger.log(`Precise mode indexing completed: ${results.length} pages`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * PDF の特定ページの画像を取得
|
|
|
|
|
|
|
+ * Get specific page of PDF as image
|
|
|
*/
|
|
*/
|
|
|
async getPageAsImage(fileId: string, pageIndex: number, userId: string, tenantId: string): Promise<string> {
|
|
async getPageAsImage(fileId: string, pageIndex: number, userId: string, tenantId: string): Promise<string> {
|
|
|
const pdfPath = await this.ensurePDFExists(fileId, userId, tenantId);
|
|
const pdfPath = await this.ensurePDFExists(fileId, userId, tenantId);
|
|
|
|
|
|
|
|
- // 特定のページを変換
|
|
|
|
|
|
|
+ // Convert specific pages
|
|
|
const result = await this.pdf2ImageService.convertToImages(pdfPath, {
|
|
const result = await this.pdf2ImageService.convertToImages(pdfPath, {
|
|
|
density: 150,
|
|
density: 150,
|
|
|
quality: 75,
|
|
quality: 75,
|
|
|
format: 'jpeg',
|
|
format: 'jpeg',
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 対応するページ番号の画像を見つける
|
|
|
|
|
|
|
+ // Find images for corresponding page numbers
|
|
|
const pageImage = result.images.find(img => img.pageIndex === pageIndex + 1);
|
|
const pageImage = result.images.find(img => img.pageIndex === pageIndex + 1);
|
|
|
if (!pageImage) {
|
|
if (!pageImage) {
|
|
|
throw new NotFoundException(this.i18nService.formatMessage('pageImageNotFoundDetail', { page: pageIndex + 1 }));
|
|
throw new NotFoundException(this.i18nService.formatMessage('pageImageNotFoundDetail', { page: pageIndex + 1 }));
|
|
@@ -631,14 +631,14 @@ export class KnowledgeBaseService {
|
|
|
const kb = await this.kbRepository.findOne({ where: { id: kbId, tenantId } });
|
|
const kb = await this.kbRepository.findOne({ where: { id: kbId, tenantId } });
|
|
|
if (!kb) return;
|
|
if (!kb) return;
|
|
|
|
|
|
|
|
- // メモリ監視 - ベクトル化前チェック
|
|
|
|
|
|
|
+ // Memory monitor - pre-vectorization check
|
|
|
const memBeforeChunk = this.memoryMonitor.getMemoryUsage();
|
|
const memBeforeChunk = this.memoryMonitor.getMemoryUsage();
|
|
|
this.logger.log(
|
|
this.logger.log(
|
|
|
- `ベクトル化前メモリ: ${memBeforeChunk.heapUsed}/${memBeforeChunk.heapTotal}MB`,
|
|
|
|
|
|
|
+ `Pre-vectorization memory: ${memBeforeChunk.heapUsed}/${memBeforeChunk.heapTotal}MB`,
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
this.logger.debug(`File ${kbId}: Validating chunk config...`);
|
|
this.logger.debug(`File ${kbId}: Validating chunk config...`);
|
|
|
- // 1. チャンク設定の検証と修正(モデルの制限と環境変数に基づく)
|
|
|
|
|
|
|
+ // 1. Validate and fix chunk config (based on model limits and env vars)
|
|
|
const validatedConfig = await this.chunkConfigService.validateChunkConfig(
|
|
const validatedConfig = await this.chunkConfigService.validateChunkConfig(
|
|
|
kb.chunkSize,
|
|
kb.chunkSize,
|
|
|
kb.chunkOverlap,
|
|
kb.chunkOverlap,
|
|
@@ -647,13 +647,13 @@ export class KnowledgeBaseService {
|
|
|
);
|
|
);
|
|
|
this.logger.debug(`File ${kbId}: Chunk config validated.`);
|
|
this.logger.debug(`File ${kbId}: Chunk config validated.`);
|
|
|
|
|
|
|
|
- // 設定が修正された場合、警告を記録しデータベースを更新
|
|
|
|
|
|
|
+ // If config modified, log warning and update database
|
|
|
if (validatedConfig.warnings.length > 0) {
|
|
if (validatedConfig.warnings.length > 0) {
|
|
|
this.logger.warn(
|
|
this.logger.warn(
|
|
|
this.i18nService.formatMessage('chunkConfigCorrection', { warnings: validatedConfig.warnings.join(', ') })
|
|
this.i18nService.formatMessage('chunkConfigCorrection', { warnings: validatedConfig.warnings.join(', ') })
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // データベース内の設定を更新
|
|
|
|
|
|
|
+ // Update config in database
|
|
|
if (validatedConfig.chunkSize !== kb.chunkSize ||
|
|
if (validatedConfig.chunkSize !== kb.chunkSize ||
|
|
|
validatedConfig.chunkOverlap !== kb.chunkOverlap) {
|
|
validatedConfig.chunkOverlap !== kb.chunkOverlap) {
|
|
|
await this.kbRepository.update(kbId, {
|
|
await this.kbRepository.update(kbId, {
|
|
@@ -663,7 +663,7 @@ export class KnowledgeBaseService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 設定サマリーを表示(実際に適用される上限を含む)
|
|
|
|
|
|
|
+ // Display config summary (including actual limits applied)
|
|
|
this.logger.debug(`File ${kbId}: Getting config summary...`);
|
|
this.logger.debug(`File ${kbId}: Getting config summary...`);
|
|
|
const configSummary = await this.chunkConfigService.getConfigSummary(
|
|
const configSummary = await this.chunkConfigService.getConfigSummary(
|
|
|
validatedConfig.chunkSize,
|
|
validatedConfig.chunkSize,
|
|
@@ -671,16 +671,16 @@ export class KnowledgeBaseService {
|
|
|
kb.embeddingModelId,
|
|
kb.embeddingModelId,
|
|
|
userId,
|
|
userId,
|
|
|
);
|
|
);
|
|
|
- this.logger.log(`チャンク設定: ${configSummary}`);
|
|
|
|
|
- this.logger.log(`設定上限: チャンク=${validatedConfig.effectiveMaxChunkSize}, 重複=${validatedConfig.effectiveMaxOverlapSize}`);
|
|
|
|
|
|
|
+ this.logger.log(`Chunk config: ${configSummary}`);
|
|
|
|
|
+ this.logger.log(`Config limits: chunk=${validatedConfig.effectiveMaxChunkSize}, overlap=${validatedConfig.effectiveMaxOverlapSize}`);
|
|
|
|
|
|
|
|
- // 2. 検証済みの設定を使用してチャンク分割
|
|
|
|
|
|
|
+ // 2. Split text using validated config
|
|
|
const chunks = this.textChunkerService.chunkText(
|
|
const chunks = this.textChunkerService.chunkText(
|
|
|
text,
|
|
text,
|
|
|
validatedConfig.chunkSize,
|
|
validatedConfig.chunkSize,
|
|
|
validatedConfig.chunkOverlap,
|
|
validatedConfig.chunkOverlap,
|
|
|
);
|
|
);
|
|
|
- this.logger.log(`ファイル ${kbId} から ${chunks.length} 個のテキストブロックを分割しました`);
|
|
|
|
|
|
|
+ this.logger.log(`File ${kbId} split into ${chunks.length} text blocks`);
|
|
|
|
|
|
|
|
if (chunks.length === 0) {
|
|
if (chunks.length === 0) {
|
|
|
this.logger.warn(this.i18nService.formatMessage('noChunksGenerated', { id: kbId }));
|
|
this.logger.warn(this.i18nService.formatMessage('noChunksGenerated', { id: kbId }));
|
|
@@ -688,7 +688,7 @@ export class KnowledgeBaseService {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 3. チャンク数が妥当か確認
|
|
|
|
|
|
|
+ // 3. Validate chunk count is reasonable
|
|
|
const estimatedChunkCount = this.chunkConfigService.estimateChunkCount(
|
|
const estimatedChunkCount = this.chunkConfigService.estimateChunkCount(
|
|
|
text.length,
|
|
text.length,
|
|
|
validatedConfig.chunkSize,
|
|
validatedConfig.chunkSize,
|
|
@@ -699,7 +699,7 @@ export class KnowledgeBaseService {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 4. 推奨バッチサイズを取得(モデルの制限に基づく)
|
|
|
|
|
|
|
+ // 4. Get recommended batch size (based on model limits)
|
|
|
const recommendedBatchSize = await this.chunkConfigService.getRecommendedBatchSize(
|
|
const recommendedBatchSize = await this.chunkConfigService.getRecommendedBatchSize(
|
|
|
kb.embeddingModelId,
|
|
kb.embeddingModelId,
|
|
|
userId,
|
|
userId,
|
|
@@ -707,20 +707,20 @@ export class KnowledgeBaseService {
|
|
|
parseInt(process.env.CHUNK_BATCH_SIZE || '100'),
|
|
parseInt(process.env.CHUNK_BATCH_SIZE || '100'),
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // 5. メモリ使用量を推定
|
|
|
|
|
|
|
+ // 5. Estimate memory usage
|
|
|
const avgChunkSize = chunks.reduce((sum, c) => sum + c.content.length, 0) / chunks.length;
|
|
const avgChunkSize = chunks.reduce((sum, c) => sum + c.content.length, 0) / chunks.length;
|
|
|
const estimatedMemory = this.memoryMonitor.estimateMemoryUsage(
|
|
const estimatedMemory = this.memoryMonitor.estimateMemoryUsage(
|
|
|
chunks.length,
|
|
chunks.length,
|
|
|
avgChunkSize,
|
|
avgChunkSize,
|
|
|
parseInt(process.env.DEFAULT_VECTOR_DIMENSIONS || '2560'),
|
|
parseInt(process.env.DEFAULT_VECTOR_DIMENSIONS || '2560'),
|
|
|
);
|
|
);
|
|
|
- this.logger.log(`推定メモリ使用量: ${estimatedMemory}MB (バッチサイズ: ${recommendedBatchSize})`);
|
|
|
|
|
|
|
+ this.logger.log(`Estimated memory usage: ${estimatedMemory}MB (batch size: ${recommendedBatchSize})`);
|
|
|
|
|
|
|
|
- // 6. 実際のモデル次元数を取得し、インデックスの存在を確認
|
|
|
|
|
|
|
+ // 6. Get actual model dimensions and check index exists
|
|
|
const actualDimensions = await this.getActualModelDimensions(kb.embeddingModelId, userId, tenantId);
|
|
const actualDimensions = await this.getActualModelDimensions(kb.embeddingModelId, userId, tenantId);
|
|
|
await this.elasticsearchService.createIndexIfNotExists(actualDimensions);
|
|
await this.elasticsearchService.createIndexIfNotExists(actualDimensions);
|
|
|
|
|
|
|
|
- // 7. ベクトル化とインデックス作成をバッチ処理
|
|
|
|
|
|
|
+ // 7. Batch vectorization and indexing
|
|
|
const useBatching = this.memoryMonitor.shouldUseBatching(
|
|
const useBatching = this.memoryMonitor.shouldUseBatching(
|
|
|
chunks.length,
|
|
chunks.length,
|
|
|
avgChunkSize,
|
|
avgChunkSize,
|
|
@@ -732,7 +732,7 @@ export class KnowledgeBaseService {
|
|
|
await this.processInBatches(
|
|
await this.processInBatches(
|
|
|
chunks,
|
|
chunks,
|
|
|
async (batch, batchIndex) => {
|
|
async (batch, batchIndex) => {
|
|
|
- // バッチサイズがモデルの制限を超えていないか検証
|
|
|
|
|
|
|
+ // Verify batch size not exceeding model limit
|
|
|
if (batch.length > recommendedBatchSize) {
|
|
if (batch.length > recommendedBatchSize) {
|
|
|
this.logger.warn(
|
|
this.logger.warn(
|
|
|
this.i18nService.formatMessage('batchSizeExceeded', { index: batchIndex, actual: batch.length, limit: recommendedBatchSize })
|
|
this.i18nService.formatMessage('batchSizeExceeded', { index: batchIndex, actual: batch.length, limit: recommendedBatchSize })
|
|
@@ -746,14 +746,14 @@ export class KnowledgeBaseService {
|
|
|
kb.embeddingModelId,
|
|
kb.embeddingModelId,
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // 次元の整合性を検証
|
|
|
|
|
|
|
+ // Validate dimension consistency
|
|
|
if (embeddings.length > 0 && embeddings[0].length !== actualDimensions) {
|
|
if (embeddings.length > 0 && embeddings[0].length !== actualDimensions) {
|
|
|
this.logger.warn(
|
|
this.logger.warn(
|
|
|
- `ベクトル次元が不一致です: 期待値 ${actualDimensions}, 実際 ${embeddings[0].length}`
|
|
|
|
|
|
|
+ `Vector dimension mismatch: expected ${actualDimensions}, got ${embeddings[0].length}`
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // このバッチデータを即座にインデックス
|
|
|
|
|
|
|
+ // Index this batch data immediately
|
|
|
for (let i = 0; i < batch.length; i++) {
|
|
for (let i = 0; i < batch.length; i++) {
|
|
|
const chunk = batch[i];
|
|
const chunk = batch[i];
|
|
|
const embedding = embeddings[i];
|
|
const embedding = embeddings[i];
|
|
@@ -779,30 +779,30 @@ export class KnowledgeBaseService {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this.logger.log(`バッチ ${batchIndex} 完了: ${batch.length} チャンク`);
|
|
|
|
|
|
|
+ this.logger.log(`Batch ${batchIndex} completed: ${batch.length} chunks`);
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
batchSize: recommendedBatchSize,
|
|
batchSize: recommendedBatchSize,
|
|
|
onBatchComplete: (batchIndex, totalBatches) => {
|
|
onBatchComplete: (batchIndex, totalBatches) => {
|
|
|
const mem = this.memoryMonitor.getMemoryUsage();
|
|
const mem = this.memoryMonitor.getMemoryUsage();
|
|
|
this.logger.log(
|
|
this.logger.log(
|
|
|
- `バッチ ${batchIndex}/${totalBatches} 完了, メモリ: ${mem.heapUsed}MB`,
|
|
|
|
|
|
|
+ `Batch ${batchIndex}/${totalBatches} completed, memory: ${mem.heapUsed}MB`,
|
|
|
);
|
|
);
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
);
|
|
);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- // コンテキスト長エラーを検出(日本語・中国語・英語に対応)
|
|
|
|
|
- if (error.message && (error.message.includes('context length') || error.message.includes('コンテキスト長が上限を超えています') || error.message.includes('コンテキスト長が上限を超えています'))) {
|
|
|
|
|
|
|
+ // Detect context length error (supports Japanese/Chinese/English)
|
|
|
|
|
+ if (error.message && (error.message.includes('context length') || error.message.includes('context length exceeded'))) {
|
|
|
this.logger.warn(this.i18nService.getMessage('contextLengthErrorFallback'));
|
|
this.logger.warn(this.i18nService.getMessage('contextLengthErrorFallback'));
|
|
|
|
|
|
|
|
- // 単一テキスト処理にダウングレード
|
|
|
|
|
|
|
+ // Downgrade to single text processing
|
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
|
const chunk = chunks[i];
|
|
const chunk = chunks[i];
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
const embeddings = await this.embeddingService.getEmbeddings(
|
|
const embeddings = await this.embeddingService.getEmbeddings(
|
|
|
- [chunk.content], // 単一テキスト
|
|
|
|
|
|
|
+ [chunk.content], // Single text
|
|
|
userId,
|
|
userId,
|
|
|
kb.embeddingModelId,
|
|
kb.embeddingModelId,
|
|
|
);
|
|
);
|
|
@@ -829,27 +829,27 @@ export class KnowledgeBaseService {
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
if ((i + 1) % 10 === 0) {
|
|
if ((i + 1) % 10 === 0) {
|
|
|
- this.logger.log(`単一処理進捗: ${i + 1}/${chunks.length}`);
|
|
|
|
|
|
|
+ this.logger.log(`Single processing progress: ${i + 1}/${chunks.length}`);
|
|
|
}
|
|
}
|
|
|
} catch (chunkError) {
|
|
} catch (chunkError) {
|
|
|
this.logger.error(
|
|
this.logger.error(
|
|
|
- `テキストブロック ${chunk.index} の処理に失敗しました。スキップします: ${chunkError.message}`
|
|
|
|
|
|
|
+ `Failed to process text block ${chunk.index}. Skipping: ${chunkError.message}`
|
|
|
);
|
|
);
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this.logger.log(`単一テキスト処理完了: ${chunks.length} チャンク`);
|
|
|
|
|
|
|
+ this.logger.log(`Single text processing completed: ${chunks.length} chunks`);
|
|
|
} else {
|
|
} else {
|
|
|
- // その他のエラーは直接スロー
|
|
|
|
|
|
|
+ // Throw other errors directly
|
|
|
throw error;
|
|
throw error;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- // 小さなファイル、一括処理(ただしバッチ制限の確認が必要)
|
|
|
|
|
|
|
+ // Small files, batch processing (but need to check batch limits)
|
|
|
const chunkTexts = chunks.map((chunk) => chunk.content);
|
|
const chunkTexts = chunks.map((chunk) => chunk.content);
|
|
|
|
|
|
|
|
- // チャンク数がモデルのバッチ制限を超える場合は、強制的にバッチ処理
|
|
|
|
|
|
|
+ // Force batch processing if chunk count exceeds model batch limit
|
|
|
if (chunks.length > recommendedBatchSize) {
|
|
if (chunks.length > recommendedBatchSize) {
|
|
|
this.logger.warn(
|
|
this.logger.warn(
|
|
|
this.i18nService.formatMessage('chunkLimitExceededForceBatch', { actual: chunks.length, limit: recommendedBatchSize })
|
|
this.i18nService.formatMessage('chunkLimitExceededForceBatch', { actual: chunks.length, limit: recommendedBatchSize })
|
|
@@ -870,7 +870,7 @@ export class KnowledgeBaseService {
|
|
|
const embedding = embeddings[i];
|
|
const embedding = embeddings[i];
|
|
|
|
|
|
|
|
if (!embedding || embedding.length === 0) {
|
|
if (!embedding || embedding.length === 0) {
|
|
|
- this.logger.warn(`空ベクトルのテキストブロック ${chunk.index} をスキップします`);
|
|
|
|
|
|
|
+ this.logger.warn(`Skipping empty vector text block ${chunk.index}`);
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -893,17 +893,17 @@ export class KnowledgeBaseService {
|
|
|
},
|
|
},
|
|
|
);
|
|
);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- // コンテキスト長エラーを検出(日本語・中国語・英語に対応)
|
|
|
|
|
- if (error.message && (error.message.includes('context length') || error.message.includes('コンテキスト長が上限を超えています') || error.message.includes('コンテキスト長が上限を超えています'))) {
|
|
|
|
|
|
|
+ // Detect context length error (supports Japanese/Chinese/English)
|
|
|
|
|
+ if (error.message && (error.message.includes('context length') || error.message.includes('context length exceeded'))) {
|
|
|
this.logger.warn(this.i18nService.getMessage('batchContextLengthErrorFallback'));
|
|
this.logger.warn(this.i18nService.getMessage('batchContextLengthErrorFallback'));
|
|
|
|
|
|
|
|
- // 単一テキスト処理にダウングレード
|
|
|
|
|
|
|
+ // Downgrade to single text processing
|
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
|
const chunk = chunks[i];
|
|
const chunk = chunks[i];
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
const embeddings = await this.embeddingService.getEmbeddings(
|
|
const embeddings = await this.embeddingService.getEmbeddings(
|
|
|
- [chunk.content], // 単一テキスト
|
|
|
|
|
|
|
+ [chunk.content], // Single text
|
|
|
userId,
|
|
userId,
|
|
|
kb.embeddingModelId,
|
|
kb.embeddingModelId,
|
|
|
);
|
|
);
|
|
@@ -930,7 +930,7 @@ export class KnowledgeBaseService {
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
if ((i + 1) % 10 === 0) {
|
|
if ((i + 1) % 10 === 0) {
|
|
|
- this.logger.log(`単一処理進捗: ${i + 1}/${chunks.length}`);
|
|
|
|
|
|
|
+ this.logger.log(`Single processing progress: ${i + 1}/${chunks.length}`);
|
|
|
}
|
|
}
|
|
|
} catch (chunkError) {
|
|
} catch (chunkError) {
|
|
|
this.logger.error(
|
|
this.logger.error(
|
|
@@ -942,12 +942,12 @@ export class KnowledgeBaseService {
|
|
|
|
|
|
|
|
this.logger.log(this.i18nService.formatMessage('singleTextProcessingComplete', { count: chunks.length }));
|
|
this.logger.log(this.i18nService.formatMessage('singleTextProcessingComplete', { count: chunks.length }));
|
|
|
} else {
|
|
} else {
|
|
|
- // その他のエラー、直接スロー
|
|
|
|
|
|
|
+ // Throw other errors directly
|
|
|
throw error;
|
|
throw error;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- // 十分に小さいファイルの場合は一括で処理
|
|
|
|
|
|
|
+ // Process if file is small enough
|
|
|
try {
|
|
try {
|
|
|
const embeddings = await this.embeddingService.getEmbeddings(
|
|
const embeddings = await this.embeddingService.getEmbeddings(
|
|
|
chunkTexts,
|
|
chunkTexts,
|
|
@@ -981,23 +981,23 @@ export class KnowledgeBaseService {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- // コンテキスト長エラーを検出(日本語・中国語・英語に対応)
|
|
|
|
|
- if (error.message && (error.message.includes('context length') || error.message.includes('コンテキスト長が上限を超えています') || error.message.includes('コンテキスト長が上限を超えています'))) {
|
|
|
|
|
|
|
+ // Detect context length error (supports Japanese/Chinese/English)
|
|
|
|
|
+ if (error.message && (error.message.includes('context length') || error.message.includes('context length exceeded'))) {
|
|
|
this.logger.warn(this.i18nService.getMessage('batchContextLengthErrorFallback'));
|
|
this.logger.warn(this.i18nService.getMessage('batchContextLengthErrorFallback'));
|
|
|
|
|
|
|
|
- // 単一テキスト処理にダウングレード
|
|
|
|
|
|
|
+ // Downgrade to single text processing
|
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
|
const chunk = chunks[i];
|
|
const chunk = chunks[i];
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
const embeddings = await this.embeddingService.getEmbeddings(
|
|
const embeddings = await this.embeddingService.getEmbeddings(
|
|
|
- [chunk.content], // 単一テキスト
|
|
|
|
|
|
|
+ [chunk.content], // Single text
|
|
|
userId,
|
|
userId,
|
|
|
kb.embeddingModelId,
|
|
kb.embeddingModelId,
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
if (!embeddings[0] || embeddings[0].length === 0) {
|
|
if (!embeddings[0] || embeddings[0].length === 0) {
|
|
|
- this.logger.warn(`空ベクトルのテキストブロック ${chunk.index} をスキップします`);
|
|
|
|
|
|
|
+ this.logger.warn(`Skipping empty vector text block ${chunk.index}`);
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1018,11 +1018,11 @@ export class KnowledgeBaseService {
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
if ((i + 1) % 10 === 0) {
|
|
if ((i + 1) % 10 === 0) {
|
|
|
- this.logger.log(`単一処理進捗: ${i + 1}/${chunks.length}`);
|
|
|
|
|
|
|
+ this.logger.log(`Single processing progress: ${i + 1}/${chunks.length}`);
|
|
|
}
|
|
}
|
|
|
} catch (chunkError) {
|
|
} catch (chunkError) {
|
|
|
this.logger.error(
|
|
this.logger.error(
|
|
|
- `テキストブロック ${chunk.index} の処理に失敗しました。スキップします: ${chunkError.message}`
|
|
|
|
|
|
|
+ `Failed to process text block ${chunk.index}. Skipping: ${chunkError.message}`
|
|
|
);
|
|
);
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
@@ -1030,7 +1030,7 @@ export class KnowledgeBaseService {
|
|
|
|
|
|
|
|
this.logger.log(this.i18nService.formatMessage('singleTextProcessingComplete', { count: chunks.length }));
|
|
this.logger.log(this.i18nService.formatMessage('singleTextProcessingComplete', { count: chunks.length }));
|
|
|
} else {
|
|
} else {
|
|
|
- // その他のエラー、直接スロー
|
|
|
|
|
|
|
+ // Throw other errors directly
|
|
|
throw error;
|
|
throw error;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1045,7 +1045,7 @@ export class KnowledgeBaseService {
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
this.logger.error(this.i18nService.formatMessage('fileVectorizationFailed', { id: kbId }), error);
|
|
this.logger.error(this.i18nService.formatMessage('fileVectorizationFailed', { id: kbId }), error);
|
|
|
|
|
|
|
|
- // エラー情報を metadata に保存
|
|
|
|
|
|
|
+ // Save error info to metadata
|
|
|
try {
|
|
try {
|
|
|
const kb = await this.kbRepository.findOne({ where: { id: kbId } });
|
|
const kb = await this.kbRepository.findOne({ where: { id: kbId } });
|
|
|
if (kb) {
|
|
if (kb) {
|
|
@@ -1063,7 +1063,7 @@ export class KnowledgeBaseService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * バッチ処理、メモリ制御付き
|
|
|
|
|
|
|
+ * Batch processing with memory control
|
|
|
*/
|
|
*/
|
|
|
private async processInBatches<T>(
|
|
private async processInBatches<T>(
|
|
|
items: T[],
|
|
items: T[],
|
|
@@ -1084,19 +1084,19 @@ export class KnowledgeBaseService {
|
|
|
const totalBatches = Math.ceil(totalItems / initialBatchSize);
|
|
const totalBatches = Math.ceil(totalItems / initialBatchSize);
|
|
|
|
|
|
|
|
for (let i = 0; i < totalItems;) {
|
|
for (let i = 0; i < totalItems;) {
|
|
|
- // メモリを確認し待機
|
|
|
|
|
|
|
+ // Check memory and wait
|
|
|
await this.memoryMonitor.waitForMemoryAvailable();
|
|
await this.memoryMonitor.waitForMemoryAvailable();
|
|
|
|
|
|
|
|
- // バッチサイズを動的に調整 (initialBatchSize から開始し、必要に応じてメモリモニターが削減できるようにします)
|
|
|
|
|
- // 注意: memoryMonitor.getDynamicBatchSize はメモリ状況に基づいてより大きな値を返す可能性がありますが、
|
|
|
|
|
- // モデルの制限 (initialBatchSize) を尊重する必要があります。
|
|
|
|
|
|
|
+ // Dynamically adjust batch size (start from initialBatchSize, memory monitor can reduce if needed)
|
|
|
|
|
+ // Note: memoryMonitor.getDynamicBatchSize may return larger values based on memory situation,
|
|
|
|
|
+ // but we must respect model limits (initialBatchSize)
|
|
|
const currentMem = this.memoryMonitor.getMemoryUsage().heapUsed;
|
|
const currentMem = this.memoryMonitor.getMemoryUsage().heapUsed;
|
|
|
const dynamicBatchSize = this.memoryMonitor.getDynamicBatchSize(currentMem);
|
|
const dynamicBatchSize = this.memoryMonitor.getDynamicBatchSize(currentMem);
|
|
|
|
|
|
|
|
// Ensure we don't exceed the model's limit (initialBatchSize) even if memory allows more
|
|
// Ensure we don't exceed the model's limit (initialBatchSize) even if memory allows more
|
|
|
const batchSize = Math.min(dynamicBatchSize, initialBatchSize);
|
|
const batchSize = Math.min(dynamicBatchSize, initialBatchSize);
|
|
|
|
|
|
|
|
- // 現在のバッチを取得
|
|
|
|
|
|
|
+ // Get current batch
|
|
|
const batch = items.slice(i, i + batchSize);
|
|
const batch = items.slice(i, i + batchSize);
|
|
|
const batchIndex = Math.floor(i / batchSize) + 1;
|
|
const batchIndex = Math.floor(i / batchSize) + 1;
|
|
|
|
|
|
|
@@ -1104,20 +1104,20 @@ export class KnowledgeBaseService {
|
|
|
this.i18nService.formatMessage('batchProcessingProgress', { index: batchIndex, total: totalBatches, count: batch.length })
|
|
this.i18nService.formatMessage('batchProcessingProgress', { index: batchIndex, total: totalBatches, count: batch.length })
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // バッチを処理
|
|
|
|
|
|
|
+ // Process batch
|
|
|
await processor(batch, batchIndex);
|
|
await processor(batch, batchIndex);
|
|
|
|
|
|
|
|
- // コールバック通知
|
|
|
|
|
|
|
+ // Callback notification
|
|
|
if (options?.onBatchComplete) {
|
|
if (options?.onBatchComplete) {
|
|
|
options.onBatchComplete(batchIndex, totalBatches);
|
|
options.onBatchComplete(batchIndex, totalBatches);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 強制GC(メモリがしきい値に近い場合)
|
|
|
|
|
|
|
+ // Force GC (if memory is near threshold)
|
|
|
if (currentMem > 800) {
|
|
if (currentMem > 800) {
|
|
|
this.memoryMonitor.forceGC();
|
|
this.memoryMonitor.forceGC();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 参照をクリアしGCを助ける
|
|
|
|
|
|
|
+ // Clear references to help GC
|
|
|
batch.length = 0;
|
|
batch.length = 0;
|
|
|
|
|
|
|
|
i += batchSize;
|
|
i += batchSize;
|
|
@@ -1128,7 +1128,7 @@ export class KnowledgeBaseService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 失敗したファイルのベクトル化を再試行
|
|
|
|
|
|
|
+ * Retry vectorization for failed files
|
|
|
*/
|
|
*/
|
|
|
async retryFailedFile(fileId: string, userId: string, tenantId: string): Promise<KnowledgeBase> {
|
|
async retryFailedFile(fileId: string, userId: string, tenantId: string): Promise<KnowledgeBase> {
|
|
|
this.logger.log(`Retrying failed file ${fileId} for user ${userId} in tenant ${tenantId}`);
|
|
this.logger.log(`Retrying failed file ${fileId} for user ${userId} in tenant ${tenantId}`);
|
|
@@ -1139,7 +1139,7 @@ export class KnowledgeBaseService {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
if (!kb) {
|
|
if (!kb) {
|
|
|
- throw new NotFoundException('ファイルが存在しません');
|
|
|
|
|
|
|
+ throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (kb.status !== FileStatus.FAILED) {
|
|
if (kb.status !== FileStatus.FAILED) {
|
|
@@ -1150,10 +1150,10 @@ export class KnowledgeBaseService {
|
|
|
throw new Error(this.i18nService.getMessage('emptyFileRetryFailed'));
|
|
throw new Error(this.i18nService.getMessage('emptyFileRetryFailed'));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. ステータスを INDEXING にリセット
|
|
|
|
|
|
|
+ // 2. Reset status to INDEXING
|
|
|
await this.updateStatus(fileId, FileStatus.INDEXING);
|
|
await this.updateStatus(fileId, FileStatus.INDEXING);
|
|
|
|
|
|
|
|
- // 3. 非同期でベクトル化をトリガー(既存ロジックを再利用)
|
|
|
|
|
|
|
+ // 3. Trigger vectorization asynchronously (reuse existing logic)
|
|
|
this.vectorizeToElasticsearch(
|
|
this.vectorizeToElasticsearch(
|
|
|
fileId,
|
|
fileId,
|
|
|
userId,
|
|
userId,
|
|
@@ -1168,16 +1168,16 @@ export class KnowledgeBaseService {
|
|
|
this.logger.error(`Retry vectorization failed for file ${fileId}`, err);
|
|
this.logger.error(`Retry vectorization failed for file ${fileId}`, err);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 4. 更新後のファイルステータスを返却
|
|
|
|
|
|
|
+ // 4. Return updated file status
|
|
|
const updatedKb = await this.kbRepository.findOne({ where: { id: fileId, tenantId } });
|
|
const updatedKb = await this.kbRepository.findOne({ where: { id: fileId, tenantId } });
|
|
|
if (!updatedKb) {
|
|
if (!updatedKb) {
|
|
|
- throw new NotFoundException('ファイルが存在しません');
|
|
|
|
|
|
|
+ throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
|
|
|
}
|
|
}
|
|
|
return updatedKb;
|
|
return updatedKb;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * ファイルのすべてのチャンク情報を取得
|
|
|
|
|
|
|
+ * Get all chunk information for a file
|
|
|
*/
|
|
*/
|
|
|
async getFileChunks(fileId: string, userId: string, tenantId: string) {
|
|
async getFileChunks(fileId: string, userId: string, tenantId: string) {
|
|
|
this.logger.log(`Getting chunks for file ${fileId}, user ${userId}, tenant ${tenantId}`);
|
|
this.logger.log(`Getting chunks for file ${fileId}, user ${userId}, tenant ${tenantId}`);
|
|
@@ -1188,13 +1188,13 @@ export class KnowledgeBaseService {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
if (!kb) {
|
|
if (!kb) {
|
|
|
- throw new NotFoundException('ファイルが存在しません');
|
|
|
|
|
|
|
+ throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. Elasticsearch からすべてのチャンクを取得
|
|
|
|
|
|
|
+ // 2. Get all chunks from Elasticsearch
|
|
|
const chunks = await this.elasticsearchService.getFileChunks(fileId, userId, tenantId);
|
|
const chunks = await this.elasticsearchService.getFileChunks(fileId, userId, tenantId);
|
|
|
|
|
|
|
|
- // 3. チャンク情報を返却
|
|
|
|
|
|
|
+ // 3. Return chunk info
|
|
|
return {
|
|
return {
|
|
|
fileId: kb.id,
|
|
fileId: kb.id,
|
|
|
fileName: kb.originalName,
|
|
fileName: kb.originalName,
|
|
@@ -1215,7 +1215,7 @@ export class KnowledgeBaseService {
|
|
|
await this.kbRepository.update(id, { status });
|
|
await this.kbRepository.update(id, { status });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // PDF プレビュー関連メソッド
|
|
|
|
|
|
|
+ // PDF preview related methods
|
|
|
async ensurePDFExists(fileId: string, userId: string, tenantId: string, force: boolean = false): Promise<string> {
|
|
async ensurePDFExists(fileId: string, userId: string, tenantId: string, force: boolean = false): Promise<string> {
|
|
|
const kb = await this.kbRepository.findOne({
|
|
const kb = await this.kbRepository.findOne({
|
|
|
where: { id: fileId, tenantId },
|
|
where: { id: fileId, tenantId },
|
|
@@ -1225,12 +1225,12 @@ export class KnowledgeBaseService {
|
|
|
throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
|
|
throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 元ファイルが PDF の場合は、元ファイルのパスを直接返す
|
|
|
|
|
|
|
+ // If original file is PDF, return the original file path directly
|
|
|
if (kb.mimetype === 'application/pdf') {
|
|
if (kb.mimetype === 'application/pdf') {
|
|
|
return kb.storagePath;
|
|
return kb.storagePath;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // プレビュー変換に対応しているか確認(ドキュメント類または画像類のみ許可)
|
|
|
|
|
|
|
+ // Check if preview conversion is supported (only documents or images allowed)
|
|
|
const ext = kb.originalName.toLowerCase().split('.').pop() || '';
|
|
const ext = kb.originalName.toLowerCase().split('.').pop() || '';
|
|
|
const isConvertible = [...DOC_EXTENSIONS, ...IMAGE_EXTENSIONS].includes(ext);
|
|
const isConvertible = [...DOC_EXTENSIONS, ...IMAGE_EXTENSIONS].includes(ext);
|
|
|
|
|
|
|
@@ -1239,14 +1239,14 @@ export class KnowledgeBaseService {
|
|
|
throw new Error(this.i18nService.getMessage('pdfPreviewNotSupported'));
|
|
throw new Error(this.i18nService.getMessage('pdfPreviewNotSupported'));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // PDF フィールドパスを生成
|
|
|
|
|
|
|
+ // Generate PDF field path
|
|
|
const path = await import('path');
|
|
const path = await import('path');
|
|
|
const fs = await import('fs');
|
|
const fs = await import('fs');
|
|
|
const uploadDir = path.dirname(kb.storagePath);
|
|
const uploadDir = path.dirname(kb.storagePath);
|
|
|
const baseName = path.basename(kb.storagePath, path.extname(kb.storagePath));
|
|
const baseName = path.basename(kb.storagePath, path.extname(kb.storagePath));
|
|
|
const pdfPath = path.join(uploadDir, `${baseName}.pdf`);
|
|
const pdfPath = path.join(uploadDir, `${baseName}.pdf`);
|
|
|
|
|
|
|
|
- // 強制再生成が指定され、ファイルが存在する場合は削除
|
|
|
|
|
|
|
+ // Delete if forced regeneration specified and file exists
|
|
|
if (force && fs.existsSync(pdfPath)) {
|
|
if (force && fs.existsSync(pdfPath)) {
|
|
|
try {
|
|
try {
|
|
|
fs.unlinkSync(pdfPath);
|
|
fs.unlinkSync(pdfPath);
|
|
@@ -1256,7 +1256,7 @@ export class KnowledgeBaseService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 変換済みかつ強制再生成が不要か確認
|
|
|
|
|
|
|
+ // Check if already converted and regeneration not needed
|
|
|
if (fs.existsSync(pdfPath) && !force) {
|
|
if (fs.existsSync(pdfPath) && !force) {
|
|
|
if (!kb.pdfPath) {
|
|
if (!kb.pdfPath) {
|
|
|
await this.kbRepository.update(kb.id, { pdfPath: pdfPath });
|
|
await this.kbRepository.update(kb.id, { pdfPath: pdfPath });
|
|
@@ -1264,14 +1264,14 @@ export class KnowledgeBaseService {
|
|
|
return pdfPath;
|
|
return pdfPath;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // PDF への変換が必要
|
|
|
|
|
|
|
+ // Need to convert to PDF
|
|
|
try {
|
|
try {
|
|
|
this.logger.log(`Starting PDF conversion for ${kb.originalName} at ${kb.storagePath}`);
|
|
this.logger.log(`Starting PDF conversion for ${kb.originalName} at ${kb.storagePath}`);
|
|
|
|
|
|
|
|
- // ファイルを変換
|
|
|
|
|
|
|
+ // Convert file
|
|
|
await this.libreOfficeService.convertToPDF(kb.storagePath);
|
|
await this.libreOfficeService.convertToPDF(kb.storagePath);
|
|
|
|
|
|
|
|
- // 変換結果を確認
|
|
|
|
|
|
|
+ // Check conversion result
|
|
|
if (!fs.existsSync(pdfPath)) {
|
|
if (!fs.existsSync(pdfPath)) {
|
|
|
throw new Error(`PDF conversion completed but file not found at ${pdfPath}`);
|
|
throw new Error(`PDF conversion completed but file not found at ${pdfPath}`);
|
|
|
}
|
|
}
|
|
@@ -1301,7 +1301,7 @@ export class KnowledgeBaseService {
|
|
|
throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
|
|
throw new NotFoundException(this.i18nService.getMessage('fileNotFound'));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 元ファイルが PDF の場合
|
|
|
|
|
|
|
+ // If original file is PDF
|
|
|
if (kb.mimetype === 'application/pdf') {
|
|
if (kb.mimetype === 'application/pdf') {
|
|
|
const token = this.generateTempToken(fileId, userId, tenantId);
|
|
const token = this.generateTempToken(fileId, userId, tenantId);
|
|
|
return {
|
|
return {
|
|
@@ -1310,14 +1310,14 @@ export class KnowledgeBaseService {
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // PDF ファイルパスを生成
|
|
|
|
|
|
|
+ // Generate PDF file path
|
|
|
const path = await import('path');
|
|
const path = await import('path');
|
|
|
const fs = await import('fs');
|
|
const fs = await import('fs');
|
|
|
const uploadDir = path.dirname(kb.storagePath);
|
|
const uploadDir = path.dirname(kb.storagePath);
|
|
|
const baseName = path.basename(kb.storagePath, path.extname(kb.storagePath));
|
|
const baseName = path.basename(kb.storagePath, path.extname(kb.storagePath));
|
|
|
const pdfPath = path.join(uploadDir, `${baseName}.pdf`);
|
|
const pdfPath = path.join(uploadDir, `${baseName}.pdf`);
|
|
|
|
|
|
|
|
- // 変換済みか確認
|
|
|
|
|
|
|
+ // Check if converted
|
|
|
if (fs.existsSync(pdfPath)) {
|
|
if (fs.existsSync(pdfPath)) {
|
|
|
if (!kb.pdfPath) {
|
|
if (!kb.pdfPath) {
|
|
|
kb.pdfPath = pdfPath;
|
|
kb.pdfPath = pdfPath;
|
|
@@ -1330,7 +1330,7 @@ export class KnowledgeBaseService {
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 変換が必要
|
|
|
|
|
|
|
+ // Conversion needed
|
|
|
return {
|
|
return {
|
|
|
status: 'pending',
|
|
status: 'pending',
|
|
|
};
|
|
};
|
|
@@ -1341,7 +1341,7 @@ export class KnowledgeBaseService {
|
|
|
|
|
|
|
|
const secret = process.env.JWT_SECRET;
|
|
const secret = process.env.JWT_SECRET;
|
|
|
if (!secret) {
|
|
if (!secret) {
|
|
|
- throw new Error('JWT_SECRET environment variable is required but not set');
|
|
|
|
|
|
|
+ throw new Error(this.i18nService.getMessage('jwtSecretRequired'));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return jwt.sign(
|
|
return jwt.sign(
|
|
@@ -1352,7 +1352,7 @@ export class KnowledgeBaseService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * モデルの実際の次元数を取得(キャッシュ確認とプローブロジック付き)
|
|
|
|
|
|
|
+ * Get actual model dimensions (with cache check and probe logic)
|
|
|
*/
|
|
*/
|
|
|
private async getActualModelDimensions(embeddingModelId: string, userId: string, tenantId: string): Promise<number> {
|
|
private async getActualModelDimensions(embeddingModelId: string, userId: string, tenantId: string): Promise<number> {
|
|
|
const defaultDimensions = parseInt(
|
|
const defaultDimensions = parseInt(
|
|
@@ -1360,7 +1360,7 @@ export class KnowledgeBaseService {
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- // 1. モデル設定から優先的に取得
|
|
|
|
|
|
|
+ // 1. Prioritize getting from model config
|
|
|
const modelConfig = await this.modelConfigService.findOne(
|
|
const modelConfig = await this.modelConfigService.findOne(
|
|
|
embeddingModelId,
|
|
embeddingModelId,
|
|
|
userId,
|
|
userId,
|
|
@@ -1368,12 +1368,12 @@ export class KnowledgeBaseService {
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
if (modelConfig && modelConfig.dimensions) {
|
|
if (modelConfig && modelConfig.dimensions) {
|
|
|
- this.logger.log(`設定から ${modelConfig.name} の次元数を取得しました: ${modelConfig.dimensions}`);
|
|
|
|
|
|
|
+ this.logger.log(`Got dimensions from ${modelConfig.name} config: ${modelConfig.dimensions}`);
|
|
|
return modelConfig.dimensions;
|
|
return modelConfig.dimensions;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. それ以外の場合はプローブにより取得
|
|
|
|
|
- this.logger.log(`モデル次元数をプローブ中: ${embeddingModelId}`);
|
|
|
|
|
|
|
+ // 2. Otherwise probe for dimensions
|
|
|
|
|
+ this.logger.log(`Probing model dimensions: ${embeddingModelId}`);
|
|
|
const probeEmbeddings = await this.embeddingService.getEmbeddings(
|
|
const probeEmbeddings = await this.embeddingService.getEmbeddings(
|
|
|
['probe'],
|
|
['probe'],
|
|
|
userId,
|
|
userId,
|
|
@@ -1382,17 +1382,17 @@ export class KnowledgeBaseService {
|
|
|
|
|
|
|
|
if (probeEmbeddings.length > 0) {
|
|
if (probeEmbeddings.length > 0) {
|
|
|
const actualDimensions = probeEmbeddings[0].length;
|
|
const actualDimensions = probeEmbeddings[0].length;
|
|
|
- this.logger.log(`モデルの実際の次元数を検出しました: ${actualDimensions}`);
|
|
|
|
|
|
|
+ this.logger.log(`Detected actual model dimensions: ${actualDimensions}`);
|
|
|
|
|
|
|
|
- // 次回利用のためにモデル設定を更新
|
|
|
|
|
|
|
+ // Update model config for next use
|
|
|
if (modelConfig) {
|
|
if (modelConfig) {
|
|
|
try {
|
|
try {
|
|
|
await this.modelConfigService.update(userId, tenantId, modelConfig.id, {
|
|
await this.modelConfigService.update(userId, tenantId, modelConfig.id, {
|
|
|
dimensions: actualDimensions,
|
|
dimensions: actualDimensions,
|
|
|
});
|
|
});
|
|
|
- this.logger.log(`モデル ${modelConfig.name} の次元数設定を ${actualDimensions} に更新しました`);
|
|
|
|
|
|
|
+ this.logger.log(`Updated model ${modelConfig.name} dimension config to ${actualDimensions}`);
|
|
|
} catch (updateErr) {
|
|
} catch (updateErr) {
|
|
|
- this.logger.warn(`モデル次元数設定の更新に失敗しました: ${updateErr.message}`);
|
|
|
|
|
|
|
+ this.logger.warn(`Failed to update model dimension config: ${updateErr.message}`);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1400,7 +1400,7 @@ export class KnowledgeBaseService {
|
|
|
}
|
|
}
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
this.logger.warn(
|
|
this.logger.warn(
|
|
|
- `次元数の取得に失敗しました。デフォルト次元数を使用します: ${defaultDimensions}`,
|
|
|
|
|
|
|
+ `Failed to get dimensions. Using default: ${defaultDimensions}`,
|
|
|
err.message,
|
|
err.message,
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
@@ -1409,7 +1409,7 @@ export class KnowledgeBaseService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * AIを使用して文書のタイトルを自動生成する
|
|
|
|
|
|
|
+ * Auto-generate document title using AI
|
|
|
*/
|
|
*/
|
|
|
async generateTitle(kbId: string): Promise<string | null> {
|
|
async generateTitle(kbId: string): Promise<string | null> {
|
|
|
this.logger.log(`Generating automatic title for file ${kbId}`);
|
|
this.logger.log(`Generating automatic title for file ${kbId}`);
|
|
@@ -1421,22 +1421,22 @@ export class KnowledgeBaseService {
|
|
|
}
|
|
}
|
|
|
const tenantId = kb.tenantId;
|
|
const tenantId = kb.tenantId;
|
|
|
|
|
|
|
|
- // すでにタイトルがある場合はスキップ
|
|
|
|
|
|
|
+ // Skip if title already exists
|
|
|
if (kb.title) {
|
|
if (kb.title) {
|
|
|
return kb.title;
|
|
return kb.title;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // コンテンツの冒頭サンプルを取得(最大2500文字)
|
|
|
|
|
|
|
+ // Get content sample (max 2500 characters)
|
|
|
const contentSample = kb.content.substring(0, 2500);
|
|
const contentSample = kb.content.substring(0, 2500);
|
|
|
|
|
|
|
|
- // 組織設定から言語を取得、またはデフォルトを使用
|
|
|
|
|
|
|
+ // Get language from org settings, or use default
|
|
|
const userSettings = await this.userSettingService.getByUser(kb.userId);
|
|
const userSettings = await this.userSettingService.getByUser(kb.userId);
|
|
|
const language = userSettings.language || 'zh';
|
|
const language = userSettings.language || 'zh';
|
|
|
|
|
|
|
|
- // プロンプトを構築
|
|
|
|
|
|
|
+ // Build prompt
|
|
|
const prompt = this.i18nService.getDocumentTitlePrompt(language, contentSample);
|
|
const prompt = this.i18nService.getDocumentTitlePrompt(language, contentSample);
|
|
|
|
|
|
|
|
- // LLMを呼び出してタイトルを生成
|
|
|
|
|
|
|
+ // Call LLM to generate title
|
|
|
let generatedTitle: string | undefined;
|
|
let generatedTitle: string | undefined;
|
|
|
try {
|
|
try {
|
|
|
generatedTitle = await this.chatService.generateSimpleChat(
|
|
generatedTitle = await this.chatService.generateSimpleChat(
|
|
@@ -1450,11 +1450,11 @@ export class KnowledgeBaseService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (generatedTitle && generatedTitle.trim().length > 0) {
|
|
if (generatedTitle && generatedTitle.trim().length > 0) {
|
|
|
- // 余分な引用符や改行を除去
|
|
|
|
|
|
|
+ // Remove extra quotes and newlines
|
|
|
const cleanedTitle = generatedTitle.trim().replace(/^["']|["']$/g, '').substring(0, 100);
|
|
const cleanedTitle = generatedTitle.trim().replace(/^["']|["']$/g, '').substring(0, 100);
|
|
|
await this.kbRepository.update(kbId, { title: cleanedTitle });
|
|
await this.kbRepository.update(kbId, { title: cleanedTitle });
|
|
|
|
|
|
|
|
- // Elasticsearch のチャンクも更新
|
|
|
|
|
|
|
+ // Also update ES chunks
|
|
|
await this.elasticsearchService.updateTitleByFileId(kbId, cleanedTitle, tenantId).catch((err) => {
|
|
await this.elasticsearchService.updateTitleByFileId(kbId, cleanedTitle, tenantId).catch((err) => {
|
|
|
this.logger.error(`Failed to update title in Elasticsearch for ${kbId}`, err);
|
|
this.logger.error(`Failed to update title in Elasticsearch for ${kbId}`, err);
|
|
|
});
|
|
});
|