How to Get Started with Vector Databases: Embeddings, Similarity Search, and RAG
Prerequisites
- Python 3.11+
- Basic understanding of LLMs and embeddings
chromadborpgvectorinstalled
What is a Vector Database?
Traditional databases search by exact keyword matches. A vector database searches by semantic meaning. You store text as mathematical vectors (embeddings) and query with “find documents similar in meaning to this text.”
Traditional: "Find all rows WHERE name = 'John'"
Vector: "Find documents similar to 'how to debug a memory leak'"
Step 1: Generate Embeddings
Embeddings are dense vectors (lists of floats) that represent meaning. OpenAI’s text-embedding-3-small produces 1536-dimensional vectors.
from openai import OpenAI
client = OpenAI()
def get_embedding(text: str) -> list[float]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
# Example
embedding = get_embedding("How to configure PostgreSQL replication")
# [0.0123, -0.0456, 0.0789, ...] # 1536 numbers
Step 2: Set Up ChromaDB
ChromaDB is a lightweight open-source vector database:
pip install chromadb
import chromadb
client = chromadb.PersistentClient(path="./chroma_data")
collection = client.get_or_create_collection(
name="docs",
metadata={"hnsw:space": "cosine"} # Cosine similarity
)
Step 3: Chunk Documents
Don’t embed entire documents. Split into small chunks for precise retrieval:
def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
"""Split text into overlapping chunks."""
words = text.split()
chunks = []
start = 0
while start < len(words):
chunk = " ".join(words[start:start + chunk_size])
chunks.append(chunk)
start += chunk_size - overlap # Overlap maintains context across chunks
return chunks
document = "Your long document text here..."
chunks = chunk_text(document)
Chunk size guidelines:
- 256-512 tokens: best for precise Q&A
- 1000-2000 tokens: better for summarisation
- Always include 10-20% overlap between chunks
Step 4: Store Chunks with Metadata
for i, chunk in enumerate(chunks):
embedding = get_embedding(chunk)
collection.add(
ids=[f"doc_{i}"],
embeddings=[embedding],
documents=[chunk],
metadatas=[{
"source": "postgresql_guide.pdf",
"page": i,
"topic": "database"
}]
)
Metadata filtering enables hybrid search:
# Semantic search filtered by metadata
results = collection.query(
query_embeddings=[get_embedding("replication setup")],
n_results=3,
where={"topic": "database"}
)
Step 5: Similarity Search
Query the vector database:
query = "How do I set up PostgreSQL replication?"
query_embedding = get_embedding(query)
results = collection.query(
query_embeddings=[query_embedding],
n_results=5
)
for i, doc in enumerate(results["documents"][0]):
distance = results["distances"][0][i]
print(f"Distance: {distance:.4f} | {doc[:100]}...")
Similarity metrics:
| Metric | Range | Best for |
|---|---|---|
| Cosine | [-1, 1] | Text similarity (default) |
| Euclidean | [0, ∞) | Image/sensor data |
| Dot Product | [-∞, ∞] | Normalized embeddings |
Step 6: Basic RAG Pipeline
RAG (Retrieval-Augmented Generation) retrieves relevant documents and feeds them to an LLM:
def rag_query(query: str, collection, llm_client) -> str:
# 1. Get query embedding
query_embedding = get_embedding(query)
# 2. Retrieve relevant chunks
results = collection.query(
query_embeddings=[query_embedding],
n_results=3
)
# 3. Build context from retrieved chunks
context = "\n\n".join(results["documents"][0])
# 4. Generate response with context
response = llm_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "Answer the question using ONLY the provided context. If the answer isn't in the context, say 'I don't have enough information.'"
},
{
"role": "user",
"content": f"Context:\n{context}\n\nQuestion: {query}"
}
]
)
return response.choices[0].message.content
Using pgvector Instead
If you already use PostgreSQL, add vector search with pgvector:
CREATE EXTENSION vector;
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536)
);
-- Create an index for fast similarity search
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops);
-- Query
SELECT content, 1 - (embedding <=> '[0.1, 0.2, ...]') AS similarity
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'
LIMIT 5;
pgvector is production-ready and avoids running a separate vector DB.
Verification
# Add test documents
collection.add(
ids=["test1"],
embeddings=[get_embedding("PostgreSQL replication uses WAL shipping.")],
documents=["PostgreSQL replication uses WAL shipping."]
)
collection.add(
ids=["test2"],
embeddings=[get_embedding("Python virtual environments isolate dependencies.")],
documents=["Python virtual environments isolate dependencies."]
)
# Query about databases
results = collection.query(
query_embeddings=[get_embedding("How does database replication work?")],
n_results=2
)
print(results["documents"])
# [['PostgreSQL replication uses WAL shipping.', 'Python virtual environments isolate dependencies.']]
# First result is correct (database topic)
Summary
- Embeddings convert text into semantic vectors
- Chunking splits documents into retrievable pieces with overlap
- ChromaDB is great for prototyping; pgvector for production
- Metadata filtering enables hybrid search
- RAG = retrieve relevant chunks + feed them to LLM for grounded answers