vector-db / VECTOR_DB_EXPLAINED.md
mittalsahab's picture
Vector DB example
b16a546
|
Raw
History Blame Contribute Delete
15.7 kB
# Vector Database β€” Implementation Guide
A walkthrough of what we built, how it works, and the core concepts you need to understand.
---
## Table of Contents
1. [Project Overview](#1-project-overview)
2. [File Structure](#2-file-structure)
3. [Core Concept: What is an Embedding?](#3-core-concept-what-is-an-embedding)
4. [Core Concept: What is a Vector Database?](#4-core-concept-what-is-a-vector-database)
5. [Core Concept: Cosine Similarity](#5-core-concept-cosine-similarity)
6. [Code Walkthrough: vector_db.py](#6-code-walkthrough-vector_dbpy)
7. [Code Walkthrough: demo.py](#7-code-walkthrough-demopy)
8. [The Data File: data.json](#8-the-data-file-datajson)
9. [Reading the Search Results](#9-reading-the-search-results)
10. [How This Scales to Production](#10-how-this-scales-to-production)
11. [Glossary](#11-glossary)
---
## 1. Project Overview
We built a **minimal vector database from scratch** using only:
| Component | What it does |
|---|---|
| `sentence-transformers` | Converts text into a list of numbers (an embedding) |
| `numpy` | Efficient math on those number lists |
| Python `json` | Persists the data to disk |
| Our own code | The search logic (cosine similarity ranking) |
The full pipeline in one diagram:
```
Your Text
|
v
[ Embedding Model ] <-- all-MiniLM-L6-v2 (runs locally on your machine)
|
v
[0.12, -0.44, 0.87, ..., 0.03] <-- 384 numbers representing the meaning
|
v
[ data.json ] <-- stored alongside your original text and metadata
|
v
[ Cosine Similarity ] <-- at search time, compare query vector vs all stored vectors
|
v
Ranked Results (most similar first)
```
---
## 2. File Structure
```
ai/
β”œβ”€β”€ vector_db.py # The VectorDB class (embed, store, search)
β”œβ”€β”€ demo.py # Demo: populates the DB and runs queries
β”œβ”€β”€ data.json # Generated file: stores all records + embeddings
└── VECTOR_DB_EXPLAINED.md # This file
```
---
## 3. Core Concept: What is an Embedding?
> **This is the most important concept in the entire project.**
An **embedding** is a list of numbers that represents the *meaning* of a piece of text.
The key insight: **texts with similar meanings get similar numbers.**
### Example
```
"Machine learning models learn patterns from data"
--> [0.12, -0.44, 0.87, 0.03, ...] (384 numbers)
"AI systems improve by training on examples"
--> [0.11, -0.41, 0.89, 0.04, ...] (384 numbers, very close!)
"Pizza originated in Naples"
--> [-0.55, 0.72, -0.10, 0.91, ...] (384 numbers, very different)
```
The two ML sentences land close together in "meaning space" even though they share **no words** in common. This is what makes semantic search powerful.
### How the model creates embeddings
The `all-MiniLM-L6-v2` model is a neural network trained on hundreds of millions of sentence pairs. During training, it learned to push similar sentences close together and push unrelated sentences apart.
When you call `model.encode("some text")`, the model passes your text through its layers and returns that final 384-number vector. You do not need to understand the neural network internals β€” just know the output is a compact numerical "fingerprint" of the meaning.
### Why 384 dimensions?
`all-MiniLM-L6-v2` produces 384-dimensional vectors. This is a design choice of the model β€” larger models like `all-mpnet-base-v2` produce 768 dimensions and are more accurate but slower. 384 is the sweet spot for speed vs. quality.
---
## 4. Core Concept: What is a Vector Database?
A **vector database** stores embeddings alongside your original data and lets you search by meaning rather than by exact keywords.
### Traditional database search (keyword)
```sql
SELECT * FROM docs WHERE text LIKE '%machine learning%'
```
This only finds documents that contain those exact words. It would miss:
- "AI training on data"
- "neural network optimization"
- "computers that learn"
### Vector database search (semantic)
```
Query: "how do computers learn from data?"
--> embed the query --> [0.12, -0.39, ...]
--> compare against every stored vector
--> return the closest ones
```
This finds documents about machine learning, AI, and training β€” even if they use completely different words.
### What makes it a "database"
A vector database needs to do three things:
1. **Store** β€” keep the embeddings (and the original data) somewhere persistent
2. **Index** β€” organize embeddings so searching is fast (our JSON version skips this; production DBs use HNSW trees)
3. **Query** β€” given a new vector, find the most similar stored vectors quickly
---
## 5. Core Concept: Cosine Similarity
This is how we measure "how similar" two vectors are.
### The intuition
Think of each embedding vector as an **arrow pointing in some direction** in 384-dimensional space. Two sentences with similar meanings point in nearly the same direction. Cosine similarity measures the **angle between those arrows**.
```
Similar meaning --> small angle --> cosine close to 1.0
Unrelated --> large angle --> cosine close to 0.0
Opposite meaning --> 180 degrees --> cosine close to -1.0
```
### The formula
```
cosine_similarity(A, B) = (A Β· B) / (|A| Γ— |B|)
```
Where:
- `A Β· B` is the **dot product**: multiply each pair of numbers and sum them up
- `|A|` is the **magnitude** of vector A: square root of the sum of squares
### Our implementation
```python
@staticmethod
def _cosine_similarity(a: list[float], b: list[float]) -> float:
dot = sum(x * y for x, y in zip(a, b)) # dot product
mag_a = math.sqrt(sum(x * x for x in a)) # |A|
mag_b = math.sqrt(sum(x * x for x in b)) # |B|
return dot / (mag_a * mag_b)
```
### Why cosine and not Euclidean distance?
Euclidean distance measures how far apart two points are. Cosine similarity measures the **angle** between them. For text embeddings, the angle matters more than the raw distance β€” a short sentence and a long sentence about the same topic will have different magnitudes but the same direction.
### Score interpretation from our demo output
```
#1 score=0.6257 ############ <- strongly related
#2 score=0.2572 ##### <- loosely related
#3 score=0.2546 ##### <- loosely related
```
Scores above 0.5 are typically strong matches. Below 0.2 is often noise. The `#` bar gives you a quick visual feel.
---
## 6. Code Walkthrough: `vector_db.py`
```python
class VectorDB:
def __init__(self, db_path: str = "data.json", model_name: str = "all-MiniLM-L6-v2"):
self.db_path = db_path
self._model_name = model_name
self._model = None # loaded lazily so imports stay fast
self._records: list[dict] = []
if os.path.exists(db_path):
self.load()
```
**Why `self._model = None` (lazy loading)?**
Loading the model takes ~1 second and ~80MB of RAM. By setting it to `None` initially and only loading it the first time `_get_model()` is called, the class is cheap to create even if you only want to read the JSON file.
---
```python
def _get_model(self):
if self._model is None:
from sentence_transformers import SentenceTransformer
self._model = SentenceTransformer(self._model_name)
return self._model
```
**Why import inside the function?**
The `from sentence_transformers import ...` line is placed inside the method (not at the top of the file). This means the slow import only happens when you actually need to embed something β€” not when the module is first loaded.
---
```python
def add(self, text: str, metadata: dict[str, Any] | None = None) -> None:
model = self._get_model()
embedding: list[float] = model.encode(text).tolist()
record = {
"id": len(self._records),
"text": text,
"metadata": metadata or {},
"embedding": embedding, # <-- the 384 numbers
}
self._records.append(record)
```
**What `model.encode(text)` returns:**
A NumPy array of shape `(384,)`. We call `.tolist()` to convert it to a plain Python list so it can be serialized to JSON. NumPy arrays cannot be written to JSON directly.
**What `metadata` is for:**
Any extra information you want to store alongside the text β€” category, source URL, date, author, etc. It is stored in JSON and returned with search results so you can use it in your application.
---
```python
def search(self, query: str, top_k: int = 5) -> list[dict]:
query_vec: list[float] = model.encode(query).tolist()
scored = [
{
"score": self._cosine_similarity(query_vec, rec["embedding"]),
...
}
for rec in self._records
]
scored.sort(key=lambda r: r["score"], reverse=True)
return scored[:top_k]
```
**The brute-force search:**
We compute cosine similarity between the query and **every single record** β€” this is called a **linear scan** or **flat search**. It is simple and exact, but slow at scale (O(n) comparisons). Production vector DBs use approximate nearest-neighbor indexes (like HNSW) to do this in O(log n).
**`reverse=True`:**
We sort highest score first because cosine similarity of 1.0 means identical. We want the best match at the top.
---
## 7. Code Walkthrough: `demo.py`
```python
DOCUMENTS = [
("Python is a high-level programming language...", {"category": "tech", "topic": "programming"}),
("Machine learning models learn patterns...", {"category": "tech", "topic": "AI/ML"}),
...
]
```
Each entry is a tuple of `(text, metadata)`. The categories are deliberately diverse so that the semantic search has to cross category boundaries β€” proving it works by meaning, not by keyword matching.
---
```python
def build_db():
db = VectorDB(db_path="data.json")
for text, meta in DOCUMENTS:
db.add(text, meta)
db.save()
return db
```
**Why `db.save()` at the end and not inside `db.add()`?**
Writing to disk on every single `add()` call would be slow β€” we batch all the embeddings in memory first, then flush to disk once. This is the same reason databases have write buffers.
---
```python
queries = [
"how do computers learn from data?", # should find AI/ML
"space and the universe", # should find astronomy
"food from Italy", # should find Italian cuisine
"athletic competition between nations", # should find Olympics
"storing and querying high-dimensional data", # should find vector DB
]
```
Notice that **none of these queries share words with their target documents**. The match is purely semantic. This is the whole point of vector search.
---
## 8. The Data File: `data.json`
Open `data.json` after running the demo. Each record looks like this:
```json
{
"id": 1,
"text": "Machine learning models learn patterns from data without being explicitly programmed.",
"metadata": {
"category": "tech",
"topic": "AI/ML"
},
"embedding": [
0.04859826713800430,
-0.04706305265426636,
0.07049278914928436,
... // 381 more numbers
0.02341187000274658
]
}
```
**Key observations:**
- The embedding has exactly **384 numbers** for every record, regardless of how long or short the text is.
- The numbers are between roughly -1 and 1 (the model normalizes them).
- The raw text and metadata are stored alongside the vector β€” this is how you get the original content back after finding a match.
- The file for 16 documents is about **430 KB**. Most of that is the embedding numbers. At 10,000 documents it would be ~270 MB β€” this is why production systems use binary formats (like `.npy` or `Parquet`) instead of JSON.
---
## 9. Reading the Search Results
Here is the actual output from our demo, annotated:
```
Query: "how do computers learn from data?"
----------------------------------------------------------------
#1 score=0.6257 ############
[tech / AI/ML]
Machine learning models learn patterns from data without being explicitly programmed.
#2 score=0.2572 #####
[tech / programming]
Python is a high-level programming language...
#3 score=0.2546 #####
[tech / database]
Vector databases store high-dimensional embeddings...
```
- **#1 (0.63)** β€” Near-perfect match. The model understood that "computers learning from data" = "machine learning models learning patterns from data."
- **#2 and #3 (0.25)** β€” Loosely related (both are tech topics) but not real matches.
- The **gap between #1 and #2** (0.63 vs 0.25) is the model confidently separating the right answer from everything else.
```
Query: "storing and querying high-dimensional data"
----------------------------------------------------------------
#1 score=0.6023 ############
[tech / database]
Vector databases store high-dimensional embeddings and support similarity search.
```
This one is almost cheating β€” the document literally contains the phrase "high-dimensional embeddings." But notice the score is still only 0.60, not 1.0, because the phrasing is different.
---
## 10. How This Scales to Production
Our implementation works for hundreds of documents. Here is what production vector databases add:
| Problem | Our solution | Production solution |
|---|---|---|
| **Search speed** | Linear scan β€” compare every record | HNSW or IVF index β€” approximate nearest neighbor in O(log n) |
| **Storage** | JSON text β€” large and slow to parse | Binary formats (`.npy`, `Parquet`, mmap) |
| **Persistence** | Single file | Distributed storage with replication |
| **Updates** | Reload entire file | Incremental inserts and deletes |
| **Scale** | ~1,000 docs before it feels slow | Pinecone, Weaviate, Qdrant handle billions |
| **Filtering** | None | Filter by metadata before or after vector search |
**Popular production vector databases:**
- **Pinecone** β€” fully managed cloud service, easiest to start with
- **Weaviate** β€” open source, supports hybrid (keyword + vector) search
- **Qdrant** β€” open source, written in Rust, very fast
- **pgvector** β€” adds vector search to regular PostgreSQL
The concepts are identical to what we built. They just replace the JSON file and the linear scan with faster infrastructure.
---
## 11. Glossary
| Term | Definition |
|---|---|
| **Embedding** | A list of numbers that represents the meaning of a piece of text |
| **Embedding model** | A neural network trained to produce embeddings (we use `all-MiniLM-L6-v2`) |
| **Vector** | The list of numbers itself β€” 384 floats in our case |
| **Dimension** | The length of that list. Our model uses 384 dimensions |
| **Cosine similarity** | A score from -1 to 1 measuring how similar two vectors are by their angle |
| **Dot product** | AΒ·B β€” multiply each pair of numbers and sum; part of the cosine formula |
| **Magnitude** | The "length" of a vector: sqrt(sum of squares) |
| **Semantic search** | Searching by meaning rather than by exact keyword matches |
| **Corpus** | The collection of documents stored in your database |
| **top_k** | How many results to return (we used 3 in the demo) |
| **Linear scan / flat search** | Comparing the query against every single stored vector β€” exact but slow |
| **HNSW** | Hierarchical Navigable Small World β€” a graph-based index for fast approximate nearest-neighbor search |
| **Lazy loading** | Delaying an expensive operation (like loading the model) until it is actually needed |
| **Metadata** | Extra data stored alongside the embedding β€” category, date, URL, etc. |
| **Normalize** | Scale a vector so its magnitude equals 1; makes cosine similarity equivalent to dot product |