n4nAI

Building a find-similar-products feature with LlamaIndex

Hands-on tutorial: implement a find similar products LlamaIndex feature for e-commerce using vector embeddings, metadata filters, and persistence.

n4n Team2 min read483 words

Audio narration

Coming soon — every post will get a voice note here.

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)
  • openai Python 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.

Tagsllamaindexecommercesimilarity-search

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All framework tutorials: e-commerce search & recommendations posts →