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