---
title: "Guide: Building a Product Recommendation Engine That Doesn't Hallucinate"
url: https://apyhub.com/blog/product-recommendation-engine-hybrid-rag
author: Nikolas Dimitroulakis
published: 2026-06-23T00:00:00Z
tags: [tutorials]
---

# Guide: Building a Product Recommendation Engine That Doesn't Hallucinate

# Building a Product Recommendation Engine That Doesn't Hallucinate: A Practical Guide

A recommendation engine that doesn't hallucinate splits one job into two: use vector search to *discover* candidate products by semantic similarity, then make a live API call to fetch the *facts* — price, stock, shipping — at request time, never from the embedding. That separation is the hybrid RAG pattern, and it's the difference between a demo and something you can put in front of customers.

The failure mode is familiar to anyone who's shipped one. You build a vector-search recommender in an afternoon, it demos beautifully, and then production reveals the cracks: embeddings go stale, a similarity query that was instant on 1,000 products crawls on 1,000,000, and — worst of all — the model confidently recommends a product at a price it made up or that's been out of stock for a week. The fix isn't a better model. It's an architecture that never asks the model to remember a fact that changes.

Here's the core shape, before we go deeper:

```python
def recommend(query: str, user_ctx: dict) -> list[dict]:
    # 1. DISCOVERY — semantic, approximate, can tolerate slight staleness
    candidate_ids = vector_store.search(embed(query), top_k=50, filters=user_ctx)

    # 2. FACTS — authoritative, live, never from the vector index
    live = commerce_api.get_products(candidate_ids)        # price, stock, shipping
    in_stock = [p for p in live if p["stock"] > 0]

    # 3. RANK — combine semantic score with live business rules
    return rank(in_stock, query, user_ctx)[:10]
```

The vector store answers "what's *relevant*." The live call answers "what's *true right now*." Keep those two questions in separate systems and the hallucinated-fact problem largely disappears.

## Why vector-only demos hit a wall in production

The afternoon demo works because the catalog is small, static, and recent. Production breaks all three assumptions:

* **Stale embeddings.** You embedded the catalog last Tuesday. Since then prices changed, items sold out, descriptions were edited, and new SKUs landed. The vectors don't know. If the LLM reads price or availability out of the indexed text, it will state last Tuesday's reality with total confidence.
* **Latency spikes.** Approximate-nearest-neighbor search is fast until it isn't — index size, filter cardinality, and re-ranking with an LLM all add up. A 40ms similarity lookup in the demo becomes a multi-hundred-millisecond p95 once you add metadata filtering and a re-rank pass.
* **Hallucinated product facts.** This is the one that gets you a support ticket. An LLM asked to "recommend and describe" will happily invent a dimension, a material, or a discount that isn't real, because generating plausible text is exactly what it does. Facts must be *retrieved and quoted*, never generated.

None of these is solved by a smarter model. They're solved by deciding, per field, whether it's *discoverable* (safe to index and embed) or *authoritative* (must be fetched live).

## The hybrid RAG pattern

The architecture is a two-lane data flow. One lane is optimized for relevance and tolerates mild staleness; the other is optimized for correctness and is always live.

```
                 ┌────────────────────────────────────────────┐
   user query →  │  DISCOVERY LANE  (tolerates staleness)       │
                 │  embed(query) → vector search (top-k)        │
                 │  + metadata filters (category, size, brand)  │
                 └───────────────┬──────────────────────────────┘
                                 │  candidate product IDs
                                 ▼
                 ┌────────────────────────────────────────────┐
   facts only →  │  TRUTH LANE  (always live)                   │
                 │  commerce API: price, stock, shipping        │
                 │  drop out-of-stock, apply live pricing rules │
                 └───────────────┬──────────────────────────────┘
                                 │  verified candidates
                                 ▼
                 ┌────────────────────────────────────────────┐
                 │  RANK + (optional) LLM copy                  │
                 │  LLM may only describe fields passed to it,  │
                 │  never invent price/stock/specs              │
                 └──────────────────────────────────────────────┘
```

