AI Engineering: Build AI-Powered Apps
ConceptIntermediate 25 min· RAG: AI over your own data · 6 of 19

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 AVector BCosine similarityMeaning
[1, 0][1, 0]1.0Same direction
[1, 0][0.8, 0.6]0.8Close
[1, 0][0, 1]0.0Unrelated
[1, 0][-1, 0]-1.0Opposite

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.

Terminal
pip install -U google-genai numpy python-dotenv
embed_util.py
import 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 normalising

A reusable helper. It returns one normalised vector per text.

Example output (your numbers will differ)
(1, 768)
[ 0.012 -0.034  0.008  0.051 -0.019]
1.0

Those five numbers mean nothing alone. The power appears when you compare vectors.

Build a semantic search

semantic_search.py
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.

Typical result (scores are illustrative; yours will differ)
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

DecisionGuidance
Which modelStart with your provider's default text embedding model; choose a multilingual one if your users write in Hindi, Tamil or mixed languages (Hinglish)
DimensionsMore 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 typeIf the model supports it, say what you are doing (document, query, similarity, classification); mismatched types lower quality
Text lengthModels have an input limit; split long documents into chunks (see Chunking, hybrid search and re-ranking)
UpdatingChanging 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 atWeak at
Matching meaning, paraphrases and synonymsExact 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 duplicatesNegation: "coffee with milk" and "coffee without milk" can look very close
Recommending related contentTelling 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).

Prompt · Explain a similarity result
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

  1. An embedding is a vector of numbers that represents the meaning of text; close vectors mean similar meaning.
  2. Cosine similarity (or the dot product of normalised vectors) measures closeness.
  3. Embed documents once and store them; embed only the query at search time.
  4. Never mix vectors from different models or sizes; store the model name with your data.
  5. Embeddings find meaning but are weak at exact codes, numbers and negation, so combine them with keyword search when needed.
  6. 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.

Start free