🔥 0
0
Lesson 4 of 6 18 min +100 XP

Embeddings & Vector Search

Traditional search matches keywords. Semantic search matches meaning. This is what makes AI search feel magical.

Keyword Search

Query: "cheap flights"

Matches: documents containing "cheap" AND "flights"

Misses: "budget airfare", "low-cost travel"

Semantic Search

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.

"Happy dog" Embedding Model text-embedding-3 Vector [0.23, -0.45, 0.12, 0.89, ... 1536 dims]

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...
Notice:

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;
Query Similar (0.95) Different (0.3)

---

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.

🧠 Quick Quiz

Test your understanding of this lesson.

1

What is an embedding?

2

Why use pgvector instead of a dedicated vector database?

3

What does cosine similarity measure?

Prompt Engineering for Devs