Module 4.3 — ANN Search, Top-K & Metadata Filtering

The search mechanics that power every RAG system — in depth


What This Module Covers

Last module gave you the big picture of how vector databases work.

This module goes one level deeper — into the actual search mechanics you will control as a developer.

When you query a vector database in your RAG application — you make decisions about:

How many results to return     → Top-K
How to filter by metadata      → Metadata Filtering  
How to balance speed vs quality → Search Parameters

Getting these right is the difference between a RAG system that works and one that works well.


Part 1 — Top-K Search

What it Means

Top-K simply means:

Return the K most similar vectors to my query.

K = 1  → return only the single best match
K = 5  → return top 5 most similar results
K = 10 → return top 10 most similar results

You've already seen this. But let's go deeper into what it actually returns and how you use it.


What a Top-K Result Looks Like

When you query a vector database with K=5, you get back something like this:


    // Query result — top 5 most similar chunks
    [
        {
            id: "handbook_chunk_47",
            score: 0.9421,
            // cosine similarity score — how close this is to your query
            // 0.94 = very similar

            text: "Common side effects of aspirin include nausea, stomach pain, and heartburn.In rare cases...",
            // the original text chunk that was stored

            metadata: {
                source: "medical_handbook.pdf",
                page: 23,
                category: "side_effects",
                lastUpdated: "2024-01"
            }
            // all the extra info stored alongside the vector
        },
        {
            id: "research_paper_chunk_12",
            score: 0.8876,
            text: "Aspirin has been associated with gastrointestinal bleeding in approximately 2% of long- term users...",
            metadata: {
                source: "research_paper_2023.pdf",
                page: 7,
                category: "clinical_studies",
                lastUpdated: "2023-11"
            }
        },
        {
            id: "drug_guide_chunk_89",
            score: 0.8234,
            text: "Patients taking aspirin regularly should be aware of potential bleeding risks...",
            metadata: { source: "drug_guide.pdf", page: 45 }
        },
        {
            id: "faq_chunk_3",
            score: 0.7123,
            text: "Is aspirin safe for everyone? Aspirin is not recommended for children under 16...",
            metadata: { source: "faq.pdf", page: 2 }
        },
        {
            id: "history_chunk_5",
            score: 0.6234,
            text: "The history of aspirin dates back to ancient willow bark used by Egyptians...",
            metadata: { source: "aspirin_history.pdf", page: 1 }
        }
    ]

Notice the scores drop from 0.94 down to 0.62. The first three are clearly relevant. The last two are less relevant but still returned because K=5.


Choosing the Right K — Practical Guide

This is a real decision you make in every RAG system:

K too small (e.g. K=1 or K=2):

Risk: Miss important context
Example: User asks a complex question that needs 
         information from multiple document sections
         → K=1 only returns one chunk
         → LLM doesn't have enough context
         → Answer is incomplete

K too large (e.g. K=20 or K=50):

Risk 1: Context window fills up
→ 20 chunks × 500 words each = 10,000 words
→ Might exceed context limit
→ Definitely expensive

Risk 2: Noisy context confuses the LLM
→ Irrelevant chunks get included
→ LLM gets confused by contradictory information
→ Answer quality drops

Risk 3: Higher cost
→ More tokens injected = more tokens billed

The sweet spot for most RAG applications:

K = 3 to 5   → good starting point for most use cases
K = 5 to 10  → when questions need broader context
K = 1 to 3   → when you want precise, focused answers

In production you'll tune this based on testing. Start with K=5 and adjust.


Score Thresholds — Filtering by Quality

Top-K always returns exactly K results — even if some are not very relevant.

To avoid injecting low-quality chunks into your LLM prompt — you can add a score threshold:


    const results = await vectorDB.query({
        vector: questionEmbedding,
        topK: 10,
        // get up to 10 results

        scoreThreshold: 0.70,
        // but only return results with similarity score above 0.70
        // results below 0.70 are dropped even if K hasn't been reached
    });

    // Example outcome:
    // Requested K=10 but only 4 chunks scored above 0.70
    // → returns 4 results (not 10)
    // Better to have 4 relevant chunks than 10 with 6 bad ones

Typical thresholds:

> 0.90  → nearly identical meaning    (very strict)
> 0.75  → very relevant               (strict — good for focused tasks)
> 0.65  → reasonably relevant         (balanced — good default)
> 0.50  → somewhat related            (loose — more results, less precise)
< 0.50  → probably not relevant       (usually not worth including)

Part 2 — Metadata Filtering

Why Metadata Filtering Exists

Pure vector search finds semantically similar content. But sometimes similarity alone is not enough.

Real scenarios where you need more:

Scenario 1 — Multi-tenant app
You have 100 companies using your RAG product.
Each company's documents must stay separate.
User from Company A must NEVER see Company B's data.

Pure vector search:
→ Might return similar chunks from ANY company ❌

Vector search + metadata filter:
→ WHERE companyId = "company_A" ✓


