@nahisaho/shikigami-mcp-server 1.7.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/cache/__tests__/global.test.d.ts +6 -0
- package/dist/cache/__tests__/global.test.d.ts.map +1 -0
- package/dist/cache/__tests__/global.test.js +269 -0
- package/dist/cache/__tests__/global.test.js.map +1 -0
- package/dist/cache/__tests__/manager.test.d.ts +6 -0
- package/dist/cache/__tests__/manager.test.d.ts.map +1 -0
- package/dist/cache/__tests__/manager.test.js +286 -0
- package/dist/cache/__tests__/manager.test.js.map +1 -0
- package/dist/cache/__tests__/semantic.test.d.ts +6 -0
- package/dist/cache/__tests__/semantic.test.d.ts.map +1 -0
- package/dist/cache/__tests__/semantic.test.js +271 -0
- package/dist/cache/__tests__/semantic.test.js.map +1 -0
- package/dist/cache/__tests__/store.test.d.ts +6 -0
- package/dist/cache/__tests__/store.test.d.ts.map +1 -0
- package/dist/cache/__tests__/store.test.js +289 -0
- package/dist/cache/__tests__/store.test.js.map +1 -0
- package/dist/cache/global.d.ts +140 -0
- package/dist/cache/global.d.ts.map +1 -0
- package/dist/cache/global.js +260 -0
- package/dist/cache/global.js.map +1 -0
- package/dist/cache/index.d.ts +10 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +10 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/manager.d.ts +146 -0
- package/dist/cache/manager.d.ts.map +1 -0
- package/dist/cache/manager.js +229 -0
- package/dist/cache/manager.js.map +1 -0
- package/dist/cache/semantic.d.ts +164 -0
- package/dist/cache/semantic.d.ts.map +1 -0
- package/dist/cache/semantic.js +241 -0
- package/dist/cache/semantic.js.map +1 -0
- package/dist/cache/store.d.ts +98 -0
- package/dist/cache/store.d.ts.map +1 -0
- package/dist/cache/store.js +469 -0
- package/dist/cache/store.js.map +1 -0
- package/dist/cache/types.d.ts +171 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +8 -0
- package/dist/cache/types.js.map +1 -0
- package/dist/config/types.d.ts +67 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +30 -0
- package/dist/config/types.js.map +1 -1
- package/dist/tools/__tests__/multilingual-search.test.d.ts +7 -0
- package/dist/tools/__tests__/multilingual-search.test.d.ts.map +1 -0
- package/dist/tools/__tests__/multilingual-search.test.js +71 -0
- package/dist/tools/__tests__/multilingual-search.test.js.map +1 -0
- package/dist/tools/search/recovery/__tests__/logger.test.d.ts +8 -0
- package/dist/tools/search/recovery/__tests__/logger.test.d.ts.map +1 -0
- package/dist/tools/search/recovery/__tests__/logger.test.js +249 -0
- package/dist/tools/search/recovery/__tests__/logger.test.js.map +1 -0
- package/dist/tools/search/recovery/__tests__/manager-logger.test.d.ts +8 -0
- package/dist/tools/search/recovery/__tests__/manager-logger.test.d.ts.map +1 -0
- package/dist/tools/search/recovery/__tests__/manager-logger.test.js +158 -0
- package/dist/tools/search/recovery/__tests__/manager-logger.test.js.map +1 -0
- package/dist/tools/search/recovery/index.d.ts +31 -2
- package/dist/tools/search/recovery/index.d.ts.map +1 -1
- package/dist/tools/search/recovery/index.js +51 -7
- package/dist/tools/search/recovery/index.js.map +1 -1
- package/dist/tools/search/recovery/logger.d.ts +149 -0
- package/dist/tools/search/recovery/logger.d.ts.map +1 -0
- package/dist/tools/search/recovery/logger.js +218 -0
- package/dist/tools/search/recovery/logger.js.map +1 -0
- package/dist/tools/search.d.ts +48 -0
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +152 -0
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/visit/recovery/__tests__/index.test.d.ts +10 -0
- package/dist/tools/visit/recovery/__tests__/index.test.d.ts.map +1 -0
- package/dist/tools/visit/recovery/__tests__/index.test.js +239 -0
- package/dist/tools/visit/recovery/__tests__/index.test.js.map +1 -0
- package/dist/tools/visit/recovery/__tests__/wayback.test.d.ts +8 -0
- package/dist/tools/visit/recovery/__tests__/wayback.test.d.ts.map +1 -0
- package/dist/tools/visit/recovery/__tests__/wayback.test.js +271 -0
- package/dist/tools/visit/recovery/__tests__/wayback.test.js.map +1 -0
- package/dist/tools/visit/recovery/index.d.ts +126 -0
- package/dist/tools/visit/recovery/index.d.ts.map +1 -0
- package/dist/tools/visit/recovery/index.js +203 -0
- package/dist/tools/visit/recovery/index.js.map +1 -0
- package/dist/tools/visit/recovery/wayback.d.ts +101 -0
- package/dist/tools/visit/recovery/wayback.d.ts.map +1 -0
- package/dist/tools/visit/recovery/wayback.js +140 -0
- package/dist/tools/visit/recovery/wayback.js.map +1 -0
- package/dist/tools/visit.d.ts +33 -0
- package/dist/tools/visit.d.ts.map +1 -1
- package/dist/tools/visit.js +127 -1
- package/dist/tools/visit.js.map +1 -1
- package/package.json +7 -3
- package/shikigami.config.example.yaml +9 -0
- package/src/cache/__tests__/global.test.ts +340 -0
- package/src/cache/__tests__/manager.test.ts +353 -0
- package/src/cache/__tests__/semantic.test.ts +331 -0
- package/src/cache/__tests__/store.test.ts +369 -0
- package/src/cache/global.ts +351 -0
- package/src/cache/index.ts +10 -0
- package/src/cache/manager.ts +325 -0
- package/src/cache/semantic.ts +368 -0
- package/src/cache/store.ts +555 -0
- package/src/cache/types.ts +189 -0
- package/src/config/types.ts +108 -0
- package/src/tools/__tests__/multilingual-search.test.ts +88 -0
- package/src/tools/search/recovery/__tests__/logger.test.ts +334 -0
- package/src/tools/search/recovery/__tests__/manager-logger.test.ts +199 -0
- package/src/tools/search/recovery/index.ts +67 -9
- package/src/tools/search/recovery/logger.ts +351 -0
- package/src/tools/search.ts +212 -0
- package/src/tools/visit/recovery/__tests__/index.test.ts +297 -0
- package/src/tools/visit/recovery/__tests__/wayback.test.ts +344 -0
- package/src/tools/visit/recovery/index.ts +312 -0
- package/src/tools/visit/recovery/wayback.ts +210 -0
- package/src/tools/visit.ts +159 -2
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecoveryLogger - フォールバックログ統計・警告機能
|
|
3
|
+
*
|
|
4
|
+
* TSK-1-001: RecoveryLogger実装
|
|
5
|
+
* REQ-SRCH-005-03: フォールバックログ
|
|
6
|
+
* DES-SRCH-005-03: RecoveryLogger設計
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { RecoveryLogEntry } from './types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 戦略別統計
|
|
13
|
+
*/
|
|
14
|
+
export interface StrategyStats {
|
|
15
|
+
/** 戦略名 */
|
|
16
|
+
strategy: string;
|
|
17
|
+
/** 試行回数 */
|
|
18
|
+
attempts: number;
|
|
19
|
+
/** 成功回数 */
|
|
20
|
+
successCount: number;
|
|
21
|
+
/** 成功率 */
|
|
22
|
+
successRate: number;
|
|
23
|
+
/** 平均処理時間(ms) */
|
|
24
|
+
avgDurationMs: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* クエリ失敗情報
|
|
29
|
+
*/
|
|
30
|
+
export interface QueryFailureInfo {
|
|
31
|
+
/** クエリ文字列 */
|
|
32
|
+
query: string;
|
|
33
|
+
/** 失敗回数 */
|
|
34
|
+
failureCount: number;
|
|
35
|
+
/** 最後の失敗日時 */
|
|
36
|
+
lastFailure: Date;
|
|
37
|
+
/** 試行した戦略一覧 */
|
|
38
|
+
strategies: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* リカバリー統計
|
|
43
|
+
*/
|
|
44
|
+
export interface RecoveryStats {
|
|
45
|
+
/** 総試行回数 */
|
|
46
|
+
totalAttempts: number;
|
|
47
|
+
/** 成功回数 */
|
|
48
|
+
successCount: number;
|
|
49
|
+
/** 失敗回数 */
|
|
50
|
+
failureCount: number;
|
|
51
|
+
/** 成功率 */
|
|
52
|
+
successRate: number;
|
|
53
|
+
/** 平均処理時間(ms) */
|
|
54
|
+
avgDurationMs: number;
|
|
55
|
+
/** 戦略別統計 */
|
|
56
|
+
byStrategy: Record<string, StrategyStats>;
|
|
57
|
+
/** 高頻度失敗クエリ */
|
|
58
|
+
highFrequencyFailures: QueryFailureInfo[];
|
|
59
|
+
/** 統計期間開始 */
|
|
60
|
+
periodStart: Date;
|
|
61
|
+
/** 統計期間終了 */
|
|
62
|
+
periodEnd: Date;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 拡張ログエントリ(処理時間を含む)
|
|
67
|
+
*/
|
|
68
|
+
export interface ExtendedLogEntry extends RecoveryLogEntry {
|
|
69
|
+
/** 一意のID */
|
|
70
|
+
id: string;
|
|
71
|
+
/** リカバリータイプ */
|
|
72
|
+
type?: 'search' | 'visit';
|
|
73
|
+
/** 処理時間(ms) */
|
|
74
|
+
durationMs: number;
|
|
75
|
+
/** エラーメッセージ(失敗時) */
|
|
76
|
+
error?: string;
|
|
77
|
+
/** 代替クエリの信頼度 */
|
|
78
|
+
confidence?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* ロガー設定
|
|
83
|
+
*/
|
|
84
|
+
export interface RecoveryLoggerConfig {
|
|
85
|
+
/** 統計出力間隔(試行回数) */
|
|
86
|
+
statsInterval: number;
|
|
87
|
+
/** 高頻度失敗警告閾値 */
|
|
88
|
+
warnThreshold: number;
|
|
89
|
+
/** 最大ログエントリ保持数 */
|
|
90
|
+
maxEntries: number;
|
|
91
|
+
/** 統計集計期間(ms) */
|
|
92
|
+
statsPeriodMs: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* デフォルト設定
|
|
97
|
+
*/
|
|
98
|
+
export const DEFAULT_LOGGER_CONFIG: RecoveryLoggerConfig = {
|
|
99
|
+
statsInterval: 100,
|
|
100
|
+
warnThreshold: 5,
|
|
101
|
+
maxEntries: 1000,
|
|
102
|
+
statsPeriodMs: 24 * 60 * 60 * 1000, // 24時間
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* リカバリーログ管理クラス
|
|
107
|
+
*
|
|
108
|
+
* フォールバック試行のログ記録、統計計算、高頻度失敗クエリの検出を行う
|
|
109
|
+
*/
|
|
110
|
+
export class RecoveryLogger {
|
|
111
|
+
private readonly entries: ExtendedLogEntry[] = [];
|
|
112
|
+
private readonly config: RecoveryLoggerConfig;
|
|
113
|
+
private readonly queryFailureMap: Map<string, QueryFailureInfo> = new Map();
|
|
114
|
+
private attemptCount = 0;
|
|
115
|
+
private idCounter = 0;
|
|
116
|
+
|
|
117
|
+
constructor(config?: Partial<RecoveryLoggerConfig>) {
|
|
118
|
+
this.config = { ...DEFAULT_LOGGER_CONFIG, ...config };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* UUIDライクなIDを生成
|
|
123
|
+
*/
|
|
124
|
+
private generateId(): string {
|
|
125
|
+
this.idCounter++;
|
|
126
|
+
const timestamp = Date.now().toString(36);
|
|
127
|
+
const counter = this.idCounter.toString(36).padStart(4, '0');
|
|
128
|
+
return `${timestamp}-${counter}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* ログエントリを記録
|
|
133
|
+
*/
|
|
134
|
+
log(entry: Omit<ExtendedLogEntry, 'id'>): void {
|
|
135
|
+
const fullEntry: ExtendedLogEntry = {
|
|
136
|
+
...entry,
|
|
137
|
+
id: this.generateId(),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
this.entries.push(fullEntry);
|
|
141
|
+
this.attemptCount++;
|
|
142
|
+
|
|
143
|
+
// 最大エントリ数を超えたら古いものを削除
|
|
144
|
+
if (this.entries.length > this.config.maxEntries) {
|
|
145
|
+
this.entries.shift();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 失敗マップを更新
|
|
149
|
+
if (!entry.success) {
|
|
150
|
+
this.updateFailureMap(entry);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 定期的に統計を出力
|
|
154
|
+
if (this.attemptCount % this.config.statsInterval === 0) {
|
|
155
|
+
this.outputStats();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 高頻度失敗をチェック
|
|
159
|
+
this.checkHighFrequencyFailures(entry.originalQuery);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 失敗マップを更新
|
|
164
|
+
*/
|
|
165
|
+
private updateFailureMap(entry: Omit<ExtendedLogEntry, 'id'>): void {
|
|
166
|
+
const existing = this.queryFailureMap.get(entry.originalQuery);
|
|
167
|
+
|
|
168
|
+
if (existing) {
|
|
169
|
+
existing.failureCount++;
|
|
170
|
+
existing.lastFailure = entry.timestamp;
|
|
171
|
+
if (!existing.strategies.includes(entry.strategy)) {
|
|
172
|
+
existing.strategies.push(entry.strategy);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
this.queryFailureMap.set(entry.originalQuery, {
|
|
176
|
+
query: entry.originalQuery,
|
|
177
|
+
failureCount: 1,
|
|
178
|
+
lastFailure: entry.timestamp,
|
|
179
|
+
strategies: [entry.strategy],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 高頻度失敗をチェックして警告
|
|
186
|
+
*/
|
|
187
|
+
private checkHighFrequencyFailures(query: string): void {
|
|
188
|
+
const failure = this.queryFailureMap.get(query);
|
|
189
|
+
if (failure && failure.failureCount === this.config.warnThreshold) {
|
|
190
|
+
console.error(
|
|
191
|
+
`[RecoveryLogger] ⚠️ High frequency failure detected: "${query}" (${failure.failureCount} failures)`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 統計をstderrに出力
|
|
198
|
+
*/
|
|
199
|
+
private outputStats(): void {
|
|
200
|
+
const stats = this.getStats();
|
|
201
|
+
console.error(
|
|
202
|
+
`[RecoveryLogger] 📊 Stats: ${stats.totalAttempts} attempts, ${(stats.successRate * 100).toFixed(1)}% success rate`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 統計情報を取得
|
|
208
|
+
*/
|
|
209
|
+
getStats(): RecoveryStats {
|
|
210
|
+
const now = new Date();
|
|
211
|
+
const periodStart = new Date(now.getTime() - this.config.statsPeriodMs);
|
|
212
|
+
|
|
213
|
+
// 期間内のエントリをフィルタ
|
|
214
|
+
const recentEntries = this.entries.filter(
|
|
215
|
+
(e) => e.timestamp >= periodStart
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (recentEntries.length === 0) {
|
|
219
|
+
return {
|
|
220
|
+
totalAttempts: 0,
|
|
221
|
+
successCount: 0,
|
|
222
|
+
failureCount: 0,
|
|
223
|
+
successRate: 0,
|
|
224
|
+
avgDurationMs: 0,
|
|
225
|
+
byStrategy: {},
|
|
226
|
+
highFrequencyFailures: [],
|
|
227
|
+
periodStart,
|
|
228
|
+
periodEnd: now,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const successCount = recentEntries.filter((e) => e.success).length;
|
|
233
|
+
const failureCount = recentEntries.length - successCount;
|
|
234
|
+
const totalDurationMs = recentEntries.reduce(
|
|
235
|
+
(sum, e) => sum + (e.durationMs || 0),
|
|
236
|
+
0
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// 戦略別統計
|
|
240
|
+
const byStrategy: Record<string, StrategyStats> = {};
|
|
241
|
+
for (const entry of recentEntries) {
|
|
242
|
+
if (!byStrategy[entry.strategy]) {
|
|
243
|
+
byStrategy[entry.strategy] = {
|
|
244
|
+
strategy: entry.strategy,
|
|
245
|
+
attempts: 0,
|
|
246
|
+
successCount: 0,
|
|
247
|
+
successRate: 0,
|
|
248
|
+
avgDurationMs: 0,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const stats = byStrategy[entry.strategy];
|
|
252
|
+
stats.attempts++;
|
|
253
|
+
if (entry.success) {
|
|
254
|
+
stats.successCount++;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 戦略別の成功率と平均時間を計算
|
|
259
|
+
for (const strategyName of Object.keys(byStrategy)) {
|
|
260
|
+
const stats = byStrategy[strategyName];
|
|
261
|
+
stats.successRate = stats.attempts > 0 ? stats.successCount / stats.attempts : 0;
|
|
262
|
+
|
|
263
|
+
const strategyEntries = recentEntries.filter(
|
|
264
|
+
(e) => e.strategy === strategyName
|
|
265
|
+
);
|
|
266
|
+
const strategyDuration = strategyEntries.reduce(
|
|
267
|
+
(sum, e) => sum + (e.durationMs || 0),
|
|
268
|
+
0
|
|
269
|
+
);
|
|
270
|
+
stats.avgDurationMs =
|
|
271
|
+
strategyEntries.length > 0
|
|
272
|
+
? strategyDuration / strategyEntries.length
|
|
273
|
+
: 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 高頻度失敗クエリを取得
|
|
277
|
+
const highFrequencyFailures = this.getHighFrequencyQueries(
|
|
278
|
+
this.config.warnThreshold
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
totalAttempts: recentEntries.length,
|
|
283
|
+
successCount,
|
|
284
|
+
failureCount,
|
|
285
|
+
successRate: recentEntries.length > 0 ? successCount / recentEntries.length : 0,
|
|
286
|
+
avgDurationMs:
|
|
287
|
+
recentEntries.length > 0 ? totalDurationMs / recentEntries.length : 0,
|
|
288
|
+
byStrategy,
|
|
289
|
+
highFrequencyFailures,
|
|
290
|
+
periodStart,
|
|
291
|
+
periodEnd: now,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 高頻度失敗クエリを取得
|
|
297
|
+
*/
|
|
298
|
+
getHighFrequencyQueries(threshold?: number): QueryFailureInfo[] {
|
|
299
|
+
const minFailures = threshold ?? this.config.warnThreshold;
|
|
300
|
+
return Array.from(this.queryFailureMap.values())
|
|
301
|
+
.filter((info) => info.failureCount >= minFailures)
|
|
302
|
+
.sort((a, b) => b.failureCount - a.failureCount);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* ログをJSON形式でエクスポート
|
|
307
|
+
*/
|
|
308
|
+
exportToJson(): string {
|
|
309
|
+
const stats = this.getStats();
|
|
310
|
+
return JSON.stringify(
|
|
311
|
+
{
|
|
312
|
+
period: `${stats.periodStart.toISOString()}/${stats.periodEnd.toISOString()}`,
|
|
313
|
+
stats: {
|
|
314
|
+
totalAttempts: stats.totalAttempts,
|
|
315
|
+
successCount: stats.successCount,
|
|
316
|
+
failureCount: stats.failureCount,
|
|
317
|
+
successRate: stats.successRate,
|
|
318
|
+
avgDurationMs: stats.avgDurationMs,
|
|
319
|
+
},
|
|
320
|
+
byStrategy: stats.byStrategy,
|
|
321
|
+
highFrequencyFailures: stats.highFrequencyFailures,
|
|
322
|
+
entries: this.entries.slice(-100), // 最新100件
|
|
323
|
+
},
|
|
324
|
+
null,
|
|
325
|
+
2
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* ログエントリを取得
|
|
331
|
+
*/
|
|
332
|
+
getEntries(): ExtendedLogEntry[] {
|
|
333
|
+
return [...this.entries];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* ログをクリア
|
|
338
|
+
*/
|
|
339
|
+
clear(): void {
|
|
340
|
+
this.entries.length = 0;
|
|
341
|
+
this.queryFailureMap.clear();
|
|
342
|
+
this.attemptCount = 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* 試行回数を取得
|
|
347
|
+
*/
|
|
348
|
+
getAttemptCount(): number {
|
|
349
|
+
return this.attemptCount;
|
|
350
|
+
}
|
|
351
|
+
}
|
package/src/tools/search.ts
CHANGED
|
@@ -413,3 +413,215 @@ export function resetSearchInfrastructure(): void {
|
|
|
413
413
|
healthChecker = null;
|
|
414
414
|
recoveryManager = null;
|
|
415
415
|
}
|
|
416
|
+
|
|
417
|
+
// ============================================================
|
|
418
|
+
// v1.8.0: 多言語並列検索 (REQ-SRCH-004)
|
|
419
|
+
// ============================================================
|
|
420
|
+
|
|
421
|
+
import type { MultilingualSearchConfig } from '../config/types.js';
|
|
422
|
+
import { DEFAULT_MULTILINGUAL_SEARCH_CONFIG } from '../config/types.js';
|
|
423
|
+
import { BUILTIN_DICTIONARY } from './search/recovery/strategies/translate.js';
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* v1.8.0追加の翻訳辞書(自動車・素材業界用語)
|
|
427
|
+
*/
|
|
428
|
+
const MULTILINGUAL_DICTIONARY_V180: Record<string, string> = {
|
|
429
|
+
// 自動車業界
|
|
430
|
+
'ネオジム磁石': 'neodymium magnet',
|
|
431
|
+
'永久磁石': 'permanent magnet',
|
|
432
|
+
'重希土類': 'heavy rare earth',
|
|
433
|
+
'ジスプロシウム': 'dysprosium',
|
|
434
|
+
'テルビウム': 'terbium',
|
|
435
|
+
'磁石レスモーター': 'magnet-free motor',
|
|
436
|
+
'巻線界磁モーター': 'wound field motor',
|
|
437
|
+
|
|
438
|
+
// 企業名
|
|
439
|
+
'プロテリアル': 'Proterial',
|
|
440
|
+
'大同特殊鋼': 'Daido Steel',
|
|
441
|
+
'信越化学': 'Shin-Etsu Chemical',
|
|
442
|
+
'TDK': 'TDK',
|
|
443
|
+
|
|
444
|
+
// ビジネス用語
|
|
445
|
+
'脱中国依存': 'reducing China dependency',
|
|
446
|
+
'代替材料': 'alternative materials',
|
|
447
|
+
'調達': 'procurement',
|
|
448
|
+
'戦略': 'strategy',
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 多言語検索結果の型定義
|
|
453
|
+
*/
|
|
454
|
+
export interface MultilingualSearchResult {
|
|
455
|
+
query: {
|
|
456
|
+
original: string;
|
|
457
|
+
translated: string | null;
|
|
458
|
+
detectedLanguage: 'ja' | 'en' | 'mixed';
|
|
459
|
+
};
|
|
460
|
+
results: Array<{
|
|
461
|
+
url: string;
|
|
462
|
+
title: string;
|
|
463
|
+
snippet: string;
|
|
464
|
+
sourceLanguage: 'ja' | 'en';
|
|
465
|
+
relevanceScore?: number;
|
|
466
|
+
}>;
|
|
467
|
+
metadata: {
|
|
468
|
+
totalResults: number;
|
|
469
|
+
japaneseResults: number;
|
|
470
|
+
englishResults: number;
|
|
471
|
+
duplicatesRemoved: number;
|
|
472
|
+
executionTimeMs: number;
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* 言語検出 (TSK-002)
|
|
478
|
+
* REQ-SRCH-004-01: 言語検出
|
|
479
|
+
*/
|
|
480
|
+
export function detectLanguage(query: string): 'ja' | 'en' | 'mixed' {
|
|
481
|
+
const japanesePattern = /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/;
|
|
482
|
+
const hasJapanese = japanesePattern.test(query);
|
|
483
|
+
const hasAscii = /[a-zA-Z]/.test(query);
|
|
484
|
+
|
|
485
|
+
if (hasJapanese && hasAscii) return 'mixed';
|
|
486
|
+
if (hasJapanese) return 'ja';
|
|
487
|
+
return 'en';
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* クエリ翻訳 (TSK-003)
|
|
492
|
+
* REQ-SRCH-004-02: クエリ翻訳
|
|
493
|
+
* v1.7.0のBUILTIN_DICTIONARYを拡張して使用
|
|
494
|
+
*/
|
|
495
|
+
export function translateQuery(
|
|
496
|
+
query: string,
|
|
497
|
+
customDictionary?: Record<string, string>
|
|
498
|
+
): string | null {
|
|
499
|
+
// v1.7.0辞書 + v1.8.0追加辞書 + カスタム辞書をマージ
|
|
500
|
+
const dictionary: Record<string, string> = {
|
|
501
|
+
...BUILTIN_DICTIONARY,
|
|
502
|
+
...MULTILINGUAL_DICTIONARY_V180,
|
|
503
|
+
...customDictionary,
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
let translated = query;
|
|
507
|
+
for (const [ja, en] of Object.entries(dictionary)) {
|
|
508
|
+
translated = translated.replace(new RegExp(ja, 'g'), en);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 変換があった場合のみ返却
|
|
512
|
+
return translated !== query ? translated : null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* URL正規化 (TSK-004)
|
|
517
|
+
* REQ-SRCH-004-04: 結果マージと重複排除
|
|
518
|
+
*/
|
|
519
|
+
export function normalizeUrl(url: string): string {
|
|
520
|
+
try {
|
|
521
|
+
const u = new URL(url);
|
|
522
|
+
// 末尾スラッシュ削除
|
|
523
|
+
u.pathname = u.pathname.replace(/\/$/, '');
|
|
524
|
+
// クエリパラメータソート
|
|
525
|
+
u.searchParams.sort();
|
|
526
|
+
// トラッキングパラメータ削除
|
|
527
|
+
['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'ref', 'fbclid', 'gclid'].forEach(p => {
|
|
528
|
+
u.searchParams.delete(p);
|
|
529
|
+
});
|
|
530
|
+
// フラグメント削除
|
|
531
|
+
u.hash = '';
|
|
532
|
+
return u.toString().toLowerCase();
|
|
533
|
+
} catch {
|
|
534
|
+
return url.toLowerCase();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* 結果マージ・重複排除 (TSK-004)
|
|
540
|
+
* REQ-SRCH-004-04: 結果マージと重複排除
|
|
541
|
+
*/
|
|
542
|
+
function mergeAndDeduplicate(
|
|
543
|
+
jaResults: SearchResult[],
|
|
544
|
+
enResults: SearchResult[],
|
|
545
|
+
priorityLang: 'ja' | 'en'
|
|
546
|
+
): Array<{ url: string; title: string; snippet: string; sourceLanguage: 'ja' | 'en' }> {
|
|
547
|
+
const seen = new Map<string, { url: string; title: string; snippet: string; sourceLanguage: 'ja' | 'en' }>();
|
|
548
|
+
|
|
549
|
+
// 優先言語の結果を先に処理
|
|
550
|
+
const first = priorityLang === 'ja' ? jaResults : enResults;
|
|
551
|
+
const firstLang = priorityLang;
|
|
552
|
+
const second = priorityLang === 'ja' ? enResults : jaResults;
|
|
553
|
+
const secondLang: 'ja' | 'en' = priorityLang === 'ja' ? 'en' : 'ja';
|
|
554
|
+
|
|
555
|
+
for (const r of first) {
|
|
556
|
+
const normalized = normalizeUrl(r.url);
|
|
557
|
+
if (!seen.has(normalized)) {
|
|
558
|
+
seen.set(normalized, { ...r, sourceLanguage: firstLang });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
for (const r of second) {
|
|
563
|
+
const normalized = normalizeUrl(r.url);
|
|
564
|
+
if (!seen.has(normalized)) {
|
|
565
|
+
seen.set(normalized, { ...r, sourceLanguage: secondLang });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return Array.from(seen.values());
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* 多言語並列検索 (TSK-005)
|
|
574
|
+
* REQ-SRCH-004-03: 並列検索実行
|
|
575
|
+
*
|
|
576
|
+
* 設定で`multilingualSearch.enabled: true`の場合に使用
|
|
577
|
+
*/
|
|
578
|
+
export async function searchMultilingual(
|
|
579
|
+
query: string,
|
|
580
|
+
config?: Partial<MultilingualSearchConfig>
|
|
581
|
+
): Promise<MultilingualSearchResult> {
|
|
582
|
+
const startTime = Date.now();
|
|
583
|
+
|
|
584
|
+
// 設定をマージ
|
|
585
|
+
const mergedConfig: Required<MultilingualSearchConfig> = {
|
|
586
|
+
...DEFAULT_MULTILINGUAL_SEARCH_CONFIG,
|
|
587
|
+
...config,
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const detectedLang = detectLanguage(query);
|
|
591
|
+
const translatedQuery = detectedLang === 'ja' || detectedLang === 'mixed'
|
|
592
|
+
? translateQuery(query, mergedConfig.customDictionary)
|
|
593
|
+
: null;
|
|
594
|
+
|
|
595
|
+
// 並列検索実行
|
|
596
|
+
const [jaResultsSettled, enResultsSettled] = await Promise.allSettled([
|
|
597
|
+
searchDuckDuckGo(query, mergedConfig.maxResults),
|
|
598
|
+
translatedQuery
|
|
599
|
+
? searchDuckDuckGo(translatedQuery, mergedConfig.maxResults)
|
|
600
|
+
: Promise.resolve([]),
|
|
601
|
+
]);
|
|
602
|
+
|
|
603
|
+
const jaResults = jaResultsSettled.status === 'fulfilled' ? jaResultsSettled.value : [];
|
|
604
|
+
const enResults = enResultsSettled.status === 'fulfilled' ? enResultsSettled.value : [];
|
|
605
|
+
|
|
606
|
+
// 結果マージ・重複排除
|
|
607
|
+
const merged = mergeAndDeduplicate(jaResults, enResults, mergedConfig.priorityLanguage);
|
|
608
|
+
|
|
609
|
+
const executionTimeMs = Date.now() - startTime;
|
|
610
|
+
const duplicatesRemoved = (jaResults.length + enResults.length) - merged.length;
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
query: {
|
|
614
|
+
original: query,
|
|
615
|
+
translated: translatedQuery,
|
|
616
|
+
detectedLanguage: detectedLang,
|
|
617
|
+
},
|
|
618
|
+
results: merged,
|
|
619
|
+
metadata: {
|
|
620
|
+
totalResults: merged.length,
|
|
621
|
+
japaneseResults: jaResults.length,
|
|
622
|
+
englishResults: enResults.length,
|
|
623
|
+
duplicatesRemoved,
|
|
624
|
+
executionTimeMs,
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
}
|