File size: 15,699 Bytes
b16a546
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# 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 |