Scenario 2 — Time-sensitive information
User asks about "current tax rates"
Your database has tax documents from 2019, 2021, 2023.

Pure vector search:
→ Returns most semantically similar — might return 2019 doc ❌

Vector search + metadata filter:
→ WHERE year = 2023 ✓ (only search recent documents)


Scenario 3 — Category-specific search
Medical app — user is a doctor asking about pediatric dosage.

Pure vector search:
→ Returns all aspirin-related content including adult dosage ❌

Vector search + metadata filter:
→ WHERE patientType = "pediatric" ✓

How Metadata Filtering Works

The filter runs at the same time as the vector search — not after it.

WITHOUT filter:
Search ALL vectors → find top K similar → return results

WITH filter:
Search ONLY vectors matching the filter → find top K similar → return results

The filter narrows down the search space FIRST.
Then similarity search runs within that space.

This is important to understand because it affects your results:

Example:
Total vectors: 100,000
After filter (category = "medical"): 8,000 vectors
Top K from those 8,000: 5 results

You searched 8,000 not 100,000.
Results are all medical AND most similar to your question.

Metadata Filtering Syntax

Different vector databases have slightly different syntax — but the concepts are the same.

Here are the common filter operations:

Exact match:


    filter: { category: "medical" }
    // only return vectors where category equals "medical"

Multiple conditions (AND):


    filter: {
        category: "medical",
        language: "english"
    }
    // category = "medical" AND language = "english"

Comparison operators:


    filter: {
        year: { $gte: 2022 },
        // year greater than or equal to 2022

        pageCount: { $lte: 100 }
        // pageCount less than or equal to 100
    }

OR conditions:


    filter: {
        $or: [
            { category: "medical" },
            { category: "pharmaceutical" }
        ]
    }
    // category = "medical" OR category = "pharmaceutical"

IN operator:


    filter: {
        source: { $in: ["handbook.pdf", "guidelines.pdf", "faq.pdf"] }
    }
    // source is one of these three files

NOT operator:


    filter: {
        category: { $ne: "archived" }
    }
    // category is NOT "archived"


Designing Good Metadata — Practical Tips

The metadata you store when indexing determines what filters you can apply when searching.

Think ahead — what filters will your users need?

Bad metadata design:


    // Storing too little
    {
        source: "document1.pdf"
    }
    // Can only filter by filename — not very useful

Good metadata design:


    // Storing everything useful
    {
    source: "medical_handbook_2023.pdf",
    // which file this came from

    page: 47,
    // which page in the file

    chunkIndex: 3,
    // which chunk on that page (for ordering)

    category: "side_effects",
    // what type of content

    documentType: "handbook",
    // what type of document

    specialty: "cardiology",
    // medical specialty

    lastUpdated: "2024-01-15",
    // when this document was last updated

    authorVerified: true,
    // quality flag

    language: "english",
    // language of the content

    companyId: "company_abc",
    // for multi-tenant apps — critical for data isolation
    }

Good metadata makes your RAG system dramatically more useful.


Part 3 — Search Quality Parameters

Beyond K and filters — vector databases expose parameters that let you control the tradeoff between search speed and accuracy.


The Speed vs Accuracy Tradeoff

Remember HNSW from last module — it skips most vectors to search fast.

But how many vectors does it check? You control this.

Parameter: ef (in HNSW — also called efSearch)
→ Higher ef = check more candidates = more accurate = slower
→ Lower ef  = check fewer candidates = faster = slightly less accurate
ef = 10   → very fast, ~90% accuracy
ef = 50   → fast, ~97% accuracy    ← good default
ef = 200  → slower, ~99% accuracy
ef = 500  → slow, ~99.9% accuracy

For most RAG applications — the default ef setting is fine. You'd only tune this if:

  • You need maximum accuracy (medical, legal) → increase ef
  • You have extremely high traffic needs → decrease ef

Reranking — A Second Pass for Better Results

This is an advanced technique but important to know about early.

The problem with ANN search alone:

ANN search returns top 5 by vector similarity.
But vector similarity is not perfect.
Sometimes a chunk that's "semantically similar" 
is not actually the best answer to the specific question.

The solution — Reranking:

Step 1 — Initial ANN search
→ Get top 20 results by vector similarity (fast)
→ Cast a wide net

Step 2 — Reranker model
→ Take those 20 candidates
→ Run a more expensive but more accurate model
→ Score each one specifically for this question
→ Re-order them

Step 3 — Take top 5 from reranked results
→ Now you have the best 5 from the best 20
→ Much higher quality than top 5 from ANN alone
Without reranking:
ANN returns [A, B, C, D, E] by vector similarity
→ C and D might not actually answer the question well

With reranking:
ANN returns top 20: [A, B, C, D, E, F, G, H...]
Reranker reorders: [A, F, B, K, E] ← F and K moved up
→ Better answers, even if they weren't top 5 by similarity

We'll use reranking in Phase 5 when building the full RAG pipeline.


Part 4 — Putting It All Together

