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
- Verschlüsselung: PostgreSQL SSL, verschlüsselte Backups
- Zugriffskontrolle: Rollenbasierte Zugriffe (RBAC) in .NET
- Rate Limiting: Max 100 Anfragen/Minute pro Benutzer
- 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
Team von Programmierexperten, die sich auf moderne Webtechnologien spezialisiert haben.