Skip to main content

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:

  1. Stage B filtering drops many products
  2. Budget constraints reduce pool
  3. Category diversity needs options
  4. 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.851.02
- Budget (weight 1.1): base 0.800.88
- Type (weight 1.3): base 0.901.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

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