Suchdienst-Architektur: Eigenständige Flask-Anwendung mit Valkey

Dieser Artikel erklärt, wie unser Suchdienst als eigenständige Flask-Anwendung auf einem separaten Server läuft und dabei Valkey (Redis-Fork) für hochperformante Vektorsuche, Caching und Autovervollständigung nutzt.

Das Problem: Suchleistung und Skalierbarkeit

Suchoperationen sind rechenintensiv:

  • Filterextraktion: 2.500+ Phrasen mit jeder Abfrage abgleichen

  • Verwandte Suchen: Ähnlichkeit über 65.000 Abfragen berechnen

  • Autovervollständigung: Präfix-Matching auf 65.000 Abfragen

  • Produktfilterung: 5.000+ Produkte nach mehreren Kriterien filtern

Die Ausführung dieser Operationen auf dem Haupt-Webserver führt zu:

  • Langsamen Seitenladezeiten: Suchvorgänge blockieren andere Anfragen

  • Speicherbelastung: Embeddings haben einen enormen Speicherbedarf

  • CPU-Konkurrenz: Ähnlichkeitsberechnung ist CPU-intensiv

  • Skalierungsproblemen: Suche kann nicht unabhängig skaliert werden

Wir benötigen einen dedizierten Suchdienst, der unabhängig skaliert werden kann.

Die Lösung: Eigenständiger Suchdienst

Wir betreiben eine separate Flask-Anwendung auf einem dedizierten Server:

Haupt-Webserver
    ↓ HTTP-API-Aufrufe
Suchdienst
    ↓ Valkey-Abfragen
Valkey-Server

Diese Architektur bietet:

  • Unabhängige Skalierung: Suchdienst skalieren, ohne den Haupt-Webserver zu beeinflussen

  • Ressourcenisolation: Suchoperationen beeinträchtigen den Haupt-Webserver nicht

  • Caching: Valkey cached Ergebnisse für schnelle wiederholte Abfragen

  • Hohe Verfügbarkeit: Suchdienst kann neu starten, ohne den Haupt-Webserver zu beeinflussen

Suchdienst-Komponenten

1. Filter-Extraktions-API

  • Endpunkt: /api/extract_filters

  • Zweck: Strukturierte Filter aus natürlichsprachigen Abfragen extrahieren

  • Beispiel:

GET /api/extract_filters?q=mini+pc+16gb+ram

Antwort:
{
  "Form Factor": "Mini PC",
  "Main Memory": "16"
}

Implementierung:

Caching: Phrasenzuordnungen in Valkey gecached (30 Tage TTL)

2. Verwandte-Suchen-API

  • Endpunkt: /api/related

  • Zweck: Semantisch ähnliche Abfragen mittels Vektorsuche finden

  • Beispiel:

POST /api/related
{
  "query": "mini pc",
  "limit": 10
}

Antwort:
{
  "related": [
    {"query": "small computer", "similarity": 0.92},
    {"query": "compact desktop", "similarity": 0.89},
    {"query": "mini pc 8gb", "similarity": 0.87}
  ]
}

Implementierung:

  • Embedde Abfrage mit all-mpnet-base-v2

  • Frage Valkey RediSearch nach nächsten Nachbarn ab

  • Gib die Top-N-Ergebnisse sortiert nach Ähnlichkeit zurück

Caching: Ergebnisse in Valkey gecached (7 Tage TTL)

3. Autovervollständigungs-API

  • Endpunkt: /api/autocomplete

  • Zweck: Abfragen vorschlagen, während der Benutzer tippt

  • Beispiel:

GET /api/autocomplete?q=mini+p&limit=5

Antwort:
{
  "suggestions": [
    "mini pc",
    "mini pc 16gb",
    "mini pc 8gb ram",
    "mini pc fanless",
    "mini pc windows 11"
  ]
}

Implementierung:

  • Frage Valkey RediSearch mit Präfix-Matching ab

  • Sortiere nach Popularität (Impression + Klick-Score)

  • Gib die Top-N-Vorschläge zurück

Caching: Autovervollständigungs-Index in Valkey (täglich aktualisiert)

4. Beliebte-Abfragen-API

  • Endpunkt: /api/popular

  • Zweck: Die beliebtesten Abfragen abrufen

  • Beispiel:

GET /api/popular?limit=10

Antwort:
{
  "queries": [
    "mini pc",
    "thin client",
    "industrial pc",
    "all in one pc"
  ]
}

Implementierung:

  • Lade Abfragen aus Valkey oder JSON

  • Sortiere nach Traffic-Score (Impressionen + Klicks)

  • Gib die Top-N-Abfragen zurück

Caching: Beliebte Abfragen in Valkey gecached (30 Tage TTL)

Valkey-Integration

Valkey ist ein Redis-Fork, der Folgendes bietet:

  • Vektorsuche: RediSearch-Modul für Ähnlichkeitssuche

  • Caching: Schneller In-Memory-Key-Value-Store

  • Autovervollständigung: Präfix-Matching mit sortierten Mengen

  • Persistenz: AOF (Append-Only File) für Dauerhaftigkeit

Vektorsuche mit RediSearch

Wir nutzen Valkeys RediSearch-Modul für Vektor-Ähnlichkeitssuche:

Index-Erstellung:

client.ft("queries_idx").create_index([
    VectorField("embedding", "FLAT", {
        "TYPE": "FLOAT32",
        "DIM": 768,
        "DISTANCE_METRIC": "COSINE"
    }),
    TextField("query"),
    NumericField("score")
])

Vektorsuche:

query_embedding = model.encode(query)
results = client.ft("queries_idx").search(
    Query("*=>[KNN 10 @embedding $vec AS score]")
    .sort_by("score")
    .return_fields("query", "score")
    .dialect(2),
    query_params={"vec": query_embedding.tobytes()}
)

Dies gibt die 10 nächsten Nachbarn nach Kosinus-Ähnlichkeit zurück.

Caching-Strategie

Wir cachen mehrere Datentypen in Valkey:

Phrasenzuordnungen (30 Tage TTL):

client.setex(
    "seo:phrase_mappings",
    30 * 24 * 3600,
    json.dumps(phrase_mappings)
)

Verwandte Suchen (7 Tage TTL):

cache_key = f"related:{query_hash}"
client.setex(cache_key, 7 * 24 * 3600, json.dumps(results))

Beliebte Abfragen (30 Tage TTL):

client.setex(
    "seo:popular_queries",
    30 * 24 * 3600,
    json.dumps(popular_queries)
)

Autovervollständigungs-Index (täglich aktualisiert):

for query, score in queries:
    client.zadd("autocomplete:mini", {query: score})

Autovervollständigung mit sortierten Mengen

Wir nutzen Valkey sortierte Mengen für die Autovervollständigung:

Index-Struktur:

autocomplete:m     → ["mini pc": 5000, "mini computer": 3000]
autocomplete:mi    → ["mini pc": 5000, "mini computer": 3000]
autocomplete:min   → ["mini pc": 5000, "mini computer": 3000]
autocomplete:mini  → ["mini pc": 5000, "mini computer": 3000]

Präfix-Lookup:

prefix = "mini"
results = client.zrevrange(f"autocomplete:{prefix}", 0, 9, withscores=True)

Dies gibt die Top-10-Abfragen zurück, die mit "mini" beginnen, sortiert nach Score.

API-Kommunikation

Der Haupt-Webserver ruft den Suchdienst über HTTP auf:

Filterextraktion

from app.shared.filter_service import extract_filters_from_query

filters = extract_filters_from_query("mini pc 16gb ram")
# Interner Aufruf: GET SEARCH_SERVICE_URL/api/extract_filters?q=...

Verwandte Suchen

import requests

response = requests.post(
    "SEARCH_SERVICE_URL/api/related",
    json={"query": "mini pc", "limit": 10},
    timeout=2
)
related = response.json()["related"]

Autovervollständigung

response = requests.get(
    "SEARCH_SERVICE_URL/api/autocomplete",
    params={"q": "mini p", "limit": 5},
    timeout=1
)
suggestions = response.json()["suggestions"]

Fehlerbehandlung und Fallbacks

Der Haupt-Webserver behandelt Ausfälle des Suchdienstes elegant:

try:
    filters = extract_filters_from_query(query)
except Exception as e:
    logger.error(f"Search service failed: {e}")
    filters = {}  # Fallback auf leere Filter

Dies stellt sicher, dass der Haupt-Webserver auch dann weiterfunktioniert, wenn der Suchdienst ausfällt.


### Netzwerkkonfiguration

Alle Server befinden sich in einem privaten Netzwerk:

- **Haupt-Webserver**: Kann auf Suchdienst und Valkey zugreifen

- **Suchdienst**: Kann auf Valkey zugreifen

- **Valkey**: Nur vom Haupt-Webserver und Suchdienst erreichbar

Kein externer Zugriff auf Suchdienst oder Valkey.

## Integration in die SEO-Pipeline

Der Suchdienst ist in die SEO-Pipeline integriert:

### Schritt 11: Migration zu Valkey

Die SEO-Pipeline lädt Daten in Valkey:

```python
# Lade Abfrage-Embeddings
for query, embedding in zip(queries, embeddings):
    client.hset(f"query:{query_hash}", mapping={
        "query": query,
        "embedding": embedding.tobytes(),
        "score": score
    })

# Erstelle RediSearch-Index
client.ft("queries_idx").create_index([...])

Siehe Valkey-Migration für Details.

Abfrage-Protokollierung

Der Suchdienst protokolliert Abfragen für die SEO-Pipeline:

log_entry = {
    "timestamp": datetime.now(timezone.utc).isoformat(),
    "query": query,
    "filters_extracted": filters,
    "results_count": len(results)
}
with open(SEO_LIVE_QUERIES_LOG, "a") as f:
    f.write(json.dumps(log_entry) + "\n")

Diese Protokolle speisen Schritt 1d zurück: Live-Abfragen abrufen.

Referenzen

Technische Konzepte

Verwandte Artikel

Zusammenfassung

Unser Suchdienst läuft als eigenständige Flask-Anwendung auf einem separaten Server:

Architektur:

  • Eigenständige Flask-App auf dediziertem Server

  • Valkey (Redis-Fork) für Caching und Vektorsuche

  • HTTP-API für Kommunikation mit Haupt-Webserver

APIs:

  • /api/extract_filters - Filter aus Abfragen extrahieren

  • /api/related - Ähnliche Abfragen finden (Vektorsuche)

  • /api/autocomplete - Abfragen vorschlagen (Präfix-Matching)

  • /api/popular - Beliebte Abfragen abrufen

Valkey-Features:

  • Vektorsuche (RediSearch-Modul)

  • Caching (30 Tage TTL für Phrasenzuordnungen)

  • Autovervollständigung (sortierte Mengen)

  • Persistenz (AOF)

Vorteile:

  • Unabhängige Skalierung

  • Ressourcenisolation

  • Hohe Leistung (Valkey-Caching)

  • Fehlertoleranz (graceful degradation)

Diese Architektur ermöglicht schnelle, skalierbare Suche bei gleichzeitig reaktionsschnellem Haupt-Webserver.


← Zurück zum Dokumentationsindex