Embeddings You Can See: Search, Map and Clean a Catalogue with Vectors
Hands-on lab · IDE in your browser

Embeddings You Can See: Search, Map and Clean a Catalogue with Vectors

Turn 200 book blurbs into embedding vectors and see what the numbers do: measure closeness with cosine similarity, find nearest neighbours, search by meaning with query and passage embeddings, draw the catalogue as a 2D map with PCA, recover the shop's sections with KMeans, and find the rows that are not books by their lack of neighbours.

Time
50 min
Checked steps
5
Level
Beginner
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit50 min · 5 stepsSession running
4 / 5 steps passingFind what does not belong · step 5 of 5
vectors.py▶ Run✓ Check
# ---------- Step 5: find what does not belong ----------def isolation_scores(m, k=5):    """For each row, the mean similarity to its k nearest neighbours. Low = nothing like it nearby."""    
TerminalOutput

The job

Brightline's online catalogue has 200 book blurbs filed under six sections, and the shop wants search that understands questions like "a story for a small child at bedtime". You embed every blurb, see which ones sit close together, build that search, draw the catalogue as a map, and let an algorithm rediscover the sections without being told them. The export also contains a few rows that are not books at all, filed under real sections; you find them without reading the catalogue.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Text in, numbers out

    An embedding model turns a piece of text into a list of numbers, a vector, arranged so that texts with similar meaning get similar vectors.

    You writeembed()
  2. 2

    Closeness: cosine similarity and nearest neighbours

    Two vectors are similar when they point in the same direction.

    You writenormalise()cosine()nearest()
  3. 3

    Search by meaning, not by words

    Keyword search finds blurbs that share words with the question.

    You writesearch()
  4. 4

    See the map, find the groups

    You cannot look at 2,048 dimensions, but you can squeeze them down.

    You writecluster()purity()
  5. 5

    Find what does not belong

    The catalogue export is supposed to be 200 book blurbs.

    You writeisolation_scores()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Text in, numbers out

An embedding model turns a piece of text into a list of numbers, a vector, arranged so that texts with similar meaning get similar vectors. Search, recommendations, clustering and retrieval-augmented generation (RAG) all rest on that one property. This lab makes it visible on Brightline's catalogue: books.csv holds 200 blurbs, each filed under a shop section.

The model here is NVIDIA's llama-nemotron-embed-vl-1b-v2, through the same kind of client as the chat models (client.embeddings.create). Two details matter:

  • It is asymmetric: it embeds a stored document (input_type: "passage") slightly differently from a search question ("query"), and it refuses requests that do not say which. The OpenAI client has no such argument, so it goes in extra_body, which passes extra fields through to the server.
  • It returns 2,048 numbers per text by default and can return fewer (dimensions=256), trading a little accuracy for a lot less storage.
Do this

1. Write embed(texts, input_type="passage", dimensions=None, batch_size=64). Send the texts in chunks of batch_size (one request per chunk rather than one per text), with extra_body={"input_type": input_type} and dimensions only when it is given. Collect each item's embedding in order (sort response.data by index) and return np.array(rows, dtype=np.float32): one row per text.

2. Run. It embeds all 200 blurbs in four requests and saves embeddings.npy, which the later steps reuse.

vectors.py, the file you edit59 lines
"""Embeddings you can see: turn blurbs into vectors, measure closeness, search, map and find the misfits."""
import numpy as np
from openai import OpenAI

CLIENT = OpenAI()
EMBED_MODEL = "nvidia/llama-nemotron-embed-vl-1b-v2"


# ---------- Step 1: text in, vectors out ----------
def embed(texts, input_type="passage", dimensions=None, batch_size=64, client=None):
    """One row per text: a float32 numpy array of shape (len(texts), d).
    input_type is "passage" for documents you store and "query" for questions you search with."""
    client = client or CLIENT
    # TODO (Step 1): for each chunk of batch_size texts:
    #   kwargs = {"model": EMBED_MODEL, "input": <the chunk as a list>, "extra_body": {"input_type": input_type}}
    #   add kwargs["dimensions"] = dimensions only when dimensions is given
    #   response = client.embeddings.create(**kwargs)
    #   collect item.embedding for each item in response.data (sorted by item.index)
    # return np.array(rows, dtype=np.float32)
    raise NotImplementedError("Step 1: write embed()")


# ---------- Step 2: how close are two meanings? ----------
def normalise(m):
    """Every row scaled to length 1 (so a dot product is the cosine similarity)."""
    raise NotImplementedError("normalise() arrives in Step 2")


def cosine(a, b):
    """Cosine similarity of two vectors: 1 = same direction, 0 = unrelated."""
    raise NotImplementedError("cosine() arrives in Step 2")


def nearest(i, m, k=5):
    """Indices of the k rows most similar to row i (not i itself), most similar first."""
    raise NotImplementedError("nearest() arrives in Step 2")


# ---------- Step 3: search by meaning ----------
def search(query, passages, k=5, dimensions=None, client=None):
    """Indices of the k passages closest to the query (embedded with input_type="query")."""
    raise NotImplementedError("search() arrives in Step 3")


# ---------- Step 4: see it ----------
def cluster(m, n_clusters=6, seed=0):
    """KMeans cluster number for every row (on normalised vectors)."""
    raise NotImplementedError("cluster() arrives in Step 4")


def purity(clusters, labels):
    """Share of rows whose label is the most common label in their cluster."""
    raise NotImplementedError("purity() arrives in Step 4")


# ---------- Step 5: find what does not belong ----------
def isolation_scores(m, k=5):
    """For each row, the mean similarity to its k nearest neighbours. Low = nothing like it nearby."""
    raise NotImplementedError("isolation_scores() arrives in Step 5")
Provided for you:books.csvtry_it.py

Frequently asked questions

What is cosine similarity?

A measure of how closely two vectors point in the same direction: the dot product divided by the product of their lengths. For normalised vectors it is just the dot product. With modern embedding models even unrelated texts often score well above zero, so scores are for ranking, not for reading as percentages.

What do input_type query and passage mean?

Asymmetric embedding models encode a short search question differently from a stored document, which improves retrieval. The lab's model requires one or the other on every request; stored blurbs use passage and search questions use query.

Why reduce dimensions?

Fewer numbers per text means less storage and faster search. Models trained for it, such as the one in this lab, keep most of their accuracy at a fraction of the size when asked for 256 or 512 dimensions instead of 2,048.

How does the lab find rows that do not belong?

Each row is scored by its mean similarity to its five nearest neighbours. Real book blurbs have similar books nearby; an invoice reminder or a car park notice does not, so it gets the lowest score.

What embeddings are, shown on real data

An embedding model turns text into a vector of numbers so that texts with similar meaning get vectors that point in similar directions. It is the machinery under semantic search, recommendations, clustering, deduplication and the retrieval step of RAG. The idea is simple, and it becomes clear once you can see it working on data you understand. In this lab you call a hosted NVIDIA embedding model in batches, learn why asymmetric models embed queries and passages differently, and use the dimensions parameter to shrink vectors. You compute cosine similarity and nearest neighbours with numpy, build a semantic search in a few lines, project 2,048 dimensions to a 2D map with PCA, recover the catalogue's sections with KMeans and measure cluster purity, and detect misfiled rows with a nearest-neighbour isolation score.