Embeddings Explained Simply: How AI Finds Similar Meaning
Generative AI course · Chapter 37 · Estimated reading time: 13 minutes
Type "Can I pay with UPI?" into a shop's help page, and a normal search looks for the word "UPI". If the saved question says "Which payment methods do you accept?", the words do not match and the search finds nothing, even though a person can see it is the same question. AI solves this with embeddings: it turns each piece of text into a list of numbers so that texts with similar meaning get similar numbers. Embeddings are behind smart search, duplicate finding, recommendations and the chatbots you will build in the next chapter. In this chapter you will see real embeddings, measure how close two meanings are, and build 3 small tools with free code that runs on a laptop.
What you will learn in this chapter
- Why keyword search misses questions with the same meaning
- What an embedding is: text turned into a list of numbers
- Cosine similarity: a score for how close two meanings are
- Build a similar-FAQ finder
- Find duplicate customer complaints
- Build a tiny meaning search box
- How to choose a threshold, and where embeddings go wrong
1. Suppose a shop's help page keeps failing
Suppose our friend Sunita runs an online kitchenware shop. She has 5 saved answers on her help page. Customers type their own questions, and the keyword search fails all the time:
- "Can I pay with UPI?" finds nothing, because the saved question is "Which payment methods do you accept?".
- "where is my package" finds nothing, because the help article says "Track your parcel".
The customer gives up and calls her. Sunita does not need more answers. She needs a search that understands that "package" and "parcel" mean the same here, and that "pay with UPI" is a question about payment methods.
2. What an embedding is
An embedding is a list of numbers that represents the meaning of a piece of text. An embedding model is a special AI model that makes these lists.
Think of a map. Every city has 2 numbers: latitude and longitude. Pune and Mumbai have close numbers because they are close on the map. An embedding is like a map position for meaning, but with hundreds of numbers instead of 2. Texts with similar meanings land close together on this map.
- "Which payment methods do you accept?" and "Can I pay with UPI?" land close.
- "Can I pay with UPI?" and "What are your shop timings?" land far apart.
From text to a similarity score in five steps
How close? We use cosine similarity, a score that compares the direction of two lists of numbers. It can range from -1 to 1. In practice, for text, a higher score means more similar meaning. You do not need the maths; you need to know that bigger means closer.
3. Which embedding model to use
| Model | Where it runs | Notes (checked 19 October 2026) |
|---|---|---|
| BAAI/bge-small-en-v1.5 | On your laptop, free, via the fastembed Python package |
384 numbers per text; English; good for learning |
| gemini-embedding-2 (Google) | Gemini API | Text, images and more; 3,072 numbers by default (768 or 1,536 also recommended); up to 8,192 input tokens |
| gemini-embedding-001 (Google) | Gemini API | Text only |
We use the small free model in this chapter so you can run everything without an API key:
pip install fastembed numpy
The first run downloads the model once (it is small). After that, it works offline.
To use Gemini's model instead, the call looks like this (with your key in .env, as in the previous chapter):
from google import genai
client = genai.Client()
result = client.models.embed_content(model="gemini-embedding-2",
contents="Can I pay with UPI?")
Ravindra Bagale's Tip
The mistake: comparing scores from two different embedding models, or embedding the questions with one model and the answers with another. Each model draws its own map, so the numbers do not match. Pick one model for a project, embed everything with it, and if you ever change the model, embed everything again.
4. Worked example 1: a similar-FAQ finder
# Similar FAQ finder: which saved question means the same as the new one?
import numpy as np
from fastembed import TextEmbedding
faqs = [
"What are your shop timings?",
"Do you deliver to my area?",
"How can I return a damaged product?",
"Which payment methods do you accept?",
"Is there a discount for bulk orders?",
]
new_question = "Can I pay with UPI?"
model = TextEmbedding("BAAI/bge-small-en-v1.5") # small free model that runs on a laptop
faq_vecs = np.array(list(model.embed(faqs)))
q_vec = np.array(list(model.embed([new_question])))[0]
print("Each text became", len(q_vec), "numbers")
def cosine(a, b):
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
scores = [(cosine(q_vec, v), f) for v, f in zip(faq_vecs, faqs)]
for s, f in sorted(scores, reverse=True):
print(f"{s:.2f} {f}")
Real output (we ran this on 19 October 2026):
Each text became 384 numbers
0.73 Which payment methods do you accept?
0.64 Do you deliver to my area?
0.57 Is there a discount for bulk orders?
0.54 What are your shop timings?
0.53 How can I return a damaged product?
What to notice:
- The right FAQ is on top, even though it shares no important word with "Can I pay with UPI?".
- Every score is above 0.5. With this model, unrelated shop questions still score around 0.5, because they are all short questions about a shop. So do not read a score alone; look at the order and the gap between the first and the second.
5. Worked example 2: a duplicate complaint finder
Suppose Sunita's team logs complaints from email, WhatsApp and phone. The same problem gets logged in different words, and two people work on it twice.
# Duplicate complaint finder: flag pairs of complaints that mean the same thing.
import numpy as np
from fastembed import TextEmbedding
complaints = [
"My order arrived two days late.",
"Delivery was delayed by 2 days.",
"The mixer jar was cracked when I opened the box.",
"I was charged twice for one order.",
"Received a broken jar in the package.",
"Money got debited two times for the same purchase.",
]
model = TextEmbedding("BAAI/bge-small-en-v1.5")
v = np.array(list(model.embed(complaints)))
v = v / np.linalg.norm(v, axis=1, keepdims=True) # make every vector length 1
sim = v @ v.T # cosine similarity of every pair
THRESHOLD = 0.80
for i in range(len(complaints)):
for j in range(i + 1, len(complaints)):
if sim[i, j] >= THRESHOLD:
print(f"{sim[i, j]:.2f} #{i+1} and #{j+1}: possible duplicate")
print("Highest score below the line:",
round(max(sim[i, j] for i in range(6) for j in range(i + 1, 6) if sim[i, j] < THRESHOLD), 2))
Real output (19 October 2026):
0.87 #1 and #2: possible duplicate
0.84 #3 and #5: possible duplicate
0.81 #4 and #6: possible duplicate
Highest score below the line: 0.72
All 3 real pairs were found: late delivery, broken jar and double charge. "Charged twice" and "money got debited two times" share almost no words, yet the score is 0.81.
How we chose 0.80: we looked at the scores. Real duplicates were 0.81 to 0.87; the closest non-duplicate pair was 0.72. A line between them works for this data. For your own data, label 20 to 30 pairs by hand, look at their scores, and pick the line from those. Then a person checks every flagged pair; the tool only says "possible duplicate".
6. Worked example 3: a tiny meaning search box
# A tiny meaning search box over help articles.
import numpy as np
from fastembed import TextEmbedding
articles = [
"How to reset your account password",
"Track your parcel after you place an order",
"Change the delivery address of an order",
"Cancel an order and get a refund",
"Update your mobile number on your account",
]
model = TextEmbedding("BAAI/bge-small-en-v1.5")
doc = np.array(list(model.embed(articles)))
doc = doc / np.linalg.norm(doc, axis=1, keepdims=True)
def search(query, top=2):
q = np.array(list(model.embed([query])))[0]
q = q / np.linalg.norm(q)
scores = doc @ q
best = np.argsort(-scores)[:top]
print(f"Search: {query}")
for k in best:
print(f" {scores[k]:.2f} {articles[k]}")
for query in ["I forgot my login", "where is my package", "I want my money back"]:
search(query)
Real output (19 October 2026):
Search: I forgot my login
0.78 How to reset your account password
0.60 Update your mobile number on your account
Search: where is my package
0.71 Track your parcel after you place an order
0.68 Change the delivery address of an order
Search: I want my money back
0.75 Cancel an order and get a refund
0.54 Update your mobile number on your account
All 3 searches found the right article, with no shared keywords: login and password, package and parcel, money back and refund.
But look at "where is my package": the second result scored 0.68, very close to the first (0.71). Both are about orders and delivery. That is why a search box shows the top 2 or 3 results, not only one.
Important for bigger projects: we embedded the articles once and reused the numbers for every search. For thousands of documents, you save these numbers in a vector database, a database built to find the closest lists of numbers fast. That is the next step after this chapter.
7. Where embeddings go wrong
- Opposite meanings can score high. We tested "The delivery was fast." against "The delivery was not fast." with the same model on 19 October 2026: the score was 0.87, higher than our real duplicates. Against an unrelated question it was 0.55. Embeddings measure "about the same thing" much better than "agree or disagree".
- Exact codes and numbers: for an order ID like
ORD-DUMMY-1042, a normal keyword search is better. Many real systems use both. - Language: our small model is English only. For Hindi or Marathi text, choose a multilingual model.
- Very long text: a whole book in one embedding loses detail. Cut long documents into chunks first (the next chapter shows how).
8. Beginner mistakes and quick fixes
| Mistake | What happens | Quick fix |
|---|---|---|
| Reading one score alone | 0.55 looks "similar" but is not | Compare the order and the gap between results |
| Mixing two embedding models | Scores make no sense | One model per project; re-embed after a change |
| A threshold from a blog post | Too many or too few matches | Label 20 to 30 pairs of your own data and choose |
| Embedding every document on every search | Slow and costly | Embed once, save the numbers |
| Using embeddings for order IDs | Wrong matches | Use keyword search for exact codes |
| Auto-deleting "duplicates" | Real complaints lost | Flag them; a person decides |
9. Try at home
Try at home
Task 1: your own FAQ finder
- Write 6 FAQs for a business you know.
- Write 3 new questions in different words.
- Run the FAQ finder and check that the right FAQ is on top.
Task 2: choose a threshold
- Write 10 complaints with 3 duplicate pairs hidden inside.
- Print every pair's score.
- Pick a threshold that catches all 3 pairs and nothing else.
Task 3: find a weak spot
- Embed "The food was good" and "The food was not good".
- Note the score.
- Write one line on why a review sorter should not rely on embeddings alone.
Recap
In short
- Keyword search misses questions with the same meaning in different words.
- An embedding turns text into a list of numbers so that similar meanings get similar numbers.
- Cosine similarity scores how close two embeddings are; higher means closer.
- A free model like BAAI/bge-small-en-v1.5 runs on a laptop; gemini-embedding-2 is Google's API model.
- Look at the order and gaps between scores, not one score alone.
- Choose a threshold from your own labelled examples, and let a person check matches.
- Embed documents once and save the numbers; a vector database does this at scale.
- Embeddings are weak on exact codes and on agree-or-disagree; combine them with keyword search when needed.
Run the FAQ finder on 6 questions from your own work this week. Once you see meaning turn into numbers, the chatbot in the next chapter will make complete sense.
Previous chapter: Your First OpenAI and Gemini API Call in Python (Free Key Setup)
Next chapter: What Is RAG? Build a Chatbot from Your Own PDF (Step by Step)
Ravindra Bagale, trainer: linkedin.com/in/ravindra-bagale. Sunita, her shop, the FAQs and complaints on this page are made up for learning, and the order ID is a dummy. Gemini embedding model details were checked on Google's Gemini API embeddings page on 19 October 2026. The Python output was produced on 19 October 2026 with fastembed and the BAAI/bge-small-en-v1.5 model. Your scores may differ slightly with other versions.