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
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)
Semantic search (embeddings)
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||)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]
| Angle | Cosine | Meaning |
|---|---|---|
theta = 0 deg | 1 | same direction - identical meaning |
theta = 90 deg | 0 | orthogonal - unrelated |
theta = 180 deg | -1 | opposite direction |
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)Applied to the earlier query, the ranked candidates look like this (illustrative scores):
| Candidate memory / document | cos(query, doc) | Verdict |
|---|---|---|
| how to build a DB index | 0.94 | nearest - retrieve |
| speed up slow SQL queries | 0.89 | retrieve |
| banana bread recipe | 0.31 | skip |
| adopt a kitten | 0.12 | skip |
Tap a node to see what it does.
Where this fits in an agent
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
Match the metric to the model
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]
Tap a node to see what it does.
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:
| Store | What it is | Reach for it when |
|---|---|---|
| pgvector | Postgres extension | You already run Postgres and want SQL + transactions alongside vectors (under ~1 to 2M vectors). Tune m/ef_search yourself. |
| Pinecone | Fully managed cloud | You want zero infra and are happy to let it hide the HNSW knobs. |
| Qdrant | Open-source (Rust) | You need heavy metadata filtering and want to self-host or use their cloud. |
| Chroma | In-process, embedded | You are prototyping locally or in a notebook and want zero config. |
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;Failure modes that fail silently
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.
encodes meaning as a vector
scores nearness by angle
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