A product search box looks simple until users stop typing exact product names. Someone searches for "snacks for a movie night", "comfortable shoes for my grandfather", or "light jacket for a cold hill trip". A keyword index can match exact terms, synonyms, and filters, but it often misses the intent behind the query.
The tempting fix is to call a large language model (LLM) on every search request. The model can rewrite vague queries, infer categories, and suggest related products. That works in a demo, but it is dangerous in production. LLM calls are slower than normal service calls, cost grows with traffic, and provider failures can turn search into an outage.
For a mid-to-large e-commerce platform, the numbers make the problem clear. With 10 million daily active users and 5 searches per user per day, the platform handles about 50 million searches daily. That is about 579 average requests per second, and about 2,895 requests per second at 5x peak. The search experience should usually respond in less than a second at P90, while difficult P99 searches can tolerate a few seconds only when that extra time avoids a dead-end result.
The goal is not to remove LLMs from search. The goal is to put them in the right place: mostly offline, sometimes in a controlled fallback path, and never as an unconditional dependency for every user request.
The Problem
An e-commerce search system must return relevant products, filters, and discovery suggestions from a large product catalog. In this scenario, assume the catalog contains about 10 million products. Each product has fields such as name, description, brand, category, price, stock state, rating, and attributes.
The system has four practical requirements:
| Requirement | Why it matters |
|---|---|
| Natural language understanding | Users describe needs, not database fields |
| Semantic search | Relevant products may not share exact query words |
| Low latency | Search is part of the purchase path, so slow results hurt conversion |
| Freshness | Price, stock, and ratings must reflect the live catalog |
A naive LLM-first flow looks like this:
User search
-> API Gateway
-> Search Service
-> LLM rewrite for every query
-> Search index
-> Product results
This design has three problems.
First, it adds variable latency to every request. Even if most queries are simple, they still wait for the model.
Second, it creates unbounded cost. A high-traffic query such as "running shoes" should not need repeated model reasoning.
Third, it creates a hard availability dependency. If the model provider is slow, rate limited, or unavailable, the search page degrades even for queries that a normal index could answer.
A better design treats LLM query understanding as an optimization layer, not the primary request path.
Target Architecture
The architecture has three cooperating flows:
Offline intelligence path
Search logs
-> Batch aggregation
-> Query enrichment
-> LLM segmentation and classification
-> Semantic linking and query expansion
-> Cached search plans
Online search path
User query
-> API Gateway
-> Search Service
-> Query cache lookup
-> Keyword search plus vector search
-> Lightweight re-ranking
-> Product results
Controlled fallback path
Low confidence or zero results
-> LLM query rewrite
-> Run search again
-> Return rescued results
The key idea is to precompute expensive intelligence for common and emerging queries, then let the live service do fast retrieval. The LLM still helps with ambiguous searches, but it no longer blocks the majority of requests.
Step 1: Build the Offline Query Enrichment Pipeline
The offline pipeline converts search behavior into reusable search intelligence. It can run daily for comprehensive analysis and more frequently during high-traffic periods such as sales or festivals.
A practical pipeline looks like this:
Analytic data lake
-> Spark aggregation job
-> Query event queue
-> Enrichment workers
-> Query processing queue
-> Segmentation and classification service
-> Query expansion service
-> Ranking and indexing workers
-> Smart query cache
Aggregate Search Logs
The first job reads search events from the analytics store. Each event should connect a user query to what happened next: impressions, clicks, add-to-cart actions, and purchases.
The aggregation step should also count query frequency. Frequency decides which queries deserve the fastest cache tier later.
A simplified event model can be expressed without tying it to a specific storage format:
SearchEvent
user_id
session_id
raw_query
normalized_query
displayed_products
clicked_product
added_to_cart_product
purchased_product
device_type
event_time
The output is not just a list of queries. It is evidence about which products users actually found useful for those queries.
Enrich Each Query
Next, enrichment workers normalize and hydrate each query before model processing.
Useful enrichment fields include:
- Corrected spelling and normalized casing
- User purchase history and brand preferences
- Location, time of day, device type, and local trends
- Query frequency and conversion signals
This step should be parallelized behind a queue so log aggregation and enrichment can scale independently. If an enrichment worker fails, the rest of the pipeline can continue processing other batches.
Segment and Classify the Query
The segmentation service turns a natural language query into the platform's product taxonomy.
For example, a query such as "organic long-grain rice" might become:
| Query part | Meaning |
|---|---|
| organic | Attribute |
| long-grain | Product quality |
| rice | Product category |
The important guardrail is that the model should classify into known platform categories and attributes. It should not invent taxonomy paths that your catalog does not support.
Expand the Query Using Existing Products
An LLM cannot see a huge product catalog in one prompt because every model has a context window limit. The system must retrieve a small candidate set first.
That is where vector search helps. Product descriptions and tags are converted into embeddings, which are numerical representations of meaning. The query is also embedded, and the vector database returns products that are conceptually close even when they do not share keywords.
The expansion service should use this small candidate set to build a better structured search plan. It can include synonyms, alternatives, and related product types that actually exist in the catalog.
Step 2: Cache Search Plans, Not Full Product Lists
Caching the final list of product IDs looks attractive because it is the fastest possible read. It is also risky.
If a cached product goes out of stock, changes price, or receives a rating update, the user can see stale results. For search, freshness is part of correctness.
The safer default is to cache the structured search plan that the offline pipeline produced. At request time, the search service executes that plan against the live search index. This keeps price, stock, filters, and ratings current.
| Cache value | Advantage | Risk |
|---|---|---|
| Final product IDs | Fastest response | Can become stale when catalog data changes |
| Structured search plan | Smaller and fresher | Still requires a search index call |
A hybrid strategy works best:
- Cache structured search plans for most precomputed queries.
- Cache final results only for a small set of very hot and stable queries.
- Use a short time-to-live for any final result cache.
- Refresh query plans when the offline pipeline produces better versions.
Step 3: Use Cache Tiers Based on Query Frequency
Not every query deserves the same cache. A few queries drive a large share of traffic, while the long tail contains many unique phrases.
Use three tiers:
| Tier | Stored in | Best for | Lookup type |
|---|---|---|---|
| L1 | Local in-process memory | Top 5 percent of normalized queries | Exact key lookup |
| L2 | Redis or similar distributed cache | Top 20 percent of normalized queries | Exact key lookup |
| Warm tier | Vector database | Long-tail queries with similar meaning | Semantic lookup |
The runtime lookup becomes:
Normalize query
-> Check L1 exact cache
-> Check L2 exact cache
-> Embed query
-> Check warm semantic cache
-> Run fast hybrid search
-> Use LLM fallback only if confidence is low
L1 cache should be warmed from a central hot-query list generated by the offline pipeline. At startup, each search service instance loads the current top queries into local memory. When the pipeline publishes a new hot list, running instances refresh their local cache.
This avoids a common operational mistake: letting each service instance guess what belongs in memory.
Step 4: Keep the Online Search Path Fast
The online service should assume most searches can be answered without a model call.
A fast path has three parts:
- Keyword search against Elasticsearch or OpenSearch for exact terms, filters, and facets.
- Semantic search against the vector database for intent matches.
- A lightweight re-ranker that merges, deduplicates, and orders the candidate results.
The re-ranker can use signals such as keyword score, semantic score, product popularity, and user affinity. Personalization should be lightweight on the hot path. Expensive global analysis belongs in the offline job.
Here is a conceptual orchestrator. It avoids provider-specific details and focuses on the decision flow.
from dataclasses import dataclass
from typing import Sequence
@dataclass(frozen=True)
class SearchRequest:
query: str
user_id: str
page: int
filters: tuple[str, ...]
@dataclass(frozen=True)
class ProductHit:
product_id: str
title: str
category: str
score: float
def search_products(request: SearchRequest) -> Sequence[ProductHit]:
normalized_query = normalize_query(request.query)
cached_plan = find_cached_search_plan(normalized_query)
if cached_plan is not None:
candidates = execute_search_plan(cached_plan, request.filters)
return rerank_for_user(candidates, request.user_id)
fast_candidates = run_hybrid_fast_path(normalized_query, request.filters)
fast_score = best_score(fast_candidates)
if fast_score >= 0.65:
return rerank_for_user(fast_candidates, request.user_id)
rewritten_query = rewrite_query_with_llm(normalized_query)
rescued_candidates = run_hybrid_fast_path(rewritten_query, request.filters)
return rerank_for_user(rescued_candidates, request.user_id)
This design makes the LLM a P99 rescue path. It is called only when cache lookup and hybrid retrieval cannot produce a confident result.
That tradeoff is intentional. For normal traffic, users get sub-second behavior. For difficult long-tail searches, the system can spend a few seconds rewriting the query because a slower useful result is better than a fast empty page.
Step 5: Add Product Discovery Without Blocking Search
Search should not only find the requested product. It should also surface complementary items. A search for a cake mix might show frosting, candles, or baking trays. A search for hiking shoes might suggest socks or a rain jacket.
Do not compute those suggestions with an online LLM call.
Instead, let the offline pipeline combine segmented products with co-purchase analytics. The LLM can help choose complementary categories and product groups, then write the result into a discovery cache.
The online flow stays simple:
User searches
-> Main search executes
-> Discovery cache lookup runs in parallel
-> Results page shows products plus related suggestions
This gives the user a richer product discovery experience without adding model latency to the main result path.
Step 6: Handle New Trends With a Near-Real-Time Speed Layer
A daily batch job is not enough during sudden demand shifts. A celebrity outfit, local weather change, sports event, or flash sale can create a new query pattern in minutes.
Use a near-real-time speed layer beside the daily batch layer:
Live query log topic
-> Stream processor
-> Sliding window count
-> Trend detector
-> Lite enrichment with fast model
-> Staging warm cache
-> Human approval for high-velocity trends
-> Public cache
Daily batch layer
-> Deep analysis
-> Global ranking
-> Overwrite temporary trend entries
The stream processor can use a 5-minute sliding window that updates every 10 seconds. If a normalized query crosses a trend threshold, such as more than 100 searches in 5 minutes, send only that query through a faster enrichment path.
The speed layer should skip slow, expensive steps. It should perform simple segmentation and write a good-enough entry to a staging cache. Later, the full batch job overwrites it with the higher-quality result.
For high-velocity trends, do not promote model-generated search plans directly to production. Send an approval alert to a merchandising or search-quality reviewer first. Viral queries amplify mistakes quickly.
Step 7: Choose the Vector Index for Your Catalog Size
Semantic search depends on nearest-neighbor lookup in a high-dimensional vector space. Exact nearest-neighbor search is often too slow at catalog scale, so production systems use approximate nearest neighbor techniques.
Two common choices are graph-based search and inverted partitioning.
| Approach | How it works | Best for | Main weakness |
|---|---|---|---|
| HNSW graph search | Builds layers of connected vectors and navigates toward nearby items | Millions of products where low latency and high recall matter | Higher memory use and slower index builds |
| Inverted partitioning | Groups vectors into clusters and compresses them | Very large catalogs where memory cost dominates | Lower recall and often higher latency |
For a 10 million product catalog, HNSW is a reasonable default because it favors low latency and high recall at the scale described here. If the catalog grows toward billions of items and memory becomes the dominant constraint, inverted partitioning becomes more attractive.
Step 8: Test Relevance Before Shipping Ranking Changes
Search changes should not ship only because the code passes unit tests. A ranking change can be technically correct and still reduce revenue.
Use three quality gates.
Golden Query Regression
A golden set contains high-value queries that must continue to work. For example, a query for a known brand should return that brand near the top. A query for a major category should return products from that category.
from dataclasses import dataclass
@dataclass(frozen=True)
class GoldenCase:
query: str
expected_value: str
expected_field: str
GOLDEN_CASES = (
GoldenCase("wireless earbuds", "Electronics", "category"),
GoldenCase("trail running shoes", "Footwear", "category"),
GoldenCase("vitamin drink", "Beverages", "category"),
GoldenCase("noise cancelling headphones", "Audio", "category"),
)
def test_top_results_match_golden_cases():
for case in GOLDEN_CASES:
results = get_search_results(case.query, limit=3)
assert results, "No results for golden query: " + case.query
top = results[0]
actual = getattr(top, case.expected_field)
assert case.expected_value in actual
LLM-as-a-Judge on Samples
For broader coverage, a strong judge model can compare search results against the user query and assign a relevance score. Do not run this on live traffic. Run it on a controlled sample, such as a few hundred daily queries, so cost stays bounded.
Use the judge score as a directional quality signal, not as the only release gate. Human review is still needed for ambiguous, sensitive, or business-critical queries.
A/B Testing
Any major ranking or query expansion change should roll out to a subset of users first. Compare the treatment group against the existing search stack using click-through rate, conversion rate, and revenue per session.
import hashlib
def experiment_group(user_id: str, experiment_name: str) -> str:
seed = (user_id + ":" + experiment_name).encode("utf-8")
bucket = int(hashlib.sha256(seed).hexdigest(), 16) % 100
if bucket < 50:
return "control"
return "treatment"
def handle_search(request: SearchRequest):
group = experiment_group(request.user_id, "hybrid_rerank_v2")
if group == "treatment":
return search_products(request)
return legacy_search(request)
The bucket function must be deterministic so the same user stays in the same group throughout the test.
Common Mistakes
- Calling the LLM for every query, even when a cache or hybrid search can answer it.
- Caching product IDs as the default value and serving stale prices or out-of-stock items.
- Forgetting to count query frequency during log aggregation, then having no reliable way to decide cache tiers.
- Treating long-tail semantic cache misses as normal failures instead of routing them through a controlled fallback path.
- Promoting near-real-time trend outputs directly to the public cache without review.
- Optimizing P99 for speed only, when a slower rescued result may be better than an immediate empty result.
- Measuring only latency and error rate while ignoring click-through rate and conversion rate.
Production Checklist
Before shipping LLM-assisted search, verify the following:
- The online search path can serve common queries without calling an LLM.
- Query plans are cached separately from final product results.
- Final result caches use short time-to-live values.
- L1 cache is loaded from a generated hot-query list.
- L2 cache handles the broader set of frequent queries.
- The warm tier supports semantic lookup for long-tail queries.
- The fallback LLM path triggers only on low-confidence or zero-result searches.
- Keyword and vector results are merged, deduplicated, and re-ranked.
- Product discovery suggestions are precomputed offline.
- Near-real-time trend entries are staged before public promotion.
- Ranking changes pass golden query regression tests.
- A/B tests monitor relevance and business metrics, not only service health.
- The GenAI service has timeouts, retries, circuit breakers, and model fallback.
Conclusion
LLMs can make e-commerce search smarter, but only if they are not placed blindly in the hot path. The scalable design is to move expensive query understanding into offline and near-real-time pipelines, cache the resulting search plans, and reserve live model calls for difficult low-confidence searches.
The result is a search system that can understand ambiguous user intent, preserve catalog freshness, handle product discovery, and stay responsive under high traffic. The important architectural move is simple: let LLMs build search intelligence, but let the search service serve most users from fast retrieval, caches, and lightweight ranking.