Skip to content

Memory and Knowledge

Embeddings and vector search

Semantic similarity, vector DBs

An agent can only keep a tiny working set in its context window - so where does the rest of what it knows live, and how does it find the right piece on demand? The answer is a geometry trick: turn meaning into coordinates, then search by proximity. This lesson builds that intuition from the ground up - embeddings, cosine similarity, and the vector databases that make it fast at scale.

Meaning becomes geometry

An is a learned, dense numeric vector that represents a piece of content - a word, a sentence, a document - as a point in high-dimensional space, arranged so that geometric closeness reflects semantic closeness.[1] Modern text embedding models emit vectors of 384, 768, 1536, or 3072 dimensions. We cannot picture 1536 axes, but the idea survives a projection down to two: things that mean similar things land near each other, and unrelated things land far apart.

Picture three clusters in that space - cooking, pets, databases. The query "make Postgres faster" was never seen before, yet it lands inside the databases cluster; its nearest neighbour is "build a DB index," not any cooking or pet sentence. The query shares almost no words with that neighbour ("Postgres" vs "DB", "make faster" vs "build index"), yet they sit right next to each other. That is the whole payoff of embeddings - and the thing plain keyword search cannot do.

The core idea

Embeddings convert the fuzzy problem of "what does this mean" into the exact problem of "which vectors are nearby". Retrieval stops being string matching and becomes measuring distance in space.

Semantic search vs keyword search

Classic keyword search (BM25, the ranking behind Lucene/Elasticsearch) matches on tokens: it counts term overlaps. It is fast, exact, and completely blind to meaning - search "car" and it will never surface a document that only says "automobile." Semantic search embeds the query and the documents into the same space and ranks by vector proximity, so paraphrases and synonyms match even with zero shared words.

Keyword search (lexical)

Matches exact tokens - "car" is not "automobile". Brittle to paraphrase, synonyms, and typos. But: unbeatable on exact strings - IDs, error codes, rare proper nouns.

Semantic search (embeddings)

Matches meaning - "make Postgres faster" is near "build a DB index". Robust to wording; great for fuzzy, natural-language queries. But: can miss an exact rare token if the wording diverges hard.[6]

Neither wins outright, which is why production retrieval usually runs both and fuses the scores - "hybrid search." You will wire that into a full pipeline in retrieval-augmented generation (RAG).

Cosine similarity: meaning is a direction

To rank by proximity we need a number. The default metric for text embeddings is - the cosine of the angle between two vectors:[2]

cos(theta) = (A . B) / (||A|| * ||B||)
Cosine similarity: the cosine of the angle between two vectors.

It ranges from -1 to 1: 1 = same direction (identical meaning), 0 = orthogonal (unrelated), -1 = opposite. The crucial property is that it looks only at the angle, not the length. "kitten" and "a very tiny kitten" point the same way even if one vector is longer - cosine is magnitude-invariant, so it captures meaning without being fooled by verbosity.[2]

AngleCosineMeaning
theta = 0 deg1same direction - identical meaning
theta = 90 deg0orthogonal - unrelated
theta = 180 deg-1opposite direction
Cosine similarity is the angle between two vectors. A small angle means a cosine near 1, so very similar. Lengthening either vector leaves the angle - and the score - unchanged.

Why cosine and not the alternatives? Dot product is magnitude-dependent and unbounded, so a long vector can outrank a more relevant short one; Euclidean (L2) distance measures absolute spatial separation, also unbounded. Cosine throws away length and keeps only alignment - which is exactly what "similar meaning" should mean.[2] One handy shortcut: if you L2-normalise every vector to unit length, cosine, dot product, and Euclidean all rank identically - so systems normalise and then use the fastest operator.[2]

Worked example: find the nearest neighbour

Cosine is a two-line function. Embed a query and a few candidate documents, score each, and the top score is your nearest neighbour:

from numpy import dot
from numpy.linalg import norm

def cosine(a, b):
    return dot(a, b) / (norm(a) * norm(b))

query   = [1.0, 0.0, 1.0]     # "database indexing"
doc_hit = [0.9, 0.1, 0.8]     # "how to build a DB index"
doc_off = [0.0, 1.0, 0.0]     # "banana bread recipe"

cosine(query, doc_hit)   # ~ 0.99  -> highly similar (retrieve)
cosine(query, doc_off)   # ~ 0.00  -> orthogonal    (skip)
Score the query against each candidate; the top score is the nearest neighbour. Scale doc_hit by 10x and the cosine is unchanged - magnitude-invariance in one line.

Applied to the earlier query, the ranked candidates look like this (illustrative scores):

Candidate memory / documentcos(query, doc)Verdict
how to build a DB index0.94nearest - retrieve
speed up slow SQL queries0.89retrieve
banana bread recipe0.31skip
adopt a kitten0.12skip
The two database sentences score high and are retrieved; the cooking and pet sentences fall away.
Text input
Vector (e.g. 1536 numbers)

Tap a node to see what it does.

The same model embeds both stored documents and the incoming query, so both live in one shared space and can be compared directly.

Where this fits in an agent

This is the retrieval substrate for an agent's long-term memory and knowledge. The store may hold thousands of items, but only the top few high-signal matches (~a few hundred tokens) get pulled into the window just-in-time - the small-working-set discipline from context engineering, made concrete.

Embedding models: same space, both sides

An embedding model is a neural network trained (usually contrastively) so that its cosine geometry lines up with human notions of similarity. Two non-negotiable rules:

