Module P-21·34 min read

Embeddings as coordinates, HNSW vs IVF, the recall-for-latency trade you are silently making, pgvector vs a dedicated store, and hybrid ranking.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module P-21 — Vector Search and Embeddings

What this module covers: The vocabulary-mismatch problem that keyword search cannot solve, and embeddings as coordinates where distance stands in for meaning. Why the embedding model is a schema commitment you will pay to change, and why vectors from two models are not comparable. Chunking, which determines retrieval quality more than the index does. Why exact k-NN is a full scan and where the ceiling is. The two ANN index families — IVF and HNSW — with their real knobs, memory profiles, and update behaviour, plus quantisation as the memory lever. Recall as a number you must measure rather than assume, because a badly tuned index returns confident wrong answers with no error. The filtered-search trap, which is where multi-tenant vector search goes wrong. Hybrid retrieval with reciprocal rank fusion and why fusing ranks beats fusing scores. And the embedding pipeline, which is a background-job and cost problem before it is a search problem.


Keyword Search Fails When the User's Words Are Not the Document's Words

Module P-20 built an index over the words people wrote. It cannot help when the words differ:

A user searches forThe document saysBM25 finds
"my app keeps crashing""handling unhandled promise rejections"nothing
"how do I stop being charged""cancelling your subscription"nothing
"car""automobile"nothing (unless you listed the synonym)
"the film with the spinning top""Inception (2010)"nothing

This is vocabulary mismatch, and it is not a tuning problem. Stemming does not connect crashing to exception. Synonym lists help exactly as far as somebody has enumerated them, which means they help with the mismatches you already knew about — and every list is a maintenance burden that grows without ever being complete.

The lexical index has a structural limit: it matches tokens, and meaning is not made of tokens. What is needed is a representation where "my app keeps crashing" and "handling unhandled promise rejections" land near each other because they are about the same thing, without anyone having written down that they are.

Analogy: a library where every book is placed on a floor plan by subject rather than filed under its title's spelling. The cookbooks cluster in one corner, the physics texts in another, and a book about the thermodynamics of baking sits between them. To find something you walk to roughly where the topic lives and look around, instead of needing the exact word on the spine. Two things follow, and both matter for the rest of this module: the arrangement is only as good as the librarian who placed the books, and "look around nearby" is a fundamentally approximate act — you might miss the one shelf you did not turn towards.


An Embedding Is a Coordinate, and Distance Stands In for Meaning

An embedding model maps text (or an image, or audio) to a fixed-length vector of floats — a point in a space of 384, 768, 1,536 or 3,072 dimensions. The model is trained so that semantically similar inputs land close together. That is the whole idea; everything else is engineering.

ts

Closeness is measured by one of three functions, and the distinction is mostly bookkeeping:

MeasureFormulaNotes
Cosine similaritya·b / (‖a‖‖b‖)angle only, ignores magnitude — the usual choice for text
Dot producta·bidentical to cosine if vectors are normalised; cheaper
Euclidean (L2)‖a − b‖monotonically related to cosine for normalised vectors

The practical rule: normalise your vectors once at write time, then use dot product, and you get cosine semantics at lower cost. The failure to check is a mismatch between the distance function your index was built with and the one your query uses — vector_cosine_ops index queried with <-> (L2) in pgvector means the index is silently not used, and you get a sequential scan that is correct but slow, or with some configurations results ordered by the wrong metric. No error either way.

Two consequences of dimensionality that show up on invoices:

Storage is n × d × 4 bytes for float32. Ten million chunks at 1,536 dimensions is 61 GB of raw vectors before any index — and an HNSW index is stored on top of that, in memory. Choosing a 3,072-dimension model over a 768-dimension one is a 4× decision about RAM, not a detail.

Higher dimensions are not straightforwardly better. Larger models often retrieve better, but the gain is frequently small relative to a 4× cost, and some models support dimension truncation (Matryoshka embeddings) that lets you keep most of the quality at a fraction of the size. Measure the retrieval quality difference on your corpus before paying for width.

The model is a schema commitment

This is the point that catches teams, and it is worth stating bluntly: vectors produced by two different models are not comparable. Not "less accurate" — meaningless. The spaces are unrelated, so a query embedded with model B searched against a corpus embedded with model A returns essentially arbitrary results, ranked confidently, with no error anywhere.