The rule that keeps it honest: **the LLM only ever sees, and may only repeat, the fields you hand it from the truth lane.** If price came from the live API, the model can mention it. If a spec wasn't retrieved, the model isn't allowed to assert it. This is enforced in the prompt and, ideally, validated on the way out.

### What a discovery query looks like

The vector search returns candidates and similarity scores — not facts a customer sees:

```json
{
  "query_vector": "[0.0123, -0.0456, ...]",
  "top_k": 50,
  "filters": { "category": "cookware", "in_catalog": true },
  "results": [
    { "product_id": "SKU-10293", "score": 0.91 },
    { "product_id": "SKU-44871", "score": 0.88 }
  ]
}
```

### What the truth lane returns

A live call to your own commerce backend (or a commerce API) resolves the facts at request time:

```json
{
  "products": [
    {
      "product_id": "SKU-10293",
      "title": "Cast Iron Skillet, 12in",
      "price": { "amount": 49.00, "currency": "EUR" },
      "stock": 23,
      "updated_at": "2026-06-23T18:55:02Z"
    }
  ]
}
```

Notice `updated_at`: if your truth lane can't tell you how fresh a fact is, you don't have a truth lane, you have a second cache.

## The data-freshness problem

The hard design question isn't "vectors or live calls" — it's *which fields go in which lane*, and how often the discovery lane is rebuilt.

A workable default:

* **Index and embed** the slow-changing, descriptive fields: title, long description, category, material, tags, key attributes. These power discovery and can tolerate being hours — even a day — old.
* **Never index** the fast-changing, customer-visible facts: price, stock, promotions, shipping estimates. Always fetch these live.
* **Re-embed on change, not on a clock.** Trigger re-embedding from your catalog's change events (a product description edit, a new SKU) rather than a nightly full rebuild, so the discovery lane drifts as little as possible. Fall back to a periodic full rebuild only as a safety net.

The freshness budget is a real number you should write down: how stale is the discovery lane allowed to be before it's a problem? For descriptive fields, "a few hours" is usually fine. For anything a customer transacts on, the answer is "zero," which is exactly why those fields live in the truth lane.

## The messy parts: getting clean data into the discovery lane

Most of the engineering pain in a real recommender isn't the vector math — it's getting a messy, multi-source catalog into a clean, consistent shape worth embedding. Supplier feeds arrive as inconsistent CSVs and spreadsheets, descriptions are unstructured prose, specs are trapped in PDFs, and product images are wildly inconsistent. Garbage in the discovery lane produces irrelevant recommendations no amount of re-ranking fixes.