One model, both sides

Embed query and documents with the same model. Two different models produce incompatible coordinate systems - comparing across them is meaningless.

Match the metric to the model

Most text embedders are trained for cosine; normalise vectors so cosine/dot/L2 agree, then use the fastest operator.[2]

Dimensionality is a cost/quality dial: more dimensions can capture more nuance but cost more storage, memory, and compute per comparison. 768 and 1536 are common sweet spots for retrieval.

Vector databases and ANN: why brute force dies

Cosine over a handful of vectors is trivial. The problem is scale. Exact nearest-neighbour search - the / top-k problem - compares the query against every stored vector, which is O(n) per query.[1] At a million vectors that is a million dot products for one query; at a billion it is hopeless. So production systems use an index that trades a small amount of recall for a large speedup.[1]

The industry-default ANN index is (Hierarchical Navigable Small World): a multi-layer graph. The sparse upper layers are "long-hop" shortcuts across the space; the dense bottom layer holds every point. A query enters at the top, greedily hops toward the target on the coarse layer, then descends layer by layer to refine - coarse-to-fine navigation that turns O(n) scanning into roughly O(log n) search.[1][3]

Greedily hop toward the query

Tap a node to see what it does.

HNSW search: enter at the sparse top layer, hop toward the query, then descend into the dense bottom layer to pin down the nearest neighbour - roughly O(log n) instead of scanning everything.

HNSW exposes three knobs worth knowing: m (edges per node - higher gives better recall, more memory), ef_construction (build-time search breadth), and ef_search (query-time breadth - the recall vs latency dial). Turn ef_search up for recall-critical apps, down for latency-critical ones.[3]

The vector-store landscape

HNSW is now the default index across Chroma, Qdrant, Weaviate, Pinecone serverless, and pgvector[4] - so the real choice is about operations, filtering, and how much infra you want to run:

StoreWhat it isReach for it when
pgvectorPostgres extensionYou already run Postgres and want SQL + transactions alongside vectors (under ~1 to 2M vectors). Tune m/ef_search yourself.
PineconeFully managed cloudYou want zero infra and are happy to let it hide the HNSW knobs.
QdrantOpen-source (Rust)You need heavy metadata filtering and want to self-host or use their cloud.
ChromaIn-process, embeddedYou are prototyping locally or in a notebook and want zero config.
HNSW is the shared default; the choice is operational - infra, filtering, and scale.

Because pgvector rides on real SQL, the whole pipeline - schema, index, filter, ranked query - is readable at a glance:

-- HNSW index; vector_cosine_ops => cosine distance operator (<=>)
CREATE INDEX ON memories
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

-- query-time recall/latency dial
SET hnsw.ef_search = 40;

-- top-3 nearest memories for this user (<=> is cosine distance = 1 - cosine sim)
SELECT content, 1 - (embedding <=> :query_vec) AS similarity
FROM   memories
WHERE  user_id = 42
ORDER BY embedding <=> :query_vec
LIMIT  3;
pgvector: HNSW index, the query-time recall dial, and a ranked cosine query in plain SQL.

Failure modes that fail silently

Recall cliffs. Too-low ef_search/m quietly drops relevant results - you get fast, wrong answers. Always validate recall@k against a brute-force baseline.[3] Wrong metric / unnormalised vectors. Dot product on unnormalised embeddings lets magnitude dominate meaning and tanks recall.[2] Distractors poison retrieval. Even a single semantically-similar-but-irrelevant chunk lowers downstream accuracy.[6] pgvector scaling wall. Past ~2M vectors on one node, HNSW build times balloon; you will need partitioning, replicas, or a dedicated store.[5]

Check yourself

Match each term to what it does.

drop here

encodes meaning as a vector

drop here

scores nearness by angle

drop here

finds neighbours without a full scan

Why is cosine similarity the default metric for comparing text embeddings, over Euclidean distance?

Why doesn't a production vector store scan every stored vector for each query, and what index replaces the scan?

What is the cost of exact search, and what does the index cost you back? Try to state it, then check.

Lock it in

  • Embeddings turn meaning into coordinates. A learned dense vector places content in space where geometric closeness means semantic closeness.
  • Cosine similarity is the angle, not the length. Range -1 to 1; magnitude-invariant, so verbosity cannot fake relevance. Normalise and it agrees with dot/L2.
  • Semantic beats keyword on paraphrase, loses on exact tokens - production retrieval fuses both (hybrid).
  • Exact search is O(n) and dies at scale. ANN indexes trade a little recall for big speed; HNSW's multi-layer graph gives ~O(log n).
  • Pick the store for ops, not just speed. pgvector if you are on Postgres, Pinecone for managed, Qdrant for filtering, Chroma for prototyping - and always validate recall.

Primary source

Lilian Weng, LLM Powered Autonomous Agents (Lil'Log, 2023)

The canonical walkthrough of embeddings as the retrieval substrate, Maximum Inner Product Search (MIPS), and the ANN family (LSH, ANNOY, HNSW, FAISS, ScaNN) that makes vector search scale.

Sources

  1. 1.Lilian Weng, LLM Powered Autonomous Agents (2023)
  2. 2.Tigerdata, Understanding Cosine Similarity
  3. 3.Neon, Understanding vector search and HNSW index with pgvector
  4. 4.Sysdebug, Vector Database Comparison Guide 2025
  5. 5.Vecstore, Vector Database Performance Compared
  6. 6.Chroma, Context Rot