@reftrixmcp/ml 0.1.7
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/LICENSE +661 -0
- package/dist/dinov2/index.d.ts +7 -0
- package/dist/dinov2/index.d.ts.map +1 -0
- package/dist/dinov2/index.js +7 -0
- package/dist/dinov2/index.js.map +1 -0
- package/dist/dinov2/service.d.ts +106 -0
- package/dist/dinov2/service.d.ts.map +1 -0
- package/dist/dinov2/service.js +446 -0
- package/dist/dinov2/service.js.map +1 -0
- package/dist/dinov2/worker-thread-types.d.ts +77 -0
- package/dist/dinov2/worker-thread-types.d.ts.map +1 -0
- package/dist/dinov2/worker-thread-types.js +4 -0
- package/dist/dinov2/worker-thread-types.js.map +1 -0
- package/dist/dinov2/worker-thread.d.ts +2 -0
- package/dist/dinov2/worker-thread.d.ts.map +1 -0
- package/dist/dinov2/worker-thread.js +231 -0
- package/dist/dinov2/worker-thread.js.map +1 -0
- package/dist/embeddings/index.d.ts +12 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +13 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/multimodal-embedding.service.d.ts +400 -0
- package/dist/embeddings/multimodal-embedding.service.d.ts.map +1 -0
- package/dist/embeddings/multimodal-embedding.service.js +650 -0
- package/dist/embeddings/multimodal-embedding.service.js.map +1 -0
- package/dist/embeddings/service.d.ts +261 -0
- package/dist/embeddings/service.d.ts.map +1 -0
- package/dist/embeddings/service.js +1093 -0
- package/dist/embeddings/service.js.map +1 -0
- package/dist/embeddings/style-embedding.service.d.ts +117 -0
- package/dist/embeddings/style-embedding.service.d.ts.map +1 -0
- package/dist/embeddings/style-embedding.service.js +157 -0
- package/dist/embeddings/style-embedding.service.js.map +1 -0
- package/dist/embeddings/types.d.ts +83 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/embeddings/types.js +4 -0
- package/dist/embeddings/types.js.map +1 -0
- package/dist/embeddings/vision-embedding.service.d.ts +70 -0
- package/dist/embeddings/vision-embedding.service.d.ts.map +1 -0
- package/dist/embeddings/vision-embedding.service.js +221 -0
- package/dist/embeddings/vision-embedding.service.js.map +1 -0
- package/dist/embeddings/vision-embedding.types.d.ts +102 -0
- package/dist/embeddings/vision-embedding.types.d.ts.map +1 -0
- package/dist/embeddings/vision-embedding.types.js +4 -0
- package/dist/embeddings/vision-embedding.types.js.map +1 -0
- package/dist/embeddings/worker-thread-types.d.ts +152 -0
- package/dist/embeddings/worker-thread-types.d.ts.map +1 -0
- package/dist/embeddings/worker-thread-types.js +4 -0
- package/dist/embeddings/worker-thread-types.js.map +1 -0
- package/dist/embeddings/worker-thread.d.ts +4 -0
- package/dist/embeddings/worker-thread.d.ts.map +1 -0
- package/dist/embeddings/worker-thread.js +441 -0
- package/dist/embeddings/worker-thread.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/search/fulltext-helpers.d.ts +30 -0
- package/dist/search/fulltext-helpers.d.ts.map +1 -0
- package/dist/search/fulltext-helpers.js +40 -0
- package/dist/search/fulltext-helpers.js.map +1 -0
- package/dist/search/hybrid.d.ts +40 -0
- package/dist/search/hybrid.d.ts.map +1 -0
- package/dist/search/hybrid.js +61 -0
- package/dist/search/hybrid.js.map +1 -0
- package/dist/search/index.d.ts +12 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +11 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/rrf.d.ts +69 -0
- package/dist/search/rrf.d.ts.map +1 -0
- package/dist/search/rrf.js +108 -0
- package/dist/search/rrf.js.map +1 -0
- package/dist/search/types.d.ts +130 -0
- package/dist/search/types.d.ts.map +1 -0
- package/dist/search/types.js +4 -0
- package/dist/search/types.js.map +1 -0
- package/package.json +65 -0
- package/src/dinov2/index.ts +21 -0
- package/src/dinov2/service.ts +539 -0
- package/src/dinov2/worker-thread-types.ts +103 -0
- package/src/dinov2/worker-thread.ts +306 -0
- package/src/embeddings/index.ts +74 -0
- package/src/embeddings/multimodal-embedding.service.ts +992 -0
- package/src/embeddings/service.ts +1287 -0
- package/src/embeddings/style-embedding.service.ts +201 -0
- package/src/embeddings/types.ts +95 -0
- package/src/embeddings/vision-embedding.service.ts +273 -0
- package/src/embeddings/vision-embedding.types.ts +117 -0
- package/src/embeddings/worker-thread-types.ts +194 -0
- package/src/embeddings/worker-thread.ts +529 -0
- package/src/index.ts +70 -0
- package/src/search/fulltext-helpers.ts +45 -0
- package/src/search/hybrid.ts +107 -0
- package/src/search/index.ts +25 -0
- package/src/search/rrf.ts +154 -0
- package/src/search/types.ts +141 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 TKMD and Reftrix Contributors
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Multimodal Embedding Service
|
|
6
|
+
*
|
|
7
|
+
* VisionAnalyzerの特徴抽出結果とテキストEmbeddingを組み合わせて
|
|
8
|
+
* マルチモーダルEmbeddingを生成します。
|
|
9
|
+
*
|
|
10
|
+
* text_embedding + vision_embedding の統合サービス
|
|
11
|
+
* - searchMode: 'text_only' | 'vision_only' | 'combined'
|
|
12
|
+
* - Graceful Degradation: visionFeaturesがない場合はtext_onlyにフォールバック
|
|
13
|
+
* - L2正規化: すべてのEmbeddingは正規化される
|
|
14
|
+
*
|
|
15
|
+
* @module embeddings/multimodal-embedding.service
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import pLimit from "p-limit";
|
|
20
|
+
import type { VisionFeatures } from "./vision-embedding.types.js";
|
|
21
|
+
import { visionFeaturesToText } from "./vision-embedding.service.js";
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// 型定義
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 検索モード
|
|
29
|
+
*/
|
|
30
|
+
export type SearchMode = "text_only" | "vision_only" | "combined";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* マルチモーダルEmbedding設定
|
|
34
|
+
*/
|
|
35
|
+
export interface MultimodalEmbeddingConfig {
|
|
36
|
+
/** テキストEmbeddingの重み (0-1) */
|
|
37
|
+
textWeight?: number;
|
|
38
|
+
/** Vision Embeddingの重み (0-1) */
|
|
39
|
+
visionWeight?: number;
|
|
40
|
+
/** Embeddingの次元数 */
|
|
41
|
+
embeddingDimension?: number;
|
|
42
|
+
/** 検索モード */
|
|
43
|
+
searchMode?: SearchMode;
|
|
44
|
+
/** 出力のL2正規化 */
|
|
45
|
+
normalizeOutput?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* デフォルト設定
|
|
50
|
+
*/
|
|
51
|
+
export const DEFAULT_MULTIMODAL_CONFIG: Required<MultimodalEmbeddingConfig> = {
|
|
52
|
+
textWeight: 0.6,
|
|
53
|
+
visionWeight: 0.4,
|
|
54
|
+
embeddingDimension: 768,
|
|
55
|
+
searchMode: "combined",
|
|
56
|
+
normalizeOutput: true,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 設定スキーマ
|
|
61
|
+
*/
|
|
62
|
+
export const multimodalEmbeddingConfigSchema = z.object({
|
|
63
|
+
textWeight: z.number().min(0).max(1).optional(),
|
|
64
|
+
visionWeight: z.number().min(0).max(1).optional(),
|
|
65
|
+
embeddingDimension: z.number().int().positive().optional(),
|
|
66
|
+
searchMode: z.enum(["text_only", "vision_only", "combined"]).optional(),
|
|
67
|
+
normalizeOutput: z.boolean().optional(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* マルチモーダルEmbedding入力
|
|
72
|
+
*/
|
|
73
|
+
export interface MultimodalEmbeddingInput {
|
|
74
|
+
/** テキスト表現(VisionAnalyzer.generateTextRepresentationの出力) */
|
|
75
|
+
textRepresentation: string;
|
|
76
|
+
/** Vision特徴からのテキスト(カラーパレット、レイアウト等) */
|
|
77
|
+
visionFeatureText?: string;
|
|
78
|
+
/** 元のテキスト(名前、説明等) */
|
|
79
|
+
originalText?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 入力スキーマ
|
|
84
|
+
*/
|
|
85
|
+
export const multimodalEmbeddingInputSchema = z.object({
|
|
86
|
+
textRepresentation: z.string().min(1),
|
|
87
|
+
visionFeatureText: z.string().optional(),
|
|
88
|
+
originalText: z.string().optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* マルチモーダルEmbedding結果
|
|
93
|
+
*/
|
|
94
|
+
export interface MultimodalEmbeddingResult {
|
|
95
|
+
/** 統合Embedding (768次元) */
|
|
96
|
+
combinedEmbedding: number[];
|
|
97
|
+
/** テキストEmbedding (768次元) */
|
|
98
|
+
textEmbedding: number[];
|
|
99
|
+
/** Vision特徴Embedding (768次元) */
|
|
100
|
+
visionEmbedding: number[];
|
|
101
|
+
/** 使用した重み */
|
|
102
|
+
weights: {
|
|
103
|
+
text: number;
|
|
104
|
+
vision: number;
|
|
105
|
+
};
|
|
106
|
+
/** 処理時間(ミリ秒) */
|
|
107
|
+
processingTimeMs: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 結果スキーマ
|
|
112
|
+
*/
|
|
113
|
+
export const multimodalEmbeddingResultSchema = z.object({
|
|
114
|
+
combinedEmbedding: z.array(z.number()).length(768),
|
|
115
|
+
textEmbedding: z.array(z.number()).length(768),
|
|
116
|
+
visionEmbedding: z.array(z.number()).length(768),
|
|
117
|
+
weights: z.object({
|
|
118
|
+
text: z.number().min(0).max(1),
|
|
119
|
+
vision: z.number().min(0).max(1),
|
|
120
|
+
}),
|
|
121
|
+
processingTimeMs: z.number().min(0),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// =============================================================================
|
|
125
|
+
// 新しい型定義
|
|
126
|
+
// =============================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 新しいマルチモーダルEmbedding結果
|
|
130
|
+
*/
|
|
131
|
+
export interface MultimodalEmbeddingResultV2 {
|
|
132
|
+
/** テキストEmbedding (768次元、L2正規化済み) */
|
|
133
|
+
textEmbedding: number[] | null;
|
|
134
|
+
/** Vision Embedding (768次元、L2正規化済み) */
|
|
135
|
+
visionEmbedding: number[] | null;
|
|
136
|
+
/** 統合Embedding (768次元、L2正規化済み) */
|
|
137
|
+
combinedEmbedding: number[] | null;
|
|
138
|
+
/** 使用した検索モード */
|
|
139
|
+
searchMode: SearchMode;
|
|
140
|
+
/** 使用した重み */
|
|
141
|
+
weights: {
|
|
142
|
+
text: number;
|
|
143
|
+
vision: number;
|
|
144
|
+
};
|
|
145
|
+
/** メタデータ */
|
|
146
|
+
metadata: {
|
|
147
|
+
/** テキスト表現 */
|
|
148
|
+
textRepresentation: string;
|
|
149
|
+
/** VisionFeatures (combinedモード時のみ) */
|
|
150
|
+
visionFeatures?: VisionFeatures;
|
|
151
|
+
/** 処理時間(ミリ秒) */
|
|
152
|
+
processingTimeMs: number;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* バッチ処理入力アイテム
|
|
158
|
+
*/
|
|
159
|
+
export interface MultimodalBatchItem {
|
|
160
|
+
/** テキストコンテンツ */
|
|
161
|
+
content: string;
|
|
162
|
+
/** VisionFeatures(オプション) */
|
|
163
|
+
visionFeatures?: VisionFeatures;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* バッチ処理結果
|
|
168
|
+
*/
|
|
169
|
+
export interface MultimodalBatchResult {
|
|
170
|
+
/** 成功した結果 */
|
|
171
|
+
results: MultimodalEmbeddingResultV2[];
|
|
172
|
+
/** 処理統計 */
|
|
173
|
+
stats: {
|
|
174
|
+
/** 処理件数 */
|
|
175
|
+
total: number;
|
|
176
|
+
/** 成功件数 */
|
|
177
|
+
success: number;
|
|
178
|
+
/** 失敗件数 */
|
|
179
|
+
failed: number;
|
|
180
|
+
/** 合計処理時間(ミリ秒) */
|
|
181
|
+
totalProcessingTimeMs: number;
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// =============================================================================
|
|
186
|
+
// バッチ処理最適化(並列化 + メトリクス)
|
|
187
|
+
// =============================================================================
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* バッチ処理パフォーマンスメトリクス
|
|
191
|
+
*/
|
|
192
|
+
export interface MultimodalBatchMetrics {
|
|
193
|
+
/** 総アイテム数 */
|
|
194
|
+
totalItems: number;
|
|
195
|
+
/** 成功件数 */
|
|
196
|
+
successCount: number;
|
|
197
|
+
/** 失敗件数 */
|
|
198
|
+
failedCount: number;
|
|
199
|
+
/** キャッシュヒット件数 */
|
|
200
|
+
cacheHitCount: number;
|
|
201
|
+
/** キャッシュヒット率 (0-1) */
|
|
202
|
+
cacheHitRate: number;
|
|
203
|
+
/** 平均処理時間(ミリ秒) */
|
|
204
|
+
avgProcessingTimeMs: number;
|
|
205
|
+
/** 合計処理時間(ミリ秒) */
|
|
206
|
+
totalProcessingTimeMs: number;
|
|
207
|
+
/** スループット(件/分) */
|
|
208
|
+
throughputPerMinute: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 最適化バッチ処理の進捗情報
|
|
213
|
+
*/
|
|
214
|
+
export interface OptimizedBatchProgress {
|
|
215
|
+
/** 完了件数 */
|
|
216
|
+
completed: number;
|
|
217
|
+
/** 総件数 */
|
|
218
|
+
total: number;
|
|
219
|
+
/** 進捗率 (0-1) */
|
|
220
|
+
progress: number;
|
|
221
|
+
/** 現在のアイテムインデックス */
|
|
222
|
+
currentIndex: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 最適化バッチ処理オプション
|
|
227
|
+
*/
|
|
228
|
+
export interface OptimizedBatchOptions {
|
|
229
|
+
/** 並列処理の同時実行数 (デフォルト: 5) */
|
|
230
|
+
concurrency?: number;
|
|
231
|
+
/** 個別アイテムのタイムアウト(ミリ秒)(デフォルト: 30000) */
|
|
232
|
+
itemTimeoutMs?: number;
|
|
233
|
+
/** バッチ全体のタイムアウト(ミリ秒)(デフォルト: 300000 = 5分) */
|
|
234
|
+
totalTimeoutMs?: number;
|
|
235
|
+
/** 進捗コールバック */
|
|
236
|
+
onProgress?: (progress: OptimizedBatchProgress) => void;
|
|
237
|
+
/** Embedding設定のオーバーライド */
|
|
238
|
+
embeddingConfig?: MultimodalEmbeddingConfig;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 最適化バッチ処理結果
|
|
243
|
+
*/
|
|
244
|
+
export interface OptimizedBatchResult {
|
|
245
|
+
/** 成功した結果 */
|
|
246
|
+
results: MultimodalEmbeddingResultV2[];
|
|
247
|
+
/** パフォーマンスメトリクス */
|
|
248
|
+
metrics: MultimodalBatchMetrics;
|
|
249
|
+
/** エラー情報(失敗したアイテム) */
|
|
250
|
+
errors: Array<{
|
|
251
|
+
index: number;
|
|
252
|
+
error: string;
|
|
253
|
+
}>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// =============================================================================
|
|
257
|
+
// サービスインターフェース
|
|
258
|
+
// =============================================================================
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Embeddingサービスインターフェース(DI用)
|
|
262
|
+
*/
|
|
263
|
+
export interface IEmbeddingService {
|
|
264
|
+
generateEmbedding(text: string, type: "query" | "passage"): Promise<number[]>;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// =============================================================================
|
|
268
|
+
// MultimodalEmbeddingService
|
|
269
|
+
// =============================================================================
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* マルチモーダルEmbeddingサービス
|
|
273
|
+
*
|
|
274
|
+
* VisionAnalyzerの特徴抽出結果とテキストEmbeddingを組み合わせて
|
|
275
|
+
* 統合Embeddingを生成します。
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```typescript
|
|
279
|
+
* const service = new MultimodalEmbeddingService(embeddingService);
|
|
280
|
+
*
|
|
281
|
+
* const result = await service.generateMultimodalEmbedding({
|
|
282
|
+
* textRepresentation: "Layout: two-column grid...",
|
|
283
|
+
* visionFeatureText: "Colors: blue, white. Whitespace: generous.",
|
|
284
|
+
* originalText: "Hero section design",
|
|
285
|
+
* });
|
|
286
|
+
*
|
|
287
|
+
* console.log(result.combinedEmbedding.length); // 768
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
export class MultimodalEmbeddingService {
|
|
291
|
+
private config: Required<MultimodalEmbeddingConfig>;
|
|
292
|
+
private embeddingService: IEmbeddingService;
|
|
293
|
+
|
|
294
|
+
constructor(embeddingService: IEmbeddingService, config: MultimodalEmbeddingConfig = {}) {
|
|
295
|
+
// 設定をマージ
|
|
296
|
+
this.config = { ...DEFAULT_MULTIMODAL_CONFIG, ...config };
|
|
297
|
+
|
|
298
|
+
// 重みの合計が1になるように正規化
|
|
299
|
+
this.normalizeWeights();
|
|
300
|
+
|
|
301
|
+
this.embeddingService = embeddingService;
|
|
302
|
+
|
|
303
|
+
if (process.env.NODE_ENV === "development") {
|
|
304
|
+
// eslint-disable-next-line no-console
|
|
305
|
+
console.log("[ML] MultimodalEmbeddingService created with config:", {
|
|
306
|
+
textWeight: this.config.textWeight,
|
|
307
|
+
visionWeight: this.config.visionWeight,
|
|
308
|
+
embeddingDimension: this.config.embeddingDimension,
|
|
309
|
+
searchMode: this.config.searchMode,
|
|
310
|
+
normalizeOutput: this.config.normalizeOutput,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 重みを正規化(合計が1になるように)
|
|
317
|
+
*/
|
|
318
|
+
private normalizeWeights(): void {
|
|
319
|
+
const totalWeight = this.config.textWeight + this.config.visionWeight;
|
|
320
|
+
if (totalWeight !== 1 && totalWeight > 0) {
|
|
321
|
+
this.config.textWeight = this.config.textWeight / totalWeight;
|
|
322
|
+
this.config.visionWeight = this.config.visionWeight / totalWeight;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* マルチモーダルEmbeddingを生成
|
|
328
|
+
*
|
|
329
|
+
* @param input - 入力データ
|
|
330
|
+
* @returns マルチモーダルEmbedding結果
|
|
331
|
+
*/
|
|
332
|
+
async generateMultimodalEmbedding(
|
|
333
|
+
input: MultimodalEmbeddingInput
|
|
334
|
+
): Promise<MultimodalEmbeddingResult> {
|
|
335
|
+
const startTime = Date.now();
|
|
336
|
+
|
|
337
|
+
// 入力バリデーション
|
|
338
|
+
multimodalEmbeddingInputSchema.parse(input);
|
|
339
|
+
|
|
340
|
+
if (process.env.NODE_ENV === "development") {
|
|
341
|
+
// eslint-disable-next-line no-console
|
|
342
|
+
console.log("[ML] Generating multimodal embedding for:", {
|
|
343
|
+
textRepLength: input.textRepresentation.length,
|
|
344
|
+
hasVisionFeatureText: !!input.visionFeatureText,
|
|
345
|
+
hasOriginalText: !!input.originalText,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// テキスト表現からEmbeddingを生成
|
|
350
|
+
const textForEmbedding = this.buildTextForEmbedding(input);
|
|
351
|
+
const textEmbedding = await this.embeddingService.generateEmbedding(
|
|
352
|
+
textForEmbedding,
|
|
353
|
+
"passage"
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Vision特徴テキストからEmbeddingを生成
|
|
357
|
+
const visionText = this.buildVisionText(input);
|
|
358
|
+
const visionEmbedding = await this.embeddingService.generateEmbedding(visionText, "passage");
|
|
359
|
+
|
|
360
|
+
// 重み付き結合
|
|
361
|
+
const combinedEmbedding = this.combineEmbeddings(textEmbedding, visionEmbedding);
|
|
362
|
+
|
|
363
|
+
const processingTimeMs = Date.now() - startTime;
|
|
364
|
+
|
|
365
|
+
if (process.env.NODE_ENV === "development") {
|
|
366
|
+
// eslint-disable-next-line no-console
|
|
367
|
+
console.log("[ML] Multimodal embedding generated in", processingTimeMs, "ms");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
combinedEmbedding,
|
|
372
|
+
textEmbedding,
|
|
373
|
+
visionEmbedding,
|
|
374
|
+
weights: {
|
|
375
|
+
text: this.config.textWeight,
|
|
376
|
+
vision: this.config.visionWeight,
|
|
377
|
+
},
|
|
378
|
+
processingTimeMs,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* テキストEmbedding用の文字列を構築
|
|
384
|
+
*/
|
|
385
|
+
private buildTextForEmbedding(input: MultimodalEmbeddingInput): string {
|
|
386
|
+
const parts: string[] = [];
|
|
387
|
+
|
|
388
|
+
if (input.originalText) {
|
|
389
|
+
parts.push(input.originalText);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
parts.push(input.textRepresentation);
|
|
393
|
+
|
|
394
|
+
return parts.join(" ");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Vision特徴テキストを構築
|
|
399
|
+
*/
|
|
400
|
+
private buildVisionText(input: MultimodalEmbeddingInput): string {
|
|
401
|
+
if (input.visionFeatureText) {
|
|
402
|
+
return input.visionFeatureText;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// visionFeatureTextがない場合はtextRepresentationを使用
|
|
406
|
+
return input.textRepresentation;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* 2つのEmbeddingを重み付き結合
|
|
411
|
+
*/
|
|
412
|
+
private combineEmbeddings(textEmbedding: number[], visionEmbedding: number[]): number[] {
|
|
413
|
+
if (textEmbedding.length !== visionEmbedding.length) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Embedding dimensions mismatch: text=${textEmbedding.length}, vision=${visionEmbedding.length}`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 片方の重みが1(もう一方が0)の場合は、そのままそのEmbeddingを返す
|
|
420
|
+
if (this.config.textWeight === 1) {
|
|
421
|
+
return [...textEmbedding];
|
|
422
|
+
}
|
|
423
|
+
if (this.config.visionWeight === 1) {
|
|
424
|
+
return [...visionEmbedding];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const combined: number[] = new Array(textEmbedding.length);
|
|
428
|
+
|
|
429
|
+
for (let i = 0; i < textEmbedding.length; i++) {
|
|
430
|
+
const textVal = textEmbedding[i] ?? 0;
|
|
431
|
+
const visionVal = visionEmbedding[i] ?? 0;
|
|
432
|
+
combined[i] = textVal * this.config.textWeight + visionVal * this.config.visionWeight;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// L2正規化
|
|
436
|
+
return this.normalizeVector(combined);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* ベクトルをL2正規化
|
|
441
|
+
*/
|
|
442
|
+
private normalizeVector(vector: number[]): number[] {
|
|
443
|
+
const norm = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
|
444
|
+
if (norm === 0) return vector;
|
|
445
|
+
return vector.map((val) => val / norm);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 現在の設定を取得
|
|
450
|
+
*/
|
|
451
|
+
getConfig(): Readonly<Required<MultimodalEmbeddingConfig>> {
|
|
452
|
+
return { ...this.config };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ===========================================================================
|
|
456
|
+
// 新しいメソッド
|
|
457
|
+
// ===========================================================================
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* マルチモーダルEmbeddingを生成
|
|
461
|
+
*
|
|
462
|
+
* searchModeに応じて適切なEmbeddingを生成します。
|
|
463
|
+
* - text_only: テキストEmbeddingのみ
|
|
464
|
+
* - vision_only: VisionEmbeddingのみ(visionFeatures必須)
|
|
465
|
+
* - combined: テキスト + Vision の重み付き結合
|
|
466
|
+
*
|
|
467
|
+
* Graceful Degradation:
|
|
468
|
+
* - visionFeaturesがない場合、combinedモードはtext_onlyにフォールバック
|
|
469
|
+
*
|
|
470
|
+
* @param content - テキストコンテンツ
|
|
471
|
+
* @param visionFeatures - VisionFeatures(オプション)
|
|
472
|
+
* @param config - オーバーライド設定(オプション)
|
|
473
|
+
* @returns MultimodalEmbeddingResultV2
|
|
474
|
+
*/
|
|
475
|
+
async createMultimodalEmbedding(
|
|
476
|
+
content: string,
|
|
477
|
+
visionFeatures?: VisionFeatures,
|
|
478
|
+
config?: MultimodalEmbeddingConfig
|
|
479
|
+
): Promise<MultimodalEmbeddingResultV2> {
|
|
480
|
+
const startTime = Date.now();
|
|
481
|
+
|
|
482
|
+
// 設定をマージ(インスタンス設定 < 呼び出し時設定)
|
|
483
|
+
const mergedConfig = this.mergeConfig(config);
|
|
484
|
+
|
|
485
|
+
// 検索モードを決定(Graceful Degradation対応)
|
|
486
|
+
let effectiveSearchMode = mergedConfig.searchMode;
|
|
487
|
+
|
|
488
|
+
// Graceful Degradation: visionFeaturesがない場合、combinedはtext_onlyにフォールバック
|
|
489
|
+
if (effectiveSearchMode === "combined" && !visionFeatures) {
|
|
490
|
+
effectiveSearchMode = "text_only";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// vision_onlyモードでvisionFeaturesがない場合はエラー
|
|
494
|
+
if (effectiveSearchMode === "vision_only" && !visionFeatures) {
|
|
495
|
+
throw new Error("visionFeatures is required for vision_only mode");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (process.env.NODE_ENV === "development") {
|
|
499
|
+
// eslint-disable-next-line no-console
|
|
500
|
+
console.log("[ML] createMultimodalEmbedding:", {
|
|
501
|
+
contentLength: content.length,
|
|
502
|
+
hasVisionFeatures: !!visionFeatures,
|
|
503
|
+
requestedMode: mergedConfig.searchMode,
|
|
504
|
+
effectiveMode: effectiveSearchMode,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Embedding生成
|
|
509
|
+
let textEmbedding: number[] | null = null;
|
|
510
|
+
let visionEmbedding: number[] | null = null;
|
|
511
|
+
let combinedEmbedding: number[] | null = null;
|
|
512
|
+
|
|
513
|
+
// text_only または combined: テキストEmbeddingを生成
|
|
514
|
+
if (effectiveSearchMode === "text_only" || effectiveSearchMode === "combined") {
|
|
515
|
+
textEmbedding = await this.embeddingService.generateEmbedding(content, "passage");
|
|
516
|
+
if (mergedConfig.normalizeOutput) {
|
|
517
|
+
textEmbedding = this.normalizeVector(textEmbedding);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// vision_only または combined: VisionEmbeddingを生成
|
|
522
|
+
if (
|
|
523
|
+
(effectiveSearchMode === "vision_only" || effectiveSearchMode === "combined") &&
|
|
524
|
+
visionFeatures
|
|
525
|
+
) {
|
|
526
|
+
const visionText = visionFeaturesToText(visionFeatures);
|
|
527
|
+
visionEmbedding = await this.embeddingService.generateEmbedding(visionText, "passage");
|
|
528
|
+
if (mergedConfig.normalizeOutput) {
|
|
529
|
+
visionEmbedding = this.normalizeVector(visionEmbedding);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// combinedEmbeddingを計算
|
|
534
|
+
if (effectiveSearchMode === "text_only" && textEmbedding) {
|
|
535
|
+
// text_only: combinedはtextEmbeddingと同等
|
|
536
|
+
combinedEmbedding = [...textEmbedding];
|
|
537
|
+
} else if (effectiveSearchMode === "vision_only" && visionEmbedding) {
|
|
538
|
+
// vision_only: combinedはvisionEmbeddingと同等
|
|
539
|
+
combinedEmbedding = [...visionEmbedding];
|
|
540
|
+
} else if (effectiveSearchMode === "combined" && textEmbedding && visionEmbedding) {
|
|
541
|
+
// combined: 重み付き結合
|
|
542
|
+
combinedEmbedding = this.combineEmbeddingsWithConfig(
|
|
543
|
+
textEmbedding,
|
|
544
|
+
visionEmbedding,
|
|
545
|
+
mergedConfig
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const processingTimeMs = Date.now() - startTime;
|
|
550
|
+
|
|
551
|
+
if (process.env.NODE_ENV === "development") {
|
|
552
|
+
// eslint-disable-next-line no-console
|
|
553
|
+
console.log("[ML] createMultimodalEmbedding completed in", processingTimeMs, "ms");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
textEmbedding,
|
|
558
|
+
visionEmbedding,
|
|
559
|
+
combinedEmbedding,
|
|
560
|
+
searchMode: effectiveSearchMode,
|
|
561
|
+
weights: {
|
|
562
|
+
text: mergedConfig.textWeight,
|
|
563
|
+
vision: mergedConfig.visionWeight,
|
|
564
|
+
},
|
|
565
|
+
metadata: {
|
|
566
|
+
textRepresentation: content,
|
|
567
|
+
...(visionFeatures !== undefined && { visionFeatures }),
|
|
568
|
+
processingTimeMs,
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* バッチでマルチモーダルEmbeddingを生成
|
|
575
|
+
*
|
|
576
|
+
* @param items - バッチ処理入力アイテムの配列
|
|
577
|
+
* @param config - オーバーライド設定(オプション)
|
|
578
|
+
* @returns MultimodalBatchResult
|
|
579
|
+
*/
|
|
580
|
+
async createBatchMultimodalEmbeddings(
|
|
581
|
+
items: MultimodalBatchItem[],
|
|
582
|
+
config?: MultimodalEmbeddingConfig
|
|
583
|
+
): Promise<MultimodalBatchResult> {
|
|
584
|
+
const startTime = Date.now();
|
|
585
|
+
|
|
586
|
+
if (items.length === 0) {
|
|
587
|
+
return {
|
|
588
|
+
results: [],
|
|
589
|
+
stats: {
|
|
590
|
+
total: 0,
|
|
591
|
+
success: 0,
|
|
592
|
+
failed: 0,
|
|
593
|
+
totalProcessingTimeMs: 0,
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (process.env.NODE_ENV === "development") {
|
|
599
|
+
// eslint-disable-next-line no-console
|
|
600
|
+
console.log("[ML] createBatchMultimodalEmbeddings:", {
|
|
601
|
+
count: items.length,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const results: MultimodalEmbeddingResultV2[] = [];
|
|
606
|
+
let successCount = 0;
|
|
607
|
+
let failedCount = 0;
|
|
608
|
+
|
|
609
|
+
// 各アイテムを処理(partial failure対応)
|
|
610
|
+
for (const item of items) {
|
|
611
|
+
try {
|
|
612
|
+
const result = await this.createMultimodalEmbedding(
|
|
613
|
+
item.content,
|
|
614
|
+
item.visionFeatures,
|
|
615
|
+
config
|
|
616
|
+
);
|
|
617
|
+
results.push(result);
|
|
618
|
+
successCount++;
|
|
619
|
+
} catch (error) {
|
|
620
|
+
// partial failure: エラーを記録し、処理を続行
|
|
621
|
+
failedCount++;
|
|
622
|
+
if (process.env.NODE_ENV === "development") {
|
|
623
|
+
console.warn("[ML] Batch item failed:", error);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const totalProcessingTimeMs = Date.now() - startTime;
|
|
629
|
+
|
|
630
|
+
if (process.env.NODE_ENV === "development") {
|
|
631
|
+
// eslint-disable-next-line no-console
|
|
632
|
+
console.log("[ML] createBatchMultimodalEmbeddings completed:", {
|
|
633
|
+
total: items.length,
|
|
634
|
+
success: successCount,
|
|
635
|
+
failed: failedCount,
|
|
636
|
+
timeMs: totalProcessingTimeMs,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
results,
|
|
642
|
+
stats: {
|
|
643
|
+
total: items.length,
|
|
644
|
+
success: successCount,
|
|
645
|
+
failed: failedCount,
|
|
646
|
+
totalProcessingTimeMs,
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* 設定をマージ
|
|
653
|
+
*/
|
|
654
|
+
private mergeConfig(override?: MultimodalEmbeddingConfig): Required<MultimodalEmbeddingConfig> {
|
|
655
|
+
if (!override) {
|
|
656
|
+
return { ...this.config };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const merged = { ...this.config, ...override };
|
|
660
|
+
|
|
661
|
+
// 重みの正規化
|
|
662
|
+
const totalWeight = merged.textWeight + merged.visionWeight;
|
|
663
|
+
if (totalWeight !== 1 && totalWeight > 0) {
|
|
664
|
+
merged.textWeight = merged.textWeight / totalWeight;
|
|
665
|
+
merged.visionWeight = merged.visionWeight / totalWeight;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return merged;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* 設定を使用して2つのEmbeddingを重み付き結合
|
|
673
|
+
*/
|
|
674
|
+
private combineEmbeddingsWithConfig(
|
|
675
|
+
textEmbedding: number[],
|
|
676
|
+
visionEmbedding: number[],
|
|
677
|
+
config: Required<MultimodalEmbeddingConfig>
|
|
678
|
+
): number[] {
|
|
679
|
+
if (textEmbedding.length !== visionEmbedding.length) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`Embedding dimensions mismatch: text=${textEmbedding.length}, vision=${visionEmbedding.length}`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const combined: number[] = new Array(textEmbedding.length);
|
|
686
|
+
|
|
687
|
+
for (let i = 0; i < textEmbedding.length; i++) {
|
|
688
|
+
const textVal = textEmbedding[i] ?? 0;
|
|
689
|
+
const visionVal = visionEmbedding[i] ?? 0;
|
|
690
|
+
combined[i] = textVal * config.textWeight + visionVal * config.visionWeight;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// L2正規化(normalizeOutput=trueの場合)
|
|
694
|
+
if (config.normalizeOutput) {
|
|
695
|
+
return this.normalizeVector(combined);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return combined;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ===========================================================================
|
|
702
|
+
// バッチ処理最適化(並列化 + メトリクス)
|
|
703
|
+
// ===========================================================================
|
|
704
|
+
|
|
705
|
+
/** キャッシュ(コンテンツハッシュ -> 結果) */
|
|
706
|
+
private embeddingCache: Map<string, MultimodalEmbeddingResultV2> = new Map();
|
|
707
|
+
|
|
708
|
+
/** 最大キャッシュサイズ */
|
|
709
|
+
private readonly maxCacheSize = 5000;
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* キャッシュキーを生成
|
|
713
|
+
*/
|
|
714
|
+
private generateCacheKey(
|
|
715
|
+
content: string,
|
|
716
|
+
visionFeatures?: VisionFeatures,
|
|
717
|
+
searchMode?: SearchMode
|
|
718
|
+
): string {
|
|
719
|
+
const visionPart = visionFeatures
|
|
720
|
+
? JSON.stringify({
|
|
721
|
+
r: visionFeatures.rhythm,
|
|
722
|
+
w: visionFeatures.whitespaceRatio,
|
|
723
|
+
d: visionFeatures.density,
|
|
724
|
+
g: visionFeatures.gravity,
|
|
725
|
+
t: visionFeatures.theme,
|
|
726
|
+
m: visionFeatures.mood,
|
|
727
|
+
b: visionFeatures.brandTone,
|
|
728
|
+
})
|
|
729
|
+
: "null";
|
|
730
|
+
return `${searchMode ?? this.config.searchMode}:${content}:${visionPart}`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* キャッシュから取得(LRU更新)
|
|
735
|
+
*/
|
|
736
|
+
private getCacheEntry(key: string): MultimodalEmbeddingResultV2 | undefined {
|
|
737
|
+
const value = this.embeddingCache.get(key);
|
|
738
|
+
if (value !== undefined) {
|
|
739
|
+
// LRU: 最近使用したエントリを末尾に移動
|
|
740
|
+
this.embeddingCache.delete(key);
|
|
741
|
+
this.embeddingCache.set(key, value);
|
|
742
|
+
}
|
|
743
|
+
return value;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* キャッシュに保存(LRU eviction)
|
|
748
|
+
*/
|
|
749
|
+
private setCacheEntry(key: string, value: MultimodalEmbeddingResultV2): void {
|
|
750
|
+
// キャッシュがいっぱいの場合、最も古いエントリを削除
|
|
751
|
+
while (this.embeddingCache.size >= this.maxCacheSize) {
|
|
752
|
+
const oldestKey = this.embeddingCache.keys().next().value;
|
|
753
|
+
if (oldestKey !== undefined) {
|
|
754
|
+
this.embeddingCache.delete(oldestKey);
|
|
755
|
+
} else {
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
this.embeddingCache.set(key, value);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* 最適化バッチでマルチモーダルEmbeddingを生成
|
|
764
|
+
*
|
|
765
|
+
* p-limitを使用した並列処理により、高スループットを実現します。
|
|
766
|
+
*
|
|
767
|
+
* 特徴:
|
|
768
|
+
* - 並列処理(デフォルト同時実行数: 5)
|
|
769
|
+
* - キャッシュによる重複処理の回避
|
|
770
|
+
* - 個別アイテムタイムアウト
|
|
771
|
+
* - バッチ全体タイムアウト
|
|
772
|
+
* - 進捗コールバック
|
|
773
|
+
* - Graceful Degradation(部分失敗許容)
|
|
774
|
+
* - 詳細なパフォーマンスメトリクス
|
|
775
|
+
*
|
|
776
|
+
* @param items - バッチ処理入力アイテムの配列
|
|
777
|
+
* @param options - 最適化オプション
|
|
778
|
+
* @returns OptimizedBatchResult
|
|
779
|
+
*/
|
|
780
|
+
async createOptimizedBatchMultimodalEmbeddings(
|
|
781
|
+
items: MultimodalBatchItem[],
|
|
782
|
+
options: OptimizedBatchOptions = {}
|
|
783
|
+
): Promise<OptimizedBatchResult> {
|
|
784
|
+
const startTime = Date.now();
|
|
785
|
+
|
|
786
|
+
// デフォルトオプション
|
|
787
|
+
const concurrency = options.concurrency ?? 5;
|
|
788
|
+
const itemTimeoutMs = options.itemTimeoutMs ?? 30000;
|
|
789
|
+
const totalTimeoutMs = options.totalTimeoutMs ?? 300000; // 5分
|
|
790
|
+
const onProgress = options.onProgress;
|
|
791
|
+
const embeddingConfig = options.embeddingConfig;
|
|
792
|
+
|
|
793
|
+
// 空の入力の場合
|
|
794
|
+
if (items.length === 0) {
|
|
795
|
+
return {
|
|
796
|
+
results: [],
|
|
797
|
+
metrics: {
|
|
798
|
+
totalItems: 0,
|
|
799
|
+
successCount: 0,
|
|
800
|
+
failedCount: 0,
|
|
801
|
+
cacheHitCount: 0,
|
|
802
|
+
cacheHitRate: 0,
|
|
803
|
+
avgProcessingTimeMs: 0,
|
|
804
|
+
totalProcessingTimeMs: 0,
|
|
805
|
+
throughputPerMinute: 0,
|
|
806
|
+
},
|
|
807
|
+
errors: [],
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (process.env.NODE_ENV === "development") {
|
|
812
|
+
// eslint-disable-next-line no-console
|
|
813
|
+
console.log("[ML] createOptimizedBatchMultimodalEmbeddings:", {
|
|
814
|
+
count: items.length,
|
|
815
|
+
concurrency,
|
|
816
|
+
itemTimeoutMs,
|
|
817
|
+
totalTimeoutMs,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// p-limitでの並列制御
|
|
822
|
+
const limit = pLimit(concurrency);
|
|
823
|
+
|
|
824
|
+
// 結果とメトリクス追跡
|
|
825
|
+
const results: Array<{ index: number; result: MultimodalEmbeddingResultV2 }> = [];
|
|
826
|
+
const errors: Array<{ index: number; error: string }> = [];
|
|
827
|
+
let cacheHitCount = 0;
|
|
828
|
+
let completedCount = 0;
|
|
829
|
+
const processingTimes: number[] = [];
|
|
830
|
+
|
|
831
|
+
// タイムアウトフラグ
|
|
832
|
+
let isTimedOut = false;
|
|
833
|
+
|
|
834
|
+
// 全体タイムアウト設定
|
|
835
|
+
const totalTimeoutPromise = new Promise<"timeout">((resolve) => {
|
|
836
|
+
setTimeout(() => {
|
|
837
|
+
isTimedOut = true;
|
|
838
|
+
resolve("timeout");
|
|
839
|
+
}, totalTimeoutMs);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// 各アイテムの処理関数
|
|
843
|
+
const processItem = async (item: MultimodalBatchItem, index: number): Promise<void> => {
|
|
844
|
+
// 全体タイムアウトチェック
|
|
845
|
+
if (isTimedOut) {
|
|
846
|
+
errors.push({ index, error: "Batch timeout exceeded" });
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const itemStartTime = Date.now();
|
|
851
|
+
|
|
852
|
+
// キャッシュキーを生成
|
|
853
|
+
const cacheKey = this.generateCacheKey(
|
|
854
|
+
item.content,
|
|
855
|
+
item.visionFeatures,
|
|
856
|
+
embeddingConfig?.searchMode
|
|
857
|
+
);
|
|
858
|
+
|
|
859
|
+
// キャッシュチェック
|
|
860
|
+
const cached = this.getCacheEntry(cacheKey);
|
|
861
|
+
if (cached) {
|
|
862
|
+
cacheHitCount++;
|
|
863
|
+
results.push({ index, result: cached });
|
|
864
|
+
completedCount++;
|
|
865
|
+
|
|
866
|
+
// 進捗コールバック
|
|
867
|
+
if (onProgress) {
|
|
868
|
+
onProgress({
|
|
869
|
+
completed: completedCount,
|
|
870
|
+
total: items.length,
|
|
871
|
+
progress: completedCount / items.length,
|
|
872
|
+
currentIndex: index,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
processingTimes.push(Date.now() - itemStartTime);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// タイムアウト付きでEmbedding生成
|
|
881
|
+
try {
|
|
882
|
+
const result = await Promise.race([
|
|
883
|
+
this.createMultimodalEmbedding(item.content, item.visionFeatures, embeddingConfig),
|
|
884
|
+
new Promise<never>((_, reject) =>
|
|
885
|
+
setTimeout(() => reject(new Error("Item timeout")), itemTimeoutMs)
|
|
886
|
+
),
|
|
887
|
+
]);
|
|
888
|
+
|
|
889
|
+
// キャッシュに保存
|
|
890
|
+
this.setCacheEntry(cacheKey, result);
|
|
891
|
+
results.push({ index, result });
|
|
892
|
+
} catch (error) {
|
|
893
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
894
|
+
errors.push({ index, error: errorMessage });
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
completedCount++;
|
|
898
|
+
processingTimes.push(Date.now() - itemStartTime);
|
|
899
|
+
|
|
900
|
+
// 進捗コールバック
|
|
901
|
+
if (onProgress) {
|
|
902
|
+
onProgress({
|
|
903
|
+
completed: completedCount,
|
|
904
|
+
total: items.length,
|
|
905
|
+
progress: completedCount / items.length,
|
|
906
|
+
currentIndex: index,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
// 並列処理を実行
|
|
912
|
+
const processingPromise = Promise.all(
|
|
913
|
+
items.map((item, index) => limit(() => processItem(item, index)))
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
// 全体タイムアウトとの競合
|
|
917
|
+
await Promise.race([processingPromise, totalTimeoutPromise]);
|
|
918
|
+
|
|
919
|
+
// 残りの未処理アイテムをエラーとして追加
|
|
920
|
+
if (isTimedOut) {
|
|
921
|
+
for (let i = 0; i < items.length; i++) {
|
|
922
|
+
const hasResult = results.some((r) => r.index === i);
|
|
923
|
+
const hasError = errors.some((e) => e.index === i);
|
|
924
|
+
if (!hasResult && !hasError) {
|
|
925
|
+
errors.push({ index: i, error: "Batch timeout exceeded" });
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// 結果をインデックス順にソート
|
|
931
|
+
results.sort((a, b) => a.index - b.index);
|
|
932
|
+
errors.sort((a, b) => a.index - b.index);
|
|
933
|
+
|
|
934
|
+
const totalProcessingTimeMs = Date.now() - startTime;
|
|
935
|
+
|
|
936
|
+
// メトリクス計算
|
|
937
|
+
const successCount = results.length;
|
|
938
|
+
const failedCount = errors.length;
|
|
939
|
+
const avgProcessingTimeMs =
|
|
940
|
+
processingTimes.length > 0
|
|
941
|
+
? processingTimes.reduce((sum, t) => sum + t, 0) / processingTimes.length
|
|
942
|
+
: 0;
|
|
943
|
+
const throughputPerMinute =
|
|
944
|
+
totalProcessingTimeMs > 0 ? (successCount / totalProcessingTimeMs) * 60000 : 0;
|
|
945
|
+
|
|
946
|
+
const metrics: MultimodalBatchMetrics = {
|
|
947
|
+
totalItems: items.length,
|
|
948
|
+
successCount,
|
|
949
|
+
failedCount,
|
|
950
|
+
cacheHitCount,
|
|
951
|
+
cacheHitRate: items.length > 0 ? cacheHitCount / items.length : 0,
|
|
952
|
+
avgProcessingTimeMs,
|
|
953
|
+
totalProcessingTimeMs,
|
|
954
|
+
throughputPerMinute,
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
if (process.env.NODE_ENV === "development") {
|
|
958
|
+
// eslint-disable-next-line no-console
|
|
959
|
+
console.log("[ML] createOptimizedBatchMultimodalEmbeddings completed:", {
|
|
960
|
+
...metrics,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return {
|
|
965
|
+
results: results.map((r) => r.result),
|
|
966
|
+
metrics,
|
|
967
|
+
errors,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Embeddingキャッシュをクリア
|
|
973
|
+
*/
|
|
974
|
+
clearEmbeddingCache(): void {
|
|
975
|
+
this.embeddingCache.clear();
|
|
976
|
+
|
|
977
|
+
if (process.env.NODE_ENV === "development") {
|
|
978
|
+
// eslint-disable-next-line no-console
|
|
979
|
+
console.log("[ML] Embedding cache cleared");
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* キャッシュ統計を取得
|
|
985
|
+
*/
|
|
986
|
+
getEmbeddingCacheStats(): { size: number; maxSize: number } {
|
|
987
|
+
return {
|
|
988
|
+
size: this.embeddingCache.size,
|
|
989
|
+
maxSize: this.maxCacheSize,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
}
|