Building a find similar products llamaindex feature for an e-commerce site does not require a heavy recommendation service. With LlamaIndex and a vector store, you can turn product descriptions into embeddings and retrieve nearest neighbors in a few dozen lines of Python. This tutorial walks through a runnable implementation you can drop into a catalog service.
Prerequisites
- Python 3.10 or newer
llama-index(installs core + default integrations)openaiPython package (used by the embedding provider)- A product catalog; we’ll mock one inline
pip install llama-index openai
Set your API key for the embedding provider:
export OPENAI_API_KEY="sk-..."
If you later swap to a gateway, the same env var works as long as the endpoint is OpenAI-compatible.
Sample catalog
We’ll use a small list of products. In production this comes from your DB or search index.
products = [
{"id": "p1", "name": "Trailrunner 2 sneakers", "category": "footwear",
"description": "Lightweight mesh running shoes with responsive foam midsole."},
{"id": "p2", "name": "Court Classic tennis shoes", "category": "footwear",
"description": "Leather tennis sneakers with cushioned insole for court play."},
{"id": "p3", "name": "Alpine hiking boots", "category": "footwear",
"description": "Waterproof leather boots with ankle support for rugged trails."},
{"id": "p4", "name": "Yoga mat pro", "category": "fitness",
"description": "Non-slip TPE yoga mat with alignment lines and carry strap."},
{"id": "p5", "name": "Resistance band set", "category": "fitness",
"description": "Latex resistance bands for mobility and strength training."},
]
Load documents with metadata
LlamaIndex Document objects carry text plus a metadata dict. We store id, name, and category so we can filter and present results.
from llama_index.core import Document
documents = [
Document(
text=p["description"],
metadata={"id": p["id"], "name": p["name"], "category": p["category"]},
)
for p in products
]
print(f"Loaded {len(documents)} documents")
Expected output:
Loaded 5 documents
Configure the embedding model
LlamaIndex defaults to OpenAI embeddings if OPENAI_API_KEY is set. We’ll pin the model explicitly and set the global Settings so the index uses it.
from llama_index.core import Settings
from llama_index.embeddings.openai import OpenAIEmbedding
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
If you’d rather not call OpenAI directly, point LlamaIndex at any OpenAI-compatible gateway. For example, n4n.ai exposes one OpenAI-compatible endpoint covering 240+ models and handles provider fallback, so you can swap the base URL without changing your index code:
Settings.embed_model = OpenAIEmbedding(
model="text-embedding-3-small",
api_base="https://api.n4n.ai/v1",
api_key="your-gateway-key",
)
The rest of the tutorial is identical regardless of which backend serves embeddings.
Build the vector index
VectorStoreIndex computes embeddings for each document and stores them in an in-memory vector store. For production, back it with Pinecone, Chroma, or a persisted local store.
from llama_index.core import VectorStoreIndex
index = VectorStoreIndex.from_documents(documents)
print("Index built")
Expected output:
Index built
Implement find similar products
The core of our find similar products llamaindex implementation is a retriever that takes a product ID, looks up its description, and fetches the top k nearest vectors. We exclude the product itself from results.
def find_similar(product_id: str, k: int = 3):
# Find the source product description
src = next(p for p in products if p["id"] == product_id)
retriever = index.as_retriever(similarity_top_k=k + 1)
nodes = retriever.retrieve(src["description"])
results = []
for node in nodes:
meta = node.metadata
if meta["id"] == product_id:
continue # skip self
results.append((meta["id"], meta["name"], node.score))
if len(results) == k:
break
return results
print(find_similar("p1"))
Expected output (scores are cosine similarities, exact values vary):
[('p2', 'Court Classic tennis shoes', 0.92), ('p3', 'Alpine hiking boots', 0.88), ('p4', 'Yoga mat pro', 0.71)]
The sneakers cluster together; the yoga mat is a weaker match. That’s the find similar products llamaindex behavior we want.
Constrain by category with metadata filters
Often you want similar items only within the same category. LlamaIndex supports MetadataFilters on the retriever.
from llama_index.core.vector_stores import MetadataFilters, FilterCondition
from llama_index.core import VectorStoreIndex
def find_similar_in_category(product_id: str, k: int = 3):
src = next(p for p in products if p["id"] == product_id)
filters = MetadataFilters.from_dicts(
[{"key": "category", "value": src["category"]}],
condition=FilterCondition.AND,
)
retriever = index.as_retriever(similarity_top_k=k + 1, filters=filters)
nodes = retriever.retrieve(src["description"])
results = []
for node in nodes:
if node.metadata["id"] == product_id:
continue
results.append(node.metadata["name"])
if len(results) == k:
break
return results
print(find_similar_in_category("p1"))
Expected output:
['Court Classic tennis shoes', 'Alpine hiking boots']
Only footwear appears, even though the unfiltered query pulled a fitness item.
Persist the index for reuse
Re-embedding the catalog on every request is wasteful. Persist the index to disk and reload it in your API process.
index.storage_context.persist(persist_dir="./product_index")
# Later, in a fresh process:
from llama_index.core import StorageContext, load_index_from_storage
storage_context = StorageContext.from_defaults(persist_dir="./product_index")
reloaded_index = load_index_from_storage(storage_context)
print("Index reloaded:", reloaded_index.docstore.docs.keys())
Expected output:
Index reloaded: dict_keys(['p1', 'p2', 'p3', 'p4', 'p5'])
Wire into a service
A minimal FastAPI endpoint looks like this:
from fastapi import FastAPI
app = FastAPI()
@app.get("/similar/{product_id}")
def similar(product_id: str, k: int = 3):
return {"matches": find_similar(product_id, k)}
Run with uvicorn main:app and hit /similar/p1?k=2. The find similar products llamaindex logic stays unchanged; you’ve just wrapped it in HTTP.
Notes on scaling
For catalogs beyond a few thousand SKUs, use a dedicated vector database instead of the default in-memory store. LlamaIndex supports ChromaVectorStore, PineconeVectorStore, and others via the same VectorStoreIndex API. Keep metadata like category and price on each node so you can apply filters server-side before the similarity scan. Embedding cost is per token; cache descriptions and only re-embed on product updates.