Skip to main content

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:

  1. Keep: Budget, Product Type, Main Category
  2. Relax: Subcategory
  3. Relax: Budget (±20%)
  4. Relax: Product Type
  5. 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:

  1. Query validation - Catch nonsense early
  2. Orchestrator logic - Handle constraint violations
  3. 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

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