Zum Inhalt springen
Technologien

RAG + Postgres (pgvector) im E-Commerce - Next.js + .NET

Veröffentlicht am:
·Autor: MDS Software Solutions Group
RAG + Postgres (pgvector) im E-Commerce - Next.js + .NET

RAG + Postgres (pgvector) im E-Commerce: Next.js + .NET

Künstliche Intelligenz verändert das Gesicht des E-Commerce, und RAG (Retrieval-Augmented Generation) in Kombination mit Vektordatenbanken eröffnet neue Möglichkeiten für Suche, Empfehlungen und Kundenservice. In diesem Artikel zeigen wir, wie man ein RAG-System auf Basis von PostgreSQL mit der pgvector-Erweiterung erstellt, integriert mit Next.js und .NET API.

Warum RAG im E-Commerce?

Die traditionelle Volltextsuche hat ihre Grenzen. Nutzer suchen oft nach Produkten mit natürlicher Sprache, Synonymen oder Funktionsbeschreibungen statt exakter Namen. RAG löst dieses Problem und bietet:

1. Semantische Produktsuche

Statt Keyword-Matching versteht das System die Absicht des Nutzers:

  • "günstige Laufschuhe" → findet Produkte beschrieben als "wirtschaftliche Sportschuhe"
  • "Laptop zum Programmieren" → passt Parameter (RAM, Prozessor) ohne wörtliche Phrasen
  • "Geschenk für Mama" → schlägt Kategorien basierend auf Kontext vor

2. Ähnliche Artikel

Die Vektordarstellung von Produkten ermöglicht das Finden wirklich ähnlicher Items - nicht nur nach Kategorie, sondern basierend auf semantischer Ähnlichkeit von Beschreibungen, Parametern und Bewertungen.

3. Intelligente FAQ und Bestellungs-Q&A

RAG ermöglicht den Aufbau eines Chatbots, der:

  • Fragen zum Bestellstatus basierend auf Datenbankdaten beantwortet
  • FAQ-Antworten unter Verwendung von Wissen aus Dokumentation und Historie generiert
  • Support-Last durch automatische Antworten reduziert

4. Personalisierte Empfehlungen

Embeddings ermöglichen fortgeschrittene Empfehlungen unter Berücksichtigung von Browser-Historie, Präferenzen und Einkaufskontext.

Systemarchitektur

Unser System besteht aus vier Hauptkomponenten:

Frontend: Next.js 15 App Router

  • Server Components für SEO und Performance
  • API Routes für Backend-Kommunikation
  • Streaming UI für progressive Ergebnisanzeige

Backend: .NET Minimal API

  • Embedding-Generierung (OpenAI/Azure/Local)
  • Ranking und Reranking der Ergebnisse
  • Orchestrierung von Datenbankabfragen

Datenbank: PostgreSQL 16 + pgvector

  • Speicherung von Vektor-Embeddings
  • HNSW-Index für schnelle Suche
  • Hybride Suche (BM25 + Vektor)

Cache: Redis

  • Caching von Abfrage-Embeddings
  • Speicherung von Suchergebnissen
  • Session-Speicher für Chatbot
┌─────────────┐      ┌─────────────────┐      ┌──────────────────┐
│  Next.js    │─────▶│  .NET API       │─────▶│  PostgreSQL      │
│  (UI/SSR)   │      │  (Embeddings)   │      │  + pgvector      │
└─────────────┘      └─────────────────┘      └──────────────────┘
       │                      │                         │
       │                      ▼                         │
       │              ┌─────────────┐                   │
       └─────────────▶│   Redis     │◀──────────────────┘
                      │   (Cache)   │
                      └─────────────┘

Schritt-für-Schritt-Implementierung

Schritt 1: PostgreSQL + pgvector Setup

Installieren Sie die pgvector-Erweiterung in PostgreSQL 16:

-- pgvector-Erweiterung aktivieren
CREATE EXTENSION IF NOT EXISTS vector;

-- Produkttabelle mit Embeddings
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    category VARCHAR(100),
    price DECIMAL(10, 2),
    metadata JSONB,
    -- Embedding-Vektor (OpenAI ada-002 = 1536 Dimensionen)
    embedding vector(1536),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- HNSW-Index für schnelle Vektorsuche
-- m = Anzahl der Verbindungen (16-64, höher = bessere Qualität, langsamerer Build)
-- ef_construction = dynamische Listengröße (64-200, höher = bessere Qualität)
CREATE INDEX ON products 
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- Index für hybride Suche (Text + Vektor)
CREATE INDEX ON products USING GIN(to_tsvector('english', name || ' ' || description));