Which means:

  • Changing embedding model requires re-embedding the entire corpus, at full API cost and full ingestion time.
  • Query-time and index-time models must match exactly, including the version. Providers do update models behind an unversioned alias; pin the version.
  • Store the model identifier alongside every vector, and filter on it. A model_version column is three bytes of foresight that prevents a whole class of silent corruption during a migration.
  • The migration pattern is Module P-20's alias swap: embed into a new column or index, verify quality, switch reads, keep the old one for rollback.

Why this matters in production: the failure mode of a mismatched model is not an exception, it is plausible nonsense. Search returns ten results, they look like results, and they are unrelated to the query. If those results feed an LLM (Module A-12), the model will cheerfully synthesise an answer from irrelevant context, and the first person to notice will be a customer reading something confidently wrong.


Chunking Decides Retrieval Quality More Than the Index Does

You cannot embed a 40-page document usefully. One vector must summarise everything the text is about, and averaging forty topics produces a point that is near nothing in particular — the vector equivalent of a blurry photograph. So documents are split into chunks, each embedded separately, and chunking is where most retrieval quality is won or lost.

The tension is fixed:

  • Chunks too large: the vector is diluted. A 4,000-token chunk covering installation, configuration and troubleshooting is not strongly near any of those queries, and once retrieved it delivers mostly irrelevant text to whatever consumes it.
  • Chunks too small: context is destroyed. "This value defaults to 30 seconds" is a useless chunk — which value? A sentence-level chunk retrieves precisely and answers nothing.

The strategies, in the order most teams should try them:

StrategyHowTrade-off
Fixed size + overlap500 tokens, 50–100 overlappingtrivial to implement; splits mid-thought; overlap inflates storage ~15%
Structuralsplit on headings, list items, code blocks, paragraphsrespects the author's own boundaries; requires parsing per format
Recursivetry paragraph, then sentence, then hard split at the size limitgood general default; the practical baseline
Semanticsplit where embedding similarity between adjacent sentences dropsbest boundaries; costs an embedding pass over every sentence to compute
Parent-documentembed small chunks, but return the larger parent sectionprecise matching with full context — usually the biggest single quality win

Parent-document retrieval deserves the emphasis. Match on a tight 200-token chunk so the vector is sharp, then hand back the 2,000-token section it belongs to so the consumer has enough context to be useful. It decouples "what makes a good match" from "what makes a good result," which are genuinely different requirements, and it costs only a parent pointer per chunk.

Two things every chunk needs regardless of strategy:

Metadata, denormalised onto the chunk. Tenant ID, document ID, source URL, section heading, timestamp, ACL, and model_version. You will filter on these, and a filter you cannot express is a feature you cannot ship. The tenant ID in particular is not optional — see the filtered-search section.

Contextual prefixing. Prepending the document title and section heading to the chunk text before embedding measurably improves retrieval, because it restores the context that splitting removed. "Billing → Cancelling: This value defaults to 30 seconds" embeds far more usefully than the sentence alone.


Exact k-NN Is a Full Scan; Approximation Is the Whole Field

Finding the true nearest neighbours means computing the distance to every vector: O(n · d). That is a sequential scan with arithmetic, and its performance is more forgiving than people assume — modern SIMD makes a few hundred thousand comparisons at 768 dimensions a matter of tens of milliseconds.

Corpus sizeExact k-NN (768-d, rough)Verdict
10,000~1 msexact is obviously right
100,000~10–30 msexact is still fine, and it is perfectly accurate
1,000,000~200–400 mstoo slow interactively
10,000,000+secondsneeds an index

The lesson from that table is one worth acting on: below roughly 100,000 vectors, do not build an ANN index. A brute-force scan is simpler, has no build step, no tuning, no recall loss, no staleness, and handles filters and updates for free. Adding an approximate index to a small corpus imports every operational problem below in exchange for milliseconds nobody notices.

Above that, you trade accuracy for speed. That is what the "A" in ANN means, and it is not a caveat — it is the deal.

IVF: partition the space, search a few partitions

Cluster the vectors into lists centroids (k-means). At query time, find the nearest few centroids and scan only those lists.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.