Skip to content
Laviyx Learn

Try

Machine Learning course menu

Embeddings and Semantic Search Explained (with Python)

Understand embeddings: representing words and documents as vectors, cosine similarity, building a small semantic search and recommendation engine, sentence embedding models, vector databases, and retrieval-augmented generation (RAG).

Advanced8 min readUpdated
On this page

An embedding is a list of numbers (a vector) that represents a piece of text, an image or a product, arranged so that similar things get similar vectors. Embeddings power semantic search, recommendations, duplicate detection, clustering of documents, and the retrieval step of AI assistants that answer questions from your documents.

Similarity between vectors#

The standard way to compare embeddings is cosine similarity: the cosine of the angle between two vectors. 1 means the same direction (very similar), 0 means unrelated.

Python
import numpy as np

def cosine(a, b):
    return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))

# made-up 3-number "embeddings" for illustration: [animal, vehicle, size]
cat = np.array([0.9, 0.0, 0.2])
dog = np.array([0.8, 0.1, 0.4])
truck = np.array([0.0, 0.9, 0.9])
print("cat vs dog:", round(cosine(cat, dog), 3))
print("cat vs truck:", round(cosine(cat, truck), 3))
Output
cat vs dog: 0.964
cat vs truck: 0.153

Real embedding models produce vectors with hundreds or thousands of numbers, learned from huge amounts of text. Individual numbers do not have readable meanings like "animal"; only the distances between vectors matter.

Building document vectors from scratch#

Real sentence embedding models are large neural networks. To see the idea with tools you already know, compress TF-IDF vectors with truncated SVD (a PCA-like method for sparse data, known as latent semantic analysis). Words that appear in similar contexts end up in similar directions:

Python
import numpy as np
from sklearn.decomposition import TruncatedSVD
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import Normalizer

docs = [
    "How do I reset my account password?", "I forgot my password and cannot log in to my account",
    "Change the login password for my account", "What are your delivery charges for an order?",
    "How much does delivery cost for my order?", "Delivery fee for orders outside the city",
    "Can I return a damaged product for a refund?", "Refund for a damaged or broken item", "Return policy and refund for broken products",
]
embed = make_pipeline(TfidfVectorizer(stop_words="english"), TruncatedSVD(n_components=3, random_state=0), Normalizer())
vectors = embed.fit_transform(docs)
print("embedding shape:", vectors.shape)

similarity = vectors @ vectors.T      # rows are normalised, so this is cosine similarity
np.set_printoptions(precision=2, suppress=True)
print(similarity[[0, 3, 6]][:, [1, 4, 7]])
Output
embedding shape: (9, 3)
[[ 1. -0. -0.]
 [-0.  1.  0.]
 [ 0.  0.  1.]]

Each document is now 3 numbers. The rows are a password, a delivery and a returns question; the columns are another question from each topic. The 1s sit on the diagonal: each question matches the one on the same topic and not the others. With only three numbers per document and three topics, each topic collapses to its own direction, so similarities come out as exactly 1 or 0 (-0. is just a tiny negative number rounded). Real embeddings, with hundreds of dimensions, give graded similarities instead.

Search = embed the query with the same model, then return the documents with the highest similarity:

Python
import numpy as np
from sklearn.decomposition import TruncatedSVD
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import Normalizer

docs = [
    "How do I reset my account password?", "I forgot my password and cannot log in to my account",
    "Change the login password for my account", "What are your delivery charges for an order?",
    "How much does delivery cost for my order?", "Delivery fee for orders outside the city",
    "Can I return a damaged product for a refund?", "Refund for a damaged or broken item", "Return policy and refund for broken products",
]
embed = make_pipeline(TfidfVectorizer(stop_words="english"), TruncatedSVD(n_components=3, random_state=0), Normalizer())
doc_vectors = embed.fit_transform(docs)

def search(query, k=2):
    q = embed.transform([query])[0]
    scores = doc_vectors @ q
    return [(round(float(scores[i]), 2), docs[i]) for i in np.argsort(scores)[::-1][:k]]

print(search("my item arrived broken, I want a refund"))
print(search("cost of delivery to my city"))
print(search("parcel arrived late"))
Output
[(1.0, 'Return policy and refund for broken products'), (1.0, 'Refund for a damaged or broken item')]
[(1.0, 'Delivery fee for orders outside the city'), (1.0, 'How much does delivery cost for my order?')]
[(0.0, 'Return policy and refund for broken products'), (0.0, 'Refund for a damaged or broken item')]

The first two queries find the right topic even though their wording differs from the documents. The third finds nothing (every score is 0.0): this tiny model only knows words from its nine documents, and "parcel" and "late" are not among them. A pretrained embedding model knows that "parcel" relates to "delivery" and "faulty" to "broken" even when those words never appear together in your data, which is what makes real semantic search work.

Pretrained embedding models#

In practice you call a pretrained model, either an open-source library or a hosted API:

Python
# requires: pip install sentence-transformers  (downloads the model on first use; not run here)
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")
vectors = model.encode(["How do I reset my password?", "I forgot my login details"],
                       normalize_embeddings=True)
print(vectors.shape)            # (2, 384): each sentence becomes 384 numbers
print(vectors[0] @ vectors[1])  # cosine similarity

This snippet needs a model download, so it was not run for this lesson; the shape comment comes from the model's published size of 384 dimensions.

Vector databases#

Comparing a query with millions of vectors one by one is slow. Vector databases and libraries (FAISS, pgvector for PostgreSQL, Chroma, and hosted services) store embeddings and find the nearest ones quickly using approximate nearest-neighbour indexes. Conceptually it is the same as k-nearest neighbours search, made fast at scale.

Retrieval-augmented generation (RAG)#

AI assistants that answer from your own documents usually work like this:

  1. Split documents into chunks and embed each chunk; store them in a vector database.
  2. Embed the user's question and retrieve the most similar chunks.
  3. Send the question plus those chunks to a large language model and ask it to answer using only that context.

This lets a model answer from up-to-date, private information without retraining, and makes it possible to show which sources an answer came from. The next lesson shows how to call an LLM from Python.

Common mistakes#

Practice#

Practice exercise

Using the cosine function from the start of this lesson, find which of these "product embeddings" is most similar to the phone [0.9, 0.1, 0.3]: laptop [0.8, 0.2, 0.5], sofa [0.1, 0.9, 0.4], tablet [0.85, 0.05, 0.35]. Print the similarities sorted from most to least similar.

Show hint

Store the products in a dictionary, compute the cosine with the phone for each, and sort the items by score with reverse=True.

Show solution
Python
import numpy as np

def cosine(a, b):
    return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))

phone = np.array([0.9, 0.1, 0.3])
products = {"laptop": [0.8, 0.2, 0.5], "sofa": [0.1, 0.9, 0.4], "tablet": [0.85, 0.05, 0.35]}
scores = {name: round(float(cosine(phone, np.array(v))), 3) for name, v in products.items()}
print(sorted(scores.items(), key=lambda item: item[1], reverse=True))
Output
[('tablet', 0.996), ('laptop', 0.967), ('sofa', 0.318)]

Key takeaway#

Next: Using LLM APIs in Python.

Finished this lesson?