This ingestion-and-normalization layer is where ready-made utility APIs earn their place — not as the recommendation engine, but as the unglamorous pre-processing that makes embeddings clean and metadata reliable. [ApyHub](https://apyhub.com/) covers a lot of exactly this work:

* **Normalize multi-source catalog data.** File-conversion APIs turn supplier CSV / Excel / JSON feeds into one consistent JSON shape before ingestion, so you're embedding clean records instead of fighting ten feed formats.
* **Extract structured attributes from unstructured text.** Text-AI APIs (keyword/entity extraction, summarization, classification) and data-extraction APIs pull clean attributes and tidy descriptions out of messy prose and product documents — better embedding input, and better metadata filters for the discovery lane.
* **Consistent taxonomy.** The [Product Categorization API](https://apyhub.com/catalog/commerce-quick-tools) assigns a uniform category to every item, which is what makes your metadata filters (`category: cookware`) actually trustworthy across suppliers.
* **Normalize product images.** Image-processing APIs (thumbnail generation, background removal, metadata stripping) standardize images, and image-analysis APIs (object/label detection) enrich them with tags you can filter on. Note the honest boundary: these prepare and label images — the visual-similarity vectors themselves are still computed by your own embedding model, not by these APIs.
* **Live price display.** When you do show prices across markets, the [Currency Conversion API](https://apyhub.com/catalog/commerce-quick-tools) handles real-time conversion in the truth lane — the authoritative numbers still come from your commerce backend; conversion just formats them per market.

A real normalization call in the ingestion pipeline looks like any other REST request — here, converting a supplier spreadsheet feed to JSON before embedding:

```bash
curl --request POST \
  --url 'https://api.apyhub.com/convert/excel/json' \
  --header 'apy-token: APY_TOKEN' \
  --header 'content-type: multipart/form-data' \
  --form 'file=@supplier-feed.xlsx'
```

```json
{ "data": [ { "sku": "SKU-10293", "title": "Cast Iron Skillet, 12in", "category": "cookware" } ] }
```

*(Endpoint paths and exact response shapes should be confirmed against the live API docs before you wire them in — treat the above as illustrative of the shape, not a verified contract.)*

## Where ApyHub fits

To be precise about the boundary: ApyHub doesn't host your vectors, run your embedding model, or rank your results — those stay in your stack (your vector DB, your model, your business rules). What it consolidates is the long tail of ingestion and normalization utilities the discovery lane depends on — file conversion, text extraction, categorization, image preprocessing — behind one key and one consistent integration pattern instead of a dozen separate vendor integrations. ApyHub describes its catalog as 200+ APIs and 1,000+ endpoints, and names GDPR, SOC 2, ISO 27001, and OWASP API guidelines on the compliance side. Where it's *not* the answer: the recommendation logic, the vector index, and the embedding model itself — bring your own there.

[Explore the ecommerce APIs →](https://apyhub.com/catalog/commerce-quick-tools)

## Conclusion

The reason most AI recommenders hallucinate isn't a weak model — it's an architecture that asks the model to remember facts that change. Split the job: let vectors handle discovery, let a live call handle the truth, and never let the LLM assert a field you didn't retrieve. Get the boring ingestion layer clean and consistent, keep the truth lane authoritative and timestamped, and the "great demo, broken in production" gap closes.

[Try ApyHub →](https://apyhub.com/)

## FAQ

**What is the hybrid RAG pattern for recommendations?**
It's an architecture that uses vector search to find semantically relevant candidate products, then makes a live API call to fetch authoritative facts (price, stock, shipping) at request time. Discovery tolerates mild staleness; facts are always live and never read from the embedding.

**Why do vector-only recommendation engines hallucinate?**
Because the indexed text captures the catalog at embedding time, and an LLM asked to describe a product will repeat those stale fields — or invent plausible ones — with full confidence. Facts that change must be retrieved live and quoted, not generated.

**Which product fields should I embed, and which should I fetch live?**
Embed slow-changing descriptive fields: title, description, category, material, tags. Fetch live anything a customer transacts on: price, stock, promotions, shipping. The test is whether being a few hours stale would ever mislead a customer.

**How often should I re-embed my catalog?**
Prefer re-embedding on catalog change events (edits, new SKUs) so the discovery lane drifts minimally, with a periodic full rebuild as a safety net. A fixed nightly rebuild alone leaves a full day of drift in descriptive fields.

**How do I stop the LLM from inventing product details?**
Only pass the model fields you actually retrieved from the truth lane, instruct it to describe only those fields, and validate the output against the retrieved data. If a spec wasn't retrieved, the model must not assert it.

**Does ApyHub provide the vector search or the recommendation model?**
No. ApyHub covers the ingestion and normalization utilities around the engine — file conversion, text/data extraction, product categorization, image preprocessing. The vector index, embedding model, and ranking logic stay in your own stack.

**What causes the latency spikes in production RAG?**
Index growth, high-cardinality metadata filters, and LLM re-ranking passes each add latency that wasn't visible at demo scale. Set a p95 budget, measure each stage, and keep the truth-lane lookup tight since it's on the critical path.

**Can I use image similarity for "shop the look" recommendations?**
Yes, but the similarity vectors come from your own image-embedding model. Utility APIs help by normalizing images (thumbnails, background removal) and adding filterable labels (object/logo detection) — useful inputs to your similarity step, not a replacement for it.

***

**About ApyHub**

ApyHub is the trusted layer of external APIs — powering developers and AI agents to build awesome applications without rebuilding commodity functionality. With a catalog of 200+ APIs and 1,000+ endpoints available under a single key and one consistent integration pattern, ApyHub lets teams offload the utility layer and focus on what actually makes their product theirs. [**Explore the catalog →**](https://apyhub.com/catalog)
