GenAI: Embeddings & Semantic Search
A ShopKart shopper types "comfortable shoes for the monsoon" into search. Keyword search fails them — no product description contains that exact phrase, and the best match (waterproof running shoes) never mentions "monsoon". This is where GenAI embeddings change the game: they let the computer match on meaning, not words. This lesson is your entry into GenAI, and it reuses the cosine-similarity idea from the recommender — just with far smarter vectors.
What is an embedding?
An embedding is a list of numbers (a vector, often 384 or 1536 dimensions) that represents the meaning of a piece of text. A neural model trained on huge amounts of text produces them, and the magic is this: texts with similar meaning get similar vectors, even if they share no words. "Monsoon" and "waterproof" land near each other; "earbuds" and "headphones" land near each other; "earbuds" and "frying pan" land far apart.
TF-IDF (Lesson 5) only knew about exact word overlap. Embeddings understand that different words can mean the same thing — that's the leap.
Generating embeddings
Two easy options. sentence-transformers runs locally and free — great for learning and for privacy. OpenAI embeddings are a hosted API call. Both return vectors you use identically.
Use Option A for this lesson. It needs no account and no API key, and every result shown below comes from it. Option B is there so you recognise the hosted equivalent when you meet it — you don't need to run it.Option A — local, with sentence-transformers:
> Need the data? This lesson loads the ShopKart CSVs. Download orders.csv, customers.csv and products.csv into the same folder as your notebook first — Lesson 1 covers the setup.
import pandas as pd
from sentence_transformers import SentenceTransformer
products = pd.read_csv("products.csv")
products["text"] = products["name"] + ". " + products["description"].fillna("")
# Small, fast, 384-dimensional model.
# First run downloads it (~90 MB) and caches it in ~/.cache; later runs are instant.
model = SentenceTransformer("all-MiniLM-L6-v2")
embeddings = model.encode(products["text"].tolist(),
normalize_embeddings=True,
show_progress_bar=True)
print(embeddings.shape) # (n_products, 384)
(600, 384)
Option B — hosted, with OpenAI. Reference only — this one needs a paid API key, so don't run it unless you have one. Note that OpenAI() raises immediately if OPENAI_API_KEY isn't set, which is why the whole block is commented out here:
# Requires OPENAI_API_KEY in your environment — see Lesson 8 for setup.
# from openai import OpenAI
# client = OpenAI() # reads OPENAI_API_KEY from the environment
#
# def embed(texts):
# resp = client.embeddings.create(model="text-embedding-3-small", input=texts)
# return [d.embedding for d in resp.data]
#
# embeddings = embed(products["text"].tolist()) # 1536-D vectors
The trade-off in one line: the local model is free, private and 384-dimensional; the hosted one is stronger and 1,536-dimensional but costs per call and sends your catalog to a third party.
Each product is now a point in a 384- (or 1536-) dimensional meaning-space. normalize_embeddings=True makes the vectors unit-length, which simplifies similarity (a normalised dot product is cosine similarity).
Semantic search
Now the payoff. To search, embed the query with the same model and find the products whose vectors are closest. Same cosine-similarity machinery as the recommender — but because the vectors encode meaning, it matches intent.
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
def search(query, k=5):
q = model.encode([query], normalize_embeddings=True)
scores = cosine_similarity(q, embeddings)[0]
top = np.argsort(scores)[::-1][:k]
out = products.iloc[top][["product_id", "name", "category"]].copy()
out["score"] = np.round(scores[top], 3)
return out
search("comfortable shoes for the monsoon")
product_id name category score
PRD078 Woodland Sports Sandals Fashion 0.563
PRD233 Campus Waterproof Running Shoes Sports 0.561
PRD199 Woodland Leather Trekking Boots Fashion 0.548
PRD139 Woodland Sports Sandals Lite M240 Fashion 0.548
PRD210 Sparx Anti-Skid Rain Sandals Fashion 0.524
None of those products literally say "monsoon", yet the model ranked waterproof, anti-skid and rain-ready footwear at the top — it understood the intent behind the words. That's semantic search, and it's impossible with keyword matching alone.
Be honest about the ranking, though. The top five are separated by only 0.04, and the ordering among them is close to arbitrary — a plain leather trekking boot (0.548) edges out the rain sandals actually designed for monsoon use (0.524). Embeddings capture topic ("outdoor footwear") far more reliably than they capture fine distinctions ("waterproof, specifically"). This is why production search systems usually add a re-ranking step, or filter on real attributes, rather than trusting raw vector order.
Try a query with no matching keywords at all:
search("gift for someone who loves cooking")
product_id name category score
PRD060 HarperCollins Cookbook Books 0.417
PRD140 Prestige Non-Stick Cookware Combo Home & Kitchen 0.396
PRD137 Wonderchef Steel Lunch Box Plus Home & Kitchen 0.368
PRD094 Rupa Cookbook Classic Books 0.367
PRD160 Borosil Glass Mixing Bowl Set Home & Kitchen 0.364
"Gift" and "loves cooking" pulled up cookware and cookbooks — the model connected the intent across two different categories with barely any keyword overlap. A category filter on "Home & Kitchen" would have missed the cookbooks entirely.
Notice the scores are much lower here (0.42 at the top versus 0.56 for the shoes). Cosine scores are relative, not absolute — there's no universal threshold above which a match is "good". A cutoff tuned on one kind of query will quietly misbehave on another.
Scaling up: vector indexes
For 600 products, comparing against every embedding (a "brute-force" search) is instant. For millions, you'd use a vector database or index like FAISS, which finds nearest neighbours in milliseconds.
import faiss
import numpy as np
emb = np.asarray(embeddings, dtype="float32")
index = faiss.IndexFlatIP(emb.shape[1]) # inner product = cosine on normalised vectors
index.add(emb)
q = model.encode(["comfortable shoes for the monsoon"], normalize_embeddings=True)
scores, ids = index.search(np.asarray(q, dtype="float32"), k=5)
print(ids[0]) # row indices of the top-5 products
[ 77 232 198 138 209]
Those are row indices, not product IDs — index 77 is the row holding PRD078. They match the five products from the brute-force search above, in the same order. FAISS returns the same neighbours, just far faster at scale. The concept is unchanged — nearest vectors in meaning-space.
Embeddings power more than search
The same product embeddings can drive:
- Semantic search (what we just built).
- Recommendations — a smarter version of Lesson 5, using meaning instead of word counts.
- Clustering — group products by theme automatically.
- RAG — retrieving relevant context to feed a language model, which is exactly the next lesson.
On the job
Embeddings are the foundation of virtually every modern GenAI application — search, recommendations, deduplication, classification, and the retrieval step in RAG systems. The pattern you learned is the whole game: embed your content once, embed the query, compare with cosine similarity. Knowing when to run models locally (sentence-transformers — free, private) versus calling a hosted API (OpenAI — more powerful, costs per call), and when to graduate from brute-force to a vector index like FAISS, is exactly the kind of practical judgement teams expect. In the final lesson we take these retrieved products and hand them to a large language model to answer shopper questions in natural language.
Key takeaways
- An embedding is a vector capturing a text's meaning; similar meanings get nearby vectors.
- Generate them locally with sentence-transformers or via OpenAI's embeddings API — both give you vectors.
- Semantic search = embed the query, cosine-compare against product embeddings, return the closest — it matches intent, not keywords.
- Use FAISS or a vector database to scale nearest-neighbour search to millions of items.
- Embeddings underpin search, recommendations, clustering, and the retrieval step of RAG.