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