-- Funktion zur Aktualisierung des Zeitstempels
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = CURRENT_TIMESTAMP;
    RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_products_updated_at 
    BEFORE UPDATE ON products 
    FOR EACH ROW 
    EXECUTE FUNCTION update_updated_at_column();

Schritt 2: Embedding-Generierungs-Pipeline

In der .NET API erstellen wir einen Service für die Generierung und Verwaltung von Embeddings:

// EmbeddingService.cs
using Azure.AI.OpenAI;
using Npgsql;
using System.Text.Json;

public class EmbeddingService
{
    private readonly OpenAIClient _openAIClient;
    private readonly IConfiguration _config;
    private readonly ILogger<EmbeddingService> _logger;
    private readonly string _connectionString;
    
    public EmbeddingService(OpenAIClient openAIClient, IConfiguration config, ILogger<EmbeddingService> logger)
    {
        _openAIClient = openAIClient;
        _config = config;
        _logger = logger;
        _connectionString = config.GetConnectionString("PostgreSQL");
    }
    
    // Embeddings für einzelnen Text generieren
    public async Task<float[]> GenerateEmbeddingAsync(string text)
    {
        var options = new EmbeddingsOptions("text-embedding-ada-002", new[] { text });
        var response = await _openAIClient.GetEmbeddingsAsync(options);
        
        return response.Value.Data[0].Embedding.ToArray();
    }
    
    // Batch-Verarbeitung für mehrere Produkte
    public async Task GenerateProductEmbeddingsAsync(int batchSize = 100)
    {
        await using var conn = new NpgsqlConnection(_connectionString);
        await conn.OpenAsync();
        
        // Produkte ohne Embeddings finden
        var query = @"
            SELECT id, name, description, category 
            FROM products 
            WHERE embedding IS NULL 
            LIMIT @batchSize";
            
        await using var cmd = new NpgsqlCommand(query, conn);
        cmd.Parameters.AddWithValue("batchSize", batchSize);
        
        await using var reader = await cmd.ExecuteReaderAsync();
        var products = new List<(int id, string text)>();
        
        while (await reader.ReadAsync())
        {
            var id = reader.GetInt32(0);
            var name = reader.GetString(1);
            var description = reader.IsDBNull(2) ? "" : reader.GetString(2);
            var category = reader.IsDBNull(3) ? "" : reader.GetString(3);
            
            // Produktinformationen zu einem Text kombinieren
            var combinedText = $"{name}. {description}. Kategorie: {category}";
            products.Add((id, combinedText));
        }
        
        await reader.CloseAsync();
        
        // Embeddings für jedes Produkt generieren
        foreach (var (id, text) in products)
        {
            try
            {
                var embedding = await GenerateEmbeddingAsync(text);
                
                // Embedding in Datenbank speichern
                await UpdateProductEmbeddingAsync(conn, id, embedding);
                
                // Rate Limiting - OpenAI hat 3000 RPM Limits
                await Task.Delay(20); // ~50 req/sec
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Fehler beim Generieren des Embeddings für Produkt {ProductId}", id);
            }
        }
    }
    
    private async Task UpdateProductEmbeddingAsync(
        NpgsqlConnection conn, 
        int productId, 
        float[] embedding)
    {
        var updateQuery = "UPDATE products SET embedding = @embedding WHERE id = @id";
        await using var cmd = new NpgsqlCommand(updateQuery, conn);
        cmd.Parameters.AddWithValue("id", productId);
        cmd.Parameters.AddWithValue("embedding", embedding);
        
        await cmd.ExecuteNonQueryAsync();
    }
}

Schritt 3: Semantischer Such-Endpunkt

Erstellen Sie einen Minimal API Endpunkt für die Suche:

// Program.cs - Minimal API
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Services hinzufügen
builder.Services.AddSingleton<OpenAIClient>(sp => 
    new OpenAIClient(builder.Configuration["OpenAI:ApiKey"]));
builder.Services.AddScoped<EmbeddingService>();
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration["Redis:Connection"];
});

var app = builder.Build();

// Semantischer Such-Endpunkt
app.MapPost("/api/search", async (
    [FromBody] SearchRequest request,
    [FromServices] EmbeddingService embeddingService,
    [FromServices] IDistributedCache cache,
    [FromServices] IConfiguration config) =>
{
    try
    {
        // 1. Cache prüfen
        var cacheKey = $"search:{request.Query}:{request.Limit}";
        var cachedResult = await cache.GetStringAsync(cacheKey);
        
        if (!string.IsNullOrEmpty(cachedResult))
        {
            return Results.Ok(JsonSerializer.Deserialize<SearchResponse>(cachedResult));
        }
        
        // 2. Embedding für Abfrage generieren
        var queryEmbedding = await embeddingService.GenerateEmbeddingAsync(request.Query);
        
        // 3. Ähnliche Produkte in PostgreSQL suchen
        var connectionString = config.GetConnectionString("PostgreSQL");
        await using var conn = new NpgsqlConnection(connectionString);
        await conn.OpenAsync();
        
        // Vektorsuche mit <-> Operator (Kosinus-Distanz)
        var query = @"
            SELECT 
                id,
                name,
                description,
                price,
                category,
                metadata,
                1 - (embedding <-> @queryEmbedding) as similarity
            FROM products
            WHERE embedding IS NOT NULL
            ORDER BY embedding <-> @queryEmbedding
            LIMIT @limit";
        
        await using var cmd = new NpgsqlCommand(query, conn);
        cmd.Parameters.AddWithValue("queryEmbedding", queryEmbedding);
        cmd.Parameters.AddWithValue("limit", request.Limit);
        
        var results = new List<ProductResult>();
        await using var reader = await cmd.ExecuteReaderAsync();
        
        while (await reader.ReadAsync())
        {
            results.Add(new ProductResult
            {
                Id = reader.GetInt32(0),
                Name = reader.GetString(1),
                Description = reader.IsDBNull(2) ? null : reader.GetString(2),
                Price = reader.GetDecimal(3),
                Category = reader.IsDBNull(4) ? null : reader.GetString(4),
                Metadata = reader.IsDBNull(5) ? null : 
                    JsonSerializer.Deserialize<Dictionary<string, object>>(reader.GetString(5)),
                Similarity = reader.GetFloat(6)
            });
        }
        
        var response = new SearchResponse { Results = results };
        
        // 4. Im Cache speichern (5 Minuten)
        await cache.SetStringAsync(
            cacheKey, 
            JsonSerializer.Serialize(response),
            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }
        );
        
        return Results.Ok(response);
    }
    catch (Exception ex)
    {
        return Results.Problem($"Suche fehlgeschlagen: {ex.Message}");
    }
});

app.Run();

// DTOs
public record SearchRequest(string Query, int Limit = 20);
public record SearchResponse { public List<ProductResult> Results { get; set; } }
public record ProductResult
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string? Description { get; set; }
    public decimal Price { get; set; }
    public string? Category { get; set; }
    public Dictionary<string, object>? Metadata { get; set; }
    public float Similarity { get; set; }
}

Schritt 4: Next.js Integration

In Next.js erstellen wir API Route und Such-Komponente:

// app/api/products/search/route.ts
import { NextRequest, NextResponse } from 'next/server';

const DOTNET_API_URL = process.env.DOTNET_API_URL || 'http://localhost:5000';

export async function POST(request: NextRequest) {
  try {
    const { query, limit = 20 } = await request.json();
    
    if (!query || typeof query !== 'string') {
      return NextResponse.json(
        { error: 'Abfrage erforderlich' },
        { status: 400 }
      );
    }
    
    // .NET API aufrufen
    const response = await fetch(`${DOTNET_API_URL}/api/search`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ query, limit }),
    });
    
    if (!response.ok) {
      throw new Error(`API-Fehler: ${response.statusText}`);
    }
    
    const data = await response.json();
    
    return NextResponse.json(data);
  } catch (error) {
    console.error('Suchfehler:', error);
    return NextResponse.json(
      { error: 'Suche fehlgeschlagen' },
      { status: 500 }
    );
  }
}

Embedding-Aktualisierungsstrategien

Embeddings müssen aktuell bleiben, wenn sich Produktdaten ändern:

1. Trigger-basierte Aktualisierungen

-- Trigger zum Markieren von Produkten zur Neueinbettung
CREATE OR REPLACE FUNCTION mark_for_reembedding()
RETURNS TRIGGER AS $$
BEGIN
    -- Wenn Name, Beschreibung oder Kategorie geändert wurde, Embedding zurücksetzen
    IF (NEW.name != OLD.name OR 
        NEW.description != OLD.description OR 
        NEW.category != OLD.category) THEN
        NEW.embedding = NULL;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER products_reembed_trigger
    BEFORE UPDATE ON products
    FOR EACH ROW
    EXECUTE FUNCTION mark_for_reembedding();

2. Batch-Verarbeitung mit Warteschlange

Verwenden Sie Redis Queue oder Background Job für asynchrone Verarbeitung:

// BackgroundEmbeddingService.cs
public class BackgroundEmbeddingService : BackgroundService
{
    private readonly IServiceProvider _services;
    private readonly ILogger<BackgroundEmbeddingService> _logger;
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = _services.CreateScope();
            var embeddingService = scope.ServiceProvider.GetRequiredService<EmbeddingService>();
            
            try
            {
                // 100 Produkte alle 5 Minuten verarbeiten
                await embeddingService.GenerateProductEmbeddingsAsync(100);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Fehler im Hintergrund-Embedding-Service");
            }
            
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

Hybride Suche: BM25 + Vektor

Für beste Ergebnisse kombinieren Sie Volltextsuche mit Vektorsuche:

-- Hybride Suche mit Gewichten
WITH vector_results AS (
    SELECT 
        id,
        name,
        description,
        price,
        (1 - (embedding <-> @queryEmbedding)) * 0.7 as vector_score
    FROM products
    WHERE embedding IS NOT NULL
    ORDER BY embedding <-> @queryEmbedding
    LIMIT 50
),
text_results AS (
    SELECT 
        id,
        ts_rank(to_tsvector('english', name || ' ' || description), 
                plainto_tsquery('english', @queryText)) * 0.3 as text_score
    FROM products
    WHERE to_tsvector('english', name || ' ' || description) @@ 
          plainto_tsquery('english', @queryText)
)
SELECT 
    p.id,
    p.name,
    p.description,
    p.price,
    COALESCE(v.vector_score, 0) + COALESCE(t.text_score, 0) as combined_score
FROM products p
LEFT JOIN vector_results v ON p.id = v.id
LEFT JOIN text_results t ON p.id = t.id
WHERE v.id IS NOT NULL OR t.id IS NOT NULL
ORDER BY combined_score DESC
LIMIT 20;

UX und Kostenoptimierung

Latenz (p95)

  • Ziel: < 500ms für 95% der Abfragen
  • Cache-Trefferquote: > 60% (Redis)
  • HNSW ef_search: 40-100 (höher = bessere Qualität, langsamer)

Embedding-Kosten

Für OpenAI text-embedding-ada-002:

  • Preis: $0.0001 / 1K Tokens
  • Beispiel: 10.000 Produkte × 200 Tokens Durchschnitt = 2M Tokens = $0.20
  • Aktualisierungen: ~$0.01 täglich bei 5% geänderten Produkten

Datensicherheit

  1. Verschlüsselung: PostgreSQL SSL, verschlüsselte Backups
  2. Zugriffskontrolle: Rollenbasierte Zugriffe (RBAC) in .NET
  3. Rate Limiting: Max 100 Anfragen/Minute pro Benutzer
  4. Eingabevalidierung: Abfragevalidierung, max 500 Zeichen

Bereitstellungs-Checkliste

  • [ ] PostgreSQL 16 + pgvector installieren
  • [ ] Tabellen und Indizes erstellen (HNSW)
  • [ ] .NET API mit OpenAI Client konfigurieren
  • [ ] EmbeddingService implementieren
  • [ ] Batch-Verarbeitung für bestehende Produkte ausführen
  • [ ] Redis Cache konfigurieren
  • [ ] Next.js API Routes erstellen
  • [ ] Such-UI-Komponente hinzufügen
  • [ ] Background Job für Aktualisierungen einrichten
  • [ ] Latenz und Genauigkeit testen
  • [ ] Kosten und API-Nutzung überwachen
  • [ ] Logging und Metriken hinzufügen (Application Insights / Grafana)

Anwendungsfälle

1. FAQ Chatbot

Dokumentations-Embeddings + RAG = intelligenter Bot, der Kundenfragen beantwortet.

2. "Das könnte Ihnen gefallen"-Empfehlungen

-- Produkte ähnlich zu ID=123
SELECT id, name, price,
       1 - (embedding <-> (SELECT embedding FROM products WHERE id = 123)) as similarity
FROM products
WHERE id != 123 AND embedding IS NOT NULL
ORDER BY embedding <-> (SELECT embedding FROM products WHERE id = 123)
LIMIT 6;

3. Automatische Kategorisierung

Neue Produkte können automatisch kategorisiert werden, indem ihre Embeddings mit bestehenden Kategorien verglichen werden.

Zusammenfassung

RAG mit PostgreSQL pgvector bietet leistungsstarke Funktionen für E-Commerce:

  • Semantische Suche statt Keyword-Matching
  • Skalierbar auf Millionen von Produkten (HNSW-Index)
  • Niedrige Kosten im Vergleich zu verwalteten Vektor-DBs
  • Einfache Integration mit bestehendem Next.js + .NET Stack

Bereit zur Implementierung? Kontaktieren Sie uns für ein Such-Audit oder Proof-of-Concept in 1-2 Wochen.

Weitere Ressourcen

Autor
MDS Software Solutions Group

Team von Programmierexperten, die sich auf moderne Webtechnologien spezialisiert haben.

RAG + Postgres (pgvector) im E-Commerce - Next.js + .NET | MDS Software Solutions Group | MDS Software Solutions Group