In 30 seconds
A vector database stores embeddings, lists of numbers that capture the meaning of text, images or other data, and finds the most similar ones to a query. It uses approximate nearest neighbour (ANN) indexes such as HNSW to search millions of vectors in milliseconds. This powers semantic search, recommendations and retrieval for AI assistants (RAG). You can use a dedicated engine or an extension like pgvector.
Not in my daily stack — I know the concepts and would learn the details when a project needs it.
Why it matters
- Keyword search fails when people use different words for the same thing. Embeddings match “cheap flights” with “low-cost airfare”, which is why semantic search feels smarter.
- Retrieval-augmented generation (RAG) depends on it: an LLM answers from your documents only if you can quickly find the right passages.
- Teams choose between adding vectors to an existing database (pgvector, Elasticsearch, Redis) and running a dedicated engine; the right choice depends on scale, filters and team skills.
- It complements, not replaces, keyword search such as Algolia or Elasticsearch BM25: the best systems combine both in hybrid search.
How it works
01Embeddings: meaning as coordinates
An embedding model turns a piece of content into a vector, for example 1,024 numbers. Think of a huge map where similar meanings are close together: “dog” is near “puppy”, far from “invoice”. The model is trained so that distance on this map reflects similarity. You embed your documents once and store the vectors; at query time you embed the question with the same model and look for the nearest neighbours. Similarity is measured with cosine similarity (angle between vectors), dot product or Euclidean (L2) distance. Most text models produce normalised vectors, so cosine and dot product rank the same way.
- Dimension: how many numbers each vector has (for example 384, 768, 1024, 1536, 3072).
- Always use the same model for documents and queries; vectors from different models are not comparable.
- Changing the model means re-embedding everything, so record the model name and version with each vector.
02Why exact search does not scale, and how ANN helps
Exact search compares the query with every stored vector. For 10,000 vectors that is fine; for 50 million with 1,024 dimensions it is far too slow. Approximate nearest neighbour (ANN) indexes trade a little accuracy for huge speed. HNSW (Hierarchical Navigable Small World) builds a layered graph: upper layers are sparse highways, lower layers are dense local streets. A search starts at the top, greedily moves toward the query, and descends. IVF (inverted file) clusters vectors and only searches the nearest clusters. Recall measures quality: the share of true nearest neighbours the index returns. Good settings reach 95–99% recall in a few milliseconds.
- HNSW: best recall/speed trade-off, more memory, slower to build.
- IVF: less memory and faster builds, needs training on data and tuning of probes.
- Quantization: storing vectors in fewer bits (int8, binary) to cut memory, with rescoring for accuracy.
03Starting with pgvector in PostgreSQL
If you already run PostgreSQL, pgvector is often the simplest start: vectors live next to your rows, you get transactions, joins, backups and access control for free, and filters are plain SQL. The extension adds the vector type (and halfvec for 16-bit storage), distance operators (<-> for L2, <=> for cosine distance, <#> for negative inner product) and HNSW and IVFFlat indexes. Use the operator class that matches your distance; an index built with vector_cosine_ops only helps queries that order by <=>. Raise hnsw.ef_search for better recall at some latency cost.
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE doc_chunks (
id bigserial PRIMARY KEY,
doc_id bigint NOT NULL,
tenant_id int NOT NULL,
content text NOT NULL,
embedding vector(1024) NOT NULL
);
CREATE INDEX ON doc_chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
SET hnsw.ef_search = 100;
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM doc_chunks WHERE tenant_id = 7
ORDER BY embedding <=> $1 LIMIT 10;
04Dedicated vector databases
Dedicated engines such as Qdrant, Weaviate, Milvus and Pinecone are built around vector search. They offer fast filtered search, quantization, sharding across nodes, multi-tenancy features, sparse vectors for hybrid search and managed cloud options. You store points made of an ID, one or more vectors and a payload (metadata) that you filter on. The trade-off is another system to run, secure and keep in sync with your primary database. A practical rule: below a few million vectors with moderate traffic, pgvector or your existing search engine is usually enough; with tens of millions of vectors, heavy filtering or strict latency targets, a dedicated engine earns its place.
# Qdrant: create a collection and search with a filter (HTTP API)
curl -X PUT http://localhost:6333/collections/docs -H 'Content-Type: application/json' \
-d '{ "vectors": { "size": 1024, "distance": "Cosine" } }'
curl -X POST http://localhost:6333/collections/docs/points/query \
-H 'Content-Type: application/json' -d '{
"query": [0.01, 0.12, 0.03],
"filter": { "must": [ { "key": "tenant_id", "match": { "value": 7 } } ] },
"limit": 10, "with_payload": true
}'
05Hybrid search and reranking
Vectors are great at meaning but weak at exact tokens: product codes, names, error messages. Keyword search (BM25) is the opposite. Hybrid search runs both and merges the lists, most often with reciprocal rank fusion (RRF), where each document scores 1 / (k + rank) in each list and the sums decide the final order. RRF needs no score normalisation, which is why it is popular. After merging, a cross-encoder reranker can re-score the top 50–100 candidates by reading query and document together; it is slower but much more accurate, so it runs only on a short list.
-- Hybrid in PostgreSQL: full-text + vector, merged with RRF (k = 60)
WITH kw AS (
SELECT id, row_number() OVER (ORDER BY ts_rank(tsv, q) DESC) AS r
FROM doc_chunks, websearch_to_tsquery('english', $2) q
WHERE tsv @@ q ORDER BY r LIMIT 50
), vec AS (
SELECT id, row_number() OVER (ORDER BY embedding <=> $1) AS r
FROM doc_chunks ORDER BY embedding <=> $1 LIMIT 50
)
SELECT id, sum(1.0 / (60 + r)) AS score
FROM (SELECT * FROM kw UNION ALL SELECT * FROM vec) s
GROUP BY id ORDER BY score DESC LIMIT 10;
Use it when
- Semantic search over documents, help centres, tickets or product descriptions.
- Retrieval for RAG assistants that must answer from your own content.
- Recommendations and “similar items” based on content or behaviour embeddings.
- Deduplication and clustering of near-identical texts or images.
Skip it when
- Exact lookups by ID, SKU or filter conditions; a normal index is faster and exact.
- Small collections of a few thousand items, where exact search in memory or in PostgreSQL is enough.
- When users mostly search exact names and codes; good keyword search will serve them better.
Alternatives
- pgvector (PostgreSQL)
Vectors next to relational data with SQL filters and transactions; the simplest start for many teams.
- Qdrant / Weaviate / Milvus
Open-source dedicated engines with strong filtering, quantization and scaling, self-hosted or managed.
- Pinecone
Fully managed serverless vector database; no operations work, usage-based pricing.
- Elasticsearch / OpenSearch / Redis
Add vector search to an engine you may already run, with built-in keyword search for hybrid.
Best practices
- Pick the embedding model by evaluating it on your own queries, not only by leaderboard rank.
- Store the model name and version with every vector, and plan for full re-embedding.
- Match the index operator class or metric to the distance your model expects (usually cosine).
- Measure recall against exact search on a sample before tuning parameters like ef_search.
- Use hybrid search (keyword plus vector) and a reranker for the final top results.
- Apply tenant and permission filters inside the vector query, never only after retrieval.
- Keep the source of truth in your primary database and sync vectors asynchronously and idempotently.
Common mistakes
- Mixing vectors from two embedding models in one index, which silently produces nonsense rankings.
- Embedding whole long documents as one vector, so the meaning blurs and retrieval misses details.
- Filtering results after a top-10 vector search, leaving zero results for selective filters.
- Assuming vector search is exact and not measuring recall or relevance at all.
- Forgetting that index memory grows with dimensions times vectors, and running out of RAM in production.
Deep dive
06Tuning HNSW: m, ef_construction, ef_search
m is the number of links per node. Higher m improves recall on high-dimensional data but increases memory and build time; 16 is a common default, 32–64 for hard datasets. ef_construction is the size of the candidate list while building; higher means a better graph and slower inserts (64–200 is typical). ef_search is the candidate list at query time and the main runtime knob: higher means better recall and more latency, and it must be at least as large as the LIMIT. Tune empirically: take 1,000 sample queries, compute exact neighbours with a sequential scan, then sweep ef_search and plot recall at 10 against p95 latency. Building HNSW is memory hungry; in PostgreSQL raise maintenance_work_mem and max_parallel_maintenance_workers for the build.
-- Build faster with more memory and workers (PostgreSQL)
SET maintenance_work_mem = '4GB';
SET max_parallel_maintenance_workers = 7;
CREATE INDEX CONCURRENTLY idx_chunks_emb
ON doc_chunks USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 128);
-- Filtered queries: keep scanning until enough rows match (pgvector 0.8+)
SET hnsw.iterative_scan = relaxed_order;
07Memory, quantization and rescoring
A float32 vector of 1,024 dimensions takes 4 KB. Ten million of them is about 40 GB before index overhead, and HNSW wants the hot part in RAM. Quantization shrinks this. Half precision (halfvec in pgvector) halves size with almost no recall loss. Scalar quantization to int8 cuts it by four. Binary quantization stores one bit per dimension, 32 times smaller, and works surprisingly well on many modern embedding models when combined with rescoring: search the compact index for, say, the top 200 candidates, then re-rank them with the full-precision vectors. Engines like Qdrant, Milvus and Elasticsearch implement this pattern natively; in pgvector you can index a binary expression and rescore in SQL.
-- pgvector: binary-quantized index with full-precision rescoring
CREATE INDEX ON doc_chunks USING hnsw
((binary_quantize(embedding)::bit(1024)) bit_hamming_ops);
SELECT id FROM (
SELECT id, embedding FROM doc_chunks
ORDER BY binary_quantize(embedding)::bit(1024) <~> binary_quantize($1)
LIMIT 200
) candidates
ORDER BY embedding <=> $1 LIMIT 10;
08Keeping vectors fresh: ingestion and re-embedding
Vectors are derived data. Treat embedding as a pipeline: when a document changes, an event (outbox row or queue job) triggers chunking and embedding, and the result is upserted by a stable ID such as doc_id plus chunk number, deleting stale chunks of the same document. Make jobs idempotent and rate-limited, because embedding APIs have quotas. Model upgrades need a migration plan: create a new column or collection for the new model, backfill in the background, evaluate side by side, switch reads behind a flag, then drop the old vectors. HNSW indexes degrade a little with heavy updates and deletes; schedule periodic reindexing (REINDEX CONCURRENTLY in PostgreSQL) or rely on the engine's optimiser.
09Security and privacy of embeddings
Embeddings are not anonymous. Research has shown that text can be partly reconstructed from its embedding, so treat vectors with the same sensitivity as the source content. Enforce access control at query time: tenant and permission filters inside the search, row-level security in PostgreSQL, or separate collections for strict isolation. When sending content to an external embedding API, check data processing terms and avoid sending secrets or unnecessary personal data. Deleting a document must delete its vectors too, which matters for GDPR erasure. Finally, retrieved content can contain prompt injection when used in RAG, so treat retrieved text as untrusted data in the prompt.
Real-world scenarios
Semantic search for a help centre
Problem
A SaaS help centre has 3,000 articles. Users search in their own words and keyword search returns nothing for half the queries.
How to solve it
Start simple: enable pgvector in the existing PostgreSQL. Split articles into chunks of a few hundred tokens with headings preserved, embed them with one model, and store doc_id, chunk text and model version. Create an HNSW index with cosine operators. Build hybrid search: PostgreSQL full-text search plus vector search, merged with RRF, as in the lesson. Collect 100 real queries from search logs, label the correct articles, and compare zero-result rate and recall at 5 before and after. Re-embed changed articles through a queue job on publish. No new database is needed at this size.
Filtered search returns too few results
Problem
A marketplace searches 20 million listing embeddings with filters on city and category. For small cities, queries often return two or three results instead of twenty.
How to solve it
The ANN index returns the nearest candidates first and the filter removes most of them afterwards. In pgvector 0.8+, enable iterative scans with SET hnsw.iterative_scan = relaxed_order and raise hnsw.max_scan_tuples if needed. For very selective filters, let the planner use a B-tree index on city and do exact distance over the small subset, or create partial HNSW indexes per large category. In a dedicated engine like Qdrant, create payload indexes on city and category so filtering happens during graph traversal. Validate with recall tests on small-city queries, not only global averages.
Upgrading the embedding model
Problem
A team wants to move to a newer embedding model with better multilingual quality. They have 8 million chunks embedded with the old model, and search must keep working.
How to solve it
Add a new column (or collection) for the new model's vectors with its own dimension and index; never mix them. Backfill in the background with a rate-limited worker, newest and most-viewed content first. While backfilling, new and changed documents get both embeddings. Run an offline evaluation on a labelled query set and a shadow comparison on live queries. When the new model wins, switch reads behind a feature flag, monitor click-through and zero-result rate, keep the old vectors for a rollback window, then drop the old column and index to free memory.
Cheat sheet
- Enable pgvector
CREATE EXTENSION IF NOT EXISTS vector;- Cosine HNSW index
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);- Nearest by cosine distance
SELECT id FROM items ORDER BY embedding <=> $1 LIMIT 10;- Higher recall at query time
SET hnsw.ef_search = 100;- Filtered queries keep scanning
SET hnsw.iterative_scan = relaxed_order;- Half-precision storage
embedding halfvec(1024) -- index with halfvec_cosine_ops- RRF score for one list
score = 1.0 / (60 + rank)- Qdrant collection
PUT /collections/docs { "vectors": { "size": 1024, "distance": "Cosine" } }- Exact baseline for recall tests
SET enable_indexscan = off; -- then run the same ORDER BY query
Questions & answers
Q1What is an embedding?
An embedding is a fixed-length list of numbers that represents the meaning of some content, produced by a trained model. The model places similar items close together in this vector space, so “reset my password” and “I forgot my login” end up near each other even without shared words. You compare embeddings with cosine similarity, dot product or Euclidean distance. Embeddings are only comparable when they come from the same model and version. They are the bridge between unstructured content and fast mathematical search, and they power semantic search, recommendations, clustering and RAG.
Q2Do I need a dedicated vector database?
Often not at the start. If you already use PostgreSQL, pgvector handles millions of vectors with HNSW indexes, SQL filters, transactions and your existing backups. Elasticsearch, OpenSearch and Redis also support vector search. A dedicated engine such as Qdrant, Milvus, Weaviate or Pinecone makes sense with tens or hundreds of millions of vectors, heavy filtered search, strict latency at high concurrency, or features like advanced quantization and multi-vector search. The cost is another system to operate and keep in sync. I would start with what you have, measure, and move when numbers justify it.
Q3How does HNSW work?
HNSW builds a multi-layer graph where each vector is a node connected to its close neighbours. The top layers contain few nodes with long links, like motorways; lower layers contain more nodes with short links, like local streets, and the bottom layer contains all vectors. A search starts at an entry point on the top layer, greedily moves to the neighbour closest to the query, then drops a layer and repeats, ending with a local search on the bottom layer. Parameters m (links per node) and ef_construction control build quality and memory; ef_search controls recall versus speed at query time.
Q4What is hybrid search and why use it?
Hybrid search combines keyword search, usually BM25, with vector similarity and merges the results. Each method fails differently: vectors understand paraphrases but can miss exact terms like product codes or names, while keyword search matches exact tokens but misses synonyms and intent. Running both and merging them with reciprocal rank fusion gives more robust results than either alone in most evaluations. Many engines support it natively: Elasticsearch and OpenSearch, Weaviate, Qdrant with sparse vectors, or PostgreSQL by combining full-text and pgvector in one query. A reranker on the merged top results improves precision further.
Q5How do you handle filters, such as per-tenant data, in vector search?
The filter must be part of the search, not applied afterwards. If you take the top 10 nearest vectors and then remove other tenants' results, you may end up with nothing. Dedicated engines apply payload filters during graph traversal and index payload fields for this. In pgvector, use a WHERE clause; since version 0.8, iterative index scans (hnsw.iterative_scan) keep searching until enough rows pass the filter, and for very selective filters a B-tree index or partial index per tenant can be better. For strict isolation, separate collections or partitions per tenant are an option at the cost of more objects.
Q6How do you choose an embedding model?
Start with requirements: languages, content type (code, legal, product data), maximum input length, hosting (API or self-hosted for privacy) and cost. Shortlist a few models using public benchmarks such as MTEB, then evaluate on your own data: collect 50 to 200 real queries with the documents that should be found, and measure recall at 10 and MRR for each model. Consider dimensions: larger vectors can be more accurate but cost memory; many modern models support shorter truncated vectors (Matryoshka embeddings) with small quality loss. Whatever you pick, store the model version so you can migrate later.
Senior-level questions
S1pgvector or a dedicated vector database for 50 million vectors?
It is possible with pgvector, but trade-offs sharpen. 50 million 1,024-dimension float vectors are about 200 GB raw; with halfvec or binary quantization plus rescoring, the index can fit a large machine's RAM, and you keep SQL, transactions and one backup story. But HNSW builds take hours, heavy write rates make maintenance harder, and PostgreSQL scales vertically, so very high query concurrency means read replicas. A dedicated engine shards automatically, filters during traversal with payload indexes, and has quantization and tiered storage built in, at the cost of a second system and a sync pipeline. I would benchmark both on real data with realistic filters and target p95 latency and recall, then decide on cost and operational fit.
S2How do you evaluate a retrieval system end to end?
Separate retrieval quality from generation quality. For retrieval, build a labelled set of real queries with relevant chunks, and measure recall at k, MRR or nDCG for each configuration: chunk size, embedding model, hybrid weights, reranker. Measure index recall separately against exact search, so you know whether misses come from the ANN index or from the embeddings. Track latency at p95 and p99 and cost per query. Online, monitor zero-result rate, click-through or answer acceptance, and sample failures for review. Keep the evaluation set in version control and run it in CI when anything changes, so a model or chunking change cannot silently degrade results.
S3How would you design multi-tenant vector search with strict isolation?
There are three levels. Shared index with a tenant filter is the most efficient: one HNSW graph, tenant_id as an indexed payload or WHERE clause, and row-level security as a second guard. It risks leaks only if filters are missing, so enforce them in a data-access layer and test it. Partitioned storage, such as PostgreSQL partitions per large tenant or Qdrant's tenant-aware sharding, keeps each tenant's graph smaller and filtering cheaper. Collection or database per tenant gives the strongest isolation and easy deletion, but thousands of small indexes waste memory and complicate operations. A common design is shared storage for small tenants and dedicated partitions or collections for large or regulated ones.
Glossary
- Embedding
- A list of numbers representing the meaning of content, produced by a trained model.
- Cosine similarity
- Similarity measured by the angle between two vectors; 1 means same direction.
- ANN
- Approximate nearest neighbour search: fast search that may miss a few true neighbours.
- HNSW
- A layered graph index for ANN search with excellent speed and recall.
- Recall
- The share of the true nearest neighbours that the index actually returns.
- Quantization
- Storing vectors with fewer bits to save memory, usually with a small accuracy loss.
- Hybrid search
- Combining keyword and vector search results into one ranked list.
- Reranker
- A model that re-scores a short list of candidates by reading query and document together.
- Chunking
- Splitting documents into smaller passages so each embedding captures a focused meaning.
Keep learning
Have a product in mind?Let's build it.
Tell me about your idea, your product or your team. I usually reply within one working day.