Here is a complete, realistic vector database query for a RAG system:


    async function searchDocuments(userQuestion, userId, category) {

        // Step 1 — Embed the user's question
        const questionEmbedding = await getEmbedding(userQuestion);
        // questionEmbedding = [0.23, -0.87, 0.41, ...] (1536 numbers)

        // Step 2 — Query the vector database
        const searchResults = await vectorDB.query({

            vector: questionEmbedding,
            // the question as a vector

            topK: 10,
            // get up to 10 candidates

            scoreThreshold: 0.65,
            // only results with similarity above 0.65

            filter: {
                userId: userId,
                // CRITICAL — only search this user's documents
                // prevents data leakage between users

                category: category,
                // only search within the relevant category

                isArchived: { $ne: true },
                // exclude archived documents
            },

            includeMetadata: true,
            // return metadata alongside results

            includeValues: false,
            // don't return the actual vectors (saves bandwidth)
        });

        // searchResults is now an array of up to 10 objects
        // each has: id, score, text, metadata

        // Step 3 — Filter out low quality results
        const relevantChunks = searchResults
            .filter(result => result.score > 0.65)
            // double-check threshold (some DBs handle this internally)

            .slice(0, 5);
        // take only top 5 from the results

        // relevantChunks is now array of 5 most relevant chunks
        // example:
        // [
        //   { score: 0.94, text: "Aspirin side effects include...", ... },
        //   { score: 0.88, text: "Common aspirin reactions are...", ... },
        //   ...
        // ]

        return relevantChunks;
    }  


Using the Results in Your LLM Prompt

Once you have the chunks — here's how they flow into the LLM:


    async function answerQuestion(userQuestion, userId, category) {

        // Get relevant chunks from vector DB
        const chunks = await searchDocuments(userQuestion, userId, category);

        if (chunks.length === 0) {
            return "I couldn't find relevant information to answer your question.";
            // Handle the case where nothing relevant was found
        }

        // Build context string from chunks
        const context = chunks
            .map((chunk, index) =>
                `[Source ${index + 1}: ${chunk.metadata.source}, Page ${chunk.metadata.page}]
        ${chunk.text}`
            )
            .join("\n\n");

        // context now looks like:
        // "[Source 1: medical_handbook.pdf, Page 23]
        //  Common side effects of aspirin include nausea...
        //
        //  [Source 2: research_paper.pdf, Page 7]
        //  Aspirin has been associated with gastrointestinal bleeding..."

        // Build the LLM prompt
        const prompt = `Answer the user's question using ONLY the context provided below.
        If the answer is not in the context, say "I don't have that information."
        Always mention which source your answer comes from.

        CONTEXT:
        ${context}

        QUESTION: ${userQuestion}

        ANSWER:`;

        // Send to LLM
        const response = await openai.chat.completions.create({
            model: "gpt-4o",
            messages: [{ role: "user", content: prompt }],
            temperature: 0.1,
            // low temperature = more factual, less creative
            // good for RAG where accuracy matters
        });

        return response.choices[0].message.content;
    }

This is the complete RAG search-to-answer flow. Everything we've learned in Phase 3 and Phase 4 comes together here.


The Complete Mental Model — Search to Answer

User question
      ↓
Embed question → vector [1536 numbers]
      ↓
Query Vector DB:
  → topK: 5-10
  → scoreThreshold: 0.65+
  → filter: {userId, category, ...}
      ↓
ANN search (HNSW) runs in ~2ms
      ↓
Get back: text chunks + scores + metadata
      ↓
(Optional) Rerank for better ordering
      ↓
Take top 3-5 chunks
      ↓
Build prompt: system + context + question
      ↓
Send to LLM (temperature: 0.1)
      ↓
LLM generates answer grounded in real context
      ↓
User gets accurate, sourced answer

3-Line Summary

  1. Top-K controls how many similar chunks you retrieve — K=3 to 5 is the sweet spot for most RAG systems — too small misses context, too large adds noise and cost — score thresholds ensure only genuinely relevant chunks are returned.
  2. Metadata filtering lets you combine semantic similarity with structured conditions — like userId for data isolation, date for freshness, or category for relevance — this runs before the vector search to narrow the search space first.
  3. For production quality, reranking adds a second pass — get top 20 by ANN search, then use a more precise model to reorder them, then take the top 5 — this gives much better results than ANN similarity alone.

Module 4.3 — Complete ✅

Coming up — Module 4.4 — Hands-on: Working With Chroma (Local Vector Database)

Theory is done. Now we write real code. We'll set up Chroma locally, store real embeddings, run real similarity searches, and use metadata filters — all in JavaScript. By the end you'll have a working local vector database with documents stored and searchable.


Module 4.2 — What is a Vector Database & How it Works

The solution to the problem — how vector databases make similarity search fast


Start With the Core Problem From Last Module

Last module we ended with this:

