ms365-mcp-server 1.1.16 → 1.1.18
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/dist/index.js +1122 -9
- package/dist/utils/batch-performance-monitor.js +106 -0
- package/dist/utils/batch-test-scenarios.js +277 -0
- package/dist/utils/context-aware-search.js +499 -0
- package/dist/utils/cross-reference-detector.js +352 -0
- package/dist/utils/document-workflow.js +433 -0
- package/dist/utils/enhanced-fuzzy-search.js +514 -0
- package/dist/utils/error-handler.js +337 -0
- package/dist/utils/intelligence-engine.js +71 -0
- package/dist/utils/intelligent-cache.js +379 -0
- package/dist/utils/large-mailbox-search.js +599 -0
- package/dist/utils/ms365-operations.js +730 -208
- package/dist/utils/performance-monitor.js +395 -0
- package/dist/utils/proactive-intelligence.js +390 -0
- package/dist/utils/rate-limiter.js +284 -0
- package/dist/utils/search-batch-pipeline.js +222 -0
- package/dist/utils/thread-reconstruction.js +700 -0
- package/package.json +1 -1
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import { logger } from './api.js';
|
|
2
|
+
export class LargeMailboxSearch {
|
|
3
|
+
constructor(ms365Operations, contextAwareSearch, intelligenceEngine, proactiveIntelligence) {
|
|
4
|
+
this.ms365Operations = ms365Operations;
|
|
5
|
+
this.contextAwareSearch = contextAwareSearch;
|
|
6
|
+
this.intelligenceEngine = intelligenceEngine;
|
|
7
|
+
this.proactiveIntelligence = proactiveIntelligence;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Perform scalable search for large mailboxes (20k+ emails)
|
|
11
|
+
*/
|
|
12
|
+
async search(query, options = {}) {
|
|
13
|
+
const startTime = Date.now();
|
|
14
|
+
const opts = { ...LargeMailboxSearch.DEFAULT_OPTIONS, ...options };
|
|
15
|
+
logger.log(`🔍 Large mailbox search initiated for: "${query}"`);
|
|
16
|
+
logger.log(`📊 Search options: ${JSON.stringify(opts)}`);
|
|
17
|
+
// Dynamic timeout based on mailbox size estimation
|
|
18
|
+
const estimatedSize = await this.quickMailboxSizeEstimate();
|
|
19
|
+
const baseTimeout = 60000; // 60 seconds base
|
|
20
|
+
const searchTimeout = estimatedSize > 50000 ?
|
|
21
|
+
180000 : // 3 minutes for very large mailboxes (50k+)
|
|
22
|
+
estimatedSize > 20000 ?
|
|
23
|
+
120000 : // 2 minutes for large mailboxes (20k+)
|
|
24
|
+
baseTimeout; // 1 minute for smaller mailboxes
|
|
25
|
+
logger.log(`⏱️ Dynamic timeout set to ${searchTimeout / 1000} seconds based on estimated mailbox size: ${estimatedSize}`);
|
|
26
|
+
// Progressive timeout with partial results
|
|
27
|
+
const searchPromise = this.performProgressiveSearch(query, opts, searchTimeout);
|
|
28
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
reject(new Error(`Large mailbox search timed out after ${searchTimeout / 1000} seconds. Try using more specific search terms, reducing the time window (timeWindowDays), or using smaller maxTotalResults.`));
|
|
31
|
+
}, searchTimeout);
|
|
32
|
+
});
|
|
33
|
+
return Promise.race([searchPromise, timeoutPromise]);
|
|
34
|
+
}
|
|
35
|
+
async performActualSearch(query, opts) {
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
const result = {
|
|
38
|
+
query,
|
|
39
|
+
totalEmailsInMailbox: 0,
|
|
40
|
+
searchStrategy: '',
|
|
41
|
+
tierResults: [],
|
|
42
|
+
finalResults: [],
|
|
43
|
+
confidence: 0,
|
|
44
|
+
searchTime: 0,
|
|
45
|
+
recommendations: []
|
|
46
|
+
};
|
|
47
|
+
// Get total mailbox size estimate
|
|
48
|
+
result.totalEmailsInMailbox = await this.estimateMailboxSize();
|
|
49
|
+
if (result.totalEmailsInMailbox < 1000) {
|
|
50
|
+
// Small mailbox - use direct approach
|
|
51
|
+
return await this.performDirectSearch(query, opts, result);
|
|
52
|
+
}
|
|
53
|
+
else if (result.totalEmailsInMailbox < 10000) {
|
|
54
|
+
// Medium mailbox - use filtered approach
|
|
55
|
+
return await this.performFilteredSearch(query, opts, result);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Large mailbox (20k+) - use multi-tier approach
|
|
59
|
+
return await this.performTieredSearch(query, opts, result);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Multi-tier search strategy for 20k+ emails
|
|
64
|
+
*/
|
|
65
|
+
async performTieredSearch(query, options, result) {
|
|
66
|
+
result.searchStrategy = 'multi-tier-large-mailbox';
|
|
67
|
+
// Define search tiers in order of priority
|
|
68
|
+
const tiers = this.buildSearchTiers(query, options);
|
|
69
|
+
let allResults = [];
|
|
70
|
+
let totalSearched = 0;
|
|
71
|
+
for (const tier of tiers) {
|
|
72
|
+
if (allResults.length >= options.maxTotalResults) {
|
|
73
|
+
logger.log(`🎯 Reached target results (${options.maxTotalResults}), stopping search`);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
const tierStartTime = Date.now();
|
|
77
|
+
logger.log(`🔍 Executing tier: ${tier.name}`);
|
|
78
|
+
try {
|
|
79
|
+
// Use native MS365 search for initial filtering
|
|
80
|
+
const nativeResults = await this.ms365Operations.searchEmails({
|
|
81
|
+
...tier.searchCriteria,
|
|
82
|
+
maxResults: Math.min(options.batchSize, 500)
|
|
83
|
+
});
|
|
84
|
+
const emailsSearched = nativeResults.messages.length;
|
|
85
|
+
totalSearched += emailsSearched;
|
|
86
|
+
if (emailsSearched > 0) {
|
|
87
|
+
// Apply AI intelligence to filtered results
|
|
88
|
+
let intelligentResults = [];
|
|
89
|
+
if (tier.name.includes('context')) {
|
|
90
|
+
// Use context-aware search
|
|
91
|
+
const contextResults = await this.contextAwareSearch.search(query, nativeResults.messages);
|
|
92
|
+
intelligentResults = contextResults.emails;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Use fuzzy/intelligent search
|
|
96
|
+
const fuzzyResults = await this.intelligenceEngine.intelligentSearch(query, nativeResults.messages, { maxResults: 50 });
|
|
97
|
+
intelligentResults = fuzzyResults.results;
|
|
98
|
+
}
|
|
99
|
+
// Add to results (avoid duplicates)
|
|
100
|
+
const newResults = intelligentResults.filter(email => !allResults.some(existing => existing.id === email.id));
|
|
101
|
+
allResults.push(...newResults);
|
|
102
|
+
const tierTime = Date.now() - tierStartTime;
|
|
103
|
+
result.tierResults.push({
|
|
104
|
+
tier: tier.name,
|
|
105
|
+
emailsSearched,
|
|
106
|
+
resultsFound: newResults.length,
|
|
107
|
+
processingTime: tierTime
|
|
108
|
+
});
|
|
109
|
+
logger.log(`✅ Tier "${tier.name}": ${emailsSearched} searched, ${newResults.length} found (${tierTime}ms)`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
logger.log(`⚠️ Tier "${tier.name}": No emails found matching criteria`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
logger.error(`❌ Error in tier "${tier.name}":`, error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Limit final results and calculate confidence
|
|
120
|
+
result.finalResults = allResults.slice(0, options.maxTotalResults);
|
|
121
|
+
result.confidence = this.calculateSearchConfidence(result, totalSearched);
|
|
122
|
+
result.searchTime = Date.now() - Date.now();
|
|
123
|
+
result.recommendations = this.generateLargeMailboxRecommendations(result, options);
|
|
124
|
+
logger.log(`🎯 Large mailbox search completed: ${result.finalResults.length} results from ${totalSearched} emails searched`);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Build prioritized search tiers based on query analysis
|
|
129
|
+
*/
|
|
130
|
+
buildSearchTiers(query, options) {
|
|
131
|
+
const tiers = [];
|
|
132
|
+
const currentDate = new Date();
|
|
133
|
+
// Tier 1: Recent emails with query terms (highest priority)
|
|
134
|
+
const recentDays = Math.min(options.timeWindowDays / 4, 30); // Last 30 days max
|
|
135
|
+
const recentDate = new Date(currentDate.getTime() - recentDays * 24 * 60 * 60 * 1000);
|
|
136
|
+
tiers.push({
|
|
137
|
+
name: 'recent-context-search',
|
|
138
|
+
searchCriteria: {
|
|
139
|
+
query: query,
|
|
140
|
+
after: recentDate.toISOString().split('T')[0],
|
|
141
|
+
folder: 'inbox'
|
|
142
|
+
},
|
|
143
|
+
priority: 1,
|
|
144
|
+
estimatedResults: 100
|
|
145
|
+
});
|
|
146
|
+
// Tier 2: Subject line matches (medium-high priority)
|
|
147
|
+
tiers.push({
|
|
148
|
+
name: 'subject-fuzzy-search',
|
|
149
|
+
searchCriteria: {
|
|
150
|
+
subject: query,
|
|
151
|
+
maxResults: 300
|
|
152
|
+
},
|
|
153
|
+
priority: 2,
|
|
154
|
+
estimatedResults: 150
|
|
155
|
+
});
|
|
156
|
+
// Tier 3: Sender-based search if query contains person/email
|
|
157
|
+
if (this.queryContainsPerson(query)) {
|
|
158
|
+
const personName = this.extractPersonFromQuery(query);
|
|
159
|
+
tiers.push({
|
|
160
|
+
name: 'sender-intelligent-search',
|
|
161
|
+
searchCriteria: {
|
|
162
|
+
from: personName,
|
|
163
|
+
maxResults: 200
|
|
164
|
+
},
|
|
165
|
+
priority: 3,
|
|
166
|
+
estimatedResults: 100
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// Tier 4: Extended time range for important keywords
|
|
170
|
+
if (this.queryContainsImportantKeywords(query)) {
|
|
171
|
+
const extendedDate = new Date(currentDate.getTime() - options.timeWindowDays * 24 * 60 * 60 * 1000);
|
|
172
|
+
tiers.push({
|
|
173
|
+
name: 'extended-context-search',
|
|
174
|
+
searchCriteria: {
|
|
175
|
+
query: query,
|
|
176
|
+
after: extendedDate.toISOString().split('T')[0],
|
|
177
|
+
maxResults: 400
|
|
178
|
+
},
|
|
179
|
+
priority: 4,
|
|
180
|
+
estimatedResults: 200
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// Tier 5: Folder-specific search for document-related queries
|
|
184
|
+
if (this.queryContainsDocumentKeywords(query)) {
|
|
185
|
+
tiers.push({
|
|
186
|
+
name: 'attachment-search',
|
|
187
|
+
searchCriteria: {
|
|
188
|
+
query: query,
|
|
189
|
+
hasAttachment: true,
|
|
190
|
+
maxResults: 200
|
|
191
|
+
},
|
|
192
|
+
priority: 5,
|
|
193
|
+
estimatedResults: 80
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// Sort by priority and return
|
|
197
|
+
return tiers.sort((a, b) => a.priority - b.priority);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Estimate total mailbox size
|
|
201
|
+
*/
|
|
202
|
+
async estimateMailboxSize() {
|
|
203
|
+
try {
|
|
204
|
+
// Get sample from different folders to estimate total
|
|
205
|
+
const folders = ['inbox', 'sent', 'archive'];
|
|
206
|
+
let totalEstimate = 0;
|
|
207
|
+
for (const folder of folders) {
|
|
208
|
+
try {
|
|
209
|
+
const sample = await this.ms365Operations.searchEmails({
|
|
210
|
+
query: '*',
|
|
211
|
+
folder: folder,
|
|
212
|
+
maxResults: 50
|
|
213
|
+
});
|
|
214
|
+
// Rough estimation: if we get 50, assume there are many more
|
|
215
|
+
if (sample.messages.length === 50) {
|
|
216
|
+
totalEstimate += 1000; // Conservative estimate per active folder
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
totalEstimate += sample.messages.length;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
// Folder might not exist, continue
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return Math.max(totalEstimate, 1000);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
logger.error('Error estimating mailbox size:', error);
|
|
230
|
+
return 5000; // Default assumption for large mailbox
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Direct search for small mailboxes
|
|
235
|
+
*/
|
|
236
|
+
async performDirectSearch(query, options, result) {
|
|
237
|
+
result.searchStrategy = 'direct-small-mailbox';
|
|
238
|
+
const emails = await this.ms365Operations.searchEmails({
|
|
239
|
+
query: '*',
|
|
240
|
+
maxResults: 1000
|
|
241
|
+
});
|
|
242
|
+
const contextResults = await this.contextAwareSearch.search(query, emails.messages);
|
|
243
|
+
result.finalResults = contextResults.emails.slice(0, options.maxTotalResults);
|
|
244
|
+
result.confidence = contextResults.confidence;
|
|
245
|
+
result.tierResults = [{
|
|
246
|
+
tier: 'direct-search',
|
|
247
|
+
emailsSearched: emails.messages.length,
|
|
248
|
+
resultsFound: result.finalResults.length,
|
|
249
|
+
processingTime: 0
|
|
250
|
+
}];
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Filtered search for medium mailboxes
|
|
255
|
+
*/
|
|
256
|
+
async performFilteredSearch(query, options, result) {
|
|
257
|
+
result.searchStrategy = 'filtered-medium-mailbox';
|
|
258
|
+
// Use native search to filter first
|
|
259
|
+
const filtered = await this.ms365Operations.searchEmails({
|
|
260
|
+
query: query,
|
|
261
|
+
maxResults: 500
|
|
262
|
+
});
|
|
263
|
+
const intelligentResults = await this.intelligenceEngine.intelligentSearch(query, filtered.messages, { maxResults: options.maxTotalResults });
|
|
264
|
+
result.finalResults = intelligentResults.results;
|
|
265
|
+
result.confidence = intelligentResults.insights.averageConfidence;
|
|
266
|
+
result.tierResults = [{
|
|
267
|
+
tier: 'filtered-search',
|
|
268
|
+
emailsSearched: filtered.messages.length,
|
|
269
|
+
resultsFound: result.finalResults.length,
|
|
270
|
+
processingTime: intelligentResults.insights.processingTime
|
|
271
|
+
}];
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
// Helper methods
|
|
275
|
+
queryContainsPerson(query) {
|
|
276
|
+
return /\b(from|by|sent by)\s+\w+/.test(query.toLowerCase()) ||
|
|
277
|
+
/\b[A-Z][a-z]+\s+[A-Z][a-z]+\b/.test(query);
|
|
278
|
+
}
|
|
279
|
+
extractPersonFromQuery(query) {
|
|
280
|
+
const match = query.match(/(?:from|by|sent by)\s+([a-zA-Z\s]+)/i);
|
|
281
|
+
return match ? match[1].trim() : '';
|
|
282
|
+
}
|
|
283
|
+
queryContainsImportantKeywords(query) {
|
|
284
|
+
const important = ['tax', 'legal', 'contract', 'government', 'urgent', 'important'];
|
|
285
|
+
return important.some(keyword => query.toLowerCase().includes(keyword));
|
|
286
|
+
}
|
|
287
|
+
queryContainsDocumentKeywords(query) {
|
|
288
|
+
const docKeywords = ['document', 'file', 'pdf', 'attachment', 'report'];
|
|
289
|
+
return docKeywords.some(keyword => query.toLowerCase().includes(keyword));
|
|
290
|
+
}
|
|
291
|
+
calculateSearchConfidence(result, totalSearched) {
|
|
292
|
+
const searchCoverage = Math.min(totalSearched / result.totalEmailsInMailbox, 1.0);
|
|
293
|
+
const resultQuality = result.finalResults.length > 0 ? 0.8 : 0.3;
|
|
294
|
+
return (searchCoverage * 0.4) + (resultQuality * 0.6);
|
|
295
|
+
}
|
|
296
|
+
generateLargeMailboxRecommendations(result, options) {
|
|
297
|
+
const recommendations = [];
|
|
298
|
+
if (result.finalResults.length === 0) {
|
|
299
|
+
recommendations.push("Try broader search terms or extend time range");
|
|
300
|
+
recommendations.push("Check if emails might be in Archive or Sent folders");
|
|
301
|
+
}
|
|
302
|
+
if (result.totalEmailsInMailbox > 20000) {
|
|
303
|
+
recommendations.push("Consider organizing emails into folders for faster searches");
|
|
304
|
+
recommendations.push("Use specific date ranges when possible (e.g., 'last month', '2023')");
|
|
305
|
+
}
|
|
306
|
+
const totalSearched = result.tierResults.reduce((sum, tier) => sum + tier.emailsSearched, 0);
|
|
307
|
+
const searchPercentage = (totalSearched / result.totalEmailsInMailbox) * 100;
|
|
308
|
+
if (searchPercentage < 10) {
|
|
309
|
+
recommendations.push(`Only searched ${searchPercentage.toFixed(1)}% of emails. Use more specific terms for better coverage.`);
|
|
310
|
+
}
|
|
311
|
+
return recommendations;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Quick mailbox size estimation for dynamic timeout calculation
|
|
315
|
+
*/
|
|
316
|
+
async quickMailboxSizeEstimate() {
|
|
317
|
+
try {
|
|
318
|
+
// Try to get a quick count from recent emails to estimate total size
|
|
319
|
+
const graphClient = await this.ms365Operations.getGraphClient();
|
|
320
|
+
// Sample recent emails to estimate total mailbox size
|
|
321
|
+
const recentSample = await graphClient
|
|
322
|
+
.api('/me/messages')
|
|
323
|
+
.select('id')
|
|
324
|
+
.top(100)
|
|
325
|
+
.get();
|
|
326
|
+
if (recentSample.value && recentSample.value.length > 0) {
|
|
327
|
+
// If we get a full page of 100, estimate larger mailbox
|
|
328
|
+
if (recentSample.value.length === 100) {
|
|
329
|
+
// Try a folder count approach for better estimation
|
|
330
|
+
try {
|
|
331
|
+
const folders = await graphClient.api('/me/mailFolders').get();
|
|
332
|
+
const folderCount = folders.value ? folders.value.length : 1;
|
|
333
|
+
// Rough estimation: assume 1000+ emails per active folder
|
|
334
|
+
const estimatedSize = folderCount * 1000;
|
|
335
|
+
logger.log(`📊 Quick mailbox size estimate: ${estimatedSize} (based on ${folderCount} folders)`);
|
|
336
|
+
return Math.min(estimatedSize, 100000); // Cap at 100k
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// Fallback to conservative estimate
|
|
340
|
+
return 10000;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
// Small mailbox
|
|
345
|
+
return recentSample.value.length * 10; // Multiply by 10 for rough total
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Default fallback
|
|
349
|
+
return 5000;
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
logger.error('❌ Error in quick mailbox size estimation:', error);
|
|
353
|
+
return 10000; // Conservative default
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Progressive search with partial results and early termination
|
|
358
|
+
*/
|
|
359
|
+
async performProgressiveSearch(query, opts, totalTimeout) {
|
|
360
|
+
const startTime = Date.now();
|
|
361
|
+
const result = {
|
|
362
|
+
query,
|
|
363
|
+
totalEmailsInMailbox: 0,
|
|
364
|
+
searchStrategy: 'progressive-search',
|
|
365
|
+
tierResults: [],
|
|
366
|
+
finalResults: [],
|
|
367
|
+
confidence: 0,
|
|
368
|
+
searchTime: 0,
|
|
369
|
+
recommendations: []
|
|
370
|
+
};
|
|
371
|
+
// Quick mailbox size for strategy selection
|
|
372
|
+
result.totalEmailsInMailbox = await this.quickMailboxSizeEstimate();
|
|
373
|
+
try {
|
|
374
|
+
if (result.totalEmailsInMailbox < 1000) {
|
|
375
|
+
// Small mailbox - use direct approach
|
|
376
|
+
return await this.performDirectSearch(query, opts, result);
|
|
377
|
+
}
|
|
378
|
+
else if (result.totalEmailsInMailbox < 10000) {
|
|
379
|
+
// Medium mailbox - use filtered approach with timeout check
|
|
380
|
+
return await this.performFilteredSearchWithTimeout(query, opts, result, totalTimeout, startTime);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
// Large mailbox - use progressive tiered approach
|
|
384
|
+
return await this.performProgressiveTieredSearch(query, opts, result, totalTimeout, startTime);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
// If search fails, return partial results if any
|
|
389
|
+
if (result.finalResults.length > 0) {
|
|
390
|
+
result.searchTime = Date.now() - startTime;
|
|
391
|
+
result.confidence = 0.3; // Low confidence for partial results
|
|
392
|
+
result.recommendations.push(`Search interrupted but found ${result.finalResults.length} partial results. Consider using more specific search terms.`);
|
|
393
|
+
logger.log(`⚠️ Returning ${result.finalResults.length} partial results due to error: ${error}`);
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Progressive tiered search with timeout monitoring
|
|
403
|
+
*/
|
|
404
|
+
async performProgressiveTieredSearch(query, options, result, totalTimeout, startTime) {
|
|
405
|
+
result.searchStrategy = 'progressive-tiered-large-mailbox';
|
|
406
|
+
// Build optimized search tiers for large mailboxes
|
|
407
|
+
const tiers = this.buildOptimizedSearchTiers(query, options);
|
|
408
|
+
let allResults = [];
|
|
409
|
+
let totalSearched = 0;
|
|
410
|
+
const timePerTier = Math.floor(totalTimeout / tiers.length * 0.8); // Reserve 20% for processing
|
|
411
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
412
|
+
const tier = tiers[i];
|
|
413
|
+
const tierStartTime = Date.now();
|
|
414
|
+
// Check overall timeout
|
|
415
|
+
if (Date.now() - startTime > totalTimeout * 0.9) {
|
|
416
|
+
logger.log(`⏱️ Approaching timeout, stopping at tier ${i + 1}/${tiers.length}`);
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
// Check if we have enough results
|
|
420
|
+
if (allResults.length >= options.maxTotalResults) {
|
|
421
|
+
logger.log(`🎯 Reached target results (${options.maxTotalResults}), stopping search`);
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
logger.log(`🔍 Executing tier ${i + 1}/${tiers.length}: ${tier.name} (${timePerTier / 1000}s limit)`);
|
|
425
|
+
try {
|
|
426
|
+
// Set timeout for this tier
|
|
427
|
+
const tierPromise = this.executeTierSearch(tier, query, options);
|
|
428
|
+
const tierTimeoutPromise = new Promise((_, reject) => {
|
|
429
|
+
setTimeout(() => reject(new Error('Tier timeout')), timePerTier);
|
|
430
|
+
});
|
|
431
|
+
const tierResults = await Promise.race([tierPromise, tierTimeoutPromise]);
|
|
432
|
+
if (tierResults && tierResults.length > 0) {
|
|
433
|
+
// Add to results (avoid duplicates)
|
|
434
|
+
const newResults = tierResults.filter((email) => !allResults.some(existing => existing.id === email.id));
|
|
435
|
+
allResults.push(...newResults);
|
|
436
|
+
totalSearched += tierResults.length;
|
|
437
|
+
const tierTime = Date.now() - tierStartTime;
|
|
438
|
+
result.tierResults.push({
|
|
439
|
+
tier: tier.name,
|
|
440
|
+
emailsSearched: tierResults.length,
|
|
441
|
+
resultsFound: newResults.length,
|
|
442
|
+
processingTime: tierTime
|
|
443
|
+
});
|
|
444
|
+
logger.log(`✅ Tier "${tier.name}": ${tierResults.length} searched, ${newResults.length} new results (${tierTime}ms)`);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
logger.log(`⚠️ Tier "${tier.name}": No results found`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
logger.log(`⚠️ Tier "${tier.name}" timed out or failed: ${error.message}`);
|
|
452
|
+
// Continue with next tier
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Finalize results
|
|
456
|
+
result.finalResults = allResults.slice(0, options.maxTotalResults);
|
|
457
|
+
result.confidence = this.calculateSearchConfidence(result, totalSearched);
|
|
458
|
+
result.searchTime = Date.now() - startTime;
|
|
459
|
+
result.recommendations = this.generateOptimizedRecommendations(result, options);
|
|
460
|
+
logger.log(`🎯 Progressive search completed: ${result.finalResults.length} results from ${totalSearched} emails searched in ${result.searchTime}ms`);
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Execute individual tier search with optimizations
|
|
465
|
+
*/
|
|
466
|
+
async executeTierSearch(tier, query, options) {
|
|
467
|
+
// Use faster native search for initial filtering
|
|
468
|
+
const nativeResults = await this.ms365Operations.searchEmails({
|
|
469
|
+
...tier.searchCriteria,
|
|
470
|
+
maxResults: Math.min(options.batchSize, 300) // Reduced batch size for faster execution
|
|
471
|
+
});
|
|
472
|
+
const emailsFound = nativeResults.messages || [];
|
|
473
|
+
if (emailsFound.length === 0) {
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
// Apply lighter AI processing for speed
|
|
477
|
+
if (tier.name.includes('recent') || tier.name.includes('urgent')) {
|
|
478
|
+
// Skip heavy AI processing for time-sensitive tiers
|
|
479
|
+
return emailsFound.slice(0, 30); // Return top 30 for speed
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
// Use intelligent search for better relevance
|
|
483
|
+
const fuzzyResults = await this.intelligenceEngine.intelligentSearch(query, emailsFound, { maxResults: 25 } // Reduced for speed
|
|
484
|
+
);
|
|
485
|
+
return fuzzyResults.results || [];
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Filtered search with timeout monitoring for medium mailboxes
|
|
490
|
+
*/
|
|
491
|
+
async performFilteredSearchWithTimeout(query, options, result, totalTimeout, startTime) {
|
|
492
|
+
result.searchStrategy = 'filtered-search-with-timeout';
|
|
493
|
+
// Use existing filtered search but with timeout monitoring
|
|
494
|
+
try {
|
|
495
|
+
const filteredResult = await this.performFilteredSearch(query, options, result);
|
|
496
|
+
return filteredResult;
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
// If timeout, return partial results if available
|
|
500
|
+
if (result.finalResults.length > 0) {
|
|
501
|
+
result.searchTime = Date.now() - startTime;
|
|
502
|
+
result.confidence = 0.5;
|
|
503
|
+
result.recommendations.push('Search timed out but returned partial results. Try using more specific search terms.');
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Build optimized search tiers for faster large mailbox searches
|
|
511
|
+
*/
|
|
512
|
+
buildOptimizedSearchTiers(query, options) {
|
|
513
|
+
const tiers = [];
|
|
514
|
+
const currentDate = new Date();
|
|
515
|
+
// Tier 1: Very recent emails (last 7 days) - highest priority, fastest
|
|
516
|
+
const veryRecentDate = new Date(currentDate.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
517
|
+
tiers.push({
|
|
518
|
+
name: 'very-recent-urgent-search',
|
|
519
|
+
searchCriteria: {
|
|
520
|
+
query: query,
|
|
521
|
+
after: veryRecentDate.toISOString().split('T')[0],
|
|
522
|
+
folder: 'inbox'
|
|
523
|
+
},
|
|
524
|
+
priority: 1,
|
|
525
|
+
estimatedResults: 50
|
|
526
|
+
});
|
|
527
|
+
// Tier 2: Recent emails (last 30 days) with subject focus
|
|
528
|
+
const recentDate = new Date(currentDate.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
529
|
+
tiers.push({
|
|
530
|
+
name: 'recent-subject-search',
|
|
531
|
+
searchCriteria: {
|
|
532
|
+
subject: query,
|
|
533
|
+
after: recentDate.toISOString().split('T')[0],
|
|
534
|
+
maxResults: 200
|
|
535
|
+
},
|
|
536
|
+
priority: 2,
|
|
537
|
+
estimatedResults: 100
|
|
538
|
+
});
|
|
539
|
+
// Tier 3: Sender-based search if query contains person/email (faster than content search)
|
|
540
|
+
if (this.queryContainsPerson(query)) {
|
|
541
|
+
const personName = this.extractPersonFromQuery(query);
|
|
542
|
+
tiers.push({
|
|
543
|
+
name: 'sender-optimized-search',
|
|
544
|
+
searchCriteria: {
|
|
545
|
+
from: personName,
|
|
546
|
+
after: recentDate.toISOString().split('T')[0],
|
|
547
|
+
maxResults: 150
|
|
548
|
+
},
|
|
549
|
+
priority: 3,
|
|
550
|
+
estimatedResults: 75
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
// Tier 4: Extended time range but limited scope (only if still needed)
|
|
554
|
+
const extendedDate = new Date(currentDate.getTime() - Math.min(options.timeWindowDays, 90) * 24 * 60 * 60 * 1000);
|
|
555
|
+
tiers.push({
|
|
556
|
+
name: 'extended-limited-search',
|
|
557
|
+
searchCriteria: {
|
|
558
|
+
query: query,
|
|
559
|
+
after: extendedDate.toISOString().split('T')[0],
|
|
560
|
+
maxResults: 250
|
|
561
|
+
},
|
|
562
|
+
priority: 4,
|
|
563
|
+
estimatedResults: 125
|
|
564
|
+
});
|
|
565
|
+
return tiers.slice(0, 4); // Limit to 4 tiers for speed
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Generate optimized recommendations for performance
|
|
569
|
+
*/
|
|
570
|
+
generateOptimizedRecommendations(result, options) {
|
|
571
|
+
const recommendations = [];
|
|
572
|
+
if (result.finalResults.length === 0) {
|
|
573
|
+
recommendations.push('No results found. Try using broader search terms or increasing timeWindowDays.');
|
|
574
|
+
recommendations.push('Consider searching for sender names, subject keywords, or specific date ranges.');
|
|
575
|
+
}
|
|
576
|
+
else if (result.finalResults.length < 10) {
|
|
577
|
+
recommendations.push('Few results found. Consider expanding search terms or increasing timeWindowDays.');
|
|
578
|
+
}
|
|
579
|
+
if (result.searchTime > 30000) { // > 30 seconds
|
|
580
|
+
recommendations.push('Search took longer than expected. Try using more specific terms, reduce timeWindowDays, or lower maxTotalResults.');
|
|
581
|
+
}
|
|
582
|
+
if (result.confidence < 0.5) {
|
|
583
|
+
recommendations.push('Low confidence results. Use more specific search terms for better accuracy.');
|
|
584
|
+
}
|
|
585
|
+
if (result.totalEmailsInMailbox > 50000) {
|
|
586
|
+
recommendations.push('Very large mailbox detected. For best performance, use specific date ranges (timeWindowDays: 30) and targeted search terms.');
|
|
587
|
+
}
|
|
588
|
+
return recommendations;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
LargeMailboxSearch.DEFAULT_OPTIONS = {
|
|
592
|
+
maxTotalResults: 50,
|
|
593
|
+
enableTieredSearch: true,
|
|
594
|
+
enableCaching: false,
|
|
595
|
+
prioritizeRecent: true,
|
|
596
|
+
timeWindowDays: 90,
|
|
597
|
+
batchSize: 150,
|
|
598
|
+
maxBatches: 8
|
|
599
|
+
};
|