RAG Step 3: Set Up pgvector

pgvector adds vector search to PostgreSQL. Instead of SQLite, we'll use PostgreSQL with the pgvector extension.

Why PostgreSQL + pgvector?

Feature SQLite PostgreSQL + pgvector
Vector search Not supported Native <=> operator
Concurrent users Locked during writes Handles many writes
Production-ready No Yes
Supabase support No Built-in

Step 1: Start PostgreSQL with Docker Compose

Create advanced/docker-compose.rag.yml:

services:
  db:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_USER: aiproject
      POSTGRES_PASSWORD: aiproject
      POSTGRES_DB: aiproject
    ports:
      - "5432:5432"
    volumes:
      - pgvector_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U aiproject"]
      interval: 5s
      timeout: 5s
      retries: 5

  app:
    build: ../backend
    ports:
      - "5000:5000"
    volumes:
      - ../backend:/app
      - sqlite_data:/app/instance
    env_file:
      - ../backend/app/.env
    environment:
      DATABASE_URL: postgresql://aiproject:aiproject@db:5432/aiproject
    depends_on:
      db:
        condition: service_healthy

volumes:
  pgvector_data:
  sqlite_data:

This file adds a PostgreSQL service alongside your Flask app. The app service connects to the database using the DATABASE_URL environment variable.

Start both services:

cd aiproject/advanced
docker compose -f docker-compose.rag.yml up

Step 2: Install Dependencies

pip install psycopg2-binary sentence-transformers

Step 3: Add the Document Model

Create a new model for storing document chunks and their embeddings. Add to backend/app/models.py:

from pgvector.sqlalchemy import Vector


class Document(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(500), nullable=False)
    content = db.Column(db.Text, nullable=False)
    source = db.Column(db.String(500), default="")
    category = db.Column(db.String(100), default="general")
    embedding = db.Column(Vector(384), nullable=False)

    def to_dict(self):
        return {
            "id": self.id,
            "title": self.title,
            "content": self.content[:200],
            "source": self.source,
            "category": self.category,
        }

The Vector(384) column stores embeddings. The <=> operator will search by cosine distance.

Also add pgvector to requirements.txt:

pgvector
psycopg2-binary
sentence-transformers

Step 4: Create the Database Tables

The embeddings table needs to be created. Run the app once with PostgreSQL connected:

cd aiproject
docker compose -f advanced/docker-compose.rag.yml up --build

The app will create tables automatically (via db.create_all()). You can verify:

docker compose -f advanced/docker-compose.rag.yml exec db psql -U aiproject -c "\dt"

You should see tables including document with an embedding column.

Step 5: Test the Connection and Model

Run this quick check:

# test_pgvector.py
from app import create_app, db
from app.models import Document

app = create_app()
with app.app_context():
    db.create_all()
    count = Document.query.count()
    print(f"Connection OK. Documents in DB: {count}")

Run it:

DATABASE_URL="postgresql://aiproject:aiproject@localhost:5432/aiproject" python test_pgvector.py
# → Connection OK. Documents in DB: 0

Write an Automated Test

Add a test that verifies the Document model works with pgvector. Create advanced/rag/tests/test_knowledge_base.py:

import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from sample_data import DOCUMENTS
from knowledge_base import chunk_text, get_all_chunks


def test_chunk_text_creates_chunks():
    text = "word " * 500
    chunks = chunk_text(text, chunk_size=100, overlap=20)
    assert len(chunks) >= 5
    assert all(isinstance(c, str) for c in chunks)


def test_chunk_text_short_text():
    text = "short text"
    chunks = chunk_text(text, chunk_size=100, overlap=20)
    assert len(chunks) == 1
    assert chunks[0] == "short text"


def test_get_all_chunks_returns_list():
    chunks = get_all_chunks(DOCUMENTS[:3])
    assert len(chunks) > 0
    assert all("title" in c for c in chunks)
    assert all("content" in c for c in chunks)

Run them:

cd aiproject
python -m pytest advanced/rag/tests/ -v

These tests run without a database — they test the chunking logic in isolation.

Write a quick test:

# test_pgvector.py
from app import create_app, db
from app.models import Document

app = create_app()
with app.app_context():
    db.create_all()
    count = Document.query.count()
    print(f"Connection OK. Documents in DB: {count}")

Run it:

DATABASE_URL="postgresql://aiproject:aiproject@localhost:5432/aiproject" python test_pgvector.py
# → Connection OK. Documents in DB: 0

How It Works

┌─────────────────────────────────────────────────────────┐
│                    pgvector                              │
│                                                         │
│  ┌─────────────────────────────────────────────────┐    │
│  │  document table                                  │    │
│  │  id │ title │ content │ source │ embedding (384) │    │
│  ├─────┼───────┼─────────┼────────┼─────────────────┤    │
│  │  1  │ Tut…  │ Clark…  │ url…   │ [0.04, -0.13,…] │    │
│  │  2  │ Fin…  │ Reso…   │ url…   │ [0.21, 0.07,…]  │    │
│  │  3  │ Dis…  │ The…    │ url…   │ [-0.09, 0.33,…] │    │
│  └─────────────────────────────────────────────────┘    │
│                                                         │
│  Query: SELECT * FROM document                          │
│         ORDER BY embedding <=> :query_vec               │
│         LIMIT 5                                         │
└─────────────────────────────────────────────────────────┘