Search Resilience & Fallbacks
The search system is built to handle edge cases gracefully through multiple layers of fallback logic. When searches fail or return unexpected results, the system automatically adjusts its strategy to provide useful recommendations rather than empty results.
Overview
Resilience is implemented at three levels:
Level 1: Query Validation
Nonsense Detection
Before any expensive search operations, the handler validates query quality.
Implementation: app/api/chat/handlers/product-search-handler.ts:202-220
Validation Rules
// Quick regex checks
const isTooShort = query.length < 3;
const isKeyboardMash = /^[a-z]{10,}$/i.test(query); // "asdfghjkl"
const isJustPunctuation = /^[\s\W]+$/.test(query); // "!!!!!!"
if (isTooShort || isKeyboardMash || isJustPunctuation) {
return {
route: 'CLARIFICATION_PATH',
reason: 'Invalid query format'
};
}
// Optional: LLM validation for ambiguous cases
if (config.enableLLMValidation) {
const isValid = await validateQueryWithLLM(query);
if (!isValid) {
return { route: 'CLARIFICATION_PATH' };
}
}
Level 2: Search Orchestrator Resilience
Book-Only Fallback
Problem: User explicitly excludes books, but Convex only returns books.
Root Cause: Category hints may be book-biased (e.g., "reading enthusiast" → "Raamatud").
Implementation: app/api/chat/orchestrators/search-orchestrator.ts:450-493
// Check if all results are books
const onlyBooks = searchResult.products.every(product =>
product.product_type === 'book' ||
product.csv_category?.includes('Raamatud')
);
// User explicitly excluded books?
const excludeBooks = context.constraints?.includes('EXCLUDE_BOOKS');
if (onlyBooks && excludeBooks) {
logger.warn('Book-only result despite EXCLUDE_BOOKS', {
productCount: searchResult.products.length
});
// Mutate context to broader gift categories
const fallbackContext = {
...context,
categoryHints: CANONICAL_GIFT_CATEGORIES,
productType: 'gift',
meta: {
...context.meta,
fallbackApplied: 'gift_categories'
}
};
// Regenerate variations with gift focus
const fallbackVariations = await QueryRewritingService
.generateVariations(fallbackContext);
// Retry search
const retryResult = await ProductSearchService.searchMulti(
fallbackVariations,
{ seed: 'book-fallback' }
);
// Filter books from retry
searchResult.products = retryResult.products.filter(p =>
!isBook(p)
);
// If still only books, discard them
if (searchResult.products.every(p => isBook(p))) {
searchResult.products = [];
searchResult.warnings.push('Unable to find non-book products');
}
}
Language Fallback
Problem: User requests books in Estonian, but catalog only has English versions.
Solution: Retry without language filter, then manually filter results.
Implementation: app/api/chat/orchestrators/search-orchestrator.ts:506-520
// Initial search with language filter
const searchResult = await ProductSearchService.searchMulti(
variations,
{ languageTag: 'estonian' }
);
if (searchResult.products.length === 0 &&
context.bookLanguage === 'estonian') {
logger.info('Zero results with Estonian filter, retrying all languages');
// Retry without language constraint
const retryResult = await ProductSearchService.searchMulti(
variations,
{ languageTag: undefined } // No filter
);
// Manually filter by language tags
const estonianBooks = retryResult.products.filter(p =>
p.tags?.includes('estonian') ||
p.tags?.includes('eesti_keel')
);
if (estonianBooks.length > 0) {
// Found Estonian books in manual filtering
searchResult.products = estonianBooks;
} else {
// No Estonian books, return all with warning
searchResult.products = retryResult.products;
searchResult.warnings.push('Estonian books unavailable, showing alternatives');
}
}
Category Over-Constraint
Problem: Very specific category + budget + type filters yield zero results.
Solution: Progressively relax constraints in priority order.
Priority Order:
- Keep: Budget, Product Type, Main Category
- Relax: Subcategory
- Relax: Budget (±20%)
- Relax: Product Type
- Emergency: Use parent category only
Level 3: Handler Safety Nets
Fallback Search Cascade
When search orchestrator returns zero products, the handler performs iterative broadening.
Implementation: app/api/chat/handlers/product-search-handler.ts:35-166
Fallback Strategies
1. Previous Context Hints
// Try previously inferred hints from conversation
const previousHints = memory.get('previousCategoryHints');
if (previousHints && previousHints.length > 0) {
const fallbackContext = {
...context,
categoryHints: previousHints,
meta: { fallbackStrategy: 'previous_hints' }
};
const result = await SearchOrchestrator.orchestrate(fallbackContext);
if (result.products.length > 0) {
return {
products: result.products,
fallbackApplied: true,
fallbackReason: 'Used previous conversation hints'
};
}
}
2. Canonical Gift Product Types
const CANONICAL_GIFT_TYPES = [
'tech',
'home_decor',
'fashion_accessories',
'beauty',
'sports',
'games',
'food_beverage'
];
for (const productType of CANONICAL_GIFT_TYPES) {
const fallbackContext = {
...context,
productType,
categoryHints: [], // Clear hints
meta: { fallbackStrategy: 'canonical_type' }
};
const result = await SearchOrchestrator.orchestrate(fallbackContext);
if (result.products.length > 0) {
return {
products: result.products,
fallbackApplied: true,
fallbackReason: `Broadened to ${productType} products`
};
}
}
3. Curated Book Categories (Last Resort)
const CURATED_BOOK_CATEGORIES = [
'Ilukirjandus', // Fiction
'Ulme ja fantaasia', // Sci-fi & Fantasy
'Detektiiv ja põnevik', // Mystery & Thriller
'Lasteraamatud', // Children's books
'Elulood' // Biographies
];
for (const category of CURATED_BOOK_CATEGORIES) {
const fallbackContext = {
occasion: 'general',
categoryHints: [category],
productType: 'book',
meta: {
fallbackStrategy: 'curated_books',
route: 'BOOK_CATEGORY_PATH'
}
};
const result = await SearchOrchestrator.orchestrate(fallbackContext);
if (result.products.length > 0) {
return {
products: result.products,
fallbackApplied: true,
fallbackReason: 'Showing popular book category as fallback'
};
}
}
Fallback Decision Tree
Transparent Messaging
When fallbacks are applied, the system transparently communicates changes to the user.
Fallback Messaging Examples
Implementation: app/api/chat/handlers/product-search-handler.ts:356-689
// Track fallback messaging
const fallbackMessages = [];
if (searchResult.meta?.budgetRelaxed) {
fallbackMessages.push(
`I expanded the budget to €${searchResult.meta.adjustedBudget} ` +
`to find more options.`
);
}
if (searchResult.meta?.categoryBroadened) {
fallbackMessages.push(
`I broadened the search to ${searchResult.meta.newCategory} ` +
`products for better variety.`
);
}
if (searchResult.meta?.booksExcluded) {
fallbackMessages.push(
`I excluded book recommendations as you requested.`
);
}
// Include in LLM generation prompt
const systemContext = {
...baseContext,
fallbacksApplied: fallbackMessages
};
User-Facing Examples
Budget Relaxation:
"I noticed that €30 was quite limiting, so I expanded the budget slightly to €40 to show you these great options that are just above your initial range."
Category Broadening:
"I couldn't find gift bags in the exact style you wanted, so I'm showing related gift packaging options instead."
Books Removed:
"I've excluded books from these recommendations as you mentioned you prefer non-book gifts."
Language Fallback:
"Unfortunately, these titles aren't available in Estonian yet, but here are the English versions which are highly rated."
Emergency Bypass
In extreme cases where all fallbacks fail, the system has an emergency bypass to prevent user frustration.
Implementation:
// Emergency bypass: better to show something than nothing
if (finalResult.products.length === 0) {
logger.error('Emergency bypass triggered', { context });
const emergencyResult = await SearchOrchestrator.orchestrate({
occasion: 'general',
categoryHints: [],
productType: 'gift',
sortBy: 'popularity',
meta: {
route: 'GIFT_OCCASION_PATH',
emergencyBypass: true
}
});
return {
products: emergencyResult.products.slice(0, 3),
fallbackApplied: true,
fallbackReason: 'emergency_popular_gifts',
message: "I'm having trouble finding exactly what you're looking for. " +
"Here are some popular gift ideas that might help:"
};
}
Memory-Based Recovery
Conversation Memory
The system remembers previous successful searches to recover from errors.
Exclusion Memory
Implementation:
// Check exclusion list from memory
const excludedIds = memory.get('excludedProductIds') || [];
// Add to context
context.excludeIds = [
...context.excludeIds,
...excludedIds
];
// After search, filter again to be sure
searchResult.products = searchResult.products.filter(
p => !excludedIds.includes(p._id)
);
Metrics & Observability
Fallback Tracking
{
fallbacksApplied: [
{
type: 'budget_relaxation',
original: 50,
adjusted: 60,
timestamp: '2025-01-15T10:30:00Z'
},
{
type: 'category_broadening',
original: 'Specific Category',
adjusted: 'Parent Category',
timestamp: '2025-01-15T10:30:01Z'
}
],
attemptCount: 3,
successfulAttempt: 2,
totalDurationMs: 450
}
Warning Logs
{
level: 'warn',
message: 'Book-only result despite EXCLUDE_BOOKS',
context: {
productCount: 25,
userConstraint: 'EXCLUDE_BOOKS',
categoryHints: ['Raamatud'],
fallbackStrategy: 'gift_categories'
}
}
Configuration
Location: app/api/chat/handlers/product-search-handler.ts
Fallback Settings
export const FALLBACK_CONFIG = {
ENABLE_FALLBACKS: true,
MAX_FALLBACK_ATTEMPTS: 5,
BUDGET_RELAXATION_PERCENT: 20,
ENABLE_EMERGENCY_BYPASS: true,
TRANSPARENT_MESSAGING: true,
CANONICAL_TYPES: [
'tech', 'home_decor', 'fashion',
'beauty', 'sports', 'games', 'food'
],
BOOK_CATEGORIES: [
'Ilukirjandus', 'Ulme ja fantaasia',
'Detektiiv ja põnevik', 'Lasteraamatud'
]
};
Key Takeaways
Multi-Layered Defense
Three levels of resilience ensure robustness:
- Query validation - Catch nonsense early
- Orchestrator logic - Handle constraint violations
- Handler fallbacks - Iterative broadening
Transparent Communication
Users are informed when fallbacks occur:
- Budget adjustments explained
- Category changes clarified
- Constraints honored or discussed
Memory-Driven Recovery
Conversation memory enables smart recovery:
- Previous context reuse
- Exclusion tracking
- Intent preservation
Graceful Degradation
Better to show approximate results than nothing:
- Emergency bypass prevents frustration
- Popular fallbacks maintain quality
- Clear messaging manages expectations
Observability Built-In
Comprehensive metrics enable debugging:
- Fallback tracking
- Attempt counts
- Warning logs
- Performance impact
Related Documentation
- Query Routing & Detection - Initial validation
- Search Orchestrator Paths - Path-specific logic
- Multi-Query Retrieval - Search execution
- Finalist Selection Pipeline - Complete flow
File References:
- Product Search Handler:
app/api/chat/handlers/product-search-handler.ts - Search Orchestrator:
app/api/chat/orchestrators/search-orchestrator.ts - Product Search Service:
app/api/chat/services/product-search.ts - Memory Resolution:
app/api/chat/services/memory-resolution.ts