Embeddings & Vector Search
Traditional search matches keywords. Semantic search matches meaning. This is what makes AI search feel magical.
Query: "cheap flights"
Matches: documents containing "cheap" AND "flights"
Misses: "budget airfare", "low-cost travel"
Query: "cheap flights"
Matches: documents about affordable air travel
Finds: "budget airfare", "low-cost travel", "affordable trips"
---
What Are Embeddings?
An embedding converts text into a vector - a list of numbers that captures meaning.
The Key Insight
Similar text = similar vectors. "Happy dog" and "joyful puppy" will have vectors that are close together in vector space.
# These will have SIMILAR embeddings (close vectors)
"happy dog"
"joyful puppy"
"cheerful canine"
# This will have a DIFFERENT embedding (far vector)
"database optimization techniques"
---
Generating Embeddings
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
# Generate an embedding
embedding = get_embedding("How do I reset my password?")
print(f"Dimensions: {len(embedding)}") # 1536
print(f"First 5 values: {embedding[:5]}") # [0.023, -0.045, ...]
| Model | Dimensions | Best for |
|---|---|---|
| text-embedding-3-small | 1536 | Most use cases, good balance |
| text-embedding-3-large | 3072 | Higher accuracy, more storage |
---
Setting Up pgvector
pgvector is a Postgres extension for vector storage and search. If you use Postgres, this is the easiest path.
Install the Extension
-- In your Postgres database
CREATE EXTENSION IF NOT EXISTS vector;
Create a Table with Vector Column
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding vector(1536), -- 1536 dimensions for OpenAI
created_at TIMESTAMP DEFAULT NOW()
);
-- Create an index for fast similarity search
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
---
Storing Embeddings
import psycopg2
from openai import OpenAI
client = OpenAI()
conn = psycopg2.connect("postgresql://localhost/mydb")
def store_document(content: str):
# Generate embedding
response = client.embeddings.create(
model="text-embedding-3-small",
input=content
)
embedding = response.data[0].embedding
# Store in Postgres
with conn.cursor() as cur:
cur.execute(
"INSERT INTO documents (content, embedding) VALUES (%s, %s)",
(content, embedding)
)
conn.commit()
# Store some documents
store_document("How to reset your password: Go to Settings > Security > Reset Password")
store_document("Changing your email: Navigate to Profile > Edit > Email Address")
store_document("Billing FAQ: We accept Visa, Mastercard, and PayPal")
---
Semantic Search
Now the magic - searching by meaning:
def semantic_search(query: str, limit: int = 5) -> list[dict]:
# Generate embedding for the query
response = client.embeddings.create(
model="text-embedding-3-small",
input=query
)
query_embedding = response.data[0].embedding
# Find similar documents
with conn.cursor() as cur:
cur.execute("""
SELECT content, 1 - (embedding <=> %s::vector) AS similarity
FROM documents
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (query_embedding, query_embedding, limit))
results = []
for row in cur.fetchall():
results.append({
"content": row[0],
"similarity": round(row[1], 3)
})
return results
# Search!
results = semantic_search("I forgot my login credentials")
for r in results:
print(f"{r['similarity']}: {r['content'][:50]}...")
# Output:
# 0.892: How to reset your password: Go to Settings > Sec...
# 0.756: Changing your email: Navigate to Profile > Edit...
# 0.423: Billing FAQ: We accept Visa, Mastercard, and Pay...
The query "I forgot my login credentials" matched "How to reset your password" even though they share no keywords. That's semantic search!
---
The Distance Operator
pgvector uses <=> for cosine distance:
-- <=> returns cosine DISTANCE (lower = more similar)
-- To get SIMILARITY (higher = more similar), use: 1 - distance
SELECT content,
1 - (embedding <=> query_embedding) AS similarity
FROM documents
ORDER BY embedding <=> query_embedding -- Closest first
LIMIT 5;
---
Complete Example: FAQ Search
from openai import OpenAI
import psycopg2
client = OpenAI()
conn = psycopg2.connect("postgresql://localhost/mydb")
class FAQSearch:
def __init__(self):
self._ensure_table()
def _ensure_table(self):
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS faqs (
id SERIAL PRIMARY KEY,
question TEXT,
answer TEXT,
embedding vector(1536)
)
""")
conn.commit()
def add_faq(self, question: str, answer: str):
embedding = self._get_embedding(question)
with conn.cursor() as cur:
cur.execute(
"INSERT INTO faqs (question, answer, embedding) VALUES (%s, %s, %s)",
(question, answer, embedding)
)
conn.commit()
def search(self, query: str, threshold: float = 0.7) -> dict | None:
query_embedding = self._get_embedding(query)
with conn.cursor() as cur:
cur.execute("""
SELECT question, answer, 1 - (embedding <=> %s::vector) AS similarity
FROM faqs
WHERE 1 - (embedding <=> %s::vector) > %s
ORDER BY embedding <=> %s::vector
LIMIT 1
""", (query_embedding, query_embedding, threshold, query_embedding))
row = cur.fetchone()
if row:
return {"question": row[0], "answer": row[1], "similarity": row[2]}
return None
def _get_embedding(self, text: str) -> list[float]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
# Usage
faq = FAQSearch()
faq.add_faq("How do I cancel my subscription?", "Go to Settings > Billing > Cancel")
faq.add_faq("What payment methods do you accept?", "We accept Visa, Mastercard, PayPal")
result = faq.search("I want to stop paying")
if result:
print(f"Q: {result['question']}")
print(f"A: {result['answer']}")
print(f"Confidence: {result['similarity']:.0%}")
---
Key Takeaways
- Embeddings capture meaning - similar text = similar vectors
- pgvector is production-ready - no need for separate vector DB
- Use cosine similarity - measures direction, not magnitude
- Set similarity thresholds - don't return low-confidence matches
Next up: Building RAG Systems - combining embeddings with LLMs for accurate, grounded responses.