Embeddings and semantic search
Embeddings turn text into vectors so a computer can compare meaning, not just words. Learn what they are, how cosine similarity works and build a small semantic search over menu items with the Gemini API.
Search that understands meaning is the first half of RAG (retrieval-augmented generation). A customer types "something cold and strong, not sweet", and a keyword search finds nothing because the menu says "Cold Brew: slow-steeped, bold, unsweetened". Embeddings let the computer see that these mean nearly the same thing. In this lesson you will learn what an embedding is, how similarity is measured, and how to build a small semantic search in about 30 lines of Python.
What is an embedding?
An embedding is a list of numbers (a vector) that represents the meaning of a piece of text. An embedding model reads your text and outputs, for example, 768 numbers. Texts with similar meaning get vectors that point in similar directions; unrelated texts get vectors that point elsewhere.
Think of a map. On a map of India, each city has two numbers, latitude and longitude, and nearby numbers mean nearby places. An embedding is a map of meaning with hundreds or thousands of directions instead of two. "Cold Brew" and "iced coffee" are neighbours; "Cold Brew" and "income tax" are far apart.
Important facts:
- The numbers have no meaning one by one. Only the distance between vectors matters. - Every text, short or long, becomes a vector of the same length for a given model. - Vectors from different models cannot be compared. If you change the model, you must re-embed everything.
Measuring closeness: cosine similarity
The standard measure is cosine similarity: the cosine of the angle between two vectors. It ranges from -1 to 1. Vectors pointing the same way score near 1; unrelated ones score near 0 (in practice, many text pairs sit well above 0, so only compare scores against each other); opposite ones score near -1. It ignores vector length and looks only at direction.
Cosine similarity with two-number vectors
| Vector A | Vector B | Cosine similarity | Meaning |
|---|---|---|---|
| [1, 0] | [1, 0] | 1.0 | Same direction |
| [1, 0] | [0.8, 0.6] | 0.8 | Close |
| [1, 0] | [0, 1] | 0.0 | Unrelated |
| [1, 0] | [-1, 0] | -1.0 | Opposite |
Real embeddings work the same way, just with hundreds of numbers. The formula is the dot product of the two vectors divided by the product of their lengths. If vectors are already normalised (length 1), cosine similarity is just the dot product, which is why many systems normalise once and then use fast dot products.
Embed some text
Gemini offers embedding models through the same SDK. You pass a list of texts and get one vector per text. Model names change, so check the embeddings page in the Gemini API docs. At the time of writing (2026), gemini-embedding-001 is a text embedding model that lets you choose the output size and a task type, and a newer multimodal model, gemini-embedding-2, exists too (it works a little differently, for example without task types). The code below uses gemini-embedding-001.
pip install -U google-genai numpy python-dotenvimport numpy as np
from dotenv import load_dotenv
from google import genai
from google.genai import types
load_dotenv()
client = genai.Client()
EMBED_MODEL = "gemini-embedding-001" # check the docs for the current embedding model
DIMS = 768
def embed(texts: list[str], task_type: str) -> np.ndarray:
result = client.models.embed_content(
model=EMBED_MODEL,
contents=texts,
config=types.EmbedContentConfig(task_type=task_type, output_dimensionality=DIMS),
)
vectors = np.array([e.values for e in result.embeddings], dtype=np.float32)
# Smaller-than-default sizes are not normalised for this model, so do it ourselves
vectors /= np.linalg.norm(vectors, axis=1, keepdims=True)
return vectors
if __name__ == "__main__":
v = embed(["Cold Brew: slow-steeped, bold and unsweetened"], "RETRIEVAL_DOCUMENT")
print(v.shape) # (1, 768)
print(v[0][:5]) # the first five of 768 numbers
print(np.linalg.norm(v[0])) # about 1.0 after normalisingA reusable helper. It returns one normalised vector per text.
(1, 768)
[ 0.012 -0.034 0.008 0.051 -0.019]
1.0Those five numbers mean nothing alone. The power appears when you compare vectors.
Build a semantic search
import numpy as np
from embed_util import embed
DOCS = [
"Espresso: a short, intense shot of coffee.",
"Cappuccino: espresso with steamed milk and a thick layer of foam.",
"Cold Brew: coffee steeped slowly in cold water, smooth, bold and unsweetened.",
"Filter Coffee: South Indian style coffee with hot milk, served in a steel tumbler.",
"Masala Chai: black tea simmered with milk and spices like ginger and cardamom.",
"Butter Croissant: flaky, buttery pastry baked fresh every morning.",
"Banana Bread: moist, lightly sweet loaf made with ripe bananas.",
"Veg Grilled Sandwich: grilled bread with vegetables and cheese.",
"Opening hours: we are open from 8 am to 10 pm every day.",
]
doc_vectors = embed(DOCS, "RETRIEVAL_DOCUMENT") # embed once, reuse many times
def search(query: str, k: int = 3):
q = embed([query], "RETRIEVAL_QUERY")[0]
scores = doc_vectors @ q # dot product = cosine similarity (normalised)
top = np.argsort(scores)[::-1][:k] # indexes of the k highest scores
return [(DOCS[i], float(scores[i])) for i in top]
for query in ["something cold and strong, not sweet", "a snack for the morning", "when do you close?"]:
print("\nQuery:", query)
for text, score in search(query):
print(f" {score:.2f} {text}")Embeds the documents once, embeds each query, and ranks by dot product. RETRIEVAL_DOCUMENT and RETRIEVAL_QUERY tell the model which side each text is on.
Query: something cold and strong, not sweet
0.71 Cold Brew: coffee steeped slowly in cold water, smooth, bold and unsweetened.
0.59 Espresso: a short, intense shot of coffee.
...
Query: when do you close?
0.66 Opening hours: we are open from 8 am to 10 pm every day.The first query shares almost no words with the Cold Brew line, yet it ranks first. A keyword search could not do that. The last query finds the opening hours even though the word "close" is not in that sentence.
Choosing and using embeddings well
Practical choices
| Decision | Guidance |
|---|---|
| Which model | Start with your provider's default text embedding model; choose a multilingual one if your users write in Hindi, Tamil or mixed languages (Hinglish) |
| Dimensions | More dimensions can capture more detail but cost more storage and search time; many models let you shorten the vector (768 is a common balance) |
| Task type | If the model supports it, say what you are doing (document, query, similarity, classification); mismatched types lower quality |
| Text length | Models have an input limit; split long documents into chunks (see Chunking, hybrid search and re-ranking) |
| Updating | Changing the model means re-embedding all documents; store the model name next to your vectors |
Embed once, search many times
Embedding the documents is the expensive step; embedding a query is cheap. Compute document vectors once, save them (a file or a vector database, covered next), and only embed new queries at run time.
What embeddings are good and bad at
Strengths and limits
| Good at | Weak at |
|---|---|
| Matching meaning, paraphrases and synonyms | Exact codes, IDs and rare names ("order 4821") |
| Search across languages (with a multilingual model) | Precise facts such as numbers and dates |
| Clustering similar items, finding duplicates | Negation: "coffee with milk" and "coffee without milk" can look very close |
| Recommending related content | Telling you *why* something matched |
Because of these limits, strong search systems often combine embeddings with keyword search and re-ranking. That is covered in the chunking lesson. Other uses of embeddings: removing near-duplicate questions in a forum, grouping customer feedback by topic, and classifying text by finding the nearest labelled example.
Common mistakes
Do not compare embeddings from two different models, or from the same model with different output sizes. The numbers live in different spaces and the scores are meaningless. Also, a high score does not prove the text answers the question; it only means the topics are close.
How this comes up in interviews
Typical questions: "What is an embedding?", "Why cosine similarity instead of Euclidean distance?" (direction matters more than length; with normalised vectors the rankings are the same), "How would you build semantic search?" (chunk, embed, store, embed the query, take top-k by similarity), and "Why might semantic search miss an exact product code?" (embeddings capture meaning, not exact tokens, so add keyword search).
I built semantic search with text embeddings. For the query "[query]" my top 3 results were:
1. [score] [text]
2. [score] [text]
3. [score] [text]
The result I expected was: [expected text]. Suggest the most likely reasons it was not ranked first (chunk size, wording, model choice, missing keyword match), and give me 3 experiments to try, cheapest first.What to expect: A useful answer lists concrete, testable changes, such as rewriting the document text, adding keyword search or trying a multilingual model.
Try it: Test the limits
Add 5 more documents to semantic_search.py (for example, a refund policy, a Wi-Fi note, a vegan option and a sentence in Hinglish). Then write 6 queries: 3 that paraphrase a document, 1 with an exact word match, 1 negation ("coffee without milk") and 1 in Hindi or Hinglish. Record the top result for each and mark right or wrong.
Key takeaways
- An embedding is a vector of numbers that represents the meaning of text; close vectors mean similar meaning.
- Cosine similarity (or the dot product of normalised vectors) measures closeness.
- Embed documents once and store them; embed only the query at search time.
- Never mix vectors from different models or sizes; store the model name with your data.
- Embeddings find meaning but are weak at exact codes, numbers and negation, so combine them with keyword search when needed.
- Semantic search is: chunk, embed, store, embed the query, return the top-k most similar.
Check yourself
Finishing the quiz marks the lesson as done.
1. What does it mean when two text embeddings have a cosine similarity near 1?
2. You switch to a different embedding model. What must you do with your stored document vectors?
3. Why does a semantic search for "something cold and strong" find "Cold Brew: bold and unsweetened"?
4. Which query is semantic search most likely to get wrong?
5. Why embed documents once and store the vectors?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.