Multi-Query Retrieval
The multi-query retrieval system generates and executes multiple search variations in parallel to cast a wide net while maintaining relevance. This approach balances coverage (finding many options) with precision (finding the right options).
Overview
Instead of running a single search query, the system generates multiple focused variations and merges the results. Each variation targets a different aspect of the user's intent (occasion, budget, product type, etc.), ensuring comprehensive coverage without sacrificing relevance.
Why Multi-Query?
Single Query Problem
A single search query can't capture all aspects of intent:
Multi-Query Solution
Multiple variations capture different facets:
Query Generation Process
Input: Gift Context
The query rewriting service receives rich context from extraction:
{
occasion: "birthday",
recipient: {
age: 25,
gender: "female",
interests: ["technology", "gadgets"]
},
budget: {
min: 0,
max: 50,
currency: "EUR"
},
productType: "tech",
categoryHints: ["Electronics", "Gadgets"]
}
Output: Search Variations
Variation Types
1. Occasion-Focused Variation
Purpose: Emphasize the occasion and recipient demographics.
Template: {occasion} gift for {age}yo {gender}
Example: "birthday gift for 25yo female"
Filters:
- Category hint from occasion
- Age-appropriate flag
- Gender affinity boost
Weight: 1.2x (highest priority)
2. Budget-Focused Variation
Purpose: Find value-conscious options.
Template: gifts under {max_budget} {currency}
Example: "gifts under 50 euros"
Filters:
- Price range: 0 - max_budget
- Value-for-money ranking
- Popular in price tier
Weight: 1.1x
3. Product Type Variation
Purpose: Target specific product categories.
Template: {product_type} {interests}
Example: "tech gadgets"
Filters:
- Product type exact match
- Category alignment
- Interest keywords
Weight: 1.3x (highest for specific requests)
4. Category Variation
Purpose: Leverage pre-classified categories.
Template: {category_hint}
Example: "electronics"
Filters:
- csv_category exact match
- Subcategory expansion
- Cross-category similar items
Weight: 1.0x (baseline)
5. General Variation
Purpose: Catch unexpected relevant items.
Template: gift ideas or {recipient_gender} gifts
Example: "gift ideas"
Filters:
- Minimal filtering
- Gender affinity
- Popularity ranking
Weight: 0.8x (lower priority)
Parallel Search Execution
Search Service Flow
Implementation: app/api/chat/services/product-search.ts:41-75
Seed Management
First Request (Deterministic):
seed = deterministicSeed(userId, conversationId, context)
// Same results for same context = reproducible
Show More (Random):
seed = randomSeed()
// Different results each time = variety
Result Limits
Each variation requests 50 documents (not 20) because:
- Stage B filtering drops many products
- Budget constraints reduce pool
- Category diversity needs options
- Better to oversample and filter than undersample and regret
Guardrails & Safety
Category Filter Enforcement
Problem: Without filters, Convex scans the entire catalog (slow + irrelevant).
Solution: Automatic filter injection.
Implementation: app/api/chat/services/product-search.ts:124-155
// Guardrail: Ensure occasion/gift variations have filters
if (variation.variationType === 'occasion' ||
variation.variationType === 'gift') {
if (!variation.categoryFilter &&
!variation.productTypeFilter &&
!variation.authorFilter) {
// Safety: Inject first category hint
variation.categoryFilter = context.categoryHints[0];
logger.warn('Filter injection applied', {
variation: variation.variationType
});
}
}
Filter Fallback Strategy
When primary filters yield zero results, the system expands intelligently:
Implementation: app/api/chat/services/product-search.ts:77-121
Merging & Deduplication
Merge Process
Scoring Formula
Each product receives a composite score:
compositeScore = (
baseScore * variationWeight +
merchandisingBoost +
popularityBoost +
categoryAffinityBoost
)
Example:
Product A appears in 3 variations:
- Occasion (weight 1.2): base 0.85 → 1.02
- Budget (weight 1.1): base 0.80 → 0.88
- Type (weight 1.3): base 0.90 → 1.17
Final score: max(1.02, 0.88, 1.17) = 1.17
Or average: (1.02 + 0.88 + 1.17) / 3 = 1.02
Deduplication Logic
Diagnostics & Metrics
Timing Metrics
{
multiSearchMs: 145, // Total parallel time
variationCount: 5, // Number of variations
perVariationMs: [120, 145, 130, 135, 140],
mergeMs: 12, // Merge + dedupe time
candidatesFound: 247 // Before funnel
}
Variation Metrics
{
variations: [
{
type: 'occasion',
resultsCount: 45,
seedUsed: 'deterministic-abc123',
filtersApplied: ['csv_category', 'age_appropriate']
},
{
type: 'budget',
resultsCount: 38,
seedUsed: 'deterministic-abc123',
filtersApplied: ['price_range']
},
// ...
]
}
Book-Only Fallback
Problem
User says "no books" but Convex only returns books.
Solution
Implementation: app/api/chat/orchestrators/search-orchestrator.ts:450-493
// Check if only books returned
const onlyBooks = searchResult.products.every(p =>
p.product_type === 'book' ||
p.csv_category?.includes('Raamatud')
);
if (onlyBooks && context.constraints?.includes('EXCLUDE_BOOKS')) {
// Mutate context to broader gift categories
const fallbackContext = applyGiftFallbackContext(context);
// Retry with gift-focused variations
const retryResult = await ProductSearchService.searchMulti(
await QueryRewritingService.generateVariations(fallbackContext),
{ seed: 'fallback' }
);
// Filter out books from retry
searchResult.products = retryResult.products.filter(p =>
!isBook(p)
);
}
Language Fallback
Problem
User requests Estonian books, but catalog only has English versions.
Solution
Implementation: app/api/chat/orchestrators/search-orchestrator.ts:506-520
Performance Optimization
Parallel Execution
Sequential would be slow:
V1: 150ms
V2: 140ms
V3: 145ms
V4: 130ms
V5: 135ms
Total: 700ms ❌
Parallel is fast:
All execute simultaneously
Total: max(150, 140, 145, 130, 135) = 150ms ✅
Caching Strategy
Cache Key: hash(variationText + filters + seed)
Cache TTL: 5 minutes (balances freshness vs performance)
Configuration
Location: app/api/chat/services/product-search.ts
Key Settings
export const MULTI_QUERY_CONFIG = {
MAX_RESULTS_PER_VARIATION: 50, // Up from 20
MAX_VARIATIONS: 5, // Total variations
MERGE_TIMEOUT_MS: 5000, // Abort if too slow
ENABLE_GUARDRAILS: true, // Auto-inject filters
ENABLE_FALLBACKS: true, // Category expansion
DEDUP_STRATEGY: 'max_score' // Or 'avg_score'
};
Variation Weights
export const VARIATION_WEIGHTS = {
occasion: 1.2, // Highest for occasion-based
product_type: 1.3, // Highest for specific requests
budget: 1.1, // Moderate for budget-conscious
category: 1.0, // Baseline
general: 0.8 // Lower for exploratory
};
Key Takeaways
Coverage Through Diversity
Multiple variations capture different aspects:
- Occasion relevance
- Budget appropriateness
- Product specificity
- Category alignment
- General exploration
Parallel for Speed
Parallel execution keeps latency low:
- 5 searches complete in ~150ms
- Not 5x slower than single search
- Throughput scales horizontally
Guardrails Prevent Issues
Automatic filter injection:
- Prevents full-catalog scans
- Ensures category relevance
- Maintains performance
Intelligent Merging
Deduplication and scoring:
- Removes duplicates across variations
- Weights by variation importance
- Preserves best matches
Resilient Fallbacks
Handles edge cases gracefully:
- Book-only constraint violations
- Language mismatches
- Empty result sets
- Filter over-constraint
Related Documentation
- Query Routing & Detection - How variations are triggered
- Search Orchestrator Paths - Which paths use multi-query
- Search Resilience & Fallbacks - Error handling
- Finalist Selection Pipeline - What happens after retrieval
File References:
- Product Search Service:
app/api/chat/services/product-search.ts - Query Rewriting:
app/api/chat/services/query-rewriting/index.ts - Search Orchestrator:
app/api/chat/orchestrators/search-orchestrator.ts