Building with AI · Guide 20 of 35

Vector Databases and Similarity Search

Understand vector indexes, distance metrics and filtered retrieval, and evaluate search quality without confusing similarity with truth.

IntermediateAbout 9 min readPractical exercise

Before you start: Embeddings · Database basics. Reading time is an estimate; allow extra time for practice.

What you’ll learn

  • Explain what a vector store contributes beyond computing an embedding.
  • Store vectors with the source, model and permission metadata needed for safe retrieval.
  • Compare exact and approximate nearest-neighbor results using recall and measured cost.
  • Plan filtered queries, updates and model migrations without mixing incompatible representations.
On this page

Store and search representations

Suppose a documentation search feature has thousands of approved passages and a vector representing each one. Computing a query embedding is only the beginning. The application must find nearby vectors, recover the associated passages, apply access and version rules, and keep results synchronized when documents change. A vector store provides storage and similarity-query capabilities for that work.

A database with vector support does not create meaningful embeddings automatically, verify documents or generate answers. Those are separate pipeline responsibilities. You may use a dedicated vector service or vector support in an existing database, such as pgvector for PostgreSQL. The right choice depends on measured workload, filtering, operations and existing infrastructure; a new database is not required merely because a feature uses embeddings.

Treat metadata as part of the record

Store a stable chunk ID, document ID, source revision, text location, embedding model identifier, vector dimension and the vector itself. Include the version and access metadata needed to restrict retrieval. These fields let you answer which source a result came from, whether it is current, and whether it was embedded with a compatible model. A bare array of floats cannot answer those questions.

Use deterministic IDs or another clear replacement policy so reprocessing a document does not leave duplicate chunks. Keep authoritative permissions in a trusted system and ensure queries use current rules; copying a permission flag once during ingestion is insufficient if access later changes. Protect vectors and metadata with the source’s sensitivity in mind. A vector is a representation of information, not an anonymization guarantee.

Define the search contract before the index

Choose the distance measure expected by the embedding model. Euclidean distance measures separation, cosine compares direction, and inner product includes magnitude unless vectors are normalized. Do not swap metrics casually or compare vectors from incompatible model versions just because their dimensions match. Validate dimensions and finite numeric values before storing or querying, and preserve any normalization required by the model.

An exact nearest-neighbor search scores all eligible rows and returns the best k under the chosen metric and tie policy. It is a useful baseline because it separates retrieval approximation from embedding quality. Exact neighbors are not necessarily useful source passages: the model’s geometry can still rank an irrelevant passage first. Evaluate semantic relevance with labeled questions as well as numerical search correctness.

Measure a missed neighbor with a tiny baseline

The following local example uses invented two-dimensional vectors and squared Euclidean distance. Squaring preserves Euclidean nearest-neighbor order while avoiding a square root. The exact search finds a and b. A deliberately incomplete candidate pool contains only a and c, so it cannot recover b even though it sorts its candidates perfectly.

Run the complete snippet in Node.js or a browser console. Recall at two is the fraction of the exact top-two IDs recovered: one of two, or 0.5. This is a simulation of candidate loss, not an implementation of HNSW or IVFFlat, a semantic benchmark or a performance measurement. All rows are assumed eligible; permission filtering is a separate precondition for the baseline comparison.

JavaScript: compare candidate recall with exact neighbors
const rows = [
  { id: 'a', vector: [1, 0] },
  { id: 'b', vector: [2, 0] },
  { id: 'c', vector: [3, 0] },
];
function nearest(items, query, k) {
  if (!query.length || !query.every(Number.isFinite) || !Number.isInteger(k) || k < 1) {
    throw new Error('Invalid query');
  }
  return items.map(item => {
    if (item.vector.length !== query.length || !item.vector.every(Number.isFinite)) {
      throw new Error('Incompatible vector');
    }
    const distance = item.vector.reduce((sum, value, i) => sum + (value - query[i]) ** 2, 0);
    return { id: item.id, distance };
  }).sort((a, b) => a.distance - b.distance || a.id.localeCompare(b.id)).slice(0, k);
}
const exact = nearest(rows, [0, 0], 2);
const limited = nearest([rows[0], rows[2]], [0, 0], 2);
const exactIds = new Set(exact.map(row => row.id));
const recall = limited.filter(row => exactIds.has(row.id)).length / exact.length;
if (exact.map(row => row.id).join(',') !== 'a,b') throw new Error('Wrong baseline');
if (limited.map(row => row.id).join(',') !== 'a,c') throw new Error('Wrong candidate result');
if (recall !== 0.5) throw new Error('Wrong recall');
console.log('Exact: a,b; limited: a,c; recall@2: ' + recall);

