PostgreSQL Examples: E-commerce¶
Examples for building e-commerce platforms with AI-powered features using SteadyText.
E-commerce Product Catalog¶
Schema Design¶
-- Create e-commerce schema
CREATE SCHEMA IF NOT EXISTS ecommerce;
-- Products table with AI fields
CREATE TABLE ecommerce.products (
id SERIAL PRIMARY KEY,
sku VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
ai_description TEXT,
features TEXT[],
price DECIMAL(10, 2) NOT NULL,
category_id INTEGER,
brand VARCHAR(100),
embedding vector(1024),
search_vector tsvector,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Categories with hierarchical structure
CREATE TABLE ecommerce.categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
parent_id INTEGER REFERENCES ecommerce.categories(id),
description TEXT,
embedding vector(1024),
path ltree,
UNIQUE(parent_id, name)
);
-- Customer profiles
CREATE TABLE ecommerce.customers (
id SERIAL PRIMARY KEY,
email VARCHAR(200) UNIQUE NOT NULL,
preferences JSONB DEFAULT '{}',
preference_embedding vector(1024),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Reviews with sentiment
CREATE TABLE ecommerce.reviews (
id SERIAL PRIMARY KEY,
product_id INTEGER REFERENCES ecommerce.products(id),
customer_id INTEGER REFERENCES ecommerce.customers(id),
rating INTEGER CHECK (rating BETWEEN 1 AND 5),
title VARCHAR(200),
content TEXT,
sentiment_score FLOAT,
helpful_count INTEGER DEFAULT 0,
verified_purchase BOOLEAN DEFAULT FALSE,
embedding vector(1024),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Shopping behavior tracking
CREATE TABLE ecommerce.customer_events (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES ecommerce.customers(id),
event_type VARCHAR(50),
product_id INTEGER REFERENCES ecommerce.products(id),
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Product Description Generation¶
-- Generate engaging product descriptions
CREATE OR REPLACE FUNCTION ecommerce.generate_product_description(
p_name TEXT,
p_features TEXT[],
p_brand TEXT,
p_category TEXT,
p_style TEXT DEFAULT 'professional'
) RETURNS TEXT AS $$
DECLARE
v_prompt TEXT;
v_description TEXT;
v_features_text TEXT;
BEGIN
v_features_text := array_to_string(p_features, ', ');
v_prompt := format(
'Write a %s product description for: %s by %s. Category: %s. Features: %s. Make it engaging and highlight benefits.',
p_style,
p_name,
COALESCE(p_brand, 'our brand'),
p_category,
v_features_text
);
v_description := steadytext_generate(v_prompt, 200);
IF v_description IS NULL THEN
-- Fallback to template-based description
v_description := format(
'Introducing the %s from %s. This %s features %s. Shop now for the best selection.',
p_name,
COALESCE(p_brand, 'our collection'),
p_category,
v_features_text
);
END IF;
RETURN v_description;
END;
$$ LANGUAGE plpgsql;
-- Generate product features from description
CREATE OR REPLACE FUNCTION ecommerce.extract_product_features(
p_description TEXT,
p_max_features INTEGER DEFAULT 5
) RETURNS TEXT[] AS $$
DECLARE
v_prompt TEXT;
v_features_text TEXT;
v_features TEXT[];
BEGIN
v_prompt := format(
'Extract %s key features from this product description as a comma-separated list: %s',
p_max_features,
p_description
);
v_features_text := steadytext_generate(v_prompt, 100);
IF v_features_text IS NOT NULL THEN
v_features := string_to_array(
regexp_replace(v_features_text, '^\s*[-•*]?\s*', '', 'gm'),
E'\n'
);
-- Clean up array
v_features := array_remove(v_features, '');
v_features := array_remove(v_features, NULL);
ELSE
v_features := ARRAY[]::TEXT[];
END IF;
RETURN v_features[1:p_max_features];
END;
$$ LANGUAGE plpgsql;
-- Generate SEO-optimized product titles
CREATE OR REPLACE FUNCTION ecommerce.optimize_product_title(
p_name TEXT,
p_brand TEXT,
p_category TEXT,
p_key_features TEXT[]
) RETURNS TEXT AS $$
DECLARE
v_prompt TEXT;
v_title TEXT;
BEGIN
v_prompt := format(
'Create an SEO-optimized product title (max 60 chars) for: %s %s in %s category with features: %s',
p_brand,
p_name,
p_category,
array_to_string(p_key_features[1:2], ', ')
);
v_title := steadytext_generate(v_prompt, 20);
IF v_title IS NULL OR length(v_title) > 60 THEN
-- Fallback to simple concatenation
v_title := substring(
format('%s %s - %s', p_brand, p_name, p_key_features[1]),
1, 60
);
END IF;
RETURN v_title;
END;
$$ LANGUAGE plpgsql;
Product Recommendations¶
-- Personalized product recommendations
CREATE OR REPLACE FUNCTION ecommerce.get_personalized_recommendations(
p_customer_id INTEGER,
p_limit INTEGER DEFAULT 10
) RETURNS TABLE(
product_id INTEGER,
product_name VARCHAR(200),
score FLOAT,
reason TEXT
) AS $$
BEGIN
RETURN QUERY
WITH customer_profile AS (
-- Build customer preference profile
SELECT
c.preference_embedding,
array_agg(DISTINCT cat.name) as preferred_categories,
avg(e.metadata->>'price_range') as avg_price_range
FROM ecommerce.customers c
LEFT JOIN ecommerce.customer_events e ON c.id = e.customer_id
LEFT JOIN ecommerce.products p ON e.product_id = p.id
LEFT JOIN ecommerce.categories cat ON p.category_id = cat.id
WHERE c.id = p_customer_id
GROUP BY c.id, c.preference_embedding
),
product_scores AS (
SELECT
p.id,
p.name,
-- Combine embedding similarity with business rules
(
CASE
WHEN cp.preference_embedding IS NOT NULL
THEN 0.6 * (1 - (p.embedding <-> cp.preference_embedding))
ELSE 0.3
END +
CASE
WHEN cat.name = ANY(cp.preferred_categories)
THEN 0.3
ELSE 0.0
END +
CASE
WHEN abs(p.price - COALESCE(cp.avg_price_range::numeric, p.price)) < 50
THEN 0.1
ELSE 0.0
END
) as score,
cp.preferred_categories
FROM ecommerce.products p
JOIN ecommerce.categories cat ON p.category_id = cat.id
CROSS JOIN customer_profile cp
WHERE p.id NOT IN (
-- Exclude already purchased
SELECT DISTINCT product_id
FROM ecommerce.customer_events
WHERE customer_id = p_customer_id
AND event_type = 'purchase'
)
)
SELECT
ps.id as product_id,
ps.name as product_name,
ps.score,
CASE
WHEN ps.preferred_categories IS NOT NULL
THEN 'Based on your interest in ' || ps.preferred_categories[1]
ELSE 'Trending product you might like'
END as reason
FROM product_scores ps
ORDER BY ps.score DESC
LIMIT p_limit;
END;
$$ LANGUAGE plpgsql;
-- Cross-sell recommendations
CREATE OR REPLACE FUNCTION ecommerce.get_cross_sell_products(
p_product_id INTEGER,
p_limit INTEGER DEFAULT 5
) RETURNS TABLE(
product_id INTEGER,
product_name VARCHAR(200),
confidence FLOAT,
relationship TEXT
) AS $$
BEGIN
RETURN QUERY
WITH product_pairs AS (
-- Find products frequently bought together
SELECT
ce2.product_id as related_id,
COUNT(*) as co_purchase_count
FROM ecommerce.customer_events ce1
JOIN ecommerce.customer_events ce2
ON ce1.customer_id = ce2.customer_id
AND ce1.product_id != ce2.product_id
AND ce2.created_at BETWEEN ce1.created_at AND ce1.created_at + INTERVAL '1 hour'
WHERE ce1.product_id = p_product_id
AND ce1.event_type = 'purchase'
AND ce2.event_type = 'purchase'
GROUP BY ce2.product_id
),
semantic_similarity AS (
-- Find semantically similar products
SELECT
p2.id as related_id,
1 - (p1.embedding <-> p2.embedding) as similarity
FROM ecommerce.products p1
JOIN ecommerce.products p2 ON p1.id != p2.id
WHERE p1.id = p_product_id
)
SELECT
p.id as product_id,
p.name as product_name,
GREATEST(
COALESCE(pp.co_purchase_count::float / 100, 0),
COALESCE(ss.similarity, 0)
) as confidence,
CASE
WHEN pp.co_purchase_count > 0 THEN 'Frequently bought together'
ELSE 'Similar product'
END as relationship
FROM ecommerce.products p
LEFT JOIN product_pairs pp ON p.id = pp.related_id
LEFT JOIN semantic_similarity ss ON p.id = ss.related_id
WHERE p.id != p_product_id
AND (pp.co_purchase_count > 0 OR ss.similarity > 0.7)
ORDER BY confidence DESC
LIMIT p_limit;
END;
$$ LANGUAGE plpgsql;
Review Analysis¶
-- Analyze review sentiment
CREATE OR REPLACE FUNCTION ecommerce.analyze_review_sentiment(
p_content TEXT
) RETURNS FLOAT AS $$
DECLARE
v_prompt TEXT;
v_result TEXT;
v_sentiment FLOAT;
BEGIN
v_prompt := format(
'Rate the sentiment of this product review from -1 (very negative) to 1 (very positive). Return only the number: %s',
substring(p_content, 1, 500)
);
v_result := steadytext_generate(v_prompt, 10);
IF v_result ~ '^-?[0-9]*\.?[0-9]+$' THEN
v_sentiment := v_result::FLOAT;
v_sentiment := GREATEST(-1, LEAST(1, v_sentiment));
ELSE
v_sentiment := 0.0;
END IF;
RETURN v_sentiment;
END;
$$ LANGUAGE plpgsql;
-- Generate review summary
CREATE OR REPLACE FUNCTION ecommerce.generate_review_summary(
p_product_id INTEGER
) RETURNS TABLE(
summary TEXT,
pros TEXT[],
cons TEXT[],
overall_sentiment FLOAT
) AS $$
DECLARE
v_reviews TEXT;
v_prompt TEXT;
v_summary TEXT;
v_pros_text TEXT;
v_cons_text TEXT;
BEGIN
-- Aggregate reviews
SELECT string_agg(
format('Rating: %s/5 - %s', rating, substring(content, 1, 200)),
E'\n'
) INTO v_reviews
FROM (
SELECT rating, content
FROM ecommerce.reviews
WHERE product_id = p_product_id
ORDER BY helpful_count DESC, created_at DESC
LIMIT 10
) r;
-- Generate summary
v_prompt := format(
'Summarize these product reviews in 2-3 sentences: %s',
v_reviews
);
v_summary := steadytext_generate(v_prompt, 100);
-- Extract pros
v_prompt := format(
'List 3 main pros mentioned in these reviews as comma-separated values: %s',
v_reviews
);
v_pros_text := steadytext_generate(v_prompt, 50);
-- Extract cons
v_prompt := format(
'List 3 main cons mentioned in these reviews as comma-separated values: %s',
v_reviews
);
v_cons_text := steadytext_generate(v_prompt, 50);
RETURN QUERY
SELECT
COALESCE(v_summary, 'No reviews yet'),
string_to_array(COALESCE(v_pros_text, ''), ','),
string_to_array(COALESCE(v_cons_text, ''), ','),
(SELECT AVG(sentiment_score) FROM ecommerce.reviews WHERE product_id = p_product_id);
END;
$$ LANGUAGE plpgsql;
-- Auto-moderate reviews
CREATE OR REPLACE FUNCTION ecommerce.moderate_review()
RETURNS TRIGGER AS $$
BEGIN
-- Analyze sentiment
NEW.sentiment_score := ecommerce.analyze_review_sentiment(NEW.content);
-- Generate embedding for similarity
NEW.embedding := steadytext_embed(
COALESCE(NEW.title, '') || ' ' || NEW.content
);
-- Check for potential issues
IF NEW.sentiment_score < -0.8 THEN
-- Flag for manual review
INSERT INTO ecommerce.moderation_queue (
review_id, reason, created_at
) VALUES (
NEW.id, 'Very negative sentiment', NOW()
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER review_moderation
BEFORE INSERT OR UPDATE ON ecommerce.reviews
FOR EACH ROW
EXECUTE FUNCTION ecommerce.moderate_review();
Dynamic Pricing¶
-- Price optimization suggestions
CREATE OR REPLACE FUNCTION ecommerce.suggest_optimal_price(
p_product_id INTEGER
) RETURNS TABLE(
current_price DECIMAL(10,2),
suggested_price DECIMAL(10,2),
expected_impact TEXT,
reasoning TEXT
) AS $$
DECLARE
v_product RECORD;
v_metrics RECORD;
v_prompt TEXT;
v_suggestion TEXT;
BEGIN
-- Get product info
SELECT * INTO v_product
FROM ecommerce.products
WHERE id = p_product_id;
-- Calculate metrics
WITH sales_data AS (
SELECT
COUNT(*) as total_sales,
COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '30 days') as recent_sales,
AVG(CASE WHEN event_type = 'view' THEN 1 ELSE 0 END) as view_rate
FROM ecommerce.customer_events
WHERE product_id = p_product_id
),
competitor_prices AS (
SELECT
AVG(price) as avg_competitor_price,
MIN(price) as min_price,
MAX(price) as max_price
FROM ecommerce.products
WHERE category_id = v_product.category_id
AND id != p_product_id
)
SELECT * INTO v_metrics
FROM sales_data, competitor_prices;
-- Generate pricing suggestion
v_prompt := format(
'Analyze pricing: Current price $%s. Recent sales: %s/month. Category avg price: $%s. Suggest optimal price and explain why.',
v_product.price,
v_metrics.recent_sales,
round(v_metrics.avg_competitor_price, 2)
);
v_suggestion := steadytext_generate(v_prompt, 150);
-- Parse suggestion (in real implementation, use structured generation)
RETURN QUERY
SELECT
v_product.price,
CASE
WHEN v_metrics.recent_sales < 10 AND v_product.price > v_metrics.avg_competitor_price
THEN v_product.price * 0.9
WHEN v_metrics.recent_sales > 50 AND v_product.price < v_metrics.avg_competitor_price
THEN v_product.price * 1.1
ELSE v_product.price
END,
CASE
WHEN v_metrics.recent_sales < 10 THEN 'Increase sales volume'
WHEN v_metrics.recent_sales > 50 THEN 'Maximize revenue'
ELSE 'Maintain current position'
END,
COALESCE(v_suggestion, 'Price appears optimal for current market conditions');
END;
$$ LANGUAGE plpgsql;
Customer Service Automation¶
-- Generate customer service responses
CREATE OR REPLACE FUNCTION ecommerce.generate_cs_response(
p_inquiry_type TEXT,
p_context JSONB
) RETURNS TEXT AS $$
DECLARE
v_prompt TEXT;
v_response TEXT;
BEGIN
v_prompt := CASE p_inquiry_type
WHEN 'order_status' THEN format(
'Write a friendly response about order #%s status: %s. Estimated delivery: %s',
p_context->>'order_id',
p_context->>'status',
p_context->>'delivery_date'
)
WHEN 'return_request' THEN format(
'Write a helpful response for a return request. Product: %s. Reason: %s. Policy: 30-day returns.',
p_context->>'product_name',
p_context->>'reason'
)
WHEN 'product_question' THEN format(
'Answer this product question: %s. Product info: %s',
p_context->>'question',
p_context->>'product_info'
)
ELSE 'Write a friendly customer service response acknowledging the inquiry.'
END;
v_response := steadytext_generate(v_prompt, 150);
RETURN COALESCE(
v_response,
'Thank you for contacting us. A customer service representative will assist you shortly.'
);
END;
$$ LANGUAGE plpgsql;
-- Categorize customer inquiries
CREATE OR REPLACE FUNCTION ecommerce.categorize_inquiry(
p_message TEXT
) RETURNS TEXT AS $$
DECLARE
v_category TEXT;
BEGIN
v_category := steadytext_generate_choice(
format('Categorize this customer inquiry: %s', p_message),
ARRAY[
'order_status',
'return_request',
'product_question',
'shipping_inquiry',
'payment_issue',
'technical_support',
'general_inquiry'
]
);
RETURN COALESCE(v_category, 'general_inquiry');
END;
$$ LANGUAGE plpgsql;
Inventory Intelligence¶
-- Predict inventory needs
CREATE OR REPLACE FUNCTION ecommerce.predict_inventory_needs(
p_product_id INTEGER,
p_days_ahead INTEGER DEFAULT 30
) RETURNS TABLE(
predicted_demand INTEGER,
confidence_level TEXT,
factors TEXT[],
recommendation TEXT
) AS $$
DECLARE
v_sales_history RECORD;
v_prompt TEXT;
v_prediction TEXT;
BEGIN
-- Analyze sales patterns
WITH sales_analysis AS (
SELECT
COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '30 days') as recent_sales,
COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '90 days') / 3.0 as monthly_avg,
EXTRACT(DOW FROM NOW()) as day_of_week,
EXTRACT(MONTH FROM NOW()) as current_month
FROM ecommerce.customer_events
WHERE product_id = p_product_id
AND event_type = 'purchase'
)
SELECT * INTO v_sales_history FROM sales_analysis;
-- Generate prediction
v_prompt := format(
'Predict inventory needs for next %s days. Recent monthly sales: %s. Average: %s. Current month: %s. Provide a number.',
p_days_ahead,
v_sales_history.recent_sales,
round(v_sales_history.monthly_avg, 1),
to_char(to_timestamp(v_sales_history.current_month::text, 'MM'), 'Month')
);
v_prediction := steadytext_generate(v_prompt, 100);
RETURN QUERY
SELECT
GREATEST(
round(v_sales_history.monthly_avg * (p_days_ahead / 30.0) * 1.2)::INTEGER,
10
),
CASE
WHEN v_sales_history.recent_sales > v_sales_history.monthly_avg * 1.5 THEN 'High'
WHEN v_sales_history.recent_sales < v_sales_history.monthly_avg * 0.5 THEN 'Low'
ELSE 'Medium'
END,
ARRAY[
'Historical sales: ' || v_sales_history.recent_sales,
'Trend: ' || CASE
WHEN v_sales_history.recent_sales > v_sales_history.monthly_avg THEN 'Increasing'
ELSE 'Stable'
END,
'Season: ' || to_char(NOW(), 'Month')
],
COALESCE(
v_prediction,
format('Recommend stocking %s units based on historical data',
round(v_sales_history.monthly_avg * (p_days_ahead / 30.0) * 1.2))
);
END;
$$ LANGUAGE plpgsql;
A/B Testing for Products¶
-- A/B test different product descriptions
CREATE TABLE ecommerce.ab_tests (
id SERIAL PRIMARY KEY,
product_id INTEGER REFERENCES ecommerce.products(id),
variant_name VARCHAR(50),
description TEXT,
embedding vector(1024),
impressions INTEGER DEFAULT 0,
conversions INTEGER DEFAULT 0,
revenue DECIMAL(10,2) DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Get A/B test variant
CREATE OR REPLACE FUNCTION ecommerce.get_ab_test_variant(
p_product_id INTEGER,
p_customer_id INTEGER
) RETURNS TABLE(
variant_id INTEGER,
description TEXT
) AS $$
DECLARE
v_hash INTEGER;
BEGIN
-- Consistent hashing for customer assignment
v_hash := abs(hashtext(p_product_id::text || p_customer_id::text));
RETURN QUERY
SELECT
id,
description
FROM ecommerce.ab_tests
WHERE product_id = p_product_id
AND is_active = TRUE
ORDER BY id
LIMIT 1
OFFSET (v_hash % (
SELECT COUNT(*)
FROM ecommerce.ab_tests
WHERE product_id = p_product_id AND is_active = TRUE
));
END;
$$ LANGUAGE plpgsql;
-- Analyze A/B test results
CREATE OR REPLACE FUNCTION ecommerce.analyze_ab_test(
p_product_id INTEGER
) RETURNS TABLE(
variant_name VARCHAR(50),
conversion_rate FLOAT,
avg_revenue DECIMAL(10,2),
statistical_significance TEXT,
recommendation TEXT
) AS $$
BEGIN
RETURN QUERY
WITH test_results AS (
SELECT
variant_name,
impressions,
conversions,
revenue,
conversions::FLOAT / NULLIF(impressions, 0) as conv_rate,
revenue / NULLIF(conversions, 0) as avg_order_value
FROM ecommerce.ab_tests
WHERE product_id = p_product_id
),
winner AS (
SELECT variant_name
FROM test_results
ORDER BY conv_rate DESC NULLS LAST
LIMIT 1
)
SELECT
tr.variant_name,
tr.conv_rate,
tr.avg_order_value,
CASE
WHEN tr.impressions < 100 THEN 'Insufficient data'
WHEN tr.conv_rate > (SELECT AVG(conv_rate) * 1.2 FROM test_results) THEN 'Significant improvement'
WHEN tr.conv_rate < (SELECT AVG(conv_rate) * 0.8 FROM test_results) THEN 'Significant decline'
ELSE 'No significant difference'
END,
CASE
WHEN tr.variant_name = (SELECT variant_name FROM winner)
THEN 'Winning variant - consider making permanent'
ELSE 'Continue testing or discontinue'
END
FROM test_results tr;
END;
$$ LANGUAGE plpgsql;