Spaces:
Sleeping
Sleeping
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 |
|