Naive vector search:
→ Compare question vector against every stored vector
→ 1 million documents = 1 million calculations
→ Each on 1536 numbers
→ Way too slow for real-time search 🐌

A vector database solves this with one key idea:

Don't search everything. Build a smart index that lets you skip most of the data and only check the promising areas.

This is called ANN — Approximate Nearest Neighbor search.

Let's understand it properly.


First — What is "Nearest Neighbor"?

Nearest neighbor simply means:

Find the vectors closest to my query vector.

Query vector (user question):
Q = [0.23, 0.87, 0.41, ...]

Stored vectors (document chunks):
A = [0.21, 0.85, 0.43, ...] ← very close to Q
B = [0.80, 0.12, 0.91, ...] ← far from Q
C = [0.22, 0.86, 0.40, ...] ← very close to Q
D = [0.11, 0.43, 0.77, ...] ← medium distance

Nearest neighbors of Q:
1st nearest → A (closest)
2nd nearest → C (second closest)

Finding the single closest vector = 1-Nearest Neighbor (1-NN) Finding the top 5 closest = 5-Nearest Neighbor (5-NN) Finding the top K closest = K-Nearest Neighbor (KNN)

In RAG — you typically search for top 3, 5, or 10 nearest neighbors. These become the document chunks you inject into your LLM prompt.


Exact KNN vs Approximate KNN

There are two approaches:

Exact KNN:

Check every single vector
Calculate similarity with all of them
Sort all results
Return top K

Result: 100% accurate
Speed: Very slow at scale 🐌

Approximate KNN (ANN):

Use a smart index to skip most vectors
Only check the "promising" areas
Return top K from those areas

Result: 95-99% accurate (might miss rare edge cases)
Speed: Milliseconds even at billions of vectors ⚡

The trade-off is tiny. You lose maybe 1-5% accuracy. You gain 1000x speed.

For RAG applications — this trade-off is completely worth it. Missing one slightly relevant document chunk almost never matters when you're returning 5-10 results anyway.


How ANN Indexing Works — The HNSW Algorithm

The most popular ANN algorithm used in vector databases is called HNSW — Hierarchical Navigable Small World.

The name sounds scary. The concept is actually beautiful.

Let me explain it with a real life analogy first.


The Analogy — Finding a Restaurant in a New City

Imagine you arrive in a new city and want to find the best pizza place near your hotel.

Naive approach (Exact KNN):

Visit every single restaurant in the city
Try the pizza at each one
Compare all of them
Pick the best

→ Accurate but takes weeks 🐌

Smart approach (HNSW):

Step 1 — Start at the city level (zoom out)
→ You know pizza places cluster in the city center
→ Head toward the city center generally

Step 2 — Zoom into neighborhood level
→ Now you're in the right area
→ Ask locals "where's the good pizza around here?"
→ They point to the Italian neighborhood

Step 3 — Zoom into street level
→ You're on the right street
→ Walk and compare the few pizza places here
→ Pick the best one

→ Fast AND finds a great result ⚡

You didn't visit every restaurant. You navigated intelligently — going from broad to specific.

HNSW does exactly this — but with vectors in multi-dimensional space.


How HNSW Actually Works

HNSW builds a multi-layer graph of your vectors.

LAYER 2 (top — few vectors, long connections)
  A ─────────────────── E
  
LAYER 1 (middle — more vectors, medium connections)  
  A ──── B ──── C ──── E
              │
              D
              
LAYER 0 (bottom — all vectors, short connections)
  A ─ F ─ B ─ G ─ C ─ H ─ D ─ I ─ E ─ J

Layer 2 — a small number of vectors with long-range connections. Good for big jumps across the space.

Layer 1 — more vectors, medium connections. Good for getting close.

Layer 0 — all vectors, short connections. Good for fine-tuning the final result.

Search process:

Query comes in: Q = [0.23, 0.87, ...]

Step 1 — Start at Layer 2 (top)
→ Start at a random entry point
→ Move to whichever neighbor is closest to Q
→ Keep moving until no neighbor is closer
→ You've found the approximate area in Layer 2

Step 2 — Drop to Layer 1
→ Use your Layer 2 position as starting point
→ Navigate again — move to closest neighbors
→ Zoom in further

Step 3 — Drop to Layer 0 (bottom)
→ Now you're very close to the target area
→ Check all nearby vectors
→ Return the closest ones

Result: Found nearest neighbors in milliseconds
without checking 99% of the data

The Numbers — Why This is So Fast

Let's make this concrete with real scale:

Without ANN index (naive):
1,000,000 vectors
Each comparison: ~0.001ms
Total: 1,000,000 × 0.001ms = 1,000ms = 1 second per query 🐌

With HNSW index:
1,000,000 vectors
HNSW checks roughly: log(1,000,000) ≈ 20 vectors per layer
Total comparisons: maybe 200-500 vectors
Time: ~1-5ms per query ⚡

Speed improvement: 200-1000x faster
Accuracy loss: less than 1%

At 1 billion vectors — the difference becomes even more dramatic.


