pgvector on VPS: PostgreSQL Vector Database for AI Embeddings and RAG Applications

pgvector on VPS: PostgreSQL Vector Database for AI Embeddings and RAG Applications

pgvector is a PostgreSQL extension that adds vector storage and similarity search — enabling you to store AI embeddings alongside your regular data in the same PostgreSQL database, and query them with cosine similarity, L2 distance, or inner product. Instead of running a separate vector database (Pinecone, Weaviate, Chroma), pgvector lets your existing PostgreSQL handle semantic search, recommendation systems, and RAG pipelines.

Why pgvector Over Dedicated Vector Databases

  • Simplicity: One database for everything — vectors live with the data they describe, no sync needed
  • SQL power: Combine vector search with SQL filters — “find similar documents written by user X after 2024”
  • Transactions: ACID compliance — embeddings stay consistent with their source records
  • Cost: No separate service — pgvector runs in your existing PostgreSQL instance
  • Scale: Handles millions of vectors on a VPS with HNSW indexing

Step 1: Install pgvector

<code"># Ubuntu 22.04/24.04 — install pgvector from PGDG repository
sudo apt install -y postgresql-16-pgvector

# Verify installation
sudo -u postgres psql -c "CREATE EXTENSION IF NOT EXISTS vector;" postgres

# For Docker:
# Use pgvector/pgvector:pg16 image instead of postgres:16

Step 2: Set Up the Database Schema

<code">sudo -u postgres psql
<code">CREATE DATABASE vectordb;
\c vectordb

CREATE EXTENSION IF NOT EXISTS vector;

-- Documents table with embeddings
-- 1536 = OpenAI text-embedding-3-small dimensions
-- 768  = nomic-embed-text (Ollama) dimensions
-- 384  = all-MiniLM-L6-v2 dimensions
CREATE TABLE documents (
    id          BIGSERIAL PRIMARY KEY,
    title       TEXT NOT NULL,
    content     TEXT NOT NULL,
    url         TEXT,
    source      TEXT,
    embedding   vector(1536),    -- Change to match your model's dimensions
    created_at  TIMESTAMP DEFAULT NOW(),
    metadata    JSONB DEFAULT '{}'
);

-- HNSW index for fast approximate nearest neighbor search
-- Better than IVFFlat for most use cases (lower latency, no training needed)
CREATE INDEX ON documents
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

-- For exact search on small datasets (no index needed):
-- Just use ORDER BY embedding <=> query_vector LIMIT 5

Step 3: Generate and Store Embeddings

With OpenAI API

<code">pip install openai psycopg2-binary
<code">import openai
import psycopg2
from psycopg2.extras import execute_values
import os

client = openai.OpenAI(api_key=os.environ['OPENAI_API_KEY'])
conn = psycopg2.connect(os.environ['DATABASE_URL'])

def embed_text(text: str) -> list[float]:
    """Generate embedding using OpenAI."""
    response = client.embeddings.create(
        model='text-embedding-3-small',  # 1536 dims, cost-effective
        input=text,
    )
    return response.data[0].embedding

def store_document(title: str, content: str, url: str = None):
    """Embed and store a document."""
    # Chunk long documents (embedding models have token limits)
    chunks = chunk_text(content, max_tokens=512)

    records = []
    for chunk in chunks:
        embedding = embed_text(chunk)
        records.append((title, chunk, url, embedding))

    with conn.cursor() as cur:
        execute_values(cur, """
            INSERT INTO documents (title, content, url, embedding)
            VALUES %s
        """, [(t, c, u, str(e)) for t, c, u, e in records])
    conn.commit()

def chunk_text(text: str, max_tokens: int = 512) -> list[str]:
    """Split text into overlapping chunks."""
    words = text.split()
    chunks = []
    for i in range(0, len(words), max_tokens - 50):  # 50-word overlap
        chunk = ' '.join(words[i:i + max_tokens])
        if chunk:
            chunks.append(chunk)
    return chunks

With Ollama (Local — No API Cost)

<code">import httpx

