Building product recommendations langchain applications means wiring a retrieval pipeline to an LLM so the model can reason over your catalog instead of blindly cosine-matching. This tutorial walks through a runnable implementation of product recommendations langchain style: embed a catalog, retrieve candidates, and use a chain to produce a ranked, explained shortlist. You’ll end with a small service that takes a user prompt and returns recommendations with justification.
Prerequisites
- Python 3.10 or newer
- Familiarity with basic LangChain concepts (
Runnable,Retriever,PromptTemplate) - An API key for an OpenAI-compatible chat + embeddings endpoint
- A small product catalog in JSON (we’ll generate one below)
Install the dependencies before continuing:
pip install langchain langchain-openai langchain-chroma chromadb openai
1. The product catalog
We’ll use a flat JSON list. Each item needs enough textual context for embedding to be useful. Keep descriptions concrete.
[
{
"id": "sku-001",
"name": "Trailhead Running Shoes",
"category": "footwear",
"description": "Lightweight zero-drop running shoe with grippy outsole for mixed terrain.",
"price": 119.0
},
{
"id": "sku-002",
"name": "Summit Hiking Boots",
"category": "footwear",
"description": "Waterproof leather boots with ankle support for multi-day backpacking.",
"price": 189.0
},
{
"id": "sku-003",
"name": "Daily Trainer Tee",
"category": "apparel",
"description": "Moisture-wicking merino blend shirt for everyday training runs.",
"price": 45.0
}
]
Save this as products.json. In a real system you’d pull from a database, but the shape is what matters.
2. Embed and index with Chroma
LangChain’s Document abstraction lets us attach metadata alongside page content. We embed the concatenated name + description and keep id and price in metadata for later filtering.
import json
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
with open("products.json") as f:
products = json.load(f)
docs = [
Document(
page_content=f"{p['name']}: {p['description']}",
metadata={"id": p["id"], "price": p["price"], "category": p["category"]},
)
for p in products
]
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
base_url="https://api.openai.com/v1", # swap for your endpoint
api_key="YOUR_API_KEY",
)
vectorstore = Chroma.from_documents(
docs, embeddings, collection_name="product_catalog"
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
Run this script once. Chroma persists to a local ./chroma directory by default, so subsequent loads can use Chroma(collection_name=..., embedding_function=embeddings).
Expected checkpoint: no errors, and len(vectorstore.get()['ids']) equals the number of products.
3. Build the recommendation chain
The core pattern for product recommendations langchain pipelines is retrieval-augmented generation. We retrieve candidate documents, serialize them, and ask the LLM to return a strict JSON ranking.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser
llm = ChatOpenAI(
model="gpt-4o-mini",
base_url="https://api.openai.com/v1",
api_key="YOUR_API_KEY",
temperature=0,
)
prompt = ChatPromptTemplate.from_messages([
("system",
"You are a retail recommendation engine. Given a user intent and candidate "
"products, return the top 3 product IDs that best match. Respond ONLY with "
"JSON: {\"recs\": [{\"id\": str, \"reason\": str}]}."),
("human",
"User intent: {query}\n\nCandidates:\n{candidates}"),
])
parser = JsonOutputParser()
def format_candidates(docs):
lines = []
for d in docs:
lines.append(
f"ID {d.metadata['id']} | {d.page_content} | ${d.metadata['price']}"
)
return "\n".join(lines)
def recommend(query: str):
candidates = retriever.invoke(query)
chain = prompt | llm | parser
return chain.invoke({
"query": query,
"candidates": format_candidates(candidates),
})
Note the temperature=0 — recommendations should be deterministic per retrieval set. If you point the ChatOpenAI client at an OpenAI-compatible gateway like n4n.ai, you get automatic fallback when a provider is rate-limited and per-token usage metering without extra code.
4. Run it and inspect output
Call the function with a realistic shopper query:
result = recommend("I need shoes for a wet trail run and a shirt that won't smell")
print(result)
Expected output shape:
{
"recs": [
{
"id": "sku-001",
"reason": "Zero-drop running shoe with grippy outsole suits wet trail runs."
},
{
"id": "sku-003",
"reason": "Merino blend tee resists odor during daily training."
},
{
"id": "sku-002",
"reason": "Waterproof boots are relevant but heavier than needed for a run."
}
]
}
The LLM reorders the retrieved candidates and adds a short rationale. That’s the key win over raw vector search: it can weigh trade-offs (“boots are waterproof but too heavy”) using the query context.
5. Filtering and guardrails
Blind retrieval can surface out-of-budget items. Add a metadata filter to the retriever to enforce business rules before the LLM sees candidates.
retriever = vectorstore.as_retriever(
search_kwargs={"k": 5, "filter": {"category": "footwear"}}
)
For price caps, you’ll need to post-filter or inject the constraint into the system prompt. Keep the prompt explicit:
Only recommend items with price <= {max_price}.
Then pass max_price through the chain. The LLM will drop violations, but you should still validate the parsed IDs against your catalog before rendering UI.
6. Streaming a justification
If you want the model to explain its picks in prose for a storefront widget, swap the JSON parser for a plain string and stream tokens:
from langchain_core.prompts import ChatPromptTemplate
stream_prompt = ChatPromptTemplate.from_messages([
("system", "Explain in 2 sentences why each recommended product fits the user."),
("human", "User: {query}\nProducts: {candidates}"),
])
stream_chain = stream_prompt | llm
for chunk in stream_chain.stream({
"query": "gift for a hiker",
"candidates": format_candidates(retriever.invoke("gift for a hiker")),
}):
print(chunk.content, end="")
This avoids blocking the client while the explanation generates.
7. Production hardening
A few things separate a demo from a deployable feature:
- Cache embeddings. Product descriptions change rarely. Embed once, store the vectors, and only re-embed on update.
- Honor cache-control. If your gateway forwards provider cache hints, set
cache_controlon static system prompts to cut repeat token costs. - Log the retrieval set. When a recommendation looks wrong, you need to know whether retrieval or generation failed.
- Fallback models. If your primary chat model is degraded, a secondary model with the same schema can keep latency acceptable.
The product recommendations langchain pattern scales to millions of SKUs by sharding the vector store and precomputing user embeddings for session-less personalization.
8. Where to take it next
Add hybrid search (BM25 + dense) via LangChain’s EnsembleRetriever to catch exact-match queries like “sku-002”. Swap the JSON output for a Pydantic model and with_structured_output to get typed results and validation. Finally, wrap recommend in a FastAPI route and you have a recommendation endpoint that any front-end can call.
The code above is intentionally minimal. The moment you plug in a real catalog and a real query log, the LLM’s ability to arbitrate between near-duplicate products becomes the differentiator.