Skip to content

Command Palette

Search for a command to run...

BLOG / RAGFoundations

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.

Bhavin Mistry October 11, 2026 10 min readLast reviewed

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.

  1. Step 1: Embed the documents once

    Split documents into chunks and turn each chunk into a vector with an embedding model

  2. Step 2: Store the vectors

    Keep each vector with its text and metadata in an index

  3. Step 3: Embed the query

    Turn the user's question into a vector with the same model

  4. Step 4: Find the nearest vectors

    Compute distances and return the closest chunks

  5. Step 5: Use the results

    Show them, rerank them or pass them to a language model as context

From a question to a result

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
Common ways to compare vectors

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
The two pgvector index types, as documented

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

Handling filters with approximate indexes

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
Keyword, vector and hybrid search

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
Changing the embedding model

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

Where to start

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

Is your vector search trustworthy?

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.

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.

  1. Read · ChapterRetrieval & Reranking
  2. Practise · Debug challengeGood Retrieval, Bad Answers
  3. Use a free toolAI Cost Calculator
  4. Follow a roadmapGenerative AI Engineer roadmap
All articles on RAG and retrieval
#embeddings#vector search#semantic search#pgvector#HNSW#hybrid search

Discuss this article

Got a question, a counter-example or a correction? Bring it to the community.

Join the discussion

Email updates

Enjoyed this? Get the next guide by email.

One email when something substantial is published. No digests, no daily drip. Unsubscribe in one click.

We will email you a confirmation link first, then send an occasional email when a substantial guide is published. Unsubscribe in one click. Stored on our own servers, never sold or shared. See the privacy policy.

Prefer a feed reader? Follow by RSS.