def embed_text_ollama(text: str) -> list[float]:
    """Generate embedding using local Ollama."""
    response = httpx.post('http://localhost:11434/api/embeddings', json={
        'model': 'nomic-embed-text',  # 768 dimensions
        'prompt': text,
    })
    return response.json()['embedding']

Step 4: Semantic Search (Core RAG Function)

<code">def semantic_search(query: str, top_k: int = 5, min_similarity: float = 0.7) -> list[dict]:
    """Find documents semantically similar to query."""
    query_embedding = embed_text(query)  # or embed_text_ollama(query)

    with conn.cursor() as cur:
        cur.execute("""
            SELECT
                id,
                title,
                content,
                url,
                1 - (embedding <=> %s::vector) AS similarity
            FROM documents
            WHERE 1 - (embedding <=> %s::vector) > %s
            ORDER BY embedding <=> %s::vector
            LIMIT %s
        """, (query_embedding, query_embedding, min_similarity,
               query_embedding, top_k))

        results = []
        for row in cur.fetchall():
            results.append({
                'id': row[0],
                'title': row[1],
                'content': row[2],
                'url': row[3],
                'similarity': float(row[4]),
            })
    return results

# Example: find documents about VPS security
results = semantic_search("how to secure a VPS server", top_k=3)
for r in results:
    print(f"{r['similarity']:.3f} — {r['title']}: {r['content'][:100]}...")

Step 5: Full RAG Pipeline

<code">def answer_with_rag(question: str) -> str:
    """Answer a question using RAG: retrieve context, then generate."""

    # 1. Retrieve relevant document chunks
    context_docs = semantic_search(question, top_k=5, min_similarity=0.6)

    if not context_docs:
        return "I don't have relevant information to answer this question."

    # 2. Build context string
    context = "\n\n".join([
        f"Source: {doc['title']}\n{doc['content']}"
        for doc in context_docs
    ])

    # 3. Generate answer using LLM with retrieved context
    response = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[
            {
                'role': 'system',
                'content': """You are a helpful assistant.
                Answer questions based on the provided context.
                If the context doesn't contain the answer, say so.
                Always cite your sources."""
            },
            {
                'role': 'user',
                'content': f"""Context:
{context}

Question: {question}

Answer based on the context above:"""
            }
        ],
        temperature=0.1,  # Low temperature for factual answers
    )

    return response.choices[0].message.content

# Usage
answer = answer_with_rag("What are the best practices for VPS backups?")
print(answer)

Step 6: Metadata Filtering with Vector Search

<code">-- Combine vector similarity with SQL filters
-- Find similar tech articles from the last 30 days
SELECT
    title,
    content,
    1 - (embedding <=> '[0.1, 0.2, ...]'::vector) AS similarity,
    metadata->>'category' AS category
FROM documents
WHERE
    metadata->>'category' = 'technology'
    AND created_at > NOW() - INTERVAL '30 days'
    AND 1 - (embedding <=> '[0.1, 0.2, ...]'::vector) > 0.7
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;

Index Types: HNSW vs IVFFlat

Factor HNSW IVFFlat
Query speed Faster Slower
Build time Slower Faster
Memory usage Higher Lower
Accuracy Higher (default) Good with right settings
Training required No Yes (VACUUM ANALYZE)
Best for General use, production Very large datasets with memory constraints

Getting Started

pgvector adds minimal overhead to an existing PostgreSQL installation — the extension itself uses no additional RAM; only the stored vectors and HNSW index consume space. On a 4 GB Ubuntu VPS at VPS.DO, pgvector with 1 million 1536-dimensional vectors uses approximately 6 GB disk space and 500 MB RAM for the HNSW index. For most RAG applications (corporate knowledge bases, documentation search, support systems), 100,000–500,000 vectors is typical — well within a 2 GB VPS capacity.

Conclusion

pgvector transforms your existing PostgreSQL into a capable vector database, eliminating the need for Pinecone ($70/month) or Weaviate for most RAG applications. The combination of vector similarity search with SQL filtering — finding semantically similar documents that also match category, date, or user ownership criteria — is pgvector’s key advantage over standalone vector databases. For teams already running PostgreSQL on a VPS, pgvector is a one-command extension installation away from enabling AI semantic search.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!