Anatomy of a Vector Database

A vector database is not just a search engine. It stores and manages several things together:

┌────────────────────────────────────────────────────┐
│              VECTOR DATABASE                       │
│                                                    │
│  ┌─────────────────────────────────────────────┐   │
│  │  VECTOR INDEX (HNSW or similar)             │   │
│  │  The fast search structure                  │   │
│  │  Stores: relationships between vectors      │   │
│  └─────────────────────────────────────────────┘   │
│                                                    │
│  ┌─────────────────────────────────────────────┐   │
│  │  VECTORS                                    │   │
│  │  The actual embedding arrays                │   │
│  │  [0.23, -0.87, 0.41, ...] × 1536            │   │
│  └─────────────────────────────────────────────┘   │
│                                                    │
│  ┌─────────────────────────────────────────────┐   │
│  │  METADATA                                   │   │
│  │  Extra information about each vector        │   │
│  │  { source: "doc1.pdf", page: 3,             │   │
│  │    category: "medical", date: "2024-01" }   │   │
│  └─────────────────────────────────────────────┘   │
│                                                    │
│  ┌─────────────────────────────────────────────┐   │
│  │  ORIGINAL TEXT (optional)                   │   │
│  │  The actual text chunk that was embedded    │   │
│  │  "Aspirin can cause stomach bleeding..."    │   │
│  └─────────────────────────────────────────────┘   │
└────────────────────────────────────────────────────┘

When you search — you get back not just vectors but the full record including the original text and metadata. This is what you inject into your LLM prompt.


Metadata Filtering — The Power Feature

This is where vector databases become really powerful for real applications.

Pure vector search gives you:

"Find the 10 most similar document chunks to this question"

Metadata filtering gives you:

"Find the 10 most similar document chunks to this question
 WHERE source = 'medical_handbook.pdf'
 AND date > '2023-01-01'
 AND language = 'english'"

You combine semantic similarity with structured filters — just like SQL WHERE clauses — but alongside vector search.

Real world examples:


    // E-commerce: similar products but only in stock
    search(queryVector, {
        filter: { inStock: true, category: "electronics" }
    })

    // Legal: similar cases but only from specific jurisdiction
    search(queryVector, {
        filter: { jurisdiction: "California", year: { gte: 2020 } }
    })

    // Medical: similar symptoms but only pediatric cases
    search(queryVector, {
        filter: { patientAge: { lte: 18 }, verified: true }
    })

This combination of vector similarity + metadata filtering is what makes vector databases production-ready.


Popular Vector Databases

There are several good options. Here are the main ones you'll hear about:


Pinecone

Type: Cloud-only (managed service)
Best for: Production apps, teams who don't want to manage infra
Pricing: Free tier available, then pay per usage
Setup: Very easy — just an API
Scaling: Handles billions of vectors automatically
Our course: We'll use this for cloud deployment

Chroma

Type: Open source, runs locally
Best for: Development, learning, small projects
Pricing: Free (self-hosted)
Setup: npm install, runs on your machine
Scaling: Good for small to medium scale
Our course: We'll use this first — perfect for learning

Qdrant

Type: Open source, can self-host or use cloud
Best for: High performance production systems
Pricing: Free self-hosted, paid cloud option
Setup: Medium complexity
Scaling: Excellent — very high performance

Weaviate

Type: Open source, can self-host or use cloud
Best for: Complex RAG with built-in ML features
Pricing: Free self-hosted, paid cloud option
Setup: More complex
Scaling: Excellent

Milvus

Type: Open source, self-hosted
Best for: Very large scale enterprise systems
Pricing: Free self-hosted
Setup: Complex
Scaling: Best-in-class for massive scale

pgvector (PostgreSQL extension)

Type: Extension to PostgreSQL
Best for: Teams already using PostgreSQL, smaller scale
Pricing: Free
Setup: Easy if you already have PostgreSQL
Scaling: Good up to ~10 million vectors

Which One For This Course?

Phase 4 learning → Chroma (local, no setup, free)
Phase 5 RAG project → Chroma first, then Pinecone
Production projects → Pinecone or Qdrant

We start with Chroma because:

  • Runs entirely on your machine
  • No API key needed
  • Perfect for learning
  • JavaScript support is great
  • You can see exactly what's happening

The Core Operations — What You Do With a Vector DB

Every vector database supports these basic operations:

1. Upsert — Store a vector


    // Store a document chunk with its embedding
    await vectorDB.upsert({
        id: "doc1_chunk3",
        // unique ID for this chunk
        // example: "medical_handbook_page_5_chunk_2"

        vector: [0.23, -0.87, 0.41, ...],
        // the embedding — 1536 numbers
        // generated by OpenAI embedding model

        metadata: {
            source: "medical_handbook.pdf",
            page: 5,
            category: "side_effects"
        },
        // any extra info you want to store alongside the vector

        text: "Aspirin can cause stomach bleeding..."
        // the original text chunk (optional but useful)
    });

