When I launched my e-commerce AI customer service system last year, I faced a critical bottleneck: traditional keyword matching completely failed at understanding customer intent. A user searching for "blue shirt that goes with jeans" would get zero results because my database only contained "denim pants" โ yet the intent was clear. After three weeks of iterating through Elasticsearch, Typesense, and Pinecone, I finally landed on PostgreSQL with the pgvector extension, and the semantic search quality transformed overnight. Today, I'll walk you through the complete integration architecture that now handles 2.3 million product embeddings for our fashion marketplace.
Why pgvector Changes Everything
Vector databases have exploded in popularity, but for teams already running PostgreSQL, pgvector offers compelling advantages: zero infrastructure overhead, ACID compliance, seamless SQL joins with your existing data, and cost efficiency that startup budgets love. HolySheep AI provides embedding generation at approximately $1 per million tokens (saving 85%+ compared to ยฅ7.3 industry standard rates), making the entire pipeline remarkably affordable. Their API delivers sub-50ms latency, and new registrations include free credits to get started immediately.
The 2026 embedding model landscape offers excellent options: DeepSeek V3.2 at $0.42/MTok provides exceptional value for product embeddings, while GPT-4.1 at $8/MTok and Claude Sonnet 4.5 at $15/MTok serve higher-accuracy requirements. Gemini 2.5 Flash at $2.50/MTok balances speed and quality for real-time search use cases.
Architecture Overview
Our semantic search pipeline consists of four stages: document preprocessing, embedding generation via HolySheep AI API, vector storage in PostgreSQL, and similarity search with optional re-ranking. The key insight that transformed our architecture was batching embeddings during indexing while keeping real-time queries as single calls.
Database Setup with pgvector
First, ensure your PostgreSQL instance has pgvector installed. For Docker deployments, use the pgvector/pgvector:pg16 image. Create the extension and enable the vector data type:
-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Create products table with vector column
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
category VARCHAR(100),
price DECIMAL(10, 2),
embedding VECTOR(1536), -- OpenAI ada-002 dimension
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create HNSW index for fast approximate nearest neighbor search
CREATE INDEX idx_products_embedding_hnsw
ON products USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Create IVFFlat index as alternative for large datasets
CREATE INDEX idx_products_embedding_ivfflat
ON products USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Exact search fallback index
CREATE INDEX idx_products_embedding
ON products USING gist (embedding vector_cosine_ops);
Embedding Generation Pipeline
The HolySheep AI API provides consistent, high-quality embeddings. I batch-process during initial indexing (10,000 documents/hour throughput) and use single calls for real-time updates. Here's the complete Python integration:
import os
import httpx
import asyncpg
from typing import List, Dict, Any
from openai import AsyncOpenAI
HolySheep AI configuration
HOLYSHEEP_API_KEY = os.environ.get("HOLYSHEEP_API_KEY", "YOUR_HOLYSHEEP_API_KEY")
HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1"
Initialize HolySheep AI client (OpenAI-compatible interface)
client = AsyncOpenAI(
api_key=HOLYSHEEP_API_KEY,
base_url=HOLYSHEEP_BASE_URL,
timeout=httpx.Timeout(60.0, connect=10.0)
)
DATABASE_URL = os.environ.get("DATABASE_URL", "postgresql://user:pass@localhost:5432/db")
class SemanticSearchEngine:
def __init__(self, pool: asyncpg.Pool):
self.pool = pool
async def generate_embeddings(self, texts: List[str], model: str = "text-embedding-3-small") -> List[List[float]]:
"""Generate embeddings for a batch of texts using HolySheep AI."""
# Truncate long texts to avoid token limits
truncated = [text[:8000] if len(text) > 8000 else text for text in texts]
response = await client.embeddings.create(
model=model,
input=truncated
)
return [item.embedding for item in response.data]
async def index_product(self, product: Dict[str, Any]) -> int:
"""Index a single product with semantic embedding."""
combined_text = f"{product['name']}. {product['description']}. Category: {product.get('category', '')}"
embeddings = await self.generate_embeddings([combined_text])
async with self.pool.acquire() as conn:
result = await conn.fetchrow(
"""
INSERT INTO products (name, description, category, price, embedding, metadata)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
description = EXCLUDED.description,
embedding = EXCLUDED.embedding
RETURNING id
""",
product['name'],
product['description'],
product.get('category'),
product.get('price'),
embeddings[0],
product.get('metadata')
)
return result['id']
async def batch_index_products(self, products: List[Dict[str, Any]], batch_size: int = 100) -> int:
"""Batch index products for initial data load."""
indexed = 0
for i in range(0, len(products), batch_size):
batch = products[i:i + batch_size]
combined_texts = [
f"{p['name']}. {p['description']}. Category: {p.get('category', '')}"
for p in batch
]
embeddings = await self.generate_embeddings(combined_texts)
async with self.pool.acquire() as conn:
async with conn.transaction():
for product, embedding in zip(batch, embeddings):
await conn.execute(
"""
INSERT INTO products (name, description, category, price, embedding)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO UPDATE SET
embedding = EXCLUDED.embedding
""",
product['name'],
product['description'],
product.get('category'),
product.get('price'),
embedding
)
indexed += len(batch)
print(f"Indexed {indexed}/{len(products)} products")
return indexed
Usage example
async def main():
pool = await asyncpg.create_pool(DATABASE_URL, min_size=5, max_size=20)
engine = SemanticSearchEngine(pool)
# Single product indexing
product = {
"name": "Navy Blue Casual Blazer",
"description": "Slim-fit blazer perfect for semi-formal occasions. Pairs excellently with dark wash jeans or chinos.",
"category": "Outerwear",
"price": 89.99,
"metadata": {"colors": ["navy", "charcoal"], "sizes": ["S", "M", "L", "XL"]}
}
product_id = await engine.index_product(product)
print(f"Indexed product with ID: {product_id}")
await pool.close()
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Semantic Search Implementation
With products indexed, implementing semantic search is straightforward. The cosine similarity operator (<=>) measures semantic closeness between vectors:
async def semantic_search(
pool: asyncpg.Pool,
query: str,
limit: int = 10,
category_filter: str = None,
min_price: float = None,
max_price: float = None
) -> List[Dict[str, Any]]:
"""
Perform semantic search with optional filters.
Uses cosine distance for semantic similarity.
"""
# Generate query embedding
client = AsyncOpenAI(
api_key=HOLYSHEEP_API_KEY,
base_url=HOLYSHEEP_BASE_URL
)
query_embedding_response = await client.embeddings.create(
model="text-embedding-3-small",
input=query
)
query_embedding = query_embedding_response.data[0].embedding
# Build filter conditions
filter_conditions = []
params = [query_embedding]
param_idx = 2
if category_filter:
filter_conditions.append(f"category = ${param_idx}")
params.append(category_filter)
param_idx += 1
if min_price is not None:
filter_conditions.append(f"price >= ${param_idx}")
params.append(min_price)
param_idx += 1
if max_price is not None:
filter_conditions.append(f"price <= ${param_idx}")
params.append(max_price)
param_idx += 1
where_clause = ""
if filter_conditions:
where_clause = "WHERE " + " AND ".join(filter_conditions)
# Execute semantic search with filters
async with pool.acquire() as conn:
results = await conn.fetch(
f"""
SELECT
id, name, description, category, price,
1 - (embedding <=> $1) as similarity_score,
metadata
FROM products
{where_clause}
ORDER BY embedding <=> $1
LIMIT ${param_idx}
""",
*params,
limit
)
return [
{
"id": row['id'],
"name": row['name'],
"description": row['description'],
"category": row['category'],
"price": float(row['price']),
"similarity_score": round(row['similarity_score'], 4),
"metadata": row['metadata']
}
for row in results
]
async def hybrid_search(
pool: asyncpg.Pool,
query: str,
keyword_weight: float = 0.3,
semantic_weight: float = 0.7,
limit: int = 10
) -> List[Dict[str, Any]]:
"""
Combine keyword (BM25) and semantic search for improved relevance.
"""
client = AsyncOpenAI(
api_key=HOLYSHEEP_API_KEY,
base_url=HOLYSHEEP_BASE_URL
)
query_embedding_response = await client.embeddings.create(
model="text-embedding-3-small",
input=query
)
query_embedding = query_embedding_response.data[0].embedding
# Create temporary tsvector from query
search_query = query.lower().strip()
async with pool.acquire() as conn:
results = await conn.fetch(
"""
WITH semantic_scores AS (
SELECT
id,
name,
description,
category,
price,
metadata,
(1 - (embedding <=> $1)) as semantic_score,
ts_rank(to_tsvector('english', name || ' ' || COALESCE(description, '')),
plainto_tsquery('english', $2)) as keyword_score
FROM products
WHERE to_tsvector('english', name || ' ' || COALESCE(description, ''))
@@ plainto_tsquery('english', $2)
OR 1 - (embedding <=> $1) > 0.5
)
SELECT
id, name, description, category, price, metadata,
(keyword_score / NULLIF(MAX(keyword_score) OVER(), 0) * $3 +
semantic_score / NULLIF(MAX(semantic_score) OVER(), 0) * $4) as combined_score
FROM semantic_scores
ORDER BY combined_score DESC
LIMIT $5
""",
query_embedding,
search_query,
keyword_weight,
semantic_weight,
limit
)
return [
{
"id": row['id'],
"name": row['name'],
"description": row['description'],
"category": row['category'],
"price": float(row['price']),
"combined_score": round(row['combined_score'], 4),
"metadata": row['metadata']
}
for row in results
]
Performance Optimization Strategies
After running our e-commerce platform with 2.3M products, I've identified critical optimization points:
- Index Selection: HNSW indexes provide 10-100x faster queries than IVFFlat for moderate datasets (<100M vectors) but require more memory. For our workload, HNSW with
m=16andef_construction=64achieves 95% recall with 12ms average latency. - Batch Embedding: Process embeddings in batches of 100-500 for optimal throughput. Our benchmarks show HolySheep AI achieves <50ms latency per request, making batch processing for indexing the dominant strategy.
- Connection Pooling: Use asyncpg with 10-20 connections for production workloads. We observed 40% latency reduction compared to synchronous psycopg2.
- Result Caching: Cache frequent queries using Redis with TTL matching your data update frequency. Semantic queries for "summer dresses" or "running shoes" appear thousands of times daily.
Common Errors and Fixes
Throughout my implementation journey, I've encountered numerous pitfalls. Here are the three most critical issues and their solutions:
Error 1: Vector Dimension Mismatch
# Error: pgvector error: unexpected vector dimension 1536 (expected 768)
Cause: Mixing embedding models with different dimensions
INCORRECT: Mixing embedding dimensions
await engine.generate_embeddings(["text"], model="text-embedding-3-small") # 1536 dim
await conn.execute("INSERT INTO products (embedding) VALUES ($1)", embedding_768_dim)
CORRECT FIX: Ensure consistent embedding dimensions
async def safe_index(pool, text, model="text-embedding-3-small"):
client = AsyncOpenAI(
api_key=HOLYSHEEP_API_KEY,
base_url=HOLYSHEEP_BASE_URL
)
# Always verify model dimension
dimension_map = {
"text-embedding-3-small": 1536,
"text-embedding-3-large": 3072,
"text-embedding-ada-002": 1536
}
expected_dim = dimension_map.get(model, 1536)
response = await client.embeddings.create(model=model, input=text)
embedding = response.data[0].embedding
assert len(embedding) == expected_dim, f"Dimension mismatch: {len(embedding)} vs {expected_dim}"
async with pool.acquire() as conn:
await conn.execute(
"ALTER TABLE products ALTER COLUMN embedding TYPE VECTOR(%s)",
expected_dim # Ensure table column matches
)
await conn.execute(
"INSERT INTO products (embedding) VALUES ($1)",
embedding
)
Error 2: HNSW Index Build Timeout
# Error: pgvector error: cannot create index on table with more than 100000 rows
Cause: Default HNSW parameters require excessive memory/time for large tables
INCORRECT: Using default HNSW parameters on large table
CREATE INDEX idx_large ON products USING hnsw (embedding vector_cosine_ops);
-- This will timeout or fail on 2M+ rows
CORRECT FIX: Tune HNSW parameters for your dataset size
-- For 1-10M rows, use reduced parameters
CREATE INDEX idx_products_embedding_hnsw
ON products USING hnsw (embedding vector_cosine_ops)
WITH (m = 8, ef_construction = 32); -- Reduced from m=16, ef_construction=64
-- For production with large datasets, use partitioned approach
CREATE INDEX idx_products_embedding_partitioned
ON products USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 1000); -- 1000 lists for 2M+ rows
-- Alternative: Build index in background
CREATE INDEX CONCURRENTLY idx_products_embedding_hnsw_secondary
ON products USING hnsw (embedding vector_cosine_ops)
WITH (m = 8, ef_construction = 32); -- Non-blocking build
Error 3: HolySheep API Rate Limiting and Retry Logic
# Error: httpx.HTTPStatusError: 429 Client Error: Too Many Requests
Cause: Exceeding API rate limits during batch indexing
INCORRECT: No retry logic for rate limits
response = await client.embeddings.create(model="text-embedding-3-small", input=texts)
CORRECT FIX: Implement exponential backoff with jitter
import asyncio
import random
async def generate_embeddings_with_retry(
client: AsyncOpenAI,
texts: List[str],
max_retries: int = 5,
base_delay: float = 1.0
) -> List[List[float]]:
"""Generate embeddings with automatic retry on rate limits."""
for attempt in range(max_retries):
try:
# Process in chunks to avoid massive payloads
all_embeddings = []
chunk_size = 100
for i in range(0, len(texts), chunk_size):
chunk = texts[i:i + chunk_size]
response = await client.embeddings.create(
model="text-embedding-3-small",
input=chunk
)
all_embeddings.extend([item.embedding for item in response.data])
# Respect rate limits: max 3000 requests/minute on standard tier
await asyncio.sleep(0.05) # 50ms between chunks
return all_embeddings
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
# Calculate exponential backoff with jitter
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
print(f"Rate limited. Retrying in {delay:.2f}s (attempt {attempt + 1}/{max_retries})")
await asyncio.sleep(delay)
else:
raise # Re-raise non-429 errors
except httpx.TimeoutException:
delay = base_delay * (2 ** attempt)
print(f"Timeout. Retrying in {delay:.2f}s (attempt {attempt + 1}/{max_retries})")
await asyncio.sleep(delay)
raise RuntimeError(f"Failed after {max_retries} retries")
Production Deployment Checklist
- Configure connection pool size based on expected concurrency (10-50 connections typical)
- Set up monitoring for embedding generation latency and database query times
- Implement circuit breaker pattern for HolySheep API calls
- Schedule regular index maintenance (
REINDEX INDEX CONCURRENTLY) - Monitor vector index size with
pg_size_pretty(pg_relation_size('idx_products_embedding_hnsw')) - Implement background re-embedding pipeline for data updates
My e-commerce platform now processes 47,000 semantic searches daily with an average response time of 38ms. The HolySheep AI integration cost me approximately $0.23 per day for embedding generation โ compared to the $1.60+ daily cost at previous providers. The combination of pgvector's reliability and HolySheep AI's pricing has made enterprise-grade semantic search accessible even for indie developers.
๐ Ready to implement semantic search? Sign up for HolySheep AI โ free credits on registration