RAG & Knowledge Systems

Vector Databases & Embeddings

Deep dive into vector databases and embedding models — the backbone of RAG and semantic search systems.

Vector Databases & Embeddings

Vector databases store and search embeddings efficiently. They're the backbone of RAG systems, enabling fast semantic search across millions of documents.

Understanding Embeddings

An embedding is a list of numbers (a vector) that represents the meaning of text. The key property: semantically similar texts have similar vectors.

python
from openai import OpenAI

client = OpenAI()

# Generate embeddings
response = client.embeddings.create(
    model="text-embedding-3-small",
    input=["Python programming", "JavaScript coding", "Cooking recipes"]
)

# Each embedding is a list of 1536 floats
emb_python = response.data[0].embedding      # [0.023, -0.041, ...]
emb_javascript = response.data[1].embedding   # [0.025, -0.038, ...]
emb_cooking = response.data[2].embedding       # [-0.015, 0.067, ...]

# Python and JavaScript embeddings will be close
# Cooking embedding will be far from both

Embedding Models

ModelProviderDimensionsBest For
text-embedding-3-smallOpenAI1536Cost-effective, general purpose
text-embedding-3-largeOpenAI3072Higher accuracy
voyage-3Voyage AI1024Code and technical text
embed-v4Cohere1024Multilingual
all-MiniLM-L6-v2Open-source384Free, runs locally

Measuring Similarity

Cosine Similarity

The standard metric for comparing embeddings:

python
import numpy as np

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# Score ranges from -1 (opposite) to 1 (identical)
# > 0.8: Very similar
# 0.5-0.8: Somewhat related
# < 0.5: Different topics

Vector Databases

A vector database is optimized for storing embeddings and performing fast similarity search.

Popular Vector Databases

DatabaseTypeFree TierBest For
PineconeManaged cloudYesEasiest to start
ChromaDBOpen-sourceYes (local)Prototyping
WeaviateOpen-source/CloudYesHybrid search
QdrantOpen-source/CloudYesPerformance
pgvectorPostgreSQL extensionYesExisting Postgres users
FAISSLibrary (Meta)Yes (local)Pure Python, no server

ChromaDB — Getting Started

The easiest vector database for development:

bash
pip install chromadb
python
import chromadb

# Create a client (in-memory for development)
client = chromadb.Client()

# Create a collection
collection = client.create_collection(
    name="company_docs",
    metadata={"description": "Company documentation"}
)

# Add documents (ChromaDB auto-generates embeddings)
collection.add(
    documents=[
        "Our refund policy allows returns within 14 days.",
        "Premium plans include priority support and custom integrations.",
        "Free shipping on orders over $50 in the US.",
        "Office hours are Monday to Friday, 9 AM to 6 PM.",
    ],
    ids=["doc1", "doc2", "doc3", "doc4"],
    metadatas=[
        {"category": "policy"},
        {"category": "pricing"},
        {"category": "shipping"},
        {"category": "support"},
    ]
)

# Query
results = collection.query(
    query_texts=["How can I get my money back?"],
    n_results=2
)

print(results["documents"])
# → [["Our refund policy allows returns within 14 days.",
#     "Premium plans include priority support..."]]

Pinecone — Production-Ready

bash
pip install pinecone-client
python
from pinecone import Pinecone
from openai import OpenAI

pc = Pinecone(api_key="your-pinecone-key")
openai_client = OpenAI()

# Create an index
pc.create_index(
    name="company-docs",
    dimension=1536,  # Match your embedding model
    metric="cosine"
)

index = pc.Index("company-docs")

# Embed and upsert documents
def embed_and_store(documents):
    for i, doc in enumerate(documents):
        embedding = openai_client.embeddings.create(
            model="text-embedding-3-small",
            input=doc
        ).data[0].embedding

        index.upsert(vectors=[{
            "id": f"doc_{i}",
            "values": embedding,
            "metadata": {"text": doc}
        }])

# Query
def search_docs(query, top_k=3):
    query_embedding = openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=query
    ).data[0].embedding

    results = index.query(
        vector=query_embedding,
        top_k=top_k,
        include_metadata=True
    )

    return [match.metadata["text"] for match in results.matches]

pgvector — Using PostgreSQL

If you already use PostgreSQL:

sql
-- Enable the extension
CREATE EXTENSION vector;

-- Create a table with vector column
CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    content TEXT,
    embedding vector(1536),
    metadata JSONB
);

-- Create an index for fast search
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

-- Insert a document
INSERT INTO documents (content, embedding, metadata)
VALUES ('Refund policy: 14 days', '[0.023, -0.041, ...]', '{"category": "policy"}');

-- Search for similar documents
SELECT content, 1 - (embedding <=> '[query_embedding]') AS similarity
FROM documents
ORDER BY embedding <=> '[query_embedding]'
LIMIT 5;

Building a Complete Indexing Pipeline

python
import os
from openai import OpenAI
import chromadb

client = OpenAI()
chroma = chromadb.PersistentClient(path="./chroma_db")

def load_documents(directory):
    """Load all .txt and .md files from a directory."""
    docs = []
    for filename in os.listdir(directory):
        if filename.endswith(('.txt', '.md')):
            with open(os.path.join(directory, filename)) as f:
                docs.append({
                    "content": f.read(),
                    "filename": filename
                })
    return docs

def chunk_document(doc, chunk_size=500, overlap=50):
    """Split a document into overlapping chunks."""
    words = doc["content"].split()
    chunks = []
    start = 0

    while start < len(words):
        end = min(start + chunk_size, len(words))
        chunk_text = " ".join(words[start:end])
        chunks.append({
            "content": chunk_text,
            "source": doc["filename"],
            "chunk_index": len(chunks)
        })
        start = end - overlap

    return chunks

def build_index(directory):
    """Build a complete vector index from a directory."""
    collection = chroma.get_or_create_collection("knowledge_base")

    docs = load_documents(directory)
    all_chunks = []
    for doc in docs:
        all_chunks.extend(chunk_document(doc))

    # Batch process
    batch_size = 100
    for i in range(0, len(all_chunks), batch_size):
        batch = all_chunks[i:i + batch_size]
        collection.add(
            documents=[c["content"] for c in batch],
            ids=[f"chunk_{i+j}" for j in range(len(batch))],
            metadatas=[{
                "source": c["source"],
                "chunk_index": c["chunk_index"]
            } for c in batch]
        )

    print(f"Indexed {len(all_chunks)} chunks from {len(docs)} documents")

# Usage
build_index("./company_docs")

Embedding Best Practices

  1. Match dimensions — Embedding model dimensions must match your vector DB index
  2. Batch embed — Send multiple texts per API call to reduce latency
  3. Normalize — Some databases require normalized vectors
  4. Choose chunk size carefully — Too small loses context, too large reduces precision
  5. Include metadata — Store source file, page number, date for filtering
  6. Use the same model for indexing and querying

Key Takeaways

  1. Embeddings capture semantic meaning — similar texts have similar vectors
  2. Vector databases enable fast similarity search over millions of documents
  3. Start with ChromaDB for development, move to Pinecone/pgvector for production
  4. Chunking strategy significantly impacts retrieval quality
  5. Metadata filtering adds precision to semantic search

Next, we'll combine everything to build complete RAG pipelines.