Embeddings and Vector Search, Explained for Engineers
How embeddings and vector search work: similarity, nearest-neighbour indexes, filters, hybrid search and the version trap, explained step by step for engineers.
Topics:RAG and retrieval
A user types "how do I get my money back" and the search returns the page titled "Refund policy". The two phrases share no important words. A keyword search would miss the match; a vector search finds it, because it compares meaning, not spelling. That ability is also why vector search can quietly go wrong, and why it deserves more than a library call and a hope.
TL;DR
An embedding model turns text into a list of numbers (a vector) so that texts with similar meaning end up close together. Vector search finds the stored vectors nearest to a query vector. At scale it uses approximate nearest-neighbour indexes, which trade a little recall for a lot of speed. The traps are filters that interact badly with those indexes, mixing embedding model versions, and trusting results you have not measured. Combine vector search with keyword search, pin your model version and measure recall on real queries.
What an embedding is
An embedding model reads a piece of text and outputs a fixed-length list of numbers. You never read the numbers directly. What matters is the geometry: the model is trained so that texts with related meaning produce vectors that are close, and unrelated texts produce vectors that are far apart.
Step 1: Embed the documents once
Split documents into chunks and turn each chunk into a vector with an embedding model
Step 2: Store the vectors
Keep each vector with its text and metadata in an index
Step 3: Embed the query
Turn the user's question into a vector with the same model
Step 4: Find the nearest vectors
Compute distances and return the closest chunks
Step 5: Use the results
Show them, rerank them or pass them to a language model as context
Because the model decides what "similar" means, the quality of your search depends on how well it fits your language and domain. Test it on your own queries rather than assuming it works.
Measuring "close": similarity and distance
You compare vectors with a distance or similarity measure. Three are common, and the vector store you use will support some or all of them.
Cosine
- Compares the angle between vectors, ignoring length
- A common choice for text embeddings
- Cosine distance is one minus cosine similarity
Euclidean (L2)
- The straight-line distance between points
- Sensitive to vector length
- Natural when vectors are not normalised
Inner product
- The sum of element-wise products
- Equivalent to cosine for unit-length vectors
- Used when the embedding model's documentation recommends it
Use the measure your embedding model's documentation recommends, and use the same one when you build the index and when you query. A mismatch gives results that look plausible and are subtly wrong. The core operation is small enough to write in a few lines:
import math
def cosine_similarity(a: list[float], b: list[float]) -> float:
dot = sum(x * y for x, y in zip(a, b))
return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))
def search(query_vec, store, k=5):
"""Brute-force nearest neighbours: exact, simple and slow at scale."""
scored = [(cosine_similarity(query_vec, item.vector), item) for item in store]
return sorted(scored, key=lambda s: s[0], reverse=True)[:k]This brute-force version is exact. It compares the query with every stored vector, so its cost grows with the size of the collection. That is fine for thousands of vectors and a problem for millions.
Why indexes are approximate
To search a large collection quickly, vector stores build an approximate nearest-neighbour (ANN) index. It finds vectors that are very likely among the nearest, without comparing against all of them. You accept that the true nearest neighbour is occasionally missed in return for a large speed-up. That trade-off is called recall: the share of the true nearest neighbours the index actually returns.
According to the pgvector documentation (read on 11 October 2026), the Postgres extension supports two approximate index types with different trade-offs:
HNSW
- Better speed-recall trade-off, according to the documentation
- Slower to build and uses more memory
- Can be created before any data is loaded
IVFFlat
- Faster to build and uses less memory
- Lower query performance than HNSW
- Needs data in the table first, so it can be trained
Other vector stores offer their own approximate indexes, with their own settings. The principle is the same everywhere: faster search costs some recall, and the settings let you move along that curve. Treat the defaults as a starting point and measure on your data.
The filter trap
Most real searches combine similarity with a condition: only documents this user may see, only from this year, only this product. How the filter interacts with the index matters.
The pgvector documentation notes that with approximate indexes, filtering is applied after the index is scanned. A selective filter can therefore leave you with few or no results, because the index returned its candidates before the condition removed most of them. Its own example: with HNSW and the default candidate setting of 40, a condition that matches about 10 percent of rows would yield around four matches on average.
If
The filter matches a large share of rows
Choose
Use the approximate index as is
Because
Enough candidates survive the filter
If
The filter is very selective
Choose
Consider an index on the filter column and an exact search, or iterative index scans
Because
Too few candidates survive otherwise
If
The filter has a few distinct values
Choose
Consider partial indexes per value
Because
Each index covers only its slice
If
The filter has many distinct values, such as one per tenant
Choose
Consider partitioning
Because
Searches then touch only the relevant partition
Check the current documentation for the exact options your version supports, since they change. The practical rule is to test your real filters with real queries and count how many results come back. Access control filters deserve the most care: a filter that silently returns too few results looks like "the answer is not in the documents", which is the wrong conclusion.
Keyword search still matters
Vector search is strong on paraphrases and weak on exact strings: product codes, error numbers, names and policy identifiers. Keyword search is the opposite. A common approach is to run both and merge the rankings, which is called hybrid search.
Keyword (for example BM25)
- Exact words, codes and names
- Misses paraphrases
- Cheap and predictable
Vector
- Meaning and paraphrase
- Weak on exact identifiers
- Needs an embedding model and an index
Hybrid
- Runs both and fuses the rankings
- Catches both kinds of query
- A common production choice
The production RAG architecture guide shows how to fuse the two with Reciprocal Rank Fusion and why reranking comes next, and the hybrid search pattern documents the design.
The version trap
A vector only makes sense relative to the model that produced it. A query embedded with one model cannot be meaningfully compared with documents embedded by another, and even a new version of the same model can shift the space.
The trap
- A new model version is used for queries while old vectors remain
- Similarity scores look normal and results quietly degrade
- Nobody notices until users complain
The safe path
- Record the model name and version with every vector
- Re-embed the whole collection when the model changes
- Build the new index alongside the old one, compare on your test queries, then switch
The embedding model drift challenge presents a search-quality drop after an embedding model upgrade and asks you to diagnose it.
Choosing where to store vectors
If
You already run Postgres and have a modest collection
Choose
A vector extension such as pgvector
Because
One system, SQL filters and transactional consistency with your other data
If
Very large collections or strict latency needs
Choose
A dedicated vector database
Because
Built for scale, with more index options
If
A prototype
Choose
Whatever is quickest to run
Because
Measure first and migrate if the data demands it
Decide from measured needs, not fashion. Moving later is possible if you keep the original text and metadata and can re-embed from them.
Measure it
You cannot tell whether vector search works by looking at a few results. Build a small set of real queries, each with the documents that should be returned, and compute recall at k: of the documents that should appear, what share appear in the top k results. Track it whenever you change the model, the chunking, the index settings or the filters.
Labelled queries
Good: A set of real queries with known correct documents
Red flag: Judged by trying a few searches by hand
Recall measured
Good: Recall at k reported and tracked over time
Red flag: Never measured, only assumed
Same model and measure
Good: Queries and documents use the same model, version and distance
Red flag: Model or version mixed, or the distance differs from the index
Filters tested
Good: Real filters tested with result counts checked
Red flag: Filters assumed to work like exact queries
Exact strings covered
Good: Keyword search handles identifiers and codes
Red flag: Pure vector search misses product codes and names
For the metrics in context, see LLM evaluation fundamentals and why good retrieval still produces bad answers.
Frequently asked questions
What is an embedding? A list of numbers produced by a model that represents the meaning of a piece of text, so that texts with similar meaning have vectors that are close together. You compare embeddings to find related content, not to read them directly.
How does vector search work? You embed documents once and store the vectors, embed the user's query with the same model, then return the stored vectors nearest to the query vector using a distance such as cosine, L2 or inner product. At scale an approximate index speeds up the search.
What is the difference between HNSW and IVFFlat? They are two approximate index types. According to the pgvector documentation, HNSW offers a better speed-recall trade-off but builds more slowly and uses more memory, while IVFFlat builds faster and uses less memory but has lower query performance and needs data present to train. Check the current documentation for your version.
Why do my filtered vector searches return too few results? With approximate indexes, filtering can happen after the index returns its candidates, so a selective filter removes most of them. Options include an index on the filter column, iterative scans, partial indexes and partitioning, depending on your store, so test your real filters.
Do I still need keyword search? Usually yes. Vector search is weak on exact identifiers such as product codes and error numbers, while keyword search misses paraphrases. Hybrid search runs both and fuses the rankings.
What happens if I change the embedding model? Vectors from different models, and sometimes different versions of one model, are not comparable. Record the model version with each vector, re-embed the whole collection, build the new index beside the old one and compare on your test queries before switching.
How do I know my vector search is good? Measure it. Build a set of real queries with the documents that should be returned, compute recall at k and track it when you change the model, chunking, index settings or filters.
Related
Sources
- pgvector (README), read on 11 October 2026: supported index types, their documented trade-offs, distance operators and how filtering interacts with approximate indexes
RAG and retrieval
Your next step
Reading is the start. Turn it into skill: read the chapter, practise on something real, check the numbers, then follow a path.
More on RAG and retrieval
- Production RAG Architecture: A Complete GuideThe full production RAG architecture: ingestion, chunking, embeddings, hybrid search, reranking, generation, evaluation and observability, with code.
- Why Good Retrieval Still Produces Bad AnswersDebug RAG failures with a runnable Python exercise that separates retrieval misses, dropped evidence, unsupported claims and incorrect refusals.
- Prompting, RAG or Fine-Tuning: A Decision GuideWhich technique first? A decision guide for prompting, RAG and fine-tuning: what each changes, what it costs to maintain and how to decide with an evaluation.
Discuss this article
Got a question, a counter-example or a correction? Bring it to the community.
Prefer a feed reader? Follow by RSS.