Phase 1-2: Query Rewriting & Multi-Search
Approach
Method: Deterministic Rule-based
No LLM Used
Purpose
Generate search query variations and execute parallel searches without LLM variability:
- Query Rewriting (Phase 1): Create primary + fallback queries
- Multi-Search (Phase 2): Execute parallel Convex searches
- Filter Application: Apply budget and constraints
- Candidate Collection: Gather ~1000+ initial candidates
Why Rule-Based?
Predictability
- No LLM drift: Same input always produces same queries
- Deterministic: Perfect for testing and debugging
- Stable: No model updates breaking behavior
- Controllable: Exact query logic is visible
Latency
- Speed: <50ms total (vs moderate with LLM)
- No API calls: Zero network overhead
- Parallel: Multiple searches execute simultaneously
- Cacheable: Query patterns can be memoized
Reliability
- No hallucination: Queries stay within known vocabulary
- No malformed output: Always valid search syntax
- Error-free: Pure functions with guaranteed outputs
- Repeatable: Critical for A/B testing
Cost
- Zero LLM cost: No model inference
- Only search cost: Convex query charges apply
- Scalable: Linear cost with volume
Implementation
Query Rewriting (Phase 1)
Location: services/query-rewriting
function generateQueryVariations(
giftContext: GiftContext
): QueryVariation[] {
const variations: QueryVariation[] = [];
// Primary query: specific category + product type
if (giftContext.category && giftContext.productType) {
variations.push({
type: 'primary',
query: `${giftContext.category} ${giftContext.productType}`,
weight: 1.0
});
}
// Fallback 1: product type only
if (giftContext.productType) {
variations.push({
type: 'fallback',
query: giftContext.productType,
weight: 0.8
});
}
// Fallback 2: category hints
if (giftContext.categoryHints?.length) {
giftContext.categoryHints.forEach(hint => {
variations.push({
type: 'fallback',
query: hint,
weight: 0.6
});
});
}
return variations;
}
Multi-Search (Phase 2)
Location: services/product-search
async function executeMultiSearch(
queries: QueryVariation[],
filters: SearchFilters
): Promise<Product[]> {
// Execute all queries in parallel
const results = await Promise.all(
queries.map(q =>
convex.query(api.search.vectorSearch, {
query: q.query,
limit: 100,
filters: {
budget: filters.budget,
excludeIds: filters.excludeIds,
constraints: filters.constraints
}
})
)
);
// Merge and deduplicate
const uniqueProducts = deduplicateProducts(results.flat());
// Weight by query importance
return weightProducts(uniqueProducts, queries);
}
Query Patterns
For "Show More"
// Special case: use last search params
if (giftContext.intent === 'show_more_products') {
return {
query: lastSearchParams.queryForSearch,
categoryHints: lastSearchParams.categoryHints,
excludeIds: [...previousIds]
};
}
For Category Search
// Example: "sünnipäevakingitused"
queries = [
{ query: "Sünnipäev Kingitus", weight: 1.0 },
{ query: "Kingitus", weight: 0.8 },
{ query: "Kodu ja Aed", weight: 0.6 } // category hint
];
For Author Search
// Example: "Tolkien"
queries = [
{ query: "Tolkien Raamat", weight: 1.0 },
{ query: "J.R.R. Tolkien", weight: 1.0 },
{ query: "Fantaasia Ilukirjandus", weight: 0.7 } // genre fallback
];
Performance Characteristics
Filters Applied
interface SearchFilters {
// Budget constraints
budget?: {
min?: number;
max?: number;
};
// Exclusions
excludeIds: string[]; // Previously shown products
// Constraints
constraints?: string[]; // e.g., ['EXCLUDE_BOOKS', 'EXCLUDE_GIFT_CARDS']
// Language
bookLanguage?: 'et' | 'en'; // For books only
// Availability
inStock?: boolean;
}
Optimization Strategies
1. Query Deduplication
// Remove duplicate queries before search
const uniqueQueries = Array.from(
new Set(queries.map(q => q.query))
).map(query =>
queries.find(q => q.query === query)!
);
2. Early Termination
// Stop searching if enough high-quality results
if (candidates.length >= 100 && minScore > 0.8) {
return candidates.slice(0, 100);
}
3. Smart Fallbacks
// If primary query returns <3 results, try fallbacks
if (primaryResults.length < 3) {
const fallbackResults = await executeFallbackQueries();
return [...primaryResults, ...fallbackResults];
}
Error Handling
async function safeMultiSearch(
queries: QueryVariation[]
): Promise<Product[]> {
try {
return await executeMultiSearch(queries);
} catch (error) {
console.error('Multi-search failed:', error);
// Fallback: single broad query
return await convex.query(api.search.vectorSearch, {
query: 'Kingitus', // Generic gift
limit: 50
});
}
}
Monitoring
Track in production:
{
queryCount: number, // Queries generated per request
searchTimeMs: number, // Total search duration
candidatesFound: number, // Products before filtering
deduplicationRate: number, // % removed as duplicates
fallbackTriggered: boolean, // Primary failed?
// Per-query metrics
queryMetrics: {
primary: { count: number, avgScore: number },
fallback: { count: number, avgScore: number }
}
}
Testing
describe('Query Rewriting', () => {
it('generates primary + fallback queries', () => {
const context = {
productType: 'Raamat',
category: 'Ilukirjandus'
};
const queries = generateQueryVariations(context);
expect(queries[0].query).toBe('Ilukirjandus Raamat');
expect(queries[1].query).toBe('Raamat');
expect(queries.length).toBeGreaterThanOrEqual(2);
});
});
describe('Multi-Search', () => {
it('executes queries in parallel', async () => {
const queries = [
{ query: 'Raamat', weight: 1.0 },
{ query: 'Ilukirjandus', weight: 0.8 }
];
const start = Date.now();
const results = await executeMultiSearch(queries);
const duration = Date.now() - start;
expect(duration).toBeLessThan(300); // Parallel speedup
expect(results.length).toBeGreaterThan(0);
});
});
Alternatives Considered
LLM-based Query Rewriting
Why Not:
- Latency: Adds moderate
- Cost: $0.10-0.20 per 1000 requests
- Variability: Same context → different queries
- Debugging: Hard to trace query decisions
Single Query Approach
Why Not:
- Recall: Misses relevant products
- No fallback: Fragile to category mismatch
- Lower quality: Fewer candidates for reranking
Related Documentation
- Phase 0: Context Detection - Previous phase
- Phase 3: Semantic Rerank - Next phase
- Architecture: Orchestration System