Expected output: “Exact: a,b; limited: a,c; recall@2: 0.5”. The example assumes ordinary small arrays and modest finite magnitudes. It is not numerically robust for extreme values or a production index. Recall here compares neighbor IDs under one metric, not answer quality.

Choose approximation through measurement

Approximate nearest-neighbor indexes reduce search work by exploring a selected region or candidate structure instead of scoring every vector. HNSW uses a graph with multiple levels; IVFFlat groups vectors into lists and searches selected lists. Their build costs, memory needs, update behavior and query settings differ. In pgvector, exact search is the default; adding an approximate index can change which neighbors are returned.

Compare candidate settings against the exact baseline on representative queries using the same eligible rows, metric and k. Measure recall, latency distribution, memory, index-build time and update cost. Increase search effort when recall is inadequate, and verify the result rather than assuming a larger index fixes every error. Keep a simple exact strategy when it already meets the workload; approximation adds a tradeoff, not a universal quality improvement.

Combine relevance with allowed scope

A query often needs only one tenant, one language or one product version. Those conditions are part of the retrieval contract, not optional cleanup after passages have reached the model. Derive tenant identity from the authenticated session and enforce authorization in the database or retrieval service. A WHERE clause supplied by an untrusted client is not sufficient authority, and a prompt is not a substitute for access control.

Filtering interacts with approximate search. An index may collect candidates before a database applies a filter, leaving fewer than k eligible results or missing eligible neighbors. Evaluate filtered recall against an exact search over the same authorized set. Depending on the database, more candidate exploration, appropriate indexes, partitioning or an exact filtered query may help. None of these performance choices permits unauthorized content to leave the retrieval boundary.

For PostgreSQL row-level security, test the actual application role. Table owners normally bypass row policies, and superusers or roles with BYPASSRLS bypass them. A successful policy test under one role does not establish protection for a service connection using another. Keep the relationship between application identity and database policy explicit.

SQL query sketch: requires a configured PostgreSQL/pgvector database
SELECT chunk_id, document_id, source_revision
FROM document_chunks
WHERE tenant_id = $1
  AND model_revision = $2
ORDER BY embedding <=> $3
LIMIT $4;

Illustrative parameterized query only; it was not executed against a database. It assumes a document_chunks table, a compatible vector column and installed pgvector extension. <=> is cosine distance, so smaller values rank first. The server supplies trusted tenant scope, compatible model revision, query vector and bounded k; enforce the actual authorization policy separately and inspect the real query plan.

Plan updates, migration and evaluation together

When a source changes, replace its affected chunks and remove obsolete ones. Track source revisions so a query can avoid mixing partially updated content. When the embedding model changes, generate a compatible new set of document vectors and an appropriate index, compare retrieval quality, then switch query generation and document search together. Do not mix old and new spaces in one comparison because their vector lengths happen to match.

Test deletion, permission revocation, failed ingestion and rollback as well as successful search. Measure freshness from source change to query-visible update, and inspect how caches or replicas affect it. Keep backups and access controls appropriate to the data, record relevant configuration with evaluation results, and retain an exact comparison path for investigations. A fast nearest-neighbor response is only useful when it returns current, permitted, relevant evidence.

Put it into practice

Try it yourself

Add a fourth vector [1.5, 0] with ID d to the local example. Predict the new exact top two for [0, 0], then compare them with the unchanged limited pool a,c. Describe a test plan for the same comparison restricted to one authorized tenant.

Show hint

The exact result becomes a,d and the limited pool still recovers one of the two, giving recall 0.5. Compare identical metrics, k and eligible rows, and decide how ties are handled. Include permission revocation and a model-version mismatch in the plan. The local arithmetic does not measure a real database’s latency or semantic relevance.

Optional practice

Check your understanding

5 questions to revisit the ideas. No score is saved, and you can retry. This is separate from Skill Quiz; it does not award XP or unlock guides.

1. What does a vector store primarily add to an embedding pipeline?
2. The exact top two are a,b and the approximate result is a,c. What is recall at two?
3. Why compare approximate results with an exact baseline using the same metric and eligible rows?
4. An approximate query returns fewer than k authorized results after filtering. What is a valid response?
5. A new embedding model has the same vector dimension as the old one. Is it safe to mix them automatically?

Sources & further reading

Primary references for the concepts in this guide. Examples and exercises are illustrative; verify provider-specific behaviour against the version you use.