AI Recommendation Engine
E-commerce platforms relying on traditional collaborative filtering face cold-start problems: new products have no purchase history to build recommendations from, and new users have no behavior data to match against. Collaborative filtering answers “users who bought X also bought Y” but cannot explain why or recommend based on product attributes. A new running shoe has zero purchase history, so collaborative filtering cannot recommend it until enough users have bought it — a circular problem.
A semantic recommendation engine uses vector embeddings to represent products and user preferences, enabling content-based recommendations that work from day one. By embedding product descriptions into vector space, the system understands semantic similarity (“lightweight trail runner” is similar to “minimalist hiking shoe”) without any purchase data. This approach is complementary to collaborative filtering — the two signals can be combined for hybrid recommendations that leverage both content understanding and behavioral patterns.
Solution Architecture
Section titled “Solution Architecture”Beluga AI’s vector store package provides similarity search at scale. The recommendation engine embeds product descriptions and user preferences into vector space, uses similarity search to find relevant products, and applies hybrid ranking that combines content-based and collaborative signals for optimal accuracy.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐│ Product │───▶│ Product │───▶│ Vector Store ││ Catalog │ │ Embedder │ │ │└──────────────┘ └──────────────┘ └──────┬───────┘ │┌──────────────┐ ┌──────────────┐ ││ User │───▶│ User Pref │ ││ Behavior │ │ Engine │ │└──────────────┘ └──────┬───────┘ │ │ │ ▼ ▼ ┌──────────────────────────┐ │ Similarity Search │ │ + Hybrid Ranker │ └──────┬───────────────────┘ │ ▼ ┌──────────────┐ │ Personalized │ │Recommendations│ └──────────────┘Product Embedding
Section titled “Product Embedding”Generate semantic vectors for product catalog:
package main
import ( "context" "fmt"
"github.com/lookatitude/beluga-ai/rag/embedding" "github.com/lookatitude/beluga-ai/rag/vectorstore" "github.com/lookatitude/beluga-ai/schema"
_ "github.com/lookatitude/beluga-ai/rag/embedding/providers/openai" _ "github.com/lookatitude/beluga-ai/rag/vectorstore/providers/pgvector")
type RecommendationEngine struct { embedder embedding.Embedder vectorStore vectorstore.VectorStore}
func NewRecommendationEngine(ctx context.Context) (*RecommendationEngine, error) { embedder, err := embedding.New("openai", &embedding.Config{ Model: "text-embedding-3-large", APIKey: os.Getenv("OPENAI_API_KEY"), }) if err != nil { return nil, fmt.Errorf("create embedder: %w", err) }
store, err := vectorstore.New("pgvector", &vectorstore.Config{ ConnectionString: os.Getenv("DATABASE_URL"), CollectionName: "products", Dimensions: 3072, }) if err != nil { return nil, fmt.Errorf("create vector store: %w", err) }
return &RecommendationEngine{ embedder: embedder, vectorStore: store, }, nil}
type ProductMetadata struct { ProductID string Name string Description string Category string Price float64 Brand string Tags []string}
func (r *RecommendationEngine) IndexProduct(ctx context.Context, product ProductMetadata) error { // Create rich product description for embedding description := fmt.Sprintf( "Product: %s\nBrand: %s\nCategory: %s\nDescription: %s\nTags: %s", product.Name, product.Brand, product.Category, product.Description, strings.Join(product.Tags, ", "), )
// Generate embedding embeddings, err := r.embedder.Embed(ctx, []string{description}) if err != nil { return fmt.Errorf("embed product: %w", err) }
// Store in vector database doc := schema.Document{ Content: description, Embedding: embeddings[0], Metadata: map[string]interface{}{ "product_id": product.ProductID, "name": product.Name, "category": product.Category, "price": product.Price, "brand": product.Brand, "tags": product.Tags, }, }
if err := r.vectorStore.Add(ctx, []schema.Document{doc}); err != nil { return fmt.Errorf("store product: %w", err) }
return nil}User Preference Learning
Section titled “User Preference Learning”Build user preference vectors from behavior:
package main
import ( "context")
type UserPreferenceEngine struct { embedder embedding.Embedder}
type UserBehavior struct { ViewedProducts []string PurchasedProducts []string SearchQueries []string ClickedCategories []string}
func (u *UserPreferenceEngine) GetUserVector(ctx context.Context, userID string) ([]float64, error) { // Fetch user behavior behavior, err := u.fetchUserBehavior(ctx, userID) if err != nil { return nil, err }
if len(behavior.PurchasedProducts) == 0 && len(behavior.ViewedProducts) == 0 { return nil, fmt.Errorf("insufficient user data for %s", userID) }
// Build user preference description from behavior var preferences strings.Builder
if len(behavior.SearchQueries) > 0 { preferences.WriteString("User searches: ") preferences.WriteString(strings.Join(behavior.SearchQueries, ", ")) preferences.WriteString(". ") }
if len(behavior.ClickedCategories) > 0 { preferences.WriteString("Interested in categories: ") preferences.WriteString(strings.Join(behavior.ClickedCategories, ", ")) preferences.WriteString(". ") }
if len(behavior.PurchasedProducts) > 0 { preferences.WriteString("Previously purchased: ") for _, productID := range behavior.PurchasedProducts { product := u.getProductInfo(ctx, productID) preferences.WriteString(fmt.Sprintf("%s, ", product.Name)) } }
// Generate preference embedding embeddings, err := u.embedder.Embed(ctx, []string{preferences.String()}) if err != nil { return nil, fmt.Errorf("embed preferences: %w", err) }
return embeddings[0], nil}
func (u *UserPreferenceEngine) fetchUserBehavior(ctx context.Context, userID string) (*UserBehavior, error) { // Fetch from database or analytics system // Implementation depends on your data storage return &UserBehavior{}, nil}
func (u *UserPreferenceEngine) getProductInfo(ctx context.Context, productID string) *ProductMetadata { // Fetch product details return &ProductMetadata{}}Personalized Recommendations
Section titled “Personalized Recommendations”Generate recommendations using vector similarity:
package main
import ( "context")
type Recommendation struct { ProductID string Name string Category string Price float64 Score float64 Reason string}
func (r *RecommendationEngine) GetRecommendations(ctx context.Context, userID string, limit int) ([]Recommendation, error) { // Get user preference vector userPrefs := NewUserPreferenceEngine(r.embedder) userVector, err := userPrefs.GetUserVector(ctx, userID) if err != nil { // Cold-start: use popular products or category-based recommendations return r.getColdStartRecommendations(ctx, limit) }
// Search for similar products results, err := r.vectorStore.SimilaritySearch(ctx, userVector, vectorstore.WithTopK(limit*2), // Get more for ranking vectorstore.WithThreshold(0.6), ) if err != nil { return nil, fmt.Errorf("similarity search: %w", err) }
// Rank and filter recommendations := r.rankRecommendations(ctx, results, userID, limit)
return recommendations, nil}
func (r *RecommendationEngine) rankRecommendations(ctx context.Context, docs []schema.Document, userID string, limit int) []Recommendation { recommendations := make([]Recommendation, 0, len(docs))
// Apply business logic and filters for _, doc := range docs { // Skip already purchased products if r.hasUserPurchased(ctx, userID, doc.Metadata["product_id"].(string)) { continue }
// Apply diversity filter // Apply price range filter // Apply inventory filter
recommendations = append(recommendations, Recommendation{ ProductID: doc.Metadata["product_id"].(string), Name: doc.Metadata["name"].(string), Category: doc.Metadata["category"].(string), Price: doc.Metadata["price"].(float64), Score: doc.Metadata["score"].(float64), Reason: r.generateReason(doc), })
if len(recommendations) >= limit { break } }
return recommendations}
func (r *RecommendationEngine) generateReason(doc schema.Document) string { // Generate human-readable reason for recommendation return fmt.Sprintf("Based on your interest in %s", doc.Metadata["category"])}
func (r *RecommendationEngine) hasUserPurchased(ctx context.Context, userID, productID string) bool { // Check purchase history return false}Cold-Start Handling
Section titled “Cold-Start Handling”Recommend to new users using content similarity:
package main
import ( "context")
func (r *RecommendationEngine) getColdStartRecommendations(ctx context.Context, limit int) ([]Recommendation, error) { // Strategy 1: Popular products // Strategy 2: New arrivals // Strategy 3: Trending in category
// For this example, use popular products popularProducts, err := r.getPopularProducts(ctx, limit) if err != nil { return nil, err }
recommendations := make([]Recommendation, len(popularProducts)) for i, product := range popularProducts { recommendations[i] = Recommendation{ ProductID: product.ProductID, Name: product.Name, Category: product.Category, Price: product.Price, Score: 1.0, Reason: "Popular choice", } }
return recommendations, nil}
func (r *RecommendationEngine) GetSimilarProducts(ctx context.Context, productID string, limit int) ([]Recommendation, error) { // Get product embedding doc, err := r.vectorStore.Get(ctx, productID) if err != nil { return nil, fmt.Errorf("get product: %w", err) }
// Find similar products results, err := r.vectorStore.SimilaritySearch(ctx, doc.Embedding, vectorstore.WithTopK(limit+1), // +1 to exclude self vectorstore.WithThreshold(0.7), ) if err != nil { return nil, fmt.Errorf("similarity search: %w", err) }
recommendations := make([]Recommendation, 0) for _, result := range results { // Skip the product itself if result.Metadata["product_id"].(string) == productID { continue }
recommendations = append(recommendations, Recommendation{ ProductID: result.Metadata["product_id"].(string), Name: result.Metadata["name"].(string), Category: result.Metadata["category"].(string), Price: result.Metadata["price"].(float64), Score: result.Metadata["score"].(float64), Reason: "Similar product", })
if len(recommendations) >= limit { break } }
return recommendations, nil}Hybrid Ranking
Section titled “Hybrid Ranking”Combine multiple signals for better recommendations:
package main
import ( "context")
type HybridRanker struct { weights map[string]float64}
func NewHybridRanker() *HybridRanker { return &HybridRanker{ weights: map[string]float64{ "semantic_similarity": 0.4, "popularity": 0.2, "user_affinity": 0.2, "freshness": 0.1, "price_match": 0.1, }, }}
func (h *HybridRanker) Rank(ctx context.Context, docs []schema.Document, userID string) []schema.Document { scored := make([]scoredDoc, len(docs))
for i, doc := range docs { score := 0.0
// Semantic similarity (from vector search) score += doc.Metadata["score"].(float64) * h.weights["semantic_similarity"]
// Popularity (view count, purchase count) popularity := h.calculatePopularity(doc) score += popularity * h.weights["popularity"]
// User affinity (category match, brand preference) affinity := h.calculateUserAffinity(ctx, userID, doc) score += affinity * h.weights["user_affinity"]
// Freshness (newer products ranked higher) freshness := h.calculateFreshness(doc) score += freshness * h.weights["freshness"]
// Price match (user's typical price range) priceMatch := h.calculatePriceMatch(ctx, userID, doc) score += priceMatch * h.weights["price_match"]
scored[i] = scoredDoc{ doc: doc, score: score, } }
// Sort by combined score sort.Slice(scored, func(i, j int) bool { return scored[i].score > scored[j].score })
result := make([]schema.Document, len(scored)) for i, s := range scored { result[i] = s.doc result[i].Metadata["final_score"] = s.score }
return result}
type scoredDoc struct { doc schema.Document score float64}
func (h *HybridRanker) calculatePopularity(doc schema.Document) float64 { // Implementation: combine view count, purchase count, rating return 0.5}
func (h *HybridRanker) calculateUserAffinity(ctx context.Context, userID string, doc schema.Document) float64 { // Implementation: check category preferences, brand loyalty return 0.5}
func (h *HybridRanker) calculateFreshness(doc schema.Document) float64 { // Implementation: time since product creation return 0.5}
func (h *HybridRanker) calculatePriceMatch(ctx context.Context, userID string, doc schema.Document) float64 { // Implementation: compare to user's typical price range return 0.5}Production Considerations
Section titled “Production Considerations”Observability
Section titled “Observability”Track recommendation quality and user engagement:
import ( "github.com/lookatitude/beluga-ai/o11y" "go.opentelemetry.io/otel/attribute")
func (r *RecommendationEngine) GetRecommendationsWithTracking(ctx context.Context, userID string, limit int) ([]Recommendation, error) { ctx, span := tracer.Start(ctx, "recommendation.get") defer span.End()
span.SetAttributes( attribute.String("user.id", userID), attribute.Int("limit", limit), )
start := time.Now() recommendations, err := r.GetRecommendations(ctx, userID, limit) duration := time.Since(start)
if err != nil { span.RecordError(err) return nil, err }
span.SetAttributes( attribute.Int("recommendations.count", len(recommendations)), attribute.Float64("duration.ms", float64(duration.Milliseconds())), )
meter.RecordHistogram(ctx, "recommendation.duration", duration.Milliseconds()) meter.IncrementCounter(ctx, "recommendations.generated")
return recommendations, nil}Caching
Section titled “Caching”Cache recommendations for frequent users:
import "github.com/lookatitude/beluga-ai/cache"
type CachedRecommendationEngine struct { RecommendationEngine cache cache.Cache}
func (c *CachedRecommendationEngine) GetRecommendations(ctx context.Context, userID string, limit int) ([]Recommendation, error) { cacheKey := fmt.Sprintf("rec:%s:%d", userID, limit)
// Check cache if cached, ok := c.cache.Get(ctx, cacheKey); ok { return cached.([]Recommendation), nil }
// Generate recommendations recommendations, err := c.RecommendationEngine.GetRecommendations(ctx, userID, limit) if err != nil { return nil, err }
// Cache for 5 minutes c.cache.Set(ctx, cacheKey, recommendations, 5*time.Minute)
return recommendations, nil}A/B Testing
Section titled “A/B Testing”Track recommendation strategy performance:
type ABTestingEngine struct { RecommendationEngine experiments map[string]RecommendationStrategy}
type RecommendationStrategy interface { GetRecommendations(ctx context.Context, userID string, limit int) ([]Recommendation, error)}
func (a *ABTestingEngine) GetRecommendations(ctx context.Context, userID string, limit int) ([]Recommendation, error) { // Assign user to experiment group variant := a.getVariant(userID)
strategy := a.experiments[variant] recommendations, err := strategy.GetRecommendations(ctx, userID, limit) if err != nil { return nil, err }
// Track which variant was served a.trackVariant(ctx, userID, variant)
return recommendations, nil}
func (a *ABTestingEngine) getVariant(userID string) string { // Hash-based assignment for consistent experience hash := hashUserID(userID) if hash%2 == 0 { return "semantic" } return "hybrid"}Batch Updates
Section titled “Batch Updates”Update product embeddings efficiently:
func (r *RecommendationEngine) IndexBatch(ctx context.Context, products []ProductMetadata) error { batchSize := 100
for i := 0; i < len(products); i += batchSize { end := min(i+batchSize, len(products)) batch := products[i:end]
// Generate descriptions descriptions := make([]string, len(batch)) for j, product := range batch { descriptions[j] = fmt.Sprintf( "Product: %s\nBrand: %s\nCategory: %s\nDescription: %s", product.Name, product.Brand, product.Category, product.Description, ) }
// Batch embed embeddings, err := r.embedder.EmbedBatch(ctx, descriptions) if err != nil { return err }
// Create documents docs := make([]schema.Document, len(batch)) for j, product := range batch { docs[j] = schema.Document{ Content: descriptions[j], Embedding: embeddings[j], Metadata: map[string]interface{}{ "product_id": product.ProductID, "name": product.Name, "category": product.Category, "price": product.Price, }, } }
// Batch store if err := r.vectorStore.Add(ctx, docs); err != nil { return err } }
return nil}Related Resources
Section titled “Related Resources”- Vector Store Guide for scaling strategies
- Embedding Guide for embedding optimization
- Knowledge QA System for similarity patterns
- Enterprise RAG for RAG pipeline patterns