2. Query — Find similar vectors


    // Find top 5 most similar chunks to a question
    const results = await vectorDB.query({
        vector: [0.19, -0.82, 0.38, ...],
        // embedding of the user's question

        topK: 5,
        // return top 5 most similar results

        filter: { category: "side_effects" },
        // optional metadata filter

        includeMetadata: true,
        // return the metadata alongside results
    });

    // results looks like:
    // [
    //   { id: "doc1_chunk3", score: 0.94, text: "Aspirin can cause...", metadata: {...} },
    //   { id: "doc2_chunk7", score: 0.87, text: "Common side effects...", metadata: {...} },
    //   ...
    // ]

3. Delete — Remove a vector


    // Remove a specific chunk (e.g. document was updated)
    await vectorDB.delete({ id: "doc1_chunk3" });

4. Fetch — Get a specific vector by ID


    // Get a specific stored vector
    const item = await vectorDB.fetch({ id: "doc1_chunk3" });

These four operations — upsert, query, delete, fetch — are 90% of what you'll do with a vector database.


The Complete Flow — Putting It All Together

Here is the full picture of how a RAG system uses a vector database:

SETUP PHASE — done once when you build the system:

PDF / Documents
      ↓
Split into chunks (e.g. 500 words each)
      ↓
For each chunk:
  → Call OpenAI embeddings API
  → Get vector [1536 numbers]
  → Store in Vector DB with metadata
      ↓
Vector DB builds HNSW index
      ↓
System is ready


QUERY PHASE — happens every time a user asks something:

User question: "What are aspirin side effects?"
      ↓
Call OpenAI embeddings API on the question
      ↓
Get question vector [1536 numbers]
      ↓
Query Vector DB with that vector (topK: 5)
      ↓
HNSW index finds top 5 similar chunks in ~2ms
      ↓
Get back 5 text chunks + their metadata
      ↓
Build LLM prompt:
  "Answer this question using only the context below:
   
   Context:
   [chunk 1 text]
   [chunk 2 text]
   [chunk 3 text]
   
   Question: What are aspirin side effects?"
      ↓
Send to GPT-4
      ↓
Get accurate, grounded answer
      ↓
Show to user

This is RAG. And vector database is the heart of it.


A Real Life Analogy — The Smart Library

Think of a vector database like a smart library — but not organized by title or author.

This library organizes books by topic similarity.

When you add a book:
→ Librarian reads it
→ Understands what it's about
→ Places it near books about similar topics
→ Books about space travel cluster together
→ Books about cooking cluster together
→ The shelves arrange themselves by meaning

When you search:
→ You describe what you're looking for
→ Librarian understands the meaning of your description
→ Goes directly to the right area of the library
→ Returns the closest matching books

You don't need to know the title, author, or exact words.
You just describe what you need — and meaning does the matching.

3-Line Summary

  1. A vector database uses ANN indexing (like HNSW) to find similar vectors in milliseconds — instead of comparing against every single vector, it navigates a smart multi-layer graph to zoom in on the right area.
  2. Vector databases store three things together — the vector itself, metadata (source, date, category), and optionally the original text — metadata filtering lets you combine semantic search with structured filters just like SQL WHERE clauses.
  3. The four core operations are upsert (store), query (find similar), delete (remove), and fetch (get by ID) — query with topK is what powers RAG, returning the most relevant document chunks to inject into your LLM prompt.

Module 4.2 — Complete ✅

Coming up — Module 4.3 — ANN Search, Top-K & Metadata Filtering in Depth

We go deeper on the search mechanics. How exactly does Top-K work in practice? What happens when you combine vector search with metadata filters? What are the parameters that control search quality? And what tradeoffs do you make in production? All of that — next.

Module 4.1 — Why SQL is Not Enough

Understanding the problem before learning the solution


Start With What You Already Know

You're a MERN developer. You've used MongoDB. Maybe you've used PostgreSQL or MySQL too.

You know how databases work:

Store data → Query data → Get results

Simple. Reliable. Fast.

So when someone says "we need a special database just for AI" — your first reaction might be:

"Why? Can't I just use MongoDB or PostgreSQL? I already know those."

That's a completely fair question.

And the answer is not "SQL/MongoDB is bad." They're great tools. But they were built to solve a completely different problem than what AI applications need.

This module explains exactly why.


What Traditional Databases Are Good At

Let's say you have a database of products:

┌────┬─────────────┬──────────┬───────────┐
│ id │ name        │ category │ price     │
├────┼─────────────┼──────────┼───────────┤
│ 1  │ iPhone 15   │ phones   │ 999       │
│ 2  │ Samsung S24 │ phones   │ 899       │
│ 3  │ MacBook Pro │ laptops  │ 1999      │
│ 4  │ Pizza Oven  │ kitchen  │ 299       │
└────┴─────────────┴──────────┴───────────┘

Traditional databases are incredible at questions like:

-- Find exact match
SELECT * FROM products WHERE name = 'iPhone 15';

-- Find by category
SELECT * FROM products WHERE category = 'phones';

-- Find by price range
SELECT * FROM products WHERE price < 1000;

-- Find with multiple conditions
SELECT * FROM products 
WHERE category = 'phones' AND price < 950;

These queries are:

  • Fast ⚡
  • Exact ✓
  • Predictable ✓
  • Scale to millions of rows ✓

Traditional databases are optimized for exact matching and range queries.


Where Traditional Databases Completely Fail

Now let's change the question slightly.

Instead of:

"Find products where category = 'phones'"

The user asks:

"Find me something to communicate with people far away"

Think about how you'd write this SQL query.

-- How do you search for "communicate with people far away"?

SELECT * FROM products WHERE name = 'communicate with people far away';
-- ❌ No exact match

SELECT * FROM products WHERE description LIKE '%communicate%';
-- ❌ "communicate" not in any description

SELECT * FROM products WHERE description LIKE '%far away%';
-- ❌ Still nothing

You can't. Because no product has those exact words. But clearly — an iPhone is the right answer. It lets you communicate with people far away.

The meaning matches perfectly. The words match zero.

This is the fundamental problem.

Traditional databases search by exact words. AI applications need to search by meaning.


The Real World Examples Where This Breaks

Example 1 — Customer Support Bot

User asks:

"My app keeps crashing when I open it"

Your database has a FAQ entry:

"Application fails to launch on startup"

SQL search:

SELECT * FROM faqs 
WHERE question LIKE '%crashing%' 
   OR question LIKE '%open%';
-- ❌ Returns nothing
-- "crashing" ≠ "fails to launch"
-- "open" ≠ "startup"

Meaning is identical. Words are different. SQL fails.

Example 2 — Document Search

User asks:

"What's the company's policy on working from home?"

Your document database has:

"Remote work guidelines and expectations for employees"

SQL search:

SELECT * FROM documents 
WHERE content LIKE '%working from home%';
-- ❌ Returns nothing
-- Document uses "remote work" not "working from home"

Same concept. Different words. SQL fails.

Example 3 — E-commerce Recommendation

User searches:

"cozy winter footwear"

Your product database has:

"Warm insulated snow boots"
SELECT * FROM products 
WHERE name LIKE '%cozy%' 
   OR name LIKE '%winter%' 
   OR name LIKE '%footwear%';
-- ❌ Returns nothing
-- None of those words appear in "Warm insulated snow boots"

SQL fails again.


What About Full-Text Search?

You might be thinking — "SQL has full-text search. That's better than LIKE queries."

You're right — full-text search is better. It handles things like:

- Stemming: "running" matches "run", "runs", "runner"
- Stop words: ignores "the", "a", "is"
- Relevance ranking: more keyword matches = higher rank

But it still fundamentally searches by words — not meaning.

Full-text search for: "remote work policy"

Finds documents containing: "remote", "work", "policy"
Misses documents containing: "work from home guidelines"
                              "telecommuting expectations"
                              "off-site employee rules"

All three mean the same thing. Full-text search misses them all.

Full-text search = better keyword matching. Not meaning matching.


The Vector Problem

Here's the deeper technical issue.

An embedding is a vector — a list of 1,536 numbers.

"iPhone" embedding:
[0.23, -0.87, 0.41, 0.92, -0.13, 0.67, ... × 1536]

To find similar products — you need to find vectors that are close in 1,536-dimensional space.

How would you store and search this in a regular database?

Option 1 — Store as a JSON column

-- PostgreSQL
ALTER TABLE products ADD COLUMN embedding JSONB;

-- Store it
UPDATE products SET embedding = '[0.23, -0.87, 0.41, ...]'
WHERE id = 1;

Storing works. But now how do you search?

-- Find the most similar product to a given vector?
-- You'd have to:
-- 1. Load ALL rows into memory
-- 2. Calculate cosine similarity for each one
-- 3. Sort by similarity
-- 4. Return top results

-- This is called a "full table scan"
-- With 1 million products = 1 million cosine similarity calculations
-- Each calculation on 1536 numbers
-- = billions of operations
-- = extremely slow 🐌

Option 2 — Use PostgreSQL's pgvector extension

PostgreSQL actually has a vector extension called pgvector. It adds vector support.

-- With pgvector
SELECT * FROM products
ORDER BY embedding <-> '[0.23, -0.87, 0.41, ...]'
LIMIT 5;

This works. And for small to medium scale — pgvector is actually a reasonable choice.

But it has limitations at large scale. And it's an add-on to a system not designed for this. A purpose-built vector database handles this much better.


What Makes Vector Search Hard

Here's the core technical challenge.

With regular data — you can use indexes to search fast:

Regular database index (B-tree):
Data sorted in order → binary search → find exact match fast

Example:
Find price = 999
[100, 200, 500, 750, 999, 1200, 1500]
                         ↑ found in log(n) steps

With vectors — you can't sort them. Vectors live in 1,536-dimensional space. There's no simple "sorted order" that works for all directions.

Finding nearest vector to [0.23, -0.87, 0.41, ...]:

Naive approach:
→ Compare against every single stored vector
→ Calculate cosine similarity each time
→ Sort results
→ Return top K

With 1 million documents:
→ 1,000,000 cosine similarity calculations
→ Each on 1,536 numbers
→ Way too slow for real-time search

This problem — finding nearest neighbors in high-dimensional space efficiently — is one of the hardest problems in computer science.

Vector databases are built specifically to solve this using clever indexing algorithms.


The Specific Things Traditional Databases Lack

Here is a clean summary of what's missing:

┌─────────────────────────┬───────────────┬──────────────────┐
│ Feature                 │ SQL/MongoDB   │ Vector DB        │
├─────────────────────────┼───────────────┼──────────────────┤
│ Store vectors natively  │ ❌ workaround │ ✅ built for it │
│ Similarity search       │ ❌ very slow  │ ✅ milliseconds │
│ Semantic search         │ ❌ impossible │ ✅ core feature │
│ ANN indexing            │ ❌ not built  │ ✅ optimized    │
│ Handle 1B+ vectors      │ ❌ struggles  │ ✅ designed for │
│ Metadata filtering      │ ✅ great      │ ✅ supported    │
│ Exact match queries     │ ✅ great      │ ⚠️ not the focus│
│ Joins, relations        │ ✅ great      │ ❌ not designed │
│ Transactions (ACID)     │ ✅ great      │ ⚠️ limited      │
└─────────────────────────┴───────────────┴──────────────────┘

Notice — it's not that one is better than the other overall. They solve different problems.

In real production apps — you often use BOTH:

PostgreSQL/MongoDB  → stores user data, orders, products, etc.
Vector Database     → stores embeddings for semantic search

A Real Life Analogy — Two Different Filing Systems

Imagine a law firm with two filing systems:

Filing System 1 — Traditional (SQL-style):

Organized by:
- Client name (alphabetical)
- Case number (numerical)
- Date filed (chronological)

Perfect for:
"Find all cases filed by John Smith in 2023"
→ Go to J section → find Smith → filter by 2023 → done

Terrible for:
"Find all cases that felt similar to the Johnson case"
→ How do you even start? There's no "similarity" shelf

Filing System 2 — Vector-style:

Organized by:
- Cases plotted in "meaning space"
- Similar cases placed physically near each other
- Corporate fraud cases cluster together
- Personal injury cases cluster together
- Contract disputes cluster together

Perfect for:
"Find cases similar to the Johnson case"
→ Find Johnson's location in the space
→ Look at nearby cases
→ Done

Awkward for:
"Find all cases filed by John Smith"
→ Smith's cases are scattered across the space
→ No easy way to gather them

Neither filing system is "better." They're optimized for different types of questions.

Your AI application needs the second type of filing system — and that's what a vector database provides.


So What Exactly IS a Vector Database?

A vector database is a database built from the ground up to:

1. Store vectors (embeddings) efficiently
2. Search for similar vectors in milliseconds
3. Scale to millions or billions of vectors
4. Filter results by metadata alongside similarity

The key technology inside is called ANN — Approximate Nearest Neighbor search.

We'll go deep on this in the next module (4.2).

For now just understand:

Traditional DB = optimized for exact matching
Vector DB      = optimized for similarity matching

Where This Fits in Your RAG Pipeline

Here is where the vector database lives in the system you'll build:

INDEXING PHASE (done once, or when docs update):
  Your documents (PDFs, articles, etc.)
          ↓
  Split into chunks
          ↓
  Embed each chunk (OpenAI API)
          ↓
  Store vectors in Vector Database ← HERE
          ↓
  Vector DB indexes them for fast search

QUERY PHASE (every time a user asks something):
  User question
          ↓
  Embed the question
          ↓
  Search Vector Database ← AND HERE
          ↓
  Get top K most similar chunks
          ↓
  Inject into LLM prompt
          ↓
  LLM generates answer

The vector database is the engine that makes RAG fast and accurate.


3-Line Summary

  1. Traditional databases search by exact words and values — they completely fail when you need to search by meaning — "remote work policy" will never find "work from home guidelines" in SQL.
  2. Storing vectors in regular databases works technically but searching them requires comparing against every single row which becomes impossibly slow at scale — vector databases solve this with specialized indexing.
  3. Vector databases and SQL databases solve different problems — production AI apps use both — SQL for structured data like users and orders, vector databases for semantic search and RAG.

Module 4.1 — Complete ✅

Coming up — Module 4.2 — What is a Vector Database & How it Works

Now that you understand the problem — we learn the solution. What is ANN search? How do vector databases index millions of vectors so search takes milliseconds? What are the popular vector databases and how do they differ? All of that — next.

Module 4.3 — ANN Search, Top-K & Metadata Filtering

The search mechanics that power every RAG system — in depth What This Module Covers Last module gave you the